From ebc717df8bb18c5e92a11f732168ddad9c569c4d Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Wed, 4 Nov 2026 00:07:15 +0400 Subject: [PATCH] Dispose a Navara layer effect's pass when the layer detaches so engine switches stop retaining its material and textures Layers re-create their ground effects on every attach. Navara's removePass leaves the EffectPass undisposed, so each switch kept its EffectMaterial in three's program and material caches, together with the uniforms' DataTextures and their Float32Array data (earthquakes: about 47 KB of buffers and one WebGL program per switch). engine.LayerEffectDesc now disposes the raw pass on delete, as the app's own effects do. --- src/globe/navara/frameDescriptor.js | 9 +++++ src/globe/navara/frameDescriptor.test.mjs | 49 ++++++++++++++++++++++- src/globe/navara/index.js | 4 +- 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/globe/navara/frameDescriptor.js b/src/globe/navara/frameDescriptor.js index 771a6bfc2..ca5bed500 100644 --- a/src/globe/navara/frameDescriptor.js +++ b/src/globe/navara/frameDescriptor.js @@ -33,3 +33,12 @@ export function withPassDisposal(Base) { } }; } + +// The base the layers' Navara effects extend (engine.LayerEffectDesc): per +// frame updates on their own callback, and the raw pass disposed when the +// effect is deleted. A layer re-creates its effect on every attach (an +// engine switch), so an undisposed pass kept its material, its shader +// program and the uniforms' data textures alive for the page's lifetime. +export function layerEffectDesc(EffectDesc) { + return withPassDisposal(withFrameUpdates(EffectDesc)); +} diff --git a/src/globe/navara/frameDescriptor.test.mjs b/src/globe/navara/frameDescriptor.test.mjs index 32510815c..3d1f14e63 100644 --- a/src/globe/navara/frameDescriptor.test.mjs +++ b/src/globe/navara/frameDescriptor.test.mjs @@ -1,6 +1,10 @@ import assert from 'node:assert/strict'; import test from 'node:test'; -import { withFrameUpdates, withPassDisposal } from './frameDescriptor.js'; +import { + layerEffectDesc, + withFrameUpdates, + withPassDisposal, +} from './frameDescriptor.js'; test('deleting a descriptor unsubscribes only that renderer from the warm view', () => { const frames = new Set(); @@ -77,3 +81,46 @@ test('a deleted effect disposes the pass the composer no longer holds', () => { assert.throws(() => new (withPassDisposal(Failing))().onDestroy(), /removal/); assert.deepEqual(events, ['dispose']); }); + +test('a layer effect re-created on every attach leaves no pass, material or frame callback behind', () => { + const frames = new Set(); + const view = { + on: (name, cb) => frames.add(cb), + off: (name, cb) => frames.delete(cb), + }; + const live = new Set(); + class EffectDesc { + view = view; + onCreate() { + const pass = { + material: {}, + dispose: () => live.delete(pass), + }; + live.add(pass); + this._instance = { rawPass: pass }; + } + get raw() { + return this._instance; + } + onDestroy() { + // Navara's removePass: out of the composer, not disposed. + this._instance = undefined; + } + } + // As the earthquakes ground effect: disposes its own effect, then super. + class LayerEffect extends layerEffectDesc(EffectDesc) { + updateFrame() {} + onDestroy() { + this.effectDisposed = true; + super.onDestroy(); + } + } + for (let attach = 0; attach < 50; attach++) { + const effect = new LayerEffect(); + effect.onCreate(); + effect.onDestroy(); + assert.equal(effect.effectDisposed, true); + } + assert.equal(live.size, 0, 'every pass of a deleted effect is disposed'); + assert.equal(frames.size, 0, 'no frame callback outlives its effect'); +}); diff --git a/src/globe/navara/index.js b/src/globe/navara/index.js index 54894c2a2..c6ff0d621 100644 --- a/src/globe/navara/index.js +++ b/src/globe/navara/index.js @@ -2,7 +2,7 @@ // the product way (viewOptions.js) and the GlobeEngine on it. Load it with a // dynamic import() so Navara stays out of the Cesium-mode bundle. -import { withFrameUpdates } from './frameDescriptor.js'; +import { layerEffectDesc, withFrameUpdates } from './frameDescriptor.js'; import ThreeView, { Color as NavaraColor, MeshDesc, @@ -241,7 +241,7 @@ export async function createNavaraGlobe({ NavaraColor, MeshDesc, EffectDesc, - LayerEffectDesc: withFrameUpdates(EffectDesc), + LayerEffectDesc: layerEffectDesc(EffectDesc), NavaraEffect, DrapedMesh, setupMaterialForDrape,