mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-21 12:35:05 +00:00
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:
@@ -0,0 +1,189 @@
|
||||
import { GroundTruthData } from "../core/GroundTruthData";
|
||||
import { loadShader } from "../core/ShaderLoader";
|
||||
import { RenderPass } from "./RenderPass";
|
||||
|
||||
/**
|
||||
* Main territory rendering pass.
|
||||
* Renders territory colors, defended tiles, fallout, and hover highlights.
|
||||
*/
|
||||
export class TerritoryRenderPass implements RenderPass {
|
||||
name = "territory";
|
||||
dependencies: string[] = [];
|
||||
|
||||
private pipeline: GPURenderPipeline | null = null;
|
||||
private bindGroupLayout: GPUBindGroupLayout | null = null;
|
||||
private bindGroup: GPUBindGroup | null = null;
|
||||
private device: GPUDevice | null = null;
|
||||
private resources: GroundTruthData | null = null;
|
||||
private canvasFormat: GPUTextureFormat | null = null;
|
||||
private clearR = 0;
|
||||
private clearG = 0;
|
||||
private clearB = 0;
|
||||
|
||||
async init(
|
||||
device: GPUDevice,
|
||||
resources: GroundTruthData,
|
||||
canvasFormat: GPUTextureFormat,
|
||||
): Promise<void> {
|
||||
this.device = device;
|
||||
this.resources = resources;
|
||||
this.canvasFormat = canvasFormat;
|
||||
|
||||
const shaderCode = await loadShader("render/territory.wgsl");
|
||||
const shaderModule = device.createShaderModule({ code: shaderCode });
|
||||
|
||||
this.bindGroupLayout = device.createBindGroupLayout({
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: 2 /* FRAGMENT */,
|
||||
buffer: { type: "uniform" },
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
visibility: 2 /* FRAGMENT */,
|
||||
buffer: { type: "uniform" },
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
visibility: 2 /* FRAGMENT */,
|
||||
texture: { sampleType: "uint" },
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
visibility: 2 /* FRAGMENT */,
|
||||
texture: { sampleType: "uint" },
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
visibility: 2 /* FRAGMENT */,
|
||||
texture: { sampleType: "float" },
|
||||
},
|
||||
{
|
||||
binding: 5,
|
||||
visibility: 2 /* FRAGMENT */,
|
||||
texture: { sampleType: "float" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.pipeline = device.createRenderPipeline({
|
||||
layout: device.createPipelineLayout({
|
||||
bindGroupLayouts: [this.bindGroupLayout],
|
||||
}),
|
||||
vertex: { module: shaderModule, entryPoint: "vsMain" },
|
||||
fragment: {
|
||||
module: shaderModule,
|
||||
entryPoint: "fsMain",
|
||||
targets: [{ format: canvasFormat }],
|
||||
},
|
||||
primitive: { topology: "triangle-list" },
|
||||
});
|
||||
|
||||
this.rebuildBindGroup();
|
||||
|
||||
// Extract clear color from theme
|
||||
const bg = resources.getTheme().backgroundColor().rgba;
|
||||
this.clearR = bg.r / 255;
|
||||
this.clearG = bg.g / 255;
|
||||
this.clearB = bg.b / 255;
|
||||
}
|
||||
|
||||
needsUpdate(): boolean {
|
||||
// Always run every frame (can be optimized later if needed)
|
||||
return true;
|
||||
}
|
||||
|
||||
execute(
|
||||
encoder: GPUCommandEncoder,
|
||||
resources: GroundTruthData,
|
||||
target: GPUTextureView,
|
||||
): void {
|
||||
if (!this.device || !this.pipeline) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Rebuild bind group if needed (e.g., after texture recreation)
|
||||
this.rebuildBindGroup();
|
||||
|
||||
if (!this.bindGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update uniforms
|
||||
resources.writeUniformBuffer(performance.now() / 1000);
|
||||
resources.writeDefenseParamsBuffer();
|
||||
|
||||
const pass = encoder.beginRenderPass({
|
||||
colorAttachments: [
|
||||
{
|
||||
view: target,
|
||||
loadOp: "clear",
|
||||
storeOp: "store",
|
||||
clearValue: {
|
||||
r: this.clearR,
|
||||
g: this.clearG,
|
||||
b: this.clearB,
|
||||
a: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
pass.setPipeline(this.pipeline);
|
||||
pass.setBindGroup(0, this.bindGroup);
|
||||
pass.draw(3);
|
||||
pass.end();
|
||||
}
|
||||
|
||||
rebuildBindGroup(): void {
|
||||
if (
|
||||
!this.device ||
|
||||
!this.bindGroupLayout ||
|
||||
!this.resources ||
|
||||
!this.resources.uniformBuffer ||
|
||||
!this.resources.defenseParamsBuffer ||
|
||||
!this.resources.stateTexture ||
|
||||
!this.resources.defendedTexture ||
|
||||
!this.resources.paletteTexture ||
|
||||
!this.resources.terrainTexture
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bindGroup = this.device.createBindGroup({
|
||||
layout: this.bindGroupLayout,
|
||||
entries: [
|
||||
{ binding: 0, resource: { buffer: this.resources.uniformBuffer } },
|
||||
{
|
||||
binding: 1,
|
||||
resource: { buffer: this.resources.defenseParamsBuffer },
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: this.resources.stateTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
resource: this.resources.defendedTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
resource: this.resources.paletteTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 5,
|
||||
resource: this.resources.terrainTexture.createView(),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.pipeline = null;
|
||||
this.bindGroupLayout = null;
|
||||
this.bindGroup = null;
|
||||
this.device = null;
|
||||
this.resources = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user