From 81592226121189c3cb9ee993ed5d224f5f0af870 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:36:11 +0100 Subject: [PATCH 1/2] Change the ring buffer to Uint32Array Store only TileRef instead of packed tile+state values --- src/client/ClientGameRunner.ts | 6 +++++- src/core/GameRunner.ts | 7 ++++--- src/core/game/GameView.ts | 2 +- src/core/worker/SharedTileRing.ts | 14 +++++++------- src/core/worker/Worker.worker.ts | 3 ++- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 5b2e0960e..57dd751c6 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -646,11 +646,15 @@ export class ClientGameRunner { if (this.tileRingViews) { const MAX_TILE_UPDATES_PER_RENDER = 100000; + const tileRefs: TileRef[] = []; drainTileUpdates( this.tileRingViews, MAX_TILE_UPDATES_PER_RENDER, - combinedPackedTileUpdates, + tileRefs, ); + for (const ref of tileRefs) { + combinedPackedTileUpdates.push(BigInt(ref)); + } } else { for (const gu of batch) { gu.packedTileUpdates.forEach((tu) => { diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 62afa3646..eddb1223d 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -37,7 +37,7 @@ export async function createGameRunner( clientID: ClientID, mapLoader: GameMapLoader, callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, - tileUpdateSink?: (update: bigint) => void, + tileUpdateSink?: (tile: TileRef) => void, sharedStateBuffer?: SharedArrayBuffer, ): Promise { const config = await getConfig(gameStart.config, null); @@ -101,7 +101,7 @@ export class GameRunner { public game: Game, private execManager: Executor, private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, - private tileUpdateSink?: (update: bigint) => void, + private tileUpdateSink?: (tile: TileRef) => void, ) {} init() { @@ -182,7 +182,8 @@ export class GameRunner { const tileUpdates = updates[GameUpdateType.Tile]; if (this.tileUpdateSink !== undefined) { for (const u of tileUpdates) { - this.tileUpdateSink(u.update); + const tileRef = Number(u.update >> 16n) as TileRef; + this.tileUpdateSink(tileRef); } packedTileUpdates = new BigUint64Array(); } else { diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 6f306e6c7..a6188b532 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -515,7 +515,7 @@ export class GameView implements GameMap { this.updatedTiles = []; if (this.usesSharedTileState) { this.lastUpdate.packedTileUpdates.forEach((tu) => { - const tileRef = Number(tu >> 16n); + const tileRef = Number(tu); this.updatedTiles.push(tileRef); }); } else { diff --git a/src/core/worker/SharedTileRing.ts b/src/core/worker/SharedTileRing.ts index 0d8d0c331..4ddf9403d 100644 --- a/src/core/worker/SharedTileRing.ts +++ b/src/core/worker/SharedTileRing.ts @@ -1,3 +1,5 @@ +import { TileRef } from "../game/GameMap"; + export interface SharedTileRingBuffers { header: SharedArrayBuffer; data: SharedArrayBuffer; @@ -5,7 +7,7 @@ export interface SharedTileRingBuffers { export interface SharedTileRingViews { header: Int32Array; - buffer: BigUint64Array; + buffer: Uint32Array; capacity: number; } @@ -18,9 +20,7 @@ export function createSharedTileRingBuffers( capacity: number, ): SharedTileRingBuffers { const header = new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT); - const data = new SharedArrayBuffer( - capacity * BigUint64Array.BYTES_PER_ELEMENT, - ); + const data = new SharedArrayBuffer(capacity * Uint32Array.BYTES_PER_ELEMENT); return { header, data }; } @@ -28,7 +28,7 @@ export function createSharedTileRingViews( buffers: SharedTileRingBuffers, ): SharedTileRingViews { const header = new Int32Array(buffers.header); - const buffer = new BigUint64Array(buffers.data); + const buffer = new Uint32Array(buffers.data); return { header, buffer, @@ -38,7 +38,7 @@ export function createSharedTileRingViews( export function pushTileUpdate( views: SharedTileRingViews, - value: bigint, + value: TileRef, ): void { const { header, buffer, capacity } = views; @@ -60,7 +60,7 @@ export function pushTileUpdate( export function drainTileUpdates( views: SharedTileRingViews, maxItems: number, - out: bigint[], + out: TileRef[], ): void { const { header, buffer, capacity } = views; diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index 104ebcab2..586f77e66 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -1,6 +1,7 @@ import version from "../../../resources/version.txt"; import { createGameRunner, GameRunner } from "../GameRunner"; import { FetchGameMapLoader } from "../game/FetchGameMapLoader"; +import { TileRef } from "../game/GameMap"; import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates"; import { createSharedTileRingViews, @@ -83,7 +84,7 @@ ctx.addEventListener("message", async (e: MessageEvent) => { mapLoader, gameUpdate, sharedTileRing - ? (update: bigint) => pushTileUpdate(sharedTileRing!, update) + ? (tile: TileRef) => pushTileUpdate(sharedTileRing!, tile) : undefined, message.sharedStateBuffer, ).then((gr) => { From ff0533fd1b4da3d725760476beef4ccbd67f5de1 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:46:05 +0100 Subject: [PATCH 2/2] add more stats to perf overlay --- src/client/ClientGameRunner.ts | 65 ++++++++++++++--- src/client/InputHandler.ts | 5 ++ .../graphics/layers/PerformanceOverlay.ts | 73 +++++++++++++++++++ 3 files changed, 131 insertions(+), 12 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 57dd751c6..da4cd22e6 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -31,6 +31,7 @@ import { drainTileUpdates, SharedTileRingBuffers, SharedTileRingViews, + TILE_RING_HEADER_OVERFLOW, } from "../core/worker/SharedTileRing"; import { WorkerClient } from "../core/worker/WorkerClient"; import { @@ -561,7 +562,8 @@ export class ClientGameRunner { batch.length > 0 && lastTick !== undefined ) { - const combinedGu = this.mergeGameUpdates(batch); + const { gameUpdate: combinedGu, tileMetrics } = + this.mergeGameUpdates(batch); if (combinedGu) { this.gameView.update(combinedGu); } @@ -599,6 +601,10 @@ export class ClientGameRunner { ticksPerRender, workerTicksPerSecond, renderTicksPerSecond, + tileMetrics.count, + tileMetrics.utilization, + tileMetrics.overflow, + tileMetrics.drainTime, ), ); @@ -616,9 +622,15 @@ export class ClientGameRunner { requestAnimationFrame(processFrame); } - private mergeGameUpdates( - batch: GameUpdateViewData[], - ): GameUpdateViewData | null { + private mergeGameUpdates(batch: GameUpdateViewData[]): { + gameUpdate: GameUpdateViewData | null; + tileMetrics: { + count: number; + utilization: number; + overflow: number; + drainTime: number; + }; + } { if (batch.length === 0) { return null; } @@ -644,31 +656,60 @@ export class ClientGameRunner { } } + let tileMetrics = { + count: 0, + utilization: 0, + overflow: 0, + drainTime: 0, + }; + if (this.tileRingViews) { const MAX_TILE_UPDATES_PER_RENDER = 100000; const tileRefs: TileRef[] = []; + const drainStart = performance.now(); drainTileUpdates( this.tileRingViews, MAX_TILE_UPDATES_PER_RENDER, tileRefs, ); + const drainTime = performance.now() - drainStart; + + // Calculate ring buffer utilization and overflow + const TILE_RING_CAPACITY = 262144; + const utilization = (tileRefs.length / TILE_RING_CAPACITY) * 100; + const overflow = Atomics.load( + this.tileRingViews.header, + TILE_RING_HEADER_OVERFLOW, + ); + + tileMetrics = { + count: tileRefs.length, + utilization, + overflow, + drainTime, + }; + for (const ref of tileRefs) { combinedPackedTileUpdates.push(BigInt(ref)); } } else { + // Non-SAB mode: count tile updates from batch + let totalTileUpdates = 0; for (const gu of batch) { - gu.packedTileUpdates.forEach((tu) => { - combinedPackedTileUpdates.push(tu); - }); + totalTileUpdates += gu.packedTileUpdates.length; } + tileMetrics.count = totalTileUpdates; } return { - tick: last.tick, - updates: combinedUpdates, - packedTileUpdates: new BigUint64Array(combinedPackedTileUpdates), - playerNameViewData: last.playerNameViewData, - tickExecutionDuration: last.tickExecutionDuration, + gameUpdate: { + tick: last.tick, + updates: combinedUpdates, + packedTileUpdates: new BigUint64Array(combinedPackedTileUpdates), + playerNameViewData: last.playerNameViewData, + tickExecutionDuration: last.tickExecutionDuration, + }, + tileMetrics, }; } diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index dbae066ee..bf2510e4d 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -137,6 +137,11 @@ export class TickMetricsEvent implements GameEvent { public readonly workerTicksPerSecond?: number, // Approximate render tick() calls per second public readonly renderTicksPerSecond?: number, + // Tile update metrics + public readonly tileUpdatesCount?: number, + public readonly ringBufferUtilization?: number, + public readonly ringBufferOverflows?: number, + public readonly ringDrainTime?: number, ) {} } diff --git a/src/client/graphics/layers/PerformanceOverlay.ts b/src/client/graphics/layers/PerformanceOverlay.ts index 64499024f..6c1de8cd7 100644 --- a/src/client/graphics/layers/PerformanceOverlay.ts +++ b/src/client/graphics/layers/PerformanceOverlay.ts @@ -319,6 +319,14 @@ export class PerformanceOverlay extends LitElement implements Layer { this.layerStats.clear(); this.layerBreakdown = []; + // reset tile metrics + this.tileUpdatesPerRender = 0; + this.tileUpdatesPeak = 0; + this.ringBufferUtilization = 0; + this.ringBufferOverflows = 0; + this.ringDrainTime = 0; + this.totalTilesUpdated = 0; + this.requestUpdate(); }; @@ -437,6 +445,24 @@ export class PerformanceOverlay extends LitElement implements Layer { @state() private renderTicksPerSecond: number = 0; + @state() + private tileUpdatesPerRender: number = 0; + + @state() + private tileUpdatesPeak: number = 0; + + @state() + private ringBufferUtilization: number = 0; + + @state() + private ringBufferOverflows: number = 0; + + @state() + private ringDrainTime: number = 0; + + @state() + private totalTilesUpdated: number = 0; + updateTickMetrics( tickExecutionDuration?: number, tickDelay?: number, @@ -444,6 +470,10 @@ export class PerformanceOverlay extends LitElement implements Layer { ticksPerRender?: number, workerTicksPerSecond?: number, renderTicksPerSecond?: number, + tileUpdatesCount?: number, + ringBufferUtilization?: number, + ringBufferOverflows?: number, + ringDrainTime?: number, ) { if (!this.isVisible || !this.userSettings.performanceOverlay()) return; @@ -497,6 +527,26 @@ export class PerformanceOverlay extends LitElement implements Layer { this.renderTicksPerSecond = renderTicksPerSecond; } + if (tileUpdatesCount !== undefined) { + this.tileUpdatesPerRender = tileUpdatesCount; + this.tileUpdatesPeak = Math.max(this.tileUpdatesPeak, tileUpdatesCount); + this.totalTilesUpdated += tileUpdatesCount; + } + + if (ringBufferUtilization !== undefined) { + this.ringBufferUtilization = + Math.round(ringBufferUtilization * 100) / 100; + } + + if (ringBufferOverflows !== undefined) { + // Accumulate overflows (overflows is a flag, so add 1 if set) + this.ringBufferOverflows += ringBufferOverflows; + } + + if (ringDrainTime !== undefined) { + this.ringDrainTime = Math.round(ringDrainTime * 100) / 100; + } + this.requestUpdate(); } @@ -527,6 +577,14 @@ export class PerformanceOverlay extends LitElement implements Layer { executionSamples: [...this.tickExecutionTimes], delaySamples: [...this.tickDelayTimes], }, + tiles: { + updatesPerRender: this.tileUpdatesPerRender, + peakUpdates: this.tileUpdatesPeak, + ringBufferUtilization: this.ringBufferUtilization, + ringBufferOverflows: this.ringBufferOverflows, + ringDrainTimeMs: this.ringDrainTime, + totalTilesUpdated: this.totalTilesUpdated, + }, layers: this.layerBreakdown.map((layer) => ({ ...layer })), }; } @@ -658,6 +716,21 @@ export class PerformanceOverlay extends LitElement implements Layer { Backlog turns: ${this.backlogTurns} +
+ Tile updates/render: + ${this.tileUpdatesPerRender} + (peak: ${this.tileUpdatesPeak}) +
+
+ Ring buffer: + ${this.ringBufferUtilization}% + (${this.totalTilesUpdated} total, ${this.ringBufferOverflows} + overflows) +
+
+ Ring drain time: + ${this.ringDrainTime.toFixed(2)}ms +
${this.layerBreakdown.length ? html`