From 69d5f3366525ec659cede6227ff33117f6e23035 Mon Sep 17 00:00:00 2001 From: Aleksey Orekhovsky Date: Fri, 1 Aug 2025 11:47:07 +0700 Subject: [PATCH] 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 --- src/client/graphics/GameRenderer.ts | 26 +++++++++++++------- src/client/graphics/layers/StructureLayer.ts | 9 ++++++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index e9842cab6..442d4d0ea 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -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 diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index a225d67cc..ec23b6209 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -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) {