Skip to content

Ocarina of Time on an iPhone 17, an M1 iPad and in any window or screen shape: no freeze or paging over the guest's files, GPU-only textures share bytes under 8 GB, the title at full speed, and the picture and HUD follow the window - #1971

Open
stan69b wants to merge 40 commits into
seedds:mainfrom
stan69b:pr/oot-ipad-m1

Conversation

@stan69b

@stan69b stan69b commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Ocarina of Time now plays on an iPhone 17 and an M1 iPad Pro (both 8 GB), and on the Mac it fills any window shape, resized live: no bars, no stretching. The iPhone holds 27–30 fps in Kokiri Forest, and frames just over the 30 fps cap now show at the next vsync instead of waiting a full 33 ms tick. The iPad holds 20 fps where the views are light, but 5–12 fps, GPU-bound, in heavier rooms of the Great Deku Tree. Follows #1829.

Rebased onto main 0c078130 (2026-11-12)

Rebased onto main 0c078130 (head 4283410d, 40 commits): first onto e6048dea, then onto 0f19bdf7 for main's a2514d7d, which removed the profile key render_cond (the host now evaluates conditional rendering for every game): the profile drops its render_cond on line and keeps screen_aspect any. The rebase onto 547c5db6 met main's compute dispatch barriers (#2250): its compute-dispatch counts and this branch's clock-wait line are both new log lines at the same place in power_line (hle_nvn.c), and both are kept. The last rebase, onto 0c078130, met main's queued residency changes (0c078130: res_add / res_remove queue their change, and the commit applies the queue outside g_res_mu); see the residency item below. No commit was dropped: none was empty after the rebase. Authors and dates are unchanged, and each re-fitted commit says what changed in a paragraph at the end of its message.

Five commits needed real re-fitting:

  • File pages wired before a residency commit (file_wire). With main's queue, res_add_at wires a buffer's file pages before it queues the change, and res_commit takes the wired ranges together with the queue, so each commit gives back after itself exactly the ranges of the buffers it made resident. A range wired for a change queued later waits for that change's own commit, so no commit pages the guest's files in from the GPU driver's path, where the M1 iPad froze. The placement-heap commit records the adding line beside the queued change and sets no g_res_dirty, which the queue replaced.
  • GPU-only textures share bytes under 8 GB (window_texture). It now uses main's pool table and owner. The virtual pool's runs are tagged after mtl_pool_replace, and a texture's storage range for res_top is kept before own_alias_publish (nvn_owner.m). A span's heap is an NvImageHeap with window set. Main's family-heap search (own_alias_at_locked) skips it, and it is freed with its last texture instead of waiting in rh_wait's free list. The rendering.md entry keeps main's Fix and Check wording and adds the window heap.
  • The title movie at its own speed (cv_due). Main moved the decoder onto its own thread (cv_worker). The clock now runs in fill(), both on the guest's release and on the decoder's thread. Pictures it drops reach ready() the same way a packet taken does. t_codec_video's paced player waits for the decoder's thread (settle). With cv_due disabled, the test still fails.
  • Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed #1942's frame step (uncraftablexd's commit). Main's average rule (1e3fd031) still applies: idle-GPU late frames and FSTEP_RUN late frames in a row both step up only when the frames at the hold fall short of it on average. The new hold is the cheapest of the three late frames' cost, and a fixed step is held within the stub's smax.
  • DM_NVN_GPUPROF's CPU timeline. Its park reasons sit beside main's 5 s queue-wait warnings in nvn_queue.inc.

The other conflicts were in the game notes, docs_patches.py (main's OTHER_SPAN with this branch's step_max), the iOS bridging header (DmRotation.h with save_sync.h) and t_fstep.c. The symptom index is regenerated. This PR changes no shader chain file; the chain is main's (a5e7fb6e4fa4 since main's 596d719c).

Tests on the rebased head (M4 Mac):

  • Graphics, make -C dm/gfx/tests GAME=0400486024DEC000 check_<t>, every test this PR adds or changes, all passing: t_addr_known 20, t_capture_live, t_copy_alias 126, t_cpu_tex 102, t_file_wire 10, t_fstep 114, t_gpuprof 43, t_large_mdi, t_linear_readback 61, t_link_ticks 43, t_mesh_first_use 53, t_mip_extent 22, t_pool_notes 91, t_present_crop 120, t_present_snap 13, t_queue_lifetime 38, t_queue_origin 398, t_readback 218, t_render_counter_overlap 37, t_render_enable_queue 62, t_rt_lifetime 9, t_sparse_tiles 4,253, t_sub_overlap 8, t_wait_event 103, t_window_alias 25, t_rt_recycle 22 (and off). Main's tests for the code under the re-fits pass too: t_res_commit 8, t_upload_src 41, t_serial_owner 176, t_pso_archive 343, t_dispatch_barrier 31.
  • Host: t_clockwait, t_codec_video, t_nro, t_symbols: all pass.
  • Python: test_docs, test_env_switches, test_oot_aspect (20), test_game_profile, test_nsp_chain: all OK.
  • make -C dm/host GAME=0400486024DEC000 builds.
  • All of the above ran again on the final head 4283410d (base 0c078130).

The device and regress results below are from before this rebase.

What

iPhone and M1 iPad

  1. Freeze over the guest's files (file_wire, t_file_wire): the residency commit paged the guest file in from the GPU driver's path and hung on an APFS lock. File-backed buffers are now mlocked before the commit. No change for anonymous memory (every Mac run by default). If growing the list of wired ranges fails, the new range is unlocked at once, so nothing writes through a null pointer (clang-tidy).
  2. GPU-only textures share bytes under 8 GB of working set (window_texture, t_window_alias): with 7f50222's per-target heaps the iPad ran out of GPU memory at an outdoor save load (5,184 of 5,461 MB). Placed per 32 MB span at the NVN offset × 9/8, no lossless compression. Only where recommendedMaxWorkingSetSize < 8 GB; DM_NVN_WINDOW_ALIAS=0/1 forces it.
  3. Shader-code pools stay out of the raw pool table (hle_nvn.c nvnMemoryPoolGetBufferAddress, nvn_mtl.m mtl_pool_addressed_by, t_addr_known): the game asks for each shader-code pool's address to place its programs, so the raw pool table wired all 223 of them (317 MB), though the GPU never reads shader code here. On the 8 GB iPhone that left too little room for the guest's file pages, which were read back all the time: 14–17 fps, 117 ms frames on average with 27 ms of GPU. A shader-code pool's own address now keeps it out of the table, and a buffer made in such a pool still puts it in. Holds for every game.
  4. Title in slow motion (nvn_fstep.h, hle_nvn.c, t_fstep): the frame step took the boot screens' GPU time (0.4 ms) as its reference, so the title's 40 ms always read as a slowed GPU and probes of hold 1 never stopped. The step now calls the GPU slowed only against a reference that measured the same work: the same draws and, since the title is mostly compute, the same dispatches.
  5. The title movie plays at its own speed (hle_codec_video.c, t_codec_video): a movie the player reads one picture per game frame follows its own clock, so pictures behind it are dropped instead of the movie slowing with the frame step.
  6. Clock waits in the power line (dm_clock_top, t_clockwait): a diagnostic only. It shows Ocarina makes no clock waits, so 8bdfcb7 is not behind its slow motion. The cause is GPU time.
  7. Game note: "For an iPhone or iPad build", with the iPhone's results.

