Repository navigation
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
stan69b wants to merge 40 commits into
Conversation
5aebeec to
16f5a3d
Compare
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.
1a369e8 to
688841f
Compare
5822a31 to
0e22e1f
Compare
|
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 |
… 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.
|
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. |
|
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 |
|
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 |
5f03fe5 to
6ca47ce
Compare
… 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.
… 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.
…ync as "N× so far", not "1 times"
…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)
…s import, which this pull request removed
…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.
737dc42 to
4283410
Compare
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(head4283410d, 40 commits): first ontoe6048dea, then onto0f19bdf7for main'sa2514d7d, which removed the profile keyrender_cond(the host now evaluates conditional rendering for every game): the profile drops itsrender_cond online and keepsscreen_aspect any. The rebase onto547c5db6met 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 inpower_line(hle_nvn.c), and both are kept. The last rebase, onto0c078130, met main's queued residency changes (0c078130:res_add/res_removequeue their change, and the commit applies the queue outsideg_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_wire). With main's queue,res_add_atwires a buffer's file pages before it queues the change, andres_committakes 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 nog_res_dirty, which the queue replaced.window_texture). It now uses main's pool table and owner. The virtual pool's runs are tagged aftermtl_pool_replace, and a texture's storage range forres_topis kept beforeown_alias_publish(nvn_owner.m). A span's heap is anNvImageHeapwithwindowset. Main's family-heap search (own_alias_at_locked) skips it, and it is freed with its last texture instead of waiting inrh_wait's free list. Therendering.mdentry keeps main's Fix and Check wording and adds the window heap.cv_due). Main moved the decoder onto its own thread (cv_worker). The clock now runs infill(), both on the guest's release and on the decoder's thread. Pictures it drops reachready()the same way a packet taken does.t_codec_video's paced player waits for the decoder's thread (settle). Withcv_duedisabled, the test still fails.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'ssmax.DM_NVN_GPUPROF's CPU timeline. Its park reasons sit beside main's 5 s queue-wait warnings innvn_queue.inc.The other conflicts were in the game notes,
docs_patches.py(main'sOTHER_SPANwith this branch'sstep_max), the iOS bridging header (DmRotation.hwithsave_sync.h) andt_fstep.c. The symptom index is regenerated. This PR changes no shader chain file; the chain is main's (a5e7fb6e4fa4since main's596d719c).Tests on the rebased head (M4 Mac):
make -C dm/gfx/tests GAME=0400486024DEC000 check_<t>, every test this PR adds or changes, all passing:t_addr_known20,t_capture_live,t_copy_alias126,t_cpu_tex102,t_file_wire10,t_fstep114,t_gpuprof43,t_large_mdi,t_linear_readback61,t_link_ticks43,t_mesh_first_use53,t_mip_extent22,t_pool_notes91,t_present_crop120,t_present_snap13,t_queue_lifetime38,t_queue_origin398,t_readback218,t_render_counter_overlap37,t_render_enable_queue62,t_rt_lifetime9,t_sparse_tiles4,253,t_sub_overlap8,t_wait_event103,t_window_alias25,t_rt_recycle22 (and off). Main's tests for the code under the re-fits pass too:t_res_commit8,t_upload_src41,t_serial_owner176,t_pso_archive343,t_dispatch_barrier31.t_clockwait,t_codec_video,t_nro,t_symbols: all pass.test_docs,test_env_switches,test_oot_aspect(20),test_game_profile,test_nsp_chain: all OK.make -C dm/host GAME=0400486024DEC000builds.4283410d(base0c078130).The device and regress results below are from before this rebase.
What
iPhone and M1 iPad
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 nowmlocked 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).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 whererecommendedMaxWorkingSetSize< 8 GB;DM_NVN_WINDOW_ALIAS=0/1forces it.hle_nvn.cnvnMemoryPoolGetBufferAddress,nvn_mtl.mmtl_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.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.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.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.Any window or screen shape (
screen_aspect any)nvn_mtl.mpresent_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.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_aspectanddm_screen_view_changed; until a front end provides them, these profiles render 16:9.othersof its frame-step rule:probe.c), for values a game caches:DM_PROBE_FPprints float arguments.DM_PROBE_FINDMATscans guest memory for perspective matrices, andDM_PROBE_FINDFfor a pair of floats.DM_PROBE_WATCH=@xN+<offset>arms the write watch on an object a probe reaches.DM_PROBE_XREGdumps any register's memory.front_ios.m,DmLayout.h,DmRotation.h,main.m,DmHostingController.swift,Launcher.swift,DmGamepad.m,settings.tsv,settings.py):DM_ROTATION), on the settings screen and in the in-game menu, locks the orientation, so an iPhone can be played upright.DM_PORTRAIT_LAYOUT, only forscreen_aspect anyprofiles): 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.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.0turns it off.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.screen_aspectkey,DM_SCREEN_ASPECT's any range, and the new switches.game-patches.mdregenerated: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
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.nvn_fstep.hfine,hle_nvn.cnvn_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.nvn_mtl.mlink_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.on-screenlines count gaps at the hold, one vsync longer, and two or more longer.frame-rate.mdentry, "Late frames at a 30 fps cap on the 30 Hz display link", with the measurements, and a pacing bullet in the game note.dm/games/0400486024DEC000/smoke.tsv), soregress.py checkboots 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
nvn_mtl.mmtl_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=1restores 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).DM_NVN_GPUPROFlogs 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.DM_NVN_GPUPROF_GEOMis 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.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:On this branch (04811a6, chain
39eb2f781d07; its shader libraries now exist, built with no compile failures):t_addr_known20/20 (fails without item 3),t_fstep84/84,test_docs.py;t_codec_videopassed on the test build that carries the same change;test_oot_aspect.py18/18 (unicorn), plus the rewriter tests andtest_patches.py;t_present_crop120/120,t_file_wire,t_window_alias,t_image_alias,t_clockwait;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_aspect20/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):
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_fstep106/106,t_nvn277/277,t_copy_alias126/126,t_sub_overlap8/8,test_docs; earliert_thermal21/21,present_rescue101/101.Regress:
scripts/regress.py check --quick --games 0400486024DEC000on an M4 Mac (macOS 27.2, Xcode 27.2). Baseline: upstream mainc4ffb9df. 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.regress.py impactnames 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.regress.pyreports 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.[RED].600 pictures dropped to the movie's clock, 780 put in), so it plays at its own speed. Both runs recordedframes.tsvthe same way, and nothing else ran on the Mac.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.
dispatch_overlap, on for Ocarina)nvn_mtl.mdispatch_enc: keep #1901's body and this PR'sg_stat_disp++besideg_gp_disp++DM_NVN_ISOLATE_TASK) for the Hi-Z task a5288DM_NVN_GPUPROF_GEOM: a render pass's GPU time split into geometry and pixels#1906 (iOS builds link without
IOBluetooth) is already in main as 9983d65.Open
a5288d0a6c4971fd).