Merge branch 'sab' into Atomic-SAB

This commit is contained in:
scamiv
2025-11-26 17:47:35 +01:00
7 changed files with 51 additions and 26 deletions
+26 -11
View File
@@ -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<TileRef>();
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 {
-5
View File
@@ -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);
+2
View File
@@ -7,6 +7,7 @@ export type TerrainMapData = {
gameMap: GameMap;
miniGameMap: GameMap;
sharedStateBuffer?: SharedArrayBuffer;
sharedDirtyBuffer?: SharedArrayBuffer;
};
const loadedMaps = new Map<GameMapType, TerrainMapData>();
@@ -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);
+7 -1
View File
@@ -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,
};
}
+13 -9
View File
@@ -24,6 +24,7 @@ let gameRunner: Promise<GameRunner> | 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<MainThreadMessage>) => {
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({
+2
View File
@@ -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
+1
View File
@@ -38,6 +38,7 @@ export interface InitMessage extends BaseWorkerMessage {
sharedTileRingHeader?: SharedArrayBuffer;
sharedTileRingData?: SharedArrayBuffer;
sharedStateBuffer?: SharedArrayBuffer;
sharedDirtyBuffer?: SharedArrayBuffer;
}
export interface TurnMessage extends BaseWorkerMessage {