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.
This commit is contained in:
iChris4 committed 2026-09-19 22:57:49 +02:00
1 parent ffa63fa60a
commit 6a1641e0b7
33 files changed
+1532 -527

No files matched your search

+6 -1
View File
@@ -3,6 +3,7 @@
#include "settings_overlay.h"
#include "runtime_config.h"
#include "fiber_manager.h"
#include "gx_thread.h"
#include <aurora/aurora.h>
#include <aurora/event.h>
@@ -142,7 +143,11 @@ inline bool BeginAuroraFrame() {
if (!aurora_begin_frame()) {
return false;
}
ApplyPendingMkwDynamicAspectSurface();
// The viewport policy writes guest memory (EGG screen records), so the GX
// thread leaves it to the game thread's present path.
if (!GxThread::IsGxThread()) {
ApplyPendingMkwDynamicAspectSurface();
}
return true;
}
+106
View File
@@ -0,0 +1,106 @@
#pragma once
// GX thread: the host side of the GX pipeline (state tracking, FIFO parsing,
// display-list scanning, texture object resolution and every aurora GX call)
// runs on its own thread, fed by an ordered command ring the game thread posts
// to. Each GX HLE override is split into a game-thread front (guest-visible
// side effects: shadow registers, getters, display-list recording) and a
// GX-thread back (the aurora work). When the thread is disabled, Post() runs
// the back inline, so the split is behaviour-preserving in both modes.
//
// Data hazards follow the hardware: anything the GX library copies into the
// FIFO at call time (immediate-mode vertices, matrices, colours, light objects,
// texture object registers) is snapshotted at post time; anything the GP reads
// from memory when it reaches the command (display lists, vertex arrays,
// indexed matrices, texture data) is read when the GX thread executes it.
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string>
namespace GxThread {
// Decides whether posted work runs on the GX thread. Read once, before GXInit.
void Configure(bool enabled);
bool Enabled() noexcept;
// True on the consumer thread.
bool IsGxThread() noexcept;
void Start();
// Executes everything posted so far and joins the thread.
void Stop();
// Game thread: blocks until every posted record has executed (GXDrawDone).
void Drain();
// Publishes the open immediate-mode FIFO chunk. Post() does this itself.
void FlushFifo();
// Invoked at bounded intervals while the game thread blocks in Drain() or on
// a full ring, so guest timing (VI retraces, OS alarms) keeps running.
void SetWaitCallback(void (*callback)());
// Native thread id of the consumer (Android: gettid), 0 until started.
uint32_t NativeThreadId() noexcept;
// One line for the frame-rate log; resets the window counters.
std::string FormatStatsAndReset(double windowSeconds, uint32_t frames);
// Immediate-mode write-gather bytes. Only valid when Enabled().
void PostFifoWord(uint32_t value, uint32_t sizeBytes);
void PostFifoBytes(const uint8_t* data, uint32_t sizeBytes);
namespace detail {
using Invoke = void (*)(const uint8_t* payload, uint32_t payloadBytes);
void PostRecord(Invoke invoke, const void* payload, uint32_t payloadBytes);
template <typename... Ts>
struct Pack;
template <>
struct Pack<> {
template <typename F, typename... Prev>
void Call(F f, Prev... prev) const {
f(prev...);
}
};
template <typename T, typename... Ts>
struct Pack<T, Ts...> {
T head;
Pack<Ts...> tail;
template <typename F, typename... Prev>
void Call(F f, Prev... prev) const {
tail.Call(f, prev..., head);
}
};
template <typename... Ts>
struct BuildPack;
template <>
struct BuildPack<> {
static Pack<> Make() { return {}; }
};
template <typename T, typename... Ts>
struct BuildPack<T, Ts...> {
static Pack<T, Ts...> Make(T head, Ts... tail) {
return Pack<T, Ts...>{head, BuildPack<Ts...>::Make(tail...)};
}
};
template <typename R, typename... Params>
struct CallRecord {
R (*fn)(Params...);
Pack<Params...> args;
};
template <typename R, typename... Params>
void InvokeCall(const uint8_t* payload, uint32_t) {
CallRecord<R, Params...> record;
std::memcpy(&record, payload, sizeof(record));
record.args.Call(record.fn);
}
} // namespace detail
// Posts fn(args...) to the GX thread, or runs it now when the thread is off.
// Parameters must be trivially copyable values (no pointers into guest memory
// that the game may rewrite before the GX thread reads them).
template <typename R, typename... Params, typename... Args>
inline void Post(R (*fn)(Params...), Args&&... args) {
if (!Enabled()) {
fn(static_cast<Params>(args)...);
return;
}
detail::CallRecord<R, Params...> record{fn, detail::BuildPack<Params...>::Make(static_cast<Params>(args)...)};
detail::PostRecord(&detail::InvokeCall<R, Params...>, &record, sizeof(record));
}
} // namespace GxThread
+16
View File
@@ -47,6 +47,7 @@ struct RuntimeUserConfig {
std::optional<bool> textureReplacements;
std::optional<bool> textureDumps;
std::optional<bool> showFps;
std::optional<bool> gxThread;
std::optional<uint32_t> disabledPostProcessingPaths;
std::optional<bool> vrEnabled;
std::optional<bool> vrRequired;
@@ -410,6 +411,10 @@ inline void EnsureConfigFile() {
"skip_unready_pipelines = true\n"
"disable_copy_filter = true\n"
"show_fps = true\n"
"# Run the host side of the GX pipeline (state tracking, FIFO parsing,\n"
"# texture uploads) on its own thread. On by default on the Quest, where\n"
"# the game thread is the bottleneck; opt-in elsewhere.\n"
"# gx_thread = true\n"
"# Dolphin-style custom textures. When enabled, the renderer indexes\n"
"# texture_replacements/ next to this file at startup and substitutes\n"
"# any tex1_<W>x<H>_<hash>[_<tlut hash>]_<format>.dds or .png it finds\n"
@@ -641,6 +646,7 @@ inline RuntimeUserConfig ParseConfigDocument(const toml::value& document) {
config.skipUnreadyPipelines = FindConfigValue<bool>(document, "video", "skip_unready_pipelines");
config.disableCopyFilter = FindConfigValue<bool>(document, "video", "disable_copy_filter");
config.showFps = FindConfigValue<bool>(document, "video", "show_fps");
config.gxThread = FindConfigValue<bool>(document, "video", "gx_thread");
config.textureReplacements = FindConfigValue<bool>(document, "video", "texture_replacements");
config.textureDumps = FindConfigValue<bool>(document, "video", "texture_dumps");
if (auto value = FindConfigUint(document, "video", "disabled_post_processing_paths");
@@ -1294,6 +1300,16 @@ inline bool ShowFps(bool fallback = true) {
return Get().showFps.value_or(fallback);
}
// Runs the host side of the GX pipeline on its own thread (gx_thread.h). On by
// default on the Quest, where the game thread is the bottleneck; opt-in elsewhere.
inline bool GxThread() {
#if defined(__ANDROID__)
return Get().gxThread.value_or(true);
#else
return Get().gxThread.value_or(false);
#endif
}
inline bool TextureReplacements(bool fallback = false) {
return Get().textureReplacements.value_or(fallback);
}