Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/client/render/gl/RenderSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import defaults from "./render-settings.json";
export interface RenderSettings {
passEnabled: {
terrain: boolean;
mapOverlay: boolean;
territory: boolean;
borderCompute: boolean;
borderStamp: boolean;
trail: boolean;
territoryPatterns: boolean;
structure: boolean;
unit: boolean;
Expand Down
8 changes: 4 additions & 4 deletions src/client/render/gl/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1201,7 +1201,7 @@ export class GPURenderer {
}

private computeTextures(): void {
if (this.settings.passEnabled.mapOverlay) this.borderPass.draw();
if (this.settings.passEnabled.borderCompute) this.borderPass.draw();
}

private renderFrame(): void {
Expand Down Expand Up @@ -1259,7 +1259,7 @@ export class GPURenderer {
if (pe.terrain) this.terrainPass.draw(cam);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
if (pe.mapOverlay) this.territoryPass.draw(cam);
if (pe.territory) this.territoryPass.draw(cam);
}

private renderOverlays(cam: Float32Array, zoom: number): void {
Expand All @@ -1270,7 +1270,7 @@ export class GPURenderer {
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

this.spawnOverlayPass.draw(cam);
if (pe.mapOverlay) this.borderStampPass.draw(cam);
if (pe.borderStamp) this.borderStampPass.draw(cam);
if (pe.railroad) this.railroadPass.draw(cam, zoom);
if (pe.unit) this.unitPass.drawGround(cam);
this.samRadiusPass.draw(cam);
Expand All @@ -1285,7 +1285,7 @@ export class GPURenderer {
this.moveIndicatorPass.draw(cam, zoom);
this.nukeTelegraphPass.draw(cam);
if (pe.falloutBloom) this.bloomPass.draw(cam, this.frameTick);
if (pe.mapOverlay) this.trailPass.draw(cam);
if (pe.trail) this.trailPass.draw(cam);
if (pe.unit) this.unitPass.drawMissiles(cam);

if (pe.fx) {
Expand Down
5 changes: 4 additions & 1 deletion src/client/render/gl/debug/Layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export function buildTree(s: RenderSettings, d: RenderSettings): DebugNode[] {
return [
folder("Pass Enables", [
toggle(s.passEnabled, "terrain", d.passEnabled),
toggle(s.passEnabled, "mapOverlay", d.passEnabled),
toggle(s.passEnabled, "territory", d.passEnabled),
toggle(s.passEnabled, "borderCompute", d.passEnabled),
toggle(s.passEnabled, "borderStamp", d.passEnabled),
toggle(s.passEnabled, "trail", d.passEnabled),
toggle(s.passEnabled, "structure", d.passEnabled),
toggle(s.passEnabled, "unit", d.passEnabled),
toggle(s.passEnabled, "name", d.passEnabled),
Expand Down
154 changes: 69 additions & 85 deletions src/client/render/gl/passes/TerritoryPass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { OWNER_MASK, TILE_DEFINES } from "../utils/TileCodec";

import overlayVertSrc from "../shaders/map-overlay/overlay.vert.glsl?raw";
import territoryFragSrc from "../shaders/map-overlay/territory.frag.glsl?raw";
import { TileScatterPass } from "./TileScatterPass";

export class TerritoryPass {
private gl: WebGL2RenderingContext;
Expand Down Expand Up @@ -58,9 +59,18 @@ export class TerritoryPass {
private cpuTileState: Uint16Array;
private tilesDirty = false;

/** Dirty row range for partial tile upload. Infinity/-1 = full upload. */
private dirtyRowMin = Infinity;
private dirtyRowMax = -1;
/**
* True after a full state replacement (initial load / seek). flushTileTexture
* uploads the full cpuTileState via texSubImage2D and discards any queued
* scatter patches — those are already covered by the full upload.
*/
private fullUploadPending = false;

/**
* GPU scatter pass for per-frame patches. Replaces the old dirty-row bbox
* upload — constant cost regardless of how spatially scattered patches are.
*/
private scatter!: TileScatterPass;

/**
* Drip buckets — round-robin staggering of tile updates across render frames.
Expand Down Expand Up @@ -152,6 +162,8 @@ export class TerritoryPass {
gl.uniform1i(gl.getUniformLocation(this.program, "uSkinAnchor"), 6);

this.vao = createMapQuad(gl, mapW, mapH);

this.scatter = new TileScatterPass(gl, mapW, mapH, tileTex);
}

// ---------------------------------------------------------------------------
Expand All @@ -162,30 +174,33 @@ export class TerritoryPass {
uploadFullTileState(tileState: Uint16Array): void {
this.cpuTileState.set(tileState);
this.clearDripBuckets();
this.dirtyRowMin = Infinity;
this.dirtyRowMax = -1;
this.scatter.clear();
this.fullUploadPending = true;
this.tilesDirty = true;
}

/** Live-game path: snapshot the initial tile state and clear pending drip. */
setLiveRef(tileState: Uint16Array): void {
this.cpuTileState.set(tileState);
this.clearDripBuckets();
this.dirtyRowMin = Infinity;
this.dirtyRowMax = -1;
this.scatter.clear();
this.fullUploadPending = true;
this.tilesDirty = true;
}

/** Apply tile deltas (during playback). */
uploadDeltaTiles(changedTiles: TilePair[]): void {
const ts = this.cpuTileState;
const w = this.mapW;
const pending = this.fullUploadPending;
for (let i = 0; i < changedTiles.length; i++) {
const tp = changedTiles[i];
ts[tp.ref] = tp.state;
const row = (tp.ref / w) | 0;
if (row < this.dirtyRowMin) this.dirtyRowMin = row;
if (row > this.dirtyRowMax) this.dirtyRowMax = row;
if (!pending) {
const x = tp.ref % w;
const y = (tp.ref - x) / w;
this.scatter.push(x, y, tp.state);
}
}
this.tilesDirty = true;
}
Expand All @@ -209,28 +224,19 @@ export class TerritoryPass {
drainDripBucket(): void {
const bucket = this.dripBuckets[this.currentBucket];
if (bucket.length > 0) {
const isFullUploadPending = this.tilesDirty && this.dirtyRowMax < 0;

if (isFullUploadPending) {
// Full upload pending: skip tracking dirty rows, just flush data
for (let i = 0; i < bucket.length; i += 2) {
this.cpuTileState[bucket[i]] = bucket[i + 1];
}
} else {
const w = this.mapW;
let minRow = this.dirtyRowMin;
let maxRow = this.dirtyRowMax;
for (let i = 0; i < bucket.length; i += 2) {
const ref = bucket[i];
this.cpuTileState[ref] = bucket[i + 1];
const row = (ref / w) | 0;
if (row < minRow) minRow = row;
if (row > maxRow) maxRow = row;
const ts = this.cpuTileState;
const w = this.mapW;
const pending = this.fullUploadPending;
for (let i = 0; i < bucket.length; i += 2) {
const ref = bucket[i];
const state = bucket[i + 1];
ts[ref] = state;
if (!pending) {
const x = ref % w;
const y = (ref - x) / w;
this.scatter.push(x, y, state);
}
this.dirtyRowMin = minRow;
this.dirtyRowMax = maxRow;
}

bucket.length = 0;
this.tilesDirty = true;
}
Expand All @@ -243,39 +249,25 @@ export class TerritoryPass {
*/
flushAllDripBuckets(): void {
let any = false;
const isFullUploadPending = this.tilesDirty && this.dirtyRowMax < 0;

if (isFullUploadPending) {
for (let b = 0; b < this.nBuckets; b++) {
const bucket = this.dripBuckets[b];
if (bucket.length === 0) continue;
any = true;
for (let i = 0; i < bucket.length; i += 2) {
this.cpuTileState[bucket[i]] = bucket[i + 1];
}
bucket.length = 0;
}
} else {
const w = this.mapW;
let minRow = this.dirtyRowMin;
let maxRow = this.dirtyRowMax;
for (let b = 0; b < this.nBuckets; b++) {
const bucket = this.dripBuckets[b];
if (bucket.length === 0) continue;
any = true;
for (let i = 0; i < bucket.length; i += 2) {
const ref = bucket[i];
this.cpuTileState[ref] = bucket[i + 1];
const row = (ref / w) | 0;
if (row < minRow) minRow = row;
if (row > maxRow) maxRow = row;
const ts = this.cpuTileState;
const w = this.mapW;
const pending = this.fullUploadPending;
for (let b = 0; b < this.nBuckets; b++) {
const bucket = this.dripBuckets[b];
if (bucket.length === 0) continue;
any = true;
for (let i = 0; i < bucket.length; i += 2) {
const ref = bucket[i];
const state = bucket[i + 1];
ts[ref] = state;
if (!pending) {
const x = ref % w;
const y = (ref - x) / w;
this.scatter.push(x, y, state);
}
bucket.length = 0;
}
this.dirtyRowMin = minRow;
this.dirtyRowMax = maxRow;
bucket.length = 0;
}

if (any) {
this.tilesDirty = true;
}
Expand Down Expand Up @@ -331,28 +323,13 @@ export class TerritoryPass {
flushTileTexture(): boolean {
if (!this.tilesDirty) return false;
const gl = this.gl;
const src = this.cpuTileState;
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.tileTex);
let uploaded = false;

if (this.dirtyRowMax >= 0) {
// Partial upload — only dirty rows
const minRow = this.dirtyRowMin;
const rowCount = this.dirtyRowMax - minRow + 1;
const offset = minRow * this.mapW;
gl.texSubImage2D(
gl.TEXTURE_2D,
0,
0,
minRow,
this.mapW,
rowCount,
gl.RED_INTEGER,
gl.UNSIGNED_SHORT,
src.subarray(offset, offset + rowCount * this.mapW),
);
} else {
// Full upload (first tick, seek, replay full frame, etc.)
if (this.fullUploadPending) {
// Full upload (first tick, seek, replay full frame, etc.) — supersedes
// any queued scatter patches.
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.tileTex);
gl.texSubImage2D(
gl.TEXTURE_2D,
0,
Expand All @@ -362,14 +339,20 @@ export class TerritoryPass {
this.mapH,
gl.RED_INTEGER,
gl.UNSIGNED_SHORT,
src,
this.cpuTileState,
);
this.scatter.clear();
this.fullUploadPending = false;
uploaded = true;
} else if (this.scatter.count > 0) {
// Per-frame patches — scatter via FBO + POINTS draw. Constant cost in
// patch count regardless of spatial distribution.
this.scatter.flush();
uploaded = true;
}

this.dirtyRowMin = Infinity;
this.dirtyRowMax = -1;
this.tilesDirty = false;
return true;
return uploaded;
}

setAltView(active: boolean): void {
Expand Down Expand Up @@ -449,6 +432,7 @@ export class TerritoryPass {
const gl = this.gl;
gl.deleteProgram(this.program);
gl.deleteVertexArray(this.vao);
this.scatter.dispose();
// tileTex, paletteTex, patternMetaTex, patternDataTex owned by GPUResources / renderer
}
}
Loading
Loading