fix(fps): the sound clock keeps its remainder around 60 fps, and cap 60 runs every phase #2552

Merged
hckrman101 merged 1 commit from fix/high-fps-sound-remainder into main 2026-11-11 23:34:46 +00:00
hckrman101 commented 2026-11-11 22:59:51 +00:00 (Migrated from github.com)

#2470's sound gate has two flaws. Around 60 fps with a cap above 60, the sound runs slower than real time. At a cap of 60, a sound job phase can be skipped once.

What changed

The remainder is kept across whole steps. Before, a step of 1 or more ran the sound job and threw away the partial steps added up before it. With a cap above 60 and the game near 60 fps (or VRR, or frame-time jitter), the step hovers around 1, and the frames under 1 were simply lost. In a simulation, the sound engine's clock against real time was:

Steps Before After
0.98 and 1.02 alternating 0.51 1.000
0.9 and 1.1 alternating 0.55 1.000
mean 1, spread 0.03 0.76 1.000

A step of 1 or more now runs with the game's own step and keeps the remainder, which comes back later as one more whole step. The remainder is cleared only at a cap of 60 and for a step of 0 or less (or NaN).

A cap of 60 runs every phase. The gate started with "do not run", and it answered phases 1 and 2 from phase 0's last choice before looking at the cap. So at a cap of 60, a phase 1 or 2 before the first phase 0, or right after the cap changed, was skipped once. A cap of 60 now runs every phase whatever came before, and the gate starts with "run". At a cap of 60 the sound job is now exactly the game's own again.

Also: HOS_FPS_LOG's sound counters are kept under the gate's lock, and its switch is read atomically.

The original sound fix is a port of zortos293/Lichii PR #1289 by auz466.

Tests

tests/test_high_fps_fixes.py adds checks that:

  • the remainder survives a whole step and is cleared by a step of 0;
  • the sound clock stays within 0.1% of real time for steps of 0.98/1.02, 0.9/1.1, mean 1 with jitter, and 0.5;
  • phases 1 and 2 run at a cap of 60 before any phase 0, and after a phase 0 that held the job at a cap above 60.

The same checks run against #2470's gate fail 9 cases, including the first three clock cases. The new test passes.

Not tested

  • No game run: the PC was reserved for release testing.
  • The full tests/run_all.py was not run; only tests/test_high_fps_fixes.py.
#2470's sound gate has two flaws. Around 60 fps with a cap above 60, the sound runs slower than real time. At a cap of 60, a sound job phase can be skipped once. ## What changed **The remainder is kept across whole steps.** Before, a step of 1 or more ran the sound job and threw away the partial steps added up before it. With a cap above 60 and the game near 60 fps (or VRR, or frame-time jitter), the step hovers around 1, and the frames under 1 were simply lost. In a simulation, the sound engine's clock against real time was: | Steps | Before | After | |---|---|---| | 0.98 and 1.02 alternating | 0.51 | 1.000 | | 0.9 and 1.1 alternating | 0.55 | 1.000 | | mean 1, spread 0.03 | 0.76 | 1.000 | A step of 1 or more now runs with the game's own step and keeps the remainder, which comes back later as one more whole step. The remainder is cleared only at a cap of 60 and for a step of 0 or less (or NaN). **A cap of 60 runs every phase.** The gate started with "do not run", and it answered phases 1 and 2 from phase 0's last choice before looking at the cap. So at a cap of 60, a phase 1 or 2 before the first phase 0, or right after the cap changed, was skipped once. A cap of 60 now runs every phase whatever came before, and the gate starts with "run". At a cap of 60 the sound job is now exactly the game's own again. **Also:** `HOS_FPS_LOG`'s sound counters are kept under the gate's lock, and its switch is read atomically. The original sound fix is a port of [zortos293/Lichii PR #1289](https://github.com/zortos293/Lichii/pull/1289) by auz466. ## Tests `tests/test_high_fps_fixes.py` adds checks that: - the remainder survives a whole step and is cleared by a step of 0; - the sound clock stays within 0.1% of real time for steps of 0.98/1.02, 0.9/1.1, mean 1 with jitter, and 0.5; - phases 1 and 2 run at a cap of 60 before any phase 0, and after a phase 0 that held the job at a cap above 60. The same checks run against #2470's gate fail 9 cases, including the first three clock cases. The new test passes. ## Not tested - No game run: the PC was reserved for release testing. - The full `tests/run_all.py` was not run; only `tests/test_high_fps_fixes.py`.
Commenting is not possible because the repository is archived.
No description provided.