Repository navigation
rewriter/frame-step: step by the measured frame time between vsyncs - #2259
roandegraaf wants to merge 1 commit into
Conversation
|
Re-measured on ced3612 with its own shader libraries (chain bc8830d4ba5a). The scene the body measured at 57.8 fps now holds 60.00 fps with or without the stub, so there the two are indistinguishable (1.000 of real time, no vsync paid back, both ways). At 58–59 fps (a walk through a town, added here) game time is 0.999–1.002 of real time with the stub and 0.999 without, so the game-time figure does not separate them on this base; what differs is the vsyncs paid back, 24–30 against 133–151. The body's throttled result holds (0.993 of real time with the stub, 0.627 without, at 19 fps). In a scene at 52 fps the stub reads slightly lower than without it (0.992–0.993 against 0.995–0.996). Machine and settings: Apple M4 Pro (20-core GPU, 24 GB), macOS 27.2, profile
The baseline host with the baseline's modules in the village scene (2 runs, interleaved with the above): 51.9–52.6 fps, 0.995–0.996 of real time, 169 and 180 vsyncs paid back, the same as the branch with Notes:
Tests on the branch at ced3612: Not checked: a settled scene at 55–59 fps (the town walk is the only route I have in that band, and it gives one frame step line a run), a real window, iOS, other games. |
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.
e42cb7d to
9e3fda8
Compare
… 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 s
…come out of the frame step's time beside seedds#1942's GPU-idle flag and seedds#2259's measured step
Rebased on main at c081686 (2026-11-12). Main's 1e3fd03 (the averaged step up and the stub's declared largest step,
smax) merged without a conflict innvn_fstep.handhle_nvn.c; the state page's +0x38 / +0x3c are still unused on main, and the profile'spatches.pyhas not changed since the old base, so the stub and its recomputed offsets stand as they were. Only the docs conflicted (the game note,frame-rate.md; the symptom index regenerated). This branch and #2367 (the fit rule innvn_fstep_next) touch neighbouring code innvn_fstep.handt_fstep.cbut merge together cleanly, in either order.What and why
0400486024DEC000now plays at normal speed when the Mac runs it a little under 60 fps (55–59 fps), instead of slightly slow with a small jump every few frames.This extends main's whole-vsync frame step to the range between whole vsyncs; it does not replace it.
frame_stepstub (placed by function fingerprint,MATCH_FRAME_STEP) gives it the host's step in whole vsyncs. Below the target, the frames of a GPU-bound Mac are not whole vsyncs long (55–59.7 fps), so the game ran slightly slow and the host paid the time back one whole vsync at a time. Where frames are longer than two vsyncs at hold 1 it was not paid back at all: 0.908–0.934 of real time at 27 fps.dm/games/0400486024DEC000/patches.py): after the host's step × base / 60, it loads the frame time the host measured (state page +0x38, float seconds, 0 = at the target), marks +0x3c so the host measures only for a main that reads the value, and takesfmaxof the two. The step is never shorter than the host's.dm/gfx/nvn/nvn_fstep.h,fstep_tickinhle_nvn.c):nvn_fdelta_next: the mean of the last 8 present intervals; a stall counts at most 0.1 s, the result stops at 4 frames, and under 1.01 frames (59.4 fps) it is 0, the game's own step.nvn_fstep_measured: no whole vsync is paid back beside a measured time, the debt counts what the game stepped, and the measured time is handed out only while the game is behind.DM_FRAME_DELTA=0turns the measured time off. The block runs only for a main whose stub marks the page, and not under the deterministic clock or a step the game fixes itself.Relation to #1543. That PR proposed the measured step as a separate stub and was closed on 2026-11-10 as superseded by main's whole-vsync step. Its review named two hazards of merging such a step into main's
frame_step. Both are handled here:cbzoffsets land inside the new words, and addressing the state page through x17 after a store faulted on an iPhone (x17 = 0 at x17 + 0x3c on the first frame). The two offsets are recomputed for the six added words (+13 → +19, +11 → +17, both to the original division), and the added words take the page again into x16 (ADRP) and load before they store.test_oot_real_speed.pyruns the emitted stub under Unicorn on every path: no target, no step decided, no measurement, a measured time above and below the host's step.nvn_fstep_measured. Before it, the measured time and the whole-vsync payback together ran the game at 1.003 of real time;t_fstepnow holds game time to real time over 6,000 frames for seven frame patterns.The idea of stepping by the frame time came from gunvalk's work in zortos293/Lichii PR #216 (GPL; only the fact was taken), as #1543 says.
Reference entry: Frames that are not whole vsyncs long under the frame step. Game note:
dm/games/0400486024DEC000/README.md, Frame rate.Checked
Tests, on this branch after the rebase (c081686 + this change):
make -C dm/host GAME=0400486024DEC000builds.make -C dm/gfx/tests GAME=0400486024DEC000 t_fstep && ./dm/gfx/tests/t_fstep: 98 checks, 0 failed.pytest dm/tools/tests/test_oot_real_speed.py dm/tools/tests/test_patches.py dm/tools/tests/test_nso_match.pywith Unicorn installed: 58 passed, none skipped.pytest dm/tools/tests/test_docs.py dm/tools/tests/test_env_switches.py dm/tools/tests/test_game_profile.py: pass.patch_nso.pyon the game's v131072main: rewrites, the frame ratio cut atmain+0x6e6cc4listed with its function match (not repeated; the rewriter's frame-step path and the profile are unchanged by the rebase).Games run, with title ID and version:
0400486024DEC000, v131072, chaincb2e91c138e3,DM_FPS_HOLD=off, one run a side, on a host built from 8db5dd4 with this change, not repeated in the game on ced3612:DM_PRESENT_LINK=0taskpolicy -bafter load)DM_FRAME_DELTA=0With the band at 1.1 frames (the first version, as in Ocarina of Time keeps real speed below 60 fps: its frame step follows the measured frame time #1543) a steady 55–59 fps measured nothing; 1.01 frames is 59.4 fps.
Re-measured on ced3612 with its own shader libraries (chain
bc8830d4ba5a), headless,DM_FPS_HOLD=off, runs interleaved (stub,DM_FRAME_DELTA=0, stub, …), the last 1,800 frames. Honestly, this weakens the table above:Devices and OS: Apple M4 Pro (20-core GPU), macOS 27.2.
scripts/regress.py check --games: not run. This is the only game built on this Mac, and the host's new block runs only for a main whose stub marks the state page (+0x3c), which only this profile's stub does.Not checked:
Docs
docs/reference/<subsystem>.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index)dmsh_chain.py --headerwas run anddm/gfx/nvn/dmsh_chain.his part of the change