From 6063c7b0d91ce80671735b9beb903b67baa0c9f8 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Tue, 28 Feb 2017 22:23:39 +1100 Subject: [PATCH 1/3] Super hacky caching of model textures. --- Source/Scene/Model.js | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index 9e84579703b4..47848750082a 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -2234,6 +2234,8 @@ define([ /////////////////////////////////////////////////////////////////////////// + var cachedTexturesByContextId = {}; + function createTexture(gltfTexture, model, context) { var textures = model.gltf.textures; var texture = textures[gltfTexture.id]; @@ -2258,6 +2260,24 @@ define([ var tx; var source = gltfTexture.image; + var cachedTextures = cachedTexturesByContextId[context.id]; + if (!defined(cachedTextures)) { + cachedTextures = cachedTexturesByContextId[context.id] = {}; + } + + if (defined(source)) { + var cacheKey = source.src; + var cachedTexture = cachedTextures[cacheKey]; + if (defined(cachedTexture)) { + if (!defined(cachedTexture._referenceCount)) { + cachedTexture._referenceCount = 1; + } else { + ++cachedTexture._referenceCount; + } + return cachedTexture; + } + } + if (defined(internalFormat) && texture.target === WebGLConstants.TEXTURE_2D) { tx = new Texture({ context : context, @@ -2297,6 +2317,8 @@ define([ if (mipmap) { tx.generateMipmap(); } + + cachedTextures[source.src] = tx; } model._rendererResources.textures[gltfTexture.id] = tx; @@ -4215,7 +4237,15 @@ define([ function destroy(property) { for (var name in property) { if (property.hasOwnProperty(name)) { - property[name].destroy(); + var resource = property[name]; + if (defined(resource._referenceCount)) { + --resource._referenceCount; + if (resource._referenceCount === 0) { + resource.destroy(); + } + } else { + property[name].destroy(); + } } } } From 86439c00756759ff3bf5646ab9fc9203d92dc924 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 1 Mar 2017 20:43:35 +1100 Subject: [PATCH 2/3] Less hacky caching of model textures. --- Source/Scene/Model.js | 54 +++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index 47848750082a..b680f8423e7c 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -2234,7 +2234,7 @@ define([ /////////////////////////////////////////////////////////////////////////// - var cachedTexturesByContextId = {}; + var resourcesCachedAcrossModels = {}; function createTexture(gltfTexture, model, context) { var textures = model.gltf.textures; @@ -2251,7 +2251,7 @@ define([ (sampler.minificationFilter === TextureMinificationFilter.NEAREST_MIPMAP_LINEAR) || (sampler.minificationFilter === TextureMinificationFilter.LINEAR_MIPMAP_NEAREST) || (sampler.minificationFilter === TextureMinificationFilter.LINEAR_MIPMAP_LINEAR)); - var requiresNpot = mipmap || + var requiresPowerOfTwo = mipmap || (sampler.wrapS === TextureWrap.REPEAT) || (sampler.wrapS === TextureWrap.MIRRORED_REPEAT) || (sampler.wrapT === TextureWrap.REPEAT) || @@ -2260,24 +2260,6 @@ define([ var tx; var source = gltfTexture.image; - var cachedTextures = cachedTexturesByContextId[context.id]; - if (!defined(cachedTextures)) { - cachedTextures = cachedTexturesByContextId[context.id] = {}; - } - - if (defined(source)) { - var cacheKey = source.src; - var cachedTexture = cachedTextures[cacheKey]; - if (defined(cachedTexture)) { - if (!defined(cachedTexture._referenceCount)) { - cachedTexture._referenceCount = 1; - } else { - ++cachedTexture._referenceCount; - } - return cachedTexture; - } - } - if (defined(internalFormat) && texture.target === WebGLConstants.TEXTURE_2D) { tx = new Texture({ context : context, @@ -2290,9 +2272,30 @@ define([ sampler : sampler }); } else if (defined(source)) { + var cacheKey; + + // Only try to cache textures created from an image with a reasonably short src URL. + // i.e. not data URIs. + if (defined(source.src) && source.src.length < 1024) { + cacheKey = JSON.stringify([ + context.id, + requiresPowerOfTwo, + source.src, + texture.internalFormat, + texture.type, + sampler + ]); + + var cachedTexture = resourcesCachedAcrossModels[cacheKey]; + if (defined(cachedTexture)) { + ++cachedTexture._referenceCount; + return cachedTexture; + } + } + var npot = !CesiumMath.isPowerOfTwo(source.width) || !CesiumMath.isPowerOfTwo(source.height); - if (requiresNpot && npot) { + if (requiresPowerOfTwo && npot) { // WebGL requires power-of-two texture dimensions for mipmapping and REPEAT/MIRRORED_REPEAT wrap modes. var canvas = document.createElement('canvas'); canvas.width = CesiumMath.nextPowerOfTwo(source.width); @@ -2318,7 +2321,11 @@ define([ tx.generateMipmap(); } - cachedTextures[source.src] = tx; + if (defined(cacheKey)) { + tx._referenceCount = 1; + tx._cacheKey = cacheKey; + resourcesCachedAcrossModels[cacheKey] = tx; + } } model._rendererResources.textures[gltfTexture.id] = tx; @@ -4241,6 +4248,9 @@ define([ if (defined(resource._referenceCount)) { --resource._referenceCount; if (resource._referenceCount === 0) { + if (defined(resource._cacheKey)) { + delete resourcesCachedAcrossModels[resource._cacheKey]; + } resource.destroy(); } } else { From ffc39ea1ed1520e9dc8ad52b44dd58739df97e34 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Wed, 1 Mar 2017 21:55:45 +1100 Subject: [PATCH 3/3] Write tests, fix a legit bug. --- Source/Scene/Model.js | 3 +- Specs/Scene/ModelSpec.js | 92 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index b680f8423e7c..a2620b753fb9 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -2289,7 +2289,8 @@ define([ var cachedTexture = resourcesCachedAcrossModels[cacheKey]; if (defined(cachedTexture)) { ++cachedTexture._referenceCount; - return cachedTexture; + model._rendererResources.textures[gltfTexture.id] = cachedTexture; + return; } } diff --git a/Specs/Scene/ModelSpec.js b/Specs/Scene/ModelSpec.js index ee2619045b02..bfc4e9d44ddf 100644 --- a/Specs/Scene/ModelSpec.js +++ b/Specs/Scene/ModelSpec.js @@ -922,6 +922,98 @@ defineSuite([ }); }); + it('frees a texture that is only used once', function() { + return loadModel(texturedBoxSeparateUrl, { incrementallyLoadTextures : false }).then(function(m) { + expect(m._cachedRendererResources).toBeDefined(); + expect(m._cachedRendererResources.textures).toBeDefined(); + expect(Object.keys(m._cachedRendererResources.textures).length).toBe(1); + + var texture = m._cachedRendererResources.textures[Object.keys(m._cachedRendererResources.textures)[0]]; + + scene.primitives.remove(m); + expect(texture.isDestroyed()).toBe(true); + }); + }); + + function loadTwoCopiesOfModel(url) { + return loadJson(url).then(function(modelJson) { + var model1 = primitives.add(new Model({ + modelMatrix : Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(0.0, 0.0, 100.0)), + gltf : modelJson, + id : '1', + show : false, + incrementallyLoadTextures : false, + basePath : './Data/Models/Box-Textured-Separate/' + })); + + var model2 = primitives.add(new Model({ + modelMatrix : Transforms.eastNorthUpToFixedFrame(Cartesian3.fromDegrees(1.0, 0.0, 100.0)), + gltf : modelJson, + id : '2', + show : false, + incrementallyLoadTextures : false, + basePath : './Data/Models/Box-Textured-Separate/' + })); + + return pollToPromise(function() { + // Render scene to progressively load the model + scene.renderForSpecs(); + return model1.ready && model2.ready; + }, { timeout: 10000 }).then(function() { + return { + model1: model1, + model2: model2 + }; + }); + }); + } + + it('shares common textures between models', function() { + return loadTwoCopiesOfModel(texturedBoxSeparateUrl).then(function(models) { + var model1 = models.model1; + var model2 = models.model2; + + var texture1 = model1._cachedRendererResources.textures[Object.keys(model1._cachedRendererResources.textures)[0]]; + var texture2 = model2._cachedRendererResources.textures[Object.keys(model2._cachedRendererResources.textures)[0]]; + expect(texture1).toBe(texture2); + + scene.primitives.remove(model1); + scene.primitives.remove(model2); + }); + }); + + it('does not destroy shared texture when the first model that uses it is destroyed', function() { + return loadTwoCopiesOfModel(texturedBoxSeparateUrl).then(function(models) { + var model1 = models.model1; + var model2 = models.model2; + + var texture1 = model1._cachedRendererResources.textures[Object.keys(model1._cachedRendererResources.textures)[0]]; + var texture2 = model1._cachedRendererResources.textures[Object.keys(model1._cachedRendererResources.textures)[0]]; + + scene.primitives.remove(model1); + expect(texture1.isDestroyed()).toBe(false); + expect(texture2.isDestroyed()).toBe(false); + + scene.primitives.remove(model2); + }); + }); + + it('destroys shared texture after all models that use it are destroyed', function() { + return loadTwoCopiesOfModel(texturedBoxSeparateUrl).then(function(models) { + var model1 = models.model1; + var model2 = models.model2; + + var texture1 = model1._cachedRendererResources.textures[Object.keys(model1._cachedRendererResources.textures)[0]]; + var texture2 = model1._cachedRendererResources.textures[Object.keys(model1._cachedRendererResources.textures)[0]]; + + scene.primitives.remove(model1); + scene.primitives.remove(model2); + + expect(texture1.isDestroyed()).toBe(true); + expect(texture2.isDestroyed()).toBe(true); + }); + }); + /////////////////////////////////////////////////////////////////////////// it('loads cesiumAir', function() {