diff options
| author | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-29 11:55:07 -0400 |
|---|---|---|
| committer | Takoroka <137927095+Takoroka@users.noreply.github.com> | 2026-10-29 20:49:48 -0400 |
| commit | 15e953cafd5cb348833cb373d7ddd996b81307f7 (patch) | |
| tree | 3fd08c6943dd5b620fe1c899c283c1c78a6a5545 | |
| parent | 17c10c3690eab1ab461318f7e3e8850d490fb753 (diff) | |
video_core: GPU thread split on by default for Switch 2 titles; cheaper front paths
Clean A/B in title 1 (3 rounds each): 1.46x the draws a second of one
thread, frames 17% shorter, no crashes, front register copies equal to
the back's in 57,089 checks. EDEN_NX2_SPLIT=0 runs everything on one
thread, =1 keeps macros on the back.
The front's per-write paths no longer check guarded statics
(GpuSplit::Get, Nx2Profile::Enabled, the skip-same switches) and the
ring's method writes are inline. Notes row 145.
| -rw-r--r-- | NX2_GPU_THREAD_SPLIT.md | 3 | ||||
| -rw-r--r-- | NX2_NOTES.md | 3 | ||||
| -rw-r--r-- | src/video_core/engines/maxwell_3d.cpp | 4 | ||||
| -rw-r--r-- | src/video_core/gpu_split.cpp | 36 | ||||
| -rw-r--r-- | src/video_core/gpu_split.h | 51 | ||||
| -rw-r--r-- | src/video_core/nx2_profile.h | 6 |
6 files changed, 58 insertions, 45 deletions
diff --git a/NX2_GPU_THREAD_SPLIT.md b/NX2_GPU_THREAD_SPLIT.md index 2807b930dc..75fd0e4105 100644 --- a/NX2_GPU_THREAD_SPLIT.md +++ b/NX2_GPU_THREAD_SPLIT.md @@ -59,4 +59,5 @@ on the back, `EDEN_NX2_SPLIT=2` also runs the 3D and compute macros on the front copies. Measured in title 1 (NX2_NOTES rows 142-144): ~44,000 draws a second on one thread, ~47,400 with stage 2, 50,000-66,500 with stage 3. The front waits ~4,000-5,000 times a second (semaphore acquires, MME DMA reads); the back still spends ~200 ms a second outside its zones (ring dispatch, -method sink). Next: a clean A/B for the default, then trimming the back's ring overhead. +method sink). Clean A/B (NX2_NOTES row 145): 1.46x throughput, frames 17% shorter; on by +default since. The back is ~92% busy, ~53% of it draw setup: the next step is there, not the ring. diff --git a/NX2_NOTES.md b/NX2_NOTES.md index e24a87f774..620708a59e 100644 --- a/NX2_NOTES.md +++ b/NX2_NOTES.md @@ -206,6 +206,7 @@ With the patched decoder, games 1/3/4 only leave ~60-110 rare SIMD encodings in | 142 | GPU thread ~100% busy, host GPU 20-40%: one thread parses, runs macros, sets state and records draws | the work is sequential but only a few points need a result from what came before (semaphore acquires, MME DMA reads) | GPU thread split (babd395, `gpu_split.h`, `EDEN_NX2_SPLIT=1`): engine methods run in order on a second thread fed by a lock-free ring; releases, syncpoints, wait-for-idle, flushes and channel binds are deferred into the ring, only acquires wait (~2,400 drains a second). ~47,400 draws a second against ~44,000 (3 A/B rounds), same per-call costs | | 143 | crashes with the split: CommandChunk::Record (null chunk), vkCmdEndRenderPass on the Vulkan worker | two threads recording into the scheduler: (a) the DMA pusher's split-method debug log read guest memory with the cache-flushing ReadBlock on the front; (b) with async presentation, Swapchain::AcquireNextImage -> Scheduler::Wait flushed the main scheduler from the present thread whenever its tick was not submitted yet (pre-existing race, the split made it frequent) | (a) ReadBlockUnsafe; (b) e4523ab: the present thread waits up to 20 ms for the GPU thread to submit, never flushes. Found with `EDEN_NX2_SPLIT_CHECK=1` (stack of any other thread using the scheduler while the back runs) | | 144 | macros on the split's front (`EDEN_NX2_SPLIT=2`) need the registers they read before the back has them | the front keeps copies: every write it sends plus what the back changes on its own (constant buffer offset, shadow_scratch[0], macro bind pointer, shadow RAM). `EDEN_NX2_SPLIT_VERIFY=1` found three differences: writes still in the back's method sink at the handover; a multi-word run that changes the shadow RAM mode mid-run; and shadow_state.shadow_ram_control being the shadow copy of register 0x49 itself (a Replay write reads the mode back) | all three modelled: 0 differences in 40,193 checks (~165 million macro calls). Front: macros 128 ns a call (180 on one thread, cache-hot), drops ~55% of 3D state writes as same-value. Back: 50,000-66,500 draws a second against 41,500-47,000 without the split; frames 20.5 / 25.5 ms on two courses where the unsplit runs measured 25.6-40.7 ms. Default stays off until a clean A/B | +| 145 | is the split worth having on by default | clean A/B, nothing else running, 3 rounds each (profiler on in both): one thread 41,100 / 48,600 / 37,000 draws a second, frames 30.5 / 35.0 / 27.1 ms; `EDEN_NX2_SPLIT=2` 59,200 / 62,400 / 62,700 draws a second, frames 28.6 / 23.3 / 25.1 ms. Throughput 1.46x, frames 17% shorter (the back is now ~92% busy, ~53% of it draw setup); 0 crashes, 0 register differences in 57,089 checks after the front's fast paths (no guarded statics, inline ring writes) | on by default for Switch 2 titles (`EDEN_NX2_SPLIT=0` one thread, `=1` macros on the back). Default run without the profiler: 16.9 ms frames (~59 fps) on a light course | Run settings used (in `build/bin/user/config/sdl2-config.ini`): `memory_layout_mode=4` (12 GB). Debug knobs: `EDEN_NX2_TC_EXPECTED_MIB` / `EDEN_NX2_TC_CRITICAL_MIB` (collector thresholds), @@ -220,7 +221,7 @@ faulting stack as module+offset, then a minidump), `EDEN_NX2_AUTO_STUB`, `EDEN_N `EDEN_NX2_BUFFER_DOWNLOADS=0/1`, `EDEN_NX2_INLINE_DEFER=0`, `EDEN_NX2_TC_MIN_AGE=<frames>` (rows 131, 134, 133), `EDEN_NX2_SKIP_SAME=0` (row 135), `EDEN_NX2_MME_JIT=0` (row 140), -`EDEN_NX2_SPLIT=1/2`, `EDEN_NX2_SPLIT_VERIFY=1`, `EDEN_NX2_SPLIT_CHECK=1`, `EDEN_NX2_SPLIT_SKIP=0` (rows 142-144), +`EDEN_NX2_SPLIT=0/1` (default 2), `EDEN_NX2_SPLIT_VERIFY=1`, `EDEN_NX2_SPLIT_CHECK=1`, `EDEN_NX2_SPLIT_SKIP=0` (rows 142-145), `EDEN_NX2_FRAME_CAPTURE=<dir>` (every 120th presented frame as TGA), `EDEN_NX2_PB_CHECK` (pushbuffer entries changed between submit and execution), `EDEN_NX2_QUERY_TRACE` (every GPU report/semaphore write). `C:\dev\crashrun.sh` runs eden-cli under cdb and prints the crash stack; diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp index 10527c2473..6fdc6a74b9 100644 --- a/src/video_core/engines/maxwell_3d.cpp +++ b/src/video_core/engines/maxwell_3d.cpp @@ -800,6 +800,7 @@ struct Maxwell3D::FrontState { /// A method the draw manager handles was sent since the back last ran DrawDeferred: only /// then can it have a deferred instanced draw to issue at the end of a macro. bool draw_pending{true}; + bool skip_same{}; ///< SkipSameValueWrites() && FrontSkipSame(), read once }; namespace { @@ -865,6 +866,7 @@ Maxwell3D::FrontState& Maxwell3D::Front(Core::System& system, VideoCommon::GpuSp std::memcpy(front->regs.data(), regs.reg_array.data(), sizeof(front->regs)); std::memcpy(front->shadow.data(), shadow_state.reg_array.data(), sizeof(front->shadow)); front->control = shadow_state.shadow_ram_control; + front->skip_same = SkipSameValueWrites() && FrontSkipSame(); front->executing_macro = executing_macro; front->parameters = std::move(macro_params); macro_params.clear(); @@ -897,7 +899,7 @@ void Maxwell3D::FrontWrite(VideoCommon::GpuSplit& split, FrontState& fs, u32 met if (!execution_mask[method]) { // Nothing would change on the back: CallMethod and ConsumeSinkImpl skip same-value state // writes, and the shadow RAM (tracked by default) holds the value already. - if (fs.regs[method] == value && shadow_kept && SkipSameValueWrites() && FrontSkipSame()) { + if (fs.regs[method] == value && shadow_kept && fs.skip_same) { if (Profile::Enabled()) [[unlikely]] { Profile::Add(Profile::Counter::SameValueWrites, 1); } diff --git a/src/video_core/gpu_split.cpp b/src/video_core/gpu_split.cpp index 970fc0b559..07fda589ff 100644 --- a/src/video_core/gpu_split.cpp +++ b/src/video_core/gpu_split.cpp @@ -80,15 +80,12 @@ void GpuSplit::CheckOwner(const char* where) { #endif } -GpuSplit& GpuSplit::Get() { - static GpuSplit split; - return split; -} +GpuSplit GpuSplit::instance; void GpuSplit::MaybeStart(Core::System& system_) { static const char mode = [] { const char* value = std::getenv("EDEN_NX2_SPLIT"); - return value != nullptr ? value[0] : '0'; + return value != nullptr ? value[0] : '2'; }(); const bool wanted = mode == '1' || mode == '2'; if (active || !wanted || !Tegra::g_nx2_ampere.load(std::memory_order_relaxed)) { @@ -127,7 +124,7 @@ void GpuSplit::Stop() { active = false; } -u64* GpuSplit::Reserve(size_t words) { +u64* GpuSplit::ReserveSlow(size_t words) { size_t index = write_position & RingMask; if (index + words > RingWords) { // Entries never wrap: skip to the start of the ring. @@ -194,10 +191,7 @@ void GpuSplit::Drain() { } } -void GpuSplit::SelectEngine(Tegra::Engines::EngineInterface* engine) { - if (engine == front_engine) { - return; - } +void GpuSplit::SwitchEngine(Tegra::Engines::EngineInterface* engine) { front_engine = engine; u64* const entry = Reserve(2); entry[0] = static_cast<u64>(Kind::Engine); @@ -205,28 +199,6 @@ void GpuSplit::SelectEngine(Tegra::Engines::EngineInterface* engine) { Commit(2); } -void GpuSplit::Method(Tegra::Engines::EngineInterface* engine, u32 method, u32 argument, - bool is_last, const GPUVAddr* segment) { - SelectEngine(engine); - const size_t words = segment != nullptr ? 2 : 1; - u64* const entry = Reserve(words); - entry[0] = static_cast<u64>(Kind::Method) | (is_last ? IsLast : 0) | - (segment != nullptr ? HasSegment : 0) | (u64{method & 0xffff} << 8) | - (u64{argument} << 32); - if (segment != nullptr) { - entry[1] = *segment; - } - Commit(words); -} - -void GpuSplit::MacroWrite(Tegra::Engines::EngineInterface* engine, u32 method, u32 argument) { - SelectEngine(engine); - u64* const entry = Reserve(1); - entry[0] = static_cast<u64>(Kind::Method) | IsLast | Direct | (u64{method & 0xffff} << 8) | - (u64{argument} << 32); - Commit(1); -} - void GpuSplit::MultiMethod(Tegra::Engines::EngineInterface* engine, u32 method, const u32* words, u32 amount, u32 methods_pending, const GPUVAddr* segment) { SelectEngine(engine); diff --git a/src/video_core/gpu_split.h b/src/video_core/gpu_split.h index 61644b9b80..d6f91e4141 100644 --- a/src/video_core/gpu_split.h +++ b/src/video_core/gpu_split.h @@ -28,12 +28,14 @@ namespace VideoCommon { // second thread (the back), fed through a single-producer single-consumer ring. Work the front // would do on back-owned state is deferred into the same ring; the front waits for the back to // catch up (Drain) only where it needs a result from that state, e.g. a semaphore it acquires. -// Opt-in for Switch 2 titles: EDEN_NX2_SPLIT=1. EDEN_NX2_SPLIT=2 also runs the 3D and compute -// engines' macros on the front, against copies of the registers they read (Maxwell3D and -// KeplerCompute Front*), and sends the writes they make. +// On for Switch 2 titles with the 3D and compute engines' macros on the front, against copies of +// the registers they read (Maxwell3D and KeplerCompute Front*), which sends the writes they make. +// EDEN_NX2_SPLIT=1 keeps the macros on the back; EDEN_NX2_SPLIT=0 runs everything on one thread. class GpuSplit { public: - static GpuSplit& Get(); + static GpuSplit& Get() { + return instance; + } /// Whether the front sends work to the back (front thread only). [[nodiscard]] bool Active() const { @@ -52,11 +54,28 @@ public: /// An engine method write, executed as DmaPusher would (method sink, ConsumeSink, call). /// `segment`: the engine's current_dma_segment for the call, when the engine reads it. void Method(Tegra::Engines::EngineInterface* engine, u32 method, u32 argument, bool is_last, - const GPUVAddr* segment); + const GPUVAddr* segment) { + SelectEngine(engine); + const size_t words = segment != nullptr ? 2 : 1; + u64* const entry = Reserve(words); + entry[0] = static_cast<u64>(Kind::Method) | (is_last ? IsLast : 0) | + (segment != nullptr ? HasSegment : 0) | (u64{method & 0xffff} << 8) | + (u64{argument} << 32); + if (segment != nullptr) { + entry[1] = *segment; + } + Commit(words); + } void MultiMethod(Tegra::Engines::EngineInterface* engine, u32 method, const u32* words, u32 amount, u32 methods_pending, const GPUVAddr* segment); /// A write made by a macro: CallMethod without the method sink, as the engines' macros do. - void MacroWrite(Tegra::Engines::EngineInterface* engine, u32 method, u32 argument); + void MacroWrite(Tegra::Engines::EngineInterface* engine, u32 method, u32 argument) { + SelectEngine(engine); + u64* const entry = Reserve(1); + entry[0] = static_cast<u64>(Kind::Method) | IsLast | Direct | (u64{method & 0xffff} << 8) | + (u64{argument} << 32); + Commit(1); + } /// Runs `func` on the back after everything sent before it. Must be trivially copyable. template <typename Func> @@ -103,7 +122,16 @@ private: (*reinterpret_cast<const Func*>(payload))(); } - u64* Reserve(size_t words); + /// Room for an entry of `words` at the write position (never across the ring's end). + u64* Reserve(size_t words) { + const size_t index = write_position & RingMask; + if (index + words <= RingWords && write_position + words - consumed_cache <= RingWords) + [[likely]] { + return &ring[index]; + } + return ReserveSlow(words); + } + u64* ReserveSlow(size_t words); void Commit(size_t words) { write_position += words; if (write_position - published_local >= publish_every) { @@ -111,7 +139,12 @@ private: } } void WaitForSpace(size_t words); - void SelectEngine(Tegra::Engines::EngineInterface* engine); + void SelectEngine(Tegra::Engines::EngineInterface* engine) { + if (engine != front_engine) [[unlikely]] { + SwitchEngine(engine); + } + } + void SwitchEngine(Tegra::Engines::EngineInterface* engine); void BackLoop(std::stop_token stop_token); size_t Execute(const u64* entry); @@ -132,6 +165,8 @@ private: // Back side. Tegra::Engines::EngineInterface* back_engine{}; + static GpuSplit instance; + alignas(64) std::atomic<u64> published{}; alignas(64) std::atomic<u64> consumed{}; alignas(64) std::atomic<bool> back_waiting{}; diff --git a/src/video_core/nx2_profile.h b/src/video_core/nx2_profile.h index 0ff69f9631..a921b72082 100644 --- a/src/video_core/nx2_profile.h +++ b/src/video_core/nx2_profile.h @@ -64,9 +64,11 @@ inline constexpr std::array<const char*, static_cast<size_t>(Zone::Count)> zone_ "fence skipped", "method", "split drain", "split wait", }; +// Read once at startup: Enabled() runs several times per method write (no guard to check). +inline const bool g_enabled = std::getenv("EDEN_NX2_GPU_PROFILE") != nullptr; + inline bool Enabled() { - static const bool enabled = std::getenv("EDEN_NX2_GPU_PROFILE") != nullptr; - return enabled; + return g_enabled; } // Nanoseconds; from the host's own counter where the wall clock reads it directly (the zones |
