From 9d63fcabe99e978c7846f92c5548224aa8766751 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Mon, 24 Nov 2025 17:40:11 +0100 Subject: [PATCH] Implemented time-sliced catch-up on the main thread to keep input responsive. src/client/ClientGameRunner.ts now drains pending game updates in small chunks (max 100 updates or ~8ms per slice) via requestAnimationFrame, merging and rendering per slice, and only clears the processing flag when the queue is empty. --- src/client/ClientGameRunner.ts | 73 ++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 8a92444ce..cd59e80b5 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -477,23 +477,25 @@ export class ClientGameRunner { } private processPendingUpdates() { - if (this.isProcessingUpdates) { - return; - } - if (this.pendingUpdates.length === 0) { + if (this.isProcessingUpdates || this.pendingUpdates.length === 0) { return; } this.isProcessingUpdates = true; - const batch = this.pendingUpdates.splice(0); + const processSlice = () => { + const SLICE_BUDGET_MS = 8; // keep UI responsive while catching up + const MAX_PER_SLICE = 100; + const sliceStart = performance.now(); + const batch: GameUpdateViewData[] = []; - let processedCount = 0; - let lastTickDuration: number | undefined; - let lastTick: number | undefined; + let processedCount = 0; + let lastTickDuration: number | undefined; + let lastTick: number | undefined; - try { - for (const gu of batch) { + while (this.pendingUpdates.length > 0) { + const gu = this.pendingUpdates.shift() as GameUpdateViewData; processedCount++; + batch.push(gu); this.transport.turnComplete(); gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => { @@ -509,30 +511,41 @@ export class ClientGameRunner { lastTickDuration = gu.tickExecutionDuration; } lastTick = gu.tick; - } - } finally { - this.isProcessingUpdates = false; - } - if (processedCount > 0 && lastTick !== undefined) { - const combinedGu = this.mergeGameUpdates(batch); - if (combinedGu) { - this.gameView.update(combinedGu); + const elapsed = performance.now() - sliceStart; + if (processedCount >= MAX_PER_SLICE || elapsed >= SLICE_BUDGET_MS) { + break; + } } - this.renderer.tick(); - this.eventBus.emit( - new TickMetricsEvent( - lastTickDuration, - this.currentTickDelay, - this.backlogTurns, - this.renderEveryN, - ), - ); + if (processedCount > 0 && lastTick !== undefined) { + const combinedGu = this.mergeGameUpdates(batch); + if (combinedGu) { + this.gameView.update(combinedGu); + } - // Reset tick delay for next measurement - this.currentTickDelay = undefined; - } + this.renderer.tick(); + this.eventBus.emit( + new TickMetricsEvent( + lastTickDuration, + this.currentTickDelay, + this.backlogTurns, + this.renderEveryN, + ), + ); + + // Reset tick delay for next measurement + this.currentTickDelay = undefined; + } + + if (this.pendingUpdates.length > 0) { + requestAnimationFrame(processSlice); + } else { + this.isProcessingUpdates = false; + } + }; + + requestAnimationFrame(processSlice); } private adaptRenderFrequency(frameDuration: number) {