Refine VSync detection and FixedStep logic for improved frame pacing

- Updated PresentSync to accurately measure present() block time, ensuring that the probe does not misclassify sync status due to FrameCap sleep artifacts.
- Enhanced FixedStep to implement a hard ceiling on catch-up debt, preventing input starvation during high-speed scenarios.
- Adjusted logic in Game and Game2 to utilize the new catchupLimit function for setting maxAccum, ensuring consistent behavior across speed multipliers.
- Improved test coverage for PresentProbe and PresentSync to validate the new logic and edge cases.
This commit is contained in:
1jamie
2026-08-30 11:29:43 -05:00
parent f82f565cae
commit 07071e334c
8 changed files with 148 additions and 76 deletions
+24 -6
View File
@@ -5,7 +5,12 @@
local FixedStep = {}
FixedStep.STEP = 1 / 60
local MAX_ACCUM = 0.25 -- avoid spiral of death after a stall
-- Hard ceiling on catch-up debt (seconds of logic time drained in one
-- rendered frame). 0.25s = 15 GB steps. Game:update may lower this for the
-- current speed target but must never raise it: a hitch at high multipliers
-- cannot dump unbounded steps and starve input / spiral the frame.
FixedStep.MAX_ACCUM = 0.25
local MAX_ACCUM = FixedStep.MAX_ACCUM
local SMOOTH_FRAMES = 4
local SMOOTH_MAX = 1 / 60 * 2.5
local STEP_EPS = 1 / 60 * 0.02
@@ -55,13 +60,23 @@ function FixedStep:init(callback)
self.phasedFor = nil
end
-- The anti-spiral clamp doubles as a steps-per-frame ceiling (0.25s = 15
-- steps), which silently throttled the high fast-forward levels: at 100X
-- a 60fps frame wants ~100 steps. Game:update raises this to fit the
-- current speed target; a stall still cannot snowball past one frame's
-- intended budget.
-- The anti-spiral clamp is the steps-per-frame ceiling. Game:update sets
-- maxAccum to the current speed's one-frame budget, hard-capped at MAX_ACCUM
-- so high fast-forward cannot turn a hitch into input starvation.
FixedStep.maxAccum = MAX_ACCUM
-- Compute the live catch-up ceiling for a logic-speed multiplier.
-- Always ≤ MAX_ACCUM; at least two steps so ordinary vsync wobble can recover.
function FixedStep.catchupLimit(speed)
speed = tonumber(speed) or 1
if speed < 1 then speed = 1 end
local target = speed * FixedStep.STEP * 1.5
local floor = FixedStep.STEP * 2
if target < floor then target = floor end
if target > MAX_ACCUM then target = MAX_ACCUM end
return target
end
function FixedStep:update(dt, speed)
-- A hitch's oversized dt lands on the frame AFTER discardCatchup was
-- called (the hitch itself already ran inside the current step); absorb
@@ -80,6 +95,9 @@ function FixedStep:update(dt, speed)
-- destabilized pacing under DISPLAY sync.
speed = tonumber(speed) or 1
if speed < 1 then speed = 1 end
-- Pathological wall-clock stalls: do not let dt alone exceed the catch-up
-- ceiling before speed is applied (speed amplify would just hit the clamp).
if dt > MAX_ACCUM then dt = MAX_ACCUM end
local period = self.refreshPeriod
local snapped = false
if period and dt > 0 then