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 :