Files
healer-man/src/scene/frameRateGovernor.ts
T
2026-08-30 14:16:56 -04:00

87 lines
2.7 KiB
TypeScript

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 };
}