Skip to content

Conversation

@wraith4081
Copy link
Contributor

Description:

Render optimizations were applied for RailroadLayer: limiting color updates, skipping off-screen work, drawing only the visible region, and preventing O(n) ray tile removals.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • 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:

wraith4081

@wraith4081 wraith4081 requested a review from a team as a code owner December 5, 2025 21:05
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 5, 2025

Walkthrough

Optimizations to RailroadLayer add tile indexing for O(1) removals, throttle per-frame rail color updates, and apply frustum-based visibility clipping plus partial drawImage remapping to render only visible rail canvas regions.

Changes

Cohort / File(s) Summary
Rail tile indexing & removal
src/client/graphics/layers/RailroadLayer.ts
Added railTileIndex: Map<TileRef, number> and removeRailTile(tile) which swaps the removed tile with the last element, updates indices, truncates the list, and keeps nextRailIndexToCheck consistent. Replaced array filtering with the new helper.
Color update throttling
src/client/graphics/layers/RailroadLayer.ts
Added lastRailColorUpdate and railColorIntervalMs = 50. updateRailColors() now early-returns when empty, caps tiles processed per frame (ceil(length/120), min 1), and throttles repaint frequency.
Frustum-based rendering & drawImage remapping
src/client/graphics/layers/RailroadLayer.ts
renderLayer() now skips when no rails or zero-area viewport, computes visible bounds with padding, remaps source/destination rects for drawImage() to draw only visible portions, and guards against zero-dimension draws.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify railTileIndex stays synchronized with railTileList across add/remove and swap logic.
  • Check nextRailIndexToCheck updates after removals to avoid skipping or repeating tiles.
  • Validate lastRailColorUpdate throttle behavior and initial-frame handling.
  • Confirm source/destination rectangle math for partial drawImage() (edge and off-screen cases).

Possibly related PRs

Suggested labels

Feature - Frontend, Feature - Performance

Suggested reviewers

  • scottanderson
  • evanpelle

Poem

🚂 Tiles swapped quick, colors paced with care,
The viewport trims what eyes need see;
Frames hum lighter, canvas breathes air,
Small changes, big speed — rails run free. ✨

Pre-merge checks

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main optimization work: throttling color updates, culling off-screen blits, and improving tile removal performance in RailroadLayer.
Description check ✅ Passed The description directly relates to the changeset by explaining the render optimizations applied to RailroadLayer, matching the file-level summaries of throttled color updates, viewport culling, and efficient tile removal.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5512583 and 92dd729.

