refactor: restructure WebGPU territory renderer into extensible pass-based architecture

Refactor the monolithic TerritoryWebGLRenderer into a modular, extensible
architecture that separates ground truth computation from rendering passes.
This change also includes related improvements to game state management and
hover information handling.

WebGPU Architecture Refactor:
- Extract all shaders to external .wgsl files (no inlined shaders)
- Separate ground truth data management (GroundTruthData) from rendering
- Create pass-based architecture with ComputePass and RenderPass interfaces
- Implement compute passes: StateUpdatePass, DefendedClearPass, DefendedUpdatePass
- Implement render pass: TerritoryRenderPass
- Add TerritoryRenderer orchestrator with dependency-based execution ordering
- Add WebGPUDevice for device initialization and management
- Add ShaderLoader utility for loading .wgsl files via Vite ?raw imports

Performance Optimizations:
- Dependency order computed once at init (topological sort)
- Early exit checks at orchestrator and pass levels
- Bind groups rebuilt when textures/buffers are recreated
- Zero per-frame allocations (reuse command encoders and staging buffers)

Architecture Benefits:
- Easy to extend with new compute/render passes (borders, temporal smoothing, etc.)
- Clear separation between tick-based compute and frame-based rendering
- All shaders in external files for better maintainability
- Ground truth data computed once and reused by all passes

Related Changes:
- Add defended tile state support to GameMap (isDefended/setDefended)
- Expose tileStateView() for direct GPU state access
- Extract hover info logic to HoverInfo utility
- Remove TerrainLayer (terrain now rendered by WebGPU territory pass)
- Update GameRenderer to use transparent overlay canvas
- Add viewOffset() method to TransformHandler

Files:
- Deleted: TerritoryWebGLRenderer.ts (1217 lines), TerrainLayer.ts (77 lines)
- Added: 17 new files in webgpu/ directory structure
- Updated: TerritoryLayer.ts, GameRenderer.ts, PlayerInfoOverlay.ts,
  GameMap.ts, GameView.ts, GameImpl.ts, TransformHandler.ts, vite-env.d.ts
This commit is contained in:
scamiv
2026-01-16 21:02:59 +01:00
parent aeb8d60224
commit 7cdf1b8160
25 changed files with 2264 additions and 795 deletions
+9 -1
View File
@@ -958,7 +958,6 @@ export class GameImpl implements Game {
playerID: id,
});
}
addUnit(u: Unit) {
this.unitGrid.addUnit(u);
this._unitMap.set(u.id(), u);
@@ -1097,6 +1096,12 @@ export class GameImpl implements Game {
hasFallout(ref: TileRef): boolean {
return this._map.hasFallout(ref);
}
isDefended(ref: TileRef): boolean {
return this._map.isDefended(ref);
}
setDefended(ref: TileRef, value: boolean): void {
this._map.setDefended(ref, value);
}
isBorder(ref: TileRef): boolean {
return this._map.isBorder(ref);
}
@@ -1155,6 +1160,9 @@ export class GameImpl implements Game {
updateTile(tile: TileRef, state: number): boolean {
return this._map.updateTile(tile, state);
}
tileStateView(): Uint16Array {
return this._map.tileStateView();
}
numTilesWithFallout(): number {
return this._map.numTilesWithFallout();
}
+20
View File
@@ -33,6 +33,9 @@ export interface GameMap {
setOwnerID(ref: TileRef, playerId: number): void;
hasFallout(ref: TileRef): boolean;
setFallout(ref: TileRef, value: boolean): void;
isDefended(ref: TileRef): boolean;
setDefended(ref: TileRef, value: boolean): void;
tileStateView(): Uint16Array;
isOnEdgeOfMap(ref: TileRef): boolean;
isBorder(ref: TileRef): boolean;
neighbors(ref: TileRef): TileRef[];
@@ -96,6 +99,7 @@ export class GameMapImpl implements GameMap {
// State bits (Uint16Array)
private static readonly PLAYER_ID_MASK = 0xfff;
private static readonly DEFENDED_BIT = 12;
private static readonly FALLOUT_BIT = 13;
private static readonly DEFENSE_BONUS_BIT = 14;
// Bit 15 still reserved
@@ -266,6 +270,22 @@ export class GameMapImpl implements GameMap {
}
}
isDefended(ref: TileRef): boolean {
return Boolean(this.state[ref] & (1 << GameMapImpl.DEFENDED_BIT));
}
setDefended(ref: TileRef, value: boolean): void {
if (value) {
this.state[ref] |= 1 << GameMapImpl.DEFENDED_BIT;
} else {
this.state[ref] &= ~(1 << GameMapImpl.DEFENDED_BIT);
}
}
tileStateView(): Uint16Array {
return this.state;
}
isOnEdgeOfMap(ref: TileRef): boolean {
const x = this.x(ref);
const y = this.y(ref);
+9
View File
@@ -1323,6 +1323,15 @@ export class GameView implements GameMap {
setFallout(ref: TileRef, value: boolean): void {
return this._map.setFallout(ref, value);
}
isDefended(ref: TileRef): boolean {
return this._map.isDefended(ref);
}
setDefended(ref: TileRef, value: boolean): void {
return this._map.setDefended(ref, value);
}
tileStateView(): Uint16Array {
return this._map.tileStateView();
}
isBorder(ref: TileRef): boolean {
return this._map.isBorder(ref);
}