mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-04 22:07:36 +00:00
Merge branch 'sab' into Atomic-SAB
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user