diff --git a/src/client/graphics/webgpu/core/GroundTruthData.ts b/src/client/graphics/webgpu/core/GroundTruthData.ts index ab36a30d7..c12a39bad 100644 --- a/src/client/graphics/webgpu/core/GroundTruthData.ts +++ b/src/client/graphics/webgpu/core/GroundTruthData.ts @@ -113,6 +113,10 @@ export class GroundTruthData { private paletteMaxSmallId = 0; private ownerIndexWidth = 1; private relationsSize = 1; + private needsRelationsUpload = true; + private relationsDenseBySmallId: Uint32Array | null = null; + private pendingRelationsPairs: Set = new Set(); + private readonly relationWriteScratch = new Uint8Array(256); private constructor( private readonly device: GPUDevice, @@ -720,6 +724,8 @@ export class GroundTruthData { } this.needsPaletteUpload = false; + const prevMaxSmallId = this.paletteMaxSmallId; + let maxSmallId = 0; let nextPaletteWidth = 0; let row0: Uint8Array | null = null; @@ -780,6 +786,10 @@ export class GroundTruthData { } this.paletteMaxSmallId = maxSmallId; + if (this.paletteMaxSmallId !== prevMaxSmallId) { + // Relations/owner-index textures depend on maxSmallId. + this.needsRelationsUpload = true; + } let textureRecreated = false; if (nextPaletteWidth !== this.paletteWidth) { @@ -819,6 +829,16 @@ export class GroundTruthData { } uploadRelations(): boolean { + if (!this.needsRelationsUpload && this.pendingRelationsPairs.size > 0) { + return this.uploadRelationsPartial(); + } + if (!this.needsRelationsUpload) { + return false; + } + + this.needsRelationsUpload = false; + this.pendingRelationsPairs.clear(); + const players = this.game .playerViews() .filter((p) => p.smallID() > 0) @@ -852,6 +872,7 @@ export class GroundTruthData { dense++; denseBySmallId[id] = dense; } + this.relationsDenseBySmallId = denseBySmallId; const ownerIndexBytesPerRow = align(this.ownerIndexWidth * 4, 256); const ownerIndexPaddedU32 = new Uint32Array(ownerIndexBytesPerRow / 4); @@ -916,6 +937,69 @@ export class GroundTruthData { return textureRecreated; } + private uploadRelationsPartial(): boolean { + if (!this.relationsDenseBySmallId || !this.relationsTexture) { + // No stable mapping/texture yet: fall back to a full rebuild. + this.needsRelationsUpload = true; + this.pendingRelationsPairs.clear(); + return false; + } + + const denseBySmallId = this.relationsDenseBySmallId; + const size = this.relationsSize; + const scratch = this.relationWriteScratch; + const bytesPerRow = 256; + + const writeTexel = (x: number, y: number, value: number) => { + if (x <= 0 || y <= 0 || x >= size || y >= size) { + return; + } + scratch.fill(0); + scratch[0] = value & 0xff; + this.device.queue.writeTexture( + { texture: this.relationsTexture, origin: { x, y } }, + scratch, + { bytesPerRow, rowsPerImage: 1 }, + { width: 1, height: 1, depthOrArrayLayers: 1 }, + ); + }; + + const computeCode = (aSmall: number, bSmall: number): number => { + if (aSmall === bSmall) return 0; + const aAny: any = (this.game as any).playerBySmallID?.(aSmall); + const bAny: any = (this.game as any).playerBySmallID?.(bSmall); + if (!aAny || !bAny || !aAny.isPlayer?.() || !bAny.isPlayer?.()) { + return 0; + } + if (aAny.hasEmbargo?.(bAny)) { + return 2; + } + if (aAny.isFriendly?.(bAny) || bAny.isFriendly?.(aAny)) { + return 1; + } + return 0; + }; + + for (const key of this.pendingRelationsPairs) { + const aSmall = Number(key >> 32n); + const bSmall = Number(key & 0xffffffffn); + const aDense = denseBySmallId[aSmall] ?? 0; + const bDense = denseBySmallId[bSmall] ?? 0; + if (aDense === 0 || bDense === 0) { + continue; + } + + const code = computeCode(aSmall, bSmall); + writeTexel(aDense, bDense, code); + if (aDense !== bDense) { + writeTexel(bDense, aDense, code); + } + } + + this.pendingRelationsPairs.clear(); + return false; + } + uploadDefensePosts(): void { if (!this.needsDefensePostsUpload) { return; @@ -1287,6 +1371,26 @@ export class GroundTruthData { this.needsPaletteUpload = true; } + markRelationsDirty(): void { + this.needsRelationsUpload = true; + this.pendingRelationsPairs.clear(); + } + + markRelationsPairDirty(aSmallId: number, bSmallId: number): void { + if (aSmallId <= 0 || bSmallId <= 0) { + return; + } + if (!this.relationsDenseBySmallId) { + // No mapping yet: ensure a full rebuild occurs. + this.needsRelationsUpload = true; + return; + } + const a = Math.min(aSmallId, bSmallId); + const b = Math.max(aSmallId, bSmallId); + const key = (BigInt(a) << 32n) | BigInt(b); + this.pendingRelationsPairs.add(key); + } + setPaletteOverride( paletteWidth: number, maxSmallId: number, diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index d74d8ab2d..3f037ab12 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -4,7 +4,15 @@ import { PastelTheme } from "../configuration/PastelTheme"; import { PastelThemeDark } from "../configuration/PastelThemeDark"; import { FetchGameMapLoader } from "../game/FetchGameMapLoader"; import { PlayerID } from "../game/Game"; -import { ErrorUpdate, GameUpdateViewData } from "../game/GameUpdates"; +import { + AllianceExpiredUpdate, + AllianceRequestReplyUpdate, + BrokeAllianceUpdate, + EmbargoUpdate, + ErrorUpdate, + GameUpdateType, + GameUpdateViewData, +} from "../game/GameUpdates"; import { loadTerrainMap, TerrainMapData } from "../game/TerrainMapLoader"; import { createGameRunner, GameRunner } from "../GameRunner"; import { ClientID, GameStartInfo, PlayerCosmetics } from "../Schemas"; @@ -40,22 +48,75 @@ function gameUpdate(gu: GameUpdateViewData | ErrorUpdate) { return; } + // Keep renderer-side adapter in sync (palette/relations/etc). + (renderer as any)?.updateGameView?.(gu); + + // Uploading relations is expensive; only refresh when diplomacy changes, + // and only for the affected player pairs. + const updates = gu.updates; + let relationsChanged = false; + if (renderer) { + const markPair = (aSmallId: number, bSmallId: number) => { + const r: any = renderer as any; + if (r?.markRelationsPairDirty) { + r.markRelationsPairDirty(aSmallId, bSmallId); + relationsChanged = true; + } else if (r?.markRelationsDirty) { + // Fallback for older/other renderers. + r.markRelationsDirty(); + relationsChanged = true; + } + }; + + for (const e of updates[GameUpdateType.EmbargoEvent] as EmbargoUpdate[]) { + markPair(e.playerID, e.embargoedID); + } + for (const e of updates[ + GameUpdateType.AllianceRequestReply + ] as AllianceRequestReplyUpdate[]) { + if (e.accepted) { + markPair(e.request.requestorID, e.request.recipientID); + } + } + for (const e of updates[ + GameUpdateType.BrokeAlliance + ] as BrokeAllianceUpdate[]) { + markPair(e.traitorID, e.betrayedID); + } + for (const e of updates[ + GameUpdateType.AllianceExpired + ] as AllianceExpiredUpdate[]) { + markPair(e.player1ID, e.player2ID); + } + } + // Flush simulation-derived dirty tiles into the renderer before running // compute passes for this tick. if (renderer && dirtyTiles) { + let didWork = false; + if (relationsChanged) { + didWork = true; + } if (dirtyTilesOverflow) { dirtyTilesOverflow = false; dirtyTiles.clear(); renderer.markAllDirty(); + didWork = true; } else { - const tiles = dirtyTiles.drain(dirtyTiles.pendingCount()); - for (const tile of tiles) { - renderer.markTile(tile); + const pending = dirtyTiles.pendingCount(); + if (pending > 0) { + const tiles = dirtyTiles.drain(pending); + for (const tile of tiles) { + renderer.markTile(tile); + } + didWork = true; } } // Run compute passes at simulation tick cadence (not at render FPS). - renderer.tick(); + if (didWork) { + renderer.tick(); + } } sendMessage({ diff --git a/src/core/worker/WorkerTerritoryRenderer.ts b/src/core/worker/WorkerTerritoryRenderer.ts index 2643944d8..4af7e8795 100644 --- a/src/core/worker/WorkerTerritoryRenderer.ts +++ b/src/core/worker/WorkerTerritoryRenderer.ts @@ -441,6 +441,14 @@ export class WorkerTerritoryRenderer { this.resources.markPaletteDirty(); } + markRelationsDirty(): void { + this.resources?.markRelationsDirty(); + } + + markRelationsPairDirty(aSmallId: number, bSmallId: number): void { + this.resources?.markRelationsPairDirty(aSmallId, bSmallId); + } + setPaletteFromBytes( paletteWidth: number, maxSmallId: number,