From 8a638a38427b9a431f73febb8f4d76c86557ee3f Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 30 Apr 2026 16:57:35 -0600 Subject: [PATCH] =?UTF-8?q?perf(UnitLayer):=20batch=20trail=20clears=20to?= =?UTF-8?q?=20fix=20O(n=C2=B2)=20cost=20on=20mass=20nuke=20explosions=20(#?= =?UTF-8?q?3808)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: When multiple nukes detonated in the same tick, clearTrail was called once per dying unit. Each call scanned all remaining units to repaint overlapping trail tiles — O(dead × alive × trail_len) per tick. Replace with a deferred batch: dying units are queued into pendingTrailClears during drawUnitsCells, then flushTrailClears() processes them all at once after the draw pass. All trail tiles are cleared in a single loop (skipping duplicates), followed by one repaint scan of surviving units — O((dead + alive) × trail_len). Also fixes a minor bug in the original: the surviving unit's relationship is now used when repainting its trail (previously the dying unit's relationship was used, which gave wrong colors in alternate-view mode). ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- src/client/graphics/layers/UnitLayer.ts | 40 +++++++++++++++++-------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index d14f4448c..8ded3ee6f 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -40,6 +40,7 @@ export class UnitLayer implements Layer { private unitTrailContext: CanvasRenderingContext2D; private unitToTrail = new Map(); + private pendingTrailClears: UnitView[] = []; private theme: Theme; @@ -381,6 +382,7 @@ export class UnitLayer implements Layer { // otherwise the sprite of a unit can be drawn on top of another unit this.clearUnitsCells(unitsToUpdate); this.drawUnitsCells(unitsToUpdate); + this.flushTrailClears(); } } @@ -546,19 +548,33 @@ export class UnitLayer implements Layer { } } - private clearTrail(unit: UnitView) { - const trail = this.unitToTrail.get(unit) ?? []; - const rel = this.relationship(unit); - for (const t of trail) { - this.clearCell(this.game.x(t), this.game.y(t), this.unitTrailContext); - } - this.unitToTrail.delete(unit); + private flushTrailClears() { + if (this.pendingTrailClears.length === 0) return; - // Repaint overlapping trails - const trailSet = new Set(trail); + const clearedTiles = new Set(); + for (const unit of this.pendingTrailClears) { + const trail = this.unitToTrail.get(unit); + if (trail) { + for (const t of trail) { + if (!clearedTiles.has(t)) { + this.clearCell( + this.game.x(t), + this.game.y(t), + this.unitTrailContext, + ); + clearedTiles.add(t); + } + } + this.unitToTrail.delete(unit); + } + } + this.pendingTrailClears = []; + + // Single repaint pass for all remaining units for (const [other, trail] of this.unitToTrail) { + const rel = this.relationship(other); for (const t of trail) { - if (trailSet.has(t)) { + if (clearedTiles.has(t)) { this.paintCell( this.game.x(t), this.game.y(t), @@ -609,7 +625,7 @@ export class UnitLayer implements Layer { ); this.drawSprite(unit); if (!unit.isActive()) { - this.clearTrail(unit); + this.pendingTrailClears.push(unit); } } @@ -652,7 +668,7 @@ export class UnitLayer implements Layer { this.drawSprite(unit); if (!unit.isActive()) { - this.clearTrail(unit); + this.pendingTrailClears.push(unit); } }