From 726e201314cf3d0e789c58df5341fe8b9c92ba61 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Tue, 3 Nov 2026 23:33:50 +0400 Subject: [PATCH] Read a parked globe's WebGL context from the one it was built with instead of asking its canvas during a switch Asking the canvas again during the switch made the reuse depend on getContext at that moment: with new contexts refused (qa-strict-failure-matrix, cold Navara to Cesium) the rollback to the parked Navara threw and left no globe. --- src/standalone/rendererCache.js | 15 +++++++++------ src/standalone/rendererCache.test.mjs | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/standalone/rendererCache.js b/src/standalone/rendererCache.js index 9347ab7a3..532220752 100644 --- a/src/standalone/rendererCache.js +++ b/src/standalone/rendererCache.js @@ -9,11 +9,8 @@ export function createRendererCache() { let entry = idle.get(kind); idle.delete(kind); // loseContext() takes effect before its queued DOM event is delivered. - // Check the existing context too, before resuming a parked render loop. - const canvas = entry?.scene.engine.host.canvas; - const context = - canvas?.getContext?.('webgl2') ?? canvas?.getContext?.('webgl'); - if (entry && (entry.lost || context?.isContextLost())) { + // Check the context itself too, before resuming a parked render loop. + if (entry && (entry.lost || entry.context?.isContextLost?.())) { entry.dispose(); entry = null; } @@ -39,7 +36,13 @@ export function createRendererCache() { container, defer: (fn) => cleanups.push(fn), }); - entry = { container, scene, dispose, lost: false }; + // The context the globe draws with, read once while it is new: a + // switch reads its state and never asks the canvas again (a canvas + // asked for another context type would create one). + const canvas = scene.engine.host.canvas; + const context = + canvas.getContext?.('webgl2') ?? canvas.getContext?.('webgl') ?? null; + entry = { container, scene, dispose, lost: false, context }; scene.engine.host.canvas.addEventListener( 'webglcontextlost', () => { diff --git a/src/standalone/rendererCache.test.mjs b/src/standalone/rendererCache.test.mjs index 4b4b2cddc..2810f2aaf 100644 --- a/src/standalone/rendererCache.test.mjs +++ b/src/standalone/rendererCache.test.mjs @@ -27,7 +27,11 @@ test('a parked renderer is reused, but a lost context is disposed before rebuild defer(() => { released += 1; }); + const canvas = new EventTarget(); + const context = { lost: false, isContextLost: () => context.lost }; + canvas.getContext = (type) => (type === 'webgl2' ? context : null); return { + context, suspend() { this.running = false; }, @@ -35,7 +39,7 @@ test('a parked renderer is reused, but a lost context is disposed before rebuild this.running = true; }, engine: { - host: { container, canvas: new EventTarget() }, + host: { container, canvas }, render: { request() {} }, }, }; @@ -49,13 +53,23 @@ test('a parked renderer is reused, but a lost context is disposed before rebuild release(); assert.equal(first.running, false); assert.equal(first.engine.host.container.isConnected, false); + // A parked renderer whose canvas no longer hands out contexts is still + // reused when its own context is alive. + first.engine.host.canvas.getContext = () => { + throw new Error('getContext called during a switch'); + }; assert.equal(await open(), first); assert.equal(first.running, true); assert.equal(first.engine.host.container.isConnected, true); cache.retain('navara'); release(); // The WebGL context is already lost, but its DOM event has not fired. - first.engine.host.canvas.getContext = () => ({ isContextLost: () => true }); + // The switch reads the context the globe was built with and never asks + // the canvas again (a QA or driver failure of getContext must not decide). + first.context.lost = true; + first.engine.host.canvas.getContext = () => { + throw new Error('getContext called during a switch'); + }; const second = await open(); assert.notEqual(second, first); assert.equal(released, 1);