From 6a1641e0b7e790c2326bff2932802f973cd95345 Mon Sep 17 00:00:00 2001 From: iChris4 Date: Sat, 19 Sep 2026 22:57:49 +0200 Subject: [PATCH] Implement GX Thread for Asynchronous Rendering - Introduced a new GX thread to handle the rendering pipeline, allowing the game thread to post commands without blocking. - Added `gx_thread.h` and `gx_thread.cpp` to manage the command ring buffer and thread synchronization. - Updated `vi.cpp` to utilize the GX thread for rendering tasks, improving frame pacing and responsiveness. - Modified `main.cpp` to configure and start the GX thread, ensuring it integrates with the existing rendering workflow. - Enhanced `openxr_integration.cpp` to register the GX thread with OpenXR for better performance in VR scenarios. - Refactored `settings_overlay.cpp` to remove unnecessary waits for the frame worker, as the overlay now draws directly into the game thread's ImGui frame. - Improved error handling and logging in the GX thread to capture exceptions during command execution. --- .../java/org/wiicompiled/quest/GameStorage.kt | 2 +- .../quest/launcher/SettingsPage.kt | 5 + android/app/src/main/res/values/strings.xml | 2 + aurora-main/include/aurora/aurora.h | 11 + aurora-main/include/aurora/imgui.h | 8 + aurora-main/lib/aurora.cpp | 107 +++-- aurora-main/lib/imgui.cpp | 78 +++- aurora-main/lib/imgui.hpp | 14 + docs/quest-port.md | 38 +- runtime/include/aurora_events.h | 7 +- runtime/include/gx_thread.h | 106 +++++ runtime/include/runtime_config.h | 16 + runtime/src/hle/gx/gx_copy.cpp | 72 ++-- runtime/src/hle/gx/gx_dl.cpp | 75 ++-- runtime/src/hle/gx/gx_egg.cpp | 6 +- runtime/src/hle/gx/gx_fifo.cpp | 61 ++- runtime/src/hle/gx/gx_frame.cpp | 5 +- runtime/src/hle/gx/gx_indirect.cpp | 35 +- runtime/src/hle/gx/gx_init.cpp | 30 +- runtime/src/hle/gx/gx_internal.h | 55 ++- runtime/src/hle/gx/gx_lighting.cpp | 58 ++- runtime/src/hle/gx/gx_objects.cpp | 120 +----- runtime/src/hle/gx/gx_pixel.cpp | 58 +-- runtime/src/hle/gx/gx_stubs.cpp | 35 +- runtime/src/hle/gx/gx_tev.cpp | 65 +-- runtime/src/hle/gx/gx_texture.cpp | 276 +++++++++---- runtime/src/hle/gx/gx_thread.cpp | 369 ++++++++++++++++++ runtime/src/hle/gx/gx_transform.cpp | 74 ++-- runtime/src/hle/gx/gx_vertex.cpp | 112 ++++-- runtime/src/hle/vi.cpp | 108 +++-- runtime/src/main.cpp | 26 ++ runtime/src/settings_overlay.cpp | 6 +- runtime/src/vr/openxr_integration.cpp | 19 + 33 files changed, 1532 insertions(+), 527 deletions(-) create mode 100644 runtime/include/gx_thread.h create mode 100644 runtime/src/hle/gx/gx_thread.cpp diff --git a/android/app/src/main/java/org/wiicompiled/quest/GameStorage.kt b/android/app/src/main/java/org/wiicompiled/quest/GameStorage.kt index e535c3e..3a52a9c 100644 --- a/android/app/src/main/java/org/wiicompiled/quest/GameStorage.kt +++ b/android/app/src/main/java/org/wiicompiled/quest/GameStorage.kt @@ -78,7 +78,7 @@ object GameStorage { // Written line by line: trimIndent runs after interpolation, so an interpolated line // would take the indent off every other one. val lines = mutableListOf( - "# WiiCompiled Quest configuration. Edit with the launcher, the in-game panel or adb pull/push.", + "# WiiCompiled Quest configuration. Edit with the launcher or the in-game panel. After an adb push, run chmod 664 on it or the app can no longer save settings.", "[paths]", "dvd_root = \"${discDirectory(context).absolutePath}\"", ) diff --git a/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt b/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt index 2ba8ee8..523f346 100644 --- a/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt +++ b/android/app/src/main/java/org/wiicompiled/quest/launcher/SettingsPage.kt @@ -227,6 +227,11 @@ class SettingsPage( read = { it.bool("video", "skip_unready_pipelines") ?: true }, write = { c, value -> c.setBool("video", "skip_unready_pipelines", value) }, ) + toggle( + R.string.graphics_gx_thread, R.string.graphics_gx_thread_helper, + read = { it.bool("video", "gx_thread") ?: true }, + write = { c, value -> c.setBool("video", "gx_thread", value) }, + ) } } diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 2337104..771545f 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -188,6 +188,8 @@ Bloom\'s bright glow reads poorly in a headset, so it starts off. Prevent shader stutters Skips a draw for a moment while its shader compiles instead of pausing the game. + Graphics thread + Prepares the drawing on a second CPU core so busy scenes keep their speed. Turn off only to compare against the single-threaded path. Takes effect on the next launch. Controllers diff --git a/aurora-main/include/aurora/aurora.h b/aurora-main/include/aurora/aurora.h index 44b882b..a1e1e0b 100644 --- a/aurora-main/include/aurora/aurora.h +++ b/aurora-main/include/aurora/aurora.h @@ -218,6 +218,17 @@ void aurora_end_frame(); // Seal the current frame with an opaque application safety tag. Aurora rejects // an immersive provider packet unless its contentTag matches this exact frame. void aurora_end_frame_tagged(uint64_t contentTag); +// aurora_end_frame_tagged() plus the host-owned ImGui frame to present with it (the handle from +// aurora_imgui_host_frame_end(), which this call consumes; NULL presents no host ImGui frame). +void aurora_end_frame_ex(uint64_t contentTag, void* imguiFrame); +// When the host pumps SDL events itself (aurora_update() on the window's thread) and drives +// begin/end frame from another thread, this stops those calls from pumping events. +void aurora_set_host_event_pump(bool hostPumps); +typedef void (*AuroraFrameLogCallback)(char* buffer, uint32_t bufferSize, double windowSeconds, + uint32_t frames); +// Called with each five-second frame-rate log window (where that log is enabled); a non-empty +// buffer is logged as one extra line. +void aurora_set_frame_log_callback(AuroraFrameLogCallback callback); /** * Relocates the immersive camera for the frame about to be sealed. * diff --git a/aurora-main/include/aurora/imgui.h b/aurora-main/include/aurora/imgui.h index 57fe838..81d7df6 100644 --- a/aurora-main/include/aurora/imgui.h +++ b/aurora-main/include/aurora/imgui.h @@ -25,6 +25,14 @@ ImTextureID aurora_imgui_add_texture(uint32_t width, uint32_t height, const void // producer before the frame is sealed, and leave the draw data untouched until the frame worker is // done with that frame (aurora_wait_for_frame_worker). Null hides the panel. void aurora_imgui_set_stereo_overlay(ImDrawData* drawData, float widthFraction); +// Host-owned ImGui frames for the desktop overlay. Begin starts the next frame on the calling +// thread (which must be the window's thread, since the SDL backend reads the window there); end +// renders it and returns a handle to a private copy of its draw data, which aurora_end_frame_ex() +// consumes. A handle that is never presented is freed with aurora_imgui_host_frame_release(). +// From the first begin on, aurora no longer starts ImGui frames itself. +void aurora_imgui_host_frame_begin(void); +void* aurora_imgui_host_frame_end(void); +void aurora_imgui_host_frame_release(void* imguiFrame); #ifdef __cplusplus } diff --git a/aurora-main/lib/aurora.cpp b/aurora-main/lib/aurora.cpp index 9744665..95ecf97 100644 --- a/aurora-main/lib/aurora.cpp +++ b/aurora-main/lib/aurora.cpp @@ -69,6 +69,9 @@ AuroraConfig g_config; uint32_t g_sdlCustomEventsStart; char g_gameName[4]; std::atomic g_frameWorkerWaitCallback{nullptr}; +// aurora_set_host_event_pump(): the host pumps SDL itself, from the window's thread. +std::atomic_bool g_hostEventPump{false}; +std::atomic g_frameLogCallback{nullptr}; // Presentation schedule for the frame being sealed, set by the producer. Jobs carry absolute // deadlines derived from it, so the presenter cannot drift. Zero means present when ready. std::atomic g_presentScheduleBaseNanos{0}; @@ -266,7 +269,8 @@ enum class ImGuiFramePolicy { bool begin_frame_impl(bool pumpEvents, ImGuiFramePolicy imguiPolicy = ImGuiFramePolicy::Immediate, bool* imguiNewFrameOwed = nullptr) noexcept; bool begin_frame_render_state_impl(ImGuiFramePolicy imguiPolicy, bool* imguiNewFrameOwed) noexcept; -void end_frame_impl(bool pumpEvents, bool drainFifo, uint64_t contentTag, const StereoSceneAnchor& sceneAnchor) noexcept; +void end_frame_impl(bool pumpEvents, bool drainFifo, uint64_t contentTag, const StereoSceneAnchor& sceneAnchor, + imgui::HostFramePtr hostImGuiFrame) noexcept; // The two publication points of a frame-worker cycle, cleared together under `mutex`. Sealed: // producer-shared renderer state is free again. Done: slots encoded, presented, ImGui restarted. @@ -288,6 +292,7 @@ struct FrameWorkerState { // belong to that exact queued frame, not to the producer's next frame. uint64_t contentTag = AURORA_STEREO_CONTENT_TAG_UNKNOWN; StereoSceneAnchor sceneAnchor{}; + imgui::HostFramePtr hostImGuiFrame; // Readiness is polled thousands of times per frame, so these flags double as a publication // barrier. `sealed` is released before `ready`, and both are cleared under `mutex`. std::atomic_bool sealed{true}; @@ -338,7 +343,7 @@ bool frame_worker_requested() noexcept { #ifdef AURORA_ENABLE_GX // Returns false when a stop request was observed mid-cycle. -bool run_frame_worker_cycle(gfx::SealedFrame& sealedFrame, uint64_t contentTag, +bool run_frame_worker_cycle(gfx::SealedFrame& sealedFrame, uint64_t contentTag, imgui::HostFramePtr hostImGuiFrame, const StereoSceneAnchor& sceneAnchor) noexcept; void run_retained_stereo_frame(gfx::SealedFrame& sealedFrame) noexcept; #endif @@ -361,6 +366,7 @@ void frame_worker_main() noexcept { for (;;) { uint64_t contentTag = AURORA_STEREO_CONTENT_TAG_UNKNOWN; StereoSceneAnchor sceneAnchor{}; + imgui::HostFramePtr hostImGuiFrame; bool stereoOnly = false; { std::unique_lock lock(g_frameWorker.mutex); @@ -377,6 +383,7 @@ void frame_worker_main() noexcept { g_frameWorker.contentTag = AURORA_STEREO_CONTENT_TAG_UNKNOWN; sceneAnchor = g_frameWorker.sceneAnchor; g_frameWorker.sceneAnchor = {}; + hostImGuiFrame = std::move(g_frameWorker.hostImGuiFrame); g_frameWorker.jobPending = false; } @@ -396,7 +403,7 @@ void frame_worker_main() noexcept { g_frameWorker.cv.notify_all(); continue; } - if (!run_frame_worker_cycle(sealedFrame, contentTag, sceneAnchor)) { + if (!run_frame_worker_cycle(sealedFrame, contentTag, std::move(hostImGuiFrame), sceneAnchor)) { break; } #else @@ -1556,7 +1563,7 @@ void publish_stereo_screen_aspects(const webgpu::PresentSource& presentSource, c // gfx::begin_frame() may already have cleared the display-copy override. void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder, const webgpu::PresentSource& presentSource, const PresentationImage& image, bool includeImGui, - MirrorPlan plan = MirrorPlan::Mono) { + MirrorPlan plan = MirrorPlan::Mono, const ImDrawData* hostImGuiData = nullptr) { ZoneScoped; auto viewport = webgpu::calculate_present_viewport(image.texture.size.width, image.texture.size.height, presentSource.size.width, presentSource.size.height); @@ -1635,7 +1642,11 @@ void encode_presentation_snapshot(const wgpu::CommandEncoder& encoder, const web const auto pass = encoder.BeginRenderPass(&renderPassDescriptor); pass.SetViewport(0.f, 0.f, static_cast(image.texture.size.width), static_cast(image.texture.size.height), 0.f, 1.f); - imgui::render(pass); + if (hostImGuiData != nullptr) { + imgui::render(pass, hostImGuiData); + } else { + imgui::render(pass); + } pass.End(); } } @@ -1678,7 +1689,7 @@ bool begin_frame_impl(bool pumpEvents, ImGuiFramePolicy imguiPolicy, bool* imgui ZoneScoped; #ifdef AURORA_ENABLE_GX webgpu::fail_if_device_lost(); - if (pumpEvents) { + if (pumpEvents && !g_hostEventPump.load(std::memory_order_acquire)) { window::pump_events(); } const bool surfaceReconfigurePending = g_surfaceReconfigurePending.load(std::memory_order_acquire); @@ -1735,7 +1746,9 @@ bool begin_frame_render_state_impl(ImGuiFramePolicy imguiPolicy, bool* imguiNewF std::lock_guard gpuLock(g_rendererGpuMutex); // Note the debt before gfx::begin_frame() can fail: the synchronous path always started the // ImGui frame here, and the runtime's retry loop depends on that pairing. - if (imguiPolicy == ImGuiFramePolicy::Immediate) { + if (imgui::host_frames_active()) { + // The host starts its own ImGui frames (imgui::host_frame_begin). + } else if (imguiPolicy == ImGuiFramePolicy::Immediate) { imgui::new_frame(window::get_window_size()); } else if (imguiNewFrameOwed != nullptr) { *imguiNewFrameOwed = true; @@ -1771,8 +1784,13 @@ struct SealedFrameContext { std::optional stereoInput; bool retainStereo = false; imgui::StereoOverlay stereoOverlay; + imgui::HostFramePtr imguiFrame; }; +const ImDrawData* host_imgui_data(const SealedFrameContext& ctx) noexcept { + return ctx.imguiFrame ? imgui::host_frame_draw_data(*ctx.imguiFrame) : nullptr; +} + // Worker-owned scene state. A separate buffer generation check protects against // synchronous EFB submissions overwriting the retained frame's GPU data. struct RetainedStereoContext { @@ -1831,7 +1849,7 @@ void run_retained_stereo_frame(gfx::SealedFrame& sealedFrame) noexcept { // Phase 1: everything that touches producer-shared renderer state. Needs g_rendererGpuMutex and // a FIFO already drained into the recorded pass list. void seal_frame_locked(gfx::SealedFrame& sealedFrame, SealedFrameContext& ctx, uint64_t contentTag, - const StereoSceneAnchor& sceneAnchor) { + const StereoSceneAnchor& sceneAnchor, imgui::HostFramePtr hostImGuiFrame) { ZoneScopedN("Seal frame"); // Every pass this cycle encodes, from the seal's probe blits to the final eye, is timed under // one frame; encode_sealed_frame resolves it on its last submission. @@ -1889,7 +1907,12 @@ void seal_frame_locked(gfx::SealedFrame& sealedFrame, SealedFrameContext& ctx, u ctx.presentSource = webgpu::current_present_source(); // ImGui draw lists are built once per frame and replayed by each slot's ImGui pass, which is why // the next ImGui frame cannot start until the encode phase is done. - imgui::render_frame_data(); + if (hostImGuiFrame) { + // The host closed its own ImGui frame and handed over a copy of the draw data. + ctx.imguiFrame = std::move(hostImGuiFrame); + } else { + imgui::render_frame_data(); + } // The headset panel's draw data follows the same rule on the host's side. ctx.stereoOverlay = imgui::latch_stereo_overlay(); // Drop the sealed frame's lazy RAM-readback requests while the producer is still excluded; it @@ -1981,7 +2004,7 @@ std::vector encode_sealed_frame(gfx::SealedFrame& sealedFrame, for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount; ++interpolatedFrame) { gfx::render(sealedFrame, encoder, static_cast(interpolatedFrame), false); auto image = acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight); - encode_presentation_snapshot(encoder, ctx.presentSource, *image, true, mirrorPlan); + encode_presentation_snapshot(encoder, ctx.presentSource, *image, true, mirrorPlan, host_imgui_data(ctx)); presentationJobs.push_back({ .image = std::move(image), .logicalFrame = ctx.logicalFrame, @@ -2013,7 +2036,7 @@ std::vector encode_sealed_frame(gfx::SealedFrame& sealedFrame, if (!ctx.replayInterpolatedFrames) { for (uint32_t interpolatedFrame = 0; interpolatedFrame < ctx.interpolatedFrameCount; ++interpolatedFrame) { auto image = acquire_presentation_image(interpolatedFrame, ctx.snapshotWidth, ctx.snapshotHeight); - encode_presentation_snapshot(encoder, ctx.presentSource, *image, true, mirrorPlan); + encode_presentation_snapshot(encoder, ctx.presentSource, *image, true, mirrorPlan, host_imgui_data(ctx)); presentationJobs.push_back({ .image = std::move(image), .logicalFrame = ctx.logicalFrame, @@ -2053,7 +2076,7 @@ std::vector encode_sealed_frame(gfx::SealedFrame& sealedFrame, // showing. Black re-clears it below, once the eyes have taken their copy. const bool virtualScreenNeedsMono = stereoOutput && !immersiveReplay; encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true, - virtualScreenNeedsMono ? MirrorPlan::Mono : mirrorPlan); + virtualScreenNeedsMono ? MirrorPlan::Mono : mirrorPlan, host_imgui_data(ctx)); if (stereoOutput) { publish_stereo_screen_aspects(ctx.presentSource, finalImage->texture.size, immersiveReplay); } @@ -2075,7 +2098,8 @@ std::vector encode_sealed_frame(gfx::SealedFrame& sealedFrame, stereo_overlay::composite_flat(encoder, output.view, output.size, eye); } if (mirrorPlan == MirrorPlan::Black && !headsetOnly) { - encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true, MirrorPlan::Black); + encode_presentation_snapshot(encoder, ctx.presentSource, *finalImage, true, MirrorPlan::Black, + host_imgui_data(ctx)); } } if (stereoOutput) { @@ -2231,6 +2255,14 @@ void record_frame_telemetry() { if (const std::string gpuTiming = gfx::gpu_timing_report(); !gpuTiming.empty()) { Log.info("{}", gpuTiming); } + if (const auto frameLog = g_frameLogCallback.load(std::memory_order_acquire)) { + char extra[512]; + extra[0] = '\0'; + frameLog(extra, sizeof(extra), elapsed.count(), windowFrames); + if (extra[0] != '\0') { + Log.info("{}", extra); + } + } windowStart = now; windowFrames = 0; } @@ -2242,7 +2274,7 @@ void record_frame_telemetry() { // One complete frame-worker cycle. Desktop and headset interpolation both // release the producer after sealing, before encoding their extra scene views. -bool run_frame_worker_cycle(gfx::SealedFrame& sealedFrame, uint64_t contentTag, +bool run_frame_worker_cycle(gfx::SealedFrame& sealedFrame, uint64_t contentTag, imgui::HostFramePtr hostImGuiFrame, const StereoSceneAnchor& sceneAnchor) noexcept { ZoneScopedN("Frame worker cycle"); webgpu::fail_if_device_lost(); @@ -2254,7 +2286,7 @@ bool run_frame_worker_cycle(gfx::SealedFrame& sealedFrame, uint64_t contentTag, auto stretchStarted = std::chrono::steady_clock::now(); { std::lock_guard gpuLock(g_rendererGpuMutex); - seal_frame_locked(sealedFrame, ctx, contentTag, sceneAnchor); + seal_frame_locked(sealedFrame, ctx, contentTag, sceneAnchor, std::move(hostImGuiFrame)); } g_workerSealNs.fetch_add(elapsedNs(stretchStarted), std::memory_order_relaxed); stretchStarted = std::chrono::steady_clock::now(); @@ -2311,11 +2343,11 @@ bool run_frame_worker_cycle(gfx::SealedFrame& sealedFrame, uint64_t contentTag, // Synchronous frame submission: seal, encode and present inline on the calling thread. Used when // the frame worker is disabled (RenderDoc captures) and on the boot path. void end_frame_impl(bool pumpEvents, bool drainFifo, uint64_t contentTag, - const StereoSceneAnchor& sceneAnchor) noexcept { + const StereoSceneAnchor& sceneAnchor, imgui::HostFramePtr hostImGuiFrame) noexcept { ZoneScoped; #ifdef AURORA_ENABLE_GX webgpu::fail_if_device_lost(); - if (pumpEvents) { + if (pumpEvents && !g_hostEventPump.load(std::memory_order_acquire)) { window::pump_events(); } gfx::SealedFrame sealedFrame; @@ -2326,7 +2358,7 @@ void end_frame_impl(bool pumpEvents, bool drainFifo, uint64_t contentTag, if (drainFifo) { gx::fifo::drain(); } - seal_frame_locked(sealedFrame, ctx, contentTag, sceneAnchor); + seal_frame_locked(sealedFrame, ctx, contentTag, sceneAnchor, std::move(hostImGuiFrame)); presentationJobs = encode_sealed_frame(sealedFrame, ctx); } publish_presentations(std::move(presentationJobs), ctx.interpolationActive); @@ -2352,7 +2384,9 @@ bool begin_frame() noexcept { ensure_frame_worker_started(); // SDL needs event pumping on the window-owning producer thread, and the worker passes // pumpEvents=false, so keep it here even when the fast path returns early. - window::pump_events(); + if (!g_hostEventPump.load(std::memory_order_acquire)) { + window::pump_events(); + } bool waitForSurfacePreparation = false; #ifdef AURORA_ENABLE_GX // A surface mutation can legitimately fail preparation, and optimistic success would let GX/ImGui @@ -2402,7 +2436,7 @@ bool begin_frame() noexcept { return prepared; } -void end_frame(uint64_t contentTag) noexcept { +void end_frame(uint64_t contentTag, imgui::HostFramePtr hostImGuiFrame) noexcept { #ifdef AURORA_ENABLE_GX webgpu::fail_if_device_lost(); #endif @@ -2413,7 +2447,7 @@ void end_frame(uint64_t contentTag) noexcept { g_pendingStereoLocalPlayerCount = 1; g_pendingSceneAnchor = {}; if (!frame_worker_requested()) { - end_frame_impl(true, true, contentTag, sceneAnchor); + end_frame_impl(true, true, contentTag, sceneAnchor, std::move(hostImGuiFrame)); return; } @@ -2435,6 +2469,7 @@ void end_frame(uint64_t contentTag) noexcept { g_frameWorker.ready.store(false, std::memory_order_release); g_frameWorker.contentTag = contentTag; g_frameWorker.sceneAnchor = sceneAnchor; + g_frameWorker.hostImGuiFrame = std::move(hostImGuiFrame); g_frameWorker.jobPending = true; g_frameWorker.prepareAllowed = false; } @@ -2537,8 +2572,34 @@ AuroraInfo aurora_initialize(int argc, char* argv[], const AuroraConfig* config) void aurora_shutdown() { aurora::shutdown(); } const AuroraEvent* aurora_update() { return aurora::update(); } bool aurora_begin_frame() { return aurora::begin_frame(); } -void aurora_end_frame() { aurora::end_frame(AURORA_STEREO_CONTENT_TAG_UNKNOWN); } -void aurora_end_frame_tagged(uint64_t contentTag) { aurora::end_frame(contentTag); } +void aurora_end_frame() { aurora::end_frame(AURORA_STEREO_CONTENT_TAG_UNKNOWN, {}); } +void aurora_end_frame_tagged(uint64_t contentTag) { aurora::end_frame(contentTag, {}); } +void aurora_end_frame_ex(uint64_t contentTag, void* imguiFrame) { + aurora::imgui::HostFramePtr frame; + if (imguiFrame != nullptr) { + auto* holder = static_cast(imguiFrame); + frame = std::move(*holder); + delete holder; + } + aurora::end_frame(contentTag, std::move(frame)); +} +void aurora_set_host_event_pump(bool hostPumps) { + aurora::g_hostEventPump.store(hostPumps, std::memory_order_release); +} +void aurora_set_frame_log_callback(AuroraFrameLogCallback callback) { + aurora::g_frameLogCallback.store(callback, std::memory_order_release); +} +extern "C" void aurora_imgui_host_frame_begin(void) { +#ifdef AURORA_ENABLE_GX + // ImGui's WebGPU backend creates its device objects lazily from new_frame. + std::lock_guard gpuLock(aurora::g_rendererGpuMutex); +#endif + aurora::imgui::host_frame_begin(aurora::window::get_window_size()); +} +extern "C" void* aurora_imgui_host_frame_end(void) { return new aurora::imgui::HostFramePtr(aurora::imgui::host_frame_end()); } +extern "C" void aurora_imgui_host_frame_release(void* imguiFrame) { + delete static_cast(imguiFrame); +} void aurora_set_stereo_scene_anchor(const float anchorFromScene[12]) { aurora::set_stereo_scene_anchor(anchorFromScene); } diff --git a/aurora-main/lib/imgui.cpp b/aurora-main/lib/imgui.cpp index 08e6c5e..5714d28 100644 --- a/aurora-main/lib/imgui.cpp +++ b/aurora-main/lib/imgui.cpp @@ -28,7 +28,11 @@ static std::string g_imguiLog{}; static bool g_useSdlRenderer = false; // Set once ImGui::Render() has produced this frame's draw data. Interpolation encodes up to four // ImGui passes per frame, and every one of them used to rebuild the draw lists from scratch. -static bool g_frameDataBuilt = false; +static bool g_frameDataBuilt = true; +// Host-owned frames (see imgui.hpp). Once the host begins one, aurora never calls new_frame() or +// ImGui::Render() itself; the sealed frame carries the host's copy of the draw data instead. +static bool g_hostFrames = false; +static bool g_hostFrameOpen = false; static std::vector g_sdlTextures; static std::vector g_wgpuTextures; @@ -179,7 +183,7 @@ void new_frame(const AuroraWindowSize& size) noexcept { void render_frame_data() noexcept { ZoneScoped; - if (g_frameDataBuilt) { + if (g_frameDataBuilt || g_hostFrames) { return; } ImGui::Render(); @@ -190,6 +194,11 @@ void render_frame_data() noexcept { void render(const wgpu::RenderPassEncoder& pass) noexcept { ZoneScoped; + if (g_hostFrames) { + // The shared context's draw data belongs to the host's current frame now; + // a sealed frame without a host copy has nothing safe to draw. + return; + } render_frame_data(); auto* data = ImGui::GetDrawData(); @@ -205,6 +214,71 @@ void render(const wgpu::RenderPassEncoder& pass) noexcept { } } +struct HostFrame { + ImDrawData data{}; + std::vector lists; + ~HostFrame() { + for (ImDrawList* list : lists) { + IM_DELETE(list); + } + } +}; + +void host_frame_begin(const AuroraWindowSize& size) noexcept { + g_hostFrames = true; + if (g_hostFrameOpen) { + return; + } + if (!g_frameDataBuilt) { + // aurora started this frame itself before the host took over: adopt it. + g_hostFrameOpen = true; + return; + } + new_frame(size); + g_hostFrameOpen = true; +} + +HostFramePtr host_frame_end() noexcept { + ZoneScoped; + if (!g_hostFrameOpen) { + host_frame_begin(window::get_window_size()); + } + ImGui::Render(); + ImDrawData* source = ImGui::GetDrawData(); + source->FramebufferScale = ImGui::GetIO().DisplayFramebufferScale; + auto frame = std::make_shared(); + frame->data = *source; + frame->data.CmdLists.clear(); + frame->lists.reserve(static_cast(source->CmdListsCount)); + for (int i = 0; i < source->CmdListsCount; ++i) { + const ImDrawList* src = source->CmdLists[i]; + ImDrawList* copy = IM_NEW(ImDrawList)(src->_Data); + copy->CmdBuffer = src->CmdBuffer; + copy->IdxBuffer = src->IdxBuffer; + copy->VtxBuffer = src->VtxBuffer; + copy->Flags = src->Flags; + frame->lists.push_back(copy); + frame->data.CmdLists.push_back(copy); + } + g_hostFrameOpen = false; + g_frameDataBuilt = true; + return frame; +} + +bool host_frames_active() noexcept { return g_hostFrames; } + +const ImDrawData* host_frame_draw_data(const HostFrame& frame) noexcept { return &frame.data; } + +void render(const wgpu::RenderPassEncoder& pass, const ImDrawData* data) noexcept { + ZoneScoped; + if (g_useSdlRenderer || data == nullptr) { + return; + } + pass.PushDebugGroup("Aurora: Dear Imgui"); + ImGui_ImplWGPU_RenderDrawData(const_cast(data), pass.Get()); + pass.PopDebugGroup(); +} + StereoOverlay latch_stereo_overlay() noexcept { std::lock_guard lock(g_stereoOverlayMutex); return g_stereoOverlay; diff --git a/aurora-main/lib/imgui.hpp b/aurora-main/lib/imgui.hpp index f2f3345..2dd87c7 100644 --- a/aurora-main/lib/imgui.hpp +++ b/aurora-main/lib/imgui.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include union SDL_Event; struct ImDrawData; @@ -32,4 +33,17 @@ StereoOverlay latch_stereo_overlay() noexcept; // uniform for every pass, so a pass whose display size differs from the desktop's must be submitted // before the next pass is recorded. bool render_draw_data(const wgpu::RenderPassEncoder& pass, ImDrawData* data) noexcept; + +// Host-owned ImGui frames. The host starts each frame on its own thread with host_frame_begin() and +// closes it with host_frame_end(), which renders the frame and copies its draw data out of the shared +// context. The copy is what the sealed frame replays, so the host may start the next frame while the +// worker still encodes this one, and aurora stops starting frames itself once the host has begun one. +struct HostFrame; +using HostFramePtr = std::shared_ptr; +void host_frame_begin(const AuroraWindowSize& size) noexcept; +HostFramePtr host_frame_end() noexcept; +bool host_frames_active() noexcept; +const ImDrawData* host_frame_draw_data(const HostFrame& frame) noexcept; +// Renders a host frame's copied draw data in place of the shared context's. +void render(const wgpu::RenderPassEncoder& pass, const ImDrawData* data) noexcept; } // namespace aurora::imgui diff --git a/docs/quest-port.md b/docs/quest-port.md index 4e934f4..1326fd1 100644 --- a/docs/quest-port.md +++ b/docs/quest-port.md @@ -588,7 +588,13 @@ the app: | `debug.wiicompiled.vtxpad 0` | Turns the stride padding off, to re-check a driver update | | `debug.wiicompiled.validation 1` | Keeps WebGPU validation and robustness on in release builds | | `debug.wiicompiled.inject :