diff --git a/src/Cache.js b/src/Cache.js index 66bbba200e..87c081964d 100755 --- a/src/Cache.js +++ b/src/Cache.js @@ -15,7 +15,8 @@ x3dom.Cache = function () { }; /** - * Returns a Texture 2D + * Returns the existing texture identified by "url" or generates and returns + * a new texture to be hereafter identified by the key "url." */ x3dom.Cache.prototype.getTexture2D = function (gl, doc, url, bgnd, crossOrigin, scale, genMipMaps) { var textureIdentifier = url; @@ -28,6 +29,13 @@ x3dom.Cache.prototype.getTexture2D = function (gl, doc, url, bgnd, crossOrigin, return this.textures[textureIdentifier]; }; +/** + * Returns the texture identified by "url" or returns undefined if there is no such texture. + */ +x3dom.Cache.prototype.getTexture2DByUrl = function ( url ) { + return this.textures[url]; +} + /** * Returns a Texture 2D */ @@ -41,6 +49,16 @@ x3dom.Cache.prototype.getTexture2DByDEF = function (gl, nameSpace, def) { return this.textures[textureIdentifier]; }; +/** + * Deletes the texture identified by "url" or does nothing if there is no such texture. + */ +x3dom.Cache.prototype.deleteTexture2DByUrl = function ( gl, url ) { + if( this.textures[url] !== undefined ) { + gl.deleteTexture( this.textures[url] ); + delete this.textures[url]; + } +}; + /** * Returns a Cube Texture */ diff --git a/src/Texture.js b/src/Texture.js index 3f74375650..6efc3d31e6 100755 --- a/src/Texture.js +++ b/src/Texture.js @@ -61,6 +61,7 @@ x3dom.Texture = function (gl, doc, cache, node) { this.ready = false; this.dashtexture = false; + this.lastUrlUsedForTextureCreation = null; var tex = this.node; var suffix = "mpd"; @@ -127,6 +128,35 @@ x3dom.Texture.prototype.update = function() } }; +/** + * Invoke gl.deleteTexture on the texture handle, thus freeing up some video memory. + * @pre This instance has been properly set up before and contains a handle to a valid OpenGL texture. + * @pre The texture was created by using a URL. In other words, this.lastUrlUsedForTextureCreation is defined. + * @post This instance is no longer valid, and should be either updated or deleted. + */ +x3dom.Texture.prototype.cleanGLObjects = function() +{ + if ( x3dom.isa( this.node, x3dom.nodeTypes.ImageTexture ) ) { + var textureUrl = this.lastUrlUsedForTextureCreation; + if( textureUrl === null ) { + x3dom.debug.logError( 'cleanGLObjects cannot delete texture by url since lastUrlUsedForTextureCreation is null' ); + } else { + var textureHandle = this.cache.getTexture2DByUrl( textureUrl ); + if( textureHandle === undefined || textureHandle !== this.texture ) { + // TODO: Implement cleanup logic for the case where this.cache is + // not holding the texture handle. + x3dom.debug.logError( 'cleanGLObjects not defined for case where this.cache does not contain this.texture' ); + } else { + this.cache.deleteTexture2DByUrl( this.gl, textureUrl ); + this.texture = undefined; // Make debugging easier. + } + } + } else { + // TODO: Implement for other node types, like MovieTexture and so on. + x3dom.debug.logError( 'cleanGLObjects not defined for this kind of texture node!' ); + } +}; + x3dom.Texture.prototype.setPixel = function(x, y, pixel, update) { var gl = this.gl; @@ -395,16 +425,29 @@ x3dom.Texture.prototype.updateTexture = function() } else if (x3dom.isa(tex, x3dom.nodeTypes.X3DEnvironmentTextureNode)) { - this.texture = this.cache.getTextureCube(gl, doc, tex.getTexUrl(), false, + this.lastUrlUsedForTextureCreation = tex.getTexUrl(); + this.texture = this.cache.getTextureCube( gl, doc, this.lastUrlUsedForTextureCreation, false, tex._vf.crossOrigin, tex._vf.scale, this.genMipMaps); } else { - this.texture = this.cache.getTexture2D(gl, doc, tex._nameSpace.getURL(tex._vf.url[0]), + this.lastUrlUsedForTextureCreation = this.getUrlForBasicTexture(); + this.texture = this.cache.getTexture2D( gl, doc, this.lastUrlUsedForTextureCreation, false, tex._vf.crossOrigin, tex._vf.scale, this.genMipMaps); } }; +/** + * Returns the URL of the relevant texture, assuming the relevant node is not an + * X3DEnvironmentTextureMode or some other node which defines a special way of generating + * the texture's URL. + * @pre this.node is the sort for which basic URL-generation method is appropriate. + */ +x3dom.Texture.prototype.getUrlForBasicTexture = function() +{ + return this.node._nameSpace.getURL( this.node._vf.url[0] ); +}; + x3dom.Texture.prototype.updateText = function() { var gl = this.gl; diff --git a/src/gfx_webgl.js b/src/gfx_webgl.js index 7be9bdf86d..b3d1787f7f 100755 --- a/src/gfx_webgl.js +++ b/src/gfx_webgl.js @@ -179,23 +179,39 @@ x3dom.gfx_webgl = (function () { return null; } - /***************************************************************************** * Setup GL objects for given shape *****************************************************************************/ Context.prototype.setupShape = function (gl, drawable, viewarea) { var q = 0, q6; - var textures, t; + var textureNodes, t; var vertices, positionBuffer; var texCoordBuffer, normalBuffer, colorBuffer; var indicesBuffer, indexArray; var shape = drawable.shape; var geoNode = shape._cf.geometry.node; - + if (shape._webgl !== undefined) { var needFullReInit = false; + // Make a copy of shape._webgl.texture, which is essentially the collection of + // texture wrappers that this shape is in charge of. By making a copy, we can detect + // which textures have gone out of use, at which point we can delete them if we chose. + var oldTextureWrappers = []; + Array.forEach( shape._webgl.texture, function( oldTextureWrapper ) { + // Make a deep (enough) copy to support comparisons with true Texture instances + // and the deletion of an OpenGL texture, if necessary (the latter is + // why there is a copy of cache and gl in here). + var copyOfOldTextureWrapper = {}; + copyOfOldTextureWrapper.node = oldTextureWrapper.node; + copyOfOldTextureWrapper.texture = oldTextureWrapper.texture; + copyOfOldTextureWrapper.lastUrlUsedForTextureCreation = oldTextureWrapper.lastUrlUsedForTextureCreation; + copyOfOldTextureWrapper.cache = oldTextureWrapper.cache; + copyOfOldTextureWrapper.gl = oldTextureWrapper.gl; + oldTextureWrappers.push( copyOfOldTextureWrapper ); + }); + // TODO; do same for texcoords etc.! if (shape._dirty.colors === true && shape._webgl.shader.color === undefined && geoNode._mesh._colors[0].length) { @@ -208,48 +224,47 @@ x3dom.gfx_webgl = (function () { if (needFullReInit && shape._cleanupGLObjects) { shape._cleanupGLObjects(true, false); } - - //Check for dirty Textures + + // Check for dirty Textures (Have DOM elements been modified?) if (shape._dirty.texture === true) { - //Check for Texture add or remove - if (shape._webgl.texture.length != shape.getTextures().length) { - //Delete old Textures - for (t = 0; t < shape._webgl.texture.length; ++t) { + + // ImageTexture nodes, MovieTexture nodes, etc. + textureNodes = shape.getTextures(); + + // Check if Textures have been added or removed via adding/removing nodes. + if (shape._webgl.texture.length != textureNodes.length) { + // Delete old Textures (the wrappers, not the actual OpenGL texture objects). + for (t = 0; t < shape._webgl.texture.length; ++t) { shape._webgl.texture.pop(); } - //Generate new Textures - textures = shape.getTextures(); - - for (t = 0; t < textures.length; ++t) { - shape._webgl.texture.push(new x3dom.Texture(gl, shape._nameSpace.doc, this.cache, textures[t])); + // Create new Texture wrappers. + for (t = 0; t < textureNodes.length; ++t) { + shape._webgl.texture.push(new x3dom.Texture(gl, shape._nameSpace.doc, this.cache, textureNodes[t])); } - //Set dirty shader shape._dirty.shader = true; - //Set dirty texture Coordinates + // Set dirty texture Coordinates if (shape._webgl.shader.texcoord === undefined) shape._dirty.texcoords = true; } else { - //If someone remove and append at the same time, texture count don't change - //and we have to check if all nodes the same as before - textures = shape.getTextures(); - - for (t = 0; t < textures.length; ++t) { - if (textures[t] === shape._webgl.texture[t].node) { - //only update the texture + // If client code performed an equal number of removes and appends, the texture count doesn't change + // and we have to check if all nodes the same as before + for (t = 0; t < textureNodes.length; ++t) { + if (textureNodes[t] === shape._webgl.texture[t].node) { + // Only update the texture shape._webgl.texture[t].update(); } else { - //Set texture to null for recreation + // Set texture to null for recreation shape._webgl.texture[t].texture = null; - //Set new node - shape._webgl.texture[t].node = textures[t]; + // Set new node + shape._webgl.texture[t].node = textureNodes[t]; - //Update new node + // Update new node shape._webgl.texture[t].update(); } } @@ -434,7 +449,32 @@ x3dom.gfx_webgl = (function () { geoNode.unsetGeoDirty(); shape.unsetGeoDirty(); } - + + // Check whether there are any now-unused texture wrappers whose textures can now by cleaned up by + // the GL context. + Array.forEach( oldTextureWrappers, function( oldTextureWrapper ) { + + // Check whether oldTextureWrapper is not a member of the updated + // texture wrappers (out of the updated texture wrappers, there is not + // a wrapper that wraps the same texture). + var textureNoLongerUsed = true; + Array.forEach( shape._webgl.texture, function( newTextureWrapper ) { + if( newTextureWrapper.texture === oldTextureWrapper.texture ) { + textureNoLongerUsed = false; + } + }); + + if( textureNoLongerUsed && oldTextureWrapper.node.cleanGLObjectsUponModifyOrDelete() ) { + // invoke gl.deleteTexture - would say + // 'oldTextureWrapper.cleanGLObjects()' except + // that oldTextureWrapper is not a true instance of Texture, + // just a copy of some of the fields from a Texture instance. + x3dom.Texture.prototype.cleanGLObjects.call( oldTextureWrapper ); + } + + }); + oldTextureWrappers = []; + if (!needFullReInit) { // we're done return; @@ -457,11 +497,11 @@ x3dom.gfx_webgl = (function () { } return; } - + // we're on init, thus reset all dirty flags shape.unsetDirty(); - // dynamically attach clean-up method for GL objects + // Dynamically attach clean-up method for GL objects if (!shape._cleanupGLObjects) { shape._cleanupGLObjects = function (force, delGL) @@ -469,6 +509,15 @@ x3dom.gfx_webgl = (function () { // FIXME; what if complete tree is removed? Then _parentNodes.length may be greater 0. if (this._webgl && ((arguments.length > 0 && force) || this._parentNodes.length == 0)) { + // Invoke gl.deleteTexture on those textures that are not supposed to hang around + // in memory when they go out of use. + Array.forEach( shape._webgl.texture, function( textureWrapper ) { + if( textureWrapper.node.cleanGLObjectsUponModifyOrDelete() ) { + // Here we are assuming that the Shape is going out of existence. + textureWrapper.cleanGLObjects(); + } + }); + var sp = this._webgl.shader; for (var q = 0; q < this._webgl.positions.length; q++) { @@ -539,12 +588,12 @@ x3dom.gfx_webgl = (function () { externalGeometry: 0 // 0 : no EG, 1 : indexed EG, -1 : non-indexed EG }; - //Set Textures - textures = shape.getTextures(); - for (t = 0; t < textures.length; ++t) { - shape._webgl.texture.push(new x3dom.Texture(gl, shape._nameSpace.doc, this.cache, textures[t])); + // Update texture wrappers + textureNodes = shape.getTextures(); + for (t = 0; t < textureNodes.length; ++t) { + shape._webgl.texture.push(new x3dom.Texture(gl, shape._nameSpace.doc, this.cache, textureNodes[t])); } - + //Set Shader //shape._webgl.shader = this.cache.getDynamicShader(gl, viewarea, shape); //shape._webgl.shader = this.cache.getShaderByProperties(gl, drawable.properties); diff --git a/src/nodes/Texturing/X3DTextureNode.js b/src/nodes/Texturing/X3DTextureNode.js index d2f4affc3c..9f05824d78 100644 --- a/src/nodes/Texturing/X3DTextureNode.js +++ b/src/nodes/Texturing/X3DTextureNode.js @@ -97,6 +97,24 @@ x3dom.registerNodeType( * @instance */ this.addField_SFNode('textureProperties', x3dom.nodeTypes.TextureProperties); + + /** + * Specifies whether gl.deleteTexture() should be called on the current texture + * if this node gets deleted or it switches to representing a new texture image. + * + * Warning, this field should only be set to "true" if the relevant texture + * is used within one Shape node (preferably within a single X3DTextureNode inside + * that Shape). If another Shape references the same texture by using the same + * texture URL, then when a DOM modification to the first Shape causes the texture to be + * deleted, the second Shape might display incorrectly. + * + * @var {x3dom.fields.SFBool} cleanGLObjectsUponModifyOrDelete + * @memberof x3dom.nodeTypes.X3DTextureNode + * @initvalue false + * @field x3dom + * @instance + */ + this.addField_SFBool(ctx, 'cleanGLObjectsUponModifyOrDelete', false); this._needPerFrameUpdate = false; this._isCanvas = false; @@ -106,6 +124,8 @@ x3dom.registerNodeType( }, { + // TODO: This name is misleading, since calling invalidateGLObject() does not + // necessarily lead to a texture actually getting deleted. invalidateGLObject: function () { Array.forEach(this._parentNodes, function (app) { @@ -115,7 +135,7 @@ x3dom.registerNodeType( shape._dirty.texture = true; } else { - // Texture maybe in MultiTexture or CommonSurfaceShader + // Texture may be in MultiTexture or CommonSurfaceShader Array.forEach(shape._parentNodes, function (realShape) { if (x3dom.isa(realShape, x3dom.nodeTypes.X3DShapeNode)) { realShape._dirty.texture = true; @@ -165,15 +185,20 @@ x3dom.registerNodeType( } }); }, + + cleanGLObjectsUponModifyOrDelete : function() + { + return this._vf.cleanGLObjectsUponModifyOrDelete; + }, fieldChanged: function(fieldName) { if (fieldName == "url" || fieldName == "origChannelCount" || fieldName == "repeatS" || fieldName == "repeatT" || fieldName == "scale" || fieldName == "crossOrigin") - { + { var that = this; - + Array.forEach(this._parentNodes, function (app) { if (x3dom.isa(app, x3dom.nodeTypes.X3DAppearanceNode)) { app.nodeChanged(); @@ -202,7 +227,7 @@ x3dom.registerNodeType( if(app._volumeDataParent){ app._volumeDataParent._dirty.texture = true; }else{ - //Texture maybe under a ComposedVolumeStyle + // Texture may be under a ComposedVolumeStyle var volumeDataParent = app._parentNodes[0]; while(!x3dom.isa(volumeDataParent, x3dom.nodeTypes.X3DVolumeDataNode) && x3dom.isa(volumeDataParent, x3dom.nodeTypes.X3DNode)){ volumeDataParent = volumeDataParent._parentNodes[0];