Skip to content

Attributes: Support explicit disposal & invalidate bind groups on rebind#33468

Draft
thelazylamaGit wants to merge 6 commits into
mrdoob:devfrom
thelazylamaGit:StorageBufferAtribute-Dispose
Draft

Attributes: Support explicit disposal & invalidate bind groups on rebind#33468
thelazylamaGit wants to merge 6 commits into
mrdoob:devfrom
thelazylamaGit:StorageBufferAtribute-Dispose

Conversation

@thelazylamaGit
Copy link
Copy Markdown
Contributor

Related issue: #32969

Description

Adds dispose event listener to bufferAttributes so that they are able to be explicitly disposed rather than only disposed alongside geometry. This brings their behaviour more in line with other GPU resources such as textures.

Additionally, #32847 made it so that StorageBufferAttributes could be changed between compute calls, however, WebGPU would continue to use the old cached GPUBindGroup causing errors such as [Buffer] used in submit while destroyed. This PR makes it so that when bufferAttributes are reassigned, their bind group becomes invalidated.

Overall, these changes allow for dynamic resizing & reassignment of storageBufferAttributes. Fixing the memory leak that can be seen with the example below

  function resizeStorageBuffer(newCount: number) {
    const oldPosAttr = positionArray.value
    const oldHeadAttr = headingArray.value
    const oldCount = oldPosAttr.count

    const newPosAttr = new StorageInstancedBufferAttribute(newCount, 2)
    const newHeadAttr = new StorageInstancedBufferAttribute(newCount, 2)

    const copyCount = Math.min(oldCount, newCount)
    const copySize = copyCount * oldPosAttr.itemSize * oldPosAttr.array.BYTES_PER_ELEMENT

    renderer.copyBufferToBuffer(oldPosAttr, newPosAttr, copySize)
    renderer.copyBufferToBuffer(oldHeadAttr, newHeadAttr, copySize)

    positionArray.value = newPosAttr
    headingArray.value = newHeadAttr

    oldPosAttr.dispose()
    oldHeadAttr.dispose()

    if (newCount > oldCount) {
      seedFrom.value = oldCount
      seedTo.value = newCount

      renderer.compute(seedKernel) // seeds only [oldCount, newCount)
    }
  }

Before (note storageAttribute count):

storageBufferMemLeak.mp4

After:

fixedstorageBufferMemLeakFixed.mp4

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 25, 2026

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 365.32
86.79
365.32
86.79
+0 B
+0 B
WebGPU 639.42
177.5
639.63
177.53
+207 B
+38 B
WebGPU Nodes 637.54
177.21
637.74
177.24
+207 B
+37 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 497.84
121.46
497.84
121.46
+0 B
+0 B
WebGPU 711.27
192.44
711.48
192.48
+207 B
+42 B
WebGPU Nodes 660.49
179.68
660.7
179.73
+207 B
+48 B

@thelazylamaGit thelazylamaGit changed the title Attributes: Support explicit disposal & bind group invalidation on rebind Attributes: Support explicit disposal & invalidate bind groups on rebind Apr 25, 2026
@Mugen87
Copy link
Copy Markdown
Collaborator

Mugen87 commented Apr 25, 2026

This change potentially breaks VAO setups which is why we have never supported per attribute disposal. Please study the past work on this topic, starting from #15261 and related PRs and issues.

@Mugen87 Mugen87 marked this pull request as draft April 25, 2026 08:29
@thelazylamaGit
Copy link
Copy Markdown
Contributor Author

@Mugen87 I've read through the past writing on this topic and now see the problem. Allowing disposal on all attributes & backends safely would require some kind of reverse dependency tracking similar to how Textures.js currently handles it. This would mean broader architectural changes that I myself am not confident enough to implement. However, given the main issue here is the reassignment of StorageBufferAttributes used alongside compute shaders, would it be sufficient for now, to only allow attribute disposal for the webGPUBackend and specifically StorageBufferAttributes?
e.g.

// Standalone disposal is currently limited to WebGPU storage/indirect attributes.
// Other attribute types would require additional render-state invalidation before this is safe.

if ( this.backend.isWebGPUBackend === true && ( type === AttributeType.STORAGE || type === AttributeType.INDIRECT ) ) {

	data.onDispose = () => this.delete( attribute );
	attribute.addEventListener( 'dispose', data.onDispose );

}

This would still fix the primary issue mentioned in #32969 as well as my own issues without requiring any wider changes or potential webGL regressions.

@gkjohnson
Copy link
Copy Markdown
Collaborator

@Mugen87 I understand the issue about VAO invalidation but with the new flexibility that WebGPU brings I think this needs to be resolved. As far as I understand it's possible to use a "StorageBufferAttribute" as a geometry buffer attribute so if we're going to allow for StorageBufferAttributes to be disposed then geometries need to be resilient to these disposal events.

I don't necessarily think this needs to be fixed in WebGLRenderer but at a high level it seems like we could invalidate and dispose the geometry VAO (or the WebGPU equivalent) when one of these events fires. This wouldn't mean disposing of the whole geometry but before rendering the geometry, the renderer would check whether the VAO is present and recreate it and any attributes if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants