Skip to content

A staged upload from a large pool makes its second source buffer only when taking its staging began a new batch, so a scene load no longer puts its textures' storage into the residency set twice - #2378

Closed
bymaxmarc wants to merge 1 commit into
seedds:mainfrom
bymaxmarc:pr/staged-upload-src

Conversation

@bymaxmarc

@bymaxmarc bymaxmarc commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

What and why

On Nintendo Switch Sports Resort (04008E001F098000, a Switch 2 game; no profile on main yet), the first frame of the court behind the sport menu draws 855 new packaged textures (908 MB). That frame took a quarter to half a second, nearly all of it in submit+present. The residency set up to frame line for that window counted about twice as many shared buffers from up_big_src as packaged textures (2,866 and 1,433).

An upload from a large pool (BIG_MIN), or from a pool no address was given out in, reads the texture's storage through a zero-copy buffer of the upload batch (up_big_src). The batch makes that buffer resident and retires it. pkg_upload_ made the buffer, then took the texture's staging (up_alloc), which may flush the batch. In case it had, it made a second buffer, and it did so for every texture, flush or not. So each staged upload put its storage into the residency set twice, and the second buffer was made, wired and committed for nothing.

pkg_upload_ now makes the second buffer only when up_alloc began a new batch (g_up_seq moved).

Reference: Two source buffers for each staged upload from a large pool (new); symptom index regenerated.

Checked

  • Tests, on this branch (main ccd834e + 1 commit), Apple M2 Max, macOS 27.2:

    • make -k -C dm/gfx/tests GAME=010015100B514000 -j8 run: every test passed except t_feedback, with one check (with nothing running: a commit) failing. That check is flaky on main too: run alone, it failed 1 of 3 times on this branch and 2 of 4 on main. t_upload_src: 41 checks, 0 failed.
    • t_upload_src runs under Metal validation:
      • eight textures in one batch make eight source buffers (sixteen before);
      • three 64 MB textures whose staging splits the batches make one extra buffer, for the texture that flushed its batch (six before);
      • every texel is compared with the CPU reference.
    • python -m unittest dm.tools.tests.test_docs dm.tools.tests.test_env_switches dm.tools.tests.test_pr_areas dm.tools.tests.test_lint_c: 30 tests OK.
  • Game: Nintendo Switch Sports Resort 04008E001F098000 v65536 on the user's Mac (Apple M2 Max, macOS 27.2), on our work branch. I ran 75 s twice before and twice after this commit, from the same save and pipeline state:

    Measure Before After
    Residency commits up to frame 2,400 678-692 ms 520-523 ms
    Shared buffers added in that window 2,852-2,858 (3.0 GB) 1,429-1,437 (1.6 GB)
    Court's first frame 263-270 ms 243-251 ms
    Next load (340 textures, 188 MB) 155-157 ms 134-138 ms

    Most of what is left is the residency set taking the new textures and their staging.

  • Not checked:

    • scripts/regress.py: not run. Every game's staged uploads from large pools take this path; the only difference is one buffer fewer when the batch did not change.
    • lint_c.py tidy (no clang-tidy on this Mac) and iOS.

Docs

  • The finding is in its docs/reference/<subsystem>.md entry, and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • No game files, keys or data extracted from a game in the change or the attachments

… when taking its staging began a new batch, so a scene load no longer puts its textures' storage into the residency set twice

Symptom: the first frame of a scene that draws hundreds of new packaged textures took a quarter to half a second, nearly all of it in submit+present (`[longframe] … texture uploads 855 first …, 908.5 MB`), and the `residency set up to frame` line of that window counted about twice as many shared buffers from up_big_src as packaged textures (2,866 and 1,433). Found on Nintendo Switch Sports Resort (04008E001F098000, Switch 2, v65536), the first frame of the court behind the sport menu.

