Files
gen1recomp/tests/engine/present_sync_logic.lua
T
1jamie f82f565cae Fix to vsync measuring and detection and fixes fixestep so it snaps wallclock dt before applying speed
the display sync stuff i added was probing whether vsync was actually working, but it was measuring the wrong thing. during the probe we software-cap at 60 for safety, and the probe was looking at the gap between frames... which includes the sleep 😅 .....  so it always looked like sync was fine even when the driver was ignoring it. on something like the ally x on windows thats a real problem. vsync says on, probe says gated, we lift the cap and snap logic to the panel hz, then youre basically uncapped. at 2–4x that turns into hitching, dropped frames, dropped input, that weird half second freeze. speed swapping wasnt desyncing the driver, it was just making the bad path hurt more.
the solution is just we time present() itself now, not the gap after it, this way the warmup sleep cant fake a pass. if sync is unclear or broken we just stay on a capped 60 and dont snap logic. Also fixed fixedstep so it snaps wall clock dt before applying speed, so in general the 2-4x speed swaps dont screw the pacing math anymore
2026-08-30 11:12:19 -05:00

101 lines
3.7 KiB
Lua

package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.modkit")
local FrameCap = require("src.core.FrameCap")
local RefreshRate = require("src.core.RefreshRate")
local VSync = require("src.core.VSync")
local PresentSync = require("src.core.PresentSync")
local FixedStep = require("src.core.FixedStep")
local PresentProbe = require("src.core.PresentProbe")
RefreshRate.reset()
VSync.reset()
PresentSync.reset()
PresentProbe.reset()
local function measure(hz)
RefreshRate.reset()
for _ = 1, 31 do RefreshRate.sample(1 / hz) end
end
measure(144)
T.eq(RefreshRate.mismatch(), 144, "144Hz is not a multiple of 60")
FrameCap.apply(FrameCap.DISPLAY)
VSync.reset()
love.window.getVSync = function() return 0 end
love.window.setVSync = function() end
VSync.apply("off")
T.eq(PresentSync.logicRefreshPeriod(), nil,
"DISPLAY with vsync off does not snap logic to the panel (#1958)")
VSync.reset()
love.window.getVSync = function() return 1 end
VSync.apply("on")
PresentProbe._testSetState({ osLinux = false, ready = true, gated = false,
needsSoftwareCap = true, nest = "windows" })
T.eq(PresentSync.logicRefreshPeriod(), nil,
"DISPLAY with broken sync falls back to software cap, not panel snap")
PresentProbe._testSetState({ osLinux = false, ready = true, gated = true,
needsSoftwareCap = false, nest = "windows" })
T.eq(PresentSync.logicRefreshPeriod(), 1 / 144,
"DISPLAY with working sync tracks the panel")
FrameCap.apply(60)
T.eq(PresentSync.logicRefreshPeriod(), nil,
"a 60 cap on a 144Hz panel leaves refresh snapping off")
FrameCap.apply(144)
T.eq(PresentSync.logicRefreshPeriod(), 1 / 144,
"a 144 cap on a 144Hz panel may snap logic to the panel")
PresentSync.applyFixedStepPeriod()
T.eq(FixedStep.refreshPeriod, 1 / 144, "applyFixedStepPeriod writes the module field")
FrameCap.apply(FrameCap.DISPLAY)
VSync.apply("on")
PresentProbe._testSetState({ osLinux = false, ready = true, clearGated = true,
needsSoftwareCap = false, nest = "windows" })
T.check(PresentSync.probingDisplaySync(), "DISPLAY+vsync probes before a verdict")
T.check(PresentSync.needsSoftwareCap(),
"warmup still uses FrameCap as a thermal net during the probe")
T.eq(PresentSync.logicRefreshPeriod(), nil, "and does not snap logic during warmup")
PresentProbe._testSetState({ gated = true, needsSoftwareCap = false })
T.check(not PresentSync.probingDisplaySync(), "a finished probe clears warmup")
T.check(not PresentSync.needsSoftwareCap(), "so software cap stops once sync is confirmed")
-- Broken sync after an honest ungated probe keeps the thermal net and no snap.
PresentProbe._testSetState({ gated = false, needsSoftwareCap = true })
T.check(PresentSync.needsSoftwareCap(), "ungated DISPLAY keeps FrameCap")
T.eq(PresentSync.logicRefreshPeriod(), nil, "and never snaps logic to the panel")
VSync.apply("on")
PresentProbe._testSetState({ needsSoftwareCap = true, gated = false })
T.check(PresentSync.vsyncEnableBlocked(), "broken sync blocks enabling vsync")
T.check(PresentSync.vsyncStepAllowed("on", 1), "but one step to OFF is allowed")
T.check(not PresentSync.vsyncStepAllowed("on", -1),
"while a step that stays on/adaptive is not")
-- FixedStep snaps wall-clock dt, then applies speed (not the reverse).
FixedStep.refreshPeriod = 1 / 60
local steps = 0
FixedStep:init(function() steps = steps + 1 end)
FixedStep.maxAccum = 0.25
FixedStep:update(1 / 60, 4)
T.eq(steps, 4, "4X on a snapped 60Hz frame runs four logic steps")
steps = 0
FixedStep:update(1 / 60, 2)
T.eq(steps, 2, "and 2X runs two")
FixedStep.refreshPeriod = nil
love.window.getVSync, love.window.setVSync = nil, nil
VSync.reset()
PresentSync.reset()
PresentProbe.reset()
RefreshRate.reset()
FrameCap.apply(FrameCap.DEFAULT)
T.finish("present sync logic")