aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSssnowyyy <78863999+Sssnowyyy@users.noreply.github.com>2026-11-02 11:34:40 -0500
committerSssnowyyy <78863999+Sssnowyyy@users.noreply.github.com>2026-11-02 14:53:47 -0500
commit256b1dc346e19bc60c2884d4af875fc53ef0a99a (patch)
tree1af39de66d5b0aa451b533d339706e3094939cef
parent690f80e15f411d8fa46a2eecdfc50ddadc8ed5e0 (diff)
video_core: don't execute command lists copied before their channel waited
cce6d44af2 executes each command list entry from a copy of its words taken at submit. Games submit command lists before writing them: the lists wait behind a semaphore acquire, and the game writes their commands just before it releases the acquire (Mario Kart World after present). The copies of those lists were stale. Pokopia stopped after its first frame on macOS: its GPU never reached the fence the game spun on (EDEN_NX2_PB_SNAPSHOT=0 made it run again). Count each channel's suspensions on an acquire and the resumes after them, and record the count with each copy. A list copied while its channel was suspended, or whose channel suspended since, reads guest memory instead. Lists submitted and run without a wait in between keep the copy. EDEN_NX2_PB_SNAPSHOT=0 still disables it altogether. Assisted-by: Claude Sonnet 5.5 (first version); Claude Opus 5.5 (bisect, suspension count, testing and commit message)
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp5
-rw-r--r--src/video_core/dma_pusher.cpp3
-rw-r--r--src/video_core/dma_pusher.h9
-rw-r--r--src/video_core/engines/puller.cpp2
4 files changed, 18 insertions, 1 deletions
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
index 848c15d6f8..79586446d5 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
@@ -445,7 +445,10 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL
const char* value = std::getenv("EDEN_NX2_PB_SNAPSHOT");
return value == nullptr || value[0] != '0';
}();
- if (snapshot && channel_state->memory_manager) {
+ if (snapshot && channel_state->memory_manager && channel_state->payload) {
+ // Before the copy: a suspension during it must count as one after it.
+ entries.snapshot_suspensions =
+ channel_state->payload->dma_pusher.suspensions.load(std::memory_order_acquire);
size_t words = 0;
for (const auto& entry : entries.command_lists) {
words += entry.size;
diff --git a/src/video_core/dma_pusher.cpp b/src/video_core/dma_pusher.cpp
index 6b93bb7f60..6a79e0570e 100644
--- a/src/video_core/dma_pusher.cpp
+++ b/src/video_core/dma_pusher.cpp
@@ -162,6 +162,9 @@ bool DmaPusher::Step() {
// read: their words at submit time are an older frame's.
const bool submitted = command_resume_offset == 0 &&
dma_pushbuffer_subindex < command_list.submitted_offsets.size() &&
+ (command_list.snapshot_suspensions & 1) == 0 &&
+ command_list.snapshot_suspensions ==
+ suspensions.load(std::memory_order_acquire) &&
!IsGpuWrittenCommands(dma_state.dma_get,
size_t{header.size} * sizeof(u32)) &&
!(use_safe && memory_manager.IsMemoryDirty(dma_state.dma_get,
diff --git a/src/video_core/dma_pusher.h b/src/video_core/dma_pusher.h
index 289786bb9e..feab652bb5 100644
--- a/src/video_core/dma_pusher.h
+++ b/src/video_core/dma_pusher.h
@@ -7,6 +7,7 @@
#pragma once
#include <array>
+#include <atomic>
#include <condition_variable>
#include <optional>
#include <span>
@@ -121,6 +122,10 @@ struct CommandList final {
/// executed instead of the memory, which the game may already be refilling for a later frame.
std::vector<CommandHeader> submitted_words;
std::vector<u32> submitted_offsets;
+ /// DmaPusher::suspensions when the words were copied. A list copied during a suspension, or
+ /// that waited behind one, reads memory instead: games write the commands it runs before they
+ /// release the acquire.
+ u64 snapshot_suspensions = 0;
};
/// FNV-1a over pushbuffer words (EDEN_NX2_PB_CHECK).
@@ -139,6 +144,10 @@ inline u64 HashPushbufferWords(std::span<const u32> words) {
/// details on this implementation.
class DmaPusher final {
public:
+ /// Counts suspensions on a semaphore acquire and the resumes after them, so it is odd while
+ /// the channel is suspended (see CommandList::snapshot_suspensions).
+ std::atomic<u64> suspensions{0};
+
explicit DmaPusher(Core::System& system_, MemoryManager& memory_manager_, Control::ChannelState& channel_state_);
~DmaPusher();
diff --git a/src/video_core/engines/puller.cpp b/src/video_core/engines/puller.cpp
index 0bb1809aa3..37ac3ec851 100644
--- a/src/video_core/engines/puller.cpp
+++ b/src/video_core/engines/puller.cpp
@@ -116,6 +116,7 @@ bool Puller::ResumeAcquire(DmaPusher& dma_pusher) {
return false;
}
pending_acquire.reset();
+ dma_pusher.suspensions.fetch_add(1, std::memory_order_release);
Nx2SemaphoreWaiters::Get().Remove(this);
return true;
}
@@ -257,6 +258,7 @@ void Puller::ProcessSemaphoreTriggerMethod(DmaPusher& dma_pusher) {
dma_pusher.rasterizer->ReleaseFences();
if (!waiters.Latched(this) && !acquire.Matches(ReadSemaphoreWord(dma_pusher, acquire.address))) {
pending_acquire = acquire;
+ dma_pusher.suspensions.fetch_add(1, std::memory_order_release);
VideoCommon::Nx2Profile::Add(VideoCommon::Nx2Profile::Counter::AcquireWaits, 1);
} else {
waiters.Remove(this);