SdfMesh.rebuild() (src/engine/SdfMesh.ts:187) removes the old mesh from the scene and then allocates fresh GPU resources:
- a
ShaderMaterial (:210)
- a
BoxGeometry (:243)
- one
DataTexture per texture uniform (:197)
None of the previous ones are disposed. Three.js does not release GPU-side resources when an object leaves the scene graph — scene.remove() only unlinks it. Every structural tree edit therefore leaks one compiled shader program, one geometry, and all of that build's textures.
dispose() (:296) has the same gap: it unsubscribes and removes the mesh from the scene, but never calls this.material.dispose() or this.mesh.geometry.dispose(). Viewport.tsx:20 calls eng.dispose() on unmount, so a mount/unmount cycle compounds it — including React StrictMode's double-invoke in development.
The codebase already applies the right pattern elsewhere: OutlinePass.dispose() (:267-273) disposes correctly, and OutlinePass.update() (:280) disposes the old geometry before swapping in a new one. This is the same rule, missed in the hottest path.
Fix
Dispose the previous material, geometry, and textures at the top of rebuild(), and again in dispose(). Textures need tracking, since they live in material.uniforms rather than on the mesh — either keep a DataTexture[] field, or walk the uniforms and dispose any value that is a THREE.Texture.
Note
Worth pairing with #32 (cache compiled shaders by tree hash) — a cache changes disposal from "always free the old one" to "free only when evicted", so doing them in the wrong order means writing the disposal logic twice.
SdfMesh.rebuild()(src/engine/SdfMesh.ts:187) removes the old mesh from the scene and then allocates fresh GPU resources:ShaderMaterial(:210)BoxGeometry(:243)DataTextureper texture uniform (:197)None of the previous ones are disposed. Three.js does not release GPU-side resources when an object leaves the scene graph —
scene.remove()only unlinks it. Every structural tree edit therefore leaks one compiled shader program, one geometry, and all of that build's textures.dispose()(:296) has the same gap: it unsubscribes and removes the mesh from the scene, but never callsthis.material.dispose()orthis.mesh.geometry.dispose().Viewport.tsx:20callseng.dispose()on unmount, so a mount/unmount cycle compounds it — including React StrictMode's double-invoke in development.The codebase already applies the right pattern elsewhere:
OutlinePass.dispose()(:267-273) disposes correctly, andOutlinePass.update()(:280) disposes the old geometry before swapping in a new one. This is the same rule, missed in the hottest path.Fix
Dispose the previous material, geometry, and textures at the top of
rebuild(), and again indispose(). Textures need tracking, since they live inmaterial.uniformsrather than on the mesh — either keep aDataTexture[]field, or walk the uniforms and dispose any value that is aTHREE.Texture.Note
Worth pairing with #32 (cache compiled shaders by tree hash) — a cache changes disposal from "always free the old one" to "free only when evicted", so doing them in the wrong order means writing the disposal logic twice.