From 5512583cd644bc065c07bd88d81ccdca5c432779 Mon Sep 17 00:00:00 2001 From: wraith4081 Date: Sat, 6 Dec 2025 00:02:20 +0300 Subject: [PATCH 1/4] optimize(RailroadLayer): throttle color scans, cull blits, and remove O(n) tile deletions --- src/client/graphics/layers/RailroadLayer.ts | 81 +++++++++++++++++++-- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/src/client/graphics/layers/RailroadLayer.ts b/src/client/graphics/layers/RailroadLayer.ts index 3a863a6998..213f96f17e 100644 --- a/src/client/graphics/layers/RailroadLayer.ts +++ b/src/client/graphics/layers/RailroadLayer.ts @@ -27,6 +27,9 @@ export class RailroadLayer implements Layer { private existingRailroads = new Map(); private nextRailIndexToCheck = 0; private railTileList: TileRef[] = []; + private railTileIndex = new Map(); + private lastRailColorUpdate = 0; + private readonly railColorIntervalMs = 50; constructor( private game: GameView, @@ -49,7 +52,19 @@ export class RailroadLayer implements Layer { } updateRailColors() { - const maxTilesPerFrame = this.railTileList.length / 60; + const now = performance.now(); + if (now - this.lastRailColorUpdate < this.railColorIntervalMs) { + return; + } + this.lastRailColorUpdate = now; + + const maxTilesPerFrame = Math.max( + 1, + Math.ceil(this.railTileList.length / 120), + ); + if (this.railTileList.length === 0) { + return; + } let checked = 0; while (checked < maxTilesPerFrame && this.railTileList.length > 0) { @@ -95,22 +110,49 @@ export class RailroadLayer implements Layer { } renderLayer(context: CanvasRenderingContext2D) { - this.updateRailColors(); const scale = this.transformHandler.scale; if (scale <= 1) { return; } + if (this.existingRailroads.size === 0) { + return; + } + this.updateRailColors(); const rawAlpha = (scale - 1) / (2 - 1); // maps 1->0, 2->1 const alpha = Math.max(0, Math.min(1, rawAlpha)); + const [topLeft, bottomRight] = this.transformHandler.screenBoundingRect(); + const padding = 2; // small margin so edges do not pop + const visLeft = Math.max(0, topLeft.x - padding); + const visTop = Math.max(0, topLeft.y - padding); + const visRight = Math.min(this.game.width(), bottomRight.x + padding); + const visBottom = Math.min(this.game.height(), bottomRight.y + padding); + const visWidth = Math.max(0, visRight - visLeft); + const visHeight = Math.max(0, visBottom - visTop); + if (visWidth === 0 || visHeight === 0) { + return; + } + + const srcX = visLeft * 2; + const srcY = visTop * 2; + const srcW = visWidth * 2; + const srcH = visHeight * 2; + + const dstX = -this.game.width() / 2 + visLeft; + const dstY = -this.game.height() / 2 + visTop; + context.save(); context.globalAlpha = alpha; context.drawImage( this.canvas, - -this.game.width() / 2, - -this.game.height() / 2, - this.game.width(), - this.game.height(), + srcX, + srcY, + srcW, + srcH, + dstX, + dstY, + visWidth, + visHeight, ); context.restore(); } @@ -139,6 +181,7 @@ export class RailroadLayer implements Layer { numOccurence: 1, lastOwnerId: currentOwner, }); + this.railTileIndex.set(railRoad.tile, this.railTileList.length); this.railTileList.push(railRoad.tile); this.paintRail(railRoad); } @@ -150,7 +193,7 @@ export class RailroadLayer implements Layer { if (!ref || ref.numOccurence <= 0) { this.existingRailroads.delete(railRoad.tile); - this.railTileList = this.railTileList.filter((t) => t !== railRoad.tile); + this.removeRailTile(railRoad.tile); if (this.context === undefined) throw new Error("Not initialized"); if (this.game.isWater(railRoad.tile)) { this.context.clearRect( @@ -170,6 +213,30 @@ export class RailroadLayer implements Layer { } } + private removeRailTile(tile: TileRef) { + const idx = this.railTileIndex.get(tile); + if (idx === undefined) return; + + const lastIndex = this.railTileList.length - 1; + const lastTile = this.railTileList[lastIndex]; + + this.railTileList[idx] = lastTile; + this.railTileIndex.set(lastTile, idx); + + this.railTileList.pop(); + this.railTileIndex.delete(tile); + + if (this.nextRailIndexToCheck >= this.railTileList.length) { + this.nextRailIndexToCheck = 0; + } else if ( + idx <= this.nextRailIndexToCheck && + this.nextRailIndexToCheck > 0 + ) { + // Keep iteration stable when removing an element before the current cursor + this.nextRailIndexToCheck--; + } + } + paintRail(railRoad: RailTile) { if (this.context === undefined) throw new Error("Not initialized"); const { tile } = railRoad; From cb161a562809ef9b53110233e1c0bf9cd755b6bc Mon Sep 17 00:00:00 2001 From: wraith4081 Date: Sat, 6 Dec 2025 03:16:40 +0300 Subject: [PATCH 2/4] chore(RailroadLayer): remove the cursor decrement logic in removeRailTile --- src/client/graphics/layers/RailroadLayer.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/client/graphics/layers/RailroadLayer.ts b/src/client/graphics/layers/RailroadLayer.ts index 213f96f17e..0bc49453de 100644 --- a/src/client/graphics/layers/RailroadLayer.ts +++ b/src/client/graphics/layers/RailroadLayer.ts @@ -228,12 +228,6 @@ export class RailroadLayer implements Layer { if (this.nextRailIndexToCheck >= this.railTileList.length) { this.nextRailIndexToCheck = 0; - } else if ( - idx <= this.nextRailIndexToCheck && - this.nextRailIndexToCheck > 0 - ) { - // Keep iteration stable when removing an element before the current cursor - this.nextRailIndexToCheck--; } } From 8c973588b7baabd8a046e987f2342a831c509746 Mon Sep 17 00:00:00 2001 From: wraith4081 Date: Sun, 7 Dec 2025 16:05:21 +0300 Subject: [PATCH 3/4] chore(RailroadLayer): move the early return check for the railTile list to the beginning --- src/client/graphics/layers/RailroadLayer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/graphics/layers/RailroadLayer.ts b/src/client/graphics/layers/RailroadLayer.ts index 0bc49453de..c887a42690 100644 --- a/src/client/graphics/layers/RailroadLayer.ts +++ b/src/client/graphics/layers/RailroadLayer.ts @@ -52,6 +52,9 @@ export class RailroadLayer implements Layer { } updateRailColors() { + if (this.railTileList.length === 0) { + return; + } const now = performance.now(); if (now - this.lastRailColorUpdate < this.railColorIntervalMs) { return; @@ -62,9 +65,6 @@ export class RailroadLayer implements Layer { 1, Math.ceil(this.railTileList.length / 120), ); - if (this.railTileList.length === 0) { - return; - } let checked = 0; while (checked < maxTilesPerFrame && this.railTileList.length > 0) { From 92dd72992679aad32594fd7fe6a93ee3b9bb2ecc Mon Sep 17 00:00:00 2001 From: wraith4081 Date: Sun, 7 Dec 2025 16:20:15 +0300 Subject: [PATCH 4/4] chore(RailroadLayer): clarify rail color refresh logic and simplify index wrap --- src/client/graphics/layers/RailroadLayer.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/client/graphics/layers/RailroadLayer.ts b/src/client/graphics/layers/RailroadLayer.ts index c887a42690..d53ad24703 100644 --- a/src/client/graphics/layers/RailroadLayer.ts +++ b/src/client/graphics/layers/RailroadLayer.ts @@ -55,12 +55,14 @@ export class RailroadLayer implements Layer { if (this.railTileList.length === 0) { return; } + // Throttle color checks so we do not re-evaluate on every frame const now = performance.now(); if (now - this.lastRailColorUpdate < this.railColorIntervalMs) { return; } this.lastRailColorUpdate = now; + // Spread work over multiple frames to avoid large bursts when many rails exist const maxTilesPerFrame = Math.max( 1, Math.ceil(this.railTileList.length / 120), @@ -73,15 +75,14 @@ export class RailroadLayer implements Layer { if (railRef) { const currentOwner = this.game.owner(tile)?.id() ?? null; if (railRef.lastOwnerId !== currentOwner) { + // Repaint only when the owner changed to keep colors in sync railRef.lastOwnerId = currentOwner; this.paintRail(railRef.tile); } } - this.nextRailIndexToCheck++; - if (this.nextRailIndexToCheck >= this.railTileList.length) { - this.nextRailIndexToCheck = 0; - } + this.nextRailIndexToCheck = + (this.nextRailIndexToCheck + 1) % this.railTileList.length; checked++; } }