Skip to content

Make structure icon recovery state-sourced after runtime renderer disruption#3480

Open
Skigim wants to merge 7 commits intoopenfrontio:mainfrom
Skigim:copilot/investigate-rendering-bug
Open

Make structure icon recovery state-sourced after runtime renderer disruption#3480
Skigim wants to merge 7 commits intoopenfrontio:mainfrom
Skigim:copilot/investigate-rendering-bug

Conversation

@Skigim
Copy link
Contributor

@Skigim Skigim commented Mar 20, 2026

Description:

I don't have a clean repro for the mid-match structure icon issue yet, so this PR is meant to reduce the chance of structure icons getting lost after a renderer reset.

This also does not address the separate issue where structure icons sometimes fail to show from game start.

This changes structure icon rendering to rebuild from the current game state instead of mostly waiting for future unit updates.

Main changes:

  • make StructureIconsLayer.redraw() rebuild structure icons from game.units()
  • rebuild the layer again after WebGL context restore
  • recalculate structure positions after resize/reset
  • clear stale Pixi children before rebuilding
  • add focused tests around redraw/rebuild behavior

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:

skigim

Copilot AI and others added 4 commits March 20, 2026 16:33
Co-authored-by: Skigim <217411484+Skigim@users.noreply.github.com>
Co-authored-by: Skigim <217411484+Skigim@users.noreply.github.com>
Co-authored-by: Skigim <217411484+Skigim@users.noreply.github.com>
@Skigim Skigim requested a review from a team as a code owner March 20, 2026 20:20
Copilot AI review requested due to automatic review settings March 20, 2026 20:20
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9a227e28-c575-4293-a1b2-828c20cb9181

📥 Commits

Reviewing files that changed from the base of the PR and between 0c82597 and b5d0611.

📒 Files selected for processing (6)
  • src/client/ClientGameRunner.ts
  • src/client/graphics/GameRenderer.ts
  • src/client/graphics/layers/Layer.ts
  • src/client/graphics/layers/StructureIconsLayer.ts
  • src/client/graphics/layers/UnitLayer.ts
  • tests/client/graphics/layers/StructureIconsLayer.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/client/graphics/layers/Layer.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/graphics/layers/UnitLayer.ts

Walkthrough

Reworked rendering lifecycle: added dispose hooks and stable event handlers; StructureIconsLayer now handles WebGL context lost/restored, resizes stages, recomputes screen positions, and rebuilds structure renders from game state. Minor fixes in UnitLayer and ClientGameRunner; tests updated to cover new layer behavior.

Changes

Cohort / File(s) Summary
Structure icons & lifecycle
src/client/graphics/layers/StructureIconsLayer.ts
Added stored event handlers, WebGL contextlost/contextrestored listeners, dispose() to deregister listeners, canvas/stage resize flow, full redraw()rebuildAllStructuresFromState() sequence, and helpers to clear/destroy stage children and resize stages.
Renderer lifecycle
src/client/graphics/GameRenderer.ts, src/client/graphics/layers/Layer.ts
Introduced dispose() on GameRenderer (public) and tracking of animationFrameId/disposed; added optional dispose?: () => void to Layer interface; replaced inline handlers with stable callbacks and ensured proper teardown on context loss/restoration.
Unit update minor fix
src/client/graphics/layers/UnitLayer.ts
Simplified updateUnitsSprites() to always build unitsToUpdate array and changed conditional to unitsToUpdate.length > 0 to avoid falsy-array issues.
Client stop path
src/client/ClientGameRunner.ts
Call this.renderer.dispose() immediately when stopping the client game to ensure renderer teardown during stop.
Tests
tests/client/graphics/layers/StructureIconsLayer.test.ts
Added mocks and tests verifying resize/redraw/rebuild behavior, resource cleanup of inactive renders, moveGhost on resize, and dispose() removes listeners and clears handler refs.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant WebGLContext as WebGL Context
    participant PixiCanvas
    participant StructureLayer
    participant GameState

    Browser->>WebGLContext: underlying context lost
    WebGLContext->>PixiCanvas: webglcontextlost event
    PixiCanvas->>StructureLayer: onContextLost handler
    StructureLayer->>StructureLayer: preventDefault(), cancel frame, stop draws

    Browser->>WebGLContext: context restored
    WebGLContext->>PixiCanvas: webglcontextrestored event
    PixiCanvas->>StructureLayer: onContextRestored handler
    StructureLayer->>StructureLayer: resizeCanvas() / resizeStages()
    StructureLayer->>StructureLayer: clearAllStructureRenders()
    StructureLayer->>GameState: iterate game.units()
    GameState-->>StructureLayer: active structure list
    StructureLayer->>StructureLayer: rebuildAllStructuresFromState()
    StructureLayer->>PixiCanvas: redraw structures
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Poem

