fix(fps): keep sound and moving platforms working above a 60 fps cap #2470
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2470
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/high-fps-bugs"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Above a 60 fps cap, sounds and then the music can stop for good after a few loading zones, and moving platforms (the stone one under the spikes in the Deku Tree, #1798) slide out from under Link. Both come from the game getting a frame step shorter than one 60 fps frame.
What changed
Sound (the sound manager at 60 Hz). The game's sound manager job (f_80435300) hands the frame step to the sound engine every frame. Its fade and delay timers and parameter ramps expect at least one whole 60 fps step. When they get less (0.5 at 120 fps), some sounds are not released when an area unloads, and the voices in use climb until the game's voice budget is full. New sounds (sword, voices), then the area music, stop until a restart. Above 60 fps the job now runs at 60 Hz: each frame adds its step to a remainder, and the job runs once a whole step has built up, with a step of 1. Its other phases follow that frame's choice. Sound still follows real time on average. A step of 1 or more, or of 0 or less, runs as before.
This is a port of zortos293/Lichii PR #1289 by auz466 (GPL-3.0), which found the cause and does the same thing as a code patch for the emulator. auz466 is credited with a
Co-authored-bytrailer. Here it is a lifted-code hook (nx2/oot_patches.py,patch_sound_job) plus host code (host/oot_high_fps.h,host/oot_high_fps.c).Moving platforms (physics solver steps). Each update the game sets the physics solver's step count to
(int)(frame step * steps of a 60 fps frame + 0.001)(f_838ceb80 and f_838cfc10). The places logged use 8 steps per 60 fps frame. With a step under 1 the count is cut short, and because the measured step wanders around its mean, the count changes from frame to frame: 8 x 0.49 gives 3 and 8 x 0.5 gives 4 at 120 fps. It reaches 0 at about 480 fps (with 4 steps per frame, above 240). While the step is under 1 the count is now rounded up instead, with 10% leeway for frame-time jitter. It is at least 1 and never fewer than the game's own, so a solver step never covers more time than at 60 fps. Frames slower than 60 fps keep the game's count. For #1798 (closed into #1742).At the default cap of 60 the host returns exactly what the game computed in both places, so nothing changes there. Uncapped stays uncapped.
Left out: the quick-menu crash at very high frame rates. It was reported at 640-700 fps in this game build: a null read after about two seconds in the quick menu. The faulting function is in the physics engine's collision code, near a table of Havok routines. That fits the solver count above, which would be 0 at those rates without this PR and is at least 1 with it, but I could not confirm it here. This PC reached about 300 fps at most (about 210 in the pause menu), so the crash never reproduced on unchanged
main. Forcing the solver count to 0 in a local build for a minute of play and menus did not crash either. So this PR makes no claim about that crash and does not cap Uncapped.Tests
tests/test_high_fps_fixes.py(new, game-free) covers:python -I -B tests/run_all.py: 358 passed, 4 failed, 13 skipped on Linux. The 4 failures (test_wayland_libdecor_hint,test_nup_read_name,test_bake_portability,test_builder_log_read_only) fail the same way on unchangedmainon this PC, because local tools such aszstandardare missing. After merging the latestmain, every check that readsnx2/oot_patches.pyornx2/chunks.pypasses (18 of them).Verified
On an RTX 3090 (Linux), in headless game runs from a real save, with
HOS_FPS_LOG=1:oot_patches: the sound job at 60 Hz above 60 fps,the solver steps rounded up above 60 fps. After the merge with the latestmain, which moved the lift edits intopatch_file/report, a dry run ofpatch_fileon the lifted code still finds the sound job once and both solver places.sound job ran 311 of the last 600 frames (step 0.498, held to 60 Hz)), and runs every frame when frames are slower than 60 fps.physics solver steps 7 (the game's 6: frame step 0.778 x 8 steps a 60 fps frame). That log comes from a build that also rounded up slow frames; the PR now leaves those at the game's count.Not tested
mainhere. This PC was shared with other jobs and the game ran at 40-110 fps in those areas, so the step was under 1 only part of the time. Twelve loads at a cap of 120 kept sound working on both builds. The gate's own behaviour is checked by the log and the unit test.main; only the lift dry run above.github-sync referenced this pull request2026-11-12 06:22:54 +00:00