Skip to content

Splatoon Raiders (0400380020056000) holds its frame rate at the first boss's third stage: a submission's pending-write list holds 32,768 writes (past 8,192 it counted as writing everywhere) - #1435

Closed
dpadr wants to merge 3 commits into
seedds:mainfrom
dpadr:fix/pend-list-capacity

Conversation

@dpadr

@dpadr dpadr commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

What and why

At the third stage of Splatoon Raiders' first boss fight (0400380020056000), the game dropped to 1-2 fps and stayed there until the stage ended. The GPU was busy 420-600 ms a frame, with up to 1,025 render passes a frame, against about 100 in the rest of the fight.

Cause. The stage measures its ink map with about 1,550 occlusion queries a frame. Before each query it makes 7 inline uniform updates, which comes to 11,010 queued writes in one submission (about 2,800 in the game's other fights). The host's list of a submission's queued writes (pend_add) held 8,192 entries. Past that limit:

  1. The submission counted as a write anywhere (g_pend_over).
  2. gw_commit carried that to later submissions while it ran (g_gw_all).
  3. Every partly covered uniform shadow was refused, and each refusal ended the render pass.
  4. Each query became a one-draw pass on the 6400x6400 map, the GPU fell behind, the overflowing submissions were still running at the next frame's draws, and the refusals went on.

Change.

  • PEND_MAX 8,192 → 32,768. It stays under 65,536 because g_pend_upd is a 16-bit index + 1, which a static assert now checks.
  • PEND_NODES 16,384 → 65,536, and 2,048 → 8,192 hash buckets.
  • The tables take about 1.9 MB instead of 0.5 MB. An overflow still counts as a write anywhere.

Diagnostic. A pass dump now names the host function and line that ended each render pass: E <pass> pass ends in <function>:<line>, through a rp_end() macro over rp_end_at(__func__, __LINE__). It only writes while DM_NVN_PASSDUMP is on. Without it, the dump had no record at the point where these passes ended. With it, one run named draw_ → ubd_conflict.

The reference entry is A submission past 8,192 queued writes counted as writing everywhere. The game note's Rendering section and its "Found on this game" list link to it.

Checked

  • Tests:

    • make -C dm/gfx/tests GAME=0400380020056000 check_t_ubp_running: a new case queues 10,850 updates in one submission. It fails 3 checks on main and passes all 11 here.
    • check_t_geometry (1,920 checks) and check_t_geometry_hazards (61), which set g_pend_over, pass.
    • test_tbdr_budget.py, test_docs.py and test_env_switches.py pass.
  • Games run: Splatoon Raiders 0400380020056000, played docked on the Mac through the first boss fight's third stage. The before and after builds differ only by this change: both are main with PRAGMATA's opening load fits an 8 GB Mac's GPU working set: large-pool chunks retire, pools from 128 MB are chunked, and DM_NVN_TEX_MIP_SKIP #1319 merged in and its memory switches on (DM_NVN_TEX_MIP_SKIP=1, DM_NVN_GMEM_RES=0), with DM_NVN_GPU_TS=segment. The "after" run also had the pass-end diagnostic.

    Before After
    Frame time at the stage 400-1,100 ms median 17.7-19.9 ms
    Render passes per frame, most 1,025 118
    Refused uniform shadows 173,452 (161,812 earlier submission running, 11,640 unreadable) 0
    Footprint 13-14 GB 12.6 GB at the end of the run

    The system's swap did not change across the drop: it looked like memory pressure but was not.

  • Devices and OS: MacBook Air M2 (Mac14,2, 10-core GPU), 16 GB, macOS 27.0.

  • regress.py impact origin/main..fix/pend-list-capacity: nvn_mtl.m reaches every NVN game. Of the profiles with a usage manifest: Breath of the Wild, Pokémon Scarlet and Tears of the Kingdom are affected, and Hades is broad (present path only).

  • Not checked yet: scripts/regress.py check --games on those games, which are not on this Mac. The change only raises a limit, so behaviour below 8,192 writes a submission is unchanged. A game that passed the old limit now keeps its shadows instead of ending passes.

Docs

  • The finding is in its game note and its docs/reference/rendering.md entry, and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • A chain tool changed (no chain change)
  • No game files, keys or data extracted from a game in the change or the attachments

🤖 Generated with Claude Code

seedds and others added 3 commits November 5, 2026 18:40
…counted as writing everywhere, and Splatoon Raiders' third boss stage fell to 1-2 fps

The stage queues about 11,000 buffer writes in one submission (7 inline uniform updates before each of about 1,550 ink-map occlusion queries; 2,800 in the game's other fights). Past PEND_MAX the submission counted as a write anywhere (g_pend_over), gw_commit carried that past it (g_gw_all), and every partly covered uniform shadow was refused while it ran: each query ended its render pass on the 6400x6400 ink map (about 1,000 passes a frame, GPU 420-600 ms on a 16 GB M2 Air), the GPU fell behind, the submissions kept running, and the refusals went on. A pass dump with pass-end call sites named draw_ -> ubd_conflict; the host's counters showed 173,452 refusals, 161,812 of them "earlier submission running".

PEND_MAX 32,768 (under 65,536: g_pend_upd is a 16-bit index + 1), PEND_NODES 65,536, and 8,192 hash buckets (the top 13 bits). About 1.9 MB of static tables instead of 0.5 MB. t_ubp_running queues the stage's 10,850 updates: it fails 3 checks before and passes after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ass (E <pass> pass ends in <function>:<line>)

Splatoon Raiders' third boss stage opens about 1,500 one-draw passes a frame on its 6400x6400 ink map, and its pass dump had no record at the point where each ended: the rule had to be found by reading the code. rp_end becomes a macro over rp_end_at(__func__, __LINE__), which writes the record when DM_NVN_PASSDUMP is on (no cost otherwise).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tage: the reference entry and the game's note

The stage's 11,010 queued writes in one submission overflowed the host's 8,192-entry list. From then on every partly covered uniform shadow was refused, and each of about 1,550 ink-map queries a frame ended its render pass (1-2 fps). Played on a 16 GB MacBook Air M2, docked: before, up to 1,025 passes and 173,452 refusals; after, a median of 17.7-19.9 ms, at most 118 passes and none refused. The reference entry is in rendering.md, and the symptom index is updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation game: 0400380020056000 Splatoon Raiders labels Nov 6, 2026
fpedrolucas95 added a commit to fpedrolucas95/nxnative that referenced this pull request Nov 6, 2026
…s its frame rate at the first boss's third stage: a submission's pending-write list holds 32,768 writes (past 8,192 it counted as writing everywhere)
@dpadr
dpadr marked this pull request as ready for review November 6, 2026 06:30
fpedrolucas95 added a commit to fpedrolucas95/nxnative that referenced this pull request Nov 6, 2026
…n Kokiri Forest, about 4 GB less memory) beside this tree's own work

Kept from this tree: the growing raw pool table with two-word hints (plus upstream's read-before-OR on its write
log), seedds#1439's held count, seedds#1436's diagnostics, seedds#1447's texture release, seedds#1435, the idle evictions, the GPU watchdog,
range-tracked GPU writes, the packaged-texture cache (g_pkg_alias), the synchronous task / mesh pipeline build with
prewarm, this tree's F2IP.

Taken from upstream: the pool tables (only what shaders read is made, within a budget; pool draws count as depth
readers only when one was seen reading, replacing seedds#1430's rule; table reuse), sub_cut's busy rule, the indirect
task / mesh draw through mdx, the empty mesh fragment, the larger GPU-write table (65,536 slots), linear textures by
rows, the atomic alias on any 32-bit format, its eventfds (with this tree's select and fcntl on them), its
prepo Send and audio update lock, its shader split and Ocarina profile and tables.

Duplicates both sides added were removed (SDK 22 answers, F2IP, test stubs); RED_ONCE 153 split; t_pool_table
(the replaced pool table code) removed; tests adapted to the taken rules. Translator rebuilt; chain 695a9393d125.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seedds added a commit that referenced this pull request Nov 7, 2026
…ers' first boss fight holds its frame rate at the third stage (PR #1435)

The stage measures its ink map with about 1,550 occlusion queries a frame, 7 inline uniform updates before each: 11,010 queued writes in one submission (2,800 in the game's other fights). The list held 8,192. Past that the submission counted as a write anywhere (g_pend_over), gw_commit carried that past it (g_gw_all), and every partly covered uniform shadow was refused while it ran: each query ended its render pass on the 6400x6400 map, the GPU fell behind, the submissions kept running, and the stage stayed at 1-2 fps (up to 1,025 passes and 173,452 refusals a run on a 16 GB M2 Air; after, a median of 17.7-19.9 ms, at most 118 passes, none refused).

The limit had been raised once before (256 -> 8,192, Monster Hunter Rise), so the list no longer has one: its entries and page nodes grow in pend_room and keep their size for later submissions, g_pend_upd is 32-bit, and g_ubp_idx is sized to the nodes plus the wide list (the old PEND_MAX + PEND_WIDE was below what the nodes allowed). A write anywhere is left for a list that cannot grow and a full wide list. The hash has 8,192 buckets.

A pass dump names the host function and line that ended each render pass (E <pass> pass ends in <function>:<line>): rp_end is a macro over rp_end_at, which writes only while DM_NVN_PASSDUMP is on. One run named draw_ -> ubd_conflict.

t_ubp_running queues the stage's 10,850 updates (3 checks fail with the old limit), then 43,400. regress.py check on Octopath Traveler II: PASS (59.8 fps both, no new red lines, shaders and MSL unchanged).

Co-authored-by: David Padron-Harris <34929160+dpadr@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@seedds

seedds commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Landed as 6d5ed71, with you as co-author. Thanks for the clear diagnosis and the before/after runs.

One change from the PR: instead of raising the fixed limit to 32,768, the list no longer has a limit. Its entries and page nodes grow when a submission needs more room (pend_room) and keep that size, so a steady frame allocates nothing. g_pend_upd is 32-bit, so the 65,536 ceiling and the static assert are gone. This limit had already been raised once (256 → 8,192 for Monster Hunter Rise), and Raiders' query count changes with the scene, so a bigger fixed number would only move the cliff. A submission still counts as writing everywhere only when memory runs out or the 256-entry list of wide writes is full.

Kept as you wrote them: the test (now extended to 43,400 updates, past what a 32,768 limit holds), the reference entry, the game note and the rp_end_at pass-dump diagnostic. Its comment now says the line can be in an .inc file. Your measurements were on the fixed-limit version; the grown list gives the same result for the stage's 11,010 writes. If you replay the third stage on the M2, a confirmation on main would be welcome.

Checked here: t_ubp_running, t_geometry, t_geometry_hazards, t_sbw, and regress.py check on Octopath Traveler II (PASS: 59.8 fps both, shaders and MSL unchanged, no new red lines).

@seedds seedds closed this Nov 7, 2026
john62036203-ops added a commit to john62036203-ops/NXNativeWindows that referenced this pull request Nov 7, 2026
…ds, PR seedds#1435; test link fixes) into switch2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SzykpcKm2eZPvPjGPKx1C
Aznracer pushed a commit to Aznracer/Mighty-Green-Kokiri-Kid that referenced this pull request Nov 8, 2026
…ers' first boss fight holds its frame rate at the third stage (PR seedds#1435)
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…ers' first boss fight holds its frame rate at the third stage (PR seedds#1435)

The stage measures its ink map with about 1,550 occlusion queries a frame, 7 inline uniform updates before each: 11,010 queued writes in one submission (2,800 in the game's other fights). The list held 8,192. Past that the submission counted as a write anywhere (g_pend_over), gw_commit carried that past it (g_gw_all), and every partly covered uniform shadow was refused while it ran: each query ended its render pass on the 6400x6400 map, the GPU fell behind, the submissions kept running, and the stage stayed at 1-2 fps (up to 1,025 passes and 173,452 refusals a run on a 16 GB M2 Air; after, a median of 17.7-19.9 ms, at most 118 passes, none refused).

The limit had been raised once before (256 -> 8,192, Monster Hunter Rise), so the list no longer has one: its entries and page nodes grow in pend_room and keep their size for later submissions, g_pend_upd is 32-bit, and g_ubp_idx is sized to the nodes plus the wide list (the old PEND_MAX + PEND_WIDE was below what the nodes allowed). A write anywhere is left for a list that cannot grow and a full wide list. The hash has 8,192 buckets.

A pass dump names the host function and line that ended each render pass (E <pass> pass ends in <function>:<line>): rp_end is a macro over rp_end_at, which writes only while DM_NVN_PASSDUMP is on. One run named draw_ -> ubd_conflict.

t_ubp_running queues the stage's 10,850 updates (3 checks fail with the old limit), then 43,400. regress.py check on Octopath Traveler II: PASS (59.8 fps both, no new red lines, shaders and MSL unchanged).

Co-authored-by: David Padron-Harris <34929160+dpadr@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400380020056000 Splatoon Raiders nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants