Merge branch 'sab' into Atomic-SAB

This commit is contained in:
scamiv
2025-11-26 14:48:59 +01:00
7 changed files with 152 additions and 27 deletions
+60 -15
View File
@@ -31,6 +31,7 @@ import {
drainTileUpdates,
SharedTileRingBuffers,
SharedTileRingViews,
TILE_RING_HEADER_OVERFLOW,
} from "../core/worker/SharedTileRing";
import { WorkerClient } from "../core/worker/WorkerClient";
import {
@@ -561,7 +562,8 @@ export class ClientGameRunner {
batch.length > 0 &&
lastTick !== undefined
) {
const combinedGu = this.mergeGameUpdates(batch);
const { gameUpdate: combinedGu, tileMetrics } =
this.mergeGameUpdates(batch);
if (combinedGu) {
this.gameView.update(combinedGu);
}
@@ -599,6 +601,10 @@ export class ClientGameRunner {
ticksPerRender,
workerTicksPerSecond,
renderTicksPerSecond,
tileMetrics.count,
tileMetrics.utilization,
tileMetrics.overflow,
tileMetrics.drainTime,
),
);
@@ -616,9 +622,15 @@ export class ClientGameRunner {
requestAnimationFrame(processFrame);
}
private mergeGameUpdates(
batch: GameUpdateViewData[],
): GameUpdateViewData | null {
private mergeGameUpdates(batch: GameUpdateViewData[]): {
gameUpdate: GameUpdateViewData | null;
tileMetrics: {
count: number;
utilization: number;
overflow: number;
drainTime: number;
};
} {
if (batch.length === 0) {
return null;
}
@@ -644,27 +656,60 @@ export class ClientGameRunner {
}
}
let tileMetrics = {
count: 0,
utilization: 0,
overflow: 0,
drainTime: 0,
};
if (this.tileRingViews) {
const MAX_TILE_UPDATES_PER_RENDER = 100000;
const tileRefs: TileRef[] = [];
const drainStart = performance.now();
drainTileUpdates(
this.tileRingViews,
MAX_TILE_UPDATES_PER_RENDER,
combinedPackedTileUpdates,
tileRefs,
);
} else {
for (const gu of batch) {
gu.packedTileUpdates.forEach((tu) => {
combinedPackedTileUpdates.push(tu);
});
const drainTime = performance.now() - drainStart;
// Calculate ring buffer utilization and overflow
const TILE_RING_CAPACITY = 262144;
const utilization = (tileRefs.length / TILE_RING_CAPACITY) * 100;
const overflow = Atomics.load(
this.tileRingViews.header,
TILE_RING_HEADER_OVERFLOW,
);
tileMetrics = {
count: tileRefs.length,
utilization,
overflow,
drainTime,
};
for (const ref of tileRefs) {
combinedPackedTileUpdates.push(BigInt(ref));
}
} else {
// Non-SAB mode: count tile updates from batch
let totalTileUpdates = 0;
for (const gu of batch) {
totalTileUpdates += gu.packedTileUpdates.length;
}
tileMetrics.count = totalTileUpdates;
}
return {
tick: last.tick,
updates: combinedUpdates,
packedTileUpdates: new BigUint64Array(combinedPackedTileUpdates),
playerNameViewData: last.playerNameViewData,
tickExecutionDuration: last.tickExecutionDuration,
gameUpdate: {
tick: last.tick,
updates: combinedUpdates,
packedTileUpdates: new BigUint64Array(combinedPackedTileUpdates),
playerNameViewData: last.playerNameViewData,
tickExecutionDuration: last.tickExecutionDuration,
},
tileMetrics,
};
}
+5
View File
@@ -137,6 +137,11 @@ export class TickMetricsEvent implements GameEvent {
public readonly workerTicksPerSecond?: number,
// Approximate render tick() calls per second
public readonly renderTicksPerSecond?: number,
// Tile update metrics
public readonly tileUpdatesCount?: number,
public readonly ringBufferUtilization?: number,
public readonly ringBufferOverflows?: number,
public readonly ringDrainTime?: number,
) {}
}
@@ -319,6 +319,14 @@ export class PerformanceOverlay extends LitElement implements Layer {
this.layerStats.clear();
this.layerBreakdown = [];
// reset tile metrics
this.tileUpdatesPerRender = 0;
this.tileUpdatesPeak = 0;
this.ringBufferUtilization = 0;
this.ringBufferOverflows = 0;
this.ringDrainTime = 0;
this.totalTilesUpdated = 0;
this.requestUpdate();
};
@@ -437,6 +445,24 @@ export class PerformanceOverlay extends LitElement implements Layer {
@state()
private renderTicksPerSecond: number = 0;
@state()
private tileUpdatesPerRender: number = 0;
@state()
private tileUpdatesPeak: number = 0;
@state()
private ringBufferUtilization: number = 0;
@state()
private ringBufferOverflows: number = 0;
@state()
private ringDrainTime: number = 0;
@state()
private totalTilesUpdated: number = 0;
updateTickMetrics(
tickExecutionDuration?: number,
tickDelay?: number,
@@ -444,6 +470,10 @@ export class PerformanceOverlay extends LitElement implements Layer {
ticksPerRender?: number,
workerTicksPerSecond?: number,
renderTicksPerSecond?: number,
tileUpdatesCount?: number,
ringBufferUtilization?: number,
ringBufferOverflows?: number,
ringDrainTime?: number,
) {
if (!this.isVisible || !this.userSettings.performanceOverlay()) return;
@@ -497,6 +527,26 @@ export class PerformanceOverlay extends LitElement implements Layer {
this.renderTicksPerSecond = renderTicksPerSecond;
}
if (tileUpdatesCount !== undefined) {
this.tileUpdatesPerRender = tileUpdatesCount;
this.tileUpdatesPeak = Math.max(this.tileUpdatesPeak, tileUpdatesCount);
this.totalTilesUpdated += tileUpdatesCount;
}
if (ringBufferUtilization !== undefined) {
this.ringBufferUtilization =
Math.round(ringBufferUtilization * 100) / 100;
}
if (ringBufferOverflows !== undefined) {
// Accumulate overflows (overflows is a flag, so add 1 if set)
this.ringBufferOverflows += ringBufferOverflows;
}
if (ringDrainTime !== undefined) {
this.ringDrainTime = Math.round(ringDrainTime * 100) / 100;
}
this.requestUpdate();
}
@@ -527,6 +577,14 @@ export class PerformanceOverlay extends LitElement implements Layer {
executionSamples: [...this.tickExecutionTimes],
delaySamples: [...this.tickDelayTimes],
},
tiles: {
updatesPerRender: this.tileUpdatesPerRender,
peakUpdates: this.tileUpdatesPeak,
ringBufferUtilization: this.ringBufferUtilization,
ringBufferOverflows: this.ringBufferOverflows,
ringDrainTimeMs: this.ringDrainTime,
totalTilesUpdated: this.totalTilesUpdated,
},
layers: this.layerBreakdown.map((layer) => ({ ...layer })),
};
}
@@ -658,6 +716,21 @@ export class PerformanceOverlay extends LitElement implements Layer {
Backlog turns:
<span>${this.backlogTurns}</span>
</div>
<div class="performance-line">
Tile updates/render:
<span>${this.tileUpdatesPerRender}</span>
(peak: <span>${this.tileUpdatesPeak}</span>)
</div>
<div class="performance-line">
Ring buffer:
<span>${this.ringBufferUtilization}%</span>
(${this.totalTilesUpdated} total, ${this.ringBufferOverflows}
overflows)
</div>
<div class="performance-line">
Ring drain time:
<span>${this.ringDrainTime.toFixed(2)}ms</span>
</div>
${this.layerBreakdown.length
? html`<div class="layers-section">
<div class="performance-line">
+4 -3
View File
@@ -37,7 +37,7 @@ export async function createGameRunner(
clientID: ClientID,
mapLoader: GameMapLoader,
callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
tileUpdateSink?: (update: bigint) => void,
tileUpdateSink?: (tile: TileRef) => void,
sharedStateBuffer?: SharedArrayBuffer,
): Promise<GameRunner> {
const config = await getConfig(gameStart.config, null);
@@ -106,7 +106,7 @@ export class GameRunner {
public game: Game,
private execManager: Executor,
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
private tileUpdateSink?: (update: bigint) => void,
private tileUpdateSink?: (tile: TileRef) => void,
) {}
init() {
@@ -187,7 +187,8 @@ export class GameRunner {
const tileUpdates = updates[GameUpdateType.Tile];
if (this.tileUpdateSink !== undefined) {
for (const u of tileUpdates) {
this.tileUpdateSink(u.update);
const tileRef = Number(u.update >> 16n) as TileRef;
this.tileUpdateSink(tileRef);
}
packedTileUpdates = new BigUint64Array();
} else {
+1 -1
View File
@@ -515,7 +515,7 @@ export class GameView implements GameMap {
this.updatedTiles = [];
if (this.usesSharedTileState) {
this.lastUpdate.packedTileUpdates.forEach((tu) => {
const tileRef = Number(tu >> 16n);
const tileRef = Number(tu);
this.updatedTiles.push(tileRef);
});
} else {
+7 -7
View File
@@ -1,3 +1,5 @@
import { TileRef } from "../game/GameMap";
export interface SharedTileRingBuffers {
header: SharedArrayBuffer;
data: SharedArrayBuffer;
@@ -5,7 +7,7 @@ export interface SharedTileRingBuffers {
export interface SharedTileRingViews {
header: Int32Array;
buffer: BigUint64Array;
buffer: Uint32Array;
capacity: number;
}
@@ -18,9 +20,7 @@ export function createSharedTileRingBuffers(
capacity: number,
): SharedTileRingBuffers {
const header = new SharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT);
const data = new SharedArrayBuffer(
capacity * BigUint64Array.BYTES_PER_ELEMENT,
);
const data = new SharedArrayBuffer(capacity * Uint32Array.BYTES_PER_ELEMENT);
return { header, data };
}
@@ -28,7 +28,7 @@ export function createSharedTileRingViews(
buffers: SharedTileRingBuffers,
): SharedTileRingViews {
const header = new Int32Array(buffers.header);
const buffer = new BigUint64Array(buffers.data);
const buffer = new Uint32Array(buffers.data);
return {
header,
buffer,
@@ -38,7 +38,7 @@ export function createSharedTileRingViews(
export function pushTileUpdate(
views: SharedTileRingViews,
value: bigint,
value: TileRef,
): void {
const { header, buffer, capacity } = views;
@@ -60,7 +60,7 @@ export function pushTileUpdate(
export function drainTileUpdates(
views: SharedTileRingViews,
maxItems: number,
out: bigint[],
out: TileRef[],
): void {
const { header, buffer, capacity } = views;
+2 -1
View File
@@ -1,6 +1,7 @@
import version from "../../../resources/version.txt";
import { createGameRunner, GameRunner } from "../GameRunner";
import { FetchGameMapLoader } from "../game/FetchGameMapLoader";
import { TileRef } from "../game/GameMap";
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
import {
createSharedTileRingViews,
@@ -83,7 +84,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
mapLoader,
gameUpdate,
sharedTileRing
? (update: bigint) => pushTileUpdate(sharedTileRing!, update)
? (tile: TileRef) => pushTileUpdate(sharedTileRing!, tile)
: undefined,
message.sharedStateBuffer,
).then((gr) => {