Add relations management to GroundTruthData and update Worker components

- Introduced new properties and methods in GroundTruthData for handling relations, including `needsRelationsUpload`, `relationsDenseBySmallId`, and `pendingRelationsPairs`.
- Implemented logic to mark relations as dirty and upload relations conditionally based on changes in diplomacy.
- Updated Worker.worker.ts to synchronize relations updates with the renderer, optimizing performance by only refreshing when necessary.
- Enhanced WorkerTerritoryRenderer with methods to mark relations dirty, ensuring proper resource management during updates.
This commit is contained in:
scamiv
2026-02-02 03:14:47 +01:00
parent 1ff0b4ddee
commit ae3f0aebe2
3 changed files with 178 additions and 5 deletions
@@ -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<bigint> = 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,
+66 -5
View File
@@ -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({
@@ -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,