I’m trying to ensure that I’m doing the best I can to clear up textures that I’m using using texture.dispose().
We add textures at runtime during a webpage’s lifecycle, and get rid of old ones. When I add textures, I see WebGLRenderer.info.memory.textures show an increasing count. When I dispose of old textures, I don’t see the WebGLRenderer.info.memory.textures go down.
The first question is: is this important? Or does this count correspond to GPU texture units that have been used, but aren’t necessarily now a problem, and will simply be overridden in future?
The other potentially important factor here is that, in this application, I’m trying to dispose of textures that are on unique materials in a GLTF model that are no longer shown, but will be shown again in future, so the material will be kept in place but I want to dispose of the currently-unused textures.
I performed a test with a basic cube in a scene, that has a material with a texture assigned to material.map, then disposed of that texture. The cube would still show that texture on the material, and WebGLRenderer.info.memory.textures would still reflect the count reached when I first added that texture to the cube. Once I eventually either removed the cube from the scene, or set cube.material.map to a new THREE.Texture(), the WebGLRenderer.info.memory.textures would drop.
So I can imagine that this isn’t an issue, and is never really observed, in common use cases where a material is on an object that is completely removed, but our scene setup is a bit more specific, and needs to be somewhat aggressively optimised, so I need to try to make more sense of what’s going on here.
Important. Resources aren’t disposed automatically from the GPU (like they are from the CPU, where garbage collector takes care of it.) If you create too many textures WebGL context will eventually just crash, and the canvas will disappear.
If the texture size / count isn’t too huge - you can just leave them on the GPU most of the time. Disposing small textures will require a re-upload of them to the GPU again in the future, causing unnecessary frame stutter.
This may happen if the Texture image wasn’t loaded yet - if you set material map to new Texture() quick enough, the old texture just won’t appear on the GPU yet. But if you add setTimeout around that disposal and add some delay - you should notice that the old texture remains on the GPU after you replace map property on the material.
As for the title:
WebGLRenderer.info.textures is usually: numberOfTexturesInMaterials + numberOfRenderTargets (depending on whether you’re using postprocessing or additional render targets, the value of the second part can be anywhere from 2 to “a lot”.) Disposing unused textures properly will decrease this number - it’ll not reach zero, you just have to make sure it’s not increasing over time too much.
When you add an object with a material to the scene - WebGLRenderer.info.textures will increase by the number of textures used for that object.
When you try to dispose things that are still in the scene - they will be automatically re-uploaded to the GPU on the next frame, causing occasional FPS stutter. This will neither free up the memory, nor cause the objects to disappear.
When you properly dispose an object from a scene - WebGLRenderer.info.textures will decrease by the same number it increased when you added said object (unless it reuses Materials / Textures ofc.) If it doesn’t - you’re not disposing materials properly. The most often causes of issues with disposal are probably:
Disposing material and textures before removing the object from the scene. If you call .dispose on resources before doing scene.remove(object) - there’s a chance the resources will be automatically re-created before object is removed (so in the end, you’ll remove the object, but the textures stay living rent-free on the GPU.)
Not disposing childrens’ materials. Be sure to use traverse instead of forEach when disposing anything more complex than a cube:
const model = new Mesh(geometry, material);
model.add(new Mesh(geometry2, material2);
model.children[0].add(new Mesh(geometry3, material3);
// NOTE This will not dispose all materials
model.children.forEach(child => {
child.parent.remove(child);
if (child.material) {
child.material.dispose();
}
if (child.material?.map) {
child.material.map.dispose();
}
});
// NOTE This will dispose all materials from the GPU (make sure to dispose outside of traverse though)
const disposables = [];
model.traverse(child => {
if (child.material) {
disposables.push(child);
}
});
disposables.forEach(child => {
child.parent.remove(child);
child.material.dispose();
if (child.material.map) {
child.material.map.dispose();
}
});
I think one thing is clear to me now, and I suspected this from my experiments - texture.dispose() isn’t the only thing that must be done in order to get rid of textures; materials referencing the texture must also be dealt with.
As I mentioned, I wouldn’t be looking to remove the materials in this particular application, as they are only temporarily unused; scrolling around on the page will result in the display of that material again, as it is merely hidden on geometry offscreen.
The textures in this application are 4k/2160p in size, as they are shown on geometry placed at a depth of 0 in front of the camera, acting momentarily like a flat projection, where a high level of detail is required.
So it feels unreasonable to store 6 or 7 of these textures continually on the GPU.
Therefore, I am making an inexpensive calculation to decide which of the materials in my GLTF scene are showing or close to showing, and then I instate the texture that is required.
I was aware of the possibility of frame stutter when reuploading textures, but I haven’t noticed this yet. I’ve seen this in other projects when material shaders are compiled.