mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 14:50:44 +00:00
flawed
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
readTerritoryShaderId,
|
||||
} from "../webgpu/render/TerritoryShaderRegistry";
|
||||
import { TerritoryRenderer } from "../webgpu/TerritoryRenderer";
|
||||
import { TerritoryRendererProxy } from "../webgpu/TerritoryRendererProxy";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class TerritoryLayer implements Layer {
|
||||
@@ -42,7 +43,8 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
private theme: Theme;
|
||||
|
||||
private territoryRenderer: TerritoryRenderer | null = null;
|
||||
private territoryRenderer: TerritoryRenderer | TerritoryRendererProxy | null =
|
||||
null;
|
||||
private alternativeView = false;
|
||||
|
||||
private lastPaletteSignature: string | null = null;
|
||||
@@ -119,9 +121,11 @@ export class TerritoryLayer implements Layer {
|
||||
}
|
||||
|
||||
private configureRenderer() {
|
||||
const { renderer, reason } = TerritoryRenderer.create(
|
||||
// Use proxy to render in worker thread
|
||||
const { renderer, reason } = TerritoryRendererProxy.create(
|
||||
this.game,
|
||||
this.theme,
|
||||
this.game.worker,
|
||||
);
|
||||
if (!renderer) {
|
||||
throw new Error(reason ?? "WebGPU is required for territory rendering.");
|
||||
@@ -189,6 +193,11 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
const canvas = this.territoryRenderer.canvas;
|
||||
|
||||
// Canvas must be HTMLCanvasElement for DOM operations (proxy always provides this)
|
||||
if (!(canvas instanceof HTMLCanvasElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the renderer recreated its canvas, detach the old one.
|
||||
if (this.attachedTerritoryCanvas !== canvas) {
|
||||
this.attachedTerritoryCanvas?.remove();
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
import { createCanvas } from "src/client/Utils";
|
||||
import { Theme } from "../../../core/configuration/Config";
|
||||
import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { WorkerClient } from "../../../core/worker/WorkerClient";
|
||||
import {
|
||||
InitRendererMessage,
|
||||
MarkAllDirtyMessage,
|
||||
MarkTileMessage,
|
||||
RefreshPaletteMessage,
|
||||
RefreshTerrainMessage,
|
||||
RenderFrameMessage,
|
||||
SetAlternativeViewMessage,
|
||||
SetHighlightedOwnerMessage,
|
||||
SetShaderSettingsMessage,
|
||||
SetViewSizeMessage,
|
||||
SetViewTransformMessage,
|
||||
TickRendererMessage,
|
||||
} from "../../../core/worker/WorkerMessages";
|
||||
|
||||
export interface TerritoryWebGLCreateResult {
|
||||
renderer: TerritoryRendererProxy | null;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy for TerritoryRenderer that forwards calls to worker thread.
|
||||
* Manages canvas transfer and message routing.
|
||||
*/
|
||||
export class TerritoryRendererProxy {
|
||||
public readonly canvas: HTMLCanvasElement;
|
||||
private offscreenCanvas: OffscreenCanvas | null = null;
|
||||
private worker: WorkerClient | null = null;
|
||||
private ready = false;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private pendingMessages: any[] = [];
|
||||
|
||||
private constructor(
|
||||
private readonly game: GameView,
|
||||
private readonly theme: Theme,
|
||||
) {
|
||||
this.canvas = createCanvas();
|
||||
this.canvas.style.pointerEvents = "none";
|
||||
this.canvas.width = 1;
|
||||
this.canvas.height = 1;
|
||||
}
|
||||
|
||||
static create(
|
||||
game: GameView,
|
||||
theme: Theme,
|
||||
worker: WorkerClient,
|
||||
): TerritoryWebGLCreateResult {
|
||||
const nav = globalThis.navigator as any;
|
||||
if (!nav?.gpu || typeof nav.gpu.requestAdapter !== "function") {
|
||||
return {
|
||||
renderer: null,
|
||||
reason: "WebGPU not available; GPU renderer disabled.",
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof OffscreenCanvas === "undefined") {
|
||||
return {
|
||||
renderer: null,
|
||||
reason: "OffscreenCanvas not supported; GPU renderer disabled.",
|
||||
};
|
||||
}
|
||||
|
||||
const state = game.tileStateView();
|
||||
const expected = game.width() * game.height();
|
||||
if (state.length !== expected) {
|
||||
return {
|
||||
renderer: null,
|
||||
reason: "Tile state buffer size mismatch; GPU renderer disabled.",
|
||||
};
|
||||
}
|
||||
|
||||
const renderer = new TerritoryRendererProxy(game, theme);
|
||||
renderer.worker = worker;
|
||||
renderer.startInit();
|
||||
return { renderer };
|
||||
}
|
||||
|
||||
private startInit(): void {
|
||||
if (this.initPromise) return;
|
||||
this.initPromise = this.init();
|
||||
}
|
||||
|
||||
private async init(): Promise<void> {
|
||||
if (!this.worker) {
|
||||
throw new Error("Worker not set");
|
||||
}
|
||||
|
||||
// Transfer canvas control to offscreen
|
||||
this.offscreenCanvas = this.canvas.transferControlToOffscreen();
|
||||
|
||||
// Send init message to worker
|
||||
// Determine dark mode from theme (check if it has darkShore property, same as GroundTruthData)
|
||||
const themeAny = this.theme as any;
|
||||
const darkMode = themeAny.darkShore !== undefined;
|
||||
|
||||
const messageId = `init_renderer_${Date.now()}`;
|
||||
const initMessage: InitRendererMessage = {
|
||||
type: "init_renderer",
|
||||
id: messageId,
|
||||
offscreenCanvas: this.offscreenCanvas,
|
||||
darkMode: darkMode,
|
||||
};
|
||||
|
||||
// Transfer the offscreen canvas
|
||||
this.worker.postMessage(initMessage, [this.offscreenCanvas]);
|
||||
|
||||
// Wait for renderer ready
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.worker?.removeMessageHandler(messageId);
|
||||
reject(new Error("Renderer initialization timeout"));
|
||||
}, 10000);
|
||||
|
||||
const handler = (message: any) => {
|
||||
if (message.type === "renderer_ready" && message.id === messageId) {
|
||||
clearTimeout(timeout);
|
||||
this.worker?.removeMessageHandler(messageId);
|
||||
this.ready = true;
|
||||
// Send any pending messages
|
||||
for (const msg of this.pendingMessages) {
|
||||
this.sendToWorker(msg);
|
||||
}
|
||||
this.pendingMessages = [];
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
this.worker?.addMessageHandler(messageId, handler);
|
||||
});
|
||||
}
|
||||
|
||||
private sendToWorker(message: any): void {
|
||||
if (!this.worker) {
|
||||
return;
|
||||
}
|
||||
if (!this.ready) {
|
||||
this.pendingMessages.push(message);
|
||||
return;
|
||||
}
|
||||
this.worker.postMessage(message);
|
||||
}
|
||||
|
||||
setViewSize(width: number, height: number): void {
|
||||
const message: SetViewSizeMessage = {
|
||||
type: "set_view_size",
|
||||
width,
|
||||
height,
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
setViewTransform(scale: number, offsetX: number, offsetY: number): void {
|
||||
const message: SetViewTransformMessage = {
|
||||
type: "set_view_transform",
|
||||
scale,
|
||||
offsetX,
|
||||
offsetY,
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
setAlternativeView(enabled: boolean): void {
|
||||
const message: SetAlternativeViewMessage = {
|
||||
type: "set_alternative_view",
|
||||
enabled,
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
setHighlightedOwnerId(ownerSmallId: number | null): void {
|
||||
const message: SetHighlightedOwnerMessage = {
|
||||
type: "set_highlighted_owner",
|
||||
ownerSmallId,
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
setTerritoryShader(shaderPath: string): void {
|
||||
const message: SetShaderSettingsMessage = {
|
||||
type: "set_shader_settings",
|
||||
territoryShader: shaderPath,
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
setTerrainShader(shaderPath: string): void {
|
||||
const message: SetShaderSettingsMessage = {
|
||||
type: "set_shader_settings",
|
||||
terrainShader: shaderPath,
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
setTerritoryShaderParams(
|
||||
params0: Float32Array | number[],
|
||||
params1: Float32Array | number[],
|
||||
): void {
|
||||
const message: SetShaderSettingsMessage = {
|
||||
type: "set_shader_settings",
|
||||
territoryShaderParams0: Array.from(params0),
|
||||
territoryShaderParams1: Array.from(params1),
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
setTerrainShaderParams(
|
||||
params0: Float32Array | number[],
|
||||
params1: Float32Array | number[],
|
||||
): void {
|
||||
const message: SetShaderSettingsMessage = {
|
||||
type: "set_shader_settings",
|
||||
terrainShaderParams0: Array.from(params0),
|
||||
terrainShaderParams1: Array.from(params1),
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
setPreSmoothing(
|
||||
enabled: boolean,
|
||||
shaderPath: string,
|
||||
params0: Float32Array | number[],
|
||||
): void {
|
||||
const message: SetShaderSettingsMessage = {
|
||||
type: "set_shader_settings",
|
||||
preSmoothing: {
|
||||
enabled,
|
||||
shaderPath,
|
||||
params0: Array.from(params0),
|
||||
},
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
setPostSmoothing(
|
||||
enabled: boolean,
|
||||
shaderPath: string,
|
||||
params0: Float32Array | number[],
|
||||
): void {
|
||||
const message: SetShaderSettingsMessage = {
|
||||
type: "set_shader_settings",
|
||||
postSmoothing: {
|
||||
enabled,
|
||||
shaderPath,
|
||||
params0: Array.from(params0),
|
||||
},
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
markTile(tile: TileRef): void {
|
||||
const message: MarkTileMessage = {
|
||||
type: "mark_tile",
|
||||
tile,
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
markAllDirty(): void {
|
||||
const message: MarkAllDirtyMessage = {
|
||||
type: "mark_all_dirty",
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
refreshPalette(): void {
|
||||
const message: RefreshPaletteMessage = {
|
||||
type: "refresh_palette",
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
markDefensePostsDirty(): void {
|
||||
this.markAllDirty();
|
||||
}
|
||||
|
||||
refreshTerrain(): void {
|
||||
const message: RefreshTerrainMessage = {
|
||||
type: "refresh_terrain",
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
tick(): void {
|
||||
const message: TickRendererMessage = {
|
||||
type: "tick_renderer",
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
|
||||
render(): void {
|
||||
const message: RenderFrameMessage = {
|
||||
type: "render_frame",
|
||||
};
|
||||
this.sendToWorker(message);
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,12 @@ export class WebGPUDevice {
|
||||
|
||||
/**
|
||||
* Initialize WebGPU device and canvas context.
|
||||
* @param canvas Canvas element to configure
|
||||
* @param canvas Canvas element to configure (HTMLCanvasElement or OffscreenCanvas)
|
||||
* @returns WebGPUDevice instance or null if WebGPU is not available
|
||||
*/
|
||||
static async create(canvas: HTMLCanvasElement): Promise<WebGPUDevice | null> {
|
||||
static async create(
|
||||
canvas: HTMLCanvasElement | OffscreenCanvas,
|
||||
): Promise<WebGPUDevice | null> {
|
||||
const nav = globalThis.navigator as any;
|
||||
if (!nav?.gpu || typeof nav.gpu.requestAdapter !== "function") {
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Theme } from "../configuration/Config";
|
||||
import { Game, UnitType } from "../game/Game";
|
||||
import { GameUpdateViewData } from "../game/GameUpdates";
|
||||
import { GameView } from "../game/GameView";
|
||||
import { TerrainMapData } from "../game/TerrainMapLoader";
|
||||
|
||||
/**
|
||||
* Adapter that makes Game work as GameView for rendering purposes.
|
||||
* Provides the interface that GroundTruthData and rendering passes need,
|
||||
* 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(
|
||||
private game: Game,
|
||||
private mapData: TerrainMapData,
|
||||
private theme: Theme,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Update adapter with latest game update data.
|
||||
* Invalidates caches so they're recomputed on next access.
|
||||
*/
|
||||
update(gu: GameUpdateViewData): void {
|
||||
this.lastUpdate = gu;
|
||||
// Invalidate caches when updated
|
||||
this.tileStateCache = null;
|
||||
this.terrainDataCache = null;
|
||||
}
|
||||
|
||||
config() {
|
||||
return this.game.config();
|
||||
}
|
||||
|
||||
width(): number {
|
||||
return this.game.width();
|
||||
}
|
||||
|
||||
height(): number {
|
||||
return this.game.height();
|
||||
}
|
||||
|
||||
x(tile: bigint): number {
|
||||
return this.game.x(tile);
|
||||
}
|
||||
|
||||
y(tile: bigint): number {
|
||||
return this.game.y(tile);
|
||||
}
|
||||
|
||||
units(...types: UnitType[]): any[] {
|
||||
return this.game.units(...types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build tile state view from game.
|
||||
* Cached until next update.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build terrain data view from game.
|
||||
* Cached until next update.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Game players to PlayerView-like objects for rendering.
|
||||
* 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 defaultBorderColor = theme.borderColor(defaultTerritoryColor);
|
||||
const territoryRgb = defaultTerritoryColor.toRgb();
|
||||
const borderRgb = defaultBorderColor.toRgb();
|
||||
|
||||
return {
|
||||
smallID: () => p.smallID(),
|
||||
territoryColor: () => ({
|
||||
rgba: {
|
||||
r: Math.round(territoryRgb.r),
|
||||
g: Math.round(territoryRgb.g),
|
||||
b: Math.round(territoryRgb.b),
|
||||
a: Math.round((territoryRgb.a ?? 1) * 255),
|
||||
},
|
||||
}),
|
||||
borderColor: () => ({
|
||||
rgba: {
|
||||
r: Math.round(borderRgb.r),
|
||||
g: Math.round(borderRgb.g),
|
||||
b: Math.round(borderRgb.b),
|
||||
a: Math.round((borderRgb.a ?? 1) * 255),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get my player for highlighting (returns null in worker context).
|
||||
*/
|
||||
myPlayer(): any | null {
|
||||
// Return null for now - this is used for highlighting
|
||||
// Could be implemented if we track clientID in worker
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recently updated tiles from last game update.
|
||||
*/
|
||||
recentlyUpdatedTiles(): bigint[] {
|
||||
if (!this.lastUpdate) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(this.lastUpdate.packedTileUpdates);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import version from "resources/version.txt?raw";
|
||||
import { createGameRunner, GameRunner } from "../GameRunner";
|
||||
import { Theme } from "../configuration/Config";
|
||||
import { PastelTheme } from "../configuration/PastelTheme";
|
||||
import { PastelThemeDark } from "../configuration/PastelThemeDark";
|
||||
import { FetchGameMapLoader } from "../game/FetchGameMapLoader";
|
||||
import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates";
|
||||
import { loadTerrainMap, TerrainMapData } from "../game/TerrainMapLoader";
|
||||
import { createGameRunner, GameRunner } from "../GameRunner";
|
||||
import { GameStartInfo } from "../Schemas";
|
||||
import {
|
||||
AttackAveragePositionResultMessage,
|
||||
InitializedMessage,
|
||||
@@ -9,19 +14,28 @@ import {
|
||||
PlayerActionsResultMessage,
|
||||
PlayerBorderTilesResultMessage,
|
||||
PlayerProfileResultMessage,
|
||||
RendererReadyMessage,
|
||||
TransportShipSpawnResultMessage,
|
||||
WorkerMessage,
|
||||
} from "./WorkerMessages";
|
||||
import { WorkerTerritoryRenderer } from "./WorkerTerritoryRenderer";
|
||||
|
||||
const ctx: Worker = self as any;
|
||||
let gameRunner: Promise<GameRunner> | null = null;
|
||||
let gameStartInfo: GameStartInfo | null = null;
|
||||
const mapLoader = new FetchGameMapLoader(`/maps`, version);
|
||||
let renderer: WorkerTerritoryRenderer | null = null;
|
||||
let mapData: TerrainMapData | null = null;
|
||||
|
||||
function gameUpdate(gu: GameUpdateViewData | ErrorUpdate) {
|
||||
// skip if ErrorUpdate
|
||||
if (!("updates" in gu)) {
|
||||
return;
|
||||
}
|
||||
// Update renderer with game update
|
||||
if (renderer) {
|
||||
renderer.updateGameView(gu);
|
||||
}
|
||||
sendMessage({
|
||||
type: "game_update",
|
||||
gameUpdate: gu,
|
||||
@@ -41,6 +55,7 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
|
||||
break;
|
||||
case "init":
|
||||
try {
|
||||
gameStartInfo = message.gameStartInfo;
|
||||
gameRunner = createGameRunner(
|
||||
message.gameStartInfo,
|
||||
message.clientID,
|
||||
@@ -170,6 +185,153 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
|
||||
console.error("Failed to spawn transport ship:", error);
|
||||
}
|
||||
break;
|
||||
|
||||
case "init_renderer":
|
||||
try {
|
||||
if (!gameRunner || !gameStartInfo) {
|
||||
throw new Error("Game runner not initialized");
|
||||
}
|
||||
const gr = await gameRunner;
|
||||
|
||||
// Load map data if not already loaded
|
||||
// Use gameStartInfo.config which has the original game map info
|
||||
mapData ??= await loadTerrainMap(
|
||||
gameStartInfo.config.gameMap,
|
||||
gameStartInfo.config.gameMapSize,
|
||||
mapLoader,
|
||||
);
|
||||
|
||||
// Create theme based on darkMode flag from main thread
|
||||
// (can't access userSettings in worker, so it's passed from main thread)
|
||||
const theme: Theme = message.darkMode
|
||||
? new PastelThemeDark()
|
||||
: new PastelTheme();
|
||||
|
||||
renderer = new WorkerTerritoryRenderer();
|
||||
|
||||
await renderer.init(message.offscreenCanvas, gr, mapData, theme);
|
||||
|
||||
sendMessage({
|
||||
type: "renderer_ready",
|
||||
id: message.id,
|
||||
} as RendererReadyMessage);
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize renderer:", error);
|
||||
sendMessage({
|
||||
type: "renderer_ready",
|
||||
id: message.id,
|
||||
} as RendererReadyMessage);
|
||||
throw error;
|
||||
}
|
||||
break;
|
||||
|
||||
case "set_view_size":
|
||||
if (renderer) {
|
||||
renderer.setViewSize(message.width, message.height);
|
||||
}
|
||||
break;
|
||||
|
||||
case "set_view_transform":
|
||||
if (renderer) {
|
||||
renderer.setViewTransform(
|
||||
message.scale,
|
||||
message.offsetX,
|
||||
message.offsetY,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case "set_alternative_view":
|
||||
if (renderer) {
|
||||
renderer.setAlternativeView(message.enabled);
|
||||
}
|
||||
break;
|
||||
|
||||
case "set_highlighted_owner":
|
||||
if (renderer) {
|
||||
renderer.setHighlightedOwnerId(message.ownerSmallId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "set_shader_settings":
|
||||
if (renderer) {
|
||||
if (message.territoryShader) {
|
||||
renderer.setTerritoryShader(message.territoryShader);
|
||||
}
|
||||
if (message.territoryShaderParams0 && message.territoryShaderParams1) {
|
||||
renderer.setTerritoryShaderParams(
|
||||
message.territoryShaderParams0,
|
||||
message.territoryShaderParams1,
|
||||
);
|
||||
}
|
||||
if (message.terrainShader) {
|
||||
renderer.setTerrainShader(message.terrainShader);
|
||||
}
|
||||
if (message.terrainShaderParams0 && message.terrainShaderParams1) {
|
||||
renderer.setTerrainShaderParams(
|
||||
message.terrainShaderParams0,
|
||||
message.terrainShaderParams1,
|
||||
);
|
||||
}
|
||||
if (message.preSmoothing) {
|
||||
renderer.setPreSmoothing(
|
||||
message.preSmoothing.enabled,
|
||||
message.preSmoothing.shaderPath,
|
||||
message.preSmoothing.params0,
|
||||
);
|
||||
}
|
||||
if (message.postSmoothing) {
|
||||
renderer.setPostSmoothing(
|
||||
message.postSmoothing.enabled,
|
||||
message.postSmoothing.shaderPath,
|
||||
message.postSmoothing.params0,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "mark_tile":
|
||||
if (renderer) {
|
||||
renderer.markTile(message.tile);
|
||||
}
|
||||
break;
|
||||
|
||||
case "mark_all_dirty":
|
||||
if (renderer) {
|
||||
renderer.markAllDirty();
|
||||
}
|
||||
break;
|
||||
|
||||
case "refresh_palette":
|
||||
if (renderer) {
|
||||
renderer.refreshPalette();
|
||||
}
|
||||
break;
|
||||
|
||||
case "refresh_terrain":
|
||||
if (renderer) {
|
||||
renderer.refreshTerrain();
|
||||
}
|
||||
break;
|
||||
|
||||
case "tick_renderer":
|
||||
if (renderer) {
|
||||
const start = performance.now();
|
||||
renderer.tick();
|
||||
const computeMs = performance.now() - start;
|
||||
sendMessage({
|
||||
type: "renderer_metrics",
|
||||
computeMs,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "render_frame":
|
||||
if (renderer) {
|
||||
renderer.render();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn("Unknown message :", message);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export class WorkerClient {
|
||||
break;
|
||||
|
||||
case "initialized":
|
||||
case "renderer_ready":
|
||||
default:
|
||||
if (message.id && this.messageHandlers.has(message.id)) {
|
||||
const handler = this.messageHandlers.get(message.id)!;
|
||||
@@ -56,6 +57,30 @@ export class WorkerClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message handler for a specific message ID.
|
||||
*/
|
||||
addMessageHandler(
|
||||
id: string,
|
||||
handler: (message: WorkerMessage) => void,
|
||||
): void {
|
||||
this.messageHandlers.set(id, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a message handler.
|
||||
*/
|
||||
removeMessageHandler(id: string): void {
|
||||
this.messageHandlers.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post a message to the worker with optional transferables.
|
||||
*/
|
||||
postMessage(message: any, transfer?: Transferable[]): void {
|
||||
this.worker.postMessage(message, transfer);
|
||||
}
|
||||
|
||||
initialize(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageId = generateID();
|
||||
|
||||
@@ -23,7 +23,21 @@ export type WorkerMessageType =
|
||||
| "attack_average_position"
|
||||
| "attack_average_position_result"
|
||||
| "transport_ship_spawn"
|
||||
| "transport_ship_spawn_result";
|
||||
| "transport_ship_spawn_result"
|
||||
| "init_renderer"
|
||||
| "renderer_ready"
|
||||
| "set_view_size"
|
||||
| "set_view_transform"
|
||||
| "set_alternative_view"
|
||||
| "set_highlighted_owner"
|
||||
| "set_shader_settings"
|
||||
| "mark_tile"
|
||||
| "mark_all_dirty"
|
||||
| "refresh_palette"
|
||||
| "refresh_terrain"
|
||||
| "tick_renderer"
|
||||
| "render_frame"
|
||||
| "renderer_metrics";
|
||||
|
||||
// Base interface for all messages
|
||||
interface BaseWorkerMessage {
|
||||
@@ -112,6 +126,91 @@ export interface TransportShipSpawnResultMessage extends BaseWorkerMessage {
|
||||
result: TileRef | false;
|
||||
}
|
||||
|
||||
// Renderer messages from main thread to worker
|
||||
export interface InitRendererMessage extends BaseWorkerMessage {
|
||||
type: "init_renderer";
|
||||
offscreenCanvas: OffscreenCanvas;
|
||||
darkMode: boolean; // Whether to use dark theme
|
||||
}
|
||||
|
||||
export interface SetViewSizeMessage extends BaseWorkerMessage {
|
||||
type: "set_view_size";
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface SetViewTransformMessage extends BaseWorkerMessage {
|
||||
type: "set_view_transform";
|
||||
scale: number;
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}
|
||||
|
||||
export interface SetAlternativeViewMessage extends BaseWorkerMessage {
|
||||
type: "set_alternative_view";
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SetHighlightedOwnerMessage extends BaseWorkerMessage {
|
||||
type: "set_highlighted_owner";
|
||||
ownerSmallId: number | null;
|
||||
}
|
||||
|
||||
export interface SetShaderSettingsMessage extends BaseWorkerMessage {
|
||||
type: "set_shader_settings";
|
||||
territoryShader?: string;
|
||||
territoryShaderParams0?: number[];
|
||||
territoryShaderParams1?: number[];
|
||||
terrainShader?: string;
|
||||
terrainShaderParams0?: number[];
|
||||
terrainShaderParams1?: number[];
|
||||
preSmoothing?: {
|
||||
enabled: boolean;
|
||||
shaderPath: string;
|
||||
params0: number[];
|
||||
};
|
||||
postSmoothing?: {
|
||||
enabled: boolean;
|
||||
shaderPath: string;
|
||||
params0: number[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface MarkTileMessage extends BaseWorkerMessage {
|
||||
type: "mark_tile";
|
||||
tile: TileRef;
|
||||
}
|
||||
|
||||
export interface MarkAllDirtyMessage extends BaseWorkerMessage {
|
||||
type: "mark_all_dirty";
|
||||
}
|
||||
|
||||
export interface RefreshPaletteMessage extends BaseWorkerMessage {
|
||||
type: "refresh_palette";
|
||||
}
|
||||
|
||||
export interface RefreshTerrainMessage extends BaseWorkerMessage {
|
||||
type: "refresh_terrain";
|
||||
}
|
||||
|
||||
export interface TickRendererMessage extends BaseWorkerMessage {
|
||||
type: "tick_renderer";
|
||||
}
|
||||
|
||||
export interface RenderFrameMessage extends BaseWorkerMessage {
|
||||
type: "render_frame";
|
||||
}
|
||||
|
||||
// Renderer messages from worker to main thread
|
||||
export interface RendererReadyMessage extends BaseWorkerMessage {
|
||||
type: "renderer_ready";
|
||||
}
|
||||
|
||||
export interface RendererMetricsMessage extends BaseWorkerMessage {
|
||||
type: "renderer_metrics";
|
||||
computeMs: number;
|
||||
}
|
||||
|
||||
// Union types for type safety
|
||||
export type MainThreadMessage =
|
||||
| HeartbeatMessage
|
||||
@@ -121,7 +220,19 @@ export type MainThreadMessage =
|
||||
| PlayerProfileMessage
|
||||
| PlayerBorderTilesMessage
|
||||
| AttackAveragePositionMessage
|
||||
| TransportShipSpawnMessage;
|
||||
| TransportShipSpawnMessage
|
||||
| InitRendererMessage
|
||||
| SetViewSizeMessage
|
||||
| SetViewTransformMessage
|
||||
| SetAlternativeViewMessage
|
||||
| SetHighlightedOwnerMessage
|
||||
| SetShaderSettingsMessage
|
||||
| MarkTileMessage
|
||||
| MarkAllDirtyMessage
|
||||
| RefreshPaletteMessage
|
||||
| RefreshTerrainMessage
|
||||
| TickRendererMessage
|
||||
| RenderFrameMessage;
|
||||
|
||||
// Message send from worker
|
||||
export type WorkerMessage =
|
||||
@@ -131,4 +242,6 @@ export type WorkerMessage =
|
||||
| PlayerProfileResultMessage
|
||||
| PlayerBorderTilesResultMessage
|
||||
| AttackAveragePositionResultMessage
|
||||
| TransportShipSpawnResultMessage;
|
||||
| TransportShipSpawnResultMessage
|
||||
| RendererReadyMessage
|
||||
| RendererMetricsMessage;
|
||||
|
||||
@@ -0,0 +1,603 @@
|
||||
import { Theme } from "../configuration/Config";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { GameUpdateViewData } from "../game/GameUpdates";
|
||||
import { TerrainMapData } from "../game/TerrainMapLoader";
|
||||
import { GameRunner } from "../GameRunner";
|
||||
import { GameViewAdapter } from "./GameViewAdapter";
|
||||
|
||||
// Import rendering components from client (they should work with adapter)
|
||||
import { ComputePass } from "../../client/graphics/webgpu/compute/ComputePass";
|
||||
import { DefendedStrengthFullPass } from "../../client/graphics/webgpu/compute/DefendedStrengthFullPass";
|
||||
import { DefendedStrengthPass } from "../../client/graphics/webgpu/compute/DefendedStrengthPass";
|
||||
import { StateUpdatePass } from "../../client/graphics/webgpu/compute/StateUpdatePass";
|
||||
import { TerrainComputePass } from "../../client/graphics/webgpu/compute/TerrainComputePass";
|
||||
import { VisualStateSmoothingPass } from "../../client/graphics/webgpu/compute/VisualStateSmoothingPass";
|
||||
import { GroundTruthData } from "../../client/graphics/webgpu/core/GroundTruthData";
|
||||
import { WebGPUDevice } from "../../client/graphics/webgpu/core/WebGPUDevice";
|
||||
import { RenderPass } from "../../client/graphics/webgpu/render/RenderPass";
|
||||
import { TemporalResolvePass } from "../../client/graphics/webgpu/render/TemporalResolvePass";
|
||||
import { TerritoryRenderPass } from "../../client/graphics/webgpu/render/TerritoryRenderPass";
|
||||
|
||||
/**
|
||||
* Worker-compatible WebGPU territory renderer.
|
||||
* Works with Game directly (not GameView) and uses OffscreenCanvas.
|
||||
*/
|
||||
export class WorkerTerritoryRenderer {
|
||||
private device: WebGPUDevice | null = null;
|
||||
private resources: GroundTruthData | null = null;
|
||||
private gameViewAdapter: GameViewAdapter | null = null;
|
||||
private ready = false;
|
||||
|
||||
// Compute passes
|
||||
private computePasses: ComputePass[] = [];
|
||||
private computePassOrder: ComputePass[] = [];
|
||||
private frameComputePasses: ComputePass[] = [];
|
||||
|
||||
// Render passes
|
||||
private renderPasses: RenderPass[] = [];
|
||||
private renderPassOrder: RenderPass[] = [];
|
||||
|
||||
// Pass instances
|
||||
private terrainComputePass: TerrainComputePass | null = null;
|
||||
private stateUpdatePass: StateUpdatePass | null = null;
|
||||
private defendedStrengthFullPass: DefendedStrengthFullPass | null = null;
|
||||
private defendedStrengthPass: DefendedStrengthPass | null = null;
|
||||
private visualStateSmoothingPass: VisualStateSmoothingPass | null = null;
|
||||
private territoryRenderPass: TerritoryRenderPass | null = null;
|
||||
private temporalResolvePass: TemporalResolvePass | null = null;
|
||||
|
||||
private territoryShaderPath = "render/territory.wgsl";
|
||||
private territoryShaderParams0 = new Float32Array(4);
|
||||
private territoryShaderParams1 = new Float32Array(4);
|
||||
private terrainShaderPath = "compute/terrain-compute.wgsl";
|
||||
private terrainShaderParams0 = new Float32Array(4);
|
||||
private terrainShaderParams1 = new Float32Array(4);
|
||||
private preSmoothingShaderPath = "compute/visual-state-smoothing.wgsl";
|
||||
private preSmoothingParams0 = new Float32Array(4);
|
||||
private postSmoothingShaderPath = "render/temporal-resolve.wgsl";
|
||||
private postSmoothingParams0 = new Float32Array(4);
|
||||
|
||||
private preSmoothingEnabled = false;
|
||||
private postSmoothingEnabled = false;
|
||||
private defensePostRange: number;
|
||||
|
||||
/**
|
||||
* Initialize renderer with offscreen canvas and game data.
|
||||
*/
|
||||
async init(
|
||||
offscreenCanvas: OffscreenCanvas,
|
||||
gameRunner: GameRunner,
|
||||
mapData: TerrainMapData,
|
||||
theme: Theme,
|
||||
): Promise<void> {
|
||||
const game = gameRunner.game;
|
||||
this.defensePostRange = game.config().defensePostRange();
|
||||
|
||||
// Create adapter
|
||||
this.gameViewAdapter = new GameViewAdapter(game, mapData, theme);
|
||||
|
||||
// Initialize WebGPU device with offscreen canvas
|
||||
const webgpuDevice = await WebGPUDevice.create(offscreenCanvas);
|
||||
if (!webgpuDevice) {
|
||||
throw new Error("Failed to create WebGPU device in worker");
|
||||
}
|
||||
this.device = webgpuDevice;
|
||||
|
||||
// Create ground truth data using adapter
|
||||
const state = this.gameViewAdapter.tileStateView();
|
||||
this.resources = GroundTruthData.create(
|
||||
webgpuDevice.device,
|
||||
this.gameViewAdapter as any,
|
||||
theme,
|
||||
state,
|
||||
);
|
||||
this.resources.setTerritoryShaderParams(
|
||||
this.territoryShaderParams0,
|
||||
this.territoryShaderParams1,
|
||||
);
|
||||
this.resources.setTerrainShaderParams(
|
||||
this.terrainShaderParams0,
|
||||
this.terrainShaderParams1,
|
||||
);
|
||||
|
||||
// Upload terrain data and params
|
||||
this.resources.uploadTerrainData();
|
||||
this.resources.uploadTerrainParams();
|
||||
|
||||
// Create compute passes
|
||||
this.terrainComputePass = new TerrainComputePass();
|
||||
void this.terrainComputePass.setShader(this.terrainShaderPath);
|
||||
this.stateUpdatePass = new StateUpdatePass();
|
||||
this.defendedStrengthFullPass = new DefendedStrengthFullPass();
|
||||
this.defendedStrengthPass = new DefendedStrengthPass();
|
||||
this.visualStateSmoothingPass = new VisualStateSmoothingPass();
|
||||
|
||||
this.computePasses = [
|
||||
this.terrainComputePass,
|
||||
this.stateUpdatePass,
|
||||
this.defendedStrengthFullPass,
|
||||
this.defendedStrengthPass,
|
||||
];
|
||||
|
||||
this.frameComputePasses = [this.visualStateSmoothingPass];
|
||||
|
||||
// Create render passes
|
||||
this.territoryRenderPass = new TerritoryRenderPass();
|
||||
this.temporalResolvePass = new TemporalResolvePass();
|
||||
this.renderPasses = [this.territoryRenderPass, this.temporalResolvePass];
|
||||
|
||||
// Initialize all passes
|
||||
for (const pass of this.computePasses) {
|
||||
await pass.init(webgpuDevice.device, this.resources);
|
||||
}
|
||||
|
||||
for (const pass of this.frameComputePasses) {
|
||||
await pass.init(webgpuDevice.device, this.resources);
|
||||
}
|
||||
|
||||
for (const pass of this.renderPasses) {
|
||||
await pass.init(
|
||||
webgpuDevice.device,
|
||||
this.resources,
|
||||
webgpuDevice.canvasFormat,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.territoryRenderPass) {
|
||||
await this.territoryRenderPass.setShader(this.territoryShaderPath);
|
||||
}
|
||||
|
||||
// Compute dependency order
|
||||
this.computePassOrder = this.topologicalSort(this.computePasses);
|
||||
this.renderPassOrder = this.topologicalSort(this.renderPasses);
|
||||
|
||||
this.ready = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update game view adapter with latest game update.
|
||||
*/
|
||||
updateGameView(gu: GameUpdateViewData): void {
|
||||
if (this.gameViewAdapter) {
|
||||
this.gameViewAdapter.update(gu);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Topological sort of passes based on dependencies.
|
||||
*/
|
||||
private topologicalSort<T extends { name: string; dependencies: string[] }>(
|
||||
passes: T[],
|
||||
): T[] {
|
||||
const passMap = new Map<string, T>();
|
||||
for (const pass of passes) {
|
||||
passMap.set(pass.name, pass);
|
||||
}
|
||||
|
||||
const visited = new Set<string>();
|
||||
const visiting = new Set<string>();
|
||||
const result: T[] = [];
|
||||
|
||||
const visit = (pass: T): void => {
|
||||
if (visiting.has(pass.name)) {
|
||||
console.warn(
|
||||
`Circular dependency detected involving pass: ${pass.name}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (visited.has(pass.name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
visiting.add(pass.name);
|
||||
for (const depName of pass.dependencies) {
|
||||
const dep = passMap.get(depName);
|
||||
if (dep) {
|
||||
visit(dep);
|
||||
}
|
||||
}
|
||||
visiting.delete(pass.name);
|
||||
visited.add(pass.name);
|
||||
result.push(pass);
|
||||
};
|
||||
|
||||
for (const pass of passes) {
|
||||
if (!visited.has(pass.name)) {
|
||||
visit(pass);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
setViewSize(width: number, height: number): void {
|
||||
if (!this.resources || !this.device) {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
this.resources.setViewSize(nextWidth, nextHeight);
|
||||
this.device.reconfigure();
|
||||
|
||||
if (this.postSmoothingEnabled && this.resources) {
|
||||
this.resources.ensurePostSmoothingTextures(
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
this.device.canvasFormat,
|
||||
);
|
||||
this.resources.invalidateHistory();
|
||||
}
|
||||
}
|
||||
|
||||
setViewTransform(scale: number, offsetX: number, offsetY: number): void {
|
||||
if (!this.resources) {
|
||||
return;
|
||||
}
|
||||
this.resources.setViewTransform(scale, offsetX, offsetY);
|
||||
}
|
||||
|
||||
setAlternativeView(enabled: boolean): void {
|
||||
if (!this.resources) {
|
||||
return;
|
||||
}
|
||||
this.resources.setAlternativeView(enabled);
|
||||
}
|
||||
|
||||
setHighlightedOwnerId(ownerSmallId: number | null): void {
|
||||
if (!this.resources) {
|
||||
return;
|
||||
}
|
||||
this.resources.setHighlightedOwnerId(ownerSmallId);
|
||||
}
|
||||
|
||||
setTerritoryShader(shaderPath: string): void {
|
||||
this.territoryShaderPath = shaderPath;
|
||||
if (this.territoryRenderPass) {
|
||||
void this.territoryRenderPass.setShader(shaderPath);
|
||||
}
|
||||
this.resources?.invalidateHistory();
|
||||
}
|
||||
|
||||
setTerrainShader(shaderPath: string): void {
|
||||
this.terrainShaderPath = shaderPath;
|
||||
if (!this.terrainComputePass) {
|
||||
return;
|
||||
}
|
||||
void this.terrainComputePass.setShader(shaderPath).then(() => {
|
||||
this.refreshTerrain();
|
||||
});
|
||||
}
|
||||
|
||||
setTerritoryShaderParams(
|
||||
params0: Float32Array | number[],
|
||||
params1: Float32Array | number[],
|
||||
): void {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.territoryShaderParams0[i] = Number(params0[i] ?? 0);
|
||||
this.territoryShaderParams1[i] = Number(params1[i] ?? 0);
|
||||
}
|
||||
|
||||
if (!this.resources) {
|
||||
return;
|
||||
}
|
||||
this.resources.setTerritoryShaderParams(
|
||||
this.territoryShaderParams0,
|
||||
this.territoryShaderParams1,
|
||||
);
|
||||
this.resources.invalidateHistory();
|
||||
}
|
||||
|
||||
setTerrainShaderParams(
|
||||
params0: Float32Array | number[],
|
||||
params1: Float32Array | number[],
|
||||
): void {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.terrainShaderParams0[i] = Number(params0[i] ?? 0);
|
||||
this.terrainShaderParams1[i] = Number(params1[i] ?? 0);
|
||||
}
|
||||
|
||||
if (!this.resources) {
|
||||
return;
|
||||
}
|
||||
this.resources.setTerrainShaderParams(
|
||||
this.terrainShaderParams0,
|
||||
this.terrainShaderParams1,
|
||||
);
|
||||
this.refreshTerrain();
|
||||
}
|
||||
|
||||
setPreSmoothing(
|
||||
enabled: boolean,
|
||||
shaderPath: string,
|
||||
params0: Float32Array | number[],
|
||||
): void {
|
||||
this.preSmoothingEnabled = enabled;
|
||||
if (shaderPath) {
|
||||
this.preSmoothingShaderPath = shaderPath;
|
||||
}
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.preSmoothingParams0[i] = Number(params0[i] ?? 0);
|
||||
}
|
||||
this.applyPreSmoothingConfig();
|
||||
}
|
||||
|
||||
setPostSmoothing(
|
||||
enabled: boolean,
|
||||
shaderPath: string,
|
||||
params0: Float32Array | number[],
|
||||
): void {
|
||||
this.postSmoothingEnabled = enabled;
|
||||
if (shaderPath) {
|
||||
this.postSmoothingShaderPath = shaderPath;
|
||||
}
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.postSmoothingParams0[i] = Number(params0[i] ?? 0);
|
||||
}
|
||||
this.applyPostSmoothingConfig();
|
||||
}
|
||||
|
||||
private applyPreSmoothingConfig(): void {
|
||||
if (!this.resources || !this.visualStateSmoothingPass) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resources.setUseVisualStateTexture(this.preSmoothingEnabled);
|
||||
if (this.preSmoothingEnabled) {
|
||||
this.resources.ensureVisualStateTexture();
|
||||
void this.visualStateSmoothingPass.setShader(this.preSmoothingShaderPath);
|
||||
this.visualStateSmoothingPass.setParams(this.preSmoothingParams0);
|
||||
} else {
|
||||
this.visualStateSmoothingPass.setParams(new Float32Array(4));
|
||||
this.resources.releaseVisualStateTexture();
|
||||
}
|
||||
|
||||
this.resources.invalidateHistory();
|
||||
}
|
||||
|
||||
private applyPostSmoothingConfig(): void {
|
||||
if (!this.resources || !this.temporalResolvePass || !this.device) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.postSmoothingEnabled) {
|
||||
void this.temporalResolvePass.setShader(this.postSmoothingShaderPath);
|
||||
this.temporalResolvePass.setParams(this.postSmoothingParams0);
|
||||
this.temporalResolvePass.setEnabled(true);
|
||||
// Note: canvas size not available here, will be set on first setViewSize
|
||||
if (this.resources) {
|
||||
this.resources.ensurePostSmoothingTextures(
|
||||
1,
|
||||
1,
|
||||
this.device.canvasFormat,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.temporalResolvePass.setEnabled(false);
|
||||
this.resources.releasePostSmoothingTextures();
|
||||
}
|
||||
|
||||
this.resources.invalidateHistory();
|
||||
}
|
||||
|
||||
markTile(tile: TileRef): void {
|
||||
if (this.stateUpdatePass) {
|
||||
// TileRef is number, StateUpdatePass.markTile expects number
|
||||
this.stateUpdatePass.markTile(tile as number);
|
||||
}
|
||||
}
|
||||
|
||||
markAllDirty(): void {
|
||||
this.resources?.markDefensePostsDirty();
|
||||
}
|
||||
|
||||
refreshPalette(): void {
|
||||
if (!this.resources) {
|
||||
return;
|
||||
}
|
||||
this.resources.markPaletteDirty();
|
||||
}
|
||||
|
||||
markDefensePostsDirty(): void {
|
||||
if (!this.resources) {
|
||||
return;
|
||||
}
|
||||
this.resources.markDefensePostsDirty();
|
||||
}
|
||||
|
||||
refreshTerrain(): void {
|
||||
if (!this.resources || !this.device) {
|
||||
return;
|
||||
}
|
||||
this.resources.markTerrainParamsDirty();
|
||||
if (this.terrainComputePass) {
|
||||
this.terrainComputePass.markDirty();
|
||||
this.computeTerrainImmediate();
|
||||
}
|
||||
}
|
||||
|
||||
private computeTerrainImmediate(): void {
|
||||
if (
|
||||
!this.ready ||
|
||||
!this.device ||
|
||||
!this.resources ||
|
||||
!this.terrainComputePass
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resources.uploadTerrainParams();
|
||||
|
||||
if (!this.terrainComputePass.needsUpdate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const encoder = this.device.device.createCommandEncoder();
|
||||
this.terrainComputePass.execute(encoder, this.resources);
|
||||
this.device.device.queue.submit([encoder.finish()]);
|
||||
|
||||
if (this.territoryRenderPass) {
|
||||
(this.territoryRenderPass as any).rebuildBindGroup?.();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform one simulation tick.
|
||||
* Runs compute passes to update ground truth data.
|
||||
*/
|
||||
tick(): void {
|
||||
if (!this.ready || !this.device || !this.resources) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resources.updateTickTiming(performance.now() / 1000);
|
||||
|
||||
if (
|
||||
this.gameViewAdapter?.config().defensePostRange() !==
|
||||
this.defensePostRange
|
||||
) {
|
||||
throw new Error("defensePostRange changed at runtime; unsupported.");
|
||||
}
|
||||
|
||||
// Upload palette if needed
|
||||
this.resources.uploadPalette();
|
||||
|
||||
// Upload diplomacy relations
|
||||
this.resources.uploadRelations();
|
||||
|
||||
// Upload defense posts if needed
|
||||
this.resources.uploadDefensePosts();
|
||||
|
||||
// Initial state upload
|
||||
this.resources.uploadState();
|
||||
|
||||
const stateUpdatesPending = this.stateUpdatePass?.needsUpdate() ?? false;
|
||||
if (!stateUpdatesPending) {
|
||||
this.resources.setLastStateUpdateCount(0);
|
||||
}
|
||||
|
||||
const needsCompute =
|
||||
(this.terrainComputePass?.needsUpdate() ?? false) ||
|
||||
stateUpdatesPending ||
|
||||
(this.defendedStrengthFullPass?.needsUpdate() ?? false) ||
|
||||
(this.defendedStrengthPass?.needsUpdate() ?? false);
|
||||
|
||||
if (!needsCompute) {
|
||||
return;
|
||||
}
|
||||
|
||||
const encoder = this.device.device.createCommandEncoder();
|
||||
|
||||
if (this.preSmoothingEnabled && stateUpdatesPending) {
|
||||
this.resources.ensureVisualStateTexture();
|
||||
const visualStateTexture = this.resources.getVisualStateTexture();
|
||||
if (visualStateTexture) {
|
||||
encoder.copyTextureToTexture(
|
||||
{ texture: this.resources.stateTexture },
|
||||
{ texture: visualStateTexture },
|
||||
{
|
||||
width: this.resources.getMapWidth(),
|
||||
height: this.resources.getMapHeight(),
|
||||
depthOrArrayLayers: 1,
|
||||
},
|
||||
);
|
||||
this.resources.consumeVisualStateSyncNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
// Execute compute passes in dependency order
|
||||
for (const pass of this.computePassOrder) {
|
||||
if (!pass.needsUpdate()) {
|
||||
continue;
|
||||
}
|
||||
pass.execute(encoder, this.resources);
|
||||
}
|
||||
|
||||
this.device.device.queue.submit([encoder.finish()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one frame.
|
||||
* Runs render passes to draw to the canvas.
|
||||
*/
|
||||
render(): void {
|
||||
if (
|
||||
!this.ready ||
|
||||
!this.device ||
|
||||
!this.resources ||
|
||||
!this.territoryRenderPass
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nowSec = performance.now() / 1000;
|
||||
this.resources.writeTemporalUniformBuffer(nowSec);
|
||||
|
||||
// If terrain needs recomputation, trigger it asynchronously
|
||||
if (this.terrainComputePass?.needsUpdate()) {
|
||||
this.resources.uploadTerrainParams();
|
||||
const computeEncoder = this.device.device.createCommandEncoder();
|
||||
this.terrainComputePass.execute(computeEncoder, this.resources);
|
||||
this.device.device.queue.submit([computeEncoder.finish()]);
|
||||
}
|
||||
|
||||
const encoder = this.device.device.createCommandEncoder();
|
||||
const swapchainView = this.device.context.getCurrentTexture().createView();
|
||||
|
||||
if (
|
||||
this.preSmoothingEnabled &&
|
||||
this.resources.consumeVisualStateSyncNeeded()
|
||||
) {
|
||||
const visualStateTexture = this.resources.getVisualStateTexture();
|
||||
if (visualStateTexture) {
|
||||
encoder.copyTextureToTexture(
|
||||
{ texture: this.resources.stateTexture },
|
||||
{ texture: visualStateTexture },
|
||||
{
|
||||
width: this.resources.getMapWidth(),
|
||||
height: this.resources.getMapHeight(),
|
||||
depthOrArrayLayers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const pass of this.frameComputePasses) {
|
||||
if (!pass.needsUpdate()) {
|
||||
continue;
|
||||
}
|
||||
pass.execute(encoder, this.resources);
|
||||
}
|
||||
|
||||
// Execute render passes in dependency order
|
||||
for (const pass of this.renderPassOrder) {
|
||||
if (!pass.needsUpdate()) {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
this.resources.ensurePostSmoothingTextures(
|
||||
viewWidth,
|
||||
viewHeight,
|
||||
this.device.canvasFormat,
|
||||
);
|
||||
}
|
||||
const currentTexture = this.resources.getCurrentColorTexture();
|
||||
if (currentTexture) {
|
||||
pass.execute(encoder, this.resources, currentTexture.createView());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
pass.execute(encoder, this.resources, swapchainView);
|
||||
}
|
||||
|
||||
this.device.device.queue.submit([encoder.finish()]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user