flawed but "working"

This commit is contained in:
scamiv
2026-02-01 17:59:45 +01:00
parent 91a50cec36
commit 042c3acbdb
6 changed files with 84 additions and 69 deletions
@@ -32,6 +32,7 @@ export class TerritoryRendererProxy {
private offscreenCanvas: OffscreenCanvas | null = null;
private worker: WorkerClient | null = null;
private ready = false;
private failed = false;
private initPromise: Promise<void> | null = null;
private pendingMessages: any[] = [];
@@ -82,7 +83,12 @@ export class TerritoryRendererProxy {
private startInit(): void {
if (this.initPromise) return;
this.initPromise = this.init();
this.initPromise = this.init().catch((err) => {
this.failed = true;
this.pendingMessages = [];
console.error("Worker territory renderer init failed:", err);
throw err;
});
}
private async init(): Promise<void> {
@@ -120,6 +126,13 @@ export class TerritoryRendererProxy {
if (message.type === "renderer_ready" && message.id === messageId) {
clearTimeout(timeout);
this.worker?.removeMessageHandler(messageId);
if (message.ok === false) {
reject(
new Error(message.error ?? "Renderer initialization failed"),
);
return;
}
this.ready = true;
// Send any pending messages
for (const msg of this.pendingMessages) {
@@ -138,6 +151,9 @@ export class TerritoryRendererProxy {
if (!this.worker) {
return;
}
if (this.failed) {
return;
}
if (!this.ready) {
this.pendingMessages.push(message);
return;
+39 -59
View File
@@ -1,5 +1,6 @@
import { Theme } from "../configuration/Config";
import { Game, UnitType } from "../game/Game";
import { TileRef } from "../game/GameMap";
import { GameUpdateViewData } from "../game/GameUpdates";
import { GameView } from "../game/GameView";
import { TerrainMapData } from "../game/TerrainMapLoader";
@@ -10,8 +11,6 @@ import { TerrainMapData } from "../game/TerrainMapLoader";
* without requiring the full GameView infrastructure.
*/
export class GameViewAdapter implements Partial<GameView> {
private tileStateCache: Uint16Array | null = null;
private terrainDataCache: Uint8Array | null = null;
private lastUpdate: GameUpdateViewData | null = null;
constructor(
@@ -26,9 +25,6 @@ export class GameViewAdapter implements Partial<GameView> {
*/
update(gu: GameUpdateViewData): void {
this.lastUpdate = gu;
// Invalidate caches when updated
this.tileStateCache = null;
this.terrainDataCache = null;
}
config() {
@@ -43,11 +39,11 @@ export class GameViewAdapter implements Partial<GameView> {
return this.game.height();
}
x(tile: bigint): number {
x(tile: TileRef): number {
return this.game.x(tile);
}
y(tile: bigint): number {
y(tile: TileRef): number {
return this.game.y(tile);
}
@@ -56,57 +52,20 @@ export class GameViewAdapter implements Partial<GameView> {
}
/**
* Build tile state view from game.
* Cached until next update.
* Return the authoritative tile state view.
*
* Important: this must be the live backing buffer, because GPU update passes
* read from it when individual tiles are marked dirty.
*/
tileStateView(): Uint16Array {
if (this.tileStateCache) {
return this.tileStateCache;
}
// Build tile state from game
const width = this.game.width();
const height = this.game.height();
const state = new Uint16Array(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const tile = this.game.ref(x, y);
const owner = this.game.owner(tile);
const ownerId = owner ? owner.smallID() : 0;
const terrain = this.game.terrain(tile);
const terrainType = terrain.type();
const terrainMag = terrain.magnitude();
// Pack state: ownerId (12 bits) | terrainType (2 bits) | terrainMag (2 bits)
state[y * width + x] =
(ownerId & 0xfff) |
((terrainType & 0x3) << 12) |
((terrainMag & 0x3) << 14);
}
}
this.tileStateCache = state;
return state;
return this.game.tileStateView();
}
/**
* Build terrain data view from game.
* Cached until next update.
* Return the immutable terrain data view.
*/
terrainDataView(): Uint8Array {
if (this.terrainDataCache) {
return this.terrainDataCache;
}
// Build terrain data from game
const width = this.game.width();
const height = this.game.height();
const terrainData = new Uint8Array(width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const tile = this.game.ref(x, y);
const terrain = this.game.terrain(tile);
terrainData[y * width + x] = terrain.type();
}
}
this.terrainDataCache = terrainData;
return terrainData;
return this.game.terrainDataView();
}
/**
@@ -114,16 +73,16 @@ export class GameViewAdapter implements Partial<GameView> {
* Computes colors from theme directly (no PlayerView needed).
*/
playerViews(): any[] {
const theme = this.game.config().theme();
return this.game.players().map((p) => {
// Get default colors from theme
const defaultTerritoryColor = theme.territoryColor(p as any);
const theme = this.theme;
return this.game.players().map((player) => {
const defaultTerritoryColor = theme.territoryColor(player as any);
const defaultBorderColor = theme.borderColor(defaultTerritoryColor);
const territoryRgb = defaultTerritoryColor.toRgb();
const borderRgb = defaultBorderColor.toRgb();
return {
smallID: () => p.smallID(),
const view = {
player,
smallID: () => player.smallID(),
territoryColor: () => ({
rgba: {
r: Math.round(territoryRgb.r),
@@ -140,7 +99,22 @@ export class GameViewAdapter implements Partial<GameView> {
a: Math.round((borderRgb.a ?? 1) * 255),
},
}),
hasEmbargo: (other: any) => {
const otherPlayer = other?.player;
if (!otherPlayer) return false;
return (
player.hasEmbargoAgainst(otherPlayer) ||
otherPlayer.hasEmbargoAgainst(player)
);
},
isFriendly: (other: any) => {
const otherPlayer = other?.player;
if (!otherPlayer) return false;
return player.isFriendly(otherPlayer);
},
};
return view;
});
}
@@ -156,10 +130,16 @@ export class GameViewAdapter implements Partial<GameView> {
/**
* Get recently updated tiles from last game update.
*/
recentlyUpdatedTiles(): bigint[] {
recentlyUpdatedTiles(): TileRef[] {
if (!this.lastUpdate) {
return [];
}
return Array.from(this.lastUpdate.packedTileUpdates);
// packedTileUpdates encode [tileRef << 16 | state] as bigint.
const packed = this.lastUpdate.packedTileUpdates;
const out: TileRef[] = new Array(packed.length);
for (let i = 0; i < packed.length; i++) {
out[i] = Number(packed[i] >> 16n);
}
return out;
}
}
+4 -1
View File
@@ -214,14 +214,17 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
sendMessage({
type: "renderer_ready",
id: message.id,
ok: true,
} as RendererReadyMessage);
} catch (error) {
console.error("Failed to initialize renderer:", error);
sendMessage({
type: "renderer_ready",
id: message.id,
ok: false,
error: error instanceof Error ? error.message : String(error),
} as RendererReadyMessage);
throw error;
renderer = null;
}
break;
+5 -1
View File
@@ -78,7 +78,11 @@ export class WorkerClient {
* Post a message to the worker with optional transferables.
*/
postMessage(message: any, transfer?: Transferable[]): void {
this.worker.postMessage(message, transfer);
if (transfer && transfer.length > 0) {
this.worker.postMessage(message, transfer);
return;
}
this.worker.postMessage(message);
}
initialize(): Promise<void> {
+2
View File
@@ -204,6 +204,8 @@ export interface RenderFrameMessage extends BaseWorkerMessage {
// Renderer messages from worker to main thread
export interface RendererReadyMessage extends BaseWorkerMessage {
type: "renderer_ready";
ok: boolean;
error?: string;
}
export interface RendererMetricsMessage extends BaseWorkerMessage {
+17 -7
View File
@@ -24,6 +24,7 @@ import { TerritoryRenderPass } from "../../client/graphics/webgpu/render/Territo
*/
export class WorkerTerritoryRenderer {
private device: WebGPUDevice | null = null;
private canvas: OffscreenCanvas | null = null;
private resources: GroundTruthData | null = null;
private gameViewAdapter: GameViewAdapter | null = null;
private ready = false;
@@ -70,6 +71,7 @@ export class WorkerTerritoryRenderer {
mapData: TerrainMapData,
theme: Theme,
): Promise<void> {
this.canvas = offscreenCanvas;
const game = gameRunner.game;
this.defensePostRange = game.config().defensePostRange();
@@ -218,10 +220,20 @@ export class WorkerTerritoryRenderer {
const nextWidth = Math.max(1, Math.floor(width));
const nextHeight = Math.max(1, Math.floor(height));
// OffscreenCanvas doesn't have width/height properties we can set
// The size is managed by the main thread canvas
let sizeChanged = true;
if (this.canvas) {
sizeChanged =
nextWidth !== this.canvas.width || nextHeight !== this.canvas.height;
if (sizeChanged) {
this.canvas.width = nextWidth;
this.canvas.height = nextHeight;
}
}
this.resources.setViewSize(nextWidth, nextHeight);
this.device.reconfigure();
if (sizeChanged) {
this.device.reconfigure();
}
if (this.postSmoothingEnabled && this.resources) {
this.resources.ensurePostSmoothingTextures(
@@ -229,7 +241,6 @@ export class WorkerTerritoryRenderer {
nextHeight,
this.device.canvasFormat,
);
this.resources.invalidateHistory();
}
}
@@ -579,9 +590,8 @@ export class WorkerTerritoryRenderer {
}
if (pass === this.territoryRenderPass && this.postSmoothingEnabled) {
if (!this.resources.getCurrentColorTexture()) {
// Use view size from resources (stored via setViewSize)
const viewWidth = (this.resources as any).viewWidth ?? 1;
const viewHeight = (this.resources as any).viewHeight ?? 1;
const viewWidth = this.canvas?.width ?? 1;
const viewHeight = this.canvas?.height ?? 1;
this.resources.ensurePostSmoothingTextures(
viewWidth,
viewHeight,