Any window or screen shape (screen_aspect any)

  1. The present takes the crop's shape (nvn_mtl.m present_shape / present_fit, t_present_crop): a crop whose width / height matches the shape the host asked for is shown in that shape instead of 16:9.
  2. screen_aspect any (game.py, loader.c, hle_oe.c, front_mac.m, front_ios.m, settings.tsv): a profile's stubs follow any shape from 0.25 to 6, portrait included, while the game runs. The loader writes the ratio, the size factors, a change count and the camera multipliers to the state page. The Mac reports the window's shape once a resize settles; iOS reports the space the picture has: the screen in landscape, and upright the whole screen or the part above the controls (item 12). The Picture shape setting defaults to auto. Front ends get two new calls, front_view_aspect and dm_screen_view_changed; until a front end provides them, these profiles render 16:9.
  3. Ocarina's stubs, all function-matched others of its frame-step rule:
    • Output size, window textures, camera, live resize: the output size takes the shape, the window textures grow when the crop needs it, the cached camera matrix takes the shape every frame, and a live resize makes the game recompute its size.
    • HUD: every layout file holds a 3840×2160 screen size, and each screen copies its 2D projection to the GPU every frame. The stub reshapes that copy at the HUD's pixel size, and the game's conversions between layout and screen get the same size.
    • The rule: wider than 16:9 keeps the vertical angle, between 1:1 and 16:9 the horizontal one, and upright takes the scale of the same screen turned landscape.
  4. Probe diagnostics (probe.c), for values a game caches:
    • DM_PROBE_FP prints float arguments.
    • DM_PROBE_FINDMAT scans guest memory for perspective matrices, and DM_PROBE_FINDF for a pair of floats.
    • DM_PROBE_WATCH=@xN+<offset> arms the write watch on an object a probe reaches.
    • DM_PROBE_XREG dumps any register's memory.
  5. Upright on the iPhone (front_ios.m, DmLayout.h, DmRotation.h, main.m, DmHostingController.swift, Launcher.swift, DmGamepad.m, settings.tsv, settings.py):
    • Rotation (Automatic / Landscape / Portrait, DM_ROTATION), on the settings screen and in the in-game menu, locks the orientation, so an iPhone can be played upright.
    • Portrait layout (DM_PORTRAIT_LAYOUT, only for screen_aspect any profiles): Full screen (the default) fills the screen with the touch controls over the picture; Picture above controls fills the space above them. Either way the game renders that shape.
    • Other games keep a 16:9 picture above the controls. With a controller and no touch controls shown, the picture takes the whole screen.
  6. Upright cap (loader.c, DM_SCREEN_UPRIGHT_FOV, default 1): an upright picture shows no more height than a square would (79° against about 90° without the cap), so a phone no longer looks up into parts of the world the game never builds. 0 turns it off.
  7. Docs:
    • The game note gets a "Screen shape" section.
    • memory.md: shader-code pools wired by the raw pool table. rendering.md: a camera that caches its projection. movies.md: the movie's own clock.
    • The screen_aspect key, DM_SCREEN_ASPECT's any range, and the new switches.
    • game-patches.md regenerated: docs_patches.py's dry run spaced a rule's other functions 16 MB apart, so the ninth was out of a branch's reach; now 4 MB.

