Repository navigation
puller: fix delayed-fence deadlocks — complete stalled flip-counter ring rounds #712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: fix/oot-blackscreen
Are you sure you want to change the base?
Changes from all commits
8b2c595
b0e1ba2
554bd5c
0466dc8
8305cff
df31a2f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,11 +118,29 @@ bool Puller::CanResumeAcquire(DmaPusher& dma_pusher) { | |
| pending_acquire->Matches(ReadSemaphoreWord(dma_pusher, pending_acquire->address))) { | ||
| return true; | ||
| } | ||
| // The value still hasn't landed: flush the scheduler before forcing the release | ||
| // again. A forced release waits on the fence chain, which can sit on recorded but | ||
| // unsubmitted commands (DeferFlush's batching, or a back whose idle hook already | ||
| // ran and sleeps); waiting without a flush can wedge the back itself, and with | ||
| // every channel parked on GPU semaphores whose payloads ride those fences nothing | ||
| // ever recovers (Fast + Balanced/Accurate fences deadlocked about a minute in, | ||
| // Ocarina of Time vistas: channels wanting 0xf259/0xf26b while the GPU counter | ||
| // semaphores sat 13-19 values behind with an empty buffer cache). | ||
| VideoCommon::GpuSplit::Get().Run([rasterizer = dma_pusher.rasterizer] { | ||
| rasterizer->FlushCommands(); | ||
| rasterizer->ReleaseFences(); | ||
| }); | ||
| } | ||
| if (!pending_acquire->reported && | ||
| std::chrono::steady_clock::now() - pending_acquire->since > std::chrono::seconds{2}) { | ||
| pending_acquire->reported = true; | ||
| const auto device = dma_pusher.memory_manager.GpuToCpuAddress(pending_acquire->address); | ||
| // Deadlock hunt: arm the semaphore write watch on this block so every later write | ||
| // (and its source) lands in the log. | ||
| Nx2SemaphoreWaiters::Get().MaybeArmWatchFromEnv(); | ||
| if (device) { | ||
| Nx2SemaphoreWaiters::Get().ArmWatch(*device); | ||
| } | ||
| LOG_WARNING(HW_GPU, "Channel {} waits on semaphore {:#x} (op {}, {:#x}, now {:#x}) for 2 s; " | ||
| "device {:#x}, buffer cache holds GPU data: {}, words {:08x} {:08x} {:08x} {:08x}", | ||
| dma_pusher.channel_state.bind_id, pending_acquire->address, | ||
|
|
@@ -133,6 +151,52 @@ bool Puller::CanResumeAcquire(DmaPusher& dma_pusher) { | |
| dma_pusher.memory_manager.Read<u32>((pending_acquire->address & ~GPUVAddr{0xf}) + 8), | ||
| dma_pusher.memory_manager.Read<u32>((pending_acquire->address & ~GPUVAddr{0xf}) + 12)); | ||
| } | ||
| // A passed equality never returns. These semaphores are monotonically bumped counters, | ||
| // and with the emulator's looser channel ordering the writers can already be past an | ||
| // AcquireEqual target by the time the waiting channel reaches it (2026-11-06 hang: | ||
| // channels wanting 0x48 while the word read 0xac) — hardware orders the channel against | ||
| // the writers, we cannot. A short grace period (10 ms) covers a write racing the read; | ||
| // after that the greater-or-equal reading the game's protocol implies is taken. This | ||
| // fires constantly during boot (the ring passes targets by whole rounds there), so the | ||
| // grace must stay far below the 2 s warning — gating it on the warning turned boot into | ||
| // one round per two seconds. | ||
| if (std::chrono::steady_clock::now() - pending_acquire->since >= | ||
| std::chrono::milliseconds{10} && | ||
| static_cast<GpuSemaphoreOperation>(pending_acquire->operation) == | ||
| GpuSemaphoreOperation::AcquireEqual && | ||
| current > pending_acquire->value) { | ||
| LOG_INFO(HW_GPU, | ||
| "Channel {} acquire satisfied by passage: semaphore {:#x} wanted {:#x}, now {:#x}", | ||
| dma_pusher.channel_state.bind_id, pending_acquire->address, | ||
| pending_acquire->value, current); | ||
| return true; | ||
| } | ||
|
Comment on lines
+163
to
+173
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| // A ring round that never lands. The flip-counter semaphores advance in lockstep as a | ||
| // depth-19 ring of fence-carried releases, and every stuck wait's target is exactly | ||
| // current + 19: the value the next round writes, already committed by the game's | ||
| // protocol (2026-11-06 hangs: 0x1a25b at 0x1a248, 0x10d05 at 0x10cf2, 0x2657d at | ||
| // 0x2656a — all 19). When every carrier channel is itself parked behind such an | ||
| // acquire, the fence chain holds no future release and nothing ever recovers. Complete | ||
| // the round ourselves: write the target the game already queued, notify the waiters. | ||
| // 100 ms: above normal frame pacing (a legitimate round completes within a frame or | ||
| // two), far below the seconds a real stall would cost. | ||
| if (std::chrono::steady_clock::now() - pending_acquire->since >= | ||
| std::chrono::milliseconds{100} && | ||
| static_cast<GpuSemaphoreOperation>(pending_acquire->operation) == | ||
| GpuSemaphoreOperation::AcquireEqual && | ||
| pending_acquire->value > current && pending_acquire->value - current <= 0x40) { | ||
| const auto device = dma_pusher.memory_manager.GpuToCpuAddress(pending_acquire->address); | ||
| if (device) { | ||
| dma_pusher.memory_manager.Write<u32>(pending_acquire->address, pending_acquire->value); | ||
| Nx2SemaphoreWaiters::Get().OnWrite(*device, pending_acquire->value, "ring-rescue"); | ||
| LOG_INFO(HW_GPU, | ||
| "Channel {} acquire satisfied by ring rescue: semaphore {:#x} advanced " | ||
| "{:#x} -> {:#x}", | ||
| dma_pusher.channel_state.bind_id, pending_acquire->address, current, | ||
| pending_acquire->value); | ||
| return true; | ||
| } | ||
| } | ||
|
Comment on lines
+183
to
+199
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| return false; | ||
| } | ||
|
|
||
|
|
@@ -227,6 +291,9 @@ void Puller::ProcessFenceActionMethod(DmaPusher& dma_pusher) { | |
| void Puller::ProcessSemaphoreTriggerMethod(DmaPusher& dma_pusher) { | ||
| const auto semaphoreOperationMask = 0xF; | ||
| const auto op = GpuSemaphoreOperation(regs.semaphore_trigger & semaphoreOperationMask); | ||
| // Deadlock hunt: arm the EDEN_NX2_SEM_TRACE write watch on the first semaphore op, so | ||
| // the flip-counter cadence is logged from boot, not just after a stuck acquire. | ||
| Nx2SemaphoreWaiters::Get().MaybeArmWatchFromEnv(); | ||
| // EDEN_NX2_SEM_WATCH=<gpu address>: log every release/acquire on the 16 bytes there. | ||
| static const GPUVAddr watch = [] { | ||
| const char* value = std::getenv("EDEN_NX2_SEM_WATCH"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: Djihads80/Eglantine
Length of output: 7979
Script executed:
Repository: Djihads80/Eglantine
Length of output: 16029
Complete the download before logging the watch.
When a download intersects both a CPU-written range and the watched block,
WriteDownloadedMemoryreturns without writing any part of the download.PopAsyncBufferscan then mark the skipped GPU data clean. The watch can also arm automatically after a two-second acquire wait.Write the parts outside
cpu_writtenbefore logging. Preserve the CPU-written parts.