🧩 When contexts fall and pixels hide their face,
Handlers stand ready to restore the place.
Stages resize, old ghosts neatly cleared—
Rebuilt from state, each structure reappeared. 🎨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Make structure icon recovery state-sourced after runtime renderer disruption' is specific and directly describes the main change—rebuilding structure icons from game state after renderer resets.
Description check ✅ Passed The description is well-related to the changeset, explaining the motivation (reducing structure icon loss after renderer reset), the main implementation approach (rebuild from game.units()), and the key technical changes made.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to make structure icon rendering more resilient to renderer resets by rebuilding structure icon state from the authoritative game.units() state (instead of relying primarily on incremental unit updates), and by doing additional recovery work after WebGL context restoration and resize/reset.

Changes:

  • Rebuild structure icons from current game state during StructureIconsLayer.redraw() and after WebGL context restore.
  • Recompute tracked structure screen locations after canvas resize/reset and deep-clear Pixi stages before full rebuilds.
  • Add targeted unit tests covering redraw + full rebuild behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
tests/client/graphics/layers/StructureIconsLayer.test.ts Adds tests asserting redraw triggers a state rebuild and rebuild removes stale/inactive renders.
src/client/graphics/layers/UnitLayer.ts Makes updateUnitsSprites robust to nullish mapping results and avoids running update passes on empty arrays.
src/client/graphics/layers/StructureIconsLayer.ts Adds state-sourced rebuild logic, WebGL context restore handling, stage resizing, and stage deep-clear helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/graphics/layers/StructureIconsLayer.ts (1)

202-212: ⚠️ Potential issue | 🟡 Minor

Reposition the active ghost on resize/reset too.

resizeCanvas() now fixes stored structure renders, but the build ghost only moves in moveGhost(). If the user is placing a structure when the window is resized or the renderer is reset, the ghost preview and range can stay at the old local coordinates until the next mouse move.

💡 Suggested change
       this.renderer.resize(innerWidth, innerHeight, 1);
       this.resizeStages();
