Skip to content

SparkRenderer.dispose() does not dispose its geometry — 2 WebGL buffers leak per instance (v2.1.0) #408

Description

@tyirenkyi

Summary

SparkRenderer.dispose() does not dispose the geometry of its own THREE.Mesh, so 2 WebGL buffers leak per SparkRenderer instance. Applications that mount and unmount splat scenes repeatedly (route changes, toggling a render mode, opening/closing an editor) accumulate GPU buffers for the lifetime of the WebGL context.

Confirmed by a one-line change on the caller side: adding spark.geometry.dispose() after spark.dispose() takes the growth to exactly zero.

Environment

  • @sparkjsdev/spark 2.1.0
  • three.js r180
  • Chrome (stable, real GPU via --use-gl=angle), macOS

Reproduction

Mount a SparkRenderer + SplatMesh, render one frame, tear both down with the public API, repeat. Live WebGL objects are counted by wrapping createBuffer/deleteBuffer on WebGL2RenderingContext.prototype before anything else runs, so an object created and never deleted stays counted.

for (let i = 0; i < CYCLES; i++) {
  const spark = new SparkRenderer({ renderer });
  scene.add(spark);

  const mesh = new SplatMesh({ fileBytes: bytes.slice(0), fileType });
  await mesh.initialized;
  scene.add(mesh);

  renderer.render(scene, camera);          // make sure everything is allocated + uploaded
  await new Promise(requestAnimationFrame);

  scene.remove(mesh);
  mesh.dispose();
  scene.remove(spark);
  spark.dispose();                         // <-- everything the public API offers
  // spark.geometry.dispose();             // <-- uncomment and the leak disappears
}

Result

Live WebGL buffers, sampled after a forced GC each cycle (8 cycles, 500k-Gaussian .ply):

teardown live buffers per cycle per cycle
spark.dispose() 4 → 6 → 8 → 10 → 12 → 14 → 16 → 18 +2
spark.dispose() + spark.geometry.dispose() 2 → 2 → 2 → 2 → 2 → 2 → 2 → 2 0

Live textures are flat at 7 in both, so this is specific to the geometry's buffers.

Where they are allocated

Capturing a stack at each createBuffer and reporting the ones still live shows both leaked buffers come from three.js uploading a BufferGeometry's attributes, never freed because the geometry's dispose event never fires:

buffer (x10)  createBuffer (three.module.js:71)
            ← WebGLAttributes.update (three.module.js:268)
            ← WebGLObjects.update (three.module.js:3886)

buffer  (x8)  createBuffer (three.module.js:71)
            ← WebGLAttributes.update (three.module.js:268)
            ← WebGLBindingStates.setup (three.module.js:1618)

BufferGeometry.dispose() is what makes three delete the attribute buffers and the VAO; nothing in SparkRenderer.dispose() calls it.

Suggested fix

Dispose the mesh's own geometry (and, if it is not shared, its material) in SparkRenderer.dispose():

dispose() {
  // ... existing teardown
  this.geometry.dispose();
}

Note on 0.1.x, for anyone landing here from an older version

On 0.1.10 this specific leak does not occur, because there is no SparkRenderer.dispose() and callers have to dispose spark.geometry by hand. That version instead leaks one WebGL texture per instance: SparkRenderer allocates a SplatAccumulator in its constructor whose PackedSplats owns a WebGLArrayRenderTarget, and SplatAccumulator has no dispose() at all — releaseAccumulator only decrements a refcount onto a free list. Reaching through spark.active.splats.dispose() takes it to flat. Both of those are fixed in 2.x by the new dispose() methods, so this is just context, not a request to patch 0.1.x.

Separate observation, not part of this report

With the geometry fix applied and live GL objects flat, performance.measureUserAgentSpecificMemory() still grows roughly linearly per mount/teardown cycle (single-digit MB per cycle at 500k Gaussians, scaling with splat count), entirely outside the V8 heap. I could not attribute that one confidently and it may already be covered by #237, #286 or #401 — mentioning it only so it is not assumed fixed by the change above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions