From 7a251d3a92f870649dab6068007ca3a1c910bbb5 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:16:43 -0400 Subject: video_core: split the GPU thread in two (EDEN_NX2_SPLIT, off by default) The GPU thread (front) takes the CPU's commands, parses command lists and runs the puller; engine method calls and everything behind them (registers, draws, dispatches, the rasterizer and its caches) run in order on a second thread (back), fed through a single-producer single-consumer ring (gpu_split.h). Work the front did on back-owned state (end of command list flushes, channel binds, semaphore releases, syncpoint increments, flush/invalidate requests, presentation) is deferred into the same ring; the front waits for the back only where it needs a result from that state: semaphore acquires and MME DMA reads. EDEN_NX2_SPLIT=1 runs macros on the back. EDEN_NX2_SPLIT=2 runs the 3D and compute macros on the front against copies of the registers they read (Maxwell3D/KeplerCompute Front*), drops state writes that keep a register's value there, and sends the rest. EDEN_NX2_SPLIT_VERIFY=1 compares the copies with the back's registers every 4096 macro calls (0 differences in 40,193 checks in title 1); EDEN_NX2_SPLIT_CHECK=1 logs the stack of any other thread using the Vulkan scheduler while the back runs. Switch 2 titles, Vulkan, non-accurate GPU only. Title 1, 3D attract loop: ~44,000 draws a second on one thread, ~47,400 with EDEN_NX2_SPLIT=1, 50,000-66,500 with EDEN_NX2_SPLIT=2. --- src/video_core/gpu_split.cpp | 363 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 src/video_core/gpu_split.cpp (limited to 'src/video_core/gpu_split.cpp') diff --git a/src/video_core/gpu_split.cpp b/src/video_core/gpu_split.cpp new file mode 100644 index 0000000000..970fc0b559 --- /dev/null +++ b/src/video_core/gpu_split.cpp @@ -0,0 +1,363 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include + +#ifdef _MSC_VER +#include +#endif +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include + +#include "common/logging.h" +#include "common/settings.h" +#include "common/thread.h" +#include "core/core.h" +#include "video_core/engines/engine_interface.h" +#include "video_core/gpu_split.h" +#include "video_core/nx2.h" +#include "video_core/nx2_profile.h" + +namespace VideoCommon { + +namespace { + +void Pause() { +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) + _mm_pause(); +#elif defined(__x86_64__) || defined(__i386__) + __builtin_ia32_pause(); +#else + std::this_thread::yield(); +#endif +} + +std::atomic g_back_thread_id{}; +std::atomic g_back_busy{}; + +} // Anonymous namespace + +void GpuSplit::CheckOwner(const char* where) { +#ifdef _WIN32 + if (!g_back_busy.load(std::memory_order_acquire)) { + return; + } + const DWORD thread = GetCurrentThreadId(); + if (thread == g_back_thread_id.load(std::memory_order_relaxed)) { + return; + } + static std::atomic reports{}; + if (reports.fetch_add(1) >= 12) { + return; + } + std::fprintf(stderr, "GPU split owner check: %s on thread %lu while the back runs\n", where, + thread); + void* frames[40]; + const USHORT count = RtlCaptureStackBackTrace(1, 40, frames, nullptr); + for (USHORT i = 0; i < count; ++i) { + HMODULE module{}; + char name[MAX_PATH] = "?"; + if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + static_cast(frames[i]), &module)) { + GetModuleFileNameA(module, name, sizeof(name)); + } + const char* base_name = std::strrchr(name, '\\'); + std::fprintf(stderr, " frame %s+%#llx\n", base_name ? base_name + 1 : name, + static_cast(reinterpret_cast(frames[i]) - + reinterpret_cast(module))); + } + std::fflush(stderr); +#else + (void)where; +#endif +} + +GpuSplit& GpuSplit::Get() { + static GpuSplit split; + return split; +} + +void GpuSplit::MaybeStart(Core::System& system_) { + static const char mode = [] { + const char* value = std::getenv("EDEN_NX2_SPLIT"); + return value != nullptr ? value[0] : '0'; + }(); + const bool wanted = mode == '1' || mode == '2'; + if (active || !wanted || !Tegra::g_nx2_ampere.load(std::memory_order_relaxed)) { + return; + } + // The front reads command lists straight from guest memory; a read that flushes the caches + // first (safe DMA, accurate GPU) would touch back-owned state. + const bool safe_reads = Settings::IsDMALevelDefault() ? Settings::IsGPULevelHigh() + : Settings::IsDMALevelSafe(); + if (safe_reads || Settings::IsGPULevelHigh() || + Settings::values.renderer_backend.GetValue() != Settings::RendererBackend::Vulkan) { + return; + } + system = &system_; + check_owner = std::getenv("EDEN_NX2_SPLIT_CHECK") != nullptr; + ring = std::make_unique(RingWords); + back_thread = std::jthread([this](std::stop_token stop_token) { BackLoop(stop_token); }); + active = true; +#ifdef ARCHITECTURE_x86_64 + // Front macros run compiled (macro_turing_jit.h) only. + const char* jit = std::getenv("EDEN_NX2_MME_JIT"); + front_macros = mode == '2' && (jit == nullptr || jit[0] != '0'); +#endif + LOG_INFO(HW_GPU, "GPU thread split: engine methods run on a second thread{}", + front_macros ? ", macros on the first" : ""); +} + +void GpuSplit::Stop() { + if (!back_thread.joinable()) { + return; + } + back_thread.request_stop(); + wake.fetch_add(1, std::memory_order_seq_cst); + wake.notify_one(); + back_thread.join(); + active = false; +} + +u64* GpuSplit::Reserve(size_t words) { + size_t index = write_position & RingMask; + if (index + words > RingWords) { + // Entries never wrap: skip to the start of the ring. + const size_t rest = RingWords - index; + WaitForSpace(rest); + ring[index] = static_cast(Kind::Wrap); + write_position += rest; + index = 0; + } + WaitForSpace(words); + return &ring[index]; +} + +void GpuSplit::WaitForSpace(size_t words) { + if (write_position + words - consumed_cache <= RingWords) { + return; + } + Publish(); + Nx2Profile::Scope scope{Nx2Profile::Zone::SplitDrain}; + for (u32 spins = 0;; ++spins) { + consumed_cache = consumed.load(std::memory_order_acquire); + if (write_position + words - consumed_cache <= RingWords) { + return; + } + if (spins < 4096) { + Pause(); + } else { + std::this_thread::yield(); + } + } +} + +void GpuSplit::Publish() { + if (published_local == write_position) { + return; + } + published_local = write_position; + publish_every = std::min(publish_every * 2, PublishEvery); + published.store(write_position, std::memory_order_seq_cst); + if (back_waiting.load(std::memory_order_seq_cst)) { + wake.fetch_add(1, std::memory_order_seq_cst); + wake.notify_one(); + } +} + +void GpuSplit::Drain() { + consumed_cache = consumed.load(std::memory_order_acquire); + if (consumed_cache == write_position) { + return; + } + Publish(); + Nx2Profile::Scope scope{Nx2Profile::Zone::SplitDrain}; + for (u32 spins = 0;; ++spins) { + consumed_cache = consumed.load(std::memory_order_acquire); + if (consumed_cache == write_position) { + publish_every = 8; + return; + } + if (spins < 16384) { + Pause(); + } else { + std::this_thread::yield(); + } + } +} + +void GpuSplit::SelectEngine(Tegra::Engines::EngineInterface* engine) { + if (engine == front_engine) { + return; + } + front_engine = engine; + u64* const entry = Reserve(2); + entry[0] = static_cast(Kind::Engine); + entry[1] = reinterpret_cast(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(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(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); + // Long runs (constant buffer and inline uploads) go in pieces, as when a method's data + // continues in the next GPFIFO entry. + while (amount > 0) { + const u32 piece = std::min(amount, MaxMultiWords); + const size_t head = segment != nullptr ? 3 : 2; + const size_t size = head + (piece + 1) / 2; + u64* const entry = Reserve(size); + entry[0] = static_cast(Kind::Multi) | (segment != nullptr ? HasSegment : 0) | + (u64{method & 0xffff} << 8) | (u64{piece} << 32); + entry[1] = methods_pending; + if (segment != nullptr) { + entry[2] = *segment; + } + entry[size - 1] = 0; + std::memcpy(entry + head, words, size_t{piece} * sizeof(u32)); + Commit(size); + words += piece; + amount -= piece; + methods_pending -= piece; + } +} + +void GpuSplit::DeferFunction(std::function&& func) { + auto* const heap = new std::function(std::move(func)); + Defer([heap] { + (*heap)(); + delete heap; + }); +} + +size_t GpuSplit::Execute(const u64* entry) { + const u64 head = entry[0]; + switch (static_cast(head & 0xf)) { + case Kind::Wrap: + return 0; + case Kind::Engine: + back_engine = reinterpret_cast(entry[1]); + return 2; + case Kind::Method: { + const u32 method = static_cast((head >> 8) & 0xffff); + const u32 argument = static_cast(head >> 32); + auto* const engine = back_engine; + if ((head & Direct) != 0) { + Nx2Profile::Scope method_scope{Nx2Profile::Zone::Method}; + engine->ConsumeSink(*system); + engine->CallMethod(*system, method, argument, true); + } else if (!engine->execution_mask[method]) { + engine->method_sink.emplace_back(method, argument); + } else { + Nx2Profile::Scope method_scope{Nx2Profile::Zone::Method}; + engine->ConsumeSink(*system); + if ((head & HasSegment) != 0) { + engine->current_dma_segment = entry[1]; + } + engine->CallMethod(*system, method, argument, (head & IsLast) != 0); + } + return (head & HasSegment) != 0 ? 2 : 1; + } + case Kind::Multi: { + const u32 method = static_cast((head >> 8) & 0xffff); + const u32 amount = static_cast(head >> 32); + const size_t data = (head & HasSegment) != 0 ? 3 : 2; + auto* const engine = back_engine; + Nx2Profile::Scope method_scope{Nx2Profile::Zone::Method}; + engine->ConsumeSink(*system); + if ((head & HasSegment) != 0) { + engine->current_dma_segment = entry[2]; + } + engine->CallMultiMethod(*system, method, reinterpret_cast(entry + data), + amount, static_cast(entry[1])); + return data + (amount + 1) / 2; + } + case Kind::Call: { + const size_t payload = static_cast((head >> 8) & 0xffffff); + reinterpret_cast(entry[1])(entry + 2); + return 2 + payload; + } + } + return 1; +} + +void GpuSplit::BackLoop(std::stop_token stop_token) { + Common::SetCurrentThreadName("GPUBack"); + Common::SetCurrentThreadPriority(Common::ThreadPriority::Critical); + Common::SetCurrentThreadToPerformanceCores(); + system->RegisterHostThread(); +#ifdef _WIN32 + g_back_thread_id.store(GetCurrentThreadId(), std::memory_order_relaxed); +#endif + u64 read_position = 0; + while (!stop_token.stop_requested()) { + Nx2Profile::Report("GPU back thread"); + u64 available = published.load(std::memory_order_acquire); + if (available == read_position) { + Nx2Profile::Scope scope{Nx2Profile::Zone::SplitWait}; + // The front usually sends more within microseconds: spin briefly before sleeping. + for (u32 spins = 0; spins < 8192 && available == read_position; ++spins) { + Pause(); + available = published.load(std::memory_order_acquire); + } + if (available == read_position) { + // Publish bumps `wake` when it sees back_waiting: read it before the last look + // at `published`, so a publish in between ends the wait at once. + back_waiting.store(true, std::memory_order_seq_cst); + const u32 seen = wake.load(std::memory_order_seq_cst); + available = published.load(std::memory_order_seq_cst); + if (available == read_position && !stop_token.stop_requested()) { + wake.wait(seen, std::memory_order_seq_cst); + } + back_waiting.store(false, std::memory_order_relaxed); + continue; + } + } + u64 last_store = read_position; + g_back_busy.store(true, std::memory_order_relaxed); + while (read_position != available) { + const size_t index = read_position & RingMask; + const size_t size = Execute(&ring[index]); + read_position += size != 0 ? size : RingWords - index; + // Hand ring space back now and then while working through a long batch. + if (read_position - last_store >= 4096 && read_position != available) { + consumed.store(read_position, std::memory_order_release); + last_store = read_position; + } + } + g_back_busy.store(false, std::memory_order_release); + consumed.store(read_position, std::memory_order_release); + } +} + +} // namespace VideoCommon -- cgit v1.2.3 From 15e953cafd5cb348833cb373d7ddd996b81307f7 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:55:07 -0400 Subject: 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. --- NX2_GPU_THREAD_SPLIT.md | 3 ++- NX2_NOTES.md | 3 ++- src/video_core/engines/maxwell_3d.cpp | 4 ++- src/video_core/gpu_split.cpp | 36 +++---------------------- src/video_core/gpu_split.h | 51 +++++++++++++++++++++++++++++------ src/video_core/nx2_profile.h | 6 +++-- 6 files changed, 58 insertions(+), 45 deletions(-) (limited to 'src/video_core/gpu_split.cpp') 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=` (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=` (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(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(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(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(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(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 @@ -103,7 +122,16 @@ private: (*reinterpret_cast(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 published{}; alignas(64) std::atomic consumed{}; alignas(64) std::atomic 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(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 -- cgit v1.2.3 From 092c16d6f39b395e7b3e105a42963733bf9de045 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:54:19 -0400 Subject: gpu_split: reset the ring when the split stops GpuSplit is a static instance and outlives the emulation session. Stop() joined the back thread but kept the ring positions and engine pointers, so the next session's back thread started reading at 0 of a new, empty ring while `published` still held the old session's position: each empty entry reads as a wrap, the back thread skipped ahead a ring at a time and never landed on the old position. The front then spun in Drain(), the next game's first GPU work never ran, and it waited on its first syncpoint forever (Mario Kart World loaded after another Switch 2 game: syncpoint 2 stuck at 0, waiting for 2). The back thread also held the previous session's freed engines. Stop() now drops the ring and resets the positions, the engines and the front macro mode, so MaybeStart() begins a clean ring. --- src/video_core/gpu_split.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'src/video_core/gpu_split.cpp') diff --git a/src/video_core/gpu_split.cpp b/src/video_core/gpu_split.cpp index 07fda589ff..f913ca82ce 100644 --- a/src/video_core/gpu_split.cpp +++ b/src/video_core/gpu_split.cpp @@ -122,6 +122,21 @@ void GpuSplit::Stop() { wake.notify_one(); back_thread.join(); active = false; + // The split is static and outlives the emulation session. The next session's back thread + // starts reading at 0: with the old positions kept it skipped through the new, empty ring a + // wrap at a time and never reached the old write position (the next game's first GPU work + // never ran, it waited on its syncpoint forever), and the engines it held were freed. + ring.reset(); + write_position = 0; + published_local = 0; + consumed_cache = 0; + publish_every = PublishEvery; + front_engine = nullptr; + back_engine = nullptr; + front_macros = false; + published.store(0, std::memory_order_relaxed); + consumed.store(0, std::memory_order_relaxed); + back_waiting.store(false, std::memory_order_relaxed); } u64* GpuSplit::ReserveSlow(size_t words) { -- cgit v1.2.3