Procedural texture memory leak

Hey everyone!

I have been tracking a memory leak in Cosmos Journeyer for about a week now where disposing planets would not release the RAM used.

I finally tracked it down to a large procedural texture that just does not want to die somehow :joy: Basically, calling dispose on the procedural texture does not free the RAM. What I don’t understand yet is why is the data stored in RAM while the procedural texture is used in GPU memory :thinking:

Here is a repro of the bug (be careful it might crash your computer if you let it run for too long):

If anyone knows how to fix this I would be greatful, but that kinda looks like a bug to me :thinking:

You should not use a texture if it has been disposed, it is undefined behavior. So, when you dispose of the texture, you should also set mat.diffuseTexture = null:

1 Like

So in consequence, if we want to play it safe, whenever we

  • dispose a texture,
  • disopse a material with the dispose-texture paramter
  • dispose a particle system (afaik auto-disposes its texture)

we need to iterate all materials and check all texture channels for leftover references?

Would it suffice to use onTextureRemovedObservable** in order to always prevent a memory leak? (**I mean if the observable notifies, iterate materials then)

1 Like

Yes, onTextureRemovedObservable seems a good way to remove references to a texture that has just been removed.

2 Likes

Alright that’s definitely an improvement.

However setting the texture to null is not something that can be done with a ShaderMaterial for example (setTexture only accepts a texture and not null)

Also I still don’t understand why the texture has any cpu ram footprint as it is stored on the GPU. Is there some cached texture data that can be flushed to optimize RAM usage?

You can’t pass null to setTexture because passing a null texture to a shader is not allowed. If your shader doesn’t use the texture anymore, you must update it, because the texture(procTex, vUV); instruction is wrong/undefined behavior.

That’s probably because context lost management is enabled for your engine (it is the default). In that case, a number of resources (like textures) are cached, to be able to recreate them after a context has been lost/recreated. See Babylon.js docs

Alright disabling the feature indeed saves a lot of RAM nice :ok_hand:

I also confused myself about the ShaderMaterial, sorry about that ^^

What I really meant is that in this PG, the ShaderMaterial is disposed and so is the ProceduralTexture (every second). However, waiting for about 30s on my machine, the cpu ram usage starts to rise (even with doNotHandleContextLost set to true) and then It crashes:

WebGL: CONTEXT_LOST_WEBGL: loseContext: context lost
thinEngine.ts:2817 Uncaught (in promise) Error: Unable to create texture
    at t._createTexture (thinEngine.ts:2817:19)
    at t._createHardwareTexture (thinEngine.ts:2825:46)
    at new t (internalTexture.ts:309:44)
    at t._createInternalTexture (thinEngine.ts:2877:25)
    at t.createRenderTargetTexture (engine.renderTarget.ts:74:73)
    at t._createRtWrapper (proceduralTexture.ts:228:48)
    at new t (proceduralTexture.ts:202:32)
    at t.createProceduralTexture (nodeMaterial.ts:1294:35)
    at <anonymous>:63:52
t._createTexture @ thinEngine.ts:2817
t._createHardwareTexture @ thinEngine.ts:2825
t @ internalTexture.ts:309
t._createInternalTexture @ thinEngine.ts:2877
(anonymous) @ engine.renderTarget.ts:74
(anonymous) @ proceduralTexture.ts:228
t @ proceduralTexture.ts:202
(anonymous) @ nodeMaterial.ts:1294
(anonymous) @ VM41:63
Promise.then
createTex @ VM41:62
(anonymous) @ VM41:80
setTimeout
(anonymous) @ VM41:78
(anonymous) @ proceduralTexture.ts:338
(anonymous) @ effect.ts:579
e.notifyObservers @ observable.ts:393
e._onRenderingStateCompiled @ effect.ts:743
onRenderingStateCompiled @ effect.ts:790
(anonymous) @ effect.functions.ts:308
(anonymous) @ thinEngine.functions.ts:351
f @ thinEngine.functions.ts:249
t._finalizePipelineContext @ thinEngine.ts:2067
t._isRenderingStateCompiled @ thinEngine.ts:2127
get @ webGLPipelineContext.ts:37
e._isReadyInternal @ effect.ts:447
e._checkIsReady @ effect.ts:591
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604
setTimeout
e._checkIsReady @ effect.ts:603
(anonymous) @ effect.ts:604

When running nvidia-smi, I can see the VRAM usage increase until overflow while I would expect it to be stable over time if the texture was released from memory every second :thinking:

It looks like it could be a problem with the browser… I added some debug code and could check that each time we dispose a texture, we do execute context.deleteTexture for that texture, which should free the GPU memory, or at least mark the memory as being “reclamable”: it’s not required that the browser immediately releases the memory.

Also, I saw with nvidia-smi that once I reach my max GPU memory, refreshes still work for 5 or 6s, meaning the browser is able to reuse GPU memory under-the-hood: the used memory stays at max during all that time, proving (I think) that GPU memory is not really freed by the browser, but that it is able to recycle the released textures. But at some point it is not able to do it anymore for some reasons…

Note that it works in WebGPU, memory does not raise with each refresh.

It looks quite like Memory Leak in Screenshots - #19 by Evgeni_Popov, but I don’t know what the final outcome of this case was.

1 Like

Okay, so I guess the solution for WebGL is to avoid doing large allocations and release in quick succession to let the browser take its time to give it back to the GPU.

I wanted to use WebGPU eventually so I won’t have to worry about it!

Thanks again for your time and knowledge :wink: