From 12cd73bed9a817333c3499e67e18a4c3f59c0645 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Tue, 3 Nov 2026 14:47:35 +0400 Subject: [PATCH] Skip CCTV scene work when its renderer was released during a health refresh --- src/layers/cctv/ingestion.js | 14 +++++++++----- src/layers/cctv/ingestion.test.mjs | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/layers/cctv/ingestion.js b/src/layers/cctv/ingestion.js index 77d2c75e1..f80469769 100644 --- a/src/layers/cctv/ingestion.js +++ b/src/layers/cctv/ingestion.js @@ -43,12 +43,16 @@ export function createIngestion({ } } await parts.health.syncHealthState(); - // New health can change which camera wins a crowded cell. - if (layerState._enabled && parts.rendering.refreshHorizonCulling()) { - parts.geometryQueue.enqueueViewGeometry(); - parts.cards.refreshAmbientCards(); + // An engine switch can release the renderer while health loads. The + // scene work waits for the next renderer; health itself is kept. + if (parts.renderer) { + // New health can change which camera wins a crowded cell. + if (layerState._enabled && parts.rendering.refreshHorizonCulling()) { + parts.geometryQueue.enqueueViewGeometry(); + parts.cards.refreshAmbientCards(); + } + parts.navigation.maybeAutoHop(now); } - parts.navigation.maybeAutoHop(now); parts.presentation.notifyListeners(); // Health and frame URL notifications update the panel directly. Media // requests its own frames; only actual scene changes need a tick frame. diff --git a/src/layers/cctv/ingestion.test.mjs b/src/layers/cctv/ingestion.test.mjs index dcb91792b..0fc0d72d7 100644 --- a/src/layers/cctv/ingestion.test.mjs +++ b/src/layers/cctv/ingestion.test.mjs @@ -12,6 +12,7 @@ function setup() { }; const calls = { health: 0, notify: 0, queues: [] }; const parts = { + renderer: {}, rendering: { refreshHorizonCulling: () => false }, model: { projectionTilesReady: () => true }, ground: { isGroundResolved: () => false }, @@ -76,3 +77,26 @@ test('disabled CCTV does no polling or presentation work', async () => { await h.layer.update(); assert.equal(h.calls.health + h.calls.notify + h.calls.queues.length, 0); }); + +test('a tick resuming after an engine switch released the renderer does no scene work', async () => { + const h = setup(); + let release; + h.parts.health.syncHealthState = () => + new Promise((resolve) => { + release = resolve; + }); + let sceneWork = 0; + h.parts.rendering.refreshHorizonCulling = () => { + sceneWork++; + return true; + }; + h.parts.navigation.maybeAutoHop = () => { + sceneWork++; + }; + const tick = h.layer.update(); + h.parts.renderer = null; + release(); + assert.deepEqual(await tick, { changed: false }); + assert.equal(sceneWork, 0); + assert.equal(h.calls.notify, 1); +});