aboutsummaryrefslogtreecommitdiff
path: root/NX2_GPU_THREAD_SPLIT.md
diff options
context:
space:
mode:
Diffstat (limited to 'NX2_GPU_THREAD_SPLIT.md')
-rw-r--r--NX2_GPU_THREAD_SPLIT.md53
1 files changed, 53 insertions, 0 deletions
diff --git a/NX2_GPU_THREAD_SPLIT.md b/NX2_GPU_THREAD_SPLIT.md
new file mode 100644
index 0000000000..f739704dbf
--- /dev/null
+++ b/NX2_GPU_THREAD_SPLIT.md
@@ -0,0 +1,53 @@
+# Splitting the GPU thread (design, not implemented yet)
+
+Title 1 (Mario Kart World) is bound by one host thread, the GPU thread: ~100% busy while the
+host GPU is 20-40% busy and the game's own CPU thread mostly polls the counter. Per second of
+3D scenes (profiled, 2026-10-29): command processing and register writes ~335 ms, macros
+~200 ms (1.3 million calls), draw setup ~345 ms (55,000 draws), compute ~70 ms. Speeding up the
+pieces (NX2_NOTES rows 123-139) took frames from ~22-38 ms to ~18-26 ms; a locked 60 fps on the
+heavy courses needs work on more than one core.
+
+## Split point: method calls
+
+- **Front thread** (the current GPU thread): takes the CPU's command queue, parses GPFIFO
+ entries and command lists (DmaPusher), runs macros (compiled, macro_turing_jit.h), and keeps
+ what macros read: a mirror of each engine's registers, its macro code and data RAM.
+- **Back thread**: executes the resulting stream of method writes in order: Maxwell3D /
+ KeplerCompute / Fermi2D / DMA engines' CallMethod and everything behind them (register
+ state, dirty flags, constant buffer and inline uploads, draws, dispatches, queries, fences,
+ the rasterizer and its caches). Only the back thread touches the rasterizer.
+- The stream: an SPSC ring of {engine, method, argument} plus copied data for multi-word
+ methods (command lists are often guest memory read in place, which the guest reuses).
+ CPU-side commands (flush/invalidate requests, RunInOrder actions, presentation) are
+ forwarded into the same ring so they keep their order.
+
+## What the front needs without waiting
+
+- Register values macros read (MME_READ): the front mirrors every register write it sends.
+ Back-side changes to registers it must reproduce: constant buffer offset (+4 per
+ const_buffer.buffer word), shadow_scratch[0] = 1 (falcon[4]), shadow RAM control (Track /
+ Replay change the effective value), macro upload pointers (the front handles load_mme
+ itself). Compute: native_registers (0xad is changed temporarily by the inline QMD path).
+- Macro code uploads and binds (load_mme.*, compute 0x45-0x48): handled by the front.
+
+## Where the front waits for the back (sync points)
+
+- Macro DMA reads (MME_DMA_READ / _FIFOED, ~450 a second): read guest memory as of their
+ position, so the back must have executed everything before them.
+- Semaphore acquires that are not satisfied yet (~10,500 a second, cross-channel waits on QMD
+ releases): the release is executed by the back, so the front waits until the semaphore
+ holds (or parks the channel and runs another one, as the pending-channel code does now).
+- Syncpoint waits, WaitForIdle-style methods, and anything else that reads guest memory or
+ host state on the front.
+- Expected cost: each wait drains the back only up to the dependency, then the back idles
+ until the front sends the next method (a few microseconds), so even ~10,000 waits a second
+ leave most of the overlap. Ceiling: front ~35-40% of the work, back ~60-65% -> ~1.4-1.6x.
+
+## Stages
+
+1. Instrument: count sync points per second and the front/back share precisely (zones).
+2. Ring + back thread executing method writes; the front still does everything else.
+ Correctness first: an EDEN_NX2_SPLIT=0 switch runs both on one thread.
+3. Move macros to the front with the register mirrors; verify mirrors against the back's
+ registers in a debug mode (compare on every MME_READ).
+4. Sync points; crash and screenshot tests across courses; then title 4.