mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 15:20:43 +00:00
replace defended epoch stamping with defended-strength field
Store defended influence in defendedStrengthTexture and sample it in territory render shader Recompute defended strength on tick for state-updated tiles and for post-change dirty tiles, with full-map fallback when diffs are large Pack defense posts by owner on GPU (owner offsets + posts buffer) Remove old defended clear/update passes and epoch-based params
This commit is contained in:
@@ -3,8 +3,8 @@ import { TileRef } from "../../../core/game/GameMap";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { createCanvas } from "../../Utils";
|
||||
import { ComputePass } from "./compute/ComputePass";
|
||||
import { DefendedClearPass } from "./compute/DefendedClearPass";
|
||||
import { DefendedUpdatePass } from "./compute/DefendedUpdatePass";
|
||||
import { DefendedStrengthFullPass } from "./compute/DefendedStrengthFullPass";
|
||||
import { DefendedStrengthPass } from "./compute/DefendedStrengthPass";
|
||||
import { StateUpdatePass } from "./compute/StateUpdatePass";
|
||||
import { TerrainComputePass } from "./compute/TerrainComputePass";
|
||||
import { GroundTruthData } from "./core/GroundTruthData";
|
||||
@@ -40,13 +40,10 @@ export class TerritoryRenderer {
|
||||
// Pass instances
|
||||
private terrainComputePass: TerrainComputePass | null = null;
|
||||
private stateUpdatePass: StateUpdatePass | null = null;
|
||||
private defendedClearPass: DefendedClearPass | null = null;
|
||||
private defendedUpdatePass: DefendedUpdatePass | null = null;
|
||||
private defendedStrengthFullPass: DefendedStrengthFullPass | null = null;
|
||||
private defendedStrengthPass: DefendedStrengthPass | null = null;
|
||||
private territoryRenderPass: TerritoryRenderPass | null = null;
|
||||
|
||||
// State tracking
|
||||
private needsDefendedRebuild = true;
|
||||
private needsDefendedHardClear = true;
|
||||
private readonly defensePostRange: number;
|
||||
|
||||
private constructor(
|
||||
private readonly game: GameView,
|
||||
@@ -56,6 +53,7 @@ export class TerritoryRenderer {
|
||||
this.canvas.style.pointerEvents = "none";
|
||||
this.canvas.width = 1;
|
||||
this.canvas.height = 1;
|
||||
this.defensePostRange = game.config().defensePostRange();
|
||||
}
|
||||
|
||||
static create(game: GameView, theme: Theme): TerritoryWebGLCreateResult {
|
||||
@@ -108,14 +106,14 @@ export class TerritoryRenderer {
|
||||
// Create compute passes (terrain compute should run first)
|
||||
this.terrainComputePass = new TerrainComputePass();
|
||||
this.stateUpdatePass = new StateUpdatePass();
|
||||
this.defendedClearPass = new DefendedClearPass();
|
||||
this.defendedUpdatePass = new DefendedUpdatePass();
|
||||
this.defendedStrengthFullPass = new DefendedStrengthFullPass();
|
||||
this.defendedStrengthPass = new DefendedStrengthPass();
|
||||
|
||||
this.computePasses = [
|
||||
this.terrainComputePass,
|
||||
this.stateUpdatePass,
|
||||
this.defendedClearPass,
|
||||
this.defendedUpdatePass,
|
||||
this.defendedStrengthFullPass,
|
||||
this.defendedStrengthPass,
|
||||
];
|
||||
|
||||
// Create render passes
|
||||
@@ -236,10 +234,7 @@ export class TerritoryRenderer {
|
||||
}
|
||||
|
||||
markAllDirty(): void {
|
||||
this.needsDefendedRebuild = true;
|
||||
if (this.defendedUpdatePass) {
|
||||
this.defendedUpdatePass.markDirty();
|
||||
}
|
||||
this.resources?.markDefensePostsDirty();
|
||||
}
|
||||
|
||||
refreshPalette(): void {
|
||||
@@ -254,10 +249,6 @@ export class TerritoryRenderer {
|
||||
return;
|
||||
}
|
||||
this.resources.markDefensePostsDirty();
|
||||
this.needsDefendedRebuild = true;
|
||||
if (this.defendedUpdatePass) {
|
||||
this.defendedUpdatePass.markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
refreshTerrain(): void {
|
||||
@@ -313,73 +304,31 @@ export class TerritoryRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.game.config().defensePostRange() !== this.defensePostRange) {
|
||||
throw new Error("defensePostRange changed at runtime; unsupported.");
|
||||
}
|
||||
|
||||
// Upload palette if needed
|
||||
this.resources.uploadPalette();
|
||||
|
||||
// Upload defense posts if needed
|
||||
// Upload defense posts if needed (also produces defended dirty tiles on changes)
|
||||
this.resources.uploadDefensePosts();
|
||||
|
||||
// Initial state upload
|
||||
this.resources.uploadState();
|
||||
|
||||
// Check if we need to run compute passes
|
||||
const hasStateUpdates = this.stateUpdatePass
|
||||
? this.stateUpdatePass.needsUpdate()
|
||||
: false;
|
||||
const needsTerrainCompute = this.terrainComputePass
|
||||
? this.terrainComputePass.needsUpdate()
|
||||
: false;
|
||||
const range = this.game.config().defensePostRange();
|
||||
const rangeChanged = range !== this.resources.getLastDefenseRange();
|
||||
const countChanged =
|
||||
this.resources.getDefensePostsCount() !==
|
||||
this.resources.getLastDefensePostsCount();
|
||||
const hasPosts = this.resources.getDefensePostsCount() > 0;
|
||||
|
||||
// Use explicit boolean checks to satisfy linter (|| is correct for boolean OR)
|
||||
const shouldRebuildDefended =
|
||||
this.needsDefendedRebuild === true ||
|
||||
rangeChanged === true ||
|
||||
countChanged === true ||
|
||||
(hasPosts && hasStateUpdates === true);
|
||||
|
||||
const needsCompute =
|
||||
needsTerrainCompute === true ||
|
||||
hasStateUpdates === true ||
|
||||
shouldRebuildDefended === true ||
|
||||
this.needsDefendedHardClear === true;
|
||||
(this.terrainComputePass?.needsUpdate() ?? false) ||
|
||||
(this.stateUpdatePass?.needsUpdate() ?? false) ||
|
||||
(this.defendedStrengthFullPass?.needsUpdate() ?? false) ||
|
||||
(this.defendedStrengthPass?.needsUpdate() ?? false);
|
||||
|
||||
// Update defense params even if we early-out
|
||||
if (!needsCompute) {
|
||||
this.resources.writeDefenseParamsBuffer();
|
||||
this.resources.setLastDefenseRange(range);
|
||||
this.resources.setLastDefensePostsCount(
|
||||
this.resources.getDefensePostsCount(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const encoder = this.device.device.createCommandEncoder();
|
||||
|
||||
// Handle defended rebuild (before executing passes)
|
||||
if (shouldRebuildDefended) {
|
||||
// Hard-clear defended texture before restamping. This avoids relying on
|
||||
// epoch-stamping for correctness and prevents transient mismatches where
|
||||
// defended rendering disappears between rebuilds.
|
||||
this.needsDefendedHardClear = true;
|
||||
|
||||
if (this.defendedUpdatePass) {
|
||||
this.defendedUpdatePass.markDirty();
|
||||
}
|
||||
|
||||
this.needsDefendedRebuild = false;
|
||||
}
|
||||
|
||||
// Update hard clear flag for DefendedClearPass
|
||||
if (this.defendedClearPass) {
|
||||
this.defendedClearPass.setNeedsHardClear(this.needsDefendedHardClear);
|
||||
}
|
||||
|
||||
// Execute compute passes in dependency order (clear will run before update if needed)
|
||||
for (const pass of this.computePassOrder) {
|
||||
if (!pass.needsUpdate()) {
|
||||
@@ -388,18 +337,6 @@ export class TerritoryRenderer {
|
||||
pass.execute(encoder, this.resources);
|
||||
}
|
||||
|
||||
// After all passes, update defense params and clear flags
|
||||
this.resources.writeDefenseParamsBuffer();
|
||||
if (this.needsDefendedHardClear && this.defendedClearPass) {
|
||||
this.needsDefendedHardClear = false;
|
||||
this.defendedClearPass.setNeedsHardClear(false);
|
||||
}
|
||||
|
||||
this.resources.setLastDefenseRange(range);
|
||||
this.resources.setLastDefensePostsCount(
|
||||
this.resources.getDefensePostsCount(),
|
||||
);
|
||||
|
||||
this.device.device.queue.submit([encoder.finish()]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { GroundTruthData } from "../core/GroundTruthData";
|
||||
import { loadShader } from "../core/ShaderLoader";
|
||||
import { ComputePass } from "./ComputePass";
|
||||
|
||||
/**
|
||||
* Compute pass that clears the defended texture (sets all texels to 0).
|
||||
* Used for initial clear and epoch wrap scenarios.
|
||||
*/
|
||||
export class DefendedClearPass implements ComputePass {
|
||||
name = "defended-clear";
|
||||
dependencies: string[] = [];
|
||||
|
||||
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 needsHardClear = true;
|
||||
|
||||
async init(device: GPUDevice, resources: GroundTruthData): Promise<void> {
|
||||
this.device = device;
|
||||
this.resources = resources;
|
||||
|
||||
const shaderCode = await loadShader("compute/defended-clear.wgsl");
|
||||
const shaderModule = device.createShaderModule({ code: shaderCode });
|
||||
|
||||
this.bindGroupLayout = device.createBindGroupLayout({
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
storageTexture: { format: "r32uint" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.pipeline = device.createComputePipeline({
|
||||
layout: device.createPipelineLayout({
|
||||
bindGroupLayouts: [this.bindGroupLayout],
|
||||
}),
|
||||
compute: {
|
||||
module: shaderModule,
|
||||
entryPoint: "main",
|
||||
},
|
||||
});
|
||||
|
||||
this.rebuildBindGroup();
|
||||
}
|
||||
|
||||
needsUpdate(): boolean {
|
||||
return this.needsHardClear;
|
||||
}
|
||||
|
||||
execute(encoder: GPUCommandEncoder, resources: GroundTruthData): void {
|
||||
if (!this.device || !this.pipeline || !this.bindGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mapWidth = resources.getMapWidth();
|
||||
const mapHeight = resources.getMapHeight();
|
||||
const workgroupCountX = Math.ceil(mapWidth / 8);
|
||||
const workgroupCountY = Math.ceil(mapHeight / 8);
|
||||
|
||||
const pass = encoder.beginComputePass();
|
||||
pass.setPipeline(this.pipeline);
|
||||
pass.setBindGroup(0, this.bindGroup);
|
||||
pass.dispatchWorkgroups(workgroupCountX, workgroupCountY);
|
||||
pass.end();
|
||||
|
||||
this.needsHardClear = false;
|
||||
}
|
||||
|
||||
private rebuildBindGroup(): void {
|
||||
if (
|
||||
!this.device ||
|
||||
!this.bindGroupLayout ||
|
||||
!this.resources ||
|
||||
!this.resources.defendedTexture
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bindGroup = this.device.createBindGroup({
|
||||
layout: this.bindGroupLayout,
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: this.resources.defendedTexture.createView(),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
setNeedsHardClear(value: boolean): void {
|
||||
this.needsHardClear = value;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.pipeline = null;
|
||||
this.bindGroupLayout = null;
|
||||
this.bindGroup = null;
|
||||
this.device = null;
|
||||
this.resources = null;
|
||||
}
|
||||
}
|
||||
+43
-43
@@ -3,10 +3,11 @@ import { loadShader } from "../core/ShaderLoader";
|
||||
import { ComputePass } from "./ComputePass";
|
||||
|
||||
/**
|
||||
* Compute pass that updates the defended texture from defense posts.
|
||||
* Full defended strength recompute across the entire map.
|
||||
* Used on initial upload or when post diffs are too large for a tile list.
|
||||
*/
|
||||
export class DefendedUpdatePass implements ComputePass {
|
||||
name = "defended-update";
|
||||
export class DefendedStrengthFullPass implements ComputePass {
|
||||
name = "defended-strength-full";
|
||||
dependencies: string[] = ["state-update"];
|
||||
|
||||
private pipeline: GPUComputePipeline | null = null;
|
||||
@@ -14,13 +15,13 @@ export class DefendedUpdatePass implements ComputePass {
|
||||
private bindGroup: GPUBindGroup | null = null;
|
||||
private device: GPUDevice | null = null;
|
||||
private resources: GroundTruthData | null = null;
|
||||
private needsRebuild = true;
|
||||
private boundPostsByOwnerBuffer: GPUBuffer | null = null;
|
||||
|
||||
async init(device: GPUDevice, resources: GroundTruthData): Promise<void> {
|
||||
this.device = device;
|
||||
this.resources = resources;
|
||||
|
||||
const shaderCode = await loadShader("compute/defended-update.wgsl");
|
||||
const shaderCode = await loadShader("compute/defended-strength-full.wgsl");
|
||||
const shaderModule = device.createShaderModule({ code: shaderCode });
|
||||
|
||||
this.bindGroupLayout = device.createBindGroupLayout({
|
||||
@@ -33,17 +34,22 @@ export class DefendedUpdatePass implements ComputePass {
|
||||
{
|
||||
binding: 1,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
buffer: { type: "read-only-storage" },
|
||||
texture: { sampleType: "uint" },
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
texture: { sampleType: "uint" },
|
||||
storageTexture: { format: "rgba8unorm" },
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
storageTexture: { format: "r32uint" },
|
||||
buffer: { type: "read-only-storage" },
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
buffer: { type: "read-only-storage" },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -60,12 +66,7 @@ export class DefendedUpdatePass implements ComputePass {
|
||||
}
|
||||
|
||||
needsUpdate(): boolean {
|
||||
if (!this.resources || !this.needsRebuild) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only run if we have defense posts
|
||||
return this.resources.getDefensePostsCount() > 0;
|
||||
return this.resources?.needsDefendedFullRecompute() ?? false;
|
||||
}
|
||||
|
||||
execute(encoder: GPUCommandEncoder, resources: GroundTruthData): void {
|
||||
@@ -73,38 +74,35 @@ export class DefendedUpdatePass implements ComputePass {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = resources.getGame().config().defensePostRange();
|
||||
const postsCount = resources.getDefensePostsCount();
|
||||
|
||||
if (postsCount === 0) {
|
||||
this.needsRebuild = false;
|
||||
if (!resources.needsDefendedFullRecompute()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Epoch is incremented by orchestrator before this pass runs
|
||||
resources.writeDefenseParamsBuffer();
|
||||
resources.writeDefendedStrengthParamsBuffer(0);
|
||||
|
||||
const oldBuffer = this.resources?.defensePostsBuffer;
|
||||
const bufferChanged = oldBuffer !== resources.defensePostsBuffer;
|
||||
|
||||
if (bufferChanged || !this.bindGroup) {
|
||||
const postsByOwnerBuffer = resources.defensePostsByOwnerBuffer;
|
||||
if (
|
||||
!this.bindGroup ||
|
||||
this.boundPostsByOwnerBuffer !== postsByOwnerBuffer
|
||||
) {
|
||||
this.rebuildBindGroup();
|
||||
}
|
||||
|
||||
if (!this.bindGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gridSize = 2 * range + 1;
|
||||
const workgroupCount = Math.ceil(gridSize / 8);
|
||||
const mapWidth = resources.getMapWidth();
|
||||
const mapHeight = resources.getMapHeight();
|
||||
const workgroupCountX = Math.ceil(mapWidth / 8);
|
||||
const workgroupCountY = Math.ceil(mapHeight / 8);
|
||||
|
||||
const pass = encoder.beginComputePass();
|
||||
pass.setPipeline(this.pipeline);
|
||||
pass.setBindGroup(0, this.bindGroup);
|
||||
pass.dispatchWorkgroups(workgroupCount, workgroupCount, postsCount);
|
||||
pass.dispatchWorkgroups(workgroupCountX, workgroupCountY);
|
||||
pass.end();
|
||||
|
||||
this.needsRebuild = false;
|
||||
resources.clearDefendedFullRecompute();
|
||||
}
|
||||
|
||||
private rebuildBindGroup(): void {
|
||||
@@ -112,11 +110,11 @@ export class DefendedUpdatePass implements ComputePass {
|
||||
!this.device ||
|
||||
!this.bindGroupLayout ||
|
||||
!this.resources ||
|
||||
!this.resources.defenseParamsBuffer ||
|
||||
!this.resources.defensePostsBuffer ||
|
||||
!this.resources.defendedStrengthParamsBuffer ||
|
||||
!this.resources.stateTexture ||
|
||||
!this.resources.defendedTexture ||
|
||||
this.resources.getDefensePostsCount() <= 0
|
||||
!this.resources.defendedStrengthTexture ||
|
||||
!this.resources.defenseOwnerOffsetsBuffer ||
|
||||
!this.resources.defensePostsByOwnerBuffer
|
||||
) {
|
||||
this.bindGroup = null;
|
||||
return;
|
||||
@@ -127,26 +125,28 @@ export class DefendedUpdatePass implements ComputePass {
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: { buffer: this.resources.defenseParamsBuffer },
|
||||
resource: { buffer: this.resources.defendedStrengthParamsBuffer },
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
resource: { buffer: this.resources.defensePostsBuffer },
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: this.resources.stateTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: this.resources.defendedStrengthTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
resource: this.resources.defendedTexture.createView(),
|
||||
resource: { buffer: this.resources.defenseOwnerOffsetsBuffer },
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
resource: { buffer: this.resources.defensePostsByOwnerBuffer },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
markDirty(): void {
|
||||
this.needsRebuild = true;
|
||||
this.boundPostsByOwnerBuffer = this.resources.defensePostsByOwnerBuffer;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
@@ -0,0 +1,172 @@
|
||||
import { GroundTruthData } from "../core/GroundTruthData";
|
||||
import { loadShader } from "../core/ShaderLoader";
|
||||
import { ComputePass } from "./ComputePass";
|
||||
|
||||
/**
|
||||
* Recomputes defended strength for a list of dirty tiles.
|
||||
* Dirty tiles are produced when defense posts are added/removed/moved.
|
||||
*/
|
||||
export class DefendedStrengthPass implements ComputePass {
|
||||
name = "defended-strength";
|
||||
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 boundDirtyTilesBuffer: GPUBuffer | null = null;
|
||||
private boundPostsByOwnerBuffer: GPUBuffer | null = null;
|
||||
|
||||
async init(device: GPUDevice, resources: GroundTruthData): Promise<void> {
|
||||
this.device = device;
|
||||
this.resources = resources;
|
||||
|
||||
const shaderCode = await loadShader("compute/defended-strength.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: "rgba8unorm" },
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
buffer: { type: "read-only-storage" },
|
||||
},
|
||||
{
|
||||
binding: 5,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
buffer: { type: "read-only-storage" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.pipeline = device.createComputePipeline({
|
||||
layout: device.createPipelineLayout({
|
||||
bindGroupLayouts: [this.bindGroupLayout],
|
||||
}),
|
||||
compute: {
|
||||
module: shaderModule,
|
||||
entryPoint: "main",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
needsUpdate(): boolean {
|
||||
return (this.resources?.getDefendedDirtyTilesCount() ?? 0) > 0;
|
||||
}
|
||||
|
||||
execute(encoder: GPUCommandEncoder, resources: GroundTruthData): void {
|
||||
if (!this.device || !this.pipeline) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dirtyCount = resources.getDefendedDirtyTilesCount();
|
||||
if (dirtyCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
resources.writeDefendedStrengthParamsBuffer(dirtyCount);
|
||||
|
||||
const dirtyTilesBuffer = resources.defendedDirtyTilesBuffer;
|
||||
const postsByOwnerBuffer = resources.defensePostsByOwnerBuffer;
|
||||
const shouldRebuildBindGroup =
|
||||
!this.bindGroup ||
|
||||
this.boundDirtyTilesBuffer !== dirtyTilesBuffer ||
|
||||
this.boundPostsByOwnerBuffer !== postsByOwnerBuffer;
|
||||
|
||||
if (shouldRebuildBindGroup) {
|
||||
this.rebuildBindGroup();
|
||||
}
|
||||
|
||||
if (!this.bindGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pass = encoder.beginComputePass();
|
||||
pass.setPipeline(this.pipeline);
|
||||
pass.setBindGroup(0, this.bindGroup);
|
||||
const workgroupCount = Math.ceil(dirtyCount / 64);
|
||||
pass.dispatchWorkgroups(workgroupCount);
|
||||
pass.end();
|
||||
|
||||
resources.clearDefendedDirtyTiles();
|
||||
}
|
||||
|
||||
private rebuildBindGroup(): void {
|
||||
if (
|
||||
!this.device ||
|
||||
!this.bindGroupLayout ||
|
||||
!this.resources ||
|
||||
!this.resources.defendedStrengthParamsBuffer ||
|
||||
!this.resources.defendedDirtyTilesBuffer ||
|
||||
!this.resources.stateTexture ||
|
||||
!this.resources.defendedStrengthTexture ||
|
||||
!this.resources.defenseOwnerOffsetsBuffer ||
|
||||
!this.resources.defensePostsByOwnerBuffer
|
||||
) {
|
||||
this.bindGroup = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.bindGroup = this.device.createBindGroup({
|
||||
layout: this.bindGroupLayout,
|
||||
entries: [
|
||||
{
|
||||
binding: 0,
|
||||
resource: { buffer: this.resources.defendedStrengthParamsBuffer },
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
resource: { buffer: this.resources.defendedDirtyTilesBuffer },
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
resource: this.resources.stateTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
resource: this.resources.defendedStrengthTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
resource: { buffer: this.resources.defenseOwnerOffsetsBuffer },
|
||||
},
|
||||
{
|
||||
binding: 5,
|
||||
resource: { buffer: this.resources.defensePostsByOwnerBuffer },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.boundDirtyTilesBuffer = this.resources.defendedDirtyTilesBuffer;
|
||||
this.boundPostsByOwnerBuffer = this.resources.defensePostsByOwnerBuffer;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.pipeline = null;
|
||||
this.bindGroupLayout = null;
|
||||
this.bindGroup = null;
|
||||
this.device = null;
|
||||
this.resources = null;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ export class StateUpdatePass implements ComputePass {
|
||||
private device: GPUDevice | null = null;
|
||||
private resources: GroundTruthData | null = null;
|
||||
private readonly pendingTiles: Set<number> = new Set();
|
||||
private boundUpdatesBuffer: GPUBuffer | null = null;
|
||||
private boundPostsByOwnerBuffer: GPUBuffer | null = null;
|
||||
|
||||
async init(device: GPUDevice, resources: GroundTruthData): Promise<void> {
|
||||
this.device = device;
|
||||
@@ -28,13 +30,33 @@ export class StateUpdatePass implements ComputePass {
|
||||
{
|
||||
binding: 0,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
buffer: { type: "read-only-storage" },
|
||||
buffer: { type: "uniform" },
|
||||
},
|
||||
{
|
||||
binding: 1,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
buffer: { type: "read-only-storage" },
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
storageTexture: { format: "r32uint" },
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
storageTexture: { format: "rgba8unorm" },
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
buffer: { type: "read-only-storage" },
|
||||
},
|
||||
{
|
||||
binding: 5,
|
||||
visibility: 4 /* COMPUTE */,
|
||||
buffer: { type: "read-only-storage" },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -65,9 +87,8 @@ export class StateUpdatePass implements ComputePass {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldBuffer = this.resources?.updatesBuffer;
|
||||
const updatesBuffer = resources.ensureUpdatesBuffer(numUpdates);
|
||||
const bufferChanged = oldBuffer !== updatesBuffer;
|
||||
resources.writeStateUpdateParamsBuffer(numUpdates);
|
||||
|
||||
const staging = resources.getUpdatesStaging();
|
||||
const state = resources.getState();
|
||||
@@ -88,8 +109,13 @@ export class StateUpdatePass implements ComputePass {
|
||||
staging.subarray(0, numUpdates * 2),
|
||||
);
|
||||
|
||||
// Rebuild bind group if buffer changed
|
||||
if (bufferChanged) {
|
||||
const postsByOwnerBuffer = resources.defensePostsByOwnerBuffer;
|
||||
const shouldRebuildBindGroup =
|
||||
!this.bindGroup ||
|
||||
this.boundUpdatesBuffer !== updatesBuffer ||
|
||||
this.boundPostsByOwnerBuffer !== postsByOwnerBuffer;
|
||||
|
||||
if (shouldRebuildBindGroup) {
|
||||
this.rebuildBindGroup();
|
||||
}
|
||||
|
||||
@@ -97,15 +123,12 @@ export class StateUpdatePass implements ComputePass {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.bindGroup) {
|
||||
const pass = encoder.beginComputePass();
|
||||
pass.setPipeline(this.pipeline);
|
||||
pass.setBindGroup(0, this.bindGroup);
|
||||
// Dispatch with workgroup_size(64), so divide by 64 and round up
|
||||
const workgroupCount = Math.ceil(numUpdates / 64);
|
||||
pass.dispatchWorkgroups(workgroupCount);
|
||||
pass.end();
|
||||
}
|
||||
const pass = encoder.beginComputePass();
|
||||
pass.setPipeline(this.pipeline);
|
||||
pass.setBindGroup(0, this.bindGroup);
|
||||
const workgroupCount = Math.ceil(numUpdates / 64);
|
||||
pass.dispatchWorkgroups(workgroupCount);
|
||||
pass.end();
|
||||
|
||||
this.pendingTiles.clear();
|
||||
}
|
||||
@@ -115,22 +138,46 @@ export class StateUpdatePass implements ComputePass {
|
||||
!this.device ||
|
||||
!this.bindGroupLayout ||
|
||||
!this.resources ||
|
||||
!this.resources.stateUpdateParamsBuffer ||
|
||||
!this.resources.updatesBuffer ||
|
||||
!this.resources.stateTexture
|
||||
!this.resources.stateTexture ||
|
||||
!this.resources.defendedStrengthTexture ||
|
||||
!this.resources.defenseOwnerOffsetsBuffer ||
|
||||
!this.resources.defensePostsByOwnerBuffer
|
||||
) {
|
||||
this.bindGroup = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.bindGroup = this.device.createBindGroup({
|
||||
layout: this.bindGroupLayout,
|
||||
entries: [
|
||||
{ binding: 0, resource: { buffer: this.resources.updatesBuffer } },
|
||||
{
|
||||
binding: 1,
|
||||
binding: 0,
|
||||
resource: { buffer: this.resources.stateUpdateParamsBuffer },
|
||||
},
|
||||
{ binding: 1, resource: { buffer: this.resources.updatesBuffer } },
|
||||
{
|
||||
binding: 2,
|
||||
resource: this.resources.stateTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
resource: this.resources.defendedStrengthTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
resource: { buffer: this.resources.defenseOwnerOffsetsBuffer },
|
||||
},
|
||||
{
|
||||
binding: 5,
|
||||
resource: { buffer: this.resources.defensePostsByOwnerBuffer },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
this.boundUpdatesBuffer = this.resources.updatesBuffer;
|
||||
this.boundPostsByOwnerBuffer = this.resources.defensePostsByOwnerBuffer;
|
||||
}
|
||||
|
||||
markTile(tile: number): void {
|
||||
|
||||
@@ -16,28 +16,35 @@ function align(value: number, alignment: number): number {
|
||||
export class GroundTruthData {
|
||||
public static readonly PALETTE_RESERVED_SLOTS = 10;
|
||||
public static readonly PALETTE_FALLOUT_INDEX = 0;
|
||||
private static readonly MAX_OWNER_SLOTS = 0x1000; // ownerId is 12 bits
|
||||
|
||||
// Textures
|
||||
public readonly stateTexture: GPUTexture;
|
||||
public readonly terrainTexture: GPUTexture;
|
||||
public readonly terrainDataTexture: GPUTexture;
|
||||
public readonly paletteTexture: GPUTexture;
|
||||
public readonly defendedTexture: GPUTexture;
|
||||
public readonly defendedStrengthTexture: GPUTexture;
|
||||
|
||||
// Buffers
|
||||
public readonly uniformBuffer: GPUBuffer;
|
||||
public readonly defenseParamsBuffer: GPUBuffer;
|
||||
public readonly terrainParamsBuffer: GPUBuffer;
|
||||
public readonly stateUpdateParamsBuffer: GPUBuffer;
|
||||
public readonly defendedStrengthParamsBuffer: GPUBuffer;
|
||||
public updatesBuffer: GPUBuffer | null = null;
|
||||
public defensePostsBuffer: GPUBuffer | null = null;
|
||||
public readonly defenseOwnerOffsetsBuffer: GPUBuffer;
|
||||
public defensePostsByOwnerBuffer: GPUBuffer;
|
||||
public defendedDirtyTilesBuffer: GPUBuffer;
|
||||
|
||||
// Staging arrays for buffer uploads
|
||||
private updatesStaging: Uint32Array | null = null;
|
||||
private defensePostsStaging: Uint32Array | null = null;
|
||||
private defenseOwnerOffsetsStaging: Uint32Array;
|
||||
private defensePostsByOwnerStaging: Uint32Array | null = null;
|
||||
private defendedDirtyTilesStaging: Uint32Array | null = null;
|
||||
|
||||
// Buffer capacities
|
||||
private updatesCapacity = 0;
|
||||
private defensePostsCapacity = 0;
|
||||
private defensePostsByOwnerCapacity = 0;
|
||||
private defendedDirtyTilesCapacity = 0;
|
||||
|
||||
// State tracking
|
||||
private readonly mapWidth: number;
|
||||
@@ -49,13 +56,19 @@ export class GroundTruthData {
|
||||
private needsTerrainDataUpload = true;
|
||||
private needsTerrainParamsUpload = true;
|
||||
private paletteWidth = 1;
|
||||
private defensePostsCount = 0;
|
||||
private needsDefensePostsUpload = true;
|
||||
private defensePostsTotalCount = 0;
|
||||
private defendedDirtyTilesCount = 0;
|
||||
private needsFullDefendedStrengthRecompute = false;
|
||||
private lastDefensePostKeys = new Set<string>();
|
||||
private defenseCircleRange = -1;
|
||||
private defenseCircleOffsets: Int16Array = new Int16Array(0); // [dx0, dy0, dx1, dy1, ...]
|
||||
|
||||
// Uniform data arrays
|
||||
private readonly uniformData = new Float32Array(12);
|
||||
private readonly defenseParamsData = new Uint32Array(4);
|
||||
private readonly terrainParamsData = new Float32Array(24); // 6 vec4f: shore, water, shorelineWater, plainsBase, highlandBase, mountainBase
|
||||
private readonly stateUpdateParamsData = new Uint32Array(4); // updateCount, range, pad, pad
|
||||
private readonly defendedStrengthParamsData = new Uint32Array(4); // dirtyCount, range, pad, pad
|
||||
|
||||
// View state (updated by renderer)
|
||||
private viewWidth = 1;
|
||||
@@ -66,11 +79,6 @@ export class GroundTruthData {
|
||||
private alternativeView = false;
|
||||
private highlightedOwnerId = -1;
|
||||
|
||||
// Defense state
|
||||
private defendedEpoch = 1;
|
||||
private lastDefenseRange = -1;
|
||||
private lastDefensePostsCount = -1;
|
||||
|
||||
private constructor(
|
||||
private readonly device: GPUDevice,
|
||||
private readonly game: GameView,
|
||||
@@ -99,8 +107,14 @@ export class GroundTruthData {
|
||||
usage: UNIFORM | COPY_DST_BUF,
|
||||
});
|
||||
|
||||
// Defense params: 4x u32 = 16 bytes
|
||||
this.defenseParamsBuffer = device.createBuffer({
|
||||
// State update params: 4x u32 = 16 bytes
|
||||
this.stateUpdateParamsBuffer = device.createBuffer({
|
||||
size: 16,
|
||||
usage: UNIFORM | COPY_DST_BUF,
|
||||
});
|
||||
|
||||
// Defended strength params: 4x u32 = 16 bytes
|
||||
this.defendedStrengthParamsBuffer = device.createBuffer({
|
||||
size: 16,
|
||||
usage: UNIFORM | COPY_DST_BUF,
|
||||
});
|
||||
@@ -118,10 +132,10 @@ export class GroundTruthData {
|
||||
usage: COPY_DST_TEX | TEXTURE_BINDING | STORAGE_BINDING,
|
||||
});
|
||||
|
||||
// Defended texture (r32uint)
|
||||
this.defendedTexture = device.createTexture({
|
||||
// Defended strength texture (rgba8unorm, r channel used)
|
||||
this.defendedStrengthTexture = device.createTexture({
|
||||
size: { width: mapWidth, height: mapHeight },
|
||||
format: "r32uint",
|
||||
format: "rgba8unorm",
|
||||
usage: TEXTURE_BINDING | STORAGE_BINDING,
|
||||
});
|
||||
|
||||
@@ -145,6 +159,28 @@ export class GroundTruthData {
|
||||
format: "r8uint",
|
||||
usage: COPY_DST_TEX | TEXTURE_BINDING,
|
||||
});
|
||||
|
||||
const STORAGE = GPUBufferUsage?.STORAGE ?? 0x10;
|
||||
|
||||
// Defense posts data: ownerOffsets[ownerId] = {start, count}, postsByOwner[start..] = {x,y}
|
||||
this.defenseOwnerOffsetsBuffer = device.createBuffer({
|
||||
size: GroundTruthData.MAX_OWNER_SLOTS * 8,
|
||||
usage: STORAGE | COPY_DST_BUF,
|
||||
});
|
||||
this.defenseOwnerOffsetsStaging = new Uint32Array(
|
||||
GroundTruthData.MAX_OWNER_SLOTS * 2,
|
||||
);
|
||||
|
||||
this.defensePostsByOwnerBuffer = device.createBuffer({
|
||||
size: 8,
|
||||
usage: STORAGE | COPY_DST_BUF,
|
||||
});
|
||||
|
||||
// Dirty tile indices to recompute defended strength when posts change
|
||||
this.defendedDirtyTilesBuffer = device.createBuffer({
|
||||
size: 4 * 8,
|
||||
usage: STORAGE | COPY_DST_BUF,
|
||||
});
|
||||
}
|
||||
|
||||
static create(
|
||||
@@ -471,30 +507,83 @@ export class GroundTruthData {
|
||||
}
|
||||
this.needsDefensePostsUpload = false;
|
||||
|
||||
const range = this.game.config().defensePostRange();
|
||||
const posts = this.collectDefensePosts();
|
||||
this.defensePostsCount = posts.length;
|
||||
this.defensePostsTotalCount = posts.length;
|
||||
|
||||
if (this.defensePostsCount > 0) {
|
||||
this.ensureDefensePostsBuffer(this.defensePostsCount);
|
||||
// Diff posts to produce dirty tiles for recompute (include removed + added).
|
||||
const nextKeys = new Set<string>();
|
||||
for (const p of posts) {
|
||||
nextKeys.add(`${p.ownerId},${p.x},${p.y}`);
|
||||
}
|
||||
|
||||
if (
|
||||
this.defensePostsCount > 0 &&
|
||||
this.defensePostsStaging &&
|
||||
this.defensePostsBuffer
|
||||
) {
|
||||
for (let i = 0; i < this.defensePostsCount; i++) {
|
||||
const p = posts[i];
|
||||
this.defensePostsStaging[i * 3] = p.x >>> 0;
|
||||
this.defensePostsStaging[i * 3 + 1] = p.y >>> 0;
|
||||
this.defensePostsStaging[i * 3 + 2] = p.ownerId >>> 0;
|
||||
const changedPosts: Array<{ x: number; y: number }> = [];
|
||||
for (const key of this.lastDefensePostKeys) {
|
||||
if (!nextKeys.has(key)) {
|
||||
const [ownerStr, xStr, yStr] = key.split(",");
|
||||
void ownerStr;
|
||||
changedPosts.push({ x: Number(xStr), y: Number(yStr) });
|
||||
}
|
||||
this.device.queue.writeBuffer(
|
||||
this.defensePostsBuffer,
|
||||
0,
|
||||
this.defensePostsStaging.subarray(0, this.defensePostsCount * 3),
|
||||
);
|
||||
}
|
||||
for (const key of nextKeys) {
|
||||
if (!this.lastDefensePostKeys.has(key)) {
|
||||
const [ownerStr, xStr, yStr] = key.split(",");
|
||||
void ownerStr;
|
||||
changedPosts.push({ x: Number(xStr), y: Number(yStr) });
|
||||
}
|
||||
}
|
||||
this.lastDefensePostKeys = nextKeys;
|
||||
|
||||
// Pack posts by owner into GPU buffers.
|
||||
this.packDefensePostsByOwner(posts);
|
||||
|
||||
// Build dirty tiles around changed posts (so removals clear too).
|
||||
this.buildDefendedDirtyTiles(changedPosts, range);
|
||||
}
|
||||
|
||||
getDefensePostsTotalCount(): number {
|
||||
return this.defensePostsTotalCount;
|
||||
}
|
||||
|
||||
getDefendedDirtyTilesCount(): number {
|
||||
return this.defendedDirtyTilesCount;
|
||||
}
|
||||
|
||||
needsDefendedFullRecompute(): boolean {
|
||||
return this.needsFullDefendedStrengthRecompute;
|
||||
}
|
||||
|
||||
clearDefendedFullRecompute(): void {
|
||||
this.needsFullDefendedStrengthRecompute = false;
|
||||
}
|
||||
|
||||
clearDefendedDirtyTiles(): void {
|
||||
this.defendedDirtyTilesCount = 0;
|
||||
}
|
||||
|
||||
writeStateUpdateParamsBuffer(updateCount: number): void {
|
||||
this.stateUpdateParamsData[0] = updateCount >>> 0;
|
||||
this.stateUpdateParamsData[1] = this.game.config().defensePostRange() >>> 0;
|
||||
this.stateUpdateParamsData[2] = 0;
|
||||
this.stateUpdateParamsData[3] = 0;
|
||||
this.device.queue.writeBuffer(
|
||||
this.stateUpdateParamsBuffer,
|
||||
0,
|
||||
this.stateUpdateParamsData,
|
||||
);
|
||||
}
|
||||
|
||||
writeDefendedStrengthParamsBuffer(dirtyCount: number): void {
|
||||
this.defendedStrengthParamsData[0] = dirtyCount >>> 0;
|
||||
this.defendedStrengthParamsData[1] =
|
||||
this.game.config().defensePostRange() >>> 0;
|
||||
this.defendedStrengthParamsData[2] = 0;
|
||||
this.defendedStrengthParamsData[3] = 0;
|
||||
this.device.queue.writeBuffer(
|
||||
this.defendedStrengthParamsBuffer,
|
||||
0,
|
||||
this.defendedStrengthParamsData,
|
||||
);
|
||||
}
|
||||
|
||||
private collectDefensePosts(): Array<{
|
||||
@@ -518,8 +607,11 @@ export class GroundTruthData {
|
||||
return posts;
|
||||
}
|
||||
|
||||
private ensureDefensePostsBuffer(capacity: number): void {
|
||||
if (this.defensePostsBuffer && capacity <= this.defensePostsCapacity) {
|
||||
private ensureDefensePostsByOwnerBuffer(capacityPosts: number): void {
|
||||
if (
|
||||
this.defensePostsByOwnerBuffer &&
|
||||
capacityPosts <= this.defensePostsByOwnerCapacity
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -527,24 +619,182 @@ export class GroundTruthData {
|
||||
const STORAGE = GPUBufferUsage?.STORAGE ?? 0x10;
|
||||
const COPY_DST_BUF = GPUBufferUsage?.COPY_DST ?? 0x8;
|
||||
|
||||
this.defensePostsCapacity = Math.max(
|
||||
this.defensePostsByOwnerCapacity = Math.max(
|
||||
8,
|
||||
Math.pow(2, Math.ceil(Math.log2(Math.max(1, capacity)))),
|
||||
Math.pow(2, Math.ceil(Math.log2(Math.max(1, capacityPosts)))),
|
||||
);
|
||||
|
||||
const bytesPerPost = 12; // 3 * u32
|
||||
const bufferSize = this.defensePostsCapacity * bytesPerPost;
|
||||
const bytesPerPost = 8; // 2 * u32 (x,y)
|
||||
const bufferSize = this.defensePostsByOwnerCapacity * bytesPerPost;
|
||||
|
||||
if (this.defensePostsBuffer) {
|
||||
(this.defensePostsBuffer as any).destroy?.();
|
||||
}
|
||||
|
||||
(this as any).defensePostsBuffer = this.device.createBuffer({
|
||||
(this.defensePostsByOwnerBuffer as any).destroy?.();
|
||||
this.defensePostsByOwnerBuffer = this.device.createBuffer({
|
||||
size: bufferSize,
|
||||
usage: STORAGE | COPY_DST_BUF,
|
||||
});
|
||||
|
||||
this.defensePostsStaging = new Uint32Array(this.defensePostsCapacity * 3);
|
||||
this.defensePostsByOwnerStaging = new Uint32Array(
|
||||
this.defensePostsByOwnerCapacity * 2,
|
||||
);
|
||||
}
|
||||
|
||||
private ensureDefendedDirtyTilesBuffer(capacityTiles: number): void {
|
||||
if (
|
||||
this.defendedDirtyTilesBuffer &&
|
||||
capacityTiles <= this.defendedDirtyTilesCapacity
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const GPUBufferUsage = (globalThis as any).GPUBufferUsage;
|
||||
const STORAGE = GPUBufferUsage?.STORAGE ?? 0x10;
|
||||
const COPY_DST_BUF = GPUBufferUsage?.COPY_DST ?? 0x8;
|
||||
|
||||
this.defendedDirtyTilesCapacity = Math.max(
|
||||
256,
|
||||
Math.pow(2, Math.ceil(Math.log2(Math.max(1, capacityTiles)))),
|
||||
);
|
||||
|
||||
const bufferSize = this.defendedDirtyTilesCapacity * 4; // u32 per tile
|
||||
|
||||
(this.defendedDirtyTilesBuffer as any).destroy?.();
|
||||
this.defendedDirtyTilesBuffer = this.device.createBuffer({
|
||||
size: bufferSize,
|
||||
usage: STORAGE | COPY_DST_BUF,
|
||||
});
|
||||
|
||||
this.defendedDirtyTilesStaging = new Uint32Array(
|
||||
this.defendedDirtyTilesCapacity,
|
||||
);
|
||||
}
|
||||
|
||||
private packDefensePostsByOwner(
|
||||
posts: Array<{ x: number; y: number; ownerId: number }>,
|
||||
): void {
|
||||
// Reset counts
|
||||
this.defenseOwnerOffsetsStaging.fill(0);
|
||||
const counts = new Uint32Array(GroundTruthData.MAX_OWNER_SLOTS);
|
||||
for (const p of posts) {
|
||||
const owner = p.ownerId >>> 0;
|
||||
if (owner === 0 || owner >= GroundTruthData.MAX_OWNER_SLOTS) continue;
|
||||
counts[owner]++;
|
||||
}
|
||||
|
||||
// Prefix sums into offsets (start,count) pairs.
|
||||
let running = 0;
|
||||
for (let owner = 0; owner < GroundTruthData.MAX_OWNER_SLOTS; owner++) {
|
||||
const count = counts[owner];
|
||||
this.defenseOwnerOffsetsStaging[owner * 2] = running;
|
||||
this.defenseOwnerOffsetsStaging[owner * 2 + 1] = count;
|
||||
running += count;
|
||||
}
|
||||
|
||||
this.ensureDefensePostsByOwnerBuffer(running);
|
||||
if (!this.defensePostsByOwnerStaging) {
|
||||
throw new Error("defensePostsByOwnerStaging not allocated");
|
||||
}
|
||||
|
||||
const writeCursor = new Uint32Array(GroundTruthData.MAX_OWNER_SLOTS);
|
||||
for (let owner = 0; owner < GroundTruthData.MAX_OWNER_SLOTS; owner++) {
|
||||
writeCursor[owner] = this.defenseOwnerOffsetsStaging[owner * 2];
|
||||
}
|
||||
|
||||
for (const p of posts) {
|
||||
const owner = p.ownerId >>> 0;
|
||||
if (owner === 0 || owner >= GroundTruthData.MAX_OWNER_SLOTS) continue;
|
||||
const idx = writeCursor[owner]++;
|
||||
this.defensePostsByOwnerStaging[idx * 2] = p.x >>> 0;
|
||||
this.defensePostsByOwnerStaging[idx * 2 + 1] = p.y >>> 0;
|
||||
}
|
||||
|
||||
this.device.queue.writeBuffer(
|
||||
this.defenseOwnerOffsetsBuffer,
|
||||
0,
|
||||
this.defenseOwnerOffsetsStaging,
|
||||
);
|
||||
if (running > 0) {
|
||||
this.device.queue.writeBuffer(
|
||||
this.defensePostsByOwnerBuffer,
|
||||
0,
|
||||
this.defensePostsByOwnerStaging.subarray(0, running * 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureDefenseCircleOffsets(range: number): void {
|
||||
if (range === this.defenseCircleRange) {
|
||||
return;
|
||||
}
|
||||
this.defenseCircleRange = range;
|
||||
if (range <= 0) {
|
||||
this.defenseCircleOffsets = new Int16Array(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const offsets: number[] = [];
|
||||
const r2 = range * range;
|
||||
for (let dy = -range; dy <= range; dy++) {
|
||||
for (let dx = -range; dx <= range; dx++) {
|
||||
if (dx * dx + dy * dy <= r2) {
|
||||
offsets.push(dx, dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.defenseCircleOffsets = new Int16Array(offsets);
|
||||
}
|
||||
|
||||
private buildDefendedDirtyTiles(
|
||||
changedPosts: Array<{ x: number; y: number }>,
|
||||
range: number,
|
||||
): void {
|
||||
if (changedPosts.length === 0) {
|
||||
this.defendedDirtyTilesCount = 0;
|
||||
this.needsFullDefendedStrengthRecompute = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.ensureDefenseCircleOffsets(range);
|
||||
const offsets = this.defenseCircleOffsets;
|
||||
const offsetsCount = offsets.length / 2;
|
||||
if (offsetsCount === 0) {
|
||||
this.defendedDirtyTilesCount = 0;
|
||||
this.needsFullDefendedStrengthRecompute = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const worstCase = changedPosts.length * offsetsCount;
|
||||
const mapTiles = this.mapWidth * this.mapHeight;
|
||||
if (worstCase > mapTiles) {
|
||||
this.defendedDirtyTilesCount = 0;
|
||||
this.needsFullDefendedStrengthRecompute = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.needsFullDefendedStrengthRecompute = false;
|
||||
this.ensureDefendedDirtyTilesBuffer(worstCase);
|
||||
if (!this.defendedDirtyTilesStaging) {
|
||||
throw new Error("defendedDirtyTilesStaging not allocated");
|
||||
}
|
||||
|
||||
let cursor = 0;
|
||||
for (const post of changedPosts) {
|
||||
for (let i = 0; i < offsets.length; i += 2) {
|
||||
const x = post.x + offsets[i];
|
||||
const y = post.y + offsets[i + 1];
|
||||
if (x < 0 || y < 0 || x >= this.mapWidth || y >= this.mapHeight) {
|
||||
continue;
|
||||
}
|
||||
this.defendedDirtyTilesStaging[cursor++] =
|
||||
(y * this.mapWidth + x) >>> 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.defendedDirtyTilesCount = cursor;
|
||||
this.device.queue.writeBuffer(
|
||||
this.defendedDirtyTilesBuffer,
|
||||
0,
|
||||
this.defendedDirtyTilesStaging.subarray(0, cursor),
|
||||
);
|
||||
}
|
||||
|
||||
ensureUpdatesBuffer(capacity: number): GPUBuffer {
|
||||
@@ -566,13 +816,14 @@ export class GroundTruthData {
|
||||
(this.updatesBuffer as any).destroy?.();
|
||||
}
|
||||
|
||||
(this as any).updatesBuffer = this.device.createBuffer({
|
||||
const buffer = this.device.createBuffer({
|
||||
size: bufferSize,
|
||||
usage: STORAGE | COPY_DST_BUF,
|
||||
});
|
||||
(this as any).updatesBuffer = buffer;
|
||||
|
||||
this.updatesStaging = new Uint32Array(this.updatesCapacity * 2);
|
||||
return this.updatesBuffer;
|
||||
return buffer;
|
||||
}
|
||||
|
||||
getUpdatesStaging(): Uint32Array {
|
||||
@@ -601,54 +852,10 @@ export class GroundTruthData {
|
||||
this.device.queue.writeBuffer(this.uniformBuffer, 0, this.uniformData);
|
||||
}
|
||||
|
||||
writeDefenseParamsBuffer(): void {
|
||||
const range = this.game.config().defensePostRange() >>> 0;
|
||||
this.defenseParamsData[0] = range;
|
||||
this.defenseParamsData[1] = this.defensePostsCount >>> 0;
|
||||
this.defenseParamsData[2] = this.defendedEpoch >>> 0;
|
||||
this.defenseParamsData[3] = 0;
|
||||
this.device.queue.writeBuffer(
|
||||
this.defenseParamsBuffer,
|
||||
0,
|
||||
this.defenseParamsData,
|
||||
);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// State getters/setters
|
||||
// =====================
|
||||
|
||||
getDefendedEpoch(): number {
|
||||
return this.defendedEpoch;
|
||||
}
|
||||
|
||||
incrementDefendedEpoch(): void {
|
||||
this.defendedEpoch = (this.defendedEpoch + 1) >>> 0;
|
||||
if (this.defendedEpoch === 0) {
|
||||
this.defendedEpoch = 1;
|
||||
}
|
||||
}
|
||||
|
||||
getDefensePostsCount(): number {
|
||||
return this.defensePostsCount;
|
||||
}
|
||||
|
||||
getLastDefenseRange(): number {
|
||||
return this.lastDefenseRange;
|
||||
}
|
||||
|
||||
setLastDefenseRange(range: number): void {
|
||||
this.lastDefenseRange = range;
|
||||
}
|
||||
|
||||
getLastDefensePostsCount(): number {
|
||||
return this.lastDefensePostsCount;
|
||||
}
|
||||
|
||||
setLastDefensePostsCount(count: number): void {
|
||||
this.lastDefensePostsCount = count;
|
||||
}
|
||||
|
||||
markPaletteDirty(): void {
|
||||
this.needsPaletteUpload = true;
|
||||
}
|
||||
|
||||
@@ -42,25 +42,20 @@ export class TerritoryRenderPass implements RenderPass {
|
||||
{
|
||||
binding: 1,
|
||||
visibility: 2 /* FRAGMENT */,
|
||||
buffer: { type: "uniform" },
|
||||
texture: { sampleType: "uint" },
|
||||
},
|
||||
{
|
||||
binding: 2,
|
||||
visibility: 2 /* FRAGMENT */,
|
||||
texture: { sampleType: "uint" },
|
||||
texture: { sampleType: "float" },
|
||||
},
|
||||
{
|
||||
binding: 3,
|
||||
visibility: 2 /* FRAGMENT */,
|
||||
texture: { sampleType: "uint" },
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
visibility: 2 /* FRAGMENT */,
|
||||
texture: { sampleType: "float" },
|
||||
},
|
||||
{
|
||||
binding: 5,
|
||||
binding: 4,
|
||||
visibility: 2 /* FRAGMENT */,
|
||||
texture: { sampleType: "float" },
|
||||
},
|
||||
@@ -112,7 +107,6 @@ export class TerritoryRenderPass implements RenderPass {
|
||||
|
||||
// Update uniforms
|
||||
resources.writeUniformBuffer(performance.now() / 1000);
|
||||
resources.writeDefenseParamsBuffer();
|
||||
|
||||
const pass = encoder.beginRenderPass({
|
||||
colorAttachments: [
|
||||
@@ -142,9 +136,8 @@ export class TerritoryRenderPass implements RenderPass {
|
||||
!this.bindGroupLayout ||
|
||||
!this.resources ||
|
||||
!this.resources.uniformBuffer ||
|
||||
!this.resources.defenseParamsBuffer ||
|
||||
!this.resources.stateTexture ||
|
||||
!this.resources.defendedTexture ||
|
||||
!this.resources.defendedStrengthTexture ||
|
||||
!this.resources.paletteTexture ||
|
||||
!this.resources.terrainTexture
|
||||
) {
|
||||
@@ -157,22 +150,18 @@ export class TerritoryRenderPass implements RenderPass {
|
||||
{ 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: 2,
|
||||
resource: this.resources.defendedStrengthTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 4,
|
||||
binding: 3,
|
||||
resource: this.resources.paletteTexture.createView(),
|
||||
},
|
||||
{
|
||||
binding: 5,
|
||||
binding: 4,
|
||||
resource: this.resources.terrainTexture.createView(),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
struct Uniforms {
|
||||
mapResolution_viewScale_time: vec4f, // x=mapW, y=mapH, z=viewScale, w=timeSec
|
||||
viewOffset_alt_highlight: vec4f, // x=offX, y=offY, z=alternativeView, w=highlightOwnerId
|
||||
viewSize_pad: vec4f, // x=viewW, y=viewH, z/w unused
|
||||
};
|
||||
|
||||
struct DefenseParams {
|
||||
range: u32,
|
||||
postCount: u32,
|
||||
epoch: u32,
|
||||
_pad: u32,
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
@group(0) @binding(0) var defendedTex: texture_storage_2d<r32uint, write>;
|
||||
|
||||
@compute @workgroup_size(8, 8)
|
||||
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
||||
let dims = textureDimensions(defendedTex);
|
||||
let x = i32(globalId.x);
|
||||
let y = i32(globalId.y);
|
||||
if (x < 0 || y < 0 || u32(x) >= dims.x || u32(y) >= dims.y) {
|
||||
return;
|
||||
}
|
||||
textureStore(defendedTex, vec2i(x, y), vec4u(0u, 0u, 0u, 0u));
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
struct Params {
|
||||
_dirtyCount: u32,
|
||||
range: u32,
|
||||
_pad0: u32,
|
||||
_pad1: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> p: Params;
|
||||
@group(0) @binding(1) var stateTex: texture_2d<u32>;
|
||||
@group(0) @binding(2) var defendedStrengthTex: texture_storage_2d<rgba8unorm, write>;
|
||||
@group(0) @binding(3) var<storage, read> ownerOffsets: array<vec2u>;
|
||||
@group(0) @binding(4) var<storage, read> postsByOwner: array<vec2u>;
|
||||
|
||||
@compute @workgroup_size(8, 8)
|
||||
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
||||
let dims = textureDimensions(stateTex);
|
||||
if (globalId.x >= dims.x || globalId.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
|
||||
let x = i32(globalId.x);
|
||||
let y = i32(globalId.y);
|
||||
let state = textureLoad(stateTex, vec2i(x, y), 0).x;
|
||||
let owner = state & 0xFFFu;
|
||||
|
||||
let range = i32(p.range);
|
||||
if (owner == 0u || range <= 0) {
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(0.0, 0.0, 0.0, 1.0));
|
||||
return;
|
||||
}
|
||||
|
||||
let off = ownerOffsets[owner];
|
||||
let start = off.x;
|
||||
let count = off.y;
|
||||
if (count == 0u) {
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(0.0, 0.0, 0.0, 1.0));
|
||||
return;
|
||||
}
|
||||
|
||||
let rx = f32(range);
|
||||
let r2 = range * range;
|
||||
var bestDist2: i32 = 0x7FFFFFFF;
|
||||
var i: u32 = 0u;
|
||||
loop {
|
||||
if (i >= count) { break; }
|
||||
let pos = postsByOwner[start + i];
|
||||
let dx = i32(pos.x) - x;
|
||||
let dy = i32(pos.y) - y;
|
||||
let d2 = dx * dx + dy * dy;
|
||||
if (d2 < bestDist2) {
|
||||
bestDist2 = d2;
|
||||
}
|
||||
i = i + 1u;
|
||||
}
|
||||
|
||||
if (bestDist2 > r2) {
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(0.0, 0.0, 0.0, 1.0));
|
||||
return;
|
||||
}
|
||||
|
||||
let dist = sqrt(f32(bestDist2));
|
||||
let strength = clamp(1.0 - (dist / rx), 0.0, 1.0);
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(strength, 0.0, 0.0, 1.0));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
struct Params {
|
||||
dirtyCount: u32,
|
||||
range: u32,
|
||||
_pad0: u32,
|
||||
_pad1: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> p: Params;
|
||||
@group(0) @binding(1) var<storage, read> dirtyTiles: array<u32>;
|
||||
@group(0) @binding(2) var stateTex: texture_2d<u32>;
|
||||
@group(0) @binding(3) var defendedStrengthTex: texture_storage_2d<rgba8unorm, write>;
|
||||
@group(0) @binding(4) var<storage, read> ownerOffsets: array<vec2u>;
|
||||
@group(0) @binding(5) var<storage, read> postsByOwner: array<vec2u>;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
||||
let idx = globalId.x;
|
||||
if (idx >= p.dirtyCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tileIndex = dirtyTiles[idx];
|
||||
let dims = textureDimensions(stateTex);
|
||||
let mapWidth = dims.x;
|
||||
let x = i32(tileIndex % mapWidth);
|
||||
let y = i32(tileIndex / mapWidth);
|
||||
|
||||
let state = textureLoad(stateTex, vec2i(x, y), 0).x;
|
||||
let owner = state & 0xFFFu;
|
||||
let range = i32(p.range);
|
||||
if (owner == 0u || range <= 0) {
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(0.0, 0.0, 0.0, 1.0));
|
||||
return;
|
||||
}
|
||||
|
||||
let off = ownerOffsets[owner];
|
||||
let start = off.x;
|
||||
let count = off.y;
|
||||
if (count == 0u) {
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(0.0, 0.0, 0.0, 1.0));
|
||||
return;
|
||||
}
|
||||
|
||||
let rx = f32(range);
|
||||
let r2 = range * range;
|
||||
var bestDist2: i32 = 0x7FFFFFFF;
|
||||
var i: u32 = 0u;
|
||||
loop {
|
||||
if (i >= count) { break; }
|
||||
let pos = postsByOwner[start + i];
|
||||
let dx = i32(pos.x) - x;
|
||||
let dy = i32(pos.y) - y;
|
||||
let d2 = dx * dx + dy * dy;
|
||||
if (d2 < bestDist2) {
|
||||
bestDist2 = d2;
|
||||
}
|
||||
i = i + 1u;
|
||||
}
|
||||
|
||||
if (bestDist2 > r2) {
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(0.0, 0.0, 0.0, 1.0));
|
||||
return;
|
||||
}
|
||||
|
||||
let dist = sqrt(f32(bestDist2));
|
||||
let strength = clamp(1.0 - (dist / rx), 0.0, 1.0);
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(strength, 0.0, 0.0, 1.0));
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
struct DefenseParams {
|
||||
range: u32,
|
||||
postCount: u32,
|
||||
epoch: u32,
|
||||
_pad: u32,
|
||||
};
|
||||
|
||||
struct DefensePost {
|
||||
x: u32,
|
||||
y: u32,
|
||||
ownerId: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> d: DefenseParams;
|
||||
@group(0) @binding(1) var<storage, read> posts: array<DefensePost>;
|
||||
@group(0) @binding(2) var stateTex: texture_2d<u32>;
|
||||
@group(0) @binding(3) var defendedTex: texture_storage_2d<r32uint, write>;
|
||||
|
||||
@compute @workgroup_size(8, 8, 1)
|
||||
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
||||
let postIdx = globalId.z;
|
||||
let postCount = d.postCount;
|
||||
if (postIdx >= postCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
let range = i32(d.range);
|
||||
if (range < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dx = i32(globalId.x) - range;
|
||||
let dy = i32(globalId.y) - range;
|
||||
if (dx * dx + dy * dy > range * range) {
|
||||
return;
|
||||
}
|
||||
|
||||
let post = posts[postIdx];
|
||||
let x = i32(post.x) + dx;
|
||||
let y = i32(post.y) + dy;
|
||||
|
||||
let dims = textureDimensions(stateTex);
|
||||
if (x < 0 || y < 0 || u32(x) >= dims.x || u32(y) >= dims.y) {
|
||||
return;
|
||||
}
|
||||
|
||||
let texCoord = vec2i(x, y);
|
||||
let state = textureLoad(stateTex, texCoord, 0).x;
|
||||
let owner = state & 0xFFFu;
|
||||
if (owner == post.ownerId) {
|
||||
textureStore(defendedTex, texCoord, vec4u(d.epoch, 0u, 0u, 0u));
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,24 @@ struct Update {
|
||||
newState: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<storage, read> updates: array<Update>;
|
||||
@group(0) @binding(1) var stateTex: texture_storage_2d<r32uint, write>;
|
||||
struct Params {
|
||||
updateCount: u32,
|
||||
range: u32,
|
||||
_pad0: u32,
|
||||
_pad1: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> p: Params;
|
||||
@group(0) @binding(1) var<storage, read> updates: array<Update>;
|
||||
@group(0) @binding(2) var stateTex: texture_storage_2d<r32uint, write>;
|
||||
@group(0) @binding(3) var defendedStrengthTex: texture_storage_2d<rgba8unorm, write>;
|
||||
@group(0) @binding(4) var<storage, read> ownerOffsets: array<vec2u>;
|
||||
@group(0) @binding(5) var<storage, read> postsByOwner: array<vec2u>;
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
||||
let idx = globalId.x;
|
||||
if (idx >= arrayLength(&updates)) {
|
||||
if (idx >= p.updateCount) {
|
||||
return;
|
||||
}
|
||||
let update = updates[idx];
|
||||
@@ -18,4 +29,45 @@ fn main(@builtin(global_invocation_id) globalId: vec3<u32>) {
|
||||
let x = i32(update.tileIndex % mapWidth);
|
||||
let y = i32(update.tileIndex / mapWidth);
|
||||
textureStore(stateTex, vec2i(x, y), vec4u(update.newState, 0u, 0u, 0u));
|
||||
|
||||
// Update defended strength for this tile based on the new owner.
|
||||
let owner = update.newState & 0xFFFu;
|
||||
let range = i32(p.range);
|
||||
if (owner == 0u || range <= 0) {
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(0.0, 0.0, 0.0, 1.0));
|
||||
return;
|
||||
}
|
||||
|
||||
let off = ownerOffsets[owner];
|
||||
let start = off.x;
|
||||
let count = off.y;
|
||||
if (count == 0u) {
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(0.0, 0.0, 0.0, 1.0));
|
||||
return;
|
||||
}
|
||||
|
||||
let rx = f32(range);
|
||||
let r2 = range * range;
|
||||
var bestDist2: i32 = 0x7FFFFFFF;
|
||||
var i: u32 = 0u;
|
||||
loop {
|
||||
if (i >= count) { break; }
|
||||
let pos = postsByOwner[start + i];
|
||||
let dx = i32(pos.x) - x;
|
||||
let dy = i32(pos.y) - y;
|
||||
let d2 = dx * dx + dy * dy;
|
||||
if (d2 < bestDist2) {
|
||||
bestDist2 = d2;
|
||||
}
|
||||
i = i + 1u;
|
||||
}
|
||||
|
||||
if (bestDist2 > r2) {
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(0.0, 0.0, 0.0, 1.0));
|
||||
return;
|
||||
}
|
||||
|
||||
let dist = sqrt(f32(bestDist2));
|
||||
let strength = clamp(1.0 - (dist / rx), 0.0, 1.0);
|
||||
textureStore(defendedStrengthTex, vec2i(x, y), vec4f(strength, 0.0, 0.0, 1.0));
|
||||
}
|
||||
|
||||
@@ -4,19 +4,11 @@ struct Uniforms {
|
||||
viewSize_pad: vec4f, // x=viewW, y=viewH, z/w unused
|
||||
};
|
||||
|
||||
struct DefenseParams {
|
||||
range: u32,
|
||||
postCount: u32,
|
||||
epoch: u32,
|
||||
_pad: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u: Uniforms;
|
||||
@group(0) @binding(1) var<uniform> d: DefenseParams;
|
||||
@group(0) @binding(2) var stateTex: texture_2d<u32>;
|
||||
@group(0) @binding(3) var defendedTex: texture_2d<u32>;
|
||||
@group(0) @binding(4) var paletteTex: texture_2d<f32>;
|
||||
@group(0) @binding(5) var terrainTex: texture_2d<f32>;
|
||||
@group(0) @binding(1) var stateTex: texture_2d<u32>;
|
||||
@group(0) @binding(2) var defendedStrengthTex: texture_2d<f32>;
|
||||
@group(0) @binding(3) var paletteTex: texture_2d<f32>;
|
||||
@group(0) @binding(4) var terrainTex: texture_2d<f32>;
|
||||
|
||||
@vertex
|
||||
fn vsMain(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
||||
@@ -56,15 +48,17 @@ fn fsMain(@builtin(position) pos: vec4f) -> @location(0) vec4f {
|
||||
let hasFallout = (state & 0x2000u) != 0u;
|
||||
|
||||
let terrain = textureLoad(terrainTex, texCoord, 0);
|
||||
let defendedStrength = textureLoad(defendedStrengthTex, texCoord, 0).x;
|
||||
var outColor = terrain;
|
||||
if (owner != 0u) {
|
||||
// Player colors start at index 10
|
||||
let c = textureLoad(paletteTex, vec2i(i32(owner) + 10, 0), 0);
|
||||
let defended = textureLoad(defendedTex, texCoord, 0).x == d.epoch;
|
||||
var territoryRgb = c.rgb;
|
||||
if (defended) {
|
||||
territoryRgb = mix(territoryRgb, vec3f(1.0, 0.0, 1.0), 0.35);
|
||||
}
|
||||
territoryRgb = mix(
|
||||
territoryRgb,
|
||||
vec3f(1.0, 0.0, 1.0),
|
||||
clamp(0.35 * defendedStrength, 0.0, 0.35),
|
||||
);
|
||||
if (hasFallout) {
|
||||
// Fallout color is at index 0
|
||||
let falloutColor = textureLoad(paletteTex, vec2i(0, 0), 0).rgb;
|
||||
|
||||
Reference in New Issue
Block a user