Repository navigation
Ocarina of Time keeps real speed below 60 fps: its frame step follows the measured frame time - #1543
LeonardoBai12 wants to merge 2 commits into
Conversation
|
Tested this change on an M4 MacBook Air (24 GB, macOS 27.2), cherry-picked alone onto main 75c1a8c (no conflicts), in scripted runs from the Kokiri Forest autosave at Link's house: game time against wall time was 0.998–1.000 with the change on, against 0.50 at 30 fps and 0.67–0.70 at 40–42 fps with it off; the same timed walk to the door took 1.5 s at 30 fps and uncapped with the change, 3.0 s without it at 30 fps. No GPU errors, footprint 10.2–10.8 GB. With the change, |
|
Thanks @PucaVaz, this is very helpful, especially testing it alone on main. I added your result to the PR description. I am leaving the launcher frame-rate choice out of this PR to keep it small, since it needs a separate decision about the profile's |
|
Compared #1543 and #1584 in game on an M4 MacBook Air (24 GB, macOS 27.2, built-in 60 Hz screen). Setup. Two Mac apps from the same tree: main b2d46b8 + #1544 + #1555, one with #1543 (f301c698; the later commit only changes the test) and one with #1584 (bf5dc97).
Route. Scripted runs from the Kokiri Forest autosave in Link's house: 60 s in the house, a timed walk to the door, out of the door, Saria's greeting, 8 screenshots on the balcony, then 180 s of scripted stick pushes outside (the walk). The runs alternated between the two apps. Game speed: both run at real time.
Frame pacing (house = 60 s idle, walk = 180 s outside):
"Present → display" is the host's At a 30 cap the two behave the same on this Mac: every frame is held 2 vsyncs, and the game runs at real time. At #1584's default 60 target, this Mac does not hold 60 (21–24 ms of GPU per frame at hold 1).
At the door, #1543 shows the game's one-frame stall (119–146 ms in 3 of 4 runs), as does #1584 at a 30 cap (128 ms). #1584 at its default has no frame over 74 ms there, since it is already at hold 3. Launcher. #1584's No GPU errors, and the same Not covered: docked mode, MetalFX, areas past the start of Kokiri Forest, a Mac that holds 60, and code in the game that might use 1/60 directly (#1543's note). |
…tep (upstream PRs seedds#1497 and seedds#1543) Both stubs are FRAME_STEP of main e0f3ccfaf50065ae4ea01eb5aeb13b1a, so the build's function runs both: real_speed at main+0x6e6cc4 (frame step from the measured frame time) and render_scale at main+0x31d3f48 (DM_RENDER_SCALE caps the game's dynamic-resolution scale). test_oot_real_speed checks the build takes both sites and marks the render-scale capability; game-patches.md regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Aax1HnchwCweUDRNWjhHx9
…s first write stays resident while any of them holds it: releasing one took it out of the residency set (GPU page faults, issue seedds#1326) The serials of one storage share one packaged texture (g_pkg_share). At the GPU's first write over that storage, cpu_unc_drop gives each of them the package's texture itself (tex_put), so they all hold one texture that was in no shared set. The first holder released (tex_put's ghost-ring eviction, serial_put or serial_del, through obj_drop or defer_obj_release), or the package's own release (NvPkg dealloc's res_remove), took it out of the residency set while the other serials still drew with it, and a command buffer reading it faulted. pkg_hand_over enters the handed-over texture in g_alias_set and in g_handed, a list by the package's sharing key, so releasing one holder only drops a reference (alias_has). The package's release and its re-upload's spare leave a handed-over texture resident, mtl_alias_drop_pool retires it with its pool, and tex_upgrade moves its entry. It does not enter g_alias, so a new texture object over the same storage still gets a texture of its own. DM_UNC_SHARE=0 gives back the old hand-over. Ocarina of Time v131072, Mac mini M4 16 GB, macOS 27.2, on main b2d46b8 with seedds#1590, seedds#1557, seedds#1556, seedds#1575, seedds#1522, seedds#1543 and seedds#1390: page faults at 44, 29, 24 and 15.5 minutes, with the missing textures named by DM_RES_CHECK and a diagnostic of where each left the residency set, before two of them, as 260x260 terrain tiles handed over this way and released by the ghost-ring eviction. One binary: with the change, 107 minutes and no fault (86 hand-overs); with DM_UNC_SHARE=0, a fault at 37 minutes with the same kind of tiles named. t_cpu_tex --metal handover_shared: 97 checks, 0 failed; with DM_UNC_SHARE=0, 2 failed. The other GPU suites are unchanged. (cherry picked from commit 8cbc330)
… the measured frame time The frame loop computes each frame's step as a constant over its target rate and never reads the clock, so everything ran in slow motion below the target. Found by gunvalk in zortos293/Lichii PR seedds#216; written here from those facts. A stub keeps the game's own step and takes the measured frame time when that is longer, and the host measures only for a main that marks the state page. DM_FRAME_DELTA=0 turns it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k asks Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
c254ba9 to
4d4f967
Compare
|
Thank you @PucaVaz for the careful side-by-side, especially keeping the shaders and pipeline archive byte-identical so only the frame step differed. My reading of it:
The two could also work together: #1543 for the uncapped default and #1584's I rebased this branch on current main (12ddb1f), so the CI failures from the old base ( |
…tep (upstream PRs seedds#1497 and seedds#1543) Both stubs are FRAME_STEP of main e0f3ccfaf50065ae4ea01eb5aeb13b1a, so the build's function runs both: real_speed at main+0x6e6cc4 (frame step from the measured frame time) and render_scale at main+0x31d3f48 (DM_RENDER_SCALE caps the game's dynamic-resolution scale). test_oot_real_speed checks the build takes both sites and marks the render-scale capability; game-patches.md regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Aax1HnchwCweUDRNWjhHx9 (cherry picked from commit 1e0258b)
…mmits on top of main, without the real-speed and render-scale frame steps (seedds#1543, seedds#1497), which main's own frame step (7d8dcdf) and dynamic-resolution switch (04419bc) replace (this merge keeps the earlier branch history reachable, so the push needs no force) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Aax1HnchwCweUDRNWjhHx9
…n: with the measured-time words inserted, main's fixed cbz offsets (+13, +11) landed inside them, and an iPhone 18 Pro Max faulted at x17 + 0x3c (x17 = 0) on the first frame, before the host had decided a step; the block addresses the page through x16 (PR seedds#1543's sequence). test_oot_frame_step runs the stub under Unicorn: step not decided, target 0, a slow frame, never below the host's step, and x17 lost after a store Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ull requests brought in early (seedds#1459, seedds#1543)
…ub: the ratio is never below the measured frame time (PR seedds#1543's state +0x38 / +0x3c, host measurement and t_fstep checks), so it holds with DM_FPS_HOLD off Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n: with the measured-time words inserted, main's fixed cbz offsets (+13, +11) landed inside them, and an iPhone 18 Pro Max faulted at x17 + 0x3c (x17 = 0) on the first frame, before the host had decided a step; the block addresses the page through x16 (PR seedds#1543's sequence). test_oot_frame_step runs the stub under Unicorn: step not decided, target 0, a slow frame, never below the host's step, and x17 lost after a store Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for this. One finding from the HookLink fork that touches the stub, and a check you may want to add. In HookLink's tree, #1543's measured-time words were inserted into the frame-step stub that is on I read your head (4d4f967) and it does not have this problem. Your One suggestion, not a blocker. HookLink's test has a |
|
If the floor ever meets |
|
Thanks @oliverames for reading the stub so closely, and for checking that this head doesn't address the page through
The idea came from gunvalk's work in zortos293/Lichii PR #216. Thanks again @PucaVaz for the side-by-side with #1584. |
…es are not whole vsyncs long: below the target a GPU-bound Mac runs at 55-59.7 fps, and the host's whole-vsync step left the game slightly slow and paid the time back a vsync at a time (not at all where frames are over two vsyncs at hold 1: 0.908-0.934 of real time at 27 fps). The stub now takes the larger of the host's step × base / 60 and base × the frame time the host measures (state +0x38: the mean of the last 8 present intervals, a stall at most 0.1 s, at most 4 frames, 0 under 1.01 frames), and marks +0x3c so the host measures only for a main that reads it; nvn_fstep_measured pays back no whole vsync beside a measured time, counts what the game stepped in the debt and hands the time out only while the game is behind (both together ran the game at 1.003). The two hazards named in PR seedds#1543's review are handled: the stub's cbz offsets are recomputed for the added words (+19 / +17) and the page is taken again into x16 with the load before the store (x17 after the store faulted on an iPhone there), and the debt takes what the measured step adds. On an M4 Pro, on a host built from 8db5dd4: game time 1.000 of real time at 57.8 fps headless and at 58.8 fps in fullscreen with no whole vsync paid back, 0.999 at 27 fps; DM_FRAME_DELTA=0 keeps the whole-vsync step alone. Stepping by frame time was found by gunvalk (zortos293/Lichii PR seedds#216, GPL; only the fact was taken). Not checked: an iPhone, other games, the game itself on this main (cherry picked from commit e42cb7d)
The 0400486024DEC000 frame step takes the measured frame time where frames are not whole vsyncs long: below the target a GPU-bound Mac runs at 55-59.7 fps, and the host's whole-vsync step left the game slightly slow and paid the time back a vsync at a time (not at all where frames are over two vsyncs at hold 1: 0.908-0.934 of real time at 27 fps). The stub now takes the larger of the host's step × base / 60 and base × the frame time the host measures (state +0x38: the mean of the last 8 present intervals, a stall at most 0.1 s, at most 4 frames, 0 under 1.01 frames), and marks +0x3c so the host measures only for a main that reads it; nvn_fstep_measured pays back no whole vsync beside a measured time, counts what the game stepped in the debt and hands the time out only while the game is behind (both together ran the game at 1.003). The two hazards named in PR seedds#1543's review are handled: the stub's cbz offsets are recomputed for the added words (+19 / +17) and the page is taken again into x16 with the load before the store (x17 after the store faulted on an iPhone there), and the debt takes what the measured step adds. On an M4 Pro, on a host built from 8db5dd4: game time 1.000 of real time at 57.8 fps headless and at 58.8 fps in fullscreen with no whole vsync paid back, 0.999 at 27 fps; DM_FRAME_DELTA=0 keeps the whole-vsync step alone. Re-measured on ced3612: a scene that holds 60 fps no longer separates the stub from no stub; at 58-59 fps the stub pays back 24-30 vsyncs against 133-151 without, and throttled to 19 fps game time is 0.993 of real time against 0.627. Stepping by frame time was found by gunvalk (zortos293/Lichii PR seedds#216, GPL; only the fact was taken). Rebased on c081686: main's 1e3fd03 (the averaged step up, smax) merged without conflict in nvn_fstep.h and hle_nvn.c; the state page's +0x38 / +0x3c are still unused on main, and the profile's patches.py is unchanged since the old base, so the stub's offsets stand. Checked after the rebase: host build, t_fstep (98 checks, 0 failed), test_oot_real_speed and test_patches (27 passed, with unicorn), test_game_profile, test_docs, test_env_switches. Not checked: an iPhone, other games, the game itself on this main.
Ocarina of Time played in slow motion whenever it ran below 60 fps. Its frame loop (
main+0x6e6be0) computes each frame's step as a constant over its target rate and never reads the clock, so one frame is one step however long it took.gunvalk found this in Lichii PR #216. Lichii is GPL-3, so this was written here from those facts, not copied.
The change
main+0x6e6cc4keeps the game's own step and takes the measured frame time when that is longer, so the step never falls below the original.DM_FRAME_DELTA=0turns it off.If #1497 merges first, the two stubs share
FRAME_STEPfor this build andpatches.pyneeds a small merge: one function that calls both.Tested
main, by PucaVaz on an M4 MacBook Air (see the comment below): game time against wall time was 0.998 to 1.000, against 0.50 at 30 fps without it, with no GPU errors.t_fstep: 83 checks pass.test_oot_real_speed.pyruns the emitted stub under Unicorn. It skips without Unicorn, astest_oot_render_scale.pydoes.test_docs.pypasses.Not tested
scripts/regress.py checkwas not run: Ocarina of Time is the only game on this Mac.fcselwas checked only in the Unicorn test.🤖 Generated with Claude Code