mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 19:46:43 +00:00
Use SharedArrayBuffer tile state and ring buffer for worker updates
- Share GameMapImpl tile state between worker and main via SharedArrayBuffer - Add SAB-backed tile update ring buffer to stream tile changes instead of postMessage payloads - Wire shared state/ring through WorkerClient, Worker.worker, GameRunner, and ClientGameRunner - Update GameView to skip updateTile when shared state is enabled and consume tile refs from the ring
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -38,12 +38,14 @@ export async function createGameRunner(
|
||||
mapLoader: GameMapLoader,
|
||||
callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
|
||||
tileUpdateSink?: (update: bigint) => void,
|
||||
sharedStateBuffer?: SharedArrayBuffer,
|
||||
): Promise<GameRunner> {
|
||||
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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -479,6 +479,7 @@ export class GameView implements GameMap {
|
||||
private _cosmetics: Map<string, PlayerCosmetics> = 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");
|
||||
|
||||
@@ -6,6 +6,7 @@ export type TerrainMapData = {
|
||||
nations: Nation[];
|
||||
gameMap: GameMap;
|
||||
miniGameMap: GameMap;
|
||||
sharedStateBuffer?: SharedArrayBuffer;
|
||||
};
|
||||
|
||||
const loadedMaps = new Map<GameMapType, TerrainMapData>();
|
||||
@@ -35,15 +36,37 @@ export async function loadTerrainMap(
|
||||
map: GameMapType,
|
||||
mapSize: GameMapSize,
|
||||
terrainMapFileLoader: GameMapLoader,
|
||||
sharedStateBuffer?: SharedArrayBuffer,
|
||||
): Promise<TerrainMapData> {
|
||||
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<GameMap> {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
|
||||
sharedTileRing
|
||||
? (update: bigint) => pushTileUpdate(sharedTileRing!, update)
|
||||
: undefined,
|
||||
message.sharedStateBuffer,
|
||||
).then((gr) => {
|
||||
sendMessage({
|
||||
type: "initialized",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface InitMessage extends BaseWorkerMessage {
|
||||
clientID: ClientID;
|
||||
sharedTileRingHeader?: SharedArrayBuffer;
|
||||
sharedTileRingData?: SharedArrayBuffer;
|
||||
sharedStateBuffer?: SharedArrayBuffer;
|
||||
}
|
||||
|
||||
export interface TurnMessage extends BaseWorkerMessage {
|
||||
|
||||
Reference in New Issue
Block a user