📒 Files selected for processing (1)
  • src/client/graphics/layers/RailroadLayer.ts (7 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-21T22:30:12.246Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2493
File: src/core/game/TrainStation.ts:79-80
Timestamp: 2025-11-21T22:30:12.246Z
Learning: In src/core/game/TrainStation.ts and RailNetworkImpl.ts, the railroad network maintains a system invariant of at most one railroad between any two stations. RailNetworkImpl.connect is private and only called from connectToNearbyStations during initial station connection, which iterates over unique nearby units without duplicates. The Map<TrainStation, Railroad> structure in railroadByNeighbor correctly reflects this 1-to-1 relationship and should not be flagged as potentially allowing silent overwrites.

Applied to files:

  • src/client/graphics/layers/RailroadLayer.ts
📚 Learning: 2025-11-26T22:27:31.844Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2519
File: src/core/worker/SharedTileRing.ts:55-63
Timestamp: 2025-11-26T22:27:31.844Z
Learning: In SharedTileRing.ts, the ring buffer is sized to width * height (the map dimensions). Combined with the dirty flag deduplication mechanism (each tile can only be enqueued once until the consumer clears its dirty flag), the number of pending entries is naturally bounded by the map size and drained every render frame. Therefore, ring buffer overflow should be extremely rare or effectively impossible, and any theoretical race condition between producer and consumer modifying the read index would only manifest as redundant tile refs being rendered, not memory corruption, which is acceptable.
<!-- </add_learning>

Applied to files:

  • src/client/graphics/layers/RailroadLayer.ts
📚 Learning: 2025-10-18T11:00:57.142Z
Learnt from: NewYearNewPhil
Repo: openfrontio/OpenFrontIO PR: 2230
File: src/client/graphics/GameRenderer.ts:269-277
Timestamp: 2025-10-18T11:00:57.142Z
Learning: In src/client/graphics/GameRenderer.ts, the GameRecapCapture implementation does not use setCaptureRenderEnabled on layers. Instead, it uses RecapCaptureSurface.capture() to render capture layers (TerrainLayer, TerritoryLayer, RailroadLayer, StructureIconsLayer, UnitLayer) directly to an off-screen canvas without requiring layer-level capture mode methods.

Applied to files:

  • src/client/graphics/layers/RailroadLayer.ts
🔇 Additional comments (4)
src/client/graphics/layers/RailroadLayer.ts (4)

54-88: Throttling logic looks solid.

The 50ms throttle interval combined with chunked processing (1/120th of rails per frame) is a well-balanced approach. The early returns and repaint-only-when-owner-changes optimization are good. Given the performance data you shared (8 → 36 FPS in extreme scenarios), this clearly helps with large rail networks.


113-159: Nice frustum culling implementation.

The visible region calculation with padding and the remapped drawImage parameters correctly cull off-screen rendering. The coordinate mapping between 2x canvas and tile space is handled properly. The early returns for zero-area regions are good defensive checks.


217-233: O(1) removal looks correct.

The swap-and-pop approach with index tracking properly maintains both railTileList and railTileIndex in sync. The nextRailIndexToCheck reset when the index goes out of bounds is safe. This eliminates the O(n) filtering mentioned in the PR objectives.


185-186: Index maintenance looks good.

Setting the index to the current list length before pushing correctly establishes the tile's position in the list.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/client/graphics/layers/RailroadLayer.ts (1)

54-87: Color scan throttling + chunking may make ownership color changes feel slow on large maps

The throttling and chunking logic is correct, but combined they can make a full pass over railTileList quite slow:

  • You run updateRailColors at most once every railColorIntervalMs = 50 ms.
  • Each call checks at most ceil(railTileList.length / 120) tiles.

At ~60 FPS this means you only run the scan about 20 times per second, so a full pass is ~120 * 50ms = 6s regardless of frame rate. On big maps, a rail whose owner changed might take several seconds to recolor.

If that delay is intentional, this is fine. If you want a tighter bound (for example, “recolor everything within ~2 seconds”), consider making the “120” derived from a target scan duration instead of a magic frame count:

-  private readonly railColorIntervalMs = 50;
+  private readonly railColorIntervalMs = 50;
+  // Aim to touch each rail at least once within this many ms.
+  private readonly railColorFullScanTargetMs = 2000;
@@
-    const maxTilesPerFrame = Math.max(
-      1,
-      Math.ceil(this.railTileList.length / 120),
-    );
+    const scansPerFullPass = Math.max(
+      1,
+      Math.round(this.railColorFullScanTargetMs / this.railColorIntervalMs),
+    );
+    const maxTilesPerFrame = Math.max(
+      1,
+      Math.ceil(this.railTileList.length / scansPerFullPass),
+    );

This keeps the performance win while making the “how long until all rails are refreshed?” behavior explicit and easy to tune.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ca8121 and 5512583.

📒 Files selected for processing (1)
  • src/client/graphics/layers/RailroadLayer.ts (6 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: NewYearNewPhil
Repo: openfrontio/OpenFrontIO PR: 2230
File: src/client/graphics/GameRenderer.ts:269-277
Timestamp: 2025-10-18T11:00:57.142Z
Learning: In src/client/graphics/GameRenderer.ts, the GameRecapCapture implementation does not use setCaptureRenderEnabled on layers. Instead, it uses RecapCaptureSurface.capture() to render capture layers (TerrainLayer, TerritoryLayer, RailroadLayer, StructureIconsLayer, UnitLayer) directly to an off-screen canvas without requiring layer-level capture mode methods.
📚 Learning: 2025-11-21T22:30:12.246Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2493
File: src/core/game/TrainStation.ts:79-80
Timestamp: 2025-11-21T22:30:12.246Z
Learning: In src/core/game/TrainStation.ts and RailNetworkImpl.ts, the railroad network maintains a system invariant of at most one railroad between any two stations. RailNetworkImpl.connect is private and only called from connectToNearbyStations during initial station connection, which iterates over unique nearby units without duplicates. The Map<TrainStation, Railroad> structure in railroadByNeighbor correctly reflects this 1-to-1 relationship and should not be flagged as potentially allowing silent overwrites.

Applied to files:

  • src/client/graphics/layers/RailroadLayer.ts
📚 Learning: 2025-11-26T22:27:31.844Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2519
File: src/core/worker/SharedTileRing.ts:55-63
Timestamp: 2025-11-26T22:27:31.844Z
Learning: In SharedTileRing.ts, the ring buffer is sized to width * height (the map dimensions). Combined with the dirty flag deduplication mechanism (each tile can only be enqueued once until the consumer clears its dirty flag), the number of pending entries is naturally bounded by the map size and drained every render frame. Therefore, ring buffer overflow should be extremely rare or effectively impossible, and any theoretical race condition between producer and consumer modifying the read index would only manifest as redundant tile refs being rendered, not memory corruption, which is acceptable.
<!-- </add_learning>

Applied to files:

  • src/client/graphics/layers/RailroadLayer.ts
📚 Learning: 2025-10-18T11:00:57.142Z
Learnt from: NewYearNewPhil
Repo: openfrontio/OpenFrontIO PR: 2230
File: src/client/graphics/GameRenderer.ts:269-277
Timestamp: 2025-10-18T11:00:57.142Z
Learning: In src/client/graphics/GameRenderer.ts, the GameRecapCapture implementation does not use setCaptureRenderEnabled on layers. Instead, it uses RecapCaptureSurface.capture() to render capture layers (TerrainLayer, TerritoryLayer, RailroadLayer, StructureIconsLayer, UnitLayer) directly to an off-screen canvas without requiring layer-level capture mode methods.

Applied to files:

  • src/client/graphics/layers/RailroadLayer.ts
🧬 Code graph analysis (1)
src/client/graphics/layers/RailroadLayer.ts (1)
src/core/game/GameMap.ts (1)
  • TileRef (3-3)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: 🎨 Prettier
🔇 Additional comments (3)
src/client/graphics/layers/RailroadLayer.ts (3)

28-32: Rail index map and color throttling fields look good

Using railTileIndex for O(1) lookup and lastRailColorUpdate / railColorIntervalMs to gate work is a clean way to support the later optimizations. No issues from a correctness point of view.


117-135: Viewport culling and partial draw are consistent; please confirm coordinate spaces

The early return for existingRailroads.size === 0 and the visible-rect culling (visLeft/visTop/visRight/visBottom) are correct and well clamped. Mapping:

  • vis* in game units → src* scaled by * 2 for the high‑DPI canvas
  • Destination rectangle using -game.width()/2 + visLeft / visTop and visWidth / visHeight

is internally consistent and should draw only the visible slice.

The only thing to double‑check is that screenBoundingRect() returns coordinates in the same “tile space” as game.width() / game.height(). If it were in pixels already, the * 2 factor would be off. Assuming it is tile space, this looks solid.

Also applies to: 141-155


184-187: O(1) tile removal and cursor maintenance look correct

The new index bookkeeping and removeRailTile logic are sound:

  • On insert you set railTileIndex to the current railTileList.length and then push, so the index always points at the added element.
  • On removal you:
    • Look up idx from railTileIndex and return early if the tile was never indexed.
    • Swap the last tile into idx, update its index in railTileIndex, then pop and delete the removed tile’s index.
    • Clamp nextRailIndexToCheck back into range and decrement it when removing an element at or before the current cursor, so iteration over railTileList does not skip entries.

I walked through edge cases (single element, removing last, removing before and at the cursor); they all keep the list and map in sync and keep the cursor within bounds, at the cost of some benign re-checks, which is fine.

No changes needed here.

Also applies to: 190-197, 216-238

coderabbitai[bot]
coderabbitai bot previously approved these changes Dec 5, 2025
Copy link
Contributor

@DevelopingTom DevelopingTom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. If you could address the few comments / update, it will be approved.

@DevelopingTom DevelopingTom added the Performance Performance optimization label Dec 7, 2025
Copy link
Contributor

@DevelopingTom DevelopingTom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@DevelopingTom DevelopingTom added this to the v28 milestone Dec 7, 2025
@DevelopingTom DevelopingTom added this pull request to the merge queue Dec 7, 2025
Merged via the queue into openfrontio:main with commit 7658c67 Dec 7, 2025
8 of 9 checks passed
trajkovdimitar pushed a commit to trajkovdimitar/OpenFrontIO that referenced this pull request Dec 8, 2025
…openfrontio#2565)

## Description:

Render optimizations were applied for RailroadLayer: limiting color
updates, skipping off-screen work, drawing only the visible region, and
preventing O(n) ray tile removals.

## 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:

wraith4081
@wraith4081 wraith4081 deleted the feat/optimize-railroadlayer branch December 23, 2025 04:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Performance Performance optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants