diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 54d60b1c2..53fe618d0 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -199,6 +199,11 @@ async function createClientGame( typeof SharedArrayBuffer !== "undefined" && typeof Atomics !== "undefined" && isIsolated; + const sharedStateBuffer = + canUseSharedBuffers && gameMap.sharedStateBuffer + ? gameMap.sharedStateBuffer + : undefined; + const usesSharedTileState = !!sharedStateBuffer; if (canUseSharedBuffers) { // Capacity is number of tile updates that can be queued. @@ -212,6 +217,7 @@ async function createClientGame( lobbyConfig.gameStartInfo, lobbyConfig.clientID, sharedTileRingBuffers, + sharedStateBuffer, ); await worker.initialize(); const gameView = new GameView( @@ -221,6 +227,7 @@ async function createClientGame( lobbyConfig.clientID, lobbyConfig.gameStartInfo.gameID, lobbyConfig.gameStartInfo.players, + usesSharedTileState, ); const canvas = createCanvas(); diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 34577f759..d6840e806 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -38,12 +38,14 @@ export async function createGameRunner( mapLoader: GameMapLoader, callBack: (gu: GameUpdateViewData | ErrorUpdate) => void, tileUpdateSink?: (update: bigint) => void, + sharedStateBuffer?: SharedArrayBuffer, ): Promise { const config = await getConfig(gameStart.config, null); const gameMap = await loadGameMap( gameStart.config.gameMap, gameStart.config.gameMapSize, mapLoader, + sharedStateBuffer, ); const random = new PseudoRandom(simpleHash(gameStart.gameID)); diff --git a/src/core/game/GameMap.ts b/src/core/game/GameMap.ts index 7a3bd8e6d..41368abac 100644 --- a/src/core/game/GameMap.ts +++ b/src/core/game/GameMap.ts @@ -80,6 +80,7 @@ export class GameMapImpl implements GameMap { height: number, terrainData: Uint8Array, private numLandTiles_: number, + stateBuffer?: ArrayBufferLike, ) { if (terrainData.length !== width * height) { throw new Error( @@ -89,7 +90,17 @@ export class GameMapImpl implements GameMap { this.width_ = width; this.height_ = height; this.terrain = terrainData; - this.state = new Uint16Array(width * height); + if (stateBuffer !== undefined) { + const state = new Uint16Array(stateBuffer); + if (state.length !== width * height) { + throw new Error( + `State buffer length ${state.length} doesn't match dimensions ${width}x${height}`, + ); + } + this.state = state; + } else { + this.state = new Uint16Array(width * height); + } // Precompute the LUTs let ref = 0; this.refToX = new Array(width * height); diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index d1d2fb83e..0f6387299 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -479,6 +479,7 @@ export class GameView implements GameMap { private _cosmetics: Map = new Map(); private _map: GameMap; + private readonly usesSharedTileState: boolean; constructor( public worker: WorkerClient, @@ -487,8 +488,10 @@ export class GameView implements GameMap { private _myClientID: ClientID, private _gameID: GameID, private humans: Player[], + usesSharedTileState: boolean = false, ) { this._map = this._mapData.gameMap; + this.usesSharedTileState = usesSharedTileState; this.lastUpdate = null; this.unitGrid = new UnitGrid(this._map); this._cosmetics = new Map( @@ -517,9 +520,16 @@ export class GameView implements GameMap { this.lastUpdate = gu; this.updatedTiles = []; - this.lastUpdate.packedTileUpdates.forEach((tu) => { - this.updatedTiles.push(this.updateTile(tu)); - }); + if (this.usesSharedTileState) { + this.lastUpdate.packedTileUpdates.forEach((tu) => { + const tileRef = Number(tu >> 16n); + this.updatedTiles.push(tileRef); + }); + } else { + this.lastUpdate.packedTileUpdates.forEach((tu) => { + this.updatedTiles.push(this.updateTile(tu)); + }); + } if (gu.updates === null) { throw new Error("lastUpdate.updates not initialized"); diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index e11dd7131..9b39f1137 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -6,6 +6,7 @@ export type TerrainMapData = { nations: Nation[]; gameMap: GameMap; miniGameMap: GameMap; + sharedStateBuffer?: SharedArrayBuffer; }; const loadedMaps = new Map(); @@ -35,15 +36,37 @@ export async function loadTerrainMap( map: GameMapType, mapSize: GameMapSize, terrainMapFileLoader: GameMapLoader, + sharedStateBuffer?: SharedArrayBuffer, ): Promise { - const cached = loadedMaps.get(map); - if (cached !== undefined) return cached; + const useCache = sharedStateBuffer === undefined; + if (useCache) { + const cached = loadedMaps.get(map); + if (cached !== undefined) return cached; + } const mapFiles = terrainMapFileLoader.getMapData(map); const manifest = await mapFiles.manifest(); + const stateBuffer = + sharedStateBuffer ?? + (typeof SharedArrayBuffer !== "undefined" && + typeof Atomics !== "undefined" && + // crossOriginIsolated is only defined in browser contexts + typeof (globalThis as any).crossOriginIsolated === "boolean" && + (globalThis as any).crossOriginIsolated === true + ? new SharedArrayBuffer( + manifest.map.width * + manifest.map.height * + Uint16Array.BYTES_PER_ELEMENT, + ) + : undefined); + const gameMap = mapSize === GameMapSize.Normal - ? await genTerrainFromBin(manifest.map, await mapFiles.mapBin()) + ? await genTerrainFromBin( + manifest.map, + await mapFiles.mapBin(), + stateBuffer, + ) : await genTerrainFromBin(manifest.map4x, await mapFiles.map4xBin()); const miniMap = @@ -63,18 +86,26 @@ export async function loadTerrainMap( }); } - const result = { + const result: TerrainMapData = { nations: manifest.nations, gameMap: gameMap, miniGameMap: miniMap, + sharedStateBuffer: + typeof SharedArrayBuffer !== "undefined" && + stateBuffer instanceof SharedArrayBuffer + ? stateBuffer + : undefined, }; - loadedMaps.set(map, result); + if (useCache) { + loadedMaps.set(map, result); + } return result; } export async function genTerrainFromBin( mapData: MapMetadata, data: Uint8Array, + stateBuffer?: ArrayBufferLike, ): Promise { if (data.length !== mapData.width * mapData.height) { throw new Error( @@ -87,5 +118,6 @@ export async function genTerrainFromBin( mapData.height, data, mapData.num_land_tiles, + stateBuffer, ); } diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index 3c1164849..104ebcab2 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -85,6 +85,7 @@ ctx.addEventListener("message", async (e: MessageEvent) => { sharedTileRing ? (update: bigint) => pushTileUpdate(sharedTileRing!, update) : undefined, + message.sharedStateBuffer, ).then((gr) => { sendMessage({ type: "initialized", diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index 6df22a933..4fd110732 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -24,6 +24,7 @@ export class WorkerClient { private gameStartInfo: GameStartInfo, private clientID: ClientID, private sharedTileRingBuffers?: SharedTileRingBuffers, + private sharedStateBuffer?: SharedArrayBuffer, ) { this.worker = new Worker(new URL("./Worker.worker.ts", import.meta.url)); this.messageHandlers = new Map(); @@ -74,6 +75,7 @@ export class WorkerClient { clientID: this.clientID, sharedTileRingHeader: this.sharedTileRingBuffers?.header, sharedTileRingData: this.sharedTileRingBuffers?.data, + sharedStateBuffer: this.sharedStateBuffer, }); // Add timeout for initialization diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts index 23a5ead5d..8dbb45f33 100644 --- a/src/core/worker/WorkerMessages.ts +++ b/src/core/worker/WorkerMessages.ts @@ -37,6 +37,7 @@ export interface InitMessage extends BaseWorkerMessage { clientID: ClientID; sharedTileRingHeader?: SharedArrayBuffer; sharedTileRingData?: SharedArrayBuffer; + sharedStateBuffer?: SharedArrayBuffer; } export interface TurnMessage extends BaseWorkerMessage {