Skip to content

Ocarina of Time: keep the frame ratio at step × base / 60, count a half-rate frame once, and test the stub - #2071

Open
oliverames wants to merge 2 commits into
seedds:mainfrom
oliverames:oot-frame-step-half-rate
Open

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

Conversation

@oliverames

@oliverames oliverames commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What and why

#1996 changes the frame-step stub's ratio from step × base / 60 to the step alone, reading the base rate of 30 as a cutscene speed. I went through the game code before porting that to my iPhone Duo build, and main's formula is the right one. This PR keeps it, fixes a smaller counting error I found on the way, and adds a test that tells the two formulas apart.

Here's what base 30 is on v131072 main:

  • The ratio's unit is 1/60 s whatever the base. The time object's setter (main+0x3b4e300) stores the step in seconds as ratio × 0.0166667.
  • Bit 0 of the framework's +0x24f is a half-rate mode, and bit 1 flips on each pass of its frame function (main+0x346c680). With both set, the draw (main+0x2cb020) and the present (main+0x40cd20) return at once, but the calc tree still runs. The ratio's function is in that tree, so it runs twice for each drawn frame.
  • Each calc should take half the frame's step, which is step × 30 / 60. The step alone would run that mode at twice its speed.

The counting error is in the same mode. The stub added the frame's vsyncs to the host's count at both calcs, so the log's game time would read twice the real time there. It now counts at the calc that's drawn. I also compute the stub's branch offsets from the words emitted instead of fixed numbers, so the next edit to the stub can't leave a stale branch behind.

I don't think the half-speed cutscenes behind #1996 come from the ratio. My phone never entered the half-rate mode (see below), and the slow part was the movies, because the game's player takes one picture a calc. That's #1922. #2072 and its follow-up #2082 are both closed (#2082 crashed the game in the half rate), and #2456 by PucaVaz now lets the movie player take two pictures a frame.

The game note has the finding under "Current: the base rate of 30 is the framework's half rate".

Checked

  • Tests: dm/tools/tests/test_oot_frame_step.py is new. It runs the emitted stub under Unicorn and skips without it, like test_astral_fps.py. It covers both bases at steps 1 to 3, the vsync count, x17 lost after a store, and the game's own division at target 0 and before a step is decided. All 5 pass here. The same test fails on main's stub (the count) and on Fix Ocarina of Time frame ratio calculation and enable frame buffering (0400486024DEC000) #1996's formula (the ratio). test_patches.py, test_nso_match.py, test_docs.py and lint_python.py pass, and patch_nso.py places the stub on the real v131072 main.
  • Games run, with title ID and version: Ocarina of Time 0400486024DEC000 v131072, one 18-minute run through the opening, Link's house, Kokiri Forest and the Deku Tree's speech. That build carried HookLink's version of this stub, not this exact one. In every 1,800-present window the stub ran once a present, and its vsyncs plus the dropped ones add up to the real time within 0.03 s, so the game never entered the half-rate mode.
  • Devices and OS: iPhone Duo (iPhone19,4), iOS 27.3 beta, docked.
  • Later the same day, on a build with this count and the host asking the game for its half rate (Ocarina of Time: the host can ask the game for its half rate, so subtitles and movies keep step below 60 fps #2082): with 1,464 of 1,800 frames in the half rate, the log's game time read 1.002 of real time, so a half-rate frame's vsyncs are counted once on the device too.
  • Not checked: the game entering the half rate on its own, which I haven't seen in play. This exact branch hasn't run on a device; the device build has HookLink's frame time floor in the stub as well.

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

…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>
Conflicts: dm/games/0400486024DEC000/README.md (kept both bullets).
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 tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant