Skip to content

OoT-fps C-2: texture views of a shared texture are kept by that texture, not only by the serial that asked - #2542

Closed
stan69b wants to merge 1 commit into
seedds:mainfrom
stan69b:pr/oot-fps-c2-shared-views
Closed

stan69b wants to merge 1 commit into
seedds:mainfrom
stan69b:pr/oot-fps-c2-shared-views

Conversation

@stan69b

@stan69b stan69b commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

OoT-fps batch, session C (submit-thread CPU per draw), item C-2. This PR also carries C-1: where the submit thread's CPU per draw goes (below).

What changes

Ocarina of Time initializes about 40 render targets again every frame over the same storage, each with a new texture serial in a new texture ID. The sharing table hands each one the Metal texture the last frame's had, but its views (depth read as (R, 0, 0, 1), RGBX with alpha 1, sRGB views, image slots' writable views) were kept by serial (vc_get, dropped at mtl_tex_release) or by texture ID (img_fit), so every frame made them all again on the submit thread.

Views of a texture in the sharing table are now kept by that Metal texture as well (vcb_get / vcb_put in nvn_mtl.m), for sampled and image views. A view retains its parent, so a key cannot name another texture while its entry exists, and a hit is a view of this very texture. An entry whose texture left the sharing table (pool finalized, storage taken over, upgrade) goes at a sweep once a frame (vcb_sweep). Textures outside the sharing table keep their views by serial only. The memory report's view line counts the new hits. The rule holds for every game.

Measured (M4 Pro, kokiri-deck route with the Load confirmation, encode thread on as on iOS)

views made by frame ~4,200 views reused
main 8d44aaf 84,748 169,005
this PR 1,948 (sampled views only; image views also kept in the final version) 146,423 + 111,616 by shared texture

Instructions per draw standing on the deck: within run-to-run noise (about ±1.5 %) on its own; the churn is larger while the camera turns. Together with C-3 (#2543): 43.3–43.7k → 38.7–39.3k.

C-1: where the submit thread's CPU per draw goes

sample of the NVN queue replay thread, main 8d44aaf, Ocarina of Time on the Kokiri Forest deck (M4 Pro, DM_NVN_ENC_THREAD=1 as on iOS, 650–1,050 draws a frame, 38–44k instructions per draw). Share of the thread's busy samples (waits excluded), inclusive:

rank function / record busy share what it is taken by
1 draw records (OP_DRAW / OP_DRAW_IDX → draw_) 48 % everything below that hangs off a draw
2 encode_args (draws and dispatches) 25 % argument blocks: pool_gpu 8 % (texture pool table walk → tex_by_id), img_binding 4 %, tex_by_id 3 %, cbuf_slot 1.5 %, own time 6 % C-2, C-4
3 -[AGXTexture newTextureView…] 8 % views made again every frame for re-initialized render targets (under tex_by_id, img_binding) C-2
4 mtl_sub_commit (not waiting) 14 % up_flush → res_commit 8 % (residency set commits: D's area); own time and gw_put 6.5 %: the commit walked all 16,384 slots of the storage-write page set, ~30 times a frame C-3
5 dispatch records (dispatch) 9 % mostly encode_args
6 fb_prepare 6 % same-pass feedback check per draw: a registry lock pair per fragment texture C-4
7 rp_begin_ex 5 % pass opens: descriptor build and copy, attach → tex_mtl
8 rt_dims → tex_mtl → own_alias_publish 4 % first use of a new render target texture at a pass open (texture creation: D's area)
9 rc_value / rc_source 3.5 % render-condition operands, read byte by byte
10 nq_cost_begin / nq_cost_end 3–5 % per-node cost reading: thread_info, mach_thread_self, mach_port_deallocate C-5 (part)
11 state_op, pso_get, bind_vbufs, mtl_exec own time 2–4 % each state records, pipeline lookup per draw, vertex bindings, record decoding

rec_drain (waiting for the encode thread) is 12–40 % of the thread's wall time and is not counted above.

Checks

  • New t_view_reuse (16 checks; the reuse checks fail without the change). t_view_swizzle (554), t_view_levels, t_stencil_view, t_srgb_alias, t_image_srgb, t_image_usage, t_pfv, t_cpu_tex, t_feedback, t_alias_takeover, t_serial_owner, t_partial_upload, t_nvn pass; test_docs OK.
  • scripts/regress.py check --baseline upstream/main --games 0400486024DEC000 on the stack of C-2 to C-5 (Apple M4, macOS 27.2; Ocarina of Time's title route from the unmerged ed769e9 smoke.tsv, as main has no route for it): shaders PASS (42848/42848, no MSL change), boot ok, fps p50 59.5 → 59.5, frame p99 17.82 → 17.81 ms, peak 2879 → 2872 MB, no new RED line. The check's status is REGRESSION because of its two screenshots (350 s, 385 s). They show different title backdrops (a forest at night vs Gerudo Desert; Lon Lon Ranch vs Lake Hylia), i.e. the backdrop rotation's timing, not a rendering difference. The same two screenshots were flagged between unrelated revisions on 2026-11-12 (0c07813 vs b8cb0bd), and with one run per revision the harness has no noise to compare against. A bit-exact replay check is not available for this game: DM_NVN_CAPTURE cannot capture its multiple NVN queues. Gameplay on the Kokiri Forest deck renders correctly in the bench runs. Other local NVN games (Echoes of Wisdom, Super Mario 3D All-Stars) were not run: pending.
  • iPhone 17 in Kakariko (C-2 to C-5 together, 512 ring, unattended back-to-back runs): instructions per draw 27,287 → 24,870 (−8.9 %), submit CPU a frame at the same clock 16.48 → 14.01 ms (−15 %); the batch's targets (a third, 30 %) are not reached. Table: OoT-fps C-2: texture views of a shared texture are kept by that texture, not only by the serial that asked #2542 (comment)

…re, not only by the serial that asked

Ocarina of Time initializes about 40 render targets again every frame over the same storage, each with a
new serial in a new texture ID. The sharing table hands each one the Metal texture the last frame's had,
but its views (depth as (R, 0, 0, 1), RGBX with alpha 1, sRGB views, image slots' writable views) were kept
by serial (vc_get, dropped at mtl_tex_release) or by texture ID (img_fit), so every frame made them all
again on the submit thread: 84,748 views made against 169,005 reused by frame 4,200 standing on the Kokiri
Forest deck (M4 Pro), about 2,000 with this change.

Views of a texture in the sharing table are now kept by that Metal texture too (vcb_get / vcb_put), for
sampled and image views. A view retains its parent, so a key cannot name another texture while its entry
exists; entries whose texture left the sharing table go at a sweep once a frame (vcb_sweep). Textures
outside the sharing table keep views by serial only. The memory report's view line counts the new hits.

New test t_view_reuse (fails without the change). docs/reference/frame-rate.md entry, symptom index,
game note line.
@dpadr

dpadr commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Tested on an M2 MacBook Air (16 GB, macOS 27.0, AC), Ocarina of Time, together with #2543 / #2542. We applied C-2 and C-3 on our test line (7f50222 base plus our own PR set) behind one runtime switch and ran an in-run A/B: alternating 240-frame phases on and off in the same run, along our scripted Kokiri Forest route to frame 12000 at handheld 1440x810, 60 fps cap. Two valid runs; a third ended early in an off phase (a stall we haven't attributed) and gave no data.

  • The submit thread does less work (both on together): −2.30 M cycles a frame [95% −3.51, −1.18] and −6.2 M instructions [−9.6, −3.6], with load −7 MB and store −4.75 MB a frame, in the run at 2.8 GHz. The other run (3.1 GHz) had the same sign within noise (−1.09 M cycles [−3.23, +0.01]).
  • C-2 hits on this game: 273 shared textures held views and 9,170 views were reused through them over the run, against 1,560 made by the serial cache.
  • Frame and GPU time: no change at these clocks (frame p50 −0.19 ms [−0.56, +0.23]): the submit thread isn't the limit on this machine when cool, so we expect it to show when the CPU is throttled.
  • No regressions: no red lines, no 1 s frames, no held waits. Our port needed two small adaptations because our base lacks newer main code (the nvn_owner.m module and 4c574a6's per-report tracking); on main the PRs apply as written. Unit tests t_view_reuse, t_sbw (switch on and off), t_nvn, t_feedback, t_gpuprof pass.

We couldn't separate C-2 from C-3 in this test; the desk estimate is about 0.25-0.33 ms for C-2 and 0.2-0.4 ms for C-3 of submit-thread time on this route.

@stan69b

stan69b commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

iPhone 17, Kakariko Village: C-2 to C-5 together, before / after

Unattended runs with the batch's drive script (iphone_drive_batch.sh, 7 minutes each, the owner's save in Kakariko, the launcher's 30 fps cap, dynamic resolution, MetalFX, encode thread on). Both apps are oot-build-vis (b32f7fc1 + the visibility ring) with the ring at 512 (ALLOC_MAX * ALLOC_SHARE, as upstream b515089); the after app adds C-2, C-3, C-4 and C-5. Run order base, C, C, base. Medians of the 300-frame summary lines once in the village (first two windows skipped).

run instructions per draw cycles per draw draws a frame submit M instructions a frame submit CPU a frame submit thread clock fps frame p50 submit+present p50 GPU p50
base 1 26,790 10,341 2,691 71.2 12.69 ms 2.21 GHz 29.2 33.7 ms 13.2 ms 30.8 ms
C 1 (slow GPU state) 24,886 11,279 2,706 67.3 8.96 ms 3.34 GHz 12.6 80.0 ms 9.6 ms 69.7 ms
C 2 24,853 9,219 2,771 68.9 14.01 ms 1.84 GHz 28.0 34.9 ms 14.8 ms 31.7 ms
base 2 27,784 10,538 2,783 77.4 16.48 ms 1.78 GHz 25.3 38.8 ms 18.0 ms 34.9 ms
  • Instructions per draw: 27,287 → 24,870 (−8.9 %), the same in both C runs (24,886 and 24,853); on the Mac it was −11 %. The batch's target (a third fewer) is not reached.
  • Submit CPU a frame follows the submit thread's clock more than the instructions: C 2 against base 2 at about the same clock (1.84 / 1.78 GHz): 16.48 → 14.01 ms (−15 %); cycles per draw 10,440 → 9,219 (−12 %). The 30 % target is not reached.
  • C 1 fell into the slow GPU state session A also met with both of its builds (OoT-fps A-1: the queue replay and encode threads declare the frame's deadline, so iOS no longer runs them on efficiency cores at 30 fps #2569: GPU 66-80 ms for the same draws, CPU at 3.3 GHz, 9-13 fps); only its instructions per draw compare.
  • Screenshots of all four runs show the village rendered the same way; the C runs log no red line the base runs do not (the only one in both is the pipeline archive pieces built for a later iOS). env.txt restored after every run; save untouched.

What is left of the submit thread's cost per draw (C-1's ranking in #2542): argument blocks (encode_args own time, pool_gpu, img_binding), pass opens (rp_begin_ex), render-condition operands (rc_value), state records and pipeline lookup, a few percent each, plus the residency commits (res_commit, about 8 %) and new render targets' textures (#2566's area).

@stan69b

stan69b commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

OoT-fps combined phone build, against the baseline (session A, A-2)

iPhone 17, Ocarina of Time, Kakariko Village by day, unattended drive script, 7 minutes each, back to back (2026-11-13, 22:20 and 22:36). Medians over the gameplay windows (more than 1,000 draws a frame).

baseline (512 ring) combined change
fps 28.6 29.4 +0.8
frame time p50 / p95 34.3 / 40.6 ms 33.6 / 37.1 ms p95 −3.5 ms
NVN submit+present p50 14.13 ms 12.25 ms −13 %
GPU p50 31.2 ms 30.4 ms −3 %
submit thread CPU a frame 13.47 ms at 2.05 GHz 12.00 ms at 1.82 GHz −11 % at a lower clock
instructions per draw 26,790 24,614 −8.1 %
draws per frame 2,680 2,770
share of CPU time on performance cores 49 % 51 %
CPU energy 0.99 W 0.84 W −15 %
residency set 3,499-3,561 MB 3,014-3,174 MB −0.4 GB
system wired 5,230-5,397 MB 4,808-4,950 MB −0.4 GB
memory-pressure warns 1 0
thermal state (max) 1 2
  • Both runs show the same screenshots and the same RED line: the stale pipeline archive message, which both builds print.
  • After each run, env.txt was put back byte for byte, and the save was not touched.
  • The game's real-time threads in the combined run were never demoted: 0 of 300 frames on the acquire, queue replay and encode threads.

Against A-1's targets:

  • Submit ≤ 15 ms: reached.
  • A steady 30 fps: nearly; 29.4 median, p95 37 ms.
  • Performance-core share ≥ 60 %: not reached (51 %).

An earlier pair that cannot be compared: the first pair (21:04-21:19) gave 11.4 fps for the baseline and 29.2 fps for the combined build. That gap is the time of day, not the change: the baseline's save loaded Kakariko at night (GPU p50 60 ms) and the combined build's by day, as B found (#2582). Every run's end.png was checked for time of day, and only day runs are compared above.

The combined build is the one installed on the phone.

seedds added a commit that referenced this pull request Nov 14, 2026
Texture views were kept by the serial that asked (vc_get), with the views of finalized textures read through a registered ID in sets of their own (PR #2482, DM_NVN_VC_KEEP). Ocarina of Time initializes about 40 render targets again every frame over the same storage, each a new serial in a new texture ID; the sharing table hands each one the Metal texture the last frame's had, but its views (depth read as (R, 0, 0, 1), RGBX with alpha 1, sRGB views, image slots' writable views, which img_fit kept only by ID) were made again every frame on the submit thread.

A view depends only on the texture it views and its parameters, so the cache is now keyed by the Metal texture, for sampled and image views alike: one lookup, no parentTexture check per hit. A view retains its parent, so the key cannot name another texture while its entry exists. An entry is made only while the owner holds its texture (a live or ghost entry, a pin or the sharing table: own_holds_locked, by address) and goes at the first commit after the owner lets it go: the owner lists the objects that leave their last holder (gone_note, at a count reaching zero, a texture leaving the sharing table, a rename) and vc_commit takes the list (own_gone_take_locked); past 65,536 between two commits it checks every entry instead. A pool's finalize still drops its views. This replaces the cache by serial, PR #2482's finalized textures' sets with DM_NVN_VC_KEEP (removed), and the cache by shared texture the PR first added beside them. Texture buffers' 2D rows are kept with the buffer texture while it is live, as before by serial.

Measured on an M4 Pro (20-core GPU, macOS 27.2), Ocarina of Time, kokiri-deck with scripts/bench.py, three runs a variant against main f43284e (chain a3ee07f000cb): views made about 40 a frame -> about 1.5 (118,209-120,204 -> 4,397-4,711 by frame 5,700, image views now counted too), 2,349-2,406 -> 700-786 textures holding views; frame time unchanged (the game's 30 fps step), no red lines, the pictures right. Submit-thread time not measured on its own (about 0.2 M cycles a frame at PR #2482's 5,900 cycles a view).

Checked: t_view_reuse (19, the PR's test adapted), t_vc_lifetime (19, replaces t_vc_ghost: ghost reuse with no view made, the ghost table alone holding, pool finalize, the list's overflow scan) and t_view_swizzle, t_nvn, t_alias_takeover, t_upgrade_alias, t_image_srgb, t_image_usage, t_serial_owner, t_read_owner, t_view_levels, t_stencil_view, t_srgb_alias, t_pfv, t_cpu_tex, t_feedback, t_partial_upload, t_rt_lifetime, t_queue_lifetime, t_pool_notes, t_tbuf2d, t_tbuf_align, t_pool_chunks; lint_c tidy; test_docs. regress.py check on OCTOPATH TRAVELER II, two runs a side: PASS (frame p99 18.16/18.03 -> 17.92/18.0 ms, same shaders, no new red lines). Not run: Age of Calamity (the texture-ID ring the serial cache was made for; no build on this Mac), the iPhone.

Co-authored-by: stan69b <3304418+stan69b@users.noreply.github.com>
@seedds

seedds commented Nov 14, 2026

Copy link
Copy Markdown
Owner

Landed on main as 87e258b, credited to you with Co-authored-by. Thank you for the diagnosis, the profile and t_view_reuse.

It landed in a different form. Since this PR was opened, #2482 added views kept by serial for finalized textures. With the cache by shared texture beside it, there were three view caches with three lifetimes, and they overlapped: for a shared texture, your cache served the view #2482's sets were made for. So the cache is now one, keyed by the Metal texture a view is of, for sampled and image views alike.

  • When an entry is made: only while the owner holds the texture: a live or ghost entry, a pin, or the sharing table (own_holds_locked, checked by address).
  • When it goes: at the first commit after the owner lets the texture go. The owner lists the objects that leave their last holder (gone_note), and vc_commit takes that list (own_gone_take_locked); past 65,536 between two commits it checks every entry instead. A pool's finalize still drops its views.
  • What it replaces: the cache by serial, Keep texture views for finalized textures that registered IDs still resolve #2482's sets with DM_NVN_VC_KEEP (removed), and vcb_*. Your img_fit key is kept as you wrote it, and so is your test, adapted to the new lifetime. t_vc_lifetime replaces t_vc_ghost.

Measured on an M4 Pro (20-core GPU), Ocarina of Time, kokiri-deck, three runs a side against main with #2482:

main 87e258b
views made by frame 5,700 118,209–120,204 (about 40 a frame) 4,397–4,711 (about 1.5; image views now counted too)
textures holding views 2,349–2,406 700–786
frame time p50 33.2 ms 33.1 ms
red lines 0 0

Submit-thread time wasn't measured on its own.

regress.py check on OCTOPATH TRAVELER II, two runs a side: PASS, frame p99 18.16 / 18.03 → 17.92 / 18.0 ms. Not run: Age of Calamity (no build here) and the iPhone. If you can run your Kakariko drive on main, the per-draw instruction count from this part alone would be welcome.

@seedds seedds closed this Nov 14, 2026
seedds added a commit that referenced this pull request Nov 14, 2026
… while the owner holds it

tbuf_as_2d kept a buffer texture's 2D rows with the texture only while it was live (own_tex_live), so rows a finalized texture first asked for went to the table by what they view (g_tb2_gone), a direct-mapped 1,024-entry table. Yoshi and the Mysterious Book's menus read finalized textures' rows only, and that table missed on about a third of the binds. The view cache by Metal texture (PR #2542) already keeps an entry only while the owner holds the texture (a live or ghost entry, the sharing table) and drops it at the first commit after, so the rows now follow that rule as every other view does; g_tb2_gone keeps the rows of textures the owner no longer holds.

Yoshi and the Mysterious Book, M6, from an empty save past the menus to the opening dialogue, 120 s, partial library on chain d6b05d7481e5:
- to frame 5,100: 114,091 row binds made 40,308 rows views -> 169,774 made 788;
- whole run: 804,505 binds made 108,882 -> 1,095,030 made 9,619;
- frame time 16.7 ms either way (the game's 60 fps); submit time within noise in these windows.

Checked: t_vc_lifetime (27 checks; the new rows check fails without this change), t_view_reuse, t_tbuf2d, t_tbuf_align, t_buffer_image_bias, t_upgrade_alias, t_nvn, t_serial_owner, t_alias_takeover; test_docs; lint_c tidy at its baseline.

Co-authored-by: stan69b <3304418+stan69b@users.noreply.github.com>
Co-authored-by: dpadr <34929160+dpadr@users.noreply.github.com>
@seedds

seedds commented Nov 14, 2026

Copy link
Copy Markdown
Owner

Follow-up on main, 8d003e1: tbuf_as_2d no longer keeps rows with the texture only while it is live. The live-only rule came from my rework of #2482, and Yoshi and the Mysterious Book shows it was wrong. Its menus read finalized textures' rows, and the table by what they view (g_tb2_gone) misses on about a third of those binds.

With the rows following the owner's hold, as every other entry of this cache does: to frame 5,100, 114,091 binds made 40,308 rows views before, and with the change 169,774 made 788. Over 120 s, 108,882 → 9,619. Frame time is unchanged (60 fps cap). t_vc_lifetime has a check for it, and you're credited as co-author on the commit.

seedds added a commit that referenced this pull request Nov 14, 2026
PR #2542's entry gave an estimate. From the same three runs a side (M4 Pro, kokiri-deck), standing on the deck with per-frame instructions fitted against draws a frame at 2,180 draws: 73.93 -> 73.06 M instructions a frame (-1.2 %), submit CPU p50 9.40 -> 9.18 ms.
Kayyluhh added a commit to Kayyluhh/nxnative that referenced this pull request Nov 14, 2026
…he serial cache): superseded by seedds#2542, on main as 87e258b (one view cache keyed by the Metal texture); 0400486024DEC000 keeps enc_thread, which seedds#2341's replay_ahead needs
Kayyluhh added a commit to Kayyluhh/nxnative that referenced this pull request Nov 14, 2026
…y the Metal texture (seedds#2542) in place of seedds#2260's; the game's upscaler takes the game's textures straight (seedds#2559) except square-root input and a blended output (seedds#2366)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400486024DEC000 A game 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