Merge pull request #2007 from laaledesiempre/fix/main-thread-jitter

fix(core): cut per-frame allocation churn and GC pressure on the main loop
This commit is contained in:
bryanthaboi
2026-08-30 21:38:09 -04:00
committed by GitHub
2 changed files with 28 additions and 10 deletions
+11 -2
View File
@@ -34,12 +34,21 @@ end
-- Global emergency quit: holding Start + Select for 5 seconds forcefully terminates LOVE.
local emergencyQuitTimer = 0
-- getJoysticks() allocates a fresh table every call; this runs once (twice)
-- per frame, so cache the list and refresh it once a second instead. The 5s
-- hold requirement makes a 1s hotplug delay irrelevant.
local cachedJoysticks = nil
local joystickCacheAge = 1
local function checkEmergencyQuit(dt)
local held = false
joystickCacheAge = joystickCacheAge + (dt or 0.016)
if love.joystick and love.joystick.getJoysticks then
local joysticks = love.joystick.getJoysticks()
for _, j in ipairs(joysticks) do
if not cachedJoysticks or joystickCacheAge >= 1 then
cachedJoysticks = love.joystick.getJoysticks()
joystickCacheAge = 0
end
for _, j in ipairs(cachedJoysticks) do
if j:isGamepad() then
local start = j:isGamepadDown("start")
local selectBtn = j:isGamepadDown("back") or j:isGamepadDown("guide")
+17 -8
View File
@@ -21,6 +21,12 @@ local function renderVisible(stack, state)
return state and (not stack.renderVisible or stack:renderVisible(state))
end
-- Vanilla defaults for ModRuntime.call, hoisted to module level: called from
-- the 60Hz logic step / per-frame speed resolution, an inline closure here
-- allocated a fresh function every tick for no behavioral gain.
local function noop() end
local function resolveLogicSpeedVanilla(g) return g:_resolveLogicSpeed() end
-- dev-mode gate for the F5/backtick hotkeys; false keeps every src/dev
-- module unloaded, so a player boot never touches a byte of dev code
local devMode = os.getenv("POKEPORT_DEV") == "1" or _G.POKEPORT_DEV_MODE == true
@@ -304,7 +310,7 @@ function Game:step(dt)
-- the same fixed-step boundary as a physical controller. Run them before
-- Input:step promotes queued edges so a button chosen here is visible to
-- this logic tick, not one tick later. With no wrapper this is a no-op.
ModRuntime.call("input.step", function() end, self, dt)
ModRuntime.call("input.step", noop, self, dt)
self.input:step()
-- A+B+SELECT+START held for 16 steps: SoftReset (home/init.asm) stops the
-- audio, whites the palettes out and falls through into Init, i.e. the
@@ -390,7 +396,7 @@ function Game:logicSpeed()
-- returns a bad value, so an unclamped result would flow straight into
-- the FixedStep accumulator math below and freeze or destabilize logic.
return GameSpeed.clamp(ModRuntime.call("core.logic_speed",
function(g) return g:_resolveLogicSpeed() end, self))
resolveLogicSpeedVanilla, self))
end
function Game:update(dt)
@@ -420,12 +426,15 @@ function Game:update(dt)
pcall(function() require("src.core.DiscordPresence").update(dt) end)
self:updateSync(dt)
-- Steady-state memory backstop: advance the incremental collector one
-- small step every rendered frame. The heavy GPU objects are now freed
-- explicitly (map eviction, battle exit, canvas/renderer swaps), so this
-- only has to keep ordinary Lua-heap garbage (per-frame tables/closures)
-- from drifting upward over a long session, and to spread collection out
-- so the default lazy schedule never batches it into a visible pause.
if collectgarbage then collectgarbage("step", 1) end
-- small step every few rendered frames. The heavy GPU objects are now
-- freed explicitly (map eviction, battle exit, canvas/renderer swaps), so
-- this only has to keep ordinary Lua-heap garbage (per-frame tables) from
-- drifting upward over a long session, and to spread collection out so the
-- default lazy schedule never batches it into a visible pause. Every 4th
-- frame (not every frame) so the stepping itself does not compete with
-- the frame budget on weak single-core handhelds.
self.gcStepFrame = (self.gcStepFrame or 0) + 1
if collectgarbage and self.gcStepFrame % 4 == 0 then collectgarbage("step", 1) end
end
-- render.zones' identity default: unhooked, the zone list reaches the blit