Potential memory leak in `WebGPUCacheBindGroups`?

Sorry to bother again, though I think I found another WebGPU memory leak

Thanks to fixes by Evgeni, I was able to get my WebGPU project in a state with no memory leaks. Now I’m enabling game features one by one to see if any of them introduce leaks. Indeed, after enabling only bullet trails (using GreasedLines), it seems that a new leak was found.

I’m creating and disposing several GreasedLines a second, and they are all sharing the same StandardMaterial. The code looks something like:

// Base `GreasedLine` only created once
const baseLine = BABYLON.CreateGreasedLine(/* options */);
const baseMaterial = baseLine.material; // `StandardMaterial` by default

// For every fired bullet, create a new `GreasedLine` and use the shared material:
const lineInstance = new GreasedLineMesh('', scene, { points: [/* points */] });
// By default, `lineInstance.material === null`, so the leak is not from generating new materials.
lineInstance.material = baseMaterial; // share material

// After a few seconds, every bullet's `GreasedLine` is disposed:
lineInstance.dispose();

In Chrome DevTools, I took a Heap snapshot, waited 2 minutes, then collected a 2nd Heap snapshot, which grew by ~7 MB. When comparing both snapshots, by far the most common object generated (out of ~26,500) that wasn’t being garbage collected was in WebGPUCacheBindGroups.

I also tested this on WebGL2, and there seemed to be no leak :white_check_mark:, so this only seems to affect WebGPU.

Please see a video here demonstrating the leak on WebGPU:

Link to project using WebGPU (has leak) :cross_mark:: Document

Link to project using WebGL (no leak) :white_check_mark:: Document


Claude

I asked Claude:

@workspace Is there a leak with WebGPUBindGroupCacheNode? I’m creating a lot of GreasedLines (they all share the same StandardMaterial). However, Chrome DevTools is showing a memory leak of a few MB per minute, and it’s indicating that WebGPUBindGroupCacheNode could be the culprit.

Response:

Looking at webgpuCacheBindGroups.ts, yes — there is a real leak risk with WebGPUBindGroupCacheNode.

Why it leaks with many GreasedLine meshes:

The cache is a trie keyed on:

  1. Buffer uniqueIds (from drawContext.buffers)

  2. Sampler hash codes

  3. Texture uniqueIds

// New node created for every unique buffer/texture id combination
let nextNode = new WebGPUBindGroupCacheNode();
node.values[uboId] = nextNode;

_Cache is a static, global trie that grows indefinitely:

private static _Cache: WebGPUBindGroupCacheNode = new WebGPUBindGroupCacheNode();

Each GreasedLineMesh has its own WebGPUDrawContext with unique buffer IDs (for grl_offsets, grl_widths, grl_colorPointers, etc.). When you dispose a mesh, its draw context is released, but the trie nodes keyed on those buffer IDs are never pruned — they remain in _Cache forever with dangling GPUBindGroup[] references.

Why it’s worse for GreasedLine specifically:

Each [GreasedLineMesh](packages/dev/core/src/Meshes/GreasedLine/greasedLineMesh.ts) registers several custom vertex buffers grl_offsets, grl_widths, grl_colorPointers, grl_previousAndSide, grl_nextAndCounters) — all with unique IDs — so each mesh adds significantly more trie branches than a standard mesh.

Workarounds available to you:

1. Periodically reset the cache (most aggressive, causes a one-frame rebuild):

import { WebGPUCacheBindGroups } from "@babylonjs/core/Engines/WebGPU/webgpuCacheBindGroups";

// Call after batches of dispose() calls
WebGPUCacheBindGroups.ResetCache();

2. Disable the bind group cache entirely (simpler but slightly slower rendering):

const engine = scene.getEngine() as WebGPUEngine;
engine.disableCacheBindGroups = true;

3. Reuse meshes instead of creating/disposing — use setPoints to update existing lines rather than creating new ones, so buffer IDs stay stable and the trie doesn’t grow:

// Instead of dispose + CreateGreasedLine:
existingLine.setPoints(newPoints);

Option 3 is the best long-term fix as it avoids both the leak and any cache-reset frame hitches. Option 1 is a good safety net if you must create/dispose frequently.

I’m unsure if Claude is on the right path here or if any of the workarounds it suggested are good ideas.

I would greatly appreciate any help here :smiling_face: Thank you!!

Claude is right :slight_smile: .

We assumed that the cache would not grow indefinitely and that bind groups would be reused between meshes.

We could implement mechanisms to regularly clean the cache from unused entries, but this would add an extra load to a mechanism that is already heavier than WebGL.

In your case, I would therefore try to implement case #3 described by Claude, which is the best in terms of performance (even in WebGL).

2 Likes

Ah, thank you so much for your insight, Evgeni! And will try case #3!