Skip to content

Ocarina of Time: frame step in whole vsyncs (fps_target 60, 30 when 60 does not hold) - #1584

Closed
kylerodriguezmusic wants to merge 2 commits into
seedds:mainfrom
kylerodriguezmusic:oot-frame-step
Closed

kylerodriguezmusic wants to merge 2 commits into
seedds:mainfrom
kylerodriguezmusic:oot-frame-step

Conversation

@kylerodriguezmusic

Copy link
Copy Markdown

Ocarina of Time (0400486024DEC000, v131072): frame step in whole vsyncs

Symptom: on a Mac that does not hold 60 fps (M1 Ultra, about 39 fps in Kokiri Forest) the game plays in slow motion. It runs one game tick per frame and never changes its present interval.

Cause: the sead framework's frame-rate tick (vtable +0x128, main+0x4dbd60) returns at once because the framework's frame-rate mode (fw+0x24f) & 3 is 1, so the interval controller embedded at fw+0x270 (main+0x3b4ef60, the same design as Breath of the Wild's) never runs and the interval stays 1 whatever the frame time.

The logic does follow the interval: with cuts forcing the controller to interval 2 / 30 fps, Link's run from the bed to the door took 23 presented frames against 59 at interval 1 (DM_NVN_REGLUM per-frame luminance; screenshots stall the present thread and let the game race, so they cannot time it). Two ticks a frame, as BotW's VFR does.

Fix: FRAME_STEP stubs in the profile's patches.py, fps_target 60 in game.tsv:

  • main+0x4dbd6c: with a target fps the mode check is skipped and the interval controller runs (never the VRR frame-rate controller);
  • main+0x3b4ef90 / +0x3b4ef9c: the controller's paused flag and "no frame time yet" exits are skipped with a target, so it reaches its output every frame;
  • main+0x3b4f054 (the controller's output, w21 = its interval): the host's step in whole vsyncs (state +0x20) once decided, vsyncs stepped counted at +0x30. setFrameRate (main+0x4dc0f0 → nvnWindowSetPresentInterval) and setWaitVsyncInterval (main+0x346c5c4, fps at fw+0xfc) take it from there.
  • Target fps 0: every stub runs the original instructions, the game keeps its fixed interval 1 as shipped. x16 / x17 are free at all sites; the controller's sites keep the flags.

Measured (M1 Ultra, Encode Thread on, Handheld, MetalFX, DM_FPS_TARGET=30, scripted Kokiri route): frame step: game time 60.00 s in 60.00 s (1.000 of real time); held 1 / 2 / 3 vsyncs 0 / 1800 / 0, frame p50 33.4 / p95 36.2 ms. Movies play, no first-start freeze, the Kokiri route works.

Note on automatic mode (60 with 30 when it does not hold) on that Mac: told 30 or 20 fps the game raises its render resolution, GPU time grows to about 33 ms at a 3-vsync hold and the hold sticks at 20 fps (probes fail). The 30 fps cap keeps the hold at 2 and is steady. A later change could keep the game's resolution budget at the cap while the hold varies; not attempted here.

Profile README has the same note under "Frame rate".

🤖 Generated with Claude Code

https://claude.ai/code/session_01MvzZTbmYKSqzD7rWeQmzei

Kyle Rodriguez and others added 2 commits November 6, 2026 14:35
…0 does not hold)

The game never changes its present interval (its frame-rate tick returns in mode 1), so below 60 fps it played in slow motion. Stubs open the framework's interval controller and feed it the host's step; the game's logic follows the interval (23 frames for the bed-to-door run at interval 2 against 59 at 1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MvzZTbmYKSqzD7rWeQmzei
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MvzZTbmYKSqzD7rWeQmzei
@github-actions github-actions Bot added rewriter The NSO rewriter and per-build patches docs Documentation game: 0400486024DEC000 A game labels Nov 6, 2026
@PucaVaz

PucaVaz commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

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).

  • Same shader chain, 5c852d7989ac.
  • The shader libraries (all 33,925 metallibs) and the pipeline archive are byte-identical between the two apps, so only the frame step differs.
  • Handheld, DM_UPSCALER=game, windowed (2560x1440).

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):

runs house fps walk fps walk frame p50 / p95 / p99 walk frames > 60 ms walk on-screen 1 / 2 / 3 vsyncs walk on-screen sd present → display p50
#1543 uncapped (its default, fps_target 0) 2 51.6–52.1 44.4–45.0 20.9 / 30.1–30.2 / 31.8–31.9 ms 3–5 68% / 29% / 2% 9.3–10.7 ms 58 ms
#1584 at its default (fps_target 60) 3 31.8–32.2 27.3–29.5 33.2–33.5 / 50.6–51.5 / 52.5–53.3 ms 5–8 13% / 61% / 26% 9.6–11.6 ms 76–79 ms
#1543, DM_FPS_TARGET=30 2 30.0 29.9–30.0 33.2 / 34.5–34.6 / 35.6–36.1 ms 3–5 0 / 100% / 0 2.8–3.3 ms 110 ms
#1584, DM_FPS_TARGET=30 1 30.0 29.9 33.2 / 34.5 / 35.2 ms 4 0 / 100% / 0 3.5 ms 110 ms

"Present → display" is the host's game enqueue to target display p50. A 3-vsync hold is 50 ms, so the table counts frames over 60 ms.

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).

  • The host probes hold 1 six to eight times a minute, and every probe after the title failed (41–43 per run).
  • With kmin 1 the hold can also go to 3 (kmax = FSTEP_MAX), and it did in the heavier views: 17–34% of walk frames, 73–89% of frames at the door and in Saria's greeting (20–24 fps), and the balcony at 20 fps, against 32 fps for Ocarina of Time keeps real speed below 60 fps: its frame step follows the measured frame time #1543 uncapped.
  • A one-frame pass dump in the house and on the balcony shows the scene at 854x480 with either PR, so the resolution rise described for the M1 Ultra did not happen here.

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 fps_target 60 gives the launcher a Frame rate row (30 / 60). With #1543 the profile's fps_target stays 0, and there is no such row.

No GPU errors, and the same [RED] lines in all runs (the partial-library notes and one texture type mismatch). Footprint peak 10.1–10.8 GB.

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).

john62036203-ops added a commit to john62036203-ops/NXNativeWindows that referenced this pull request Nov 7, 2026
…syncs, 30 when 60 does not hold) into switch2; one FRAME_STEP entry runs it and the fork's render-scale ceiling

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013SzykpcKm2eZPvPjGPKx1C
@kylerodriguezmusic

Copy link
Copy Markdown
Author

Closing: superseded by 7d8dcdf on main, which places the host's frame step at the game's own frame-ratio division (fingerprint-matched, build-independent) instead of opening the framework's interval controller as this PR did. Thanks to PucaVaz for the M4 comparison against #1543; the note here about automatic mode on the M1 Ultra (the game raising its render resolution when told 20 or 30 fps, so a 3-vsync hold sticks) may still be useful, a 30 fps cap avoids it.

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 rewriter The NSO rewriter and per-build patches

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants