Skip to content

Ocarina of Time: the host can ask the game for its half rate, so subtitles and movies keep step below 60 fps - #2082

Closed
oliverames wants to merge 2 commits into
seedds:mainfrom
oliverames:oot-half-rate-ask
Closed

oliverames wants to merge 2 commits into
seedds:mainfrom
oliverames:oot-half-rate-ask

Conversation

@oliverames

@oliverames oliverames commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

As of Nov 10 this crashes the game. Please don't merge it as it is.

With the half rate in effect when the opening movie ends, the game dies a few frames later, every time I've looked:

  • 6 of 6 runs on the iPhone Duo crashed the same way: CRASH sig=11 addr=0 pc=main+0x332260, on the game's main thread, three log lines after the decoder's end of stream after 9254 pictures (the first draws of the next scene). The backtrace is the same in all six: main+0x54fbc main+0x54744 main+0x61070.
  • 5 of 5 runs where the half rate wasn't in effect at that moment didn't crash: three with the hold off, two with the hold at 1 when the movie ended.
  • The six are on two builds (c3460a1d, and 8a56cfdd with main's later host fixes), at a 60 and a 30 fps target, with the allocator cap at 32 and at 8, and with present snapshots on.
  • Both runs I quote below under "Games run" are among the six. My script stopped at the movie's end and never looked for a crash line, so I reported them as passing. That was my mistake.

Where it dies: the function at main+0x3321d0 stores a word through the pointer at its object's +0x1c818 (ldr x9, [x19] then str w8, [x9], with x19 at the object +0x1c818), and that pointer is null. I don't know yet why it's null in the half rate: my guess is that the first drawn frame of the next scene comes before the calc that sets it up, but I haven't shown that. I'm looking at it, and I'll either fix it here or close this.


Stacked on #2071: the first commit here is that PR's, and the half rate needs its fix (a half-rate frame's vsyncs counted once). I'll rebase when #2071 lands. This replaces my draft #2072.

What and why

For #1922. Below 60 fps this game's cutscenes come apart since the frame step gave it real time: the subtitles and the movie picture fall behind the music, and at a 30 fps target a movie plays at half speed.

The cause is that part of the game counts calcs, not time. Its movie player takes one picture a calc, and the timeline that fires its subtitles (PlaySubtitleWithVoice trigger events) advances once a calc too. The frame ratio moves everything that reads the time object by the frame's real time and leaves those two behind.

My first try, #2072, let the movie player take two pictures a calc. On my iPhone Duo that put the picture on real time and left the subtitles where they were, which looked worse than before. "However, there was one particular boy..." came up about 98 s into the opening at about 31 fps, over the nightmare's castle gate. At 53 fps the narration was over by 61 s, over Link asleep, where it belongs.

The game has its own answer for a frame of two vsyncs, and that's what this PR uses: its half rate, two calcs a drawn frame, so everything that counts calcs sees 60 a second at 30 fps.

  • The game asks for the mode once a calc from bit 0 of [ModuleSystem+0xa30], writing 2 (enter) or 1 (leave) to the framework's +0x24e (main+0x650ea4 on v131072).
  • A stub placed by that function's fingerprint ORs in the host's ask (state +0x44). Without an ask the game's own flag still decides.
  • fstep_tick asks while the hold is 2 vsyncs or more, under DM_FPS_HALF_RATE=1. It's off by default.
  • It needs the hold. With DM_FPS_HOLD=off a half-rate frame of one vsync would calc 120 times a second, so the switch is refused there with a red line.

The reference entry is "A game that counts calcs falls out of step with its own time below 60 fps" in docs/reference/frame-rate.md, and the game note has the Duo numbers.

Checked

  • Tests: test_oot_frame_step.py gains three Unicorn tests of the request stub (the game's flag alone, the host's ask, and no stub when the function isn't found). All 8 in the file pass, and they skip without Unicorn. test_patches.py, test_nso_match.py, test_docs.py, test_env_switches.py and lint_python.py pass. patch_nso.py places the stub at main+0x650ea4 on the real v131072 main.
  • Games run, with title ID and version: Ocarina of Time 0400486024DEC000 v131072, the opening from a new game on an empty save folder, on a build that carries this stub with HookLink's engine.
    • Hold on, movie network off: the half rate was asked for when the hold went to 2 at the opening movie, 1,464 of the next 1,800 frames were held 2 vsyncs, the log's game time read 1.002 of real time, and the movie's 154.2 s took about 160 s.
    • Hold on, movie network on, phone at thermal state 2 from the start: the hold moved between 2 and 3 and the movie took about 190 s. Its 9,254 pictures came over about 4,500 drawn frames, so two a drawn frame with the player's limit at 1.
    • For comparison, with the hold off and no half rate the same movie took 262 s on this phone.
  • Devices and OS: iPhone Duo (iPhone19,4), iOS 27.3 beta. The runs were scripted with DM_CTL (Reference: a scripted run on a physical iPhone (DM_CTL pushed with devicectl) #2076), so nobody was pressing buttons.
  • Not checked: the subtitles against the picture in the half rate by eye. The runs' screenshots caught no line on screen, so what I have is the mechanism and the picture count, not a direct look. Also not checked: play outside the opening with the switch on, this exact branch on a device (the device build has HookLink's frame time floor in the stub as well), and a Mac.

Docs

  • The finding is in its game note or its docs/reference/<subsystem>.md entry, and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • A chain tool changed: dmsh_chain.py --header was run and dm/gfx/nvn/dmsh_chain.h is part of the change (no chain tool changed)
  • No game files, keys or data extracted from a game in the change or the attachments

Oliver Ames and others added 2 commits November 10, 2026 09:00
…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>
… 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.
@oliverames

Copy link
Copy Markdown
Contributor Author

Closing this. It still crashes the game after the opening movie whenever the half rate is in effect (6 of 6 runs on the Duo), and #2456 by @PucaVaz now handles the movie side of #1922 another way, by letting the movie player take up to two pictures a frame. The subtitle timing this PR was after isn't covered by #2456, so if that turns out to matter I'll come back to it with a fresh approach. #2071 stays open on its own. Thanks!

@oliverames oliverames closed this Nov 12, 2026
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 nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) rewriter The NSO rewriter and per-build patches tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant