Refactor rendering and throttle based on backlog

- Refactor rendering and metrics emission in ClientGameRunner to ensure updates occur only after all processing is complete
- Throttle renderGame() based on the current backlog
This commit is contained in:
scamiv
2025-12-11 17:17:52 +01:00
parent 2dde26223c
commit 391f19d061
3 changed files with 66 additions and 21 deletions
+26 -20
View File
@@ -28,6 +28,7 @@ import { UserSettings } from "../core/game/UserSettings";
import { WorkerClient } from "../core/worker/WorkerClient"; import { WorkerClient } from "../core/worker/WorkerClient";
import { import {
AutoUpgradeEvent, AutoUpgradeEvent,
BacklogStatusEvent,
DoBoatAttackEvent, DoBoatAttackEvent,
DoGroundAttackEvent, DoGroundAttackEvent,
InputHandler, InputHandler,
@@ -527,33 +528,35 @@ export class ClientGameRunner {
this.pendingStart = 0; this.pendingStart = 0;
} }
if (batch.length > 0 && lastTick !== undefined) { // Only update view and render when ALL processing is complete
if (
this.pendingStart >= this.pendingUpdates.length &&
batch.length > 0 &&
lastTick !== undefined
) {
const combinedGu = this.mergeGameUpdates(batch); const combinedGu = this.mergeGameUpdates(batch);
if (combinedGu) { if (combinedGu) {
this.gameView.update(combinedGu); this.gameView.update(combinedGu);
} }
// Only emit metrics when ALL processing is complete const ticksPerRender =
if (this.pendingStart >= this.pendingUpdates.length) { this.lastRenderedTick === 0
const ticksPerRender = ? lastTick
this.lastRenderedTick === 0 : lastTick - this.lastRenderedTick;
? lastTick this.lastRenderedTick = lastTick;
: lastTick - this.lastRenderedTick;
this.lastRenderedTick = lastTick;
this.renderer.tick(); this.renderer.tick();
this.eventBus.emit( this.eventBus.emit(
new TickMetricsEvent( new TickMetricsEvent(
lastTickDuration, lastTickDuration,
this.currentTickDelay, this.currentTickDelay,
this.backlogTurns, this.backlogTurns,
ticksPerRender, ticksPerRender,
), ),
); );
// Reset tick delay for next measurement // Reset tick delay for next measurement
this.currentTickDelay = undefined; this.currentTickDelay = undefined;
}
} }
if (this.pendingStart < this.pendingUpdates.length) { if (this.pendingStart < this.pendingUpdates.length) {
@@ -614,6 +617,9 @@ export class ClientGameRunner {
this.serverTurnHighWater - this.lastProcessedTick, this.serverTurnHighWater - this.lastProcessedTick,
); );
this.backlogGrowing = this.backlogTurns > previousBacklog; this.backlogGrowing = this.backlogTurns > previousBacklog;
this.eventBus.emit(
new BacklogStatusEvent(this.backlogTurns, this.backlogGrowing),
);
} }
private inputEvent(event: MouseUpEvent) { private inputEvent(event: MouseUpEvent) {
+7
View File
@@ -136,6 +136,13 @@ export class TickMetricsEvent implements GameEvent {
) {} ) {}
} }
export class BacklogStatusEvent implements GameEvent {
constructor(
public readonly backlogTurns: number,
public readonly backlogGrowing: boolean,
) {}
}
export class InputHandler { export class InputHandler {
private lastPointerX: number = 0; private lastPointerX: number = 0;
private lastPointerY: number = 0; private lastPointerY: number = 0;
+33 -1
View File
@@ -2,7 +2,10 @@ import { EventBus } from "../../core/EventBus";
import { GameView } from "../../core/game/GameView"; import { GameView } from "../../core/game/GameView";
import { UserSettings } from "../../core/game/UserSettings"; import { UserSettings } from "../../core/game/UserSettings";
import { GameStartingModal } from "../GameStartingModal"; import { GameStartingModal } from "../GameStartingModal";
import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler"; import {
BacklogStatusEvent,
RefreshGraphicsEvent as RedrawGraphicsEvent,
} from "../InputHandler";
import { FrameProfiler } from "./FrameProfiler"; import { FrameProfiler } from "./FrameProfiler";
import { TransformHandler } from "./TransformHandler"; import { TransformHandler } from "./TransformHandler";
import { UIState } from "./UIState"; import { UIState } from "./UIState";
@@ -292,6 +295,9 @@ export function createRenderer(
export class GameRenderer { export class GameRenderer {
private context: CanvasRenderingContext2D; private context: CanvasRenderingContext2D;
private backlogTurns: number = 0;
private backlogGrowing: boolean = false;
private lastRenderTime: number = 0;
constructor( constructor(
private game: GameView, private game: GameView,
@@ -309,6 +315,10 @@ export class GameRenderer {
initialize() { initialize() {
this.eventBus.on(RedrawGraphicsEvent, () => this.redraw()); this.eventBus.on(RedrawGraphicsEvent, () => this.redraw());
this.eventBus.on(BacklogStatusEvent, (event: BacklogStatusEvent) => {
this.backlogTurns = event.backlogTurns;
this.backlogGrowing = event.backlogGrowing;
});
this.layers.forEach((l) => l.init?.()); this.layers.forEach((l) => l.init?.());
// only append the canvas if it's not already in the document to avoid reparenting side-effects // only append the canvas if it's not already in the document to avoid reparenting side-effects
@@ -348,6 +358,28 @@ export class GameRenderer {
} }
renderGame() { renderGame() {
const now = performance.now();
if (this.backlogTurns > 0) {
const BASE_FPS = 60;
const MIN_FPS = 20;
const BACKLOG_MAX_TURNS = 50;
const scale = Math.min(1, this.backlogTurns / BACKLOG_MAX_TURNS);
const targetFps = BASE_FPS - scale * (BASE_FPS - MIN_FPS);
const minFrameInterval = 1000 / targetFps;
if (this.lastRenderTime !== 0) {
const sinceLast = now - this.lastRenderTime;
if (sinceLast < minFrameInterval) {
requestAnimationFrame(() => this.renderGame());
return;
}
}
}
this.lastRenderTime = now;
FrameProfiler.clear(); FrameProfiler.clear();
const start = performance.now(); const start = performance.now();
// Set background // Set background