Skip to content

Ocarina of Time: fps_hold off, a late frame is shown late instead of held a second vsync - #1927

Open
FeReuss wants to merge 1 commit into
seedds:mainfrom
FeReuss:pr/oot-fps-hold-off
Open

FeReuss wants to merge 1 commit into
seedds:mainfrom
FeReuss:pr/oot-fps-hold-off

Conversation

@FeReuss

@FeReuss FeReuss commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What and why

Ocarina of Time (0400486024DEC000, v131072) is profiled with fps_hold off. With fps_target 60 the pacer raises the hold above the cap's vsyncs after three late frames in a row, so a game that can almost keep 60 fps is held at 30. With the hold off a late frame is shown late; the game still steps by the vsyncs that passed, so its speed stays right. The launcher's Hold frames when late turns it back on. Two other profiles already have the key off.

Profile key only; no host code, no chain change.

Checked

  • Tests: python3 -m unittest test_docs test_game_profile test_launcher_settings test_fps_target (dm/tools/tests, 41 tests OK).
  • Games run: Ocarina of Time 0400486024DEC000 v131072, the Hyrule Field view at 1920x1080 docked, 120 s, window, 60 fps cap, the default hold against DM_FPS_HOLD=off, the same host and shaders: 30.1 -> 33.7 fps (frame p50 33.31 -> 29.38 ms, GPU p50 27.56 -> 27.28 ms): the GPU time does not move, the hold was quantizing the frames. On the Kokiri bridge, about 52 fps against a steady 30. Measured on a tree of 2026-11-08 (main 7f502220 plus The graphics scale is tier 0 in both modes: Star Fox's docked 3D was 768x432, smaller than handheld #1304); this branch is rebased on 7a7acfee and not re-measured in the game.
  • Devices and OS: MacBook Air M4 (10-core GPU, 16 GB, no fan), macOS 27.2.
  • scripts/regress.py check: not run; a profile key of one game, and only Ocarina of Time is built on this Mac.

Front ends

None.

Docs

  • The finding is in the game note (Frame rate); fps_hold is documented in dm/games/README.md.
  • A chain tool changed: no.
  • No game files, keys or data extracted from a game in the change or the attachments

Series: what these PRs add up to

This PR is one of a set from one effort to make Ocarina of Time (0400486024DEC000 v131072) hold its frame rate on a MacBook Air M4. The set, and how its parts combine into the last measured result:

The result (the Hyrule Field view, the heaviest of the game: 1920x1080 docked, 60 fps cap, window, 120 s runs, M4 with 10 GPU cores, 16 GB, macOS 27.2):

Tree fps frame p50 GPU p50
main 7f502220 + #1304, fps_hold at its default 30.1 33.31 ms 27.56 ms
+ fps_hold off (#1927) 33.7 29.38 ms 27.28 ms
build 3: + #1833 with the M4 window fix, border-FC (#1929), the barrier set and cut_draws 128 below, and the PRs listed as "also in the tree" 40.2 24.48 ms 22.09 ms
+ enc_thread on (#1928) 41.6 23.46 ms 21.39 ms
+ native float add (#1930) = build 4 44.6 22.39 ms 20.45 ms

(The step from 33.7 to 40.2 changes the shader chain, so it has no in-process A/B; #1833 is most of it. Kokiri Forest, walking, is at 60 fps on build 4; the Hyrule view is the worst case.)

Each part switched in one process on build 4 (DM_AB, 210 s, 600-frame phases, frame p50 with the part active against without; the noise between phases is about 0.3 ms): border-FC (#1929) 22.61 vs 23.98 ms; the barrier set (FRAG_ORDER, PASS_OVERLAP and HEAP_RELAX alternating together) 22.41 vs 22.58 ms; CUT_IDLE 22.74 vs 22.85 ms; DISPATCH_CHAIN 22.32 vs 22.48 ms.

Which PRs make that tree:

Part Where
fps_hold off #1927
enc_thread on #1928
border-FC #1929
native float add #1930
launcher's Render scale / GPU share (no frame-time effect) #1931 (draft, on #1304)
M4 address window of shader_raw_direct (without it every raw store is dropped on an M4 and the foliage is not drawn: the 48-50 fps of a build without it are false) the branch FeReuss:pr1833-m4-window, one commit on #1833's head (not yet offered on #1833 itself)
Base, other authors, open, in the measured tree #1304 (tier 0), #1714 (gpu_ts_hz), #1833 (shader_raw_direct), #1720, #1377, #1840
Base, in the measured tree, already in main or withdrawn #1844 (-fwrapv, now e4e5ff64), #1368 / #1708 (native mesh prewarm and mesh without task: main's own versions), #1707 (withdrawn)
Measured, not submitted (the net is within the noise): barrier set (FRAG_ORDER + PASS_OVERLAP + HEAP_RELAX), CUT_IDLE branches pr/pass-overlap and pr/cut-idle in FeReuss/nxnative-oot
Not ported: dispatch chain (the same idea as #1901), cut_draws 128 (64 and 128 tie on this tree)

All of it was measured on main 7f502220 plus the above; these PRs are rebased on main 8d5d4f1b; the tool and docs tests that touch them were run again there (test_env_switches, test_docs, test_nsp_chain, and each PR's own), while the host build and the graphics suite are those of 7a7acfee, and none of it has been run in the game on the newer main (a shader rebuild of the new chain takes about an hour and 10 GB of disk). This PR is #1927.

🤖 Generated with Claude Code

…held a second vsync

With fps_target 60 the pacer raises the hold above the cap's vsyncs after three late frames in a row, so a game that can almost keep 60 fps is held at 30. On a MacBook Air M4 (10-core GPU, 16 GB) at 1920x1080 docked, in the Hyrule Field view, DM_FPS_HOLD at its default against off: 30.1 -> 33.7 fps (frame p50 33.31 -> 29.38 ms); on the Kokiri bridge, about 52 fps against a steady 30. fps_hold is already off for two other profiles; the launcher's Hold frames when late turns it back on.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@FeReuss
FeReuss force-pushed the pr/oot-fps-hold-off branch from 813f4c2 to a4d50d8 Compare November 9, 2026 16:44
@JDSavvy

JDSavvy commented Nov 10, 2026

Copy link
Copy Markdown

Independent data point for this PR on an M4 Pro (20-core GPU, 24 GB, macOS 27.0.1, AC power), main 04811a6, chain 39eb2f781d07, docked, Kokiri Forest route from a fixed save with scripted inputs, 190 s per run, frame stats from 60–120 s.

With the default hold (fps_hold on), the frame step dropped to 2 vsyncs after three late frames during the walk, probed back to 1 and failed, and then stayed at 30 fps for the rest of the run, in 2 of 2 runs of one series (one of them with the window never covered):

run hold fps frame p50 / p99 ms GPU p50 ms
1 off 55.0 17.07 / 29.39 15.27
2 on 29.5 33.33 / 51.16 15.20
9 on 30.7 33.33 / 34.34 15.33
10 off 55.5 17.68 / 26.22 15.30

The log of run 9 shows the pattern: hold 1 → 2 vsyncs ... (3 late frames), the run at hold 1 after the probe lasted under 8 s, counted as a failed probe, the probe failed, then hold 2 to the end. GPU time is the same in both modes, so the hold is triggered by CPU-side late frames (the probes report 91–99 % on performance cores).

In an earlier series with the default hold, the two main runs did recover to 60 fps, but spent 62 and 213 frames at hold 2 during the walk, and one of them left the scripted route (the hold changes the game's time step, so frame-indexed inputs cover a different distance).

With off no run had a 30 fps phase. +1 for making off the profile default for this game.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400486024DEC000 A game

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants