Repository navigation
nvn/owner: a ghost entry a registered texture ID names is in use, so pool tables stop being walked whole every 16 frames - #2585
UserDerezzed wants to merge 1 commit into
Conversation
…pool tables stop being walked whole every 16 frames In Ocarina of Time's Kokiri Forest at a 120 fps target on an M3 Max, a frame hitched every 16 frames exactly (by frame count, not time), with the GPU at its usual 13 ms: the replay thread's submit took 25-41 ms on that frame, 294 M instructions against 53 M for its neighbours (88,000 a draw against 23,000 on frames as heavy). A Time Profiler trace put 75 % of those frames' replay time in full pool table walks (encode_args_ -> pool_gpu -> pool_entry over all 33,024 registered IDs), against 13 % elsewhere; the log counted tables built because a storage note came and 102,340 patches refused as too many. The cause is the ghost table's idea of use. A texture ID the game still registers for a texture it finalized (a retired registration) resolves through g_ghost when a walk takes it, and with more than 2,048 IDs its view is not a transient one taken again each submission, so the table holds it and no lookup comes again. Kept by lookups and puts alone (034e91a), every such entry, about 13,000 in Kokiri Forest, went idle GHOST_IDLE frames after the walk that resolved it, all in the same frame; past NGHOST the commits evicted them, each eviction a note, the notes ran past what a patch takes (SER_LOG, POOL_IDS_MAX), and the next table was walked whole, which put them all back in one frame. About 750 puts a frame. The owner now counts, per serial, the registered texture IDs that name it (own_reg_name: the registration names it when it links the ID under the serial; treg_unlink and treg_gone, a pool's Finalize, take the name back; a registration made again over a gone one names it again). ghost_trim stamps an idle entry an ID names used again, in its turn of the budget, instead of evicting it; once no ID names it, it goes as before, and a gone ID does not keep it. The memory line reports the entries kept and the serials named. Measured on the same build and route (oot-stack e016fd04 with this change, 219 s against 92 s before): long frames 16 frames apart 266 to none; frames of 25 ms or more 4.4 % to 0.4 %; p95 24.5 to 18.3 ms; 66.7 to 69.0 fps, the GPU being the limit; ghost puts about 750 to 42 a frame; patches refused 0; the table about 14,000 entries; resident memory 14.8 to 14.4 GB on average. Raising the cap instead (DM_NVN_GHOST_N=32768, as PR seedds#2312 proposes) also removes the beat but fills the table to 32,768 (15.2 GB). Reference entry: Finalized textures a pool table holds went idle together (docs/reference/nvn-objects.md); the game note links it, symptom index regenerated. Checked: t_serial_owner (184 checks; the new case fails 3 without the change), make -C dm/host/tests run (143), lint_c tidy on the changed files (none above the baseline), test_docs.
|
Thanks for the careful diagnosis and the numbers. Before landing this, could you reproduce the 16-frame beat on plain Reading
What would help:
A few smaller points for whichever form lands:
|
|
Sorry about that: I measured only on my stack and blamed So the beat needs both pieces: #1731's views kept across submissions, and 034e91a's idle eviction by lookups and puts. On my stack before 034e91a the ghost table's cap was 32,768, so it only showed after the resync took I'll close this one. The counting belongs with #1731, reworked along your three points:
Replacing the per-submission drop-and-retake outright, rather than gating it at 2,048 IDs, makes sense to me once the count exists. Since that's #1731's design, I can offer it there as a follow-up commit if its author wants it. |
What and why
In Ocarina of Time's Kokiri Forest at a 120 fps target, a frame hitches every 16 frames exactly (by frame count, not by time) while the GPU stays at its usual ~13 ms. On those frames the replay thread's submit takes 25-41 ms: 294 M instructions against 53 M for the neighbouring frames, or 88,000 instructions a draw against 23,000 on frames with as many draws. A Time Profiler trace, cut into the replay thread's bursts, puts 75 % of those frames' replay time in full pool table walks (
encode_args_→pool_gpu→pool_entryover all 33,024 registered IDs), against 13 % elsewhere. The log counts tables builtbecause ... storageand 102,340 patches refused astoo many.Cause. A texture ID the game still registers for a texture it has finalized (a retired registration) resolves through
g_ghostwhen a walk takes it. With more than 2,048 registered IDs its view is not a transient one taken again each submission (pool_snap.trans), so the pool table holds the view and no lookup comes again until the table is rebuilt. Since 034e91a the ghost table judges use by lookups and puts alone, so every such entry (about 13,000 in Kokiri Forest) goes idleGHOST_IDLEframes after the walk that resolved it, all in the same frame. PastNGHOSTthe commits evict them, and each eviction writes a note. The notes run past what a patch takes (SER_LOG,POOL_IDS_MAX), so the next table is walked whole, which puts them all back in one frame. That's a loop with the idle span as its period, at about 750 puts a frame.Change. The owner counts, per serial, the registered texture IDs that name it (
own_reg_name):treg_unlinktakes the name back, and so doestreg_gone(the pool's Finalize).ghost_trimstamps an idle entry an ID names as used again, in its turn of the budget, instead of evicting it. Once no ID names it, it goes as before. A gone ID (its storage finalized) does not keep it, so moved render targets are still let go. The memory line now reports the entries kept and the serials named.NGHOST,GHOST_IDLEand the 034e91a design are unchanged; this fixes how use is measured for entries that only a pool table reads.Related: issue #2213 and PR #2389 (where the table kept by use came from), and PR #2312, which raises the table to 32,768. With
DM_NVN_GHOST_N=32768the beat also goes, but the table fills to 32,768 (resident 15.2 GB against 14.4 GB with this change).A note for PR #2312 if both land: its
ghost_purgescans the ghost table's whole capacity for each shared texture let go. With a large table (32,768 with #2312's cap, or ~14,000 kept here), I saw two isolated 51-92 ms replay frames in a few minutes, in the windows with the most purges (+4,000 or more). With 512 entries they don't show. An index from object to serial (g_ghost_heldalready counts by object) would make it cheap. Onmainthere is noghost_purge, so this doesn't apply here.Reference entry: Finalized textures a pool table holds went idle together; the game note links it.
Checked
Tests:
t_serial_owner: 184 checks. The new case fails 3 checks without the change: 2,048 named entries stay through four idle spans with no eviction, note or put; they drain once no longer named; one with a second name stays; with no names left the table is back at 512.make -C dm/host/tests GAME=0400486024DEC000 run: 143 / 143.make -k -C dm/gfx/tests GAME=0400486024DEC000 run(Metal), on this branch rebased on 41acde7: every test passes. In earlier runs, made while the game was running on the same GPU,t_feedback's "with nothing running: a commit" check failed now and then. It does the same onmain41acde7 without this change (1 of 2 runs), so it looks timing-dependent, not caused by this change.lint_c.py tidyon the changed files: none above the baseline.python3 -m unittest dm/tools/tests/test_docs.py.Games run, with title ID and version: The Legend of Zelda: Ocarina of Time
0400486024DEC000v131072, walking around Kokiri Forest at a 120 fps target, per-frame TSV (DM_NVN_TSV) and the memory lines compared across runs on the same build and route. The host was this change applied to my oot-stack branch (main 2e6edda plus PRs, e016fd04); the changed functions are the same as onmainexcept that the stack also carries PR Render targets a game moved leave GPU memory: ghost serials go with their sharing entry, and an idle sweep (Ocarina of Time, Castle Town) #2312's port, whose conflicts were the stats struct and the memory line only.DM_NVN_GHOST_N=32768too manyDevices and OS: MacBook Pro M3 Max (36 GB), macOS 27.2.1 (26B131), on a 100 W adapter.
scripts/regress.py check: not run. Ocarina of Time is the only game I have locally, and the harness isn't set up on this Mac.regress.py impactlists Breath of the Wild, Pokémon Scarlet and Tears of the Kingdom as affected (texture pool registration and finalize) and Hades through a broad rule. The change can only keep an idle ghost entry longer while a registered ID still names it; it never evicts more, writes no extra notes, and changes nothing for a game with no more thanNGHOSTfinalized textures in use.Measurements before and after: above. Not checked: iOS, long sessions or area transitions beyond Kokiri Forest and its surroundings.
Docs
docs/reference/<subsystem>.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index)