From 4bd7f97cc4049befc11e96ca5064570fbd14435c Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Mon, 2 Nov 2026 22:42:40 +0400 Subject: [PATCH] Isolate globe renderer suspension from the switch cache --- scripts/package-boundaries.json | 3 +++ src/app/navaraMode.js | 10 ++++++++- src/app/scene.js | 16 +++++++++++++- src/standalone/rendererCache.js | 30 ++++++++++++--------------- src/standalone/rendererCache.test.mjs | 29 ++++++++++++++++++++------ 5 files changed, 63 insertions(+), 25 deletions(-) diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 81ce4d4df..d17f7229f 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -833,6 +833,7 @@ "src/ui/cockpitCamera.js", "src/ui/cockpitContext.js", "src/ui/cockpitController.js", + "src/ui/cockpitHost.js", "src/ui/cockpitDisplayPortal.js", "src/ui/displayPanelScroll.js", "src/ui/cockpitInput.js", @@ -975,6 +976,7 @@ "src/ui/cockpitCamera.js", "src/ui/cockpitContext.js", "src/ui/cockpitController.js", + "src/ui/cockpitHost.js", "src/ui/cockpitDisplayPortal.js", "src/ui/displayPanelScroll.js", "src/ui/cockpitInput.js", @@ -3085,6 +3087,7 @@ "src/ui/cockpitCamera.js", "src/ui/cockpitContext.js", "src/ui/cockpitController.js", + "src/ui/cockpitHost.js", "src/ui/cockpitDisplayPortal.js", "src/ui/displayPanelScroll.js", "src/ui/cockpitInput.js", diff --git a/src/app/navaraMode.js b/src/app/navaraMode.js index 317338ea8..921a27c40 100644 --- a/src/app/navaraMode.js +++ b/src/app/navaraMode.js @@ -242,7 +242,15 @@ export async function createNavaraScene({ viewer: null, engine, tileset: null, - setRenderLoop: globe.setRenderLoop, + suspend() { + uninstallRenderGovernor(engine); + engine.camera.cancelFlight(); + engine.render.setContinuous(false); + globe.setRenderLoop(false); + }, + resume() { + globe.setRenderLoop(true); + }, mapStackController, }; } diff --git a/src/app/scene.js b/src/app/scene.js index 84965b40d..be99f90c1 100644 --- a/src/app/scene.js +++ b/src/app/scene.js @@ -181,5 +181,19 @@ async function createRenderer({ }); signal.throwIfAborted(); - return { viewer, engine, tileset, mapStackController }; + return { + viewer, + engine, + tileset, + mapStackController, + suspend() { + uninstallRenderGovernor(viewer); + engine.camera.cancelFlight(); + releaseCameraVerbs(viewer); + viewer.useDefaultRenderLoop = false; + }, + resume() { + viewer.useDefaultRenderLoop = !document.hidden; + }, + }; } diff --git a/src/standalone/rendererCache.js b/src/standalone/rendererCache.js index c739158f2..4b4bb8c9f 100644 --- a/src/standalone/rendererCache.js +++ b/src/standalone/rendererCache.js @@ -1,6 +1,3 @@ -import { releaseCameraVerbs } from '../cameraVerbs.js'; -import { uninstallRenderGovernor } from '../renderGovernor.js'; - // Only the renderer and its map survive a switch. The outgoing UI, data feeds, // tools and their subscriptions are still disposed with their application. export function createRendererCache() { @@ -50,25 +47,24 @@ export function createRendererCache() { throw error; } } else { - host.prepend(entry.container); - if (entry.scene.viewer) - entry.scene.viewer.useDefaultRenderLoop = !document.hidden; - else entry.scene.setRenderLoop(true); - entry.scene.engine.render.request('engine-resume'); + try { + host.prepend(entry.container); + entry.scene.resume(); + entry.scene.engine.render.request('engine-resume'); + } catch (error) { + entry.dispose(); + throw error; + } } - const { viewer, engine } = entry.scene; defer(() => { const keep = retainKind === kind && !entry.lost; retainKind = null; if (!keep) return entry.dispose(); - uninstallRenderGovernor(viewer ?? engine); - engine.camera.cancelFlight(); - if (viewer) { - releaseCameraVerbs(viewer); - viewer.useDefaultRenderLoop = false; - } else { - engine.render.setContinuous(false); - entry.scene.setRenderLoop(false); + try { + entry.scene.suspend(); + } catch (error) { + entry.dispose(); + throw error; } entry.container.remove(); idle.set(kind, entry); diff --git a/src/standalone/rendererCache.test.mjs b/src/standalone/rendererCache.test.mjs index efe1c51ed..aca7cc207 100644 --- a/src/standalone/rendererCache.test.mjs +++ b/src/standalone/rendererCache.test.mjs @@ -28,13 +28,15 @@ test('a parked renderer is reused, but a lost context is disposed before rebuild released += 1; }); return { - setRenderLoop(enabled) { - this.running = enabled; + suspend() { + this.running = false; + }, + resume() { + this.running = true; }, engine: { host: { container, canvas: new EventTarget() }, - camera: { cancelFlight() {} }, - render: { request() {}, setContinuous() {} }, + render: { request() {} }, }, }; }; @@ -53,9 +55,24 @@ test('a parked renderer is reused, but a lost context is disposed before rebuild cache.retain('navara'); release(); first.engine.host.canvas.dispatchEvent(new Event('webglcontextlost')); - assert.notEqual(await open(), first); + const second = await open(); + assert.notEqual(second, first); assert.equal(released, 1); + cache.retain('navara'); release(); - cache.clear(); + second.resume = () => { + throw new Error('resume failed'); + }; + await assert.rejects(open(), /resume failed/); + assert.equal(second.engine.host.container.isConnected, false); assert.equal(released, 2); + const third = await open(); + third.suspend = () => { + throw new Error('suspend failed'); + }; + cache.retain('navara'); + assert.throws(release, /suspend failed/); + assert.equal(third.engine.host.container.isConnected, false); + cache.clear(); + assert.equal(released, 3); });