Skip to content

A storage takeover asks whether a live texture object holds each shared texture in one lookup instead of walking the serial table - #1439

Closed
enjoyerofslop wants to merge 2 commits into
seedds:mainfrom
enjoyerofslop:perf/alias-takeover-held
Closed

enjoyerofslop wants to merge 2 commits into
seedds:mainfrom
enjoyerofslop:perf/alias-takeover-held

Conversation

@enjoyerofslop

Copy link
Copy Markdown
Contributor

What and why

Symptom: while a game makes many textures, frames take hundreds of milliseconds and up to a second, all of it in submit+present (the [longframe] lines). A sample of the process names alias_take_over the submit thread's hottest function. Found in The Legend of Zelda: Ocarina of Time's cutscenes (Switch 2, 0400486024DEC000).

Cause: when a new shared texture takes over storage, alias_take_over (dm/gfx/nvn/nvn_mtl.m) checks each sharing-table entry it could remove. For each one it asked whether a live texture object still resolves to it with serial_holds(&g_by_serial, ...), a walk over the serial table's whole capacity. With about 7,500 serials and thousands of shared textures, every new shared texture walked the table once per overlapping entry. The disassembly puts the hot samples (alias_take_over+452..468) in that inlined walk. Ocarina of Time's texture-pool walks make hundreds of textures in one submission, each of them going through it.

Change: a count per object of the serial-table entries that hold it (g_by_serial_held). It is kept by every writer of the serial table: serial_put, serial_del, tex_put (when it writes the live table) and the upgrade's in-place replacement. alias_take_over now asks it in one lookup. An entry holds its object (CFBridgingRetain), so a counted address cannot be reused while it is counted. A slot written past these helpers (only t_upgrade_alias does that, to fill the table) is never counted, and taking it away leaves the count alone. The rule alias_take_over applies is unchanged.

Reference entry: A storage takeover walked the serial table for each entry, next to the takeover's own entry. The symptom index is regenerated. The entry also records what is still open: in that game the textures themselves are made again and again (84,000 to 103,000 storage takeovers in about 200 s of play), which I am looking into separately.

Relation to other PRs: independent of #1428 (pool tables), which touches other functions of the same file. #1299 reworks serial_put, serial_del and tex_put. On it, the same held_add calls go beside its own lines; that is how the Ocarina of Time host below was built.

