export type FrameRateGovernorMode = number | "display"; export interface FrameRateGovernorState { readonly mode: FrameRateGovernorMode; readonly lastTimestampMs: number | null; readonly nextDeadlineMs: number | null; readonly elapsedSeconds: number; } export interface FrameRateGovernorStep { readonly state: FrameRateGovernorState; readonly shouldRender: boolean; readonly elapsedSeconds: number; } function frameIntervalMs(mode: FrameRateGovernorMode): number | null { if (mode === "display") return null; return Number.isFinite(mode) && mode > 0 ? 1_000 / mode : 1_000 / 60; } export function createFrameRateGovernor( mode: FrameRateGovernorMode, elapsedSeconds = 0, ): FrameRateGovernorState { return { mode, lastTimestampMs: null, nextDeadlineMs: null, elapsedSeconds: Math.max(0, elapsedSeconds), }; } export function resetFrameRateGovernor( state: FrameRateGovernorState, mode: FrameRateGovernorMode = state.mode, ): FrameRateGovernorState { return createFrameRateGovernor(mode, state.elapsedSeconds); } export function stepFrameRateGovernor( state: FrameRateGovernorState, rawTimestampMs: number, ): FrameRateGovernorStep { const fallbackTimestamp = state.lastTimestampMs ?? 0; const finiteTimestamp = Number.isFinite(rawTimestampMs) ? rawTimestampMs : fallbackTimestamp; const timestampMs = Math.max(fallbackTimestamp, finiteTimestamp); const intervalMs = frameIntervalMs(state.mode); if (state.lastTimestampMs === null) { const nextState: FrameRateGovernorState = { ...state, lastTimestampMs: timestampMs, nextDeadlineMs: intervalMs === null ? null : timestampMs + intervalMs, }; return { state: nextState, shouldRender: true, elapsedSeconds: nextState.elapsedSeconds }; } const elapsedSeconds = state.elapsedSeconds + (timestampMs - state.lastTimestampMs) / 1_000; if (intervalMs === null) { const nextState: FrameRateGovernorState = { ...state, lastTimestampMs: timestampMs, elapsedSeconds, }; return { state: nextState, shouldRender: true, elapsedSeconds }; } const deadlineMs = state.nextDeadlineMs ?? timestampMs; if (timestampMs < deadlineMs) { const nextState: FrameRateGovernorState = { ...state, lastTimestampMs: timestampMs, elapsedSeconds, }; return { state: nextState, shouldRender: false, elapsedSeconds }; } const missedIntervals = Math.floor(Math.max(0, timestampMs - deadlineMs) / intervalMs); const nextState: FrameRateGovernorState = { ...state, lastTimestampMs: timestampMs, nextDeadlineMs: deadlineMs + (missedIntervals + 1) * intervalMs, elapsedSeconds, }; return { state: nextState, shouldRender: true, elapsedSeconds }; }