mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-01 07:40:29 -04:00
fix(core): cut per-frame allocation churn and GC pressure on the main loop
On weak single-core handhelds (RK3326/RG351MP) the main thread sits at 100% CPU, so every millisecond of jitter drops a frame: constant stuttering while walking even at PERFORMANCE LOW with all mods off, while average CPU usage stays unchanged. Three sources of per-frame garbage and pause on the hot path: - Game:step / Game:logicSpeed passed inline closures to ModRuntime.call, allocating a fresh function 60 times per second. Hoist both to module-level locals; behavior is identical. - Game:update advanced the incremental collector every rendered frame. Space it to every 4th frame: the explicit frees the comment refers to still do the heavy lifting, collection stays incremental, and the stepping itself stops competing with the 16.6ms frame budget. - checkEmergencyQuit called love.joystick.getJoysticks() (a fresh table) twice per frame to guard a 5-second hold combo. Cache the joystick list and refresh it once per second; a 1s hotplug delay is irrelevant against a 5s hold. Tested on an RG351MP (RK3326, dArkOS, v0.2.41): eliminates the constant walking stutter with all mods off, and with 11 mods re-enabled. Average CPU is unchanged (the cost was variance, not load), which matches the diagnosis.
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user