From 690f80e15f411d8fa46a2eecdfc50ddadc8ed5e0 Mon Sep 17 00:00:00 2001 From: Sssnowyyy <78863999+Sssnowyyy@users.noreply.github.com> Date: Mon, 2 Nov 2026 11:34:01 -0500 Subject: nx2: don't stall presents on a current-frame wait the GPU can't finish 0f14696044 made PresentClientBuffer also wait for the current frame's fence whenever the previous frame's wasn't reached. Mario Kart World's GPU waits on semaphores its CPU releases only after the present returns, so in that state the current fence can't be reached until the present returns, and each such present stalled up to the 4 s deadline. Wait for the current frame by default, but give up after a few ms once a GPU channel was seen suspended on a semaphore acquire and wait for the previous frame instead. EDEN_NX2_FRAMES_AHEAD=1 always waits for the previous frame (with 0f14696044's current-frame wait when the GPU is behind), =0 always for the current one. The scheduler gains a lock-free flag so the present thread can see suspended channels. Assisted-by: Claude Sonnet 5.5 (implementation and commit message) --- src/video_core/gpu.cpp | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/video_core/gpu.cpp') diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 53f8f063f7..a6f31359a3 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -505,6 +505,10 @@ bool GPU::IsAsync() const { return impl->is_async; } +bool GPU::HasSuspendedChannels() const { + return impl->scheduler.HasPendingChannelsAnyThread(); +} + bool GPU::UseNvdec() const { return impl->use_nvdec; } -- cgit v1.2.3 From 9f4f01f76e8556941c0c0c3f57edc1d600ed23ea Mon Sep 17 00:00:00 2001 From: Sssnowyyy <78863999+Sssnowyyy@users.noreply.github.com> Date: Mon, 2 Nov 2026 13:18:13 -0500 Subject: nx2: map GPU memory before later command lists are read 837133792d queues the GMMU maps on the GPU thread (GPU::RunInOrder) so the service thread never waits for the texture cache. With the GPU split (Low GPU accuracy) the GPU thread defers queued functions to its back, but its front reads the command lists: it read the lists submitted after a map through GPU addresses that weren't mapped yet. Pokopia stopped after its first frame. Add GPU::MapInOrder for the GMMU maps and the sparse tile remaps. It runs in order like RunInOrder, and with the split it runs on the front after draining the back, before the front reads further command lists. The service thread still never takes the texture cache lock, and the back is idle while the map changes the caches. Unmaps and the other queued work still run on the back. Assisted-by: Claude Opus 5.5 (bisect, fix, testing and commit message) --- src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp | 6 +++--- src/video_core/gpu.cpp | 8 ++++++++ src/video_core/gpu.h | 4 ++++ src/video_core/gpu_thread.cpp | 11 ++++++++--- src/video_core/gpu_thread.h | 5 ++++- 5 files changed, 27 insertions(+), 7 deletions(-) (limited to 'src/video_core/gpu.cpp') diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp index 843d31caf1..c5b23bef3e 100644 --- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp +++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp @@ -176,7 +176,7 @@ void nvhost_as_gpu::Nx2Map(u64 gpu_address, u64 cpu_address, u64 size) { Nx2GpuMemoryRegistry::Get().RecordDeviceRange(cpu_address, size, device_address); // On the GPU thread, like unmaps: a GMMU map takes the texture cache lock, and the callers hold // nx2:gpu locks that the GPU thread takes under that lock (sparse blocks backed on write). - system.GPU().RunInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] { + system.GPU().MapInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] { gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false); }); LOG_DEBUG(Service_NVDRV, "Switch 2 GPU mapping {:#x}+{:#x} -> CPU {:#x}", gpu_address, size, @@ -253,7 +253,7 @@ void nvhost_as_gpu::Nx2Sparse(u64 gpu_address, u64 size, bool create) { } nx2_sparse_ranges[gpu_address] = {size, device_address}; // On the GPU thread, as in Nx2Map. - system.GPU().RunInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] { + system.GPU().MapInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] { gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false); }); LOG_DEBUG(Service_NVDRV, "Switch 2 sparse region {:#x}+{:#x} -> device {:#x}", gpu_address, @@ -295,7 +295,7 @@ void nvhost_as_gpu::Nx2SparseTile(u64 gpu_address, u64 cpu_address, u64 size) { // whose contents are now what the game's memory holds there. const DAddr device = device_address + (gpu_address - start); auto& smmu = system.Host1x().MemoryManager(); - system.GPU().RunInOrder([&smmu, device, cpu_address, size, asid = session->asid] { + system.GPU().MapInOrder([&smmu, device, cpu_address, size, asid = session->asid] { smmu.Unmap(device, size); if (cpu_address != 0) { smmu.Map(device, cpu_address, size, asid, true); diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index a6f31359a3..444dc6d015 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -210,6 +210,10 @@ struct GPU::Impl { gpu_thread.Run(std::move(func), is_async); } + void MapInOrder(std::function&& func) { + gpu_thread.Run(std::move(func), is_async, true); + } + /// Notify rasterizer that any caches of the specified region should be flushed to Switch memory void FlushRegion(DAddr addr, u64 size) { gpu_thread.FlushRegion(addr, size, is_async); @@ -553,6 +557,10 @@ void GPU::RunInOrder(std::function&& func) { impl->RunInOrder(std::move(func)); } +void GPU::MapInOrder(std::function&& func) { + impl->MapInOrder(std::move(func)); +} + void GPU::InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes) { impl->InvalidateRegion(addr, size, preserve_gpu_writes); } diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h index 4afc89f0e6..d37dfa5990 100644 --- a/src/video_core/gpu.h +++ b/src/video_core/gpu.h @@ -257,6 +257,10 @@ public: /// Runs func on the GPU thread after the command lists submitted so far. void RunInOrder(std::function&& func); + /// RunInOrder for GPU memory maps: also done before the GPU thread reads the command lists + /// submitted later (with the GPU split, on its front rather than its back). + void MapInOrder(std::function&& func); + /// Notify rasterizer that any caches of the specified region should be invalidated void InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes = false); diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp index 55200b0647..3af8cb65a8 100644 --- a/src/video_core/gpu_thread.cpp +++ b/src/video_core/gpu_thread.cpp @@ -99,7 +99,12 @@ void ThreadManager::StartThread(VideoCore::RendererBase& renderer, Core::Fronten } else if (std::holds_alternative(next.data)) { split.Defer([gpu = &system.GPU()] { gpu->TickWork(); }); } else if (auto* run = std::get_if(&next.data)) { - split.DeferFunction(std::move(run->func)); + if (run->before_later_reads) { + split.Drain(); + run->func(); + } else { + split.DeferFunction(std::move(run->func)); + } } else if (const auto* flush = std::get_if(&next.data)) { split.Defer([rasterizer_ptr, addr = flush->addr, size = flush->size] { rasterizer_ptr->FlushRegion(addr, size); @@ -155,8 +160,8 @@ void ThreadManager::TickGPU(bool is_async) { PushCommand(GPUTickCommand(), false, is_async); } -void ThreadManager::Run(std::function&& func, bool is_async) { - PushCommand(RunCommand{std::move(func)}, false, is_async); +void ThreadManager::Run(std::function&& func, bool is_async, bool before_later_reads) { + PushCommand(RunCommand{std::move(func), before_later_reads}, false, is_async); } void ThreadManager::InvalidateRegion(DAddr addr, u64 size) { diff --git a/src/video_core/gpu_thread.h b/src/video_core/gpu_thread.h index 796c0d89f6..927453dab0 100644 --- a/src/video_core/gpu_thread.h +++ b/src/video_core/gpu_thread.h @@ -80,6 +80,9 @@ struct GPUTickCommand final {}; /// Command to run a function on the GPU thread after the commands queued before it struct RunCommand final { std::function func; + /// With the GPU split, run on its front (after draining the back) instead of deferring to the + /// back, so command lists read after this one see what func did (GMMU maps). + bool before_later_reads{}; }; using CommandData = @@ -132,7 +135,7 @@ public: void TickGPU(bool is_async); /// Runs func on the GPU thread once the commands queued before it have been processed. - void Run(std::function&& func, bool is_async); + void Run(std::function&& func, bool is_async, bool before_later_reads = false); void NotifyShutdown(); -- cgit v1.2.3