Checked

  • Tests:
    • make -C dm/gfx/tests GAME=01007EF00011E000 t_alias_takeover: 28 checks OK. It covers:
      • an entry a live object resolves to stays;
      • one nothing resolves to goes after ALIAS_IDLE submissions;
      • two serials naming one object keep it held until both go;
      • replacing a serial's object moves the count;
      • a timing check: a takeover past 1,000 held entries with 20,000 serials in the table takes 0.025 ms. With the old walk in that one line, the same test took 4.3 ms, so the check is set at under 1 ms.
    • t_upgrade_alias (13), t_copy_alias (101), t_srgb_alias (8), t_image_alias (318) and t_cpu_tex (56) pass.
    • make -k -C dm/gfx/tests GAME=01007EF00011E000 run: every test passes but t_nvn. It aborts in up_owner_check ("up_begin called on thread ''", from present_enqueue), and it aborts the same way on main (865cc35).
    • python3 -m unittest dm/tools/tests/test_docs.py: OK.
  • Games run: The Legend of Zelda: Ocarina of Time, 0400486024DEC000, update v131072, with [WIP] The Legend of Zelda: Ocarina of Time (Switch 2, 0400486024DEC000, v0, SDK 22.2.8) runs on the Mac through the title movie into Kokiri Forest #1299, Kokiri Forest through Saria's cutscene, M5 Max.
    • Captures: 60 s sample captures of the process over the cutscene. Before, alias_take_over took 2,277 of the 3,711 samples the submit thread spent making textures (tex_mtl_), 61 %. After, it took 38 of 618, 6 %, so each texture creation costs less than half as much.
    • Before the change: in the first capture, 88 % of the gameplay long frames were host CPU in submit+present, and this function was the largest single part of it.
    • Game runs: the runs are on the wall clock, because the game does not run under DM_DETCLOCK. The cutscene's heaviest part lands inside some 250 s runs and outside others. In the runs that reached it, frames over 100 ms went from 101 and 102 to 58, and their total from 27 and 28 s to 19 s. One run with the fix is too few to call that more than a direction.
  • regress.py: python3 scripts/regress.py check --baseline 865cc35f --candidate HEAD --games 01007EF00011E000,0100000000010000 (the full run). regress.py impact lists Breath of the Wild among the games that reach the serial table.
    • Saves: as in Pool tables for texture handles traced to no slot reuse their buffers and pass over IDs of another type #1428, the routes ran from this contributor's own saves, because the profiles' saves/outdoors and saves/look_around are not on this Mac. Breath of the Wild waits at the load prompt (frame 2500 is not reached, in the baseline too), and Odyssey plays its opening from a new game.
    • Screenshots: the check compares against one baseline run (noise 0). Measured with regress.py's shot_hashes / shot_distance against five runs of 865cc35:
      • Breath of the Wild, 60 s (the title's slideshow): the baseline runs differ by up to RGB 26.8 and 28 dHash bits, and the candidate is within RGB 15.3 / 16 bits of one.
      • Odyssey, 95 s (the opening cutscene): baseline runs differ by up to RGB 14.9 / 9 bits, and the candidate is within RGB 4.2 / 6 bits of one.
    • Everything else is unchanged: shaders, frames reached, frame rate, footprint and prebuild.

regress.py check (full): REGRESSION

Baseline 865cc35f 865cc35, candidate HEAD 4599612; Apple M5 Max, macOS 27.0.1, Xcode 27.2; 490.9 s

game status shaders (base → cand) newly failing newly building changed MSL interface (new / known) boot fps p50 frame p99 ms peak MB prepare s new RED
The Legend of Zelda Breath of the Wild 01007EF00011E000 CHANGED 19420/19420 → 19420/19420 0 0 0 0 / 0 FAIL 29.5 → 29.8 91.83 → 69.23 3057 → 3050 1.49 → 1.42 0
Super Mario Odyssey 0100000000010000 REGRESSION 27984/27984 → 27984/27984 0 0 0 0 / 0 ok 59.9 → 59.9 18.63 → 18.37 2359 → 2359 None → None 0

The Legend of Zelda Breath of the Wild 01007EF00011E000: CHANGED

  • CHANGED: screenshot at 60 s moved (RGB 19.53)
  • known (baseline too): frame 2500 not reached by 60 s (max 3216)
    • screenshots (s: dHash bits / RGB diff / baseline noise): 60: 1/19.53/0, 80: 3/1.55/0
    • baseline: exit None, first frame 2.9 s, max frame 3189, reached False, prepare 1.49 s, prebuild 90.7 s (15631 built, 0 failed), fps 29.5, p50/p99 33.35/91.83 ms, peak 3057 MB
    • candidate: exit None, first frame 2.6 s, max frame 3216, reached False, prepare 1.42 s, prebuild 87.6 s (15631 built, 0 failed), fps 29.8, p50/p99 33.36/69.23 ms, peak 3050 MB
    • time: chain 28.9 s, smoke 198.3 s

Super Mario Odyssey 0100000000010000: REGRESSION

  • REGRESSION: screenshot at 95 s differs (dHash 12 bits, RGB 14.82)

    • screenshots (s: dHash bits / RGB diff / baseline noise): 70: 2/1.21/0, 95: 12/14.82/0
    • baseline: exit None, first frame 1.3 s, max frame 6520, reached True, prepare None s, prebuild 135.8 s (32864 built, 0 failed), fps 59.9, p50/p99 16.66/18.63 ms, peak 2359 MB
    • candidate: exit None, first frame 1.1 s, max frame 6515, reached True, prepare None s, prebuild 129.4 s (32864 built, 0 failed), fps 59.9, p50/p99 16.66/18.37 ms, peak 2359 MB
    • time: chain 23.5 s, smoke 240 s
  • Devices and OS: M5 Max (Mac17,6), macOS 27.0.1.

Docs

  • The finding is in its docs/reference/rendering.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

🤖 Generated with Claude Code

…ed texture in one lookup instead of walking the serial table: Ocarina of Time's cutscene texture creation 2.4 times cheaper

For each sharing-table entry a takeover could remove, alias_take_over called serial_holds(&g_by_serial,
...), a walk over the serial table's whole capacity. With about 7,500 serials and thousands of shared
textures, every new shared texture walked the table once per overlapping entry, and in Ocarina of Time's
cutscenes, where the texture pool's walks make hundreds of textures in a submission, that walk was the
submit thread's hottest function while frames took up to a second.

A count per object of the serial-table entries holding it (g_by_serial_held) is now kept by every writer
of the serial table (serial_put, serial_del, tex_put and the upgrade's in-place replacement), and
alias_take_over asks it in one lookup. An entry holds its object, so a counted address is not reused
while counted.

t_alias_takeover: live-held entries stay, unheld ones go after ALIAS_IDLE submissions, two serials keep
an object held until both go, and a takeover past 1,000 held entries with 20,000 serials takes 0.025 ms
instead of 4.3 ms. Ocarina of Time (with PR seedds#1299), 60 s sample captures over Saria's cutscene:
alias_take_over took 2,277 of 3,711 samples of texture creation (61 %), with the fix 38 of 618 (6 %).
The reference entry records what is still open: the textures themselves are made again and again
(84,000 to 103,000 storage takeovers in about 200 s of play).

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 labels Nov 6, 2026
fpedrolucas95 added a commit to fpedrolucas95/nxnative that referenced this pull request Nov 6, 2026
…exture object holds each shared texture in one lookup instead of walking the serial table

# Conflicts:
#	dm/gfx/nvn/nvn_mtl.m
…n and its resources before it uses them

The test includes nvn_mtl.m, and with this branch's additions there the analyzer's path through the
test's main() reached two calls it had not before: newComputePipelineStateWithFunction: with the
function newFunctionWithName: may return nil, and copyFromTexture: with a texture newTextureWithDescriptor:
may return nil (clang-analyzer-nullability.NullablePassedToNonnull, 1 in the file against a baseline
of 0). The function is now checked before the pipeline is made, and the textures and buffers after
they are made; the test fails with a message either way, as it already did for the library and the
pipelines. lint_c.py tidy on the file: 0 findings (baseline 240, none above or below); the test passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
… texture object holds each shared texture in one lookup (Ocarina of Time's cutscene texture creation 2.4 times cheaper)

Resolved against NXForge's serial table (seedds#1360's program generation, seedds#1300's g_ghost_active):
- serial_put / serial_del keep the program generation and add seedds#1439's held counts
- tex_put keeps the same-object check and the g_ghost_active ring, and counts g_by_serial's names as seedds#1439 does
- t_int_float_view.m keeps main's checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
@jonpage0

jonpage0 commented Nov 6, 2026

Copy link
Copy Markdown

Measured on an M3 Max (36 GB, macOS 27.0.1, Xcode 27.2), Ocarina of Time v131072, running (not a cutscene): the same scripted run through Kokiri Forest, from the Kokiri balcony autosave, driven by DM_CTL, with the pipeline archive already warm (no new pipelines, no texture uploads after the load), one run per host, no sampler:

122 s of running, three legs without with this change
frames over 100 ms 85 2
frames over 200 ms 31 0
frame p95 / p99 41 / 151 ms 22 / 29 ms
submit-thread CPU p95 / max 34 / 391 ms 10 / 63 ms
[longframe] lines, of them submit-dominated 419 (290) 79 (65)
fps 46 54

What the sample captures said before the change (30 s legs, the game's Presentation Thread, which calls nvnQueueSubmitCommands): 77–79 % of the thread in mtl_exec; pool_gpu 27–33 %; tex_mtl_ 12–16 %, of which alias_take_over 11–15 % of the whole thread (serial_holds 7–10 %). So in plain running the pool walk's texture creation was almost entirely this walk. Legs 2 and 3 with the change had no frame over 50 ms at all. Picture unchanged at the same spot.

One note for whoever lands this on a tree with #1404's serial_holds (the version that also walks texture views' parents and a packaged texture's tex/view/spare): the count has to be kept for those holds too, or a takeover can retire a parent a live view still reads. I kept that on my tree with the same held_add at the four writers plus a held_member at the three places a packaged texture's members change (sparse_view, the re-upload, the upgrade); the test's two extra cases (a view holding its parent; a member move) pass. Happy to share that diff if it helps.

gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
) replaces the pull request heads

Upstream landed Ocarina of Time in main (chain adec7d81d970: "holds 60 fps in
Kokiri Forest, 4 GB less") and closed the pull requests it folded in, with
reworded and partly different code. Mixing a pull request head with its landed
version hunk by hunk silently broke things (two GlobalToStorage tables, a
duplicate AF2ip, two mtk_pso definitions), so the rule here is one version per
feature:

- Pull requests upstream closed (seedds#1299, seedds#1300, seedds#1312, seedds#1343, seedds#1345, seedds#1353,
  seedds#1354, seedds#1359, seedds#1360, seedds#1373, seedds#1428, seedds#1430, seedds#1434): their hunks are taken out
  of our side before the three-way merge, so upstream's landed version stands
  alone. Gone with them: bindless pool table patching (reg_bump), the ghost
  table's per-submission hold, the growing raw pool table and its write-log
  sizing, seedds#1428's pool_buf spares (upstream's pool_table_take is the same
  idea), the ixc-based indirect mesh draw (upstream uses mdx), and their tests
  (t_pool_table, t_mesh_indirect, t_gmem_raw, finalized_bindless_pool,
  test_bnsh_nvn2, test_ndve).
- Open pull requests that upstream's landing supersedes: seedds#1394 (upstream's
  mtl_pool_addressed) and seedds#1393 pool_skip_retired (upstream's budgeted pool
  walk; the profile does not use it) are dropped with their tests and docs.
  seedds#1368's mesh pipeline prewarm is dropped too: upstream kept the background
  build in nvn_geometry.inc, and the prewarm's mtk_build / mtk_claim /
  g_warm_mps went with it.
- Kept, as upstream does not have them: NXForge's own features (GPU watchdog,
  present filters and anti-aliasing, aniso floor, quick settings, pipeline
  archive compaction, upload breakdown, memory report lines) and the still
  open pull requests seedds#1304, seedds#1305, seedds#1308, seedds#1320, seedds#1331, seedds#1332, seedds#1355, seedds#1356,
  seedds#1369, seedds#1376, seedds#1377, seedds#1379, seedds#1382, seedds#1390, seedds#1391, seedds#1392, seedds#1414, seedds#1420,
  seedds#1436, seedds#1439, seedds#1446, seedds#1453.
- Combined where both sides changed the same lines: GW_N 1 << 16 with seedds#1320's
  wide-write intervals (GW_PAGES = SBW_WPAGES, 128 slots), the CPU upload log
  with upstream's linear upload route, arch_open_end before the compaction
  limit, cbn_prog in geo_gs_one (seedds#1382).
- Translator: upstream's sources and binaries, plus seedds#1414's
  InstEmitAmpereMem diff and the LDSM SubgroupBarrier in
  InstEmitAmpereTensor.cs. The binaries and dmsh_chain.lock are upstream's
  until the Mac rebuilds them (dotnet publish, tool_lock --update-lock,
  dmsh_chain --header); dmsh_chain.h is regenerated for now.
- RED_ONCE keys: upstream took 153 and 154, so the present anti-aliasing lines
  move to 156 and 157 and seedds#1420's packaged-entry line to 158 (array 159).
- Host: hle_net.c and t_net_eventfd.c are upstream's (its eventfd poll waits
  out the whole timeout, which our 5d7920c fixed another way).
- Kept ours: dm/tools/romfs_image.py and its test, so the 26 GB RomFS image is
  not packed again.

dm/tools tests on Linux: same failures as on 875449e (Linux-only quirks),
plus test_nlg4 and test_dmsh_pool_arrays, which fail on upstream main too.
Not yet built on the Mac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
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 6, 2026
…ed texture in one lookup instead of walking the serial table: Ocarina of Time's long frames in Kokiri Forest fall from 332 to 212 (PR #1439)

For each sharing-table entry a takeover could remove, alias_take_over called serial_holds(&g_by_serial,
...), a walk over the serial table's whole capacity. With thousands of serials and shared textures, every
new shared texture walked the table once per overlapping entry, and in Ocarina of Time, where the texture
pool's walks make hundreds of textures in a submission, that walk was the submit thread's hottest function
while frames took up to a second.

A count per object of the serial-table entries holding it (g_by_serial_held) is now kept by every writer
of the serial table (serial_put, serial_del, tex_put and the upgrade's in-place replacement), and
alias_take_over asks it in one lookup. An entry holds its object, so a counted address is not reused
while counted. The reference entry notes that a change letting objects hold shared textures another way
(PR #1404's views and packaged members) must count those holds too.

t_alias_takeover: live-held entries stay, unheld ones go after ALIAS_IDLE submissions, two serials keep
an object held until both go, and a takeover past 1,000 held entries with 20,000 serials takes 0.03 ms
instead of 4.3 ms. Ocarina of Time on an M4 Pro, loading a Kokiri Forest save through Saria's dialogue
and a walk: a host asking both the count and the walk compared them 393,216 times with no difference;
[longframe] lines 332 -> 212, frame p99 72.8 -> 38.8 ms (one run each, hosts on the build's chain at
88c7e03). Measured by the author on an M5 Max and by a commenter on an M3 Max (frames over 100 ms
85 -> 2). Regress: Breath of the Wild's build is in the packed layout regress.py does not read, so
Ocarina ran instead.

Co-authored-by: enjoyerofslop <281774948+enjoyerofslop@users.noreply.github.com>
@seedds

seedds commented Nov 6, 2026

Copy link
Copy Markdown
Owner

Landed as d4239a6, rebased onto main with your design and code unchanged (credited as co-author). Your second commit was not needed: main already has that t_int_float_view fix (#1338).

What I checked on an M4 Pro, Ocarina of Time loading a Kokiri Forest save, through Saria's dialogue and a walk through the village:

  • A host that asked both the count and the old walk at every takeover compared them 393,216 times and they never differed, so the takeover makes the same choices as before.
  • Main's host against this one, one run each: [longframe] lines 332 → 212, frame p99 72.8 → 38.8 ms, same red lines. Both hosts were built at 88c7e03 to match the build's shader chain.

One addition to the reference entry, after the note from jonpage0 (thanks for the M3 Max numbers): a change that lets a live object hold a shared texture another way (#1404's views and packaged members) must count those holds too. Whoever lands #1404 needs to carry the count there.

@seedds seedds closed this Nov 6, 2026
seedds added a commit that referenced this pull request Nov 6, 2026
…ng walked whole: registrations, memory pool changes, retired IDs at a new submission and storage takeovers each leave a note; Ocarina of Time's village walk builds its 33,025-entry table 16 times instead of over 4,000 (PR #1489)

pool_gpu walked a table of handles traced to no slot whole at any change of g_reg_gen or g_ser_gen, though each
change reaches only a few of its IDs. Ocarina of Time registers something between most of its draws, its streamed
textures map and unmap virtual pool runs dozens of times a second, finalized descriptors stay registered, and
textures take over each other's storage (alias_take_over), which bumped g_ser_gen with no note and so also sent
every texture ID down the slow path. TheekshanaA measured about 53 s of a seven-minute walk through Kokiri Forest
spent on 61,000 such builds (PR #1405, #1464).

This is TheekshanaA's branch oot-pool-table-notes, ported to main's walk by PucaVaz, with the table patching of
#1299 / #1387 it sat on:
- A registration records its generation and ID (g_reg_log), a storage change its serial (g_ser_log, beside the
  notes ser_sync takes). A table built at an earlier generation takes again only the IDs they name, by the walk's
  own rules (pool_entry), and is kept when no entry changes, else copied and patched.
- A memory pool added or removed records REG_POOL and no ID: a table takes again only the IDs it found without
  storage (pool_snap.dead); treg_gone notes each ID of a finalized pool.
- A table keeps the IDs whose entries are views of retired registrations, those the read scan writes in place
  among them (pool_snap.trans); the next submission takes only those again, and such a table does not stand
  under POOL_SUB_MS.
- A takeover records a note that names no texture (ser_note(0)): it changes no serial's texture (an entry a live
  serial holds is not evicted, serial_table_holds; one a finalized serial holds is only unref'd). A live sRGB /
  linear twin's view of an evicted texture is found stale by tex_mtl_ and dropped (serial_del), a note naming it.
- A patch makes textures only within what the frame may still make (pool_room). Past POOL_IDS_MAX IDs, a note
  the ring has overwritten, a bump without a note, a resized table or a sparse mapping, the table is built whole
  as before. The log line says why; the power lines gain "[nvn] pool tables up to frame ...".

On landing: the comment and the reference entry name serial_table_holds (#1439 replaced the serial table walk)
and describe the sRGB / linear twin case; a registration note is read as a sequence lock (reg_read: its
generation is checked before and after its ID, and the writer clears it first), as memory pool changes write
notes from threads that do not hold g_rmu and a note the ring overwrote during the read could be taken torn.

Checked: t_pool_notes 81 checks (patched tables compared with tables built whole), t_pool_chunks 102,
t_alias_takeover 28, t_gmem_words 16, t_feedback 159, t_image_alias 329, t_sparse_tiles 4,253, t_cpu_tex --metal
68, all 0 failed; test_docs; the Mac host builds and the iOS compile check passes (261 files). Ocarina of Time on
an M-series Mac, hosts at 51cc8bc with and without this (main's chain has no libraries on disk for it), the
village walk from save_ref: tables built whole 4,000+ (5.85 s+) on the base, 12 and 16 (0.11 s, 0.12 s) with
this; submit p50 in the village 11-16 ms on the base, about 7-12 ms with this; the same red lines and no GPU
error. Vertical stripes in both sides' shots at the same balcony points come from that old-chain setup, not this
change. The author measured submit p50 12-15 -> 3-5 ms and frames over 50 ms walking 23 and 48 -> 3 and 2 (M4,
two runs per side). No second game: none local has libraries for main's chain, and the one whose regress runs
reach these tables (Paper Mario: The Thousand-Year Door) needs 11,861 shaders translated first.

Co-authored-by: PucaVaz <65235669+PucaVaz@users.noreply.github.com>
Co-authored-by: TheekshanaA <10327965+TheekshanaA@users.noreply.github.com>
m-gnr pushed a commit to m-gnr/kid-with-sword-remake that referenced this pull request Nov 7, 2026
- Reconcile upstream PRs (seedds#1455, seedds#1447, seedds#1394, seedds#1439, seedds#1489, seedds#1590, seedds#1663) and commits (ffebfd5, e5f1ade) against local fork
- Confirm PRs seedds#1455, seedds#1447, seedds#1394, and seedds#1439 are already implemented in ad99858
- Establish that host-only renderer optimizations require zero shader rebuilds and reuse the 42,854-entry b-dc815e0f38ca.img library
- Establish that native subword stores (ffebfd5) affect only 515 shaders and leverage cas.db compilation caching
- Rank candidates for base M3 Normal + Handheld (>=40 FPS target, ideal 60 FPS) and identify the top three candidates
- Recommend the smallest independently verifiable first optimization (host-only PSO archive epoch separation from PR seedds#1590)
- Update documentation index, plan, changelog, and known issues
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…ed texture in one lookup instead of walking the serial table: Ocarina of Time's long frames in Kokiri Forest fall from 332 to 212 (PR seedds#1439)

For each sharing-table entry a takeover could remove, alias_take_over called serial_holds(&g_by_serial,
...), a walk over the serial table's whole capacity. With thousands of serials and shared textures, every
new shared texture walked the table once per overlapping entry, and in Ocarina of Time, where the texture
pool's walks make hundreds of textures in a submission, that walk was the submit thread's hottest function
while frames took up to a second.

A count per object of the serial-table entries holding it (g_by_serial_held) is now kept by every writer
of the serial table (serial_put, serial_del, tex_put and the upgrade's in-place replacement), and
alias_take_over asks it in one lookup. An entry holds its object, so a counted address is not reused
while counted. The reference entry notes that a change letting objects hold shared textures another way
(PR seedds#1404's views and packaged members) must count those holds too.

t_alias_takeover: live-held entries stay, unheld ones go after ALIAS_IDLE submissions, two serials keep
an object held until both go, and a takeover past 1,000 held entries with 20,000 serials takes 0.03 ms
instead of 4.3 ms. Ocarina of Time on an M4 Pro, loading a Kokiri Forest save through Saria's dialogue
and a walk: a host asking both the count and the walk compared them 393,216 times with no difference;
[longframe] lines 332 -> 212, frame p99 72.8 -> 38.8 ms (one run each, hosts on the build's chain at
a4b9b0f). Measured by the author on an M5 Max and by a commenter on an M3 Max (frames over 100 ms
85 -> 2). Regress: Breath of the Wild's build is in the packed layout regress.py does not read, so
Ocarina ran instead.

Co-authored-by: enjoyerofslop <281774948+enjoyerofslop@users.noreply.github.com>
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…ng walked whole: registrations, memory pool changes, retired IDs at a new submission and storage takeovers each leave a note; Ocarina of Time's village walk builds its 33,025-entry table 16 times instead of over 4,000 (PR seedds#1489)

pool_gpu walked a table of handles traced to no slot whole at any change of g_reg_gen or g_ser_gen, though each
change reaches only a few of its IDs. Ocarina of Time registers something between most of its draws, its streamed
textures map and unmap virtual pool runs dozens of times a second, finalized descriptors stay registered, and
textures take over each other's storage (alias_take_over), which bumped g_ser_gen with no note and so also sent
every texture ID down the slow path. TheekshanaA measured about 53 s of a seven-minute walk through Kokiri Forest
spent on 61,000 such builds (PR seedds#1405, seedds#1464).

This is TheekshanaA's branch oot-pool-table-notes, ported to main's walk by PucaVaz, with the table patching of
seedds#1299 / seedds#1387 it sat on:
- A registration records its generation and ID (g_reg_log), a storage change its serial (g_ser_log, beside the
  notes ser_sync takes). A table built at an earlier generation takes again only the IDs they name, by the walk's
  own rules (pool_entry), and is kept when no entry changes, else copied and patched.
- A memory pool added or removed records REG_POOL and no ID: a table takes again only the IDs it found without
  storage (pool_snap.dead); treg_gone notes each ID of a finalized pool.
- A table keeps the IDs whose entries are views of retired registrations, those the read scan writes in place
  among them (pool_snap.trans); the next submission takes only those again, and such a table does not stand
  under POOL_SUB_MS.
- A takeover records a note that names no texture (ser_note(0)): it changes no serial's texture (an entry a live
  serial holds is not evicted, serial_table_holds; one a finalized serial holds is only unref'd). A live sRGB /
  linear twin's view of an evicted texture is found stale by tex_mtl_ and dropped (serial_del), a note naming it.
- A patch makes textures only within what the frame may still make (pool_room). Past POOL_IDS_MAX IDs, a note
  the ring has overwritten, a bump without a note, a resized table or a sparse mapping, the table is built whole
  as before. The log line says why; the power lines gain "[nvn] pool tables up to frame ...".

On landing: the comment and the reference entry name serial_table_holds (seedds#1439 replaced the serial table walk)
and describe the sRGB / linear twin case; a registration note is read as a sequence lock (reg_read: its
generation is checked before and after its ID, and the writer clears it first), as memory pool changes write
notes from threads that do not hold g_rmu and a note the ring overwrote during the read could be taken torn.

Checked: t_pool_notes 81 checks (patched tables compared with tables built whole), t_pool_chunks 102,
t_alias_takeover 28, t_gmem_words 16, t_feedback 159, t_image_alias 329, t_sparse_tiles 4,253, t_cpu_tex --metal
68, all 0 failed; test_docs; the Mac host builds and the iOS compile check passes (261 files). Ocarina of Time on
an M-series Mac, hosts at 66e6e4c with and without this (main's chain has no libraries on disk for it), the
village walk from save_ref: tables built whole 4,000+ (5.85 s+) on the base, 12 and 16 (0.11 s, 0.12 s) with
this; submit p50 in the village 11-16 ms on the base, about 7-12 ms with this; the same red lines and no GPU
error. Vertical stripes in both sides' shots at the same balcony points come from that old-chain setup, not this
change. The author measured submit p50 12-15 -> 3-5 ms and frames over 50 ms walking 23 and 48 -> 3 and 2 (M4,
two runs per side). No second game: none local has libraries for main's chain, and the one whose regress runs
reach these tables (Paper Mario: The Thousand-Year Door) needs 11,861 shaders translated first.

Co-authored-by: PucaVaz <65235669+PucaVaz@users.noreply.github.com>
Co-authored-by: TheekshanaA <10327965+TheekshanaA@users.noreply.github.com>
FeReuss pushed a commit to FeReuss/nxnative-oot that referenced this pull request Nov 10, 2026
…h residency commit stops the game instead of freezing the Mac

Ported onto current main (the PR was written on a base before several upstream changes):

- alias_let_go keeps main's rule for an entry leaving the sharing table, defer_obj_release (a texture a finalized object still resolves through g_ghost stays resident through defer_drain / g_retire_held). The PR carried the older serial_holds / defer_obj_unref walk that upstream replaced in PR seedds#1439, so that part is not taken.
- front_mac.m: main's front_gpu_lost already was the PR's alert code inline. It is now one helper, front_stop_alert(title, text, status), used by front_gpu_lost (exit status 70, as before) and by the new front_gpu_memory_stop (quits as Cmd-Q).
- The memory report line keeps main's extra fields and adds the PR's 'idle' count; Makefile and front.h keep both sides.

On this Mac (Apple M4, 16 GB) Metal's working set is 11.84 GB: the guard lets go of idle sharing entries, spare textures and staging blocks from 9.47 GB and stops the game at 11.25 GB; DM_NVN_RES_LIMIT_MB=0 turns it off. Checked: t_res_guard 20 + 2 checks, the neighbouring residency / alias tests, the host builds for Ocarina of Time. Not run: a game, so how often the 80% level fires in play is not measured.

Co-Authored-By: Claude Sonnet 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 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.

3 participants