Files
gen1recomp/tests/engine/present_sync_logic.lua
T
1jamie 07071e334c 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.
2026-08-30 11:29:43 -05:00

118 lines
4.8 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")
T.check(PresentSync.needsSoftwareCap(), "failed sync trips the FrameCap cascade")
T.check(not PresentSync.displaySyncConfirmed(), "and is not treated as confirmed")
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")
T.check(PresentSync.displaySyncConfirmed(), "gated+clear softcap is confirmed sync")
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")
-- Probe isolation: during calibration we do NOT soft-cap (raw swapchain),
-- and we never snap logic until sync is confirmed.
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(not PresentSync.needsSoftwareCap(),
"probe isolation: FrameCap is not forced during calibration")
T.check(not PresentSync.displaySyncConfirmed(), "unconfirmed while still probing")
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(PresentSync.displaySyncConfirmed(), "gated verdict confirms display sync")
T.check(not PresentSync.needsSoftwareCap(), "so software cap stays off 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.check(not PresentSync.displaySyncConfirmed(), "ungated is never confirmed")
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 = FixedStep.catchupLimit(4)
FixedStep:update(1 / 60, 4)
T.eq(steps, 4, "4X on a snapped 60Hz frame runs four logic steps")
steps = 0
FixedStep.maxAccum = FixedStep.catchupLimit(2)
FixedStep:update(1 / 60, 2)
T.eq(steps, 2, "and 2X runs two")
-- Catch-up debt hard ceiling: high speed cannot raise maxAccum past MAX_ACCUM.
T.eq(FixedStep.catchupLimit(1), FixedStep.STEP * 2,
"1X catch-up floor is two steps")
T.eq(FixedStep.catchupLimit(4), 4 * FixedStep.STEP * 1.5,
"4X catch-up matches one frame of target work")
T.eq(FixedStep.catchupLimit(200), FixedStep.MAX_ACCUM,
"200X catch-up is hard-capped at MAX_ACCUM (no input-starving spiral)")
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")