mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-25 08:24:36 +00:00
bcd1412f75
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
160 lines
4.1 KiB
TypeScript
160 lines
4.1 KiB
TypeScript
import { GroundTruthData } from "../core/GroundTruthData";
|
|
import { loadShader } from "../core/ShaderLoader";
|
|
import { ComputePass } from "./ComputePass";
|
|
|
|
/**
|
|
* Compute pass that updates the defended texture from defense posts.
|
|
*/
|
|
export class DefendedUpdatePass implements ComputePass {
|
|
name = "defended-update";
|
|
dependencies: string[] = ["state-update"];
|
|
|
|
private pipeline: GPUComputePipeline | null = null;
|
|
private bindGroupLayout: GPUBindGroupLayout | null = null;
|
|
private bindGroup: GPUBindGroup | null = null;
|
|
private device: GPUDevice | null = null;
|
|
private resources: GroundTruthData | null = null;
|
|
private needsRebuild = true;
|
|
|
|
async init(device: GPUDevice, resources: GroundTruthData): Promise<void> {
|
|
this.device = device;
|
|
this.resources = resources;
|
|
|
|
const shaderCode = await loadShader("compute/defended-update.wgsl");
|
|
const shaderModule = device.createShaderModule({ code: shaderCode });
|
|
|
|
this.bindGroupLayout = device.createBindGroupLayout({
|
|
entries: [
|
|
{
|
|
binding: 0,
|
|
visibility: 4 /* COMPUTE */,
|
|
buffer: { type: "uniform" },
|
|
},
|
|
{
|
|
binding: 1,
|
|
visibility: 4 /* COMPUTE */,
|
|
buffer: { type: "read-only-storage" },
|
|
},
|
|
{
|
|
binding: 2,
|
|
visibility: 4 /* COMPUTE */,
|
|
texture: { sampleType: "uint" },
|
|
},
|
|
{
|
|
binding: 3,
|
|
visibility: 4 /* COMPUTE */,
|
|
storageTexture: { format: "r32uint" },
|
|
},
|
|
],
|
|
});
|
|
|
|
this.pipeline = device.createComputePipeline({
|
|
layout: device.createPipelineLayout({
|
|
bindGroupLayouts: [this.bindGroupLayout],
|
|
}),
|
|
compute: {
|
|
module: shaderModule,
|
|
entryPoint: "main",
|
|
},
|
|
});
|
|
}
|
|
|
|
needsUpdate(): boolean {
|
|
if (!this.resources || !this.needsRebuild) {
|
|
return false;
|
|
}
|
|
|
|
// Only run if we have defense posts
|
|
return this.resources.getDefensePostsCount() > 0;
|
|
}
|
|
|
|
execute(encoder: GPUCommandEncoder, resources: GroundTruthData): void {
|
|
if (!this.device || !this.pipeline) {
|
|
return;
|
|
}
|
|
|
|
const range = resources.getGame().config().defensePostRange();
|
|
const postsCount = resources.getDefensePostsCount();
|
|
|
|
if (postsCount === 0) {
|
|
this.needsRebuild = false;
|
|
return;
|
|
}
|
|
|
|
// Epoch is incremented by orchestrator before this pass runs
|
|
resources.writeDefenseParamsBuffer();
|
|
|
|
const oldBuffer = this.resources?.defensePostsBuffer;
|
|
const bufferChanged = oldBuffer !== resources.defensePostsBuffer;
|
|
|
|
if (bufferChanged) {
|
|
this.rebuildBindGroup();
|
|
}
|
|
|
|
if (!this.bindGroup) {
|
|
return;
|
|
}
|
|
|
|
const gridSize = 2 * range + 1;
|
|
const workgroupCount = Math.ceil(gridSize / 8);
|
|
|
|
const pass = encoder.beginComputePass();
|
|
pass.setPipeline(this.pipeline);
|
|
pass.setBindGroup(0, this.bindGroup);
|
|
pass.dispatchWorkgroups(workgroupCount, workgroupCount, postsCount);
|
|
pass.end();
|
|
|
|
this.needsRebuild = false;
|
|
}
|
|
|
|
private rebuildBindGroup(): void {
|
|
if (
|
|
!this.device ||
|
|
!this.bindGroupLayout ||
|
|
!this.resources ||
|
|
!this.resources.defenseParamsBuffer ||
|
|
!this.resources.defensePostsBuffer ||
|
|
!this.resources.stateTexture ||
|
|
!this.resources.defendedTexture ||
|
|
this.resources.getDefensePostsCount() <= 0
|
|
) {
|
|
this.bindGroup = null;
|
|
return;
|
|
}
|
|
|
|
this.bindGroup = this.device.createBindGroup({
|
|
layout: this.bindGroupLayout,
|
|
entries: [
|
|
{
|
|
binding: 0,
|
|
resource: { buffer: this.resources.defenseParamsBuffer },
|
|
},
|
|
{
|
|
binding: 1,
|
|
resource: { buffer: this.resources.defensePostsBuffer },
|
|
},
|
|
{
|
|
binding: 2,
|
|
resource: this.resources.stateTexture.createView(),
|
|
},
|
|
{
|
|
binding: 3,
|
|
resource: this.resources.defendedTexture.createView(),
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
markDirty(): void {
|
|
this.needsRebuild = true;
|
|
}
|
|
|
|
dispose(): void {
|
|
this.pipeline = null;
|
|
this.bindGroupLayout = null;
|
|
this.bindGroup = null;
|
|
this.device = null;
|
|
this.resources = null;
|
|
}
|
|
}
|