From 46d09eb61480838dcf5941d86059865ea614e3e8 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:44:31 +0100 Subject: [PATCH] throttle renderGame() based on the current backlog (again) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rendering runs on its own rAF loop and always tries to hit the display framerate, so even though it’s on a different thread it can still chew a lot of CPU while the worker is trying to catch up a huge backlog. To mitigate this, we now emit backlog status from ClientGameRunner and throttle renderGame() based on the current backlog, effectively reintroducing frame skipping so the worker and main-thread update processing get more breathing room when we’re far behind. --- src/client/ClientGameRunner.ts | 4 ++++ src/client/InputHandler.ts | 7 ++++++ src/client/graphics/GameRenderer.ts | 34 ++++++++++++++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 6cc7756e0..02edc412c 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, @@ -600,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