From 9ed794264d332080e7e55e576b09f8dcf24ffe89 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:01:43 +0100 Subject: [PATCH] Move relationship calculation (for borders) into the shader. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GPU Relation Calculations: Moved border diplomatic relation logic from CPU buffer to WebGL shader, eliminating per-tile CPU computation - Relation Matrix: Converted 1D relation array to 2D ownerĂ—other matrix for O(1) GPU lookups - Palette Refresh: Batched palette refreshes to single call per update cycle WebGL Shader Updates - Added u_viewerId uniform and bitmask relation helper functions (isFriendly(), isEmbargo(), isSelf()) - Enhanced border detection with per-neighbor relation evaluation Removed CPU-side relation state management Files: TerritoryLayer.ts, TerritoryRenderers.ts, TerritoryWebGLRenderer.ts (+102/-39 lines) --- src/client/graphics/layers/TerritoryLayer.ts | 10 +- .../graphics/layers/TerritoryRenderers.ts | 13 +- .../graphics/layers/TerritoryWebGLRenderer.ts | 118 ++++++++++++++---- 3 files changed, 102 insertions(+), 39 deletions(-) diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 7b7140f41..c63e25534 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -106,9 +106,7 @@ export class TerritoryLayer implements Layer { const unitUpdates = updates !== null ? updates[GameUpdateType.Unit] : []; const playerUpdates = updates !== null ? updates[GameUpdateType.Player] : []; - if (playerUpdates.length > 0) { - this.territoryRenderer?.refreshPalette(); - } + let needsRelationRefresh = playerUpdates.length > 0; unitUpdates.forEach((update) => { if (update.unitType === UnitType.DefensePost) { // Only update borders if the defense post is not under construction @@ -138,6 +136,7 @@ export class TerritoryLayer implements Layer { const territory = this.game.playerBySmallID(update.betrayedID); if (territory && territory instanceof PlayerView) { this.redrawBorder(territory); + needsRelationRefresh = true; } }); @@ -154,6 +153,7 @@ export class TerritoryLayer implements Layer { const territory = this.game.playerBySmallID(territoryId); if (territory && territory instanceof PlayerView) { this.redrawBorder(territory); + needsRelationRefresh = true; } } }); @@ -168,9 +168,13 @@ export class TerritoryLayer implements Layer { embargoed.id() === myPlayer?.id() ) { this.redrawBorder(player, embargoed); + needsRelationRefresh = true; } }); } + if (needsRelationRefresh) { + this.territoryRenderer?.refreshPalette(); + } const focusedPlayer = this.game.focusedPlayer(); if (focusedPlayer !== this.lastFocusedPlayer) { diff --git a/src/client/graphics/layers/TerritoryRenderers.ts b/src/client/graphics/layers/TerritoryRenderers.ts index 9710fc9a1..c3ab6aafa 100644 --- a/src/client/graphics/layers/TerritoryRenderers.ts +++ b/src/client/graphics/layers/TerritoryRenderers.ts @@ -274,7 +274,7 @@ export class WebglTerritoryRenderer implements TerritoryRendererStrategy { : null; const isBorderTile = this.game.isBorder(tile); - // Update defended and relation state in the shared buffer + // Update defended state in the shared buffer (used for checkerboard pattern). if (owner && isBorderTile) { const isDefended = this.game.hasUnitNearby( tile, @@ -282,19 +282,10 @@ export class WebglTerritoryRenderer implements TerritoryRendererStrategy { UnitType.DefensePost, owner.id(), ); - const { hasEmbargo, hasFriendly } = owner.borderRelationFlags(tile); - let relation = 0; // neutral - if (hasFriendly) { - relation = 1; // friendly - } else if (hasEmbargo) { - relation = 2; // embargo - } this.game.setDefended(tile, isDefended); - this.game.setRelation(tile, relation); } else { - // Clear defended/relation state for non-border tiles + // Clear defended state for non-border tiles this.game.setDefended(tile, false); - this.game.setRelation(tile, 0); } this.renderer.markTile(tile); diff --git a/src/client/graphics/layers/TerritoryWebGLRenderer.ts b/src/client/graphics/layers/TerritoryWebGLRenderer.ts index 66f779da0..8b69aff17 100644 --- a/src/client/graphics/layers/TerritoryWebGLRenderer.ts +++ b/src/client/graphics/layers/TerritoryWebGLRenderer.ts @@ -52,6 +52,7 @@ export class TerritoryWebGLRenderer { hoverPulseStrength: WebGLUniformLocation | null; hoverPulseSpeed: WebGLUniformLocation | null; time: WebGLUniformLocation | null; + viewerId: WebGLUniformLocation | null; // Border color uniforms for shader-computed borders borderNeutral: WebGLUniformLocation | null; borderFriendly: WebGLUniformLocation | null; @@ -126,6 +127,7 @@ export class TerritoryWebGLRenderer { hoverPulseStrength: null, hoverPulseSpeed: null, time: null, + viewerId: null, borderNeutral: null, borderFriendly: null, borderEmbargo: null, @@ -167,6 +169,7 @@ export class TerritoryWebGLRenderer { hoverPulseStrength: null, hoverPulseSpeed: null, time: null, + viewerId: null, borderNeutral: null, borderFriendly: null, borderEmbargo: null, @@ -208,6 +211,7 @@ export class TerritoryWebGLRenderer { ), hoverPulseSpeed: gl.getUniformLocation(this.program, "u_hoverPulseSpeed"), time: gl.getUniformLocation(this.program, "u_time"), + viewerId: gl.getUniformLocation(this.program, "u_viewerId"), borderNeutral: gl.getUniformLocation(this.program, "u_borderNeutral"), borderFriendly: gl.getUniformLocation(this.program, "u_borderFriendly"), borderEmbargo: gl.getUniformLocation(this.program, "u_borderEmbargo"), @@ -375,6 +379,10 @@ export class TerritoryWebGLRenderer { c.a ?? 1, ); } + if (this.uniforms.viewerId) { + const viewerId = this.game.myPlayer()?.smallID() ?? 0; + gl.uniform1i(this.uniforms.viewerId, viewerId); + } if (this.uniforms.alternativeView) { gl.uniform1i(this.uniforms.alternativeView, 0); } @@ -573,6 +581,10 @@ export class TerritoryWebGLRenderer { const currentTime = (Date.now() - this.animationStartTime) / 1000.0; gl.uniform1f(this.uniforms.time, currentTime); } + if (this.uniforms.viewerId) { + const viewerId = this.game.myPlayer()?.smallID() ?? 0; + gl.uniform1i(this.uniforms.viewerId, viewerId); + } gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); @@ -714,13 +726,12 @@ export class TerritoryWebGLRenderer { if (!this.gl || !this.paletteTexture || !this.relationTexture) return; const gl = this.gl; const players = this.game.playerViews().filter((p) => p.isPlayer()); - const myPlayer = this.game.myPlayer(); const maxId = players.reduce((max, p) => Math.max(max, p.smallID()), 0) + 1; this.paletteWidth = Math.max(maxId, 1); const paletteData = new Uint8Array(this.paletteWidth * 8); // 8 bytes per player: territory RGBA + border RGBA - const relationData = new Uint8Array(this.paletteWidth); + const relationData = new Uint8Array(this.paletteWidth * this.paletteWidth); for (const p of players) { const id = p.smallID(); @@ -737,8 +748,16 @@ export class TerritoryWebGLRenderer { paletteData[id * 8 + 5] = borderRgba.g; paletteData[id * 8 + 6] = borderRgba.b; paletteData[id * 8 + 7] = Math.round((borderRgba.a ?? 1) * 255); + } - relationData[id] = this.resolveRelationCode(p, myPlayer); + // Build relation matrix: friendly/embargo/self flags per owner/other pair. + for (let ownerId = 0; ownerId < this.paletteWidth; ownerId++) { + const owner = this.safePlayerBySmallId(ownerId); + for (let otherId = 0; otherId < this.paletteWidth; otherId++) { + const other = this.safePlayerBySmallId(otherId); + relationData[ownerId * this.paletteWidth + otherId] = + this.resolveRelationCode(owner, other); + } } gl.activeTexture(gl.TEXTURE1); @@ -772,7 +791,7 @@ export class TerritoryWebGLRenderer { 0, gl.R8UI, this.paletteWidth, - 1, + this.paletteWidth, 0, gl.RED_INTEGER, gl.UNSIGNED_BYTE, @@ -781,22 +800,31 @@ export class TerritoryWebGLRenderer { } private resolveRelationCode( - owner: PlayerView, - myPlayer: PlayerView | null, + owner: PlayerView | null, + other: PlayerView | null, ): number { - if (!myPlayer) { - return 3; // Neutral + if (!owner || !other || !owner.isPlayer() || !other.isPlayer()) { + return 0; // Neutral / no relation } - if (owner.smallID() === myPlayer.smallID()) { - return 1; // Self + + let code = 0; + if (owner.smallID() === other.smallID()) { + code |= 4; // self bit } - if (owner.isFriendly(myPlayer)) { - return 2; // Ally + // Friendly if either side is friendly toward the other. + if (owner.isFriendly(other) || other.isFriendly(owner)) { + code |= 1; } - if (!owner.hasEmbargo(myPlayer)) { - return 3; // Neutral + // Embargo if owner has embargo against other. + if (owner.hasEmbargo(other)) { + code |= 2; } - return 4; // Enemy + return code; + } + + private safePlayerBySmallId(id: number): PlayerView | null { + const player = this.game.playerBySmallID(id); + return player instanceof PlayerView ? player : null; } private createProgram(gl: WebGL2RenderingContext): WebGLProgram | null { @@ -820,6 +848,7 @@ export class TerritoryWebGLRenderer { uniform sampler2D u_palette; uniform usampler2D u_relations; uniform sampler2D u_borderColor; + uniform int u_viewerId; uniform vec2 u_resolution; uniform vec4 u_fallout; uniform vec4 u_altSelf; @@ -855,6 +884,25 @@ export class TerritoryWebGLRenderer { return texelFetch(u_state, clamped, 0).r & 0xFFFu; } + uint relationCode(uint owner, uint other) { + if (owner == 0u || other == 0u) { + return 0u; + } + return texelFetch(u_relations, ivec2(int(owner), int(other)), 0).r; + } + + bool isFriendly(uint code) { + return (code & 1u) != 0u; + } + + bool isEmbargo(uint code) { + return (code & 2u) != 0u; + } + + bool isSelf(uint code) { + return (code & 4u) != 0u; + } + void main() { ivec2 fragCoord = ivec2(gl_FragCoord.xy); // gl_FragCoord origin is bottom-left; flip Y to match top-left oriented buffers. @@ -864,7 +912,6 @@ export class TerritoryWebGLRenderer { uint owner = state & 0xFFFu; bool hasFallout = (state & 0x2000u) != 0u; // bit 13 bool isDefended = (state & 0x1000u) != 0u; // bit 12 - uint relation = (state & 0xC000u) >> 14u; // bits 14-15 if (owner == 0u) { if (hasFallout) { @@ -877,25 +924,47 @@ export class TerritoryWebGLRenderer { return; } - // Border detection via neighbor comparison + // Border detection via neighbor comparison and relation checks bool isBorder = false; + bool hasFriendlyRelation = false; + bool hasEmbargoRelation = false; uint nOwner = ownerAtTex(texCoord + ivec2(1, 0)); isBorder = isBorder || (nOwner != owner); + if (nOwner != owner && nOwner != 0u) { + uint rel = relationCode(owner, nOwner); + hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel); + hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel); + } nOwner = ownerAtTex(texCoord + ivec2(-1, 0)); isBorder = isBorder || (nOwner != owner); + if (nOwner != owner && nOwner != 0u) { + uint rel = relationCode(owner, nOwner); + hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel); + hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel); + } nOwner = ownerAtTex(texCoord + ivec2(0, 1)); isBorder = isBorder || (nOwner != owner); + if (nOwner != owner && nOwner != 0u) { + uint rel = relationCode(owner, nOwner); + hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel); + hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel); + } nOwner = ownerAtTex(texCoord + ivec2(0, -1)); isBorder = isBorder || (nOwner != owner); + if (nOwner != owner && nOwner != 0u) { + uint rel = relationCode(owner, nOwner); + hasEmbargoRelation = hasEmbargoRelation || isEmbargo(rel); + hasFriendlyRelation = hasFriendlyRelation || isFriendly(rel); + } if (u_alternativeView) { - uint relation = texelFetch(u_relations, ivec2(int(owner), 0), 0).r; + uint relationAlt = relationCode(owner, uint(u_viewerId)); vec4 altColor = u_altNeutral; - if (relation == 1u) { + if (isSelf(relationAlt)) { altColor = u_altSelf; - } else if (relation == 2u) { + } else if (isFriendly(relationAlt)) { altColor = u_altAlly; - } else if (relation >= 4u) { + } else if (isEmbargo(relationAlt)) { altColor = u_altEnemy; } float a = isBorder ? 1.0 : 0.0; @@ -925,19 +994,18 @@ export class TerritoryWebGLRenderer { const vec3 FRIENDLY_TINT_TARGET = vec3(0.0, 1.0, 0.0); // green const vec3 EMBARGO_TINT_TARGET = vec3(1.0, 0.0, 0.0); // red - if (relation == 1u) { // friendly + if (hasFriendlyRelation) { // friendly borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) + FRIENDLY_TINT_TARGET * BORDER_TINT_RATIO; - } else if (relation == 2u) { // embargo + } + if (hasEmbargoRelation) { // embargo borderColor = borderColor * (1.0 - BORDER_TINT_RATIO) + EMBARGO_TINT_TARGET * BORDER_TINT_RATIO; } - // relation == 0u (neutral) uses base border color as-is // Apply defended checkerboard pattern if (isDefended) { bool isLightTile = ((texCoord.x % 2) == (texCoord.y % 2)); - // Simple checkerboard: alternate between lighter and darker versions const float LIGHT_FACTOR = 1.2; const float DARK_FACTOR = 0.8; borderColor *= isLightTile ? LIGHT_FACTOR : DARK_FACTOR;