Repository navigation
Description
Tracking issue for the Donkey Kong Bananza GPU hangs, the stream corruption behind them, and the work still to do. It is written so the investigation can be resumed cold.
Symptoms
Bananza freezes during gameplay; fast, repeated inputs reproduce it most reliably (usually within 1 to 3 minutes). The log shows one of two signatures, and both turn out to be the same bug:
- Stream corruption: a channel suddenly parses garbage. You see
Dropping method ... on unbound subchannel,Unimplemented engine ...,Invalid KeplerMemory register,Native compute method out of range,Native QMD failed: unsupported QMD version or queue, andunknown host methodwith a count such as 2047. - Semaphore hang:
Channel 2 waits on semaphore 0x2161?1b000 (op 1, 0x2, now 0xffffffff)on device address0x5cd1c000. Channel 1 is usually also stuck:waits on semaphore 0x210?4e0000 (op 4, N, now M)on device0xe1000, with N far ahead of M.
Root cause (current understanding)
-
The semaphore hang is a consequence of the stream corruption. Device
0x5cd1c000is the first page of a 0x40000 block the game maps withsvc 0x80at about 1.86 s, right before it creates its channels. Its GPU VA changes per run; its device address does not. Each frame, channels 1 and 2 run a fixed handshake on it:- Channel 1 resets 5 words to
0xffffffff. - Channel 1 releases +0 = 0. Channel 2 acquires +0 == 0.
- Channel 2 releases +4 = 1, through a Native QMD release. Channel 1 acquires +4 == 1.
- Channel 1 releases +0 = 2. Channel 2 acquires +0 == 2.
- Channel 2 releases +4 = 3. Channel 1 acquires +4 == 3.
Exactly one reset happens per cycle; about 12k resets happen during loading, before the cycles start. In the hangs, channel 1's stream got corrupted mid-cycle. It skipped its own releases and acquires, then waited on a garbage counter far ahead of the real value. Channel 2 then waits forever for a value that never comes.
- Channel 1 resets 5 words to
-
The corruption comes from reading command memory the game had already refilled for a later frame. The game rewrites command memory after submitting it: present returns frames ahead, which Mario Kart World also does.
nvhost_gpu.cpphandles this by snapshotting each entry's words at submit time and executing the snapshot. However, any entry on a 4 KiB page that the GPU ever wrote skips the snapshot: the!IsGpuWrittenCommands(...)term in thesubmittedcondition inDmaPusher::Step. That bit is sticky (g_nx2_gpu_written_pages) and page-granular, so CPU-written command chunks that share a page with GPU-written data are read live. By parse time, the game has already overwritten them.- Evidence: in the trace run, the corruption at 152.23 s was
unknown host methodat get=0x210bf59c44.- Parsed entry -0 (
0x210bf59c44+0x6) was read on theunsafepath; all 6 of its words differ from the submit snapshot. - Parsed entry -8 (
0x210bf59c14+0xc) was also readunsafe; 11 of its 12 words differ. - The
EDEN_NX2_CMD_WRITESrecorder logged no emulator-side writes (DeviceMemoryManager::WriteBlock*) into them, so the guest CPU rewrote them.
- Parsed entry -0 (
- The earlier observation of chunks in the
0x210bxxxxx/0x210cxxxxxrings being a fragment at parse time and a proper chunk later fits this cause too.
- Evidence: in the trace run, the corruption at 152.23 s was
Fix under test (not yet verified)
Commit d3b4fb5274, behind the env var EDEN_NX2_EPOCH_SNAPSHOT=1:
g_nx2_submit_epochis incremented per submission.- Each submission records
CommandList::submit_epoch. Nx2MarkGpuWrittenstores the current epoch per 4 KiB page ing_nx2_gpu_written_epoch(64 MiB of bss).IsGpuWrittenCommands(addr, size, since_epoch)returns true, so the entry is read live, only if the GPU wrote the page at or after the entry's submission. Otherwise the snapshot is used.
Risk: a GPU write recorded before the submit but not yet in guest memory when the snapshot was taken would make the snapshot stale. Watch for missing terrain, trees or culled objects, the case the GPU-written path was added for.
A test run was launched but has no result yet. Next steps:
- Run with
EDEN_NX2_EPOCH_SNAPSHOT=1and fast inputs. Check for hangs or corruption, and for missing geometry. - If it holds, make it the default and remove the env gate. Consider a finer granularity, or drop the sticky bit in favour of the epoch.
- If corruption persists, use the
PB_TRACEdump: theparsed -Nlines give each entry's read path and how many words differ from the snapshot. - Check the case of an entry submitted after a pending GPU write. One option is to compare the snapshot with live memory when the epoch says snapshot, and log the mismatches.
Branch and builds
- Branch:
fix/bananza-terrain-fullonjojo-chaechae/Eglantine. Local worktree:~/edenpack-test/bananza-pr. - Builds use
local-build.ymlon the self-hosted runner:gh workflow run local-build.yml -R jojo-chaechae/Eglantine --ref fix/bananza-terrain-full -f linux_pgo=true -f windows=false -f android=false -f release=false docker cp eglantine-runner:/home/runner/runner/_work/Eglantine/Eglantine/out/eglantine-jojo-<sha>-linux-x86_64-pgo.AppImage <dir> - Run:
Copy the log out after each run, because the next launch truncates it.
: > ~/.local/share/eden/log/eden_log.txt ENV... <AppImage> -g ".../DKBananza-458752.nup"
Commits after 5c28733071, in order:
| Commit | Kind | What |
|---|---|---|
53fce4d58c |
revert | Reverts the "split method waits for the entry that resumes its buffer" park heuristic. It froze, and the lookahead is invalid. |
8db3e04f1a |
diag | EDEN_NX2_PB_DUMP=<file> dumps the submission at the first split method resumed in a later entry. Binary format: u64 count, u64 current, then per entry u64 addr, u32 size, words. Analyser: ~/edenpack-test/tools/pbsplit.py. |
070f1772b9 |
diag | EDEN_NX2_SNAPSHOT_CHECK logs live reads that differ from the snapshot. Skips size-5 entries. |
51ac02623a |
diag | Stuck-acquire second-page report (superseded). |
2cfdf53510 |
fix | Semaphore mirror regions use an unbounded set instead of a 32-slot table. Games register well over 32; 140 were dropped by 4.4 s. Real fix, keep it. |
72c79f0e02 |
diag | The trace dump shows each parsed entry's read path (snapshot/safe/list copy/unsafe) and its diff against the snapshot. |
cc52b2b271, be02ddbea4 |
diag | EDEN_NX2_CMD_WRITES: a 4096-entry ring of emulator-side writes to submitted command pages, printed in the trace dump. |
f179d08a2d |
diag | Logs nx2:gpu semaphore region add/remove, and on a stuck acquire logs the second page and mirror state. |
f3e53802b3, b2407f2a20 |
diag | EDEN_NX2_SEM_WATCH_DEV=<device addr> logs releases, acquires, SEM_EXECUTE and device writes on the 2 pages there, tagged with the query-cache channel. |
7138100c58 |
diag fix | DumpCurrentEntry reads CPU memory with ReadBlockUnsafe. Read32 could flush a GPU-written page from the GPU thread and deadlock: a black screen right after the main menu. |
d3b4fb5274 |
experiment | EDEN_NX2_EPOCH_SNAPSHOT (above). |
The diagnostic commits need squashing or dropping before anything goes upstream.
Useful env vars:
EDEN_NX2_PB_TRACE=1: cheap ring of the last 64 parsed entries, dumped at the first unbound-subchannel method or unknown host method.EDEN_NX2_PB_CHECKEDEN_NX2_CMD_WRITES=1EDEN_NX2_SEM_WATCH_DEV=5cd1c000EDEN_NX2_SEM_WATCH=<gpu va>EDEN_NX2_PB_DUMP=<file>: the value is the output path.=1writes a file literally named1.
Logs (local)
Logs are in ~/edenpack-test/logs/b167/:
| Log | Shows |
|---|---|
bananza-semlog.log |
The stuck region was never registered or mirrored. |
bananza-devwatch.log |
The handshake protocol and the early-reset hang. |
bananza-devwatch2.log |
The dump deadlock, plus channel attribution. |
bananza-devwatch3.log |
Corruption leading into the semaphore hang. |
bananza-trace.log |
The corruption dump showing snapshot mismatches. |
bananza-epoch.log |
The pending experiment. |
Notes are in ~/edenpack-test/SCRATCHPAD.md.
Other findings, not fixed
KeplerMemory::ConsumeSinkImplwritesregs.reg_array[method]without a bounds check. Garbage streams hit it:Invalid KeplerMemory registerasserts.- Most split methods are legitimate and not part of this bug. Hardware carries method state across GP entries (open-gpu-doc
dev_pbdma). The usual pattern is a macro0xe50/0xe51header with 5 parameters left, followed by a 5-word GPU-written record. - Entry
0x210f6c41f0+0x146, word 176, is a per-frame pointer cycling0bfeb000/0d233000/0e47b000. It was read live with a different frame's value. It may relate to jitter; it is probably the same snapshot-vs-live issue. - Rubberbanding is deliberately parked.
Pending after this
- Contribute to draft Experimental Donkey Kong Bananza terrain rendering fixes #363: reset/pointer guard, F2I byte fix, FP16 texture fix, HMMA default, and the semaphore region set fix (
2cfdf53510). Each upstream PR and comment needs a separate approval.
The human-written contributing guideline was not asserted for this issue.
Activity
jojo-chaechae commentedon Nov 3, 2026
do not f' ing close this or delete this issue istg
jojo-chaechae commentedon Nov 3, 2026
documenting this here as ill pick up on this work later.. erm hopefully
ymatrix1 commentedon Nov 4, 2026
Data point from a different setup, in case it helps narrow this down. I could not confirm the stale-rewrite explanation on current nx2, and
EDEN_NX2_EPOCH_SNAPSHOT=1did not prevent the corruption in my runs.Setup. Windows 11, RX 5700, MSVC, loose dump (
base/exefs/main,base/romfshas the full data, 0 unmapped writes at boot), Bananza v3.2.0. Build: nx2d3a659982fplus my local #306 change (Ampere lowering only) and a test-only scripted-input patch. I portedd3b4fb5274onto it by hand: the epoch is passed to bothIsGpuWrittenCommandscalls that feedgpu_writteninDmaPusher::Step, which nx2 now computes before thesubmittedcondition. I did not port2cfdf53510(it conflicts innx2.h). Everything ran in a portableuserfolder.Two otherwise identical headless runs, scripted
+/Apresses,EDEN_NX2_PB_CHECK=1, GPU split not active:EPOCH_SNAPSHOTEPOCH_SNAPSHOT=1unknown host method: method=0x1 count=8063Invalid GPU address for shader 0x0000200205f200Same corruption afterwards in both:
Dropping method ... on unbound subchannel, theSpecial puller engine method 0x00..0x3F not implementedflood, a bogus render target format andCreateImage ... layers ... failed. A manual run of mine with the variable set (I could not verify it was exported) ended the same way at 61.9 s.What doesn't fit the stale-read explanation. In the run without the variable, all 64 entries in the
PB_CHECKring before the failure, including the failing0x217efd7cd0+0x31, are reportedunchanged: the words read at parse time hash the same as at submit. So for this reproduction the game had not rewritten those entries. The few that did change (0x210f0c71e8+0x143, 3 times around 50 s) are not the failing ones.Two more observations, no conclusion drawn from them:
DumpCurrentEntryprintedentry 2232 of 8054: addr=0x2238bb6c08 size=0x96right after reporting the failure atget=0x217efd7cd0. Those are different entries, so the dump may not be showing the one that failed.If useful I can rerun with other env vars from your branch (
PB_TRACE,CMD_WRITES) once they are on current nx2.nx2: wait for each of Donkey Kong Bananza's frames
ymatrix1 commentedon Nov 4, 2026
Follow-up to my earlier comment here, which was measured on a base that is now out of date.
Current nx2 (
9279b24e4c) does not reproduce this for me. Same machine as before (Windows 11, RTX 4070 SUPER, loose dump, Bananza v3.2.0, portable user folder), plain nx2 plus a test-only scripted-input patch, no env vars, played by hand through the door and into the lava cave:unbound subchannel/Native QMD failed/Invalid GPU address/unknown host method, zero fence stalls, zero semaphore waits, and the new frame-wait counter reports0 ran a frame ahead.What I measured on the older base (
d3a659982f), in case it helps the history. Three configurations ofEDEN_NX2_FRAMES_AHEAD, same scripted run:0(always wait for this frame's fence): clean for 130 s, zero desync, zero entries changed after submit.1(always wait for the previous frame): the stream corrupted at present Alternate NS2 patch set with keyboard and macro performance support #2, 10.5 s in.EDEN_NX2_EPOCH_SNAPSHOT=1did not help on that base, nor did turning the snapshot or the GPU-written list copy off, nor the usage-tracker fix from #462. So on that base the corruption followed how far ahead the game was allowed to run, which fits the frame-wait change in #506 and the snapshot fixes that landed afterwards. This is one machine and a handful of runs, not a proof, and I did not bisect which of those commits is the one that matters.nx2: wait for each of Donkey Kong Bananza's frames