From de426de63907aa761a650845f2934c73cdd3951c Mon Sep 17 00:00:00 2001 From: Sean Lilley Date: Tue, 28 Feb 2017 13:45:05 -0500 Subject: [PATCH] Return vertex and texture memory usage for Models --- Source/Core/PixelFormat.js | 35 +++++++++++++++++ Source/Renderer/CubeMap.js | 13 ++++++- Source/Renderer/PixelDatatype.js | 23 +++++++++++ Source/Renderer/Texture.js | 17 +++++++++ Source/Scene/Model.js | 36 ++++++++++++++++++ Specs/Renderer/CubeMapSpec.js | 32 ++++++++++++++++ Specs/Renderer/TextureSpec.js | 65 ++++++++++++++++++++++++++++++-- Specs/Scene/ModelSpec.js | 8 ++++ 8 files changed, 224 insertions(+), 5 deletions(-) diff --git a/Source/Core/PixelFormat.js b/Source/Core/PixelFormat.js index ee8f9c48c12d..42351717201a 100644 --- a/Source/Core/PixelFormat.js +++ b/Source/Core/PixelFormat.js @@ -1,8 +1,10 @@ /*global define*/ define([ + '../Renderer/PixelDatatype', './freezeObject', './WebGLConstants' ], function( + PixelDatatype, freezeObject, WebGLConstants) { 'use strict'; @@ -141,6 +143,28 @@ define([ */ RGB_ETC1 : WebGLConstants.COMPRESSED_RGB_ETC1_WEBGL, + /** + * @private + */ + componentLength : function(pixelFormat) { + switch (pixelFormat) { + case PixelFormat.RGB: + // Many GPUs store RGB as RGBA internally + // https://devtalk.nvidia.com/default/topic/699479/general-graphics-programming/rgb-auto-converted-to-rgba/post/4142379/#4142379 + return 4; + case PixelFormat.RGBA: + return 4; + case PixelFormat.ALPHA: + return 1; + case PixelFormat.LUMINANCE: + return 1; + case PixelFormat.LUMINANCE_ALPHA: + return 2; + default: + return 1; + } + }, + /** * @private */ @@ -249,6 +273,17 @@ define([ default: return 0; } + }, + + /** + * @private + */ + textureSize : function(pixelFormat, pixelDatatype, width, height) { + var componentLength = PixelFormat.componentLength(pixelFormat); + if (PixelDatatype.isPackedDatatype(pixelDatatype)) { + componentLength = 1; + } + return componentLength * PixelDatatype.sizeInBytes(pixelDatatype) * width * height; } }; diff --git a/Source/Renderer/CubeMap.js b/Source/Renderer/CubeMap.js index 4947d22d9e72..1db404948a89 100644 --- a/Source/Renderer/CubeMap.js +++ b/Source/Renderer/CubeMap.js @@ -108,6 +108,8 @@ define([ } //>>includeEnd('debug'); + var sizeInBytes = PixelFormat.textureSize(pixelFormat, pixelDatatype, size, size) * 6; + // Use premultiplied alpha for opaque textures should perform better on Chrome: // http://media.tojicode.com/webglCamp4/#20 var preMultiplyAlpha = options.preMultiplyAlpha || ((pixelFormat === PixelFormat.RGB) || (pixelFormat === PixelFormat.LUMINANCE)); @@ -156,6 +158,7 @@ define([ this._pixelFormat = pixelFormat; this._pixelDatatype = pixelDatatype; this._size = size; + this._sizeInBytes = sizeInBytes; this._preMultiplyAlpha = preMultiplyAlpha; this._flipY = flipY; this._sampler = undefined; @@ -253,11 +256,16 @@ define([ return this._size; } }, - height: { + height : { get : function() { return this._size; } }, + sizeInBytes : { + get : function() { + return this._sizeInBytes; + } + }, preMultiplyAlpha : { get : function() { return this._preMultiplyAlpha; @@ -313,6 +321,9 @@ define([ gl.bindTexture(target, this._texture); gl.generateMipmap(target); gl.bindTexture(target, null); + + // The mipmap adds approximately 1/3 of the original texture size + this._sizeInBytes = Math.floor(this._sizeInBytes * 4 / 3); }; CubeMap.prototype.isDestroyed = function() { diff --git a/Source/Renderer/PixelDatatype.js b/Source/Renderer/PixelDatatype.js index 96358380f8c9..3df9feb84728 100644 --- a/Source/Renderer/PixelDatatype.js +++ b/Source/Renderer/PixelDatatype.js @@ -20,6 +20,29 @@ define([ UNSIGNED_SHORT_5_5_5_1 : WebGLConstants.UNSIGNED_SHORT_5_5_5_1, UNSIGNED_SHORT_5_6_5 : WebGLConstants.UNSIGNED_SHORT_5_6_5, + isPackedDatatype : function(pixelDatatype) { + return pixelDatatype === PixelDatatype.UNSIGNED_INT_24_8 || + pixelDatatype === PixelDatatype.UNSIGNED_SHORT_4_4_4_4 || + pixelDatatype === PixelDatatype.UNSIGNED_SHORT_5_5_5_1 || + pixelDatatype === PixelDatatype.UNSIGNED_SHORT_5_6_5; + }, + + sizeInBytes : function(pixelDatatype) { + switch (pixelDatatype) { + case PixelDatatype.UNSIGNED_BYTE: + return 1; + case PixelDatatype.UNSIGNED_SHORT: + case PixelDatatype.UNSIGNED_SHORT_4_4_4_4: + case PixelDatatype.UNSIGNED_SHORT_5_5_5_1: + case PixelDatatype.UNSIGNED_SHORT_5_6_5: + return 2; + case PixelDatatype.UNSIGNED_INT: + case PixelDatatype.FLOAT: + case PixelDatatype.UNSIGNED_INT_24_8: + return 4; + } + }, + validate : function(pixelDatatype) { return ((pixelDatatype === PixelDatatype.UNSIGNED_BYTE) || (pixelDatatype === PixelDatatype.UNSIGNED_SHORT) || diff --git a/Source/Renderer/Texture.js b/Source/Renderer/Texture.js index 242c91bfba4d..63560e7e1bfc 100644 --- a/Source/Renderer/Texture.js +++ b/Source/Renderer/Texture.js @@ -189,6 +189,13 @@ define([ } gl.bindTexture(textureTarget, null); + var sizeInBytes; + if (isCompressed) { + sizeInBytes = PixelFormat.compressedTextureSize(pixelFormat, width, height); + } else { + sizeInBytes = PixelFormat.textureSize(pixelFormat, pixelDatatype, width, height); + } + this._context = context; this._textureFilterAnisotropic = context._textureFilterAnisotropic; this._textureTarget = textureTarget; @@ -198,6 +205,7 @@ define([ this._width = width; this._height = height; this._dimensions = new Cartesian2(width, height); + this._sizeInBytes = sizeInBytes; this._preMultiplyAlpha = preMultiplyAlpha; this._flipY = flipY; this._sampler = undefined; @@ -380,6 +388,11 @@ define([ return this._height; } }, + sizeInBytes : { + get : function() { + return this._sizeInBytes; + } + }, _target : { get : function() { return this._textureTarget; @@ -527,6 +540,7 @@ define([ * @param {MipmapHint} [hint=MipmapHint.DONT_CARE] optional. * * @exception {DeveloperError} Cannot call generateMipmap when the texture pixel format is DEPTH_COMPONENT or DEPTH_STENCIL. + * @exception {DeveloperError} Cannot call generateMipmap when the texture pixel format is a compressed format. * @exception {DeveloperError} hint is invalid. * @exception {DeveloperError} This texture's width must be a power of two to call generateMipmap(). * @exception {DeveloperError} This texture's height must be a power of two to call generateMipmap(). @@ -561,6 +575,9 @@ define([ gl.bindTexture(target, this._texture); gl.generateMipmap(target); gl.bindTexture(target, null); + + // The mipmap adds approximately 1/3 of the original texture size + this._sizeInBytes = Math.floor(this._sizeInBytes * 4 / 3); }; Texture.prototype.isDestroyed = function() { diff --git a/Source/Scene/Model.js b/Source/Scene/Model.js index 98f23b653444..f652b6d05b65 100644 --- a/Source/Scene/Model.js +++ b/Source/Scene/Model.js @@ -976,6 +976,42 @@ define([ get : function() { return this._upAxis; } + }, + + /** + * Gets the model's vertex memory in bytes. This includes all vertex and index buffers. + * + * @private + */ + vertexMemoryInBytes : { + get : function() { + var memory = 0; + var buffers = this._rendererResources.buffers; + for (var id in buffers) { + if (buffers.hasOwnProperty(id)) { + memory += buffers[id].sizeInBytes; + } + } + return memory; + } + }, + + /** + * Gets the model's texture memory in bytes. + * + * @private + */ + textureMemoryInBytes : { + get : function() { + var memory = 0; + var textures = this._rendererResources.textures; + for (var id in textures) { + if (textures.hasOwnProperty(id)) { + memory += textures[id].sizeInBytes; + } + } + return memory; + } } }); diff --git a/Specs/Renderer/CubeMapSpec.js b/Specs/Renderer/CubeMapSpec.js index 6b4b7b6300d6..4e8671755e72 100644 --- a/Specs/Renderer/CubeMapSpec.js +++ b/Specs/Renderer/CubeMapSpec.js @@ -92,6 +92,7 @@ defineSuite([ var blueImage; var blueAlphaImage; var blueOverRedImage; + var red16x16Image; beforeAll(function() { context = createContext(); @@ -109,6 +110,9 @@ defineSuite([ promises.push(loadImage('./Data/Images/BlueOverRed.png').then(function(result) { blueOverRedImage = result; })); + promises.push(loadImage('./Data/Images/Red16x16.png').then(function(result) { + red16x16Image = result; + })); return when.all(promises); }); @@ -186,6 +190,16 @@ defineSuite([ expect(cubeMap.height).toEqual(16); }); + it('gets size in bytes', function() { + cubeMap = new CubeMap({ + context : context, + width : 16, + height : 16 + }); + + expect(cubeMap.sizeInBytes).toEqual(256 * 4 * 6); + }); + it('gets flip Y', function() { cubeMap = new CubeMap({ context : context, @@ -625,6 +639,24 @@ defineSuite([ }).contextToRender([0, 0, 255, 255]); }); + it('gets size in bytes for mipmap', function() { + cubeMap = new CubeMap({ + context : context, + source : { + positiveX : red16x16Image, + negativeX : red16x16Image, + positiveY : red16x16Image, + negativeY : red16x16Image, + positiveZ : red16x16Image, + negativeZ : red16x16Image + } + }); + cubeMap.generateMipmap(); + + // Allow for some leniency with the sizeInBytes approximation + expect(cubeMap.sizeInBytes).toEqualEpsilon((16*16 + 8*8 + 4*4 + 2*2 + 1) * 4 * 6, 10); + }); + it('destroys', function() { var c = new CubeMap({ context : context, diff --git a/Specs/Renderer/TextureSpec.js b/Specs/Renderer/TextureSpec.js index a6df8c418a05..4a7d143267fe 100644 --- a/Specs/Renderer/TextureSpec.js +++ b/Specs/Renderer/TextureSpec.js @@ -50,6 +50,7 @@ defineSuite([ var blueImage; var blueAlphaImage; var blueOverRedImage; + var red16x16Image; var greenDXTImage; var greenPVRImage; @@ -81,6 +82,9 @@ defineSuite([ promises.push(loadImage('./Data/Images/BlueOverRed.png').then(function(image) { blueOverRedImage = image; })); + promises.push(loadImage('./Data/Images/Red16x16.png').then(function(image) { + red16x16Image = image; + })); promises.push(loadKTX('./Data/Images/Green4x4DXT1.ktx').then(function(image) { greenDXTImage = image; })); @@ -121,8 +125,12 @@ defineSuite([ texture = Texture.fromFramebuffer({ context : context }); - expect(texture.width).toEqual(context.canvas.clientWidth); - expect(texture.height).toEqual(context.canvas.clientHeight); + + var expectedWidth = context.canvas.clientWidth; + var expectedHeight = context.canvas.clientHeight; + expect(texture.width).toEqual(expectedWidth); + expect(texture.height).toEqual(expectedHeight); + expect(texture.sizeInBytes).toEqual(expectedWidth * expectedHeight * 4); command.color = Color.WHITE; command.execute(context); @@ -158,6 +166,12 @@ defineSuite([ texture.copyFromFramebuffer(); + var expectedWidth = context.canvas.clientWidth; + var expectedHeight = context.canvas.clientHeight; + expect(texture.width).toEqual(expectedWidth); + expect(texture.height).toEqual(expectedHeight); + expect(texture.sizeInBytes).toEqual(expectedWidth * expectedHeight * 4); + // Clear to white command.color = Color.WHITE; command.execute(context); @@ -201,6 +215,8 @@ defineSuite([ } }); + expect(texture.sizeInBytes).toEqual(16); + expect({ context : context, fragmentShader : fs, @@ -224,6 +240,8 @@ defineSuite([ } }); + expect(texture.sizeInBytes).toBe(8); + expect({ context : context, fragmentShader : fs, @@ -246,6 +264,8 @@ defineSuite([ } }); + expect(texture.sizeInBytes).toBe(32); + expect({ context : context, fragmentShader : fs, @@ -268,6 +288,8 @@ defineSuite([ } }); + expect(texture.sizeInBytes).toBe(8); + expect({ context : context, fragmentShader : fs, @@ -358,6 +380,10 @@ defineSuite([ } }); + expect(texture.width).toEqual(1); + expect(texture.height).toEqual(1); + expect(texture.sizeInBytes).toEqual(4); + expect({ context : context, fragmentShader : fs, @@ -381,6 +407,10 @@ defineSuite([ arrayBufferView : bytes }); + expect(texture.width).toEqual(1); + expect(texture.height).toEqual(1); + expect(texture.sizeInBytes).toEqual(4); + expect({ context : context, fragmentShader : fs, @@ -449,19 +479,20 @@ defineSuite([ it('can generate mipmaps', function() { texture = new Texture({ context : context, - source : blueImage, + source : red16x16Image, pixelFormat : PixelFormat.RGBA, sampler : new Sampler({ minificationFilter : TextureMinificationFilter.NEAREST_MIPMAP_LINEAR }) }); texture.generateMipmap(); + expect(texture.sizeInBytes).toEqualEpsilon((16*16 + 8*8 + 4*4 + 2*2 + 1) * 4, 1); expect({ context : context, fragmentShader : fs, uniformMap : uniformMap - }).contextToRender([0, 0, 255, 255]); + }).contextToRender([255, 0, 0, 255]); }); it('can set a sampler property', function() { @@ -542,6 +573,32 @@ defineSuite([ expect(texture.dimensions).toEqual(new Cartesian2(64, 16)); }); + function expectTextureByteSize(width, height, pixelFormat, pixelDatatype, expectedSize) { + texture = new Texture({ + context : context, + width : width, + height : height, + pixelFormat : pixelFormat, + pixelDatatype : pixelDatatype + }); + expect(texture.sizeInBytes).toBe(expectedSize); + texture = texture && texture.destroy(); + } + + it('can get the size in bytes of a texture', function() { + // Depth textures + expectTextureByteSize(16, 16, PixelFormat.DEPTH_COMPONENT, PixelDatatype.UNSIGNED_SHORT, 256 * 2); + expectTextureByteSize(16, 16, PixelFormat.DEPTH_COMPONENT, PixelDatatype.UNSIGNED_INT, 256 * 4); + expectTextureByteSize(16, 16, PixelFormat.DEPTH_STENCIL, PixelDatatype.UNSIGNED_INT_24_8, 256 * 4); + + // Uncompressed formats + expectTextureByteSize(16, 16, PixelFormat.ALPHA, PixelDatatype.UNSIGNED_BYTE, 256); + expectTextureByteSize(16, 16, PixelFormat.RGB, PixelDatatype.UNSIGNED_BYTE, 256 * 4); + expectTextureByteSize(16, 16, PixelFormat.RGBA, PixelDatatype.UNSIGNED_BYTE, 256 * 4); + expectTextureByteSize(16, 16, PixelFormat.LUMINANCE, PixelDatatype.UNSIGNED_BYTE, 256); + expectTextureByteSize(16, 16, PixelFormat.LUMINANCE_ALPHA, PixelDatatype.UNSIGNED_BYTE, 256 * 2); + }); + it('can be destroyed', function() { var t = new Texture({ context : context, diff --git a/Specs/Scene/ModelSpec.js b/Specs/Scene/ModelSpec.js index 271643969cde..46bebd9b4d96 100644 --- a/Specs/Scene/ModelSpec.js +++ b/Specs/Scene/ModelSpec.js @@ -2258,6 +2258,14 @@ defineSuite([ }); }); + it('Gets memory usage', function() { + return loadModel(texturedBoxUrl).then(function(model) { + expect(model.vertexMemoryInBytes).toBe(840); + // Model is originally 211*211 but is scaled up to 256*256 to support its minification filter and then is mipmapped + expect(model.textureMemoryInBytes).toBe(Math.floor(256*256*4*(4/3))); + }); + }); + describe('height referenced model', function() { function createMockGlobe() { var globe = {