Handle canvas context loss and restoration by redrawing (#1667)

## Description:
This PR introduces handling for the canvas `contextlost` and
`contextrestored` events to make the game's rendering more robust.

Previously, if the graphics context was lost (which can happen due to
browser memory management, GPU driver resets, etc.), the render loop
would continue to run, but most drawing operations would silently fail.
This resulted in a broken visual state where terrain, structures, and
other graphics would disappear, leading to what players have referred to
as the "black screen bug".

These changes implement the following:
1.  In `GameRenderer`, event listeners are added to the main canvas.
2. On `contextlost`, the `requestAnimationFrame` loop is cancelled,
pausing rendering.
3. On `contextrestored`, a full redraw of all layers is triggered, and
the render loop is restarted, allowing the game to gracefully recover.

Additionally, a related bug in the `StructureLayer` is fixed. During a
redraw/restoration, the layer could attempt to render unit icons to its
canvas before the images were fully (re)decoded. The `init` method now
explicitly waits for all icon images to be decoded before drawing them,
ensuring the layer is restored correctly.

**Important Note:** This PR represents a partial fix for the context
loss issue. Specifically, the `StructureIconsLayer` remains in a broken
state after context restoration. This layer is rendered using Pixi.js,
which has its own specific process for handling renderer recovery. Due
to my lack of experience with the Pixi.js API, I was unable to implement
the fix for this layer. This would be an excellent follow-up
contribution for someone familiar with Pixi.

## 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
- [X] I have read and accepted the CLA agreement (only required once).

## Please put your Discord username so you can be contacted if a bug or
regression is found:

aaa4xu
This commit is contained in:
Aleksey Orekhovsky
2025-08-01 11:47:07 +07:00
committed by GitHub
parent 3cf761e9c0
commit 69d5f33665
2 changed files with 25 additions and 10 deletions
+17 -9
View File
@@ -301,14 +301,7 @@ export class GameRenderer {
}
initialize() {
this.eventBus.on(RedrawGraphicsEvent, (e) => {
this.layers.forEach((l) => {
if (l.redraw) {
l.redraw();
}
});
});
this.eventBus.on(RedrawGraphicsEvent, () => this.redraw());
this.layers.forEach((l) => l.init?.());
document.body.appendChild(this.canvas);
@@ -318,7 +311,14 @@ export class GameRenderer {
//show whole map on startup
this.transformHandler.centerAll(0.9);
requestAnimationFrame(() => this.renderGame());
let rafId = requestAnimationFrame(() => this.renderGame());
this.canvas.addEventListener("contextlost", () => {
cancelAnimationFrame(rafId);
});
this.canvas.addEventListener("contextrestored", () => {
this.redraw();
rafId = requestAnimationFrame(() => this.renderGame());
});
}
resizeCanvas() {
@@ -328,6 +328,14 @@ export class GameRenderer {
//this.redraw()
}
redraw() {
this.layers.forEach((l) => {
if (l.redraw) {
l.redraw();
}
});
}
renderGame() {
const start = performance.now();
// Set background
+8 -1
View File
@@ -135,7 +135,14 @@ export class StructureLayer implements Layer {
this.canvas.width = this.game.width() * 2;
this.canvas.height = this.game.height() * 2;
this.game.units().forEach((u) => this.handleUnitRendering(u));
Promise.all(
Array.from(this.unitIcons.values()).map((img) =>
img.decode?.().catch(() => {}),
),
).finally(() => {
this.game.units().forEach((u) => this.handleUnitRendering(u));
});
}
renderLayer(context: CanvasRenderingContext2D) {