fleeting-garden/src/game-loop/frame-performance.ts
Andras Schmelczer 10a81ba474
Some checks failed
Deploy to Pages / build (pull_request) Failing after 1m56s
v good
2026-05-16 13:46:19 +01:00

138 lines
3.9 KiB
TypeScript

import { appConfig } from '../config';
interface TelemetrySnapshot {
frameCpuStartedAt: number;
encodeCpuMs: number;
activeAgentCount: number;
agentBudgetMax: number;
canvas: HTMLCanvasElement;
devicePixelRatio: number;
renderSpeed: number;
}
const COMMON_DISPLAY_REFRESH_RATES = [
50, 60, 72, 75, 90, 100, 120, 144, 165, 180, 240,
] as const;
const DISPLAY_REFRESH_CONFIRMATION_FRAMES = 8;
const DISPLAY_REFRESH_SNAP_TOLERANCE = 0.15;
export class FramePerformance {
public latestFps = 60;
public smoothedFps = 60;
public displayRefreshFps = 60;
public readonly refreshTargetFps = 60;
private lastTelemetryAt = 0;
private hasConfirmedDisplayRefreshFps = false;
private pendingDisplayRefreshFps = 0;
private pendingDisplayRefreshFrameCount = 0;
public markCpuStart(): number {
return appConfig.telemetry.enabled ? performance.now() : 0;
}
public measureSince(startedAt: number): number {
return appConfig.telemetry.enabled ? performance.now() - startedAt : 0;
}
public update(deltaTime: number): void {
const fps = 1 / Math.max(deltaTime, appConfig.deltaTime.minDeltaTimeSeconds);
this.latestFps = fps;
this.updateDisplayRefreshEstimate(fps);
this.smoothedFps =
this.smoothedFps * appConfig.simulation.budget.fpsSmoothingRetain +
fps * appConfig.simulation.budget.fpsSmoothingNew;
}
public renderTelemetry({
frameCpuStartedAt,
encodeCpuMs,
activeAgentCount,
agentBudgetMax,
canvas,
devicePixelRatio,
renderSpeed,
}: TelemetrySnapshot): void {
if (!appConfig.telemetry.enabled) {
return;
}
const now = performance.now();
if (now - this.lastTelemetryAt < appConfig.telemetry.intervalMs) {
return;
}
this.lastTelemetryAt = now;
console.debug('Fleeting Garden telemetry', {
fps: Math.round(this.latestFps),
smoothedFps: Math.round(this.smoothedFps),
refreshTargetFps: Math.round(this.refreshTargetFps),
displayRefreshFps: Math.round(this.displayRefreshFps),
activeAgentCount,
agentBudgetMax,
canvasWidth: canvas.width,
canvasHeight: canvas.height,
dpr: devicePixelRatio,
renderSpeed,
frameCpuMs: now - frameCpuStartedAt,
encodeCpuMs,
});
}
private updateDisplayRefreshEstimate(fps: number): void {
const displayRefreshFps = this.snapDisplayRefreshRate(fps);
if (displayRefreshFps === null) {
this.resetPendingDisplayRefreshEstimate();
return;
}
if (
this.hasConfirmedDisplayRefreshFps &&
displayRefreshFps < this.displayRefreshFps
) {
this.resetPendingDisplayRefreshEstimate();
return;
}
if (displayRefreshFps !== this.pendingDisplayRefreshFps) {
this.pendingDisplayRefreshFps = displayRefreshFps;
this.pendingDisplayRefreshFrameCount = 1;
} else {
this.pendingDisplayRefreshFrameCount += 1;
}
if (this.pendingDisplayRefreshFrameCount < DISPLAY_REFRESH_CONFIRMATION_FRAMES) {
return;
}
this.displayRefreshFps = displayRefreshFps;
this.hasConfirmedDisplayRefreshFps = true;
this.resetPendingDisplayRefreshEstimate();
}
private snapDisplayRefreshRate(fps: number): number | null {
if (!Number.isFinite(fps) || fps <= 0) {
return null;
}
let nearestRefreshRate: number = COMMON_DISPLAY_REFRESH_RATES[0];
let nearestDifference = Math.abs(fps - nearestRefreshRate);
COMMON_DISPLAY_REFRESH_RATES.forEach((refreshRate) => {
const difference = Math.abs(fps - refreshRate);
if (difference < nearestDifference) {
nearestRefreshRate = refreshRate;
nearestDifference = difference;
}
});
return nearestDifference / nearestRefreshRate <= DISPLAY_REFRESH_SNAP_TOLERANCE
? nearestRefreshRate
: null;
}
private resetPendingDisplayRefreshEstimate(): void {
this.pendingDisplayRefreshFps = 0;
this.pendingDisplayRefreshFrameCount = 0;
}
}