Use dirty flags to coalesce tile updates in SAB ring

- Extend SharedTileRing to include a shared dirtyFlags buffer alongside header and data
- Pass shared dirty buffer through WorkerClient/WorkerMessages and initialize views in Worker.worker
- In SAB mode, mark tiles dirty via Atomics.compareExchange before enqueuing to ensure each tile is queued at most once until processed
- On the main thread, clear dirty flags when draining the ring and build packedTileUpdates from distinct tile refs
- Keep non-SAB behaviour unchanged while reducing ring pressure and making overflows reflect true backlog, not duplicate updates
This commit is contained in:
scamiv
2025-12-11 17:18:00 +01:00
parent 99b01d8580
commit 430d856f6b
6 changed files with 58 additions and 8 deletions
+2
View File
@@ -7,6 +7,7 @@ export type TerrainMapData = {
gameMap: GameMap;
miniGameMap: GameMap;
sharedStateBuffer?: SharedArrayBuffer;
sharedDirtyBuffer?: SharedArrayBuffer;
};
const loadedMaps = new Map<GameMapType, TerrainMapData>();
@@ -104,6 +105,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,
};
}
+20 -3
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,19 +74,35 @@ 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,
hasDirtyBuffer: !!message.sharedDirtyBuffer,
});
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 {