import { ANSI_RENDER_FPS, BACKGROUND_STALL_MS, EMULATOR_FRAME_MS, IMAGE_RENDER_FPS, MAX_CATCHUP_STEPS, MAX_TIMER_DELTA_MS, } from "./constants.js"; import type { RenderBackend } from "./runtime.js"; export interface GameLoopState { frameAccumulatorMs: number; renderAccumulatorMs: number; pausedForStall: boolean; } export interface GameLoopAdvanceResult { frameAccumulatorMs: number; renderAccumulatorMs: number; emulatorSteps: number; shouldRender: boolean; pausedForStall: boolean; enteredStall: boolean; resumedFromStall: boolean; } export function advanceGameLoop(state: GameLoopState, rawDeltaMs: number, renderBackend: RenderBackend): GameLoopAdvanceResult { const deltaMs = Number.isFinite(rawDeltaMs) && rawDeltaMs > 0 ? rawDeltaMs : 0; if (deltaMs >= BACKGROUND_STALL_MS) { // Once timers stretch this far, smooth catch-up usually looks worse than // pausing the loop and forcing a clean redraw when normal cadence returns. return { frameAccumulatorMs: 0, renderAccumulatorMs: 0, emulatorSteps: 0, shouldRender: false, pausedForStall: true, enteredStall: !state.pausedForStall, resumedFromStall: false, }; } let frameAccumulatorMs = state.frameAccumulatorMs + Math.min(MAX_TIMER_DELTA_MS, deltaMs); let renderAccumulatorMs = state.renderAccumulatorMs + Math.min(MAX_TIMER_DELTA_MS, deltaMs); let emulatorSteps = 0; while (frameAccumulatorMs >= EMULATOR_FRAME_MS && emulatorSteps < MAX_CATCHUP_STEPS) { frameAccumulatorMs -= EMULATOR_FRAME_MS; emulatorSteps++; } if (emulatorSteps === MAX_CATCHUP_STEPS) { frameAccumulatorMs = 0; } const renderFrameMs = 1000 / (renderBackend === "ansi" ? ANSI_RENDER_FPS : IMAGE_RENDER_FPS); const shouldRender = renderAccumulatorMs >= renderFrameMs; if (shouldRender) { renderAccumulatorMs %= renderFrameMs; } return { frameAccumulatorMs, renderAccumulatorMs, emulatorSteps, shouldRender, pausedForStall: false, enteredStall: false, resumedFromStall: state.pausedForStall, }; }