From 4402f4f668f3824060c9ce5b5c9f16c597d0a7da Mon Sep 17 00:00:00 2001 From: Tanish Manku Date: Tue, 6 Oct 2026 15:33:29 -0700 Subject: [PATCH] pagescene: free the scene when the editor unmounts unmountEngine disposed the renderers but none of the scene. Every mount builds a new Editor, and on a scene change it builds it on the same canvases, which hands it the same WebGL context, so the old scene's buffers and textures stayed allocated in a live context. In the webapp this happened on every scene-to-scene navigation. Unmount now finishes running entrances, disposes the gizmo (which also removes its pointer listeners from the reused canvas) and frees the scene's contents and background. This is safe because EngineProvider serializes the scene before calling unmountEngine and nothing reuses an Editor after it. The old pause/resume use of unmount/remount, which kept the scene alive across tab switches, went away in #654. forceContextLoss is deliberately not called: the next Editor may be created on these canvases and would start on a lost context. --- .../pagescene/src/lib/engine/editor.ts | 10 ++++++++++ .../pagescene/src/lib/engine/scene.spec.ts | 17 +++++++++++++++++ .../pagescene/src/lib/engine/scene.ts | 9 +++++++++ 3 files changed, 36 insertions(+) diff --git a/frontend/libs/components/pagescene/src/lib/engine/editor.ts b/frontend/libs/components/pagescene/src/lib/engine/editor.ts index 96af58ca37..0d1c9158e8 100644 --- a/frontend/libs/components/pagescene/src/lib/engine/editor.ts +++ b/frontend/libs/components/pagescene/src/lib/engine/editor.ts @@ -1128,6 +1128,16 @@ class Editor { this.bus.emit(new SceneLoadedEvent(false)); this.stopRenderLoop(); + // Free the scene's GPU resources. EngineProvider serialized the scene + // before calling us, and the next mount builds a fresh Editor, so + // nothing reads this scene again. The gizmo's dispose also removes its + // pointer listeners from the canvas. No forceContextLoss: a scene + // change mounts the next Editor on these same canvases, which hands it + // this same WebGL context. + this.entranceAnimator.clear(); + this.gizmo.control?.dispose(); + this.activeScene.dispose(); + // Fix: dispose 3D contexts this.renderer?.dispose(); this.postProcessing.dispose(); diff --git a/frontend/libs/components/pagescene/src/lib/engine/scene.spec.ts b/frontend/libs/components/pagescene/src/lib/engine/scene.spec.ts index ddcc506a75..32d8f52299 100644 --- a/frontend/libs/components/pagescene/src/lib/engine/scene.spec.ts +++ b/frontend/libs/components/pagescene/src/lib/engine/scene.spec.ts @@ -110,6 +110,23 @@ describe("Scene disposal", () => { }); }); + it("dispose frees the contents and the background", () => { + const scene = makeScene(); + const geometry = new THREE.BoxGeometry(); + const background = new THREE.CubeTexture(); + const disposed = new Set(); + geometry.addEventListener("dispose", () => disposed.add(geometry)); + background.addEventListener("dispose", () => disposed.add(background)); + scene.scene.add(new THREE.Mesh(geometry)); + scene.scene.background = background; + + scene.dispose(); + + expect(disposed).toEqual(new Set([geometry, background])); + expect(scene.scene.children).toHaveLength(0); + expect(scene.scene.background).toBeNull(); + }); + it("disposeBackground frees a texture background", () => { const scene = makeScene(); const background = new THREE.CubeTexture(); diff --git a/frontend/libs/components/pagescene/src/lib/engine/scene.ts b/frontend/libs/components/pagescene/src/lib/engine/scene.ts index 248ef1d50a..b67470cedb 100644 --- a/frontend/libs/components/pagescene/src/lib/engine/scene.ts +++ b/frontend/libs/components/pagescene/src/lib/engine/scene.ts @@ -178,6 +178,15 @@ class Scene { disposeObject3D(root); } + // Final teardown when the owning Editor unmounts: free the contents and + // the background. The Scene is not used again — every mount builds a new + // Editor, and EngineProvider serializes the scene before unmounting. + dispose() { + this.disposeContents(); + this.disposeBackground(); + this.scene.background = null; + } + disposeBackground() { if (this.scene.background instanceof THREE.Texture) { this.scene.background.dispose();