Skip to content

fix(vi): the pick-up counter fades again above a 60 fps cap (#1832) - #2006

Merged
hckrman101 merged 3 commits into
NotRexed:mainfrom
jordanella:fix/pickup-indicator
Nov 11, 2026
Merged

hckrman101 merged 3 commits into
NotRexed:mainfrom
jordanella:fix/pickup-indicator

Conversation

@jordanella

@jordanella jordanella commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1832 (one of the above-60 side effects tracked in #1742).

Cause

Above a Frame rate cap of 60, #1612 makes the game's frame step the frame's own time, so at 144 fps each frame steps about 0.42 of a 60th.

The counter that pops up above Link's head after a pickup (rupees, Deku Sticks and the other stacking items) is updated by f_80a87e50:

  • It moves the count it shows (+864) toward the new total (+860) by (int)(step * max(distance / 50, 1)).
  • It fades only when the two meet.

At a step of 0.42 that cut is 0 every frame. The shown count never moves, so the counter stays up until a door or a talk replaces it. At 60 fps the step is 1 and the count moves normally.

Fix

  • The patch: nx2/oot_patches.py routes that one cut through hos_frame_whole (host/frame_whole.h, called from host/nn_vi.c).
  • Above a 60 cap: while the frames' own time is the step, the helper carries the fractions per counter, so the count moves at the game's rate whatever the frame rate.
  • At a cap of 60: the amount is cut exactly as the game cuts it, so nothing changes there.
  • Only this counter: I scanned every reader of the frame step for a cut to a whole number, following it through arithmetic. One more, f_832b32b0 ((unsigned)step added to a counter), has the same flaw. It never ran in any of the logged pickup sessions, though, and what it drives is unknown, so it's left out of this PR. A separate look at the other above-60 truncations (The game intro doesn't run well at more than 60 fps #1742) will add sites only once they're seen to misbehave in game. Two others (f_83423a90, f_8337b110) are correct with fractional steps.
  • Missing site: the build warns if the site isn't found, for example on another game version. The patch finds the cut itself, so it still matches when the function is built as hot code (A64_HOT_FUNCS).
  • Alongside fix(vi): a cutscene's cues stay with its movie above a 60 cap (#1742) #1875: this sits next to hos_movie_frames and follows the same carry-the-fraction approach.

How it was found

  • Reproduced without a 144 fps PC: a test-only switch forced the step to 1/144 s, and the counter stuck.
  • Located the value: I put the frame-time keeper's fields back to their 60 fps values one at a time. Only the step mattered.
  • Located the function: logging the step's readers as they first ran showed f_80a87e50 starting at the pickup.

Tests

  • tests/test_frame_whole.py covers two things:
    • The helper: 144 frames at 144 fps make 60 counted frames, a step times 3 keeps its fraction, a cap of 60 cuts as the game does, counters keep separate remainders, and NaN or negative amounts give 0.
    • The patch: each site is replaced once, re-applying changes nothing, and other code is left alone.
  • test_frame_rate_cap_step, test_text_speed and test_free_view_anchor still pass.
  • In game, with the step forced to 144 fps:
    • A rupee counts up and fades.
    • The first-ever Deku Stick (with the new-item scene) goes 0/10 → 1/10, the next one 1/10 → 2/10, and both fade.
    • At normal timing nothing changed.
  • Not tested in game: bombs, seeds, nuts and arrows. They use the same counter.
  • Saves already hit by the bug: a save from a session that had hit the stuck counter before the fix later stopped adding sticks, while a clean save works. The fix prevents the stuck count, but it can't repair a save that's already in that state.

🤖 Generated with Claude Code

jordanella and others added 3 commits November 10, 2026 16:26
…#1832)

Above a Frame rate cap of 60, NotRexed#1612 makes the game's frame step the frame's
own time: under 1 (0.42 at 144 fps). The counter that pops up above Link's
head after a pickup (rupees, Deku Sticks, seeds, bombs) moves the count it
shows toward the new total by (int)(step * max(distance / 50, 1)) and fades
when they meet (f_80a87e50). At 0.42 that cut is 0 every frame: the count
never moved, and the counter stayed until a door or a talk replaced it.

The counter's step now goes through hos_frame_whole (host/frame_whole.h),
which carries the fractions per counter while the frames' own time is the
step, so the count moves at the game's rate whatever the frame rate. Without
that step (a cap of 60) the amount is cut exactly as the game cuts it.
f_832b32b0, the one other reader of the frame step that cuts it straight to
whole frames ((unsigned)step into a counter), takes the same carry. The
build warns when either site is not found.

Found with the step forced to 1/144 s on a slower PC (the counter stuck),
the frame-time keeper's fields put back to their 60 fps values one at a time
(only the step mattered), and logging the step's readers as they first ran
(f_80a87e50 started at the pickup).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ame time read atomically

Review follow-ups for NotRexed#1832:
- f_80a87e50's site was found by the multiply before its cut as well. In a lift
  where the function is among A64_HOT_FUNCS that multiply is
  a64_fpx_hot_mul32_fma32, and the patch would have been left out (with a
  warning). The cut alone is the anchor now: it is the function's only one.
- hos_frame_whole reads hos_frame_seconds_bits with __atomic_load_n, as
  hos_movie_frames does (realtime_step writes it on the present thread).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…b0 left for a separate look)

f_832b32b0 cuts the step to whole frames the same way, but it never ran in
any of the pickup sessions logged (rupees and Deku Sticks, at 60 and at the
forced 144 fps step) and what it drives is not known. It is left for a
separate look at the other above-60 truncations (NotRexed#1742), with a site added
only once it is seen to misbehave in game.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@hckrman101
hckrman101 merged commit f915af9 into NotRexed:main Nov 11, 2026
11 of 12 checks passed
DevL0rd pushed a commit to DevL0rd/Hookshot that referenced this pull request Nov 12, 2026
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>
jordanella added a commit to jordanella/Hookshot that referenced this pull request Nov 12, 2026
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.

Rupee / seed / bomb counter stays on screen after a pickup until a door or NPC clears it (reported as since v2.0.14)

1 participant