From 59ff42e52ba1988a0edd03d098ac2d26b84e0484 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:46:25 +0100 Subject: [PATCH] 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 --- src/client/ClientGameRunner.ts | 46 ++++++++++++++++------------- src/client/InputHandler.ts | 7 +++++ src/client/graphics/GameRenderer.ts | 34 ++++++++++++++++++++- 3 files changed, 66 insertions(+), 21 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index ce88d861a..4d70a3f3e 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -28,6 +28,7 @@ import { UserSettings } from "../core/game/UserSettings"; import { WorkerClient } from "../core/worker/WorkerClient"; import { AutoUpgradeEvent, + BacklogStatusEvent, DoBoatAttackEvent, DoGroundAttackEvent, InputHandler, @@ -511,33 +512,35 @@ export class ClientGameRunner { 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); if (combinedGu) { this.gameView.update(combinedGu); } - // Only emit metrics when ALL processing is complete - if (this.pendingStart >= this.pendingUpdates.length) { - const ticksPerRender = - this.lastRenderedTick === 0 - ? lastTick - : lastTick - this.lastRenderedTick; - this.lastRenderedTick = lastTick; + const ticksPerRender = + this.lastRenderedTick === 0 + ? lastTick + : lastTick - this.lastRenderedTick; + this.lastRenderedTick = lastTick; - this.renderer.tick(); - this.eventBus.emit( - new TickMetricsEvent( - lastTickDuration, - this.currentTickDelay, - this.backlogTurns, - ticksPerRender, - ), - ); + this.renderer.tick(); + this.eventBus.emit( + new TickMetricsEvent( + lastTickDuration, + this.currentTickDelay, + this.backlogTurns, + ticksPerRender, + ), + ); - // Reset tick delay for next measurement - this.currentTickDelay = undefined; - } + // Reset tick delay for next measurement + this.currentTickDelay = undefined; } if (this.pendingStart < this.pendingUpdates.length) { @@ -598,6 +601,9 @@ export class ClientGameRunner { this.serverTurnHighWater - this.lastProcessedTick, ); this.backlogGrowing = this.backlogTurns > previousBacklog; + this.eventBus.emit( + new BacklogStatusEvent(this.backlogTurns, this.backlogGrowing), + ); } private inputEvent(event: MouseUpEvent) { diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index bd95e7201..85039015d 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -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 { private lastPointerX: number = 0; private lastPointerY: number = 0; diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 1410cdbbd..97e4ad909 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -2,7 +2,10 @@ import { EventBus } from "../../core/EventBus"; import { GameView } from "../../core/game/GameView"; import { UserSettings } from "../../core/game/UserSettings"; import { GameStartingModal } from "../GameStartingModal"; -import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler"; +import { + BacklogStatusEvent, + RefreshGraphicsEvent as RedrawGraphicsEvent, +} from "../InputHandler"; import { FrameProfiler } from "./FrameProfiler"; import { TransformHandler } from "./TransformHandler"; import { UIState } from "./UIState"; @@ -292,6 +295,9 @@ export function createRenderer( export class GameRenderer { private context: CanvasRenderingContext2D; + private backlogTurns: number = 0; + private backlogGrowing: boolean = false; + private lastRenderTime: number = 0; constructor( private game: GameView, @@ -309,6 +315,10 @@ export class GameRenderer { initialize() { 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?.()); document.body.appendChild(this.canvas); @@ -344,6 +354,28 @@ export class GameRenderer { } 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(); const start = performance.now(); // Set background