Cause: an upload from a pool no address was given out in, or from a large pool (BIG_MIN), reads the texture's storage through a zero-copy buffer of the upload batch (up_big_src), which the batch makes resident and retires. pkg_upload_ made that buffer, then took the texture's staging (up_alloc), which may flush the batch, and made a second buffer in case it had: for every texture, flush or not. Each staged upload put its storage into the residency set twice, and the second buffer was made, wired and committed for nothing.

Fix: pkg_upload_ makes the second buffer only when up_alloc began a new batch (g_up_seq moved). Reference: memory.md, a new entry (Two source buffers for each staged upload from a large pool); symptom index regenerated.

Checked: t_upload_src under Metal validation (eight textures in one batch make eight source buffers, sixteen before; three 64 MB ones whose staging splits the batches make one more for the texture that flushed its batch, six before; every texel against the CPU reference), the rest of the graphics tests (Apple M2 Max, macOS 27.2; t_feedback's 'with nothing running: a commit' check failed once in the full run and 1 of 3 runs alone, as it does on main, 2 of 4), test_docs. In the game, two 75 s runs each from the same save and pipeline state, before and after on the same work branch (M2 Max): residency commits to frame 2,400 678-692 → 520-523 ms, shared buffers added in that window 2,852-2,858 (3.0 GB) → 1,429-1,437 (1.6 GB), the court's first frame 263-270 → 243-251 ms, the next load's (340 textures, 188 MB) 155-157 → 134-138 ms. Not run: regress.py (every game's staged uploads from large pools take this path, and the only change is one buffer fewer when the batch did not change), lint_c.py tidy (no clang-tidy here), iOS.
@bymaxmarc
bymaxmarc force-pushed the pr/staged-upload-src branch from 19b8ab3 to 5b07ee6 Compare November 12, 2026 11:08
@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
seedds added a commit that referenced this pull request Nov 12, 2026
… buffer only when its batch changed (PR #2378)

pkg_upload_ reads a texture in a large pool (BIG_MIN), or in a pool no
address was given out in, through a zero-copy buffer of the upload batch
(up_big_src). After taking the texture's staging (up_alloc), which may
flush the batch, it made a second buffer for every texture, flush or not,
so each staged upload put its storage into the residency set twice.

It now records the batch (g_up_seq) when it makes the first buffer and
makes a second only when the batch changed.

- Switch Sports Resort's first court frame (855 textures, 908 MB, M2 Max,
  PR #2378): up_big_src buffers 2,852-2,858 -> 1,429-1,437, residency
  commits 678-692 -> 520-523 ms, the frame 263-270 -> 243-251 ms.
- Mario Kart World, 200 s regress route (M4 Pro): up_big_src buffers
  24,696 (5,429 MB) -> 12,082 (2,424 MB); 60.0 fps and frame p99
  unchanged.
- t_upload_src: one buffer per texture in one batch, one more for a
  texture whose staging flushed its batch, every texel checked under
  Metal validation; main's code fails 2 of its 41 checks.
- regress.py check: Mario Kart World and OCTOPATH TRAVELER II pass
  (Octopath does not reach up_big_src).

Same cause found earlier in PR #2342.

Co-authored-by: bymaxmarc <354601562+bymaxmarc@users.noreply.github.com>
@seedds

seedds commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Landed on main as 0c23e0b, with one change: pkg_upload_ now records the batch (g_up_seq) when it makes the first source buffer, rather than just before up_alloc. Nothing between the two can flush today, so they behave the same, but the check now names the batch that holds the first buffer. You're credited as co-author.

Checked on an M4 Pro: t_upload_src passes 41/41, and main's code fails 2. regress.py check passes on Mario Kart World and OCTOPATH TRAVELER II. Mario Kart World reaches this path: over a 200 s run, up_big_src buffers went from 24,696 (5,429 MB) to 12,082 (2,424 MB), with fps and frame p99 unchanged. Not tested: iOS and Switch Sports Resort (no profile here). Thanks for the measurements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation 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