pagescene: release detached contexts and stop the viewport observer on unmount
Measured in the running webapp (live WebGL objects counted per context over add/delete, New Scene and navigate-away/back cycles), the scene itself is now freed, but every visit to /edit-3d still added two live WebGL contexts with the post-processing render targets and programs inside them. renderer.dispose() does not delete those GL objects, and leaving the page detaches the canvases, so the contexts lingered until garbage collection. Unmount now forces the context loss for a renderer whose canvas has left the DOM. A scene change keeps the canvases mounted and the next Editor reuses their contexts, so those are kept. ViewportController's ResizeObserver was never disconnected: after unmount it kept calling renderScene on the dead editor whenever the container resized, keeping the whole Editor reachable and, once the context was released, throwing. It is now disconnected on unmount.
This commit is contained in:
1 parent
63b2a71c28
commit
802dbd5637
3 files changed
+61
-3
No files matched your search
@@ -1127,13 +1127,12 @@ class Editor {
|
||||
|
||||
this.bus.emit(new SceneLoadedEvent(false));
|
||||
this.stopRenderLoop();
|
||||
this.viewport.dispose();
|
||||
|
||||
// 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.
|
||||
// pointer listeners from the canvas.
|
||||
this.entranceAnimator.clear();
|
||||
this.gizmo.control?.dispose();
|
||||
this.activeScene.dispose();
|
||||
@@ -1142,6 +1141,13 @@ class Editor {
|
||||
this.renderer?.dispose();
|
||||
this.postProcessing.dispose();
|
||||
this.rawRenderer?.dispose();
|
||||
// dispose() leaves the GL objects themselves (render targets, programs)
|
||||
// in the context. Leaving the page detaches the canvases, and the
|
||||
// context then lingers until garbage collection, two per visit. A
|
||||
// scene change keeps the canvases mounted and the next Editor reuses
|
||||
// their contexts, so only release a context whose canvas is gone.
|
||||
releaseContextIfDetached(this.renderer);
|
||||
releaseContextIfDetached(this.rawRenderer);
|
||||
|
||||
this.isMounted = false;
|
||||
this.bus.emit(new EngineInitializedEvent(false));
|
||||
@@ -1155,4 +1161,10 @@ class Editor {
|
||||
}
|
||||
}
|
||||
|
||||
function releaseContextIfDetached(renderer: THREE.WebGLRenderer | undefined) {
|
||||
if (renderer && !renderer.domElement.isConnected) {
|
||||
renderer.forceContextLoss();
|
||||
}
|
||||
}
|
||||
|
||||
export default Editor;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ViewportController } from "./ViewportController";
|
||||
|
||||
describe("ViewportController", () => {
|
||||
it("stops observing the container on dispose", () => {
|
||||
const observers = stubResizeObserver();
|
||||
const renderScene = vi.fn();
|
||||
const viewport = new ViewportController({
|
||||
getCamera: () => null,
|
||||
getRenderCamera: () => null,
|
||||
getRenderer: () => ({ setSize: vi.fn(), setPixelRatio: vi.fn() }) as never,
|
||||
getRenderAspectRatio: () => 1,
|
||||
resizePostProcessing: vi.fn(),
|
||||
renderScene,
|
||||
});
|
||||
viewport.container = document.createElement("div");
|
||||
|
||||
viewport.setupResizeObserver();
|
||||
viewport.dispose();
|
||||
|
||||
expect(observers[0].disconnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
// jsdom has no ResizeObserver; record each one the controller creates.
|
||||
function stubResizeObserver() {
|
||||
const observers: Array<{ disconnect: ReturnType<typeof vi.fn> }> = [];
|
||||
vi.stubGlobal(
|
||||
"ResizeObserver",
|
||||
class {
|
||||
disconnect = vi.fn();
|
||||
constructor() {
|
||||
observers.push(this);
|
||||
}
|
||||
observe() {}
|
||||
},
|
||||
);
|
||||
return observers;
|
||||
}
|
||||
@@ -100,4 +100,12 @@ export class ViewportController {
|
||||
});
|
||||
this.observer.observe(this.container);
|
||||
}
|
||||
|
||||
// Stop observing on unmount. The container can outlive this editor (a
|
||||
// scene change keeps it mounted), and a live observer would keep
|
||||
// re-rendering the dead editor's scene on every resize.
|
||||
dispose() {
|
||||
this.observer?.disconnect();
|
||||
this.observer = undefined;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user