+      if (this.ghostUnit) {
+        const rect = this.transformHandler.boundingRect();
+        if (rect) {
+          const localX = this.mousePos.x - rect.left;
+          const localY = this.mousePos.y - rect.top;
+          this.ghostUnit.container.position.set(localX, localY);
+          this.ghostUnit.range?.position.set(localX, localY);
+        }
+      }
       // Canvas size changes affect screen-space culling and icon placement, so
       // recompute every tracked structure location after a resize/reset.
       for (const render of this.rendersByUnitId.values()) {
         this.computeNewLocation(render);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/StructureIconsLayer.ts` around lines 202 - 212,
resizeCanvas currently recomputes stored renders but doesn't update the live
placement preview, so when the window/resizer resets the build preview (active
ghost) stays at stale local coords; after you finish the loop over
rendersByUnitId in resizeCanvas, if this.activeGhost is present call the same
update path used for mouse moves (e.g. invoke moveGhost with the active ghost's
current target or recompute its world/local coords via computeNewLocation and
then refresh its range/placement visuals) so the ghost and its range are
repositioned immediately on resize/reset.
🧹 Nitpick comments (2)
tests/client/graphics/layers/StructureIconsLayer.test.ts (1)

162-170: Exercise the stale-stage-child cleanup in this test.

All three removeChildren() mocks return [], so this case never runs the new clearStageChildren() destroy path. If orphan Pixi children stop being cleaned up, the suite still passes. Returning at least one mock child with a destroy spy from each stage would cover the branch this PR added.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/client/graphics/layers/StructureIconsLayer.test.ts` around lines 162 -
170, The test currently stubs layerInternals.iconsStage, levelsStage, and
dotsStage removeChildren() to always return [] so the new clearStageChildren()
destroy branch is never exercised; update each stage mock
(layerInternals.iconsStage, layerInternals.levelsStage,
layerInternals.dotsStage) so their removeChildren() returns an array containing
at least one mock child object with a destroy spy (e.g., { destroy: vi.fn() })
to ensure the destroy path is executed for each stage in
StructureIconsLayer.test.ts.
src/client/graphics/layers/StructureIconsLayer.ts (1)

174-176: Use redraw() as the single recovery path.

This listener repeats the same two steps that redraw() already owns. Calling this.redraw() here keeps the restore logic in one place, which makes future recovery changes harder to miss.

♻️ Suggested change
     this.pixicanvas.addEventListener("webglcontextrestored", () => {
-      this.resizeCanvas();
-      this.rebuildAllStructuresFromState();
+      this.redraw();
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/graphics/layers/StructureIconsLayer.ts` around lines 174 - 176,
Replace the webglcontextrestored listener body to call the single recovery path
method redraw() instead of repeating resizeCanvas() and
rebuildAllStructuresFromState(); locate the event handler attached to
this.pixicanvas for "webglcontextrestored" and invoke this.redraw() so future
restore logic stays centralized in the redraw() method.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/client/graphics/layers/StructureIconsLayer.ts`:
- Around line 202-212: resizeCanvas currently recomputes stored renders but
doesn't update the live placement preview, so when the window/resizer resets the
build preview (active ghost) stays at stale local coords; after you finish the
loop over rendersByUnitId in resizeCanvas, if this.activeGhost is present call
the same update path used for mouse moves (e.g. invoke moveGhost with the active
ghost's current target or recompute its world/local coords via
computeNewLocation and then refresh its range/placement visuals) so the ghost
and its range are repositioned immediately on resize/reset.

---

Nitpick comments:
In `@src/client/graphics/layers/StructureIconsLayer.ts`:
- Around line 174-176: Replace the webglcontextrestored listener body to call
the single recovery path method redraw() instead of repeating resizeCanvas() and
rebuildAllStructuresFromState(); locate the event handler attached to
this.pixicanvas for "webglcontextrestored" and invoke this.redraw() so future
restore logic stays centralized in the redraw() method.

In `@tests/client/graphics/layers/StructureIconsLayer.test.ts`:
- Around line 162-170: The test currently stubs layerInternals.iconsStage,
levelsStage, and dotsStage removeChildren() to always return [] so the new
clearStageChildren() destroy branch is never exercised; update each stage mock
(layerInternals.iconsStage, layerInternals.levelsStage,
layerInternals.dotsStage) so their removeChildren() returns an array containing
at least one mock child object with a destroy spy (e.g., { destroy: vi.fn() })
to ensure the destroy path is executed for each stage in
StructureIconsLayer.test.ts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 930a4e59-ecc0-4612-a049-94f76f3fe820

📥 Commits

Reviewing files that changed from the base of the PR and between 54a63ac and 0c82597.

📒 Files selected for processing (3)
  • src/client/graphics/layers/StructureIconsLayer.ts
  • src/client/graphics/layers/UnitLayer.ts
  • tests/client/graphics/layers/StructureIconsLayer.test.ts

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 20, 2026
@Skigim Skigim marked this pull request as ready for review March 20, 2026 20:41
@Skigim
Copy link
Contributor Author

Skigim commented Mar 20, 2026

mild performance tradeoff here - redraw is heavier but should be separate from hot path - tick is still incremental, full redraw should only kick in to recover from desync.

@VariableVince
Copy link
Contributor

@Skigim I don't know enough about this. But

  1. Shouldn't we check with PixiJS first if it was able to restore the context by itself or not ( PIXI.GlContextSystem(this.renderer).isLost() ), before meddling with it any further? See my comment after some testing last year:

#2147 (comment)

"I was thinking for StructureIconsLayer we could listen to event "webglcontextlost" and then after X milliseconds check new PIXI.GlContextSystem(this.renderer).isLost() if it was auto restored by pixiJS itself or not. And have a seperate function restore the icons if needed.

But i don't have actual experience with PixiJS nor WebGL. So don't know if this is a good way to go about it. Also don't know if there's a good way to test. GlContextSystem.forceContextLoss() works, but also auto-restores right away. While WEBGL_lose_context.loseContext works as a test to lose context, but it doesn't trigger pixiJS protected function handleContextLoss like it would in reality so we don't have a real-world test."

  1. Wouldn't this obstruct our view/investigation to the actual underlying causes? Meaning we're 'stuck' with a non-performarmant bandaid patch, but we can hardly ever get to the bottom with it and fix it in a better/more performant way?

@Skigim
Copy link
Contributor Author

Skigim commented Mar 20, 2026

@VariableVince Even if Pixi is recovering, that evidently doesn’t guarantee the structure icon layer comes back in the right state. This change is basically saying “the visuals may be wrong after disruption” and rebuilding from game state, rather than only acting if Pixi itself failed to restore - since reproducing that isn't something I've managed to do yet.

The bigger problem here is that I still can’t reliably reproduce the bug. There seem to be too many contributing factors, especially without knowing whether there are specific in-game triggers.

along your point though, the follow-up should be to check whether Pixi is actually failing to restore, and if so whether that’s the direct cause of the original bug. I can track that in the original issue thread so it doesn’t get lost.

Even aside from this specific bug, I think this gives the layer a more reliable recovery path. I also don’t think it obscures the issue so much as it narrows one part of the investigation.

@scamiv
Copy link
Contributor

scamiv commented Mar 21, 2026

agree with @VariableVince here, good analysis.
Also, this seems highly speculative on the benefits while having a clear negative performance impact,

@Skigim
Copy link
Contributor Author

Skigim commented Mar 21, 2026

@scamiv @VariableVince understood. I'll get back to the drawing board, and see if I can reach out to clan members to run some customs and try to force the bug out - then I can at least test Pixi's native restore path.

Any other ideas I can try to reproduce?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

5 participants