Skip to content

Textures in a sparse pool get stand-ins read through the page table, so PRAGMATA's cut to the tablet no longer shows a dark frame - #2477

Open
computerguy526 wants to merge 1 commit into
seedds:mainfrom
computerguy526:sparse-standin
Open

computerguy526 wants to merge 1 commit into
seedds:mainfrom
computerguy526:sparse-standin

Conversation

@computerguy526

@computerguy526 computerguy526 commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

At PRAGMATA's cut from the tower to the tablet, the picture drops to a dark frame for one or two frames. On a main-line host, the whole-picture luma read 65 before the cut, 22 on the cut frame and 50 after. That happened in both runs without this change. With it, both runs read 65 / 65 / 65, with no frame over 100 ms, 60 fps, and a resident peak 0.1 GB lower.

Related to the PRAGMATA bring-up, ccd834e (@seedds).

Cause

The stand-ins of #2075 left out every texture in a sparse pool. Such a pool's address range has to be read through its page table, because reading an unmapped page directly stops the host with SIGBUS. PRAGMATA puts all its streamed textures in sparse pools, and it is past the walk's budget from its first scene (about 12 GB allocated against a budget of about 0.9 GB). So the materials first read at a scene cut still found the dummy. On an earlier branch with a G-buffer dump, the base colour on the cut frame was 0.068, against 0.18 on the next frame.

What changes

  • stand_fill gathers each layer's bytes, from the stand-in's first level to the layer's end, through the page table (nvn_sparse_copy, at most 256 KB a layer). It decodes them with nvn_deswizzle_from / nvn_deswizzle_pick.
  • A texture with the SPARSE flag gets a stand-in only from its mip tail on, which is laid out as any texture's (as sparse_make reads it).
  • When a page behind those levels is not mapped yet, the ID keeps the dummy rather than a stand-in that would read 0. Before, it was refused for the registration's life. Now it stays open (stand_no 0), so it is looked at again when a mapping over its pages notes it (treg_sparse_notes, Refresh cached texture bindings after sparse mapping changes #1721) or at its next walk. PRAGMATA registers some materials long before a cut and maps their small levels later.
  • Such a refusal counts toward the 512 stand-ins a frame, so a game that maps every frame retries at most that many a frame.
  • The pool stand-ins up to frame line also counts those made in a sparse pool and those not made for an unmapped page.
  • No switch: DM_NVN_POOL_STANDIN=0 still turns all stand-ins off.

Tests

  • t_pool_standin_sparse (new, under Metal validation, 23 checks, through the real NVN entry points) covers:
    • a sparse pool's texture left out holds its stand-in, with its level 4 read back from the GPU and compared with the level decoded from the physical pages;
    • one whose small levels are unmapped holds the dummy, and the refusal counts toward the frame's budget;
    • mapped later in the same frame, with no walk, it holds its stand-in once the mapping notes it;
    • registered again with every page mapped, it holds its stand-in at the next walk.
  • It does not build against main (no sparse stand-ins). With a refused ID kept closed, as main keeps it, the same-frame check fails.
  • t_pool_standin is unchanged: 51 checks, and 31 with DM_NVN_POOL_STANDIN=0, all pass. t_pool_notes passes.
  • Reference entry: A sparse pool's streamed textures read 0 at their first read. The stand-in entry points to it, PRAGMATA's note links it, and the symptom index is regenerated.

Regression

  • regress.py impact lists The Legend of Zelda: Breath of the Wild, Pokémon Scarlet and The Legend of Zelda: Tears of the Kingdom as affected (through nvn_mtl.m). None was run, and none of them is known to use sparse pools.
  • Reach: only textures in sparse pools that the walk leaves out. Each now holds a small stand-in (at most 1 KB of levels, or 4 KB cut down) instead of the dummy until its first read.
  • In game (PRAGMATA, M4 Pro, macOS 26.6.2, a main-line host, two runs a side, with the sparse stand-ins switched off on the test host for the runs without):
    • cut-frame luma (before / cut / next): 65 / 65 / 65 in both runs with the change, and 65 / 22 / 50 (and 65 / 22 / 65) without it;
    • 5,694 and 5,698 stand-ins were made, in 27 and 38 ms over the run, and 4,872 and 4,867 of them were in sparse pools (829 and 823 without, none in sparse pools);
    • the resident set peaked at 6.5 and 6.6 GB, against 6.7 GB without; no frame went over 100 ms, and all four ran at 60 fps.

Rebased on main 3084f7a, full local CI

@@ci@@

Co-authored with UncraftableXD.

@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation labels Nov 12, 2026
…e page table, so PRAGMATA's cut to the tablet no longer shows a dark frame

The stand-ins of seedds#2075 left out every texture in a sparse pool, whose address
range must be read through its page table (an unmapped page stops the host
with SIGBUS). PRAGMATA puts all its streamed textures in sparse pools and is
past the walk's budget from its first scene, so the materials first read at a
scene cut still found the dummy: the whole-picture luma at the cut from the
tower to the tablet fell from 65 to 22 for a frame.

stand_fill gathers each layer's bytes from the stand-in's first level to the
layer's end through the page table (nvn_sparse_copy, at most 256 KB a layer)
and decodes them with nvn_deswizzle_from / nvn_deswizzle_pick. A texture with
the SPARSE flag gets one only from its mip tail on. A page behind those levels
not mapped yet: the ID keeps the dummy and stays open (stand_no 0), so it is
looked at again when a mapping over its pages notes it (treg_sparse_notes) or
at its next walk, where it was refused for the registration's life before.
Such a refusal counts toward the 512 stand-ins a frame, so a game that maps
every frame retries at most that many.

Test: t_pool_standin_sparse (new, Metal validation), 23 checks, including a
mapping in the same frame with no walk; with a refused ID kept closed, as on
main, that check fails. t_pool_standin and t_pool_notes pass. Reference entry
in rendering.md, and a line in PRAGMATA's note.

Co-authored-by: uncraftablexd <352372032+uncraftablexd@users.noreply.github.com>
@computerguy526 computerguy526 changed the title Textures in a sparse pool get stand-ins too, read through the page table, and one whose small levels are mapped later gets its stand-in then Textures in a sparse pool get stand-ins read through the page table, so PRAGMATA's cut to the tablet no longer shows a dark frame Nov 13, 2026
@github-actions github-actions Bot added the game: 0400C76024710000 A game profile label Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400C76024710000 A game profile 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.

1 participant