Frame pacing at the 30 fps cap

  1. Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed #1942's frame step is carried here (uncraftablexd's commit, unchanged from Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed #1942 except one conflict resolved in fstep_tick): late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed. The pacing below builds on it. If Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed #1942 merges first, drop that commit from this PR.
  2. Late frames at a 30 fps cap are shown at the next vsync (nvn_fstep.h fine, hle_nvn.c nvn_game_fps, t_fstep): at a 30 fps cap the hold is at most 2 vsyncs, and the display link runs at 30 Hz. A frame a little over 33.3 ms (the iPhone's 36.5 ms in Kokiri Forest) missed its tick and waited for the next one, so about 1 frame in 10 stayed 66.7 ms on screen. When frames are late at the longest hold the cap allows (the same 3 late frames in a row that would otherwise step up), the 60 Hz link runs instead: each frame is still held its two vsyncs, and a late one shows at the next vsync, 50 ms. It is never faster than 30. The 30 Hz link comes back after 8 s with no late frame. Both switches are logged.
  3. That 60 Hz link runs at frame latency 2 (nvn_mtl.m link_latency): at latency 1 on a ProMotion screen it took frames whose GPU work was still running, and held the iPad's 40 ms title frames at 50 ms. Latency 2 gives the same lead the 30 Hz link has at latency 1, so input lag is unchanged.
  4. The frame step's log lines name the cap, the longest hold it allows and why, and the on-screen lines count gaps at the hold, one vsync longer, and two or more longer.
  5. Docs: a frame-rate.md entry, "Late frames at a 30 fps cap on the 30 Hz display link", with the measurements, and a pacing bullet in the game note.
  6. A smoke route for Ocarina (dm/games/0400486024DEC000/smoke.tsv), so regress.py check boots it: an empty save to the title, no input, 400 s. Quick mode pays the cold pipeline prepare, 130–270 s for 14,748 pipelines on an M4, before the first frame.

The iPhone 17 frame on the GPU

  1. A submission's end no longer waits for the encode thread (nvn_mtl.m mtl_sub_commit, t_sub_overlap): with Metal encoding on its own thread (the iOS default), each submission waited for the encode thread to finish the previous one. Ocarina makes about 30 submissions a frame, so on the iPhone 17 frame 3600 spanned 34.9 ms for 20.2 ms of GPU work, 13.6 ms of it gaps before a submission's first encoder, and the replay waited 36 times a frame (10 ms). The records now go to the encode thread with a note of their fence, and the replay goes on; a present still waits for the submissions before it. DM_NVN_SUB_DRAIN=1 restores the old wait. Holds for every game with the encode thread on. On the iPhone it halved the GPU's idle gaps; the frame rate did not change, because gameplay there is CPU-bound (16–20 fps at thermal state 2, GPU p50 21–24 ms).
  2. DM_NVN_GPUPROF logs the host's CPU timeline beside a profiled frame's encoders (nvn_mtl.m, nvn_queue.inc, t_gpuprof): each submission's encoding, each commit, every blocking CPU wait for the GPU by its site, and the NVN queue worker's parks, on the encoders' clock. A diagnostic, off by default.
  3. DM_NVN_GPUPROF_GEOM: split each render pass's GPU time into geometry and pixels #1751's DM_NVN_GPUPROF_GEOM is carried here (nvn_mtl.m, gpuprof.py): item 22 builds on it. Off by default. If DM_NVN_GPUPROF_GEOM: split each render pass's GPU time into geometry and pixels #1751 merges first, drop that commit from this PR.

Checked

  • iPhone 17 (A19, 8 GB, iOS 26.6.1), on 04811a6 (chain 39eb2f781d07), the owner playing from a save in Kokiri Forest, the launcher's 30 fps cap and Dynamic resolution on. For testing, the build also held Ocarina of Time: opt-in task isolation (DM_NVN_ISOLATE_TASK), the Hi-Z task a5288 in command buffers of its own, a workaround for its GPU hangs on an M1 #1842, The game's compute dispatches with no game barrier between them run side by side (profile key dispatch_overlap, on for Ocarina of Time): Kokiri Forest GPU p50 15.63 → 14.76 ms #1901, Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed #1942 and DM_NVN_GPUPROF_GEOM: split each render pass's GPU time into geometry and pixels #1751.

    • Before item 3: 14–17 fps, 6,000–15,600 page-ins every 10 s, 122 of 300 frames dropped. An earlier build without these fixes ended in a kernel panic after the save loaded.
    • With it: 27 fps standing, 27–30 walking, frame p50 36.5 / p95 40 ms, GPU p50 32 ms, 100–400 page-ins every 10 s, about 30 of 300 frames dropped. The title holds 30 fps with its movie at full speed.
  • iPad Pro 11" 3rd gen (M1, 8 GB, iPadOS 27.0.1), on 7ffa63c (chain dedc9391c30b), no switches needed. Two 32-minute runs at the Deku Tree's entrance: 20.0 fps in every window, GPU 34–35 ms, game speed 0.97–1.00, residency 3.2 GB, headroom ≥ 5.87 GB. No crash, hang, GPU error or panic.

  • Screen shape on an M4 Pro Mac, at chain dedc9391c30b:

    • six window shapes resized live while playing: iPhone and iPad, upright and landscape; 1700×478; 16:9;
    • the pause menu, the "A Open" prompt, and the Kokiri Forest cutscene with its location card, at 2.2.
  • On this branch (04811a6, chain 39eb2f781d07; its shader libraries now exist, built with no compile failures):

    • items 3–5: the host builds; t_addr_known 20/20 (fails without item 3), t_fstep 84/84, test_docs.py; t_codec_video passed on the test build that carries the same change;
    • before items 3–5 (16f5a3d): every host test passed;
    • test_oot_aspect.py 18/18 (unicorn), plus the rewriter tests and test_patches.py;
    • t_present_crop 120/120, t_file_wire, t_window_alias, t_image_alias, t_clockwait;
    • Python lint and clang-tidy (LLVM 21; CI uses 23) on 16f5a3d: 0 new findings; not re-run for items 3–5.
  • Upright on the iPhone 17: six 3-minute runs into Kokiri Forest, no crash. Full screen rendered 1200x2600, capped; Picture above controls 1.07:1; landscape 2.18:1. The frame rate was the same upright and in landscape. Duo harness on the Simulator: 42 tests, 0 failures, including a real rotation to portrait. test_oot_aspect 20/20, test_launcher_settings, test_docs.

  • Pacing at the 30 fps cap, M1 iPad Pro (Deku Tree save, Dynamic resolution on, 9,000 frames of camera turns per run):

    Build fps gaps of 33 / 42 / 50 / 67 ms 66.7 ms gaps per 300 on-screen p99
    30 Hz link (before) 27.9 88.6 / 3.8 / 0 / 7.5 % 22.8 66.7 ms
    60 Hz link, latency 2 (this PR) 27.7 81.0 / 2.3 / 16.7 / 0.1 % 0.2 50.0 ms

    Game time stayed at 1.000 of real time.

  • Pacing at the 30 fps cap, iPhone 17 (Kokiri Forest, camera turns): first gameplay at thermal state 1 showed 1,098 / 997 / 1 gaps at the hold / one vsync longer / two or more (p99 45.8–50 ms, game time 0.999), where before about 30 of every 300 frames had a 66.7 ms gap. Later, at thermal state 2, gameplay ran CPU-bound at 13–22 fps and game time fell to 0.73. Those frames are longer than the 3 vsyncs a frame step can carry, so no hold changes that.

  • Tests on the rebased head: t_fstep 106/106, t_nvn 277/277, t_copy_alias 126/126, t_sub_overlap 8/8, test_docs; earlier t_thermal 21/21, present_rescue 101/101.

  • Regress: scripts/regress.py check --quick --games 0400486024DEC000 on an M4 Mac (macOS 27.2, Xcode 27.2). Baseline: upstream main c4ffb9df. Candidate: 404a6dd3, which is this PR's content before its last rebase onto three newer main commits and its two lint fixes, plus the route below.

    • Games: regress.py impact names Breath of the Wild, Pokémon Scarlet and Tears of the Kingdom as affected, and Hades through a broad rule. None of them has a Mac build here, so they are unchecked. Ocarina of Time is the game this PR is about, and the only one booted. It boots with a new smoke route, dm/games/0400486024DEC000/smoke.tsv: an empty save to the title, no input.
    • Result: regress.py reports REGRESSION, from two screenshots only, and it is not one. Both show the title rendered correctly. They differ in the title movie's position: at 385 s the candidate is at the field shot and main still at the forest shot, because the movie now follows its own clock.
      • Shaders: 42,848 / 42,848 on both, no MSL change, no interface finding, no new [RED].
      • Frames: 19.8 → 33.1 fps, frame p50 / p99 49.96 / 114.64 → 33.27 / 34.67 ms.
      • Game time: 0.955 → 1.000 of real time. Main holds 3 vsyncs most of the time with 26 probes and 30 failed trials; the candidate holds 1–2.
      • Peak footprint: 2,778 → 2,765 MB. Cold pipeline prepare: 128 → 142 s.
      • These are single runs, with replay noise not measured.
    • The movie: main reads one picture per game frame, so at 20 fps the title movie plays at about a third of its speed. The candidate drops pictures behind the movie's clock instead (600 pictures dropped to the movie's clock, 780 put in), so it plays at its own speed. Both runs recorded frames.tsv the same way, and nothing else ran on the Mac.
    • Setup: OoT's local Mac build folders hold a shader package split before records carried keys, so the harness ran from a folder that links the build to its keyed re-split.

