accept delay, use triplebuffer,

Refactor contest management in TerritoryLayer and TerritoryWebGLRenderer

- Updated contest duration handling to use ticks instead of milliseconds for improved precision.
- Introduced new tick-based state management for contest updates and rendering.
- Enhanced interpolation logic for smoother transitions between contest states.
- Removed obsolete smooth state handling and related properties to streamline code.
- Added support for older contest states in the WebGL renderer for better visual fidelity.
This commit is contained in:
scamiv
2026-01-11 21:46:31 +01:00
parent 1b8b4f1ce0
commit 4ce8ec14cc
2 changed files with 346 additions and 101 deletions
+84 -70
View File
@@ -20,7 +20,7 @@ import { TerritoryWebGLRenderer } from "./TerritoryWebGLRenderer";
const CONTEST_ID_MASK = 0x7fff;
const CONTEST_ATTACKER_EVER_BIT = 0x8000;
const CONTEST_TIME_WRAP = 32768;
const DEFAULT_CONTEST_DURATION_MS = 200;
const DEFAULT_CONTEST_DURATION_TICKS = 2;
const CONTEST_SPEED_TPS_MAX = 20;
const CONTEST_SPEED_EMA_ALPHA = 0.8;
const CONTEST_SPEED_DECAY_HALFLIFE_MS = 100;
@@ -65,7 +65,7 @@ export class TerritoryLayer implements Layer {
private lastFocusedPlayer: PlayerView | null = null;
private lastMyPlayerSmallId: number | null = null;
private lastPaletteSignature: string | null = null;
private contestDurationMs = DEFAULT_CONTEST_DURATION_MS;
private contestDurationTicks = DEFAULT_CONTEST_DURATION_TICKS;
private contestActive = false;
private contestNextId = 1;
private contestFreeIds: number[] = [];
@@ -74,10 +74,15 @@ export class TerritoryLayer implements Layer {
private contestAttackers: Uint16Array | null = null;
private contestTileIndices: Int32Array | null = null;
private contestComponents = new Map<number, ContestComponent>();
private smoothDurationMs = 100;
private smoothActive = false;
private smoothStartMs = 0;
private smoothSnapshotPending = false;
private tickSnapshotPending = false;
private tickTimeMsCurrent = 0;
private tickTimeMsPrev = 0;
private tickTimeMsOlder = 0;
private tickNumberCurrent: number | null = null;
private tickNumberPrev: number | null = null;
private tickNumberOlder: number | null = null;
private interpolationDelayMs = 100;
private lastInterpolationPair: "prevCurrent" | "olderPrev" = "prevCurrent";
private contestSpeedDeltas = new Map<number, number>();
private contestSpeedLastUpdateMs = 0;
@@ -114,17 +119,27 @@ export class TerritoryLayer implements Layer {
}
this.refreshPaletteIfNeeded();
const tickNumber = this.game.ticks();
if (this.tickNumberCurrent !== tickNumber) {
this.tickNumberOlder = this.tickNumberPrev;
this.tickNumberPrev = this.tickNumberCurrent;
this.tickNumberCurrent = tickNumber;
this.tickTimeMsOlder = this.tickTimeMsPrev;
this.tickTimeMsPrev = this.tickTimeMsCurrent;
this.tickTimeMsCurrent = now;
if (this.territoryRenderer) {
this.tickSnapshotPending = true;
}
}
this.game.recentlyUpdatedTiles().forEach((t) => this.markTile(t));
const ownerUpdates = this.game.recentlyUpdatedOwnerTiles();
if (ownerUpdates.length > 0) {
if (this.territoryRenderer) {
this.smoothSnapshotPending = true;
}
this.smoothStartMs = now;
this.smoothActive = true;
}
this.contestSpeedDeltas.clear();
this.applyContestChanges(ownerUpdates, now);
const nowTickPacked = this.packContestTick(this.game.ticks());
this.applyContestChanges(ownerUpdates, nowTickPacked);
this.updateContestState(nowTickPacked);
this.updateContestSpeeds(now);
this.updateContestStrengths();
const updates = this.game.updatesSinceLastTick();
@@ -389,7 +404,6 @@ export class TerritoryLayer implements Layer {
this.configureRenderers();
this.ensureContestScratch();
this.syncContestStateToRenderer();
this.syncSmoothStateToRenderer();
// Add a second canvas for highlights
this.highlightCanvas = document.createElement("canvas");
@@ -450,12 +464,11 @@ export class TerritoryLayer implements Layer {
return;
}
const now = this.nowMs();
if (this.smoothSnapshotPending) {
if (this.tickSnapshotPending) {
this.territoryRenderer.snapshotStateForSmoothing();
this.smoothSnapshotPending = false;
this.tickSnapshotPending = false;
}
this.updateSmoothState(now);
this.updateContestState(now);
this.updateInterpolationState(now);
const renderTerritoryStart = FrameProfiler.start();
this.territoryRenderer.setViewSize(
@@ -582,20 +595,38 @@ export class TerritoryLayer implements Layer {
}
}
private updateSmoothState(now: number) {
private updateInterpolationState(now: number) {
if (!this.territoryRenderer) {
return;
}
let progress = 1;
if (this.smoothActive) {
const elapsed = now - this.smoothStartMs;
progress = Math.max(0, Math.min(1, elapsed / this.smoothDurationMs));
if (progress >= 1) {
this.smoothActive = false;
}
if (this.tickTimeMsPrev <= 0 || this.tickTimeMsCurrent <= 0) {
this.lastInterpolationPair = "prevCurrent";
this.territoryRenderer.setInterpolationPair("prevCurrent");
this.territoryRenderer.setSmoothProgress(1);
this.territoryRenderer.setSmoothEnabled(false);
return;
}
const renderTime = now - this.interpolationDelayMs;
let pair: "prevCurrent" | "olderPrev" = "prevCurrent";
let fromTime = this.tickTimeMsPrev;
let toTime = this.tickTimeMsCurrent;
if (this.tickTimeMsOlder > 0 && renderTime < this.tickTimeMsPrev) {
pair = "olderPrev";
fromTime = this.tickTimeMsOlder;
toTime = this.tickTimeMsPrev;
}
const denom = Math.max(1, Math.min(250, toTime - fromTime));
const progress = Math.max(0, Math.min(1, (renderTime - fromTime) / denom));
this.lastInterpolationPair = pair;
this.territoryRenderer.setInterpolationPair(pair);
this.territoryRenderer.setSmoothProgress(progress);
this.territoryRenderer.setSmoothEnabled(this.smoothActive);
this.territoryRenderer.setSmoothEnabled(true);
}
private recordContestSpeed(componentId: number) {
@@ -605,13 +636,12 @@ export class TerritoryLayer implements Layer {
private applyContestChanges(
changes: Array<{ tile: TileRef; previousOwner: number; newOwner: number }>,
now: number,
nowTickPacked: number,
) {
if (!this.territoryRenderer || changes.length === 0) {
return;
}
this.ensureContestScratch();
const nowPacked = this.packContestTime(now);
for (const change of changes) {
if (change.newOwner === change.previousOwner) {
@@ -624,7 +654,7 @@ export class TerritoryLayer implements Layer {
tile,
change.previousOwner,
change.newOwner,
nowPacked,
nowTickPacked,
);
if (component) {
this.recordContestSpeed(component.id);
@@ -639,7 +669,7 @@ export class TerritoryLayer implements Layer {
tile,
change.previousOwner,
change.newOwner,
nowPacked,
nowTickPacked,
);
if (newComponent) {
this.recordContestSpeed(newComponent.id);
@@ -660,8 +690,8 @@ export class TerritoryLayer implements Layer {
component.id,
attackerEver,
);
component.lastActivityPacked = nowPacked;
this.territoryRenderer.setContestTime(component.id, nowPacked);
component.lastActivityPacked = nowTickPacked;
this.territoryRenderer.setContestTime(component.id, nowTickPacked);
this.recordContestSpeed(component.id);
} else {
this.removeTileFromComponent(tile, component);
@@ -669,7 +699,7 @@ export class TerritoryLayer implements Layer {
tile,
change.previousOwner,
change.newOwner,
nowPacked,
nowTickPacked,
);
if (newComponent) {
this.recordContestSpeed(newComponent.id);
@@ -777,13 +807,15 @@ export class TerritoryLayer implements Layer {
return total;
}
private updateContestState(now: number) {
private updateContestState(nowTickPacked: number) {
if (!this.territoryRenderer) {
return;
}
this.ensureContestScratch();
const nowPacked = this.packContestTime(now);
this.territoryRenderer.setContestNow(nowPacked, this.contestDurationMs);
this.territoryRenderer.setContestNow(
nowTickPacked,
this.contestDurationTicks,
);
if (!this.contestActive) {
return;
@@ -792,10 +824,10 @@ export class TerritoryLayer implements Layer {
const expired: ContestComponent[] = [];
for (const component of this.contestComponents.values()) {
const elapsed = this.contestElapsed(
nowPacked,
nowTickPacked,
component.lastActivityPacked,
);
if (elapsed >= this.contestDurationMs) {
if (elapsed >= this.contestDurationTicks) {
expired.push(component);
}
}
@@ -809,7 +841,7 @@ export class TerritoryLayer implements Layer {
tile: TileRef,
defender: number,
attacker: number,
nowPacked: number,
nowTickPacked: number,
): ContestComponent | null {
if (attacker === defender || attacker === 0 || defender === 0) {
return null;
@@ -817,7 +849,7 @@ export class TerritoryLayer implements Layer {
const neighbors = this.collectNeighborComponents(tile, attacker, defender);
let component: ContestComponent;
if (neighbors.length === 0) {
component = this.createContestComponent(attacker, defender, nowPacked);
component = this.createContestComponent(attacker, defender, nowTickPacked);
} else {
component = neighbors[0];
for (let i = 1; i < neighbors.length; i++) {
@@ -826,8 +858,8 @@ export class TerritoryLayer implements Layer {
}
this.addTileToComponent(tile, component, true);
component.lastActivityPacked = nowPacked;
this.territoryRenderer?.setContestTime(component.id, nowPacked);
component.lastActivityPacked = nowTickPacked;
this.territoryRenderer?.setContestTime(component.id, nowTickPacked);
return component;
}
@@ -858,14 +890,14 @@ export class TerritoryLayer implements Layer {
private createContestComponent(
attacker: number,
defender: number,
nowPacked: number,
nowTickPacked: number,
): ContestComponent {
const id = this.allocateContestComponentId();
const component: ContestComponent = {
id,
attacker,
defender,
lastActivityPacked: nowPacked,
lastActivityPacked: nowTickPacked,
tiles: [],
speed: 0,
strength: 0.5,
@@ -1026,8 +1058,8 @@ export class TerritoryLayer implements Layer {
return (this.contestComponentIds![tile] & CONTEST_ATTACKER_EVER_BIT) !== 0;
}
private packContestTime(now: number): number {
return Math.floor(now) % CONTEST_TIME_WRAP;
private packContestTick(tick: number): number {
return Math.floor(tick) % CONTEST_TIME_WRAP;
}
private contestElapsed(nowPacked: number, startPacked: number): number {
@@ -1078,25 +1110,6 @@ export class TerritoryLayer implements Layer {
}
}
private syncSmoothStateToRenderer() {
if (!this.territoryRenderer) {
return;
}
if (this.smoothActive) {
const now = this.nowMs();
const elapsed = now - this.smoothStartMs;
const progress = Math.max(
0,
Math.min(1, elapsed / this.smoothDurationMs),
);
this.territoryRenderer.setSmoothProgress(progress);
this.territoryRenderer.setSmoothEnabled(true);
} else {
this.territoryRenderer.setSmoothEnabled(false);
this.territoryRenderer.setSmoothProgress(1);
}
}
private computePaletteSignature(): string {
let maxSmallId = 0;
for (const player of this.game.playerViews()) {
@@ -1134,12 +1147,13 @@ export class TerritoryLayer implements Layer {
`view: ${stats.viewWidth}x${stats.viewHeight}`,
`scale: ${stats.viewScale.toFixed(2)}`,
`offset: ${stats.viewOffsetX.toFixed(1)}, ${stats.viewOffsetY.toFixed(1)}`,
`smooth: ${stats.smoothEnabled ? "on" : "off"} ${stats.smoothProgress.toFixed(2)} active ${this.smoothActive ? "yes" : "no"}`,
`smooth: ${stats.smoothEnabled ? "on" : "off"} ${stats.smoothProgress.toFixed(2)} pair ${this.lastInterpolationPair}`,
`tick: ${this.tickNumberCurrent ?? "-"} prev ${this.tickNumberPrev ?? "-"}`,
`delayMs: ${this.interpolationDelayMs.toFixed(0)}`,
`smoothPrereq: prevCopy ${stats.prevStateCopySupported ? "yes" : "no"}`,
`jfa: ${jfaStatus} dirty ${stats.jfaDirty ? "yes" : "no"}`,
`contest: ${this.contestActive ? "on" : "off"} comps ${this.contestComponents.size}`,
`contestMs: ${this.contestDurationMs}`,
`smoothMs: ${this.smoothDurationMs}`,
`contestTicks: ${this.contestDurationTicks}`,
`hovered: ${stats.hoveredPlayerId}`,
];
const padding = 6;
@@ -43,14 +43,18 @@ export class TerritoryWebGLRenderer {
private readonly contestSpeedsTexture: WebGLTexture | null;
private readonly contestStrengthsTexture: WebGLTexture | null;
private readonly prevOwnerTexture: WebGLTexture | null;
private readonly olderOwnerTexture: WebGLTexture | null;
private readonly stateFramebuffer: WebGLFramebuffer | null;
private readonly prevStateFramebuffer: WebGLFramebuffer | null;
private readonly olderStateFramebuffer: WebGLFramebuffer | null;
private readonly jfaTextureA: WebGLTexture | null;
private readonly jfaTextureB: WebGLTexture | null;
private readonly jfaFramebufferA: WebGLFramebuffer | null;
private readonly jfaFramebufferB: WebGLFramebuffer | null;
private readonly jfaResultOlderTexture: WebGLTexture | null;
private readonly jfaResultOldTexture: WebGLTexture | null;
private readonly jfaResultNewTexture: WebGLTexture | null;
private readonly jfaResultOlderFramebuffer: WebGLFramebuffer | null;
private readonly jfaResultOldFramebuffer: WebGLFramebuffer | null;
private readonly jfaResultNewFramebuffer: WebGLFramebuffer | null;
private readonly jfaSeedProgram: WebGLProgram | null;
@@ -70,6 +74,7 @@ export class TerritoryWebGLRenderer {
viewScale: WebGLUniformLocation | null;
viewOffset: WebGLUniformLocation | null;
state: WebGLUniformLocation | null;
latestState: WebGLUniformLocation | null;
palette: WebGLUniformLocation | null;
relations: WebGLUniformLocation | null;
patterns: WebGLUniformLocation | null;
@@ -138,14 +143,16 @@ export class TerritoryWebGLRenderer {
private hoveredPlayerId = -1;
private animationStartTime = Date.now();
private contestNow = 0;
private contestDurationMs = 0;
private contestDurationTicks = 0;
private smoothProgress = 1;
private smoothEnabled = true;
private jfaSupported = false;
private jfaDisabledReason: string | null = null;
private jfaDirty = false;
private jfaHistoryInitialized = false;
private prevStateCopySupported = false;
private jfaSteps: number[] = [];
private interpolationPair: "prevCurrent" | "olderPrev" = "prevCurrent";
private readonly userSettings = new UserSettings();
private readonly patternBytesCache = new Map<string, Uint8Array>();
@@ -191,14 +198,18 @@ export class TerritoryWebGLRenderer {
this.contestSpeedsTexture = null;
this.contestStrengthsTexture = null;
this.prevOwnerTexture = null;
this.olderOwnerTexture = null;
this.stateFramebuffer = null;
this.prevStateFramebuffer = null;
this.olderStateFramebuffer = null;
this.jfaTextureA = null;
this.jfaTextureB = null;
this.jfaFramebufferA = null;
this.jfaFramebufferB = null;
this.jfaResultOlderTexture = null;
this.jfaResultOldTexture = null;
this.jfaResultNewTexture = null;
this.jfaResultOlderFramebuffer = null;
this.jfaResultOldFramebuffer = null;
this.jfaResultNewFramebuffer = null;
this.jfaSeedProgram = null;
@@ -211,6 +222,7 @@ export class TerritoryWebGLRenderer {
viewScale: null,
viewOffset: null,
state: null,
latestState: null,
palette: null,
relations: null,
patterns: null,
@@ -264,14 +276,18 @@ export class TerritoryWebGLRenderer {
this.contestSpeedsTexture = null;
this.contestStrengthsTexture = null;
this.prevOwnerTexture = null;
this.olderOwnerTexture = null;
this.stateFramebuffer = null;
this.prevStateFramebuffer = null;
this.olderStateFramebuffer = null;
this.jfaTextureA = null;
this.jfaTextureB = null;
this.jfaFramebufferA = null;
this.jfaFramebufferB = null;
this.jfaResultOlderTexture = null;
this.jfaResultOldTexture = null;
this.jfaResultNewTexture = null;
this.jfaResultOlderFramebuffer = null;
this.jfaResultOldFramebuffer = null;
this.jfaResultNewFramebuffer = null;
this.jfaSeedProgram = null;
@@ -284,6 +300,7 @@ export class TerritoryWebGLRenderer {
viewScale: null,
viewOffset: null,
state: null,
latestState: null,
palette: null,
relations: null,
patterns: null,
@@ -357,6 +374,7 @@ export class TerritoryWebGLRenderer {
viewScale: gl.getUniformLocation(this.program, "u_viewScale"),
viewOffset: gl.getUniformLocation(this.program, "u_viewOffset"),
state: gl.getUniformLocation(this.program, "u_state"),
latestState: gl.getUniformLocation(this.program, "u_latestState"),
palette: gl.getUniformLocation(this.program, "u_palette"),
relations: gl.getUniformLocation(this.program, "u_relations"),
patterns: gl.getUniformLocation(this.program, "u_patterns"),
@@ -372,7 +390,7 @@ export class TerritoryWebGLRenderer {
contestNow: gl.getUniformLocation(this.program, "u_contestNow"),
contestDuration: gl.getUniformLocation(
this.program,
"u_contestDurationMs",
"u_contestDurationTicks",
),
prevOwner: gl.getUniformLocation(this.program, "u_prevOwner"),
jfaSeedsOld: gl.getUniformLocation(this.program, "u_jfaSeedsOld"),
@@ -466,14 +484,20 @@ export class TerritoryWebGLRenderer {
this.contestSpeedsTexture = gl.createTexture();
this.contestStrengthsTexture = gl.createTexture();
this.prevOwnerTexture = gl.createTexture();
this.olderOwnerTexture = gl.createTexture();
this.stateFramebuffer = gl.createFramebuffer();
this.prevStateFramebuffer = gl.createFramebuffer();
this.olderStateFramebuffer = gl.createFramebuffer();
this.jfaTextureA = this.jfaSupported ? gl.createTexture() : null;
this.jfaTextureB = this.jfaSupported ? gl.createTexture() : null;
this.jfaFramebufferA = this.jfaSupported ? gl.createFramebuffer() : null;
this.jfaFramebufferB = this.jfaSupported ? gl.createFramebuffer() : null;
this.jfaResultOlderTexture = this.jfaSupported ? gl.createTexture() : null;
this.jfaResultOldTexture = this.jfaSupported ? gl.createTexture() : null;
this.jfaResultNewTexture = this.jfaSupported ? gl.createTexture() : null;
this.jfaResultOlderFramebuffer = this.jfaSupported
? gl.createFramebuffer()
: null;
this.jfaResultOldFramebuffer = this.jfaSupported
? gl.createFramebuffer()
: null;
@@ -616,11 +640,32 @@ export class TerritoryWebGLRenderer {
this.state,
);
gl.activeTexture(gl.TEXTURE13);
gl.bindTexture(gl.TEXTURE_2D, this.olderOwnerTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.R16UI,
this.mapWidth,
this.mapHeight,
0,
gl.RED_INTEGER,
gl.UNSIGNED_SHORT,
this.state,
);
if (
this.stateFramebuffer &&
this.prevStateFramebuffer &&
this.olderStateFramebuffer &&
this.stateTexture &&
this.prevOwnerTexture
this.prevOwnerTexture &&
this.olderOwnerTexture
) {
gl.bindFramebuffer(gl.FRAMEBUFFER, this.stateFramebuffer);
gl.framebufferTexture2D(
@@ -640,9 +685,19 @@ export class TerritoryWebGLRenderer {
0,
);
const prevStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
gl.bindFramebuffer(gl.FRAMEBUFFER, this.olderStateFramebuffer);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
this.olderOwnerTexture,
0,
);
const olderStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
this.prevStateCopySupported =
stateStatus === gl.FRAMEBUFFER_COMPLETE &&
prevStatus === gl.FRAMEBUFFER_COMPLETE;
prevStatus === gl.FRAMEBUFFER_COMPLETE &&
olderStatus === gl.FRAMEBUFFER_COMPLETE;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
}
@@ -652,8 +707,10 @@ export class TerritoryWebGLRenderer {
this.jfaTextureB &&
this.jfaFramebufferA &&
this.jfaFramebufferB &&
this.jfaResultOlderTexture &&
this.jfaResultOldTexture &&
this.jfaResultNewTexture &&
this.jfaResultOlderFramebuffer &&
this.jfaResultOldFramebuffer &&
this.jfaResultNewFramebuffer
) {
@@ -710,6 +767,24 @@ export class TerritoryWebGLRenderer {
0,
);
gl.activeTexture(gl.TEXTURE12);
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultOlderTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.RG16F,
this.mapWidth,
this.mapHeight,
0,
gl.RG,
gl.HALF_FLOAT,
null,
);
gl.activeTexture(gl.TEXTURE10);
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultOldTexture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
@@ -746,6 +821,14 @@ export class TerritoryWebGLRenderer {
null,
);
gl.bindFramebuffer(gl.FRAMEBUFFER, this.jfaResultOlderFramebuffer);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
this.jfaResultOlderTexture,
0,
);
gl.bindFramebuffer(gl.FRAMEBUFFER, this.jfaResultOldFramebuffer);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
@@ -770,6 +853,9 @@ export class TerritoryWebGLRenderer {
gl.useProgram(this.program);
gl.uniform1i(this.uniforms.state, 0);
if (this.uniforms.latestState) {
gl.uniform1i(this.uniforms.latestState, 12);
}
gl.uniform1i(this.uniforms.palette, 1);
gl.uniform1i(this.uniforms.relations, 2);
gl.uniform1i(this.uniforms.patterns, 3);
@@ -905,7 +991,7 @@ export class TerritoryWebGLRenderer {
gl.uniform1i(this.uniforms.contestNow, this.contestNow);
}
if (this.uniforms.contestDuration) {
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationMs);
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationTicks);
}
if (this.uniforms.smoothProgress) {
gl.uniform1f(this.uniforms.smoothProgress, this.smoothProgress);
@@ -1167,9 +1253,9 @@ export class TerritoryWebGLRenderer {
this.needsContestStrengthsUpload = true;
}
setContestNow(nowPacked: number, durationMs: number) {
setContestNow(nowPacked: number, durationTicks: number) {
this.contestNow = nowPacked | 0;
this.contestDurationMs = Math.max(0, durationMs);
this.contestDurationTicks = Math.max(0, durationTicks);
}
snapshotStateForSmoothing() {
@@ -1177,11 +1263,27 @@ export class TerritoryWebGLRenderer {
!this.gl ||
!this.prevStateCopySupported ||
!this.stateFramebuffer ||
!this.prevStateFramebuffer
!this.prevStateFramebuffer ||
!this.olderStateFramebuffer
) {
return;
}
const gl = this.gl;
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.prevStateFramebuffer);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.olderStateFramebuffer);
gl.blitFramebuffer(
0,
0,
this.mapWidth,
this.mapHeight,
0,
0,
this.mapWidth,
this.mapHeight,
gl.COLOR_BUFFER_BIT,
gl.NEAREST,
);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.stateFramebuffer);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.prevStateFramebuffer);
gl.blitFramebuffer(
@@ -1198,6 +1300,44 @@ export class TerritoryWebGLRenderer {
);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
if (
this.jfaSupported &&
this.jfaResultOlderFramebuffer &&
this.jfaResultOldFramebuffer &&
this.jfaResultNewFramebuffer
) {
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.jfaResultOldFramebuffer);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOlderFramebuffer);
gl.blitFramebuffer(
0,
0,
this.mapWidth,
this.mapHeight,
0,
0,
this.mapWidth,
this.mapHeight,
gl.COLOR_BUFFER_BIT,
gl.NEAREST,
);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.jfaResultNewFramebuffer);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOldFramebuffer);
gl.blitFramebuffer(
0,
0,
this.mapWidth,
this.mapHeight,
0,
0,
this.mapWidth,
this.mapHeight,
gl.COLOR_BUFFER_BIT,
gl.NEAREST,
);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
}
this.jfaDirty = true;
}
@@ -1214,6 +1354,10 @@ export class TerritoryWebGLRenderer {
!!this.jfaResultNewTexture;
}
setInterpolationPair(pair: "prevCurrent" | "olderPrev") {
this.interpolationPair = pair;
}
markAllDirty() {
this.needsFullUpload = true;
this.dirtyRows.clear();
@@ -1270,7 +1414,7 @@ export class TerritoryWebGLRenderer {
uploadContestStrengthsSpan,
);
if (this.jfaSupported && this.smoothEnabled) {
if (this.jfaSupported) {
this.updateJfa();
}
@@ -1278,9 +1422,23 @@ export class TerritoryWebGLRenderer {
gl.viewport(0, 0, this.viewWidth, this.viewHeight);
gl.useProgram(this.program);
gl.bindVertexArray(this.vao);
if (this.stateTexture) {
const canUseOlderPair =
this.interpolationPair === "olderPrev" &&
!!this.prevOwnerTexture &&
!!this.olderOwnerTexture &&
!!this.jfaResultOldTexture &&
!!this.jfaResultOlderTexture;
const renderPair = canUseOlderPair ? "olderPrev" : "prevCurrent";
const toStateTexture =
renderPair === "olderPrev" ? this.prevOwnerTexture : this.stateTexture;
const fromStateTexture =
renderPair === "olderPrev" ? this.olderOwnerTexture : this.prevOwnerTexture;
if (toStateTexture) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.stateTexture);
gl.bindTexture(gl.TEXTURE_2D, toStateTexture);
}
if (this.paletteTexture) {
gl.activeTexture(gl.TEXTURE1);
@@ -1306,17 +1464,27 @@ export class TerritoryWebGLRenderer {
gl.activeTexture(gl.TEXTURE6);
gl.bindTexture(gl.TEXTURE_2D, this.contestTimesTexture);
}
if (this.prevOwnerTexture) {
if (fromStateTexture) {
gl.activeTexture(gl.TEXTURE7);
gl.bindTexture(gl.TEXTURE_2D, this.prevOwnerTexture);
gl.bindTexture(gl.TEXTURE_2D, fromStateTexture);
}
if (this.jfaResultOldTexture) {
const seedsOld =
renderPair === "olderPrev" ? this.jfaResultOlderTexture : this.jfaResultOldTexture;
const seedsNew =
renderPair === "olderPrev" ? this.jfaResultOldTexture : this.jfaResultNewTexture;
if (seedsOld) {
gl.activeTexture(gl.TEXTURE8);
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultOldTexture);
gl.bindTexture(gl.TEXTURE_2D, seedsOld);
}
if (this.jfaResultNewTexture) {
if (seedsNew) {
gl.activeTexture(gl.TEXTURE9);
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultNewTexture);
gl.bindTexture(gl.TEXTURE_2D, seedsNew);
}
if (this.stateTexture) {
gl.activeTexture(gl.TEXTURE12);
gl.bindTexture(gl.TEXTURE_2D, this.stateTexture);
}
if (this.contestSpeedsTexture) {
gl.activeTexture(gl.TEXTURE10);
@@ -1377,7 +1545,7 @@ export class TerritoryWebGLRenderer {
gl.uniform1i(this.uniforms.contestNow, this.contestNow);
}
if (this.uniforms.contestDuration) {
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationMs);
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationTicks);
}
if (this.uniforms.smoothProgress) {
gl.uniform1f(this.uniforms.smoothProgress, this.smoothProgress);
@@ -1408,7 +1576,7 @@ export class TerritoryWebGLRenderer {
jfaDisabledReason: this.jfaDisabledReason,
jfaDirty: this.jfaDirty,
prevStateCopySupported: this.prevStateCopySupported,
contestDurationMs: this.contestDurationMs,
contestDurationTicks: this.contestDurationTicks,
contestNow: this.contestNow,
hoveredPlayerId: this.hoveredPlayerId,
};
@@ -1662,11 +1830,8 @@ export class TerritoryWebGLRenderer {
!this.jfaFramebufferB ||
!this.jfaTextureA ||
!this.jfaTextureB ||
!this.prevOwnerTexture ||
!this.stateTexture ||
!this.jfaResultOldFramebuffer ||
!this.jfaResultNewFramebuffer ||
!this.jfaResultOldTexture ||
!this.jfaResultNewTexture ||
!this.jfaVao
) {
@@ -1749,11 +1914,47 @@ export class TerritoryWebGLRenderer {
);
};
runJfa(this.prevOwnerTexture, this.jfaResultOldFramebuffer);
runJfa(this.stateTexture, this.jfaResultNewFramebuffer);
this.jfaDirty = false;
if (
!this.jfaHistoryInitialized &&
this.jfaResultOlderFramebuffer &&
this.jfaResultOldFramebuffer
) {
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.jfaResultNewFramebuffer);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOldFramebuffer);
gl.blitFramebuffer(
0,
0,
this.mapWidth,
this.mapHeight,
0,
0,
this.mapWidth,
this.mapHeight,
gl.COLOR_BUFFER_BIT,
gl.NEAREST,
);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOlderFramebuffer);
gl.blitFramebuffer(
0,
0,
this.mapWidth,
this.mapHeight,
0,
0,
this.mapWidth,
this.mapHeight,
gl.COLOR_BUFFER_BIT,
gl.NEAREST,
);
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
this.jfaHistoryInitialized = true;
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
if (prevBlend) {
gl.enable(gl.BLEND);
@@ -2142,6 +2343,7 @@ export class TerritoryWebGLRenderer {
precision highp usampler2D;
uniform usampler2D u_state;
uniform usampler2D u_latestState;
uniform sampler2D u_palette;
uniform usampler2D u_relations;
uniform usampler2D u_patterns;
@@ -2152,7 +2354,7 @@ export class TerritoryWebGLRenderer {
uniform usampler2D u_contestStrengths;
uniform bool u_jfaAvailable;
uniform int u_contestNow;
uniform float u_contestDurationMs;
uniform float u_contestDurationTicks;
uniform usampler2D u_prevOwner;
uniform sampler2D u_jfaSeedsOld;
uniform sampler2D u_jfaSeedsNew;
@@ -2377,6 +2579,8 @@ export class TerritoryWebGLRenderer {
uint owner = state & 0xFFFu;
bool hasFallout = (state & 0x2000u) != 0u;
bool isDefended = (state & 0x1000u) != 0u;
uint latestState = texelFetch(u_latestState, texCoord, 0).r;
uint latestOwner = latestState & 0xFFFu;
uint oldOwner = prevOwnerAtTex(texCoord);
bool smoothActive = u_smoothEnabled &&
u_smoothProgress < 1.0 &&
@@ -2398,7 +2602,7 @@ export class TerritoryWebGLRenderer {
uint elapsed = nowTime >= lastTime
? (nowTime - lastTime)
: (CONTEST_WRAP - lastTime + nowTime);
contested = float(elapsed) < u_contestDurationMs;
contested = float(elapsed) < u_contestDurationTicks;
}
bool isBorder = false;
@@ -2511,8 +2715,15 @@ export class TerritoryWebGLRenderer {
vec3 contestedFillColor = fillColor;
float contestedFillAlpha = fillAlpha;
if (contested && owner != 0u) {
vec3 defenderBase = ownerBase;
bool useContestedFill = false;
if (contested && latestOwner != 0u) {
useContestedFill = true;
vec3 latestOwnerBase = texelFetch(
u_palette,
ivec2(int(latestOwner) * 2, 0),
0
).rgb;
vec3 defenderBase = latestOwnerBase;
if (defender != 0u) {
vec4 defenderColor = texelFetch(
u_palette,
@@ -2523,12 +2734,12 @@ export class TerritoryWebGLRenderer {
}
float strength = contestStrength(contestId);
float noise = blueNoise(texCoord);
contestedFillColor = noise < strength ? ownerBase : defenderBase;
contestedFillColor = noise < strength ? latestOwnerBase : defenderBase;
contestedFillAlpha = u_alpha;
}
vec3 color = contested ? contestedFillColor : fillColor;
float a = contested ? contestedFillAlpha : fillAlpha;
vec3 color = useContestedFill ? contestedFillColor : fillColor;
float a = useContestedFill ? contestedFillAlpha : fillAlpha;
if (isBorder && owner != 0u) {
color = borderColor;
@@ -2661,7 +2872,27 @@ export class TerritoryWebGLRenderer {
}
}
if (contested && owner != 0u && u_jfaAvailable) {
bool pendingOwnerChange = latestOwner != owner;
if (pendingOwnerChange && !useContestedFill && !u_alternativeView) {
vec3 hintColor = vec3(1.0);
if (latestOwner != 0u) {
hintColor = texelFetch(
u_palette,
ivec2(int(latestOwner) * 2, 0),
0
).rgb;
}
const float HINT_ALPHA_RATIO = 0.12;
float hintAlpha = u_alpha * HINT_ALPHA_RATIO;
if (a < hintAlpha) {
a = hintAlpha;
color = hintColor;
} else {
color = mix(color, hintColor, 0.08);
}
}
if (useContestedFill && u_jfaAvailable) {
vec2 seedOld = jfaSeedOldAtTex(texCoord);
vec2 seedNew = jfaSeedNewAtTex(texCoord);
if (seedOld.x >= 0.0 && seedNew.x >= 0.0) {