Skip to content

fix(vi): the time of day passes again above a 60 fps cap - #2093

Merged
hckrman101 merged 1 commit into
NotRexed:mainfrom
jordanella:fix/time-of-day-clock
Nov 12, 2026
Merged

hckrman101 merged 1 commit into
NotRexed:mainfrom
jordanella:fix/time-of-day-clock

Conversation

@jordanella

@jordanella jordanella commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Above a Frame rate cap of 60, the time of day runs slow and above 120 fps it stops: the sun swings back and forth over a frozen hour. This is another of the above-60 side effects tracked in #1742.

Builds on #2006 (merged): it adds a site to that PR's hos_frame_whole table, next to #2409's water ripples. Rebased onto main, so it is one commit.

Cause

Since #1612, above a 60 cap the game's frame step is the frame's own time (0.42 at 144 fps instead of 1).

The time of day is kept by f_803edf70:

  • Each frame it adds (int)(2 * step) game seconds (+60) to the seconds of the day (+40). At 86400 those wrap and carry into the day count (+44).
  • At 60 fps that's 2 seconds a frame, so a game day lasts about 12 real minutes.
  • Between 60 and 120 fps the cut is 1, so the day runs slow: 51% at 61 fps, 83% at 100.
  • Above 120 fps it is 0, so the clock stands still. At a cap of exactly 120 the step is about 0.5, so any frame measured a little short also adds 0.

Fix

  • The patch: f_803edf70 joins WHOLE_SITES in nx2/oot_patches.py. Its cut goes through hos_frame_whole, keyed by the clock's +60. Above a 60 cap that carries the fraction between frames, so the clock gains its 120 game seconds per real second at any frame rate.
  • At a cap of 60: the helper cuts exactly as the game does, so nothing changes there.
  • The anchor: the function makes the same cut into x8 a second time, in the branch that sets the time from another source. So the anchor is the cut plus the store to +60 after it, and the other cut is left alone (a test covers this).
  • Missing site: the build's existing warning counts this site too.

How it was found

  • Found by a cut watch: a test-only build counted, at each of the lift's 3595 float-to-integer cuts, how often it dropped a value between -1 and 1. One normal play session and one with the step forced to 1/144 s, over the same stretch, were compared. fix(vi): the pick-up counter fades again above a 60 fps cap (#1832) #2006's pickup counter showed up only in the forced run, which checked the method. The clock's cut went from 3 to 303 per 600 frames.
  • Confirmed by a log: with the step forced to 144 fps, a test-only log of the clock showed it stuck at 46627 seconds (about 12:57) with 0 added each frame, while the input was 0.4167.
  • The fix tested: with the fraction carried, the clock gained 400 seconds every 480 frames. That's 0.83 a frame, exactly 2 × 0.4167.

Tests

  • tests/test_frame_whole.py:
    • The rate: 144 frames of 2 × 60/144 make 120 seconds, a real second's worth at 60.
    • The other cut: the second cut into x8, in the branch that sets the time from another source, is left as the game's.
    • Shared checks: each site is patched once, re-applying changes nothing, and other code is left alone. These now run for this site too.
  • python -I -B tests/run_all.py after the rebase onto 8568959: 404 passed, 13 failed. All 13 fail the same way on a clean checkout of that main on this PC (which fails two more besides), so none come from this PR.
  • In game (Lake Hylia, by day, step forced to 144 fps):
    • Before (main): the clock stood still and the sun swung east → west → east.
    • With this branch's build: the sun moved steadily east to west.
    • At a cap of 60 the helper cuts as the game does (covered by the test). That wasn't re-checked in game with this build.

🤖 Generated with Claude Code

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't on main yet (f_803edf70 isn't in WHOLE_SITES), and it doesn't overlap #2470. That draft handles the sound job, the physics solver steps and the quick menu, and doesn't touch the clock or WHOLE_SITES, so the two only meet as nearby text in nx2/oot_patches.py and host/nn_vi.c.

With #2006 merged, the conflict is small but has to be resolved by hand. #2409 (water ripples) added f_800672e0 to WHOLE_SITES and its comment list, and raised FRAME_WHOLE_SLOTS in host/frame_whole.h from 16 to 64 (each water surface takes a slot). Keep both sites and main's 64 slots and slot comment: going back to 16 would bring the ripple freeze back in busy scenes. tests/test_frame_whole.py is an add/add conflict, so merge your rate and minutes-cut checks into main's version rather than replacing it. apply() in oot_patches.py also picked up heap_sites from #2451, so that hunk should keep main's line. After the rebase, hckrman101's approval should still hold, as long as the warning count in nn_vi.c includes both new sites.

DevL0rd added a commit to DevL0rd/Hookshot that referenced this pull request Nov 12, 2026
@jordanella
jordanella force-pushed the fix/time-of-day-clock branch from 7bc9019 to 087ba5d Compare November 12, 2026 03:25
@jordanella

Copy link
Copy Markdown
Contributor Author

Rebased onto main (8568959): the PR is now its one commit (087ba5d).

  • Conflicts, resolved as @Joey-Magnus described:
    • fix(fps): water ripples keep moving above 60 fps #2409's f_800672e0 site is kept next to the clock's in WHOLE_SITES, with both comment entries;
    • FRAME_WHOLE_SLOTS stays at main's 64 (this PR doesn't touch host/frame_whole.h);
    • tests/test_frame_whole.py merged with main's version, so the ripple checks and the clock's rate and other-cut checks are all there;
    • apply() is main's;
    • the missing-site warning names all three counters.
  • Wording (@hckrman101's nit): you're right. The second cut into x8 is in the block at L_803ee05c, taken on bit 25 of the flags, and the minutes are read later from +60. The patch's comment, the test and the PR text now say "in the branch that sets the time from another source".
  • Tests: test_frame_whole.py passes. In the full run, all 13 failures fail the same way on a clean checkout of main here, which fails two more besides.

@hckrman101
hckrman101 merged commit f0c40fc into NotRexed:main Nov 12, 2026
6 of 11 checks passed
f_803edf70 adds (int)(2 * step) game seconds to the time of day each frame. Above a 60 cap the step is the frame's own
time: below 120 fps the cut is 1 (the day slow, 83% at 100) and above it 0, so the clock stood still and the sun swung
back and forth over the frozen hour. The cut takes hos_frame_whole's whole seconds like the pick-up counter (NotRexed#2006).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants