mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-30 15:27:16 -04:00
96 lines
3.3 KiB
Lua
96 lines
3.3 KiB
Lua
-- Render frame-rate cap. With a driver control panel forcing
|
|
-- vsync off, the 160x144 game is trivially cheap and love.run will present
|
|
-- thousands of frames a second; over hours that cooks the graphics driver
|
|
-- until a restart, and it wastes power whenever the window is left open in
|
|
-- the background. A hard cap bounds the present rate. Render-only: game
|
|
-- logic is fixed-step off dt (src/core/FixedStep.lua), so pacing present()
|
|
-- changes nothing about timing, audio, or determinism.
|
|
--
|
|
-- Persisted as save.options.fpsCap; applied from OptionsMenu and on boot
|
|
-- via Game:applyOptions. main.lua's love.run reads FrameCap.current each
|
|
-- frame for its sleep budget. The module never touches love.timer itself,
|
|
-- so it stays safe under the headless test stub.
|
|
|
|
local FrameCap = {}
|
|
|
|
-- Selectable steps: the normal framerate stops between the floor and the
|
|
-- ceiling. STEPS[1] == MIN and STEPS[#STEPS] == MAX, so the nearest-step
|
|
FrameCap.STEPS = { 30, 40, 50, 60, 75, 90, 100, 120, 144, 160 }
|
|
FrameCap.MIN = 30
|
|
FrameCap.MAX = 160
|
|
FrameCap.DEFAULT = 60
|
|
|
|
FrameCap.DISPLAY = 0
|
|
|
|
FrameCap.CYCLE = {}
|
|
for i, step in ipairs(FrameCap.STEPS) do FrameCap.CYCLE[i] = step end
|
|
FrameCap.CYCLE[#FrameCap.CYCLE + 1] = FrameCap.DISPLAY
|
|
|
|
-- The live cap the run loop paces to. Defaults so the launcher and the
|
|
-- save editor are paced before any save applies its stored option.
|
|
FrameCap.current = FrameCap.DEFAULT
|
|
|
|
-- Nearest valid step for an arbitrary value (a hand-edited options.lua or
|
|
-- an old save with no fpsCap key), so a bad number degrades to something
|
|
function FrameCap.normalize(value)
|
|
value = tonumber(value)
|
|
if not value then return FrameCap.DEFAULT end
|
|
if value <= 0 then return FrameCap.DISPLAY end
|
|
local best, bestDiff = FrameCap.DEFAULT, math.huge
|
|
for _, step in ipairs(FrameCap.STEPS) do
|
|
local diff = math.abs(step - value)
|
|
if diff < bestDiff then best, bestDiff = step, diff end
|
|
end
|
|
return best
|
|
end
|
|
|
|
function FrameCap.label(value)
|
|
local cap = FrameCap.normalize(value)
|
|
local text = cap == FrameCap.DISPLAY and "DISPLAY" or tostring(cap)
|
|
local hz = require("src.core.RefreshRate").mismatch()
|
|
if hz then text = string.format("%s (%dHZ)", text, math.floor(hz + 0.5)) end
|
|
return text
|
|
end
|
|
|
|
function FrameCap.cycle(value, dir)
|
|
local ring = FrameCap.CYCLE
|
|
local snapped = FrameCap.normalize(value)
|
|
local cur = 1
|
|
for i, step in ipairs(ring) do
|
|
if step == snapped then cur = i break end
|
|
end
|
|
local nextIdx = (cur - 1 + (dir or 1)) % #ring + 1
|
|
return ring[nextIdx]
|
|
end
|
|
|
|
-- Store the chosen cap as the live value the run loop paces to. Never
|
|
-- touches love.timer, so it is safe headless -- the loop just reads the
|
|
-- number back. Returns the normalized value it stored.
|
|
function FrameCap.apply(value)
|
|
FrameCap.current = FrameCap.normalize(value)
|
|
return FrameCap.current
|
|
end
|
|
|
|
FrameCap.migrated = false
|
|
|
|
FrameCap.MIGRATE_MAX_HZ = 120
|
|
|
|
function FrameCap.applyOptions(opts)
|
|
local cap = opts and opts.fpsCap
|
|
if not FrameCap.migrated then
|
|
FrameCap.migrated = true
|
|
if opts and not opts.fpsCapMigrated
|
|
and FrameCap.normalize(cap) == FrameCap.DEFAULT then
|
|
local hz = require("src.core.RefreshRate").mismatch()
|
|
if hz and hz < FrameCap.MIGRATE_MAX_HZ then
|
|
cap = FrameCap.DISPLAY
|
|
opts.fpsCap = cap
|
|
opts.fpsCapMigrated = true
|
|
end
|
|
end
|
|
end
|
|
FrameCap.apply(cap)
|
|
end
|
|
|
|
return FrameCap
|