diff options
Diffstat (limited to 'NX2_GPU_THREAD_SPLIT.md')
| -rw-r--r-- | NX2_GPU_THREAD_SPLIT.md | 53 |
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. |
