87 lines
2.7 KiB
TypeScript
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 };
|
|
}
|