Skip to content

Commit 27d984f

Browse files
authored
Merge pull request #2686 from donmccurdy/feat-texture-gc
Dispose of unused textures when material is unregistered.
2 parents 8bac7ef + 444fc12 commit 27d984f

File tree

2 files changed

+66
-3
lines changed

2 files changed

+66
-3
lines changed

‎src/systems/material.js‎

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ var THREE = require('../lib/three');
33
var utils = require('../utils/');
44
var isHLS = require('../utils/material').isHLS;
55

6+
var bind = utils.bind;
67
var debug = utils.debug;
78
var error = debug('components:texture:error');
89
var TextureLoader = new THREE.TextureLoader();
@@ -14,15 +15,24 @@ TextureLoader.setCrossOrigin('anonymous');
1415
* System for material component.
1516
* Handle material registration, updates (for fog), and texture caching.
1617
*
17-
* @member materials {object} - Registered materials.
18-
* @member textureCache {object} - Texture cache for:
18+
* @member {object} materials - Registered materials.
19+
* @member {object} textureCounts - Number of times each texture is used. Tracked
20+
* separately from textureCache, because the cache (1) is populated in
21+
* multiple places, and (2) may be cleared at any time.
22+
* @member {object} textureCache - Texture cache for:
1923
* - Images: textureCache has mapping of src -> repeat -> cached three.js texture.
2024
* - Videos: textureCache has mapping of videoElement -> cached three.js texture.
2125
*/
2226
module.exports.System = registerSystem('material', {
2327
init: function () {
2428
this.materials = {};
29+
this.textureCounts = {};
2530
this.textureCache = {};
31+
32+
this.sceneEl.addEventListener(
33+
'materialtextureloaded',
34+
bind(this.onMaterialTextureLoaded, this)
35+
);
2636
},
2737

2838
clearTextureCache: function () {
@@ -189,12 +199,26 @@ module.exports.System = registerSystem('material', {
189199
},
190200

191201
/**
192-
* Stop tracking material.
202+
* Stop tracking material, and dispose of any textures not being used by
203+
* another material component.
193204
*
194205
* @param {object} material
195206
*/
196207
unregisterMaterial: function (material) {
197208
delete this.materials[material.uuid];
209+
210+
// If any textures on this material are no longer in use, dispose of them.
211+
var textureCounts = this.textureCounts;
212+
Object.keys(material)
213+
.filter(function (propName) {
214+
return material[propName] && material[propName].isTexture;
215+
})
216+
.forEach(function (mapName) {
217+
textureCounts[material[mapName].uuid]--;
218+
if (textureCounts[material[mapName].uuid] <= 0) {
219+
material[mapName].dispose();
220+
}
221+
});
198222
},
199223

200224
/**
@@ -205,6 +229,21 @@ module.exports.System = registerSystem('material', {
205229
Object.keys(materials).forEach(function (uuid) {
206230
materials[uuid].needsUpdate = true;
207231
});
232+
},
233+
234+
/**
235+
* Track textures used by material components, so that they can be safely
236+
* disposed when no longer in use. Textures must be registered here, and not
237+
* through registerMaterial(), because textures may not be attached at the
238+
* time the material is registered.
239+
*
240+
* @param {Event} e
241+
*/
242+
onMaterialTextureLoaded: function (e) {
243+
if (!this.textureCounts[e.detail.texture.uuid]) {
244+
this.textureCounts[e.detail.texture.uuid] = 0;
245+
}
246+
this.textureCounts[e.detail.texture.uuid]++;
208247
}
209248
});
210249

‎tests/systems/material.test.js‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,30 @@ suite('material system', function () {
4242
});
4343
});
4444

45+
suite('unregisterMaterial', function () {
46+
test('disposes of unused textures', function () {
47+
var el = this.el;
48+
var sinon = this.sinon;
49+
var system = el.sceneEl.systems.material;
50+
var texture1 = {uuid: 'tex1', isTexture: true, dispose: sinon.spy()};
51+
var texture2 = {uuid: 'tex2', isTexture: true, dispose: sinon.spy()};
52+
var material1 = {fooMap: texture1, barMap: texture2, dispose: sinon.spy()};
53+
var material2 = {fooMap: texture1, dispose: sinon.spy()};
54+
55+
el.emit('materialtextureloaded', {texture: texture1});
56+
el.emit('materialtextureloaded', {texture: texture1});
57+
el.emit('materialtextureloaded', {texture: texture2});
58+
59+
system.unregisterMaterial(material1);
60+
assert.notOk(texture1.dispose.called);
61+
assert.ok(texture2.dispose.called);
62+
63+
system.unregisterMaterial(material2);
64+
assert.ok(texture1.dispose.called);
65+
assert.equal(texture2.dispose.callCount, 1);
66+
});
67+
});
68+
4569
suite('texture caching', function () {
4670
setup(function () {
4771
this.system.clearTextureCache();

0 commit comments

Comments
 (0)