diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index cd56677b4..5cf654a2c 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -173,6 +173,8 @@ async function createClientGame( let sharedTileRingBuffers: SharedTileRingBuffers | undefined; let sharedTileRingViews: SharedTileRingViews | null = null; + let sharedDirtyBuffer: SharedArrayBuffer | undefined; + let sharedDirtyFlags: Uint8Array | null = null; const isIsolated = typeof (globalThis as any).crossOriginIsolated === "boolean" ? (globalThis as any).crossOriginIsolated === true @@ -198,8 +200,14 @@ async function createClientGame( // Capacity is number of tile updates that can be queued. // This is a compromise between memory usage and backlog tolerance. const TILE_RING_CAPACITY = 262144; - sharedTileRingBuffers = createSharedTileRingBuffers(TILE_RING_CAPACITY); + const numTiles = gameMap.gameMap.width() * gameMap.gameMap.height(); + sharedTileRingBuffers = createSharedTileRingBuffers( + TILE_RING_CAPACITY, + numTiles, + ); sharedTileRingViews = createSharedTileRingViews(sharedTileRingBuffers); + sharedDirtyBuffer = sharedTileRingBuffers.dirty; + sharedDirtyFlags = sharedTileRingViews.dirtyFlags; } const worker = new WorkerClient( @@ -207,6 +215,7 @@ async function createClientGame( lobbyConfig.clientID, sharedTileRingBuffers, sharedStateBuffer, + sharedDirtyBuffer, ); await worker.initialize(); const gameView = new GameView( @@ -235,6 +244,7 @@ async function createClientGame( worker, gameView, sharedTileRingViews, + sharedDirtyFlags, ); } @@ -267,6 +277,7 @@ export class ClientGameRunner { private pendingStart = 0; private isProcessingUpdates = false; private tileRingViews: SharedTileRingViews | null; + private dirtyFlags: Uint8Array | null; constructor( private lobby: LobbyConfig, @@ -277,9 +288,11 @@ export class ClientGameRunner { private worker: WorkerClient, private gameView: GameView, tileRingViews: SharedTileRingViews | null, + dirtyFlags: Uint8Array | null, ) { this.lastMessageTime = Date.now(); this.tileRingViews = tileRingViews; + this.dirtyFlags = dirtyFlags; } private saveGame(update: WinUpdate) { @@ -689,29 +702,31 @@ export class ClientGameRunner { ); const drainTime = performance.now() - drainStart; + // Deduplicate tile refs for this render slice + const uniqueTiles = new Set(); + for (const ref of tileRefs) { + uniqueTiles.add(ref); + } + // Calculate ring buffer utilization and overflow const TILE_RING_CAPACITY = 262144; - const utilization = (tileRefs.length / TILE_RING_CAPACITY) * 100; + const utilization = (uniqueTiles.size / TILE_RING_CAPACITY) * 100; const overflow = Atomics.load( this.tileRingViews.header, TILE_RING_HEADER_OVERFLOW, ); tileMetrics = { - count: tileRefs.length, + count: uniqueTiles.size, utilization, overflow, drainTime, }; - console.log("[ClientGameRunner] mergeGameUpdates SAB", { - tileCount: tileRefs.length, - utilization, - overflow, - drainTime, - }); - - for (const ref of tileRefs) { + for (const ref of uniqueTiles) { + if (this.dirtyFlags) { + Atomics.store(this.dirtyFlags, ref, 0); + } combinedPackedTileUpdates.push(BigInt(ref)); } } else { diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 8908e8927..1b43545c4 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -186,11 +186,6 @@ export class GameRunner { let packedTileUpdates: BigUint64Array; const tileUpdates = updates[GameUpdateType.Tile]; if (this.tileUpdateSink !== undefined) { - if (tileUpdates.length > 0) { - console.log("[GameRunner] tile updates for tick", this.game.ticks(), { - count: tileUpdates.length, - }); - } for (const u of tileUpdates) { const tileRef = Number(u.update >> 16n) as TileRef; this.tileUpdateSink(tileRef); diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index 3976aa7ef..820a49f4d 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -7,6 +7,7 @@ export type TerrainMapData = { gameMap: GameMap; miniGameMap: GameMap; sharedStateBuffer?: SharedArrayBuffer; + sharedDirtyBuffer?: SharedArrayBuffer; }; const loadedMaps = new Map(); @@ -112,6 +113,7 @@ export async function loadTerrainMap( stateBuffer instanceof SharedArrayBuffer ? stateBuffer : undefined, + sharedDirtyBuffer: undefined, // populated by consumer when needed }; // Always cache the result, but only use cache when appropriate loadedMaps.set(map, result); diff --git a/src/core/worker/SharedTileRing.ts b/src/core/worker/SharedTileRing.ts index 4ddf9403d..328def730 100644 --- a/src/core/worker/SharedTileRing.ts +++ b/src/core/worker/SharedTileRing.ts @@ -3,11 +3,13 @@ import { TileRef } from "../game/GameMap"; export interface SharedTileRingBuffers { header: SharedArrayBuffer; data: SharedArrayBuffer; + dirty: SharedArrayBuffer; } export interface SharedTileRingViews { header: Int32Array; buffer: Uint32Array; + dirtyFlags: Uint8Array; capacity: number; } @@ -18,10 +20,12 @@ export const TILE_RING_HEADER_OVERFLOW = 2; export function createSharedTileRingBuffers( capacity: number, + numTiles: number, ): SharedTileRingBuffers { const header = new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT); const data = new SharedArrayBuffer(capacity * Uint32Array.BYTES_PER_ELEMENT); - return { header, data }; + const dirty = new SharedArrayBuffer(numTiles * Uint8Array.BYTES_PER_ELEMENT); + return { header, data, dirty }; } export function createSharedTileRingViews( @@ -29,9 +33,11 @@ export function createSharedTileRingViews( ): SharedTileRingViews { const header = new Int32Array(buffers.header); const buffer = new Uint32Array(buffers.data); + const dirtyFlags = new Uint8Array(buffers.dirty); return { header, buffer, + dirtyFlags, capacity: buffer.length, }; } diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index 81e58b12b..aae5a69ef 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -24,6 +24,7 @@ let gameRunner: Promise | null = null; const mapLoader = new FetchGameMapLoader(`/maps`, version); let isProcessingTurns = false; let sharedTileRing: SharedTileRingViews | null = null; +let dirtyFlags: Uint8Array | null = null; function gameUpdate(gu: GameUpdateViewData | ErrorUpdate) { // skip if ErrorUpdate @@ -73,25 +74,28 @@ ctx.addEventListener("message", async (e: MessageEvent) => { sharedTileRing = createSharedTileRingViews({ header: message.sharedTileRingHeader, data: message.sharedTileRingData, + dirty: message.sharedDirtyBuffer!, }); + dirtyFlags = sharedTileRing.dirtyFlags; } else { sharedTileRing = null; + dirtyFlags = null; } - console.log("[Worker.worker] init", { - hasSharedStateBuffer: !!message.sharedStateBuffer, - hasRingHeader: !!message.sharedTileRingHeader, - hasRingData: !!message.sharedTileRingData, - }); - gameRunner = createGameRunner( message.gameStartInfo, message.clientID, mapLoader, gameUpdate, - sharedTileRing - ? (tile: TileRef) => pushTileUpdate(sharedTileRing!, tile) - : undefined, + sharedTileRing && dirtyFlags + ? (tile: TileRef) => { + if (Atomics.compareExchange(dirtyFlags!, tile, 0, 1) === 0) { + pushTileUpdate(sharedTileRing!, tile); + } + } + : sharedTileRing + ? (tile: TileRef) => pushTileUpdate(sharedTileRing!, tile) + : undefined, message.sharedStateBuffer, ).then((gr) => { sendMessage({ diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index 4fd110732..1d824546d 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -25,6 +25,7 @@ export class WorkerClient { private clientID: ClientID, private sharedTileRingBuffers?: SharedTileRingBuffers, private sharedStateBuffer?: SharedArrayBuffer, + private sharedDirtyBuffer?: SharedArrayBuffer, ) { this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url)); this.messageHandlers = new Map(); @@ -76,6 +77,7 @@ export class WorkerClient { sharedTileRingHeader: this.sharedTileRingBuffers?.header, sharedTileRingData: this.sharedTileRingBuffers?.data, sharedStateBuffer: this.sharedStateBuffer, + sharedDirtyBuffer: this.sharedDirtyBuffer, }); // Add timeout for initialization diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts index 8dbb45f33..c6b811418 100644 --- a/src/core/worker/WorkerMessages.ts +++ b/src/core/worker/WorkerMessages.ts @@ -38,6 +38,7 @@ export interface InitMessage extends BaseWorkerMessage { sharedTileRingHeader?: SharedArrayBuffer; sharedTileRingData?: SharedArrayBuffer; sharedStateBuffer?: SharedArrayBuffer; + sharedDirtyBuffer?: SharedArrayBuffer; } export interface TurnMessage extends BaseWorkerMessage {