Skip to content

Fix Ocarina of Time frame ratio calculation and enable frame buffering (0400486024DEC000) - #1996

Open
Zard-Studios wants to merge 1 commit into
seedds:mainfrom
Zard-Studios:fix/oot-frame-ratio
Open

Zard-Studios wants to merge 1 commit into
seedds:mainfrom
Zard-Studios:fix/oot-frame-ratio

Conversation

@Zard-Studios

Copy link
Copy Markdown

Summary

  • patches.py (): In cutscenes, Nintendo sets bit 0 of [x21+0x24f], which selects base rate 30.0f in s1 instead of 60.0f. The previous stub calculated (step * base_rate) / 60, resulting in (2 * 30.0) / 60 = 1.0 at 30 fps (or 0.5 at 60 fps). This caused cutscenes and dialog progression to run in half-speed slow motion while audio dialogue played in real time (e.g. 50s awkward silences between narrator phrases). Replacing this with fmov s8, s0 sets the ratio directly to (float)step and adjusts the CBZ forward jump offsets accordingly.
  • game.tsv: Add present_snap on (matching Breath of the Wild 01007EF00011E000) so that swapchain textures are copied immediately into host present snapshots, decoupling the game engine from display link vsync jitter and preventing micro-stutters under the macOS compositor.

Verification

  • Tested with The Legend of Zelda: Ocarina of Time (v131072, 0400486024DEC000) on Apple Silicon.
  • Cutscenes and dialogue now progress at full real-time speed.
  • Passed test_launcher_settings.py and test_patches.py.

…g (0400486024DEC000)

- patches.py: In cutscenes, Nintendo sets bit 0 of [x21+0x24f], which selects base rate 30.0f in s1 instead of 60.0f. The previous stub calculated (step * base_rate) / 60, resulting in (2 * 30.0) / 60 = 1.0 at 30 fps (or 0.5 at 60 fps). This caused cutscenes and dialog progression to run in half-speed slow motion while audio played in real time. Replace with fmov s8, s0 to set the ratio directly to (float)step and adjust the CBZ forward jump offsets accordingly.
- game.tsv: Add present_snap on (as Breath of the Wild) so that swapchain textures are copied immediately into host present snapshots, decoupling the game engine from display link vsync jitter and preventing micro-stutters under the macOS compositor.
@github-actions github-actions Bot added rewriter The NSO rewriter and per-build patches game: 0400486024DEC000 A game labels Nov 9, 2026
oliverames pushed a commit to oliverames/nxnative that referenced this pull request Nov 9, 2026
… takes half the step, and the frame time floor is not paid back twice

Movies: AV1 at 60 pictures a second with no sound track, and the game's player took one picture a calc at most (its limit, +0x20, from a creation argument of 1). Below 60 fps a
movie fell behind the sound the game plays beside it: on an iPhone Duo at 26-31 ms a frame the opening's 9,254 pictures (154.2 s) took 262 s, and at a 2-vsync hold a movie
plays at half speed (the half-speed cutscenes of PR seedds#1996). A function-matched cut makes the limit 2, the player's own value for a larger argument.

Frame ratio: base 30 is the framework's half rate, two calcs a drawn frame, so each calc takes half the frame's step: step x base / 60 stays (the step alone, PR seedds#1996, would
run that mode at twice its speed). The stub counts a half-rate frame's vsyncs once. test_oot_frame_step.py runs both bases and fails on the step alone.

Frame time floor: the stub's larger-of-step-and-frame-time was paid back again by the host's debt, 9 to 17% fast with the hold off at frames between one and two vsyncs
(t_fstep: 1.132 / 1.166 / 1.088 at 1.2 / 1.5 / 1.8 vsyncs, 1.000 with nvn_fstep_moved). The log's game time counts the floor's share.

Co-authored-by: Zard Studios <96824937+Zard-Studios@users.noreply.github.com>
Co-authored-by: dsi-hub <260802842+dsi-hub@users.noreply.github.com>
@oliverames

Copy link
Copy Markdown
Contributor

Thanks for raising the base-30 case, it sent me into the function. I don't think the ratio is what slowed your cutscenes, so I'd hold off on the fmov s8,s0 change.

The ratio's unit is 1/60 s whatever the base: the time object at main+0x3b4e300 stores ratio × 1/60 as its step in seconds. Base 30 is the framework's half rate. Bit 0 of its +0x24f is the mode and bit 1 flips on each pass of the frame function at main+0x346c680. With both set the draw and the present return at once while the calc tree still runs, so the game calcs twice per drawn frame and each calc needs half the frame's step. step × 30 / 60 gives that, and the step alone would run the mode at twice its speed. #2071 has a Unicorn test that tells the two apart, and it credits you.

What I think you saw is the part of the game that counts calcs instead of time. The movie player takes one picture per calc, and the subtitle and voice timeline advances once per calc too. At a 2-vsync hold the ratio is already 2, but those only move by 1, so cutscenes drag while the music keeps time. On my iPhone Duo the 154 s opening movie took 262 s.

The fix I have working is the game's own half rate. #2082 lets the host ask for it while it holds 2 vsyncs, so the game calcs 60 times a second at 30 fps. With that on, the same movie took about 160 s on the Duo. If present_snap on let your Mac hold 60 fps, that would also explain why your cutscenes came right, but I haven't measured it.

The fixed cbz offsets in this PR are fine on main, but they'd break as soon as a word is added to the stub, so #2071 computes them.

oliverames pushed a commit to oliverames/nxnative that referenced this pull request Nov 10, 2026
…e, the stub's branches are computed, and a test runs it at both base rates

Base 30 in the frame ratio is the framework's half rate, not a cutscene speed: bit 0 of its +0x24f is the mode and bit 1 flips each pass of its frame function, and with
both set the draw and the present return at once while the calc tree runs. The game then calcs twice a drawn frame, the ratio's function among them, so each calc takes half
the frame's step: step x base / 60 is right, and the step alone (PR seedds#1996) would run that mode at twice its speed. The ratio's unit is 1/60 s whatever the base (the time
object's step in seconds is ratio x 1/60).

The stub counted the frame's vsyncs at both calcs, so the host's game time would read twice the real time in that mode. It now counts them at the calc that is drawn. Its
branch targets are computed from the words emitted instead of fixed offsets.

test_oot_frame_step.py runs the emitted stub under Unicorn (optional, as test_astral_fps.py): both bases at steps 1 to 3, the count, x17 lost after a store, and the game's
own division at target 0 and before a step is decided. It fails on main's stub (the count) and on PR seedds#1996's (the ratio).

Co-authored-by: Zard Studios <96824937+Zard-Studios@users.noreply.github.com>
Co-authored-by: dsi-hub <260802842+dsi-hub@users.noreply.github.com>
oliverames pushed a commit to oliverames/nxnative that referenced this pull request Nov 10, 2026
… holds 2 vsyncs (DM_FPS_HALF_RATE=1)

Some of this game counts calcs, not time: its movie player takes one picture a calc and the timeline that fires its subtitles advances once a calc. The frame ratio
moves what reads the time object by the frame's real time, so below 60 fps the two fall apart (issue seedds#1922, the cutscenes of PR seedds#1996). The game's own answer to a
frame of two vsyncs is its half rate, two calcs a drawn frame. A stub at its request site (main+0x650ea4 on v131072, placed by the function's fingerprint) ORs the
host's ask (state +0x44) into the flag the game reads, and fstep_tick asks while the hold is 2 vsyncs or more. Off by default; refused with a red line when the hold
is off, where a half-rate frame of one vsync would calc 120 times a second.

On an iPhone Duo (the hold on, the movie network off, a new game): the half rate was asked for when the hold went to 2 at the opening movie, the log's game time read
1.002 of real time, and the movie's 154.2 s took about 160 s. test_oot_frame_step.py runs the request stub under Unicorn.
dsi-hub added a commit to dsi-hub/nxnative that referenced this pull request Nov 11, 2026
… takes half the step, and the frame time floor is not paid back twice

Movies: AV1 at 60 pictures a second with no sound track, and the game's player took one picture a calc at most (its limit, +0x20, from a creation argument of 1). Below 60 fps a
movie fell behind the sound the game plays beside it: on an iPhone Duo at 26-31 ms a frame the opening's 9,254 pictures (154.2 s) took 262 s, and at a 2-vsync hold a movie
plays at half speed (the half-speed cutscenes of PR seedds#1996). A function-matched cut makes the limit 2, the player's own value for a larger argument.

Frame ratio: base 30 is the framework's half rate, two calcs a drawn frame, so each calc takes half the frame's step: step x base / 60 stays (the step alone, PR seedds#1996, would
run that mode at twice its speed). The stub counts a half-rate frame's vsyncs once. test_oot_frame_step.py runs both bases and fails on the step alone.

Frame time floor: the stub's larger-of-step-and-frame-time was paid back again by the host's debt, 9 to 17% fast with the hold off at frames between one and two vsyncs
(t_fstep: 1.132 / 1.166 / 1.088 at 1.2 / 1.5 / 1.8 vsyncs, 1.000 with nvn_fstep_moved). The log's game time counts the floor's share.

Co-authored-by: Zard Studios <96824937+Zard-Studios@users.noreply.github.com>
Co-authored-by: dsi-hub <260802842+dsi-hub@users.noreply.github.com>
(cherry picked from commit fd62582)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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