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