Related PRs, to merge with this one

None of them is needed for this PR to build or work, and this PR is not needed for them. The iPhone 17 build above carried the first four.

PR What Measured gain In the iPhone build Merging after this PR
#1901 The game's compute dispatches with no game barrier between them run side by side (dispatch_overlap, on for Ocarina) Kokiri Forest GPU p50 15.63 → 14.76 ms (Mac) yes one conflict in nvn_mtl.m dispatch_enc: keep #1901's body and this PR's g_stat_disp++ beside g_gp_disp++
#1942 Frame step: late frames with the GPU idle do not step up; a longer hold the GPU does not explain is probed after a warm start, no more ~1,470 presents held at 20 fps or below (Mac) yes carried in this PR (the pacing builds on it); drop it here if #1942 merges first
#1842 Opt-in task isolation (DM_NVN_ISOLATE_TASK) for the Hi-Z task a5288 the M1 GPU hangs after an area change (about 15 of 16 walks without it) did not happen with it; off by default, a workaround yes, off clean
#1751 DM_NVN_GPUPROF_GEOM: a render pass's GPU time split into geometry and pixels diagnostic: Ocarina's colour pass with its 11 mesh draws is 5.8 ms of geometry, 0.2 ms after yes, off carried in this PR (item 22 builds on it); drop it here if #1751 merges first
#1903 Ocarina takes the launcher's Render scale Kokiri Forest docked at 0.75: GPU p50 15.77 → 14.31 ms, 60 fps held (Mac) no not tried
#1752 Raw global memory loads through one pointer resolve their pool once (chain change) the colour-pass mesh shaders: frame p50 22.1 → 18.8 ms, p95 38.9 → 22.6 ms no needs a full shader build
#1833 (draft) Raw global memory at Metal's own GPU addresses, opt-in per game Kokiri Forest at 854x480: GPU 39.4 → 27.4 ms, frame 33.2 → 23.6 ms (M1 Pro) no needs a full shader build

#1906 (iOS builds link without IOBluetooth) is already in main as 9983d65.

Open

@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation game: 0400486024DEC000 A game labels Nov 9, 2026
@stan69b stan69b changed the title Ocarina of Time on an M1 iPad: no freeze over the guest's files, and GPU-only textures share bytes under 8 GB of working set Ocarina of Time on an M1 iPad and in any window or screen shape: no freeze over the guest's files, GPU-only textures share bytes under 8 GB, and the picture and HUD follow the window Nov 9, 2026
@stan69b
stan69b marked this pull request as ready for review November 9, 2026 17:44
@stan69b stan69b changed the title Ocarina of Time on an M1 iPad and in any window or screen shape: no freeze over the guest's files, GPU-only textures share bytes under 8 GB, and the picture and HUD follow the window Ocarina of Time on an iPhone 17, an M1 iPad and in any window or screen shape: no freeze or paging over the guest's files, GPU-only textures share bytes under 8 GB, the title at full speed, and the picture and HUD follow the window Nov 9, 2026
@github-actions github-actions Bot added rewriter The NSO rewriter and per-build patches ios The iPhone and iPad app (dm/ios) mac The Mac front end and app (dm/mac, front_mac.m) tools Tools and scripts (dm/tools, scripts) game: 01006F8002326000 Animal Crossing: New Horizons labels Nov 9, 2026
AmateurWizrd pushed a commit to AmateurWizrd/nxnative that referenced this pull request Nov 10, 2026
Port of c06337d (PR seedds#1971, seedds/nxnative): only the exclusion in nvnMemoryPoolGetBufferAddress. Its res_top diagnostics (mtl_pool_addressed_by, pool tags) belong to commits this tree does not have and are left out.
@github-actions github-actions Bot added the game: 01007EF00011E000 The Legend of Zelda: Breath of the Wild label Nov 10, 2026
@stan69b
stan69b marked this pull request as draft November 10, 2026 10:58
@stan69b
stan69b force-pushed the pr/oot-ipad-m1 branch 2 times, most recently from 5822a31 to 0e22e1f Compare November 10, 2026 13:00
@lunks

lunks commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

In the screen-shape size stub, the scratch word is state +0xb0, which is the game's dynamic-resolution flag, so any non-16:9 window turns dynamic resolution on whatever the setting. A free word fixes it (we moved it to +0xe0).

@stan69b

stan69b commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

In the screen-shape size stub, the scratch word is state +0xb0, which is the game's dynamic-resolution flag, so any non-16:9 window turns dynamic resolution on whatever the setting. A free word fixes it (we moved it to +0xe0).

Any thing i should do on my PR based on this comment ?

oh, my brain just brained, i'm using a reserved flag if i understand correctly, i'll see to change it

stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 10, 2026
… at state +0xe0, a free word, instead of +0xb0

Review on seedds#1971 (lunks): state +0xb0 is the dynamic-resolution flag on their side, so a non-16:9
window turned dynamic resolution on whatever the setting; they moved the scratch to +0xe0. This tree
has no reader of +0xb0 (loader.c, hle_nvn.c, the profile's other stubs; the setter main+0x1087f40 reads
only the 8 bytes at x1), but +0xe0 is unused here too, so the move costs nothing. test_oot_aspect
checks the setter now gets state +0xe0.
@stan69b

stan69b commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, moved the size stub's scratch to state +0xe0 in 5f03fe5 (test_oot_aspect updated). In this tree I couldn't find a reader of +0xb0 as the dynamic-resolution flag, so the move is a no-op here, but it keeps us in line with your layout.

@stan69b
stan69b marked this pull request as ready for review November 10, 2026 13:42
@webdz9r

webdz9r commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

I was trying to OOT early on on the Vision Pro but was limited by the memory limits. I'm wondering if this might solve that issue

@stan69b

stan69b commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

This PR adds some nice optimizations, but its not a fully playable build. the repo move so fast, i need a slot to merge that part as soon as possible.

i working on a new PR with reduced textures and other memory optimization. those might help even more ;)

EDIT
A new PR is in the works, i got the iPhone running at (30)60fps during the whole first part of the game with metal frame generation, acceptable heat, played for about 40minutes without issues. I just got an issue when getting out of the woods, first time seeing the open world landscape and the app got killed by iOS, too much ram. I'm working on it today. I'm eager to know if you manage to get it running on the vision pro, that would be awesome. I expect the first boss to be doable if it has the same specs or better than my iPhone.

the iPhone 17 holds Ocarina of Time's 30 fps cap through the first dungeon: smaller textures, staging released and MetalFX for its upscaler on small devices - #2270

stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 10, 2026
… at state +0xe0, a free word, instead of +0xb0

Review on seedds#1971 (lunks): state +0xb0 is the dynamic-resolution flag on their side, so a non-16:9
window turned dynamic resolution on whatever the setting; they moved the scratch to +0xe0. This tree
has no reader of +0xb0 (loader.c, hle_nvn.c, the profile's other stubs; the setter main+0x1087f40 reads
only the 8 bytes at x1), but +0xe0 is unused here too, so the move costs nothing. test_oot_aspect
checks the setter now gets state +0xe0.
seedds and others added 27 commits November 12, 2026 20:13
… its own clock, so Ocarina of Time's title movie no longer plays in slow motion under the frame step

The owner saw the scene behind the title in slow motion, as the game was before the frame step. That scene is a 60 fps movie (Event/Movie/Dm_Title_*.webm, AV1 through
nn::codec), and the game's player reads one picture from the decoder each game frame, never skipping: right on the console, where the title runs at 60; under the host's
frame step the title runs at 20-30 fps. Measured with a new DM_MOVIE_TRACE trace of nn::codec packets and picture reads (title, 100 s, M4 Air): one read per game frame,
times 16-17 ms apart, movie speed 0.50 at a 30 cap and 0.36 at the 60 cap (the title at 21.8 fps).

The decoder now answers for such a player (cv_due in hle_codec_video.c): when a released buffer takes a picture, held pictures behind the movie's clock are dropped and the
one nearest the clock goes in. The clock is a picture's time when put in plus host time since; it starts again at the first picture, after 200 ms with none put in (paused),
after a reset and when the stream's time goes back. A dropped picture signals the submission-ready event as a packet taken does. Only streams whose pictures step 4-100 ms
read as nanoseconds are clocked: the unit is the client's, and any other is left as before. A player that keeps up drops nothing. The frame step's movie rule (hold 30 for
30 fps movies, raised by movie::Decoder only) is unchanged.

Test first: t_codec_video's movie clock case (written failing: at 30 reads a second no picture was skipped): 60 reads/s lose none, 30 reads/s step two pictures each read
(299 of 299) and show 10 s of movie in 10 s, 20 reads/s 15 s in 15 s, a µs-timed stream drops none, a 500 ms pause gives no catch-up. "nearest the clock" rather than "last
not after it": on the stream's millisecond-rounded times the latter stepped 1 and 3 pictures by turns (199 skips). t_codec_video all passed; test_env_switches and test_docs OK.

In the game (dmhost_main_mclock, title, 100 s each): speed 1.00 at the 30 cap (30.0 reads/s, steps 33 / 34 ms) and at the 60 cap. Not run on the other nn::codec games
(Donkey Kong Bananza, Fire Emblem Fortune's Weave, Mario Tennis Fever): no regress.py on this branch (the owner's rule for it); their players, read at their movies' rate,
drop nothing by the test's 60 reads/s case.

Docs: docs/reference/movies.md entry, the game note (resolved, and Found on this game), DM_MOVIE_TRACE's line in docs/env_switches.tsv, symptoms index.

Rebased onto main's decoder thread (cv_worker): the clock runs in fill(), where a released
buffer takes a picture, on the guest's release and on the decoder's thread alike; fill returns
the pictures it dropped, and they reach ready() as a packet taken (the release, and the packet's
end through cv_out.dropped). A Stop / Flush restarts the clock with the epoch. t_codec_video's
paced player waits for the decoder's thread (settle) before each read, as main's checks do, and
its clock is atomic since that thread reads it; with cv_due disabled it fails again (skips).
…arina of Time's title no longer probes hold 1 against the boot screens' reference

787e00d counts the GPU as slowed only while the draws at the longer hold match the reference's. Draws
do not see compute: on the M1 iPad the title spends 61 ms on its movie network's 14 dispatches with
20-80 draws, while the hold-1 reference came from the boot screens (0.4 ms, no dispatch). The title read
as a GPU slowed 9.9 times and probed hold 1 every 3-6 s (4 in its first 350 presents), each probe about
33 frames at hold 1, none held. nvn_fgpu now keeps the dispatches a frame encodes (+1, so none is a
count) beside the draws, and both must match; mtl_dispatches() gives the count (dispatch_enc).

t_fstep 94 checks, 0 failed: the title case does not probe with dispatch counts and probes 17 times
without; the same draws and dispatches at both holds still give BotW's one probe that holds.
…st), and a reference entry for the shader-code pools the raw pool table wired
…ngs screen and in the in-game menu locks the orientation, and lets an iPhone be played upright

DM_ROTATION, a launcher and in-game menu setting (dm/games/settings.tsv, platform:ios). The phone's Info.plist lists only the two landscapes, so an iPhone could never turn upright: the app delegate (main.m) now answers application:supportedInterfaceOrientationsForWindow: with front_ios_app_orientations (front_ios.m), which replaces that list. Under auto, the default, it returns the list itself (the phone's two landscapes, the iPad's four), so nothing changes for anyone who leaves it. landscape locks the two landscapes; portrait upright (and upside down where the device has it). The root view controller and the launcher's DmHostingController allow every orientation under auto and the locked ones otherwise (dm_rotation_vc_mask; the pure mask is DmLayout.h dm_rotation_mask). A pick calls setNeedsUpdateOfSupportedInterfaceOrientations on every view controller and, when the window's orientation is no longer allowed, requestGeometryUpdateWithPreferences, so the screen turns at once: on the settings screen as soon as it is picked (Launcher.swift onChange), in the in-game menu (dm_setting_live), and at the start for a saved pick, DM_LAUNCHER=0 or an env file. Upright, the settings screen puts the game's name and Play above the settings instead of in a third of the width.

Duo harness: the fixture defines DM_GAME_SCREEN_ASPECT and the stubs add dm_screen_view_changed, dm_players_reported and front_web_touch, which front_ios.m and DmGamepad.m call since screen_aspect any and the controller applet's prompt: the harness did not compile or link without them. Its app delegate answers as main.m's. New test: the masks for each value, and the in-game menu offering Rotation.

test_launcher_settings: DM_ROTATION joins the iOS-only settings (the test listed them; updated deliberately).

Checked: the Duo harness on the iPhone 17 Simulator, 41 tests, 0 failures (1 skipped, iOS 27.1 only), including the rotation test of the next commit, which turns the Simulator's landscape-only iPhone upright; test_launcher_settings, test_orientation_policy, test_docs.

Front-ends: new: DM_ROTATION (Rotation). An iOS host app with its own app delegate answers application:supportedInterfaceOrientationsForWindow: with front_ios_app_orientations() (DmRotation.h) for Portrait to turn a phone upright
… touch controls instead of a 16:9 picture with bars, and with a controller the whole screen

A profile with screen_aspect any renders the shape the front end reports (front_view_aspect, dm_screen_view_changed). front_ios.m reported the app window's shape in landscape and 16:9 upright, so upright the game rendered 16:9 and the front end fitted it, with bars, into the part above the controls. DmLayout now carries `area`, where the picture may go: the view in the overlay layout, the part above the controls in a split. viewDidLayoutSubviews reports that area's shape (on a TV the TV's), so upright the game renders the space above the controls: 402x375 points (1.07:1) on an iPhone 17 instead of a 402x226 picture. Landscape reports the whole view, as before. Profiles without screen_aspect any keep the 16:9 picture above the controls.

With a controller connected the touch controls hide (DmGamepad.m, now dm_gamepad_off, which lays the view out again when that changes): upright with no fold to split at, the picture's area is then the whole safe area (DmLayout.h dm_layout_controls_hidden); a half-folded Duo keeps its split, landscape is unchanged. Fill applies only to a landscape view, so a 16:9 picture upright with the controls hidden stays fitted instead of losing most of its width.

Tests (Duo harness): the area in landscape and upright, the hidden-controls layout and Fill upright, the shape the production root reports, and a real turn of the Simulator's iPhone to Portrait and back: the shape handed to the host is the area's, and once the game presents it the picture fills the area (402x375).

Checked: the Duo harness on the iPhone 17 Simulator, 41 tests, 0 failures (1 skipped, iOS 27.1 only).
…cture's (DM_SCREEN_UPRIGHT_FOV), so a phone upright no longer looks up into parts of the world the game never builds

Upright, the camera rule takes the scale of the same screen turned landscape, its width getting landscape's vertical angle; a phone upright (0.46:1) then sees about 90 degrees vertically (Ocarina's fovy is 50 degrees at 16:9), and above Link's house the world is not built. loader.c now raises the camera's y multiplier (state +0xc4 / +0xc8, which the size stub copies with the size) to that of a picture of the cap's shape when it is smaller, and x with it, so the projection keeps the screen's aspect and the width narrows instead: by default the cap is a square (79 degrees). DM_SCREEN_UPRIGHT_FOV moves it (1.777: the game's own vertical angle) or turns it off (0); landscape is never capped. The HUD's multipliers are the camera's divided by the largest of them, so the HUD does not change. No stub changes: the multipliers were already the loader's (screen_camera, screen_upright_cap; the log line names them).

test_oot_aspect: camera() mirrors the cap (checked against loader.c's code compiled on its own for 0, 1 and 1.777 and six shapes: equal to 1e-5). test_shapes encoded the uncapped rule for a phone upright; it now expects every upright shape to show a square's height (updated deliberately); the uncapped rule moved to test_upright_without_the_cap, and test_the_upright_cap_holds_the_vertical_angle checks the cap at 1, 0.75 and 16:9, the shape kept, and landscape untouched.

Checked: test_oot_aspect (20), loader.c syntax-checked with -Wall -Wextra. Not yet looked at above Link's house or in the Kokiri Forest cutscene upright.
…nders any shape fills the whole screen with the touch controls drawn over it, as in landscape; Picture above controls keeps the split

The owner asked for the picture to fill the whole screen upright with the controls over it, as in landscape, and to keep the split of the previous commit as a choice. DM_PORTRAIT_LAYOUT (launcher and in-game menu): full (the default) or split. Full screen (DmLayout.h dm_layout_full_picture) keeps the split's control arrangement in the lower half, so the controls are where the portrait layout puts them, and lays the picture over the whole view under them; the picture's area is the whole view, so the game renders the screen's shape (0.46:1 on an iPhone) and the upright cap of the previous commit holds its vertical angle to a square's. Touches above the controls go to the game's touch screen as in the split. It needs a picture that takes the screen's shape, so it applies only to a profile with screen_aspect any following the view (DM_SCREEN_ASPECT auto, front_ios.m portrait_full); other games keep their 16:9 picture above the controls, where centred on an upright screen it would sit under them, and a half-folded Duo splits at its fold. With a controller Full screen stays as it is (the controls hide).

The row is shown only for a screen_aspect any profile on iOS: settings.py's shown column now takes <cond>&<cond>, as the options column already did.

Tests: Duo harness, the full-screen layout (area the whole view, the controls' rectangle kept and inside the picture, landscape and a half-folded Duo unchanged) and the production root turned upright in each layout; test_launcher_settings, the row for Ocarina on iOS only.

Checked: the Duo harness on the iPhone 17 Simulator, 42 tests, 0 failures (1 skipped, iOS 27.1 only); test_launcher_settings, test_docs.

Front-ends: new: DM_PORTRAIT_LAYOUT (Portrait layout), and settings.tsv's shown column takes <cond>&<cond>
…after every uncapped shape

55b30f6 put the upright cap's line before the 16:9 line's else, so the else
followed the cap test instead of the shape test: an iPhone run logged
"screen shape 1.072 ... camera x 1.000 y 0.603" then "screen shape 16:9"
although the state page held 1.072. Only the log was wrong.
…ight cap), measured on an iPhone 17 with Ocarina of Time

The game note's Screen shape section follows the picture area upright
(Full screen 0.46:1, capped; Picture above controls 1.07:1), records the
upright cap and the HUD upright, and the six iPhone runs: every shape in
proportion, the same frame rate upright and in landscape, and the one-frame
present crop red line while the window textures grow. The iOS layouts
reference entry gains the symptoms, the fix, the harness checks and the
iPhone test; the symptom index is regenerated.
…read and the replay goes on, so the GPU no longer idles before each submission

With Metal encoding on its own thread (the iOS default), mtl_sub_commit waited
for the encode thread to finish the submission (rec_drain). Ocarina of Time
makes about 30 submissions a frame (the queue scheduler cuts one at every event
signal), each under the 64-draw segment size, so the GPU got each one only after
the replay had waited for the encode thread and then translated it: on the
iPhone 17 frame 3600 spanned 34.9 ms for 20.2 ms of GPU work, 13.6 ms of the
gaps before a submission's first encoder, and the replay waited for the encode
thread 36 times a frame (10 ms).

Now the submission's records go to the encode thread with a note of its fence
(RO_NOTE on g_sub_cmt) and the replay goes on. What follows on the queue is a
record too; a CPU wait for the GPU waits for the fence's signal behind the
commit; graphics suspend waits for the last fence. The display command buffer of
a present without a snapshot, the one direct commit that reads the game's work,
waits for the note of the submissions before it (sub_commit_wait).
DM_NVN_SUB_DRAIN=1 brings the old wait back, ab alternates with DM_AB.

t_sub_overlap holds the encode thread inside a submission and checks the
submission returns, the note and a present's wait hold until the commit (3 of 8
checks fail with the old wait). With DM_NVN_ENC_THREAD=1: t_reorder_chain,
t_defer_chain, t_copy_alias, t_unwritten_store, t_alias_takeover, t_rt_range,
t_clear_tex, t_clear_ds, t_upgrade_alias, t_dispatch_overlap, t_perf_order,
t_present_crop and check_t_present_snap pass; t_feedback's 3 segment-timestamp
checks fail the same with DM_NVN_SUB_DRAIN=1 (the encode thread writes no
segment timestamps, frame-rate.md), so not from this change.

Left: the iPhone A/B (DM_NVN_SUB_DRAIN=ab) and the regress games.
…r hold the GPU does not explain is probed

On a warm start Ocarina of Time stepped hold 1 → 2 → 3 at presents 9 and 12 (cost 1.92 and 5.57 vsyncs, GPU 0.02 and 0.01) on a progress screen while the pipeline archive was written. The game waits for its frames outside the window's acquire, so its cost at hold 3 was about the hold itself (54 ms, GPU 12.5 ms): the fit trial waited and no probe fired, and it stayed at 18 fps for about 1,470 presents.

- nvn_fstep.h: the caller reports each frame's GPU time (gpu_on, gpu). A late frame with the GPU under FSTEP_GPU_IDLE (0.05 of a frame) neither counts toward the 3 late frames of a step up nor ends their run; FSTEP_RUN late frames in a row still step up
- nvn_fstep.h: FSTEP_RUN frames in a row at a hold above kmin whose GPU time fits k − 1 and that waited for nothing in the host's pacing start a probe (nvn_fstep_unpaced), with their own hold-off (uhold) doubled after each failure up to 60 s and back to 1 s when kmin holds
- hle_nvn.c fstep_tick: passes the GPU time; the step log names the idle-GPU step up and the new probe, and its appends clamp their length
- t_fstep: idle-GPU late frames, the unpaced probe (also with the GPU clock falling at hold 3), CPU-bound games at hold 2, 2.95 and 3.2 vsyncs, a CPU-heavy scene with the GPU idle (5 of 88 checks fail without the change)
- docs/reference/frame-rate.md: new entry; symptom index; Ocarina of Time's Found-on line

Rebased onto main's average rule (1e3fd03): idle-GPU late frames and FSTEP_RUN late frames in
a row both still need the last FSTEP_RUN frames at the hold to fall short of it on average
(short_of_k), the new hold is the cheapest of the three late frames' cost, and a fixed step is held
within the stub's smax. t_fstep: main's 86 checks and this commit's, 102 in all, pass.
… next vsync (60 Hz link), not on the 30 Hz link's next tick

At a 30 fps cap the frame step holds at most 2 vsyncs, and the display link runs at 30 Hz, so a frame
that misses its tick waits for the next one: 66.7 ms on screen. Ocarina of Time on an iPhone at the
launcher's 30 cap (frames 36.5 ms, GPU 32 ms) left 33 of each ~333 ticks empty: 33 intervals of
66.7 ms in each 300 frames, the rest 33.3, 27 fps (log iphone5/log_walk.txt).

nvn_fstep.h: the late frames that would step the hold up (3 in a row, or FSTEP_RUN with the GPU idle)
set fine when the hold is already at kmax; it ends after FSTEP_HOLD_MAX vsyncs with no late frame,
when the hold may rise again and while the game fixes its own step. hle_nvn.c: nvn_game_fps reports 60
while fine at a 30 cap, so link_sync runs the 60 Hz link, which holds each frame its two vsyncs
(g_link_hold_until) and shows a late one at the next vsync: 33.3 / 50 ms, never faster than 30. The
switch is logged both ways; the frame-interval line keeps its 33.3 ms target, and the on-screen line
counts intervals at the hold / one vsync longer / two or more.

t_fstep: 13 new checks (106, all pass); t_nvn 277 and t_thermal 21 pass.
…hold it allows and why the hold may go no higher

A log whose frame step sat at hold 2 under a 30 fps cap (iPhone, held 1 / 2 / 3 vsyncs 0 / 1800 / 0
with 80% of frames late) did not say that hold 2 was the ceiling, nor why. fstep_kmax() now gives
fstep_tick its kmax and the reason: the 30 fps cap's 30 Hz display link, where a hold of 3 would take
66.7 ms (late frames go to the next vsync of a 60 Hz link instead, B-2); fps_hold off; 120 fps; or
the games' own interval logic (1-3) at a 60 cap. The cap-change line ("frame step cap 0 → 30 fps at
present 1: hold at most 2 vsyncs, ...") and the periodic game-time line carry it, the latter with
whether late frames are shown at the next vsync now and how often that was switched on.

t_nvn 277 and t_thermal 21 pass.
…runs at frame latency 2, so a ProMotion screen shows them at 33 / 50 ms

On an M1 iPad (120 Hz ProMotion) the first B-2 build switched to the 60 Hz link at frame latency 1,
which calls back 16.5 ms before the target time. It took a frame whose GPU work was still running
(frame 1951: GPU end 3 ms after its target), the present landed one 120 Hz refresh late, and the
link skipped its next tick. At the title (40 ms frames) the acquire, which counts ticks, then held
every frame 50 ms (25 -> 20 fps). In gameplay (36 ms frames) the screen showed frames on the 120 Hz
grid, 33 / 42 ms (41 / 54 %), instead of 33 / 50.

link_latency (nvn_mtl.m, agreed with session A, who owns the file) returns 2 while hle_nvn.c's new
nvn_fstep_fine() reports the fine mode: the same 33 ms lead the 30 Hz link has at latency 1, so the
input lag is the cap's own link's. link_sync already applies a latency change each frame and logs it.

M1 iPad, Deku Tree spot, DM_FPS_TARGET=30, dynamic resolution on, 9,000 frames of camera turns each:
  before (30 Hz link):       27.9 fps, on screen 33 / 42 / 67 ms 88.6 / 3.8 / 7.5 %, 22.8 of 66.7 ms per 300, p99 66.7
  B-2, latency 1:            27.2 fps, 33 / 42 / 50 ms 40.8 / 53.7 / 5.3 %, 0.2 per 300, p99 50; title held at 20 fps
  B-2, latency 2 (this):     27.7 fps, 33 / 42 / 50 ms 81.0 / 2.3 / 16.7 %, 0.2 per 300, p99 50; title 25 fps
Game time 1.000 of real time in all three. present_rescue 101 checks, t_nvn, t_thermal pass.
…ut every 10, written down with the iPhone's and the M1 iPad's numbers

docs/reference/frame-rate.md, "Late frames at a 30 fps cap on the 30 Hz display link": the 30 cap
holds at most 2 vsyncs because its link runs at 30 Hz. A 36.5 ms frame comes 3.2 ms later on the tick
grid each frame, so after about 10 a tick finds the queue empty and the previous frame stays 66.7 ms:
300 frames over ~333 ticks, 27 fps, 11% of the gaps at 66.7 ms, nothing discarded (iPhone empty ticks
31-33 per 300; "dropped" in the on-screen line is a gap over 1.5 holds). The frame step's paybacks
(steps of 3 on 385 of 1,800 frames) do not fall on those gaps. The entry also holds the fix (B-2), the
M1 iPad's three runs, and what is open: the iPad title's uneven gaps at 40 ms frames, the title movie's
pictures (~210 of 300 frames get one), hold 3 under the cap (not needed), the power cost, and the
iPhone, not yet measured. The game note's iPhone/iPad section and "Found on this game" link to it;
the symptom index is regenerated. test_docs passes.
…hle_nvn.c as one file compile again

B-2 (effae9a5) gave nvn_mtl.m a weak default nvn_fstep_fine for the tests that have no frame step,
and hle_nvn.c its strong definition. The tests that include both files in one translation unit
(nvn_replay and 19 t_* tests) then stopped compiling: "redefinition of 'nvn_fstep_fine'". They
rename nvn_mtl.m's weak nvn_game_fps around its include for the same reason; they now rename
nvn_fstep_fine the same way. All of them build and pass again, including check_t_present_snap
(8 runs) and check_t_capture_live (2 of 2 replays match).
…nc, and why a hold of 3 would not help its slow gameplay

Session A's iPhone 17 run of the merged build at the 30 cap (iphoneA4_merged30): the first gameplay at
thermal 1 showed gaps at the hold / one vsync longer / two or more of 1,098 / 997 / 1 (p99 45.8-50 ms),
where the 30 Hz link gave 30-33 gaps of 66.7 ms per 300. Later, at thermal 2, gameplay ran CPU-bound at
13-22 fps and game time fell to 0.731: frames longer than the 3 vsyncs a step can carry (FSTEP_MAX), which
the frame step already hands the game at hold 2 while it pays back, so option (a), hold 3 under the cap,
would change neither game time nor what the 60 Hz link shows. The frame-rate entry and the game note say so.
…d the allocator pool tried and reverted

Game note, "For an iPhone or iPad build": a new current bullet for the iPhone 17 at the owner's
Kokiri spot. Its table gives the GPU span, the GPU work and the gaps before and inside
submissions, before and after A-2. Gameplay GPU p50 is 21-24 ms, under the cap's 33.3 ms, so
shader pull requests seedds#1752 and seedds#1833 were not tried. The frame is CPU-bound at 16-20 fps under
thermal state 1-2. The bullet records the uncapped A/B of A-2, and a command allocator pool of
128 that removed its waits but no frame time, at a cost of about 300 MB (not kept). Open:
translation cost, samples-passed reports, footprint growth. The CPU timeline the table was
measured with is a local diagnostic, not in this pull request.

rendering.md: the A-2 entry gets its measured result. A Found-on line in the note, and the
outdated iPhone line under "Not run on an iPhone yet" now points to the section.
…sh stages finish, so a profile splits a pass between geometry and pixels: Ocarina of Time's colour pass with its 11 mesh draws is 5.8 ms of geometry and 0.2 ms after it

A second counter heap takes two relaxed stamps at each render pass's end (after the vertex stage and after the mesh stage); the pass line ends in "geom <us>", the time from the pass's start to the later of the two that falls inside the pass. gpuprof.py's timeline splits each pass into geometry and the time after it, and its table adds a geom ms column. Off by default: without the switch nothing changes.

(cherry picked from commit 69552d8)
…profiled frame's encoders

While a frame is profiled, each submission's encoding, each real commit to the
queue (on the encode thread, from before its uniform-copy wait), every blocking
CPU wait for the GPU by its wait-ledger site, and the NVN queue worker's parks
(no work, or held by an event or sync wait) are logged as "[gpuprof] F<n> cpu"
lines on the encoders' clock; encoder lines name their submission ("sub").
Metal 4's counter-heap stamps are Mach ticks (checked on an M4 Mac: a stamp
falls between the commit's and the completion's mach_absolute_time), so no
conversion is needed; the report says whether the clocks agree.

First iPhone 17 run (Kokiri Forest, this build's pipelines still compiling):
frame 3600 spans 34.9 ms for 20.2 ms of GPU work over 30 submissions; 13.6 of
the 14.75 ms of gaps come before the first encoder of a submission, with the GPU
done and the replay still encoding that submission (A-2).

t_gpuprof checks the timeline through the real wait, commit and park paths.
…PROF's CPU timeline, now in this pull request
…ee switches that are not on main are no longer quoted as switches

docs/env_switches.tsv: DM_NVN_SUB_DRAIN (A-2's switch to restore the old wait at each submission's end)
gets its class and meaning.

The docs quoted three names that no code reads. Each mention now drops its backticks and says the
switch is not on main:
- DM_NVN_VC_WAYS: another fork's switch, in memory.md.
- DM_NVN_CUT_FIRST: a probe switch since removed, in Breath of the Wild's note.
- DM_NVN_RT_RECLAIM: PR seedds#1588's switch, in rendering.md, and so in the symptom index too
  (docs_symptoms.py).
…ap, as the test passes 0.75 and 16/9 (pyright ratchet in CI)
…ots it

An empty save to the title: the cold pipeline prepare (265 s for 14,748 pipelines on an M4 in quick mode, with no game frame meanwhile), the logos, then the title with its movie, where the frame step and the movie clock work. No input. Calibrated with one quick baseline run of upstream main c4ffb9d: first frame at 268 s, frame 897 at 300 s.
… at state +0xe0, a free word, instead of +0xb0

Review on seedds#1971 (lunks): state +0xb0 is the dynamic-resolution flag on their side, so a non-16:9
window turned dynamic resolution on whatever the setting; they moved the scratch to +0xe0. This tree
has no reader of +0xb0 (loader.c, hle_nvn.c, the profile's other stubs; the setter main+0x1087f40 reads
only the 8 bytes at x1), but +0xe0 is unused here too, so the move costs nothing. test_oot_aspect
checks the setter now gets state +0xe0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 01006F8002326000 Animal Crossing: New Horizons game: 01007EF00011E000 The Legend of Zelda: Breath of the Wild game: 0400486024DEC000 A game host The Horizon host: SVCs, services, threads, memory (dm/host) ios The iPhone and iPad app (dm/ios) mac The Mac front end and app (dm/mac, front_mac.m) nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) rewriter The NSO rewriter and per-build patches tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants