Răsfoiți Sursa

Inspector: Add Memory tab and improve `Renderer.info` (#33097)

sunag 1 lună în urmă
părinte
comite
f122d43c48

+ 7 - 0
examples/jsm/inspector/Inspector.js

@@ -2,6 +2,7 @@
 import { RendererInspector } from './RendererInspector.js';
 import { Profiler } from './ui/Profiler.js';
 import { Performance } from './tabs/Performance.js';
+import { Memory } from './tabs/Memory.js';
 import { Console } from './tabs/Console.js';
 import { Parameters } from './tabs/Parameters.js';
 import { Settings } from './tabs/Settings.js';
@@ -58,6 +59,9 @@ class Inspector extends RendererInspector {
 		const performance = new Performance();
 		profiler.addTab( performance );
 
+		const memory = new Memory();
+		profiler.addTab( memory );
+
 		const timeline = new Timeline();
 		profiler.addTab( timeline );
 
@@ -79,6 +83,7 @@ class Inspector extends RendererInspector {
 		this.canvasNodes = new Map();
 		this.profiler = profiler;
 		this.performance = performance;
+		this.memory = memory;
 		this.console = consoleTab;
 		this.parameters = parameters;
 		this.viewer = viewer;
@@ -460,12 +465,14 @@ class Inspector extends RendererInspector {
 			setText( 'fps-counter', this.fps.toFixed() );
 
 			this.performance.updateText( this, frame );
+			this.memory.updateText( this );
 
 		}
 
 		if ( this.displayCycle.graph.needsUpdate ) {
 
 			this.performance.updateGraph( this, frame );
+			this.memory.updateGraph( this );
 
 		}
 

+ 121 - 0
examples/jsm/inspector/tabs/Memory.js

@@ -0,0 +1,121 @@
+import { Tab } from '../ui/Tab.js';
+import { List } from '../ui/List.js';
+import { Graph } from '../ui/Graph.js';
+import { Item } from '../ui/Item.js';
+import { createValueSpan, setText, formatBytes } from '../ui/utils.js';
+
+class Memory extends Tab {
+
+	constructor( options = {} ) {
+
+		super( 'Memory', options );
+
+		const memoryList = new List( 'Name', 'Count', 'Size' );
+		memoryList.setGridStyle( 'minmax(200px, 2fr) 60px 100px' );
+		memoryList.domElement.style.minWidth = '300px';
+
+		const scrollWrapper = document.createElement( 'div' );
+		scrollWrapper.className = 'list-scroll-wrapper';
+		scrollWrapper.appendChild( memoryList.domElement );
+		this.content.appendChild( scrollWrapper );
+
+		// graph
+
+		const graphContainer = document.createElement( 'div' );
+		graphContainer.className = 'graph-container';
+
+		const graph = new Graph();
+		graph.addLine( 'total', 'var( --color-yellow )' );
+		graphContainer.append( graph.domElement );
+
+		// stats
+
+		const graphStats = new Item( 'Graph Stats', '', '' );
+		memoryList.add( graphStats );
+
+		const graphItem = new Item( graphContainer );
+		graphItem.itemRow.childNodes[ 0 ].style.gridColumn = '1 / -1';
+		graphStats.add( graphItem );
+
+		// info
+
+		this.memoryStats = new Item( 'Renderer Info', '', createValueSpan() );
+		this.memoryStats.domElement.firstChild.classList.add( 'no-hover' );
+		memoryList.add( this.memoryStats );
+
+		this.attributes = new Item( 'Attributes', createValueSpan(), createValueSpan() );
+		this.memoryStats.add( this.attributes );
+
+		this.geometries = new Item( 'Geometries', createValueSpan(), 'N/A' );
+		this.memoryStats.add( this.geometries );
+
+		this.indexAttributes = new Item( 'Index Attributes', createValueSpan(), createValueSpan() );
+		this.memoryStats.add( this.indexAttributes );
+
+		this.indirectStorageAttributes = new Item( 'Indirect Storage Attributes', createValueSpan(), createValueSpan() );
+		this.memoryStats.add( this.indirectStorageAttributes );
+
+		this.programs = new Item( 'Programs', createValueSpan(), 'N/A' );
+		this.memoryStats.add( this.programs );
+
+		this.renderTargets = new Item( 'Render Targets', createValueSpan(), 'N/A' );
+		this.memoryStats.add( this.renderTargets );
+
+		this.storageAttributes = new Item( 'Storage Attributes', createValueSpan(), createValueSpan() );
+		this.memoryStats.add( this.storageAttributes );
+
+		this.textures = new Item( 'Textures', createValueSpan(), createValueSpan() );
+		this.memoryStats.add( this.textures );
+
+		this.graph = graph;
+
+	}
+
+	updateGraph( inspector ) {
+
+		const renderer = inspector.getRenderer();
+		if ( ! renderer ) return;
+
+		const memory = renderer.info.memory;
+
+		this.graph.addPoint( 'total', memory.total );
+		
+		if ( this.graph.limit === 0 ) this.graph.limit = 1;
+
+		this.graph.update();
+
+	}
+
+	updateText( inspector ) {
+
+		const renderer = inspector.getRenderer();
+		if ( ! renderer ) return;
+
+		const memory = renderer.info.memory;
+
+		setText( this.memoryStats.data[ 2 ], formatBytes( memory.total ) );
+
+		setText( this.attributes.data[ 1 ], memory.attributes.toString() );
+		setText( this.attributes.data[ 2 ], formatBytes( memory.attributesSize ) );
+		setText( this.geometries.data[ 1 ], memory.geometries.toString() );
+		
+		setText( this.indexAttributes.data[ 1 ], memory.indexAttributes.toString() );
+		setText( this.indexAttributes.data[ 2 ], formatBytes( memory.indexAttributesSize ) );
+		
+		setText( this.indirectStorageAttributes.data[ 1 ], memory.indirectStorageAttributes.toString() );
+		setText( this.indirectStorageAttributes.data[ 2 ], formatBytes( memory.indirectStorageAttributesSize ) );
+
+		setText( this.programs.data[ 1 ], memory.programs.toString() );
+		
+		setText( this.renderTargets.data[ 1 ], memory.renderTargets.toString() );
+		
+		setText( this.storageAttributes.data[ 1 ], memory.storageAttributes.toString() );
+		setText( this.storageAttributes.data[ 2 ], formatBytes( memory.storageAttributesSize ) );
+		setText( this.textures.data[ 1 ], memory.textures.toString() );
+		setText( this.textures.data[ 2 ], formatBytes( memory.texturesSize ) );
+
+	}
+
+}
+
+export { Memory };

+ 13 - 0
examples/jsm/inspector/ui/utils.js

@@ -54,3 +54,16 @@ export function splitCamelCase( str ) {
 	return str.replace( /([a-z0-9])([A-Z])/g, '$1 $2' ).trim();
 
 }
+
+export function formatBytes( bytes, decimals = 2 ) {
+
+	if ( bytes === 0 ) return '0 Bytes';
+
+	const k = 1024;
+	const dm = decimals < 0 ? 0 : decimals;
+	const sizes = [ 'Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB' ];
+	const i = Math.floor( Math.log( bytes ) / Math.log( k ) );
+
+	return parseFloat( ( bytes / Math.pow( k, i ) ).toFixed( dm ) ) + ' ' + sizes[ i ];
+
+}

BIN
examples/screenshots/webgpu_compute_particles_rain.jpg


BIN
examples/screenshots/webgpu_compute_particles_snow.jpg


BIN
examples/screenshots/webgpu_instancing_morph.jpg


BIN
examples/screenshots/webgpu_mesh_batch.jpg


BIN
examples/screenshots/webgpu_performance_renderbundle.jpg


BIN
examples/screenshots/webgpu_postprocessing.jpg


BIN
examples/screenshots/webgpu_postprocessing_afterimage.jpg


BIN
examples/screenshots/webgpu_postprocessing_outline.jpg


BIN
examples/screenshots/webgpu_postprocessing_ssaa.jpg


BIN
examples/screenshots/webgpu_sandbox.jpg


BIN
examples/screenshots/webgpu_shadowmap_array.jpg


BIN
examples/screenshots/webgpu_shadowmap_csm.jpg


BIN
examples/screenshots/webgpu_test_memory.jpg


BIN
examples/screenshots/webgpu_tsl_compute_attractors_particles.jpg


BIN
examples/screenshots/webgpu_tsl_galaxy.jpg


+ 21 - 0
examples/webgpu_loader_gltf.html

@@ -132,6 +132,27 @@
 
 				if ( currentModel ) {
 
+					currentModel.traverse( ( model ) => {
+
+						if ( model.isMesh ) {
+
+							model.geometry.dispose();
+							model.material.dispose();
+
+							for ( const value of Object.values( model.material ) ) {
+
+								if ( value && value.isTexture ) {
+
+									value.dispose();
+
+								}
+
+							}
+
+						}
+
+					} );
+
 					scene.remove( currentModel );
 					currentModel = null;
 

+ 15 - 1
src/renderers/common/Attributes.js

@@ -15,8 +15,9 @@ class Attributes extends DataMap {
 	 * Constructs a new attribute management component.
 	 *
 	 * @param {Backend} backend - The renderer's backend.
+	 * @param {Info} info - Renderer component for managing metrics and monitoring data.
 	 */
-	constructor( backend ) {
+	constructor( backend, info ) {
 
 		super();
 
@@ -27,6 +28,13 @@ class Attributes extends DataMap {
 		 */
 		this.backend = backend;
 
+		/**
+		 * Renderer component for managing metrics and monitoring data.
+		 *
+		 * @type {Info}
+		 */
+		this.info = info;
+
 	}
 
 	/**
@@ -43,6 +51,8 @@ class Attributes extends DataMap {
 
 			this.backend.destroyAttribute( attribute );
 
+			this.info.destroyAttribute( attribute );
+
 		}
 
 		return attributeData;
@@ -65,18 +75,22 @@ class Attributes extends DataMap {
 			if ( type === AttributeType.VERTEX ) {
 
 				this.backend.createAttribute( attribute );
+				this.info.createAttribute( attribute );
 
 			} else if ( type === AttributeType.INDEX ) {
 
 				this.backend.createIndexAttribute( attribute );
+				this.info.createIndexAttribute( attribute );
 
 			} else if ( type === AttributeType.STORAGE ) {
 
 				this.backend.createStorageAttribute( attribute );
+				this.info.createStorageAttribute( attribute );
 
 			} else if ( type === AttributeType.INDIRECT ) {
 
 				this.backend.createIndirectStorageAttribute( attribute );
+				this.info.createIndirectStorageAttribute( attribute );
 
 			}
 

+ 261 - 4
src/renderers/common/Info.js

@@ -1,4 +1,12 @@
 import { error } from '../../utils.js';
+import {
+	ByteType, UnsignedByteType, ShortType, UnsignedShortType, HalfFloatType,
+	IntType, UnsignedIntType, FloatType,
+	AlphaFormat, RedFormat, RedIntegerFormat, DepthFormat, DepthStencilFormat,
+	RGBFormat,
+	UnsignedShort4444Type, UnsignedShort5551Type,
+	UnsignedInt248Type, UnsignedInt5999Type, UnsignedInt101111Type
+} from '../../constants.js';
 
 /**
  * This renderer module provides a series of statistical information
@@ -87,13 +95,45 @@ class Info {
 		 * @type {Object}
 		 * @readonly
 		 * @property {number} geometries - The number of active geometries.
-		 * @property {number} frameCalls - The number of active textures.
+		 * @property {number} textures - The number of active textures.
+		 * @property {number} attributes - The number of active attributes.
+		 * @property {number} indexAttributes - The number of active index attributes.
+		 * @property {number} storageAttributes - The number of active storage attributes.
+		 * @property {number} indirectStorageAttributes - The number of active indirect storage attributes.
+		 * @property {number} programs - The number of active programs.
+		 * @property {number} renderTargets - The number of active renderTargets.
+		 * @property {number} total - The total memory size in bytes.
+		 * @property {number} texturesSize - The memory size of active textures in bytes.
+		 * @property {number} attributesSize - The memory size of active attributes in bytes.
+		 * @property {number} indexAttributesSize - The memory size of active index attributes in bytes.
+		 * @property {number} storageAttributesSize - The memory size of active storage attributes in bytes.
+		 * @property {number} indirectStorageAttributesSize - The memory size of active indirect storage attributes in bytes.
 		 */
 		this.memory = {
 			geometries: 0,
-			textures: 0
+			textures: 0,
+			attributes: 0,
+			indexAttributes: 0,
+			storageAttributes: 0,
+			indirectStorageAttributes: 0,
+			programs: 0,
+			renderTargets: 0,
+			total: 0,
+			texturesSize: 0,
+			attributesSize: 0,
+			indexAttributesSize: 0,
+			storageAttributesSize: 0,
+			indirectStorageAttributesSize: 0
 		};
 
+		/**
+		 * Map for storing calculated byte sizes of tracked objects.
+		 *
+		 * @type {Map<Object, number>}
+		 * @private
+		 */
+		this.memoryMap = new Map();
+
 	}
 
 	/**
@@ -161,8 +201,225 @@ class Info {
 
 		this.render.timestamp = 0;
 		this.compute.timestamp = 0;
-		this.memory.geometries = 0;
-		this.memory.textures = 0;
+
+		for ( const prop in this.memory ) {
+
+			this.memory[ prop ] = 0;
+
+		}
+
+		this.memoryMap.clear();
+
+	}
+
+	/**
+	 * Tracks texture memory explicitly, updating counts and byte tracking.
+	 *
+	 * @param {Texture} texture
+	 */
+	createTexture( texture ) {
+
+		const size = this._getTextureMemorySize( texture );
+		this.memoryMap.set( texture, size );
+
+		this.memory.textures ++;
+		this.memory.total += size;
+		this.memory.texturesSize += size;
+
+	}
+
+	/**
+	 * Tracks texture memory explicitly, updating counts and byte tracking.
+	 *
+	 * @param {Texture} texture
+	 */
+	destroyTexture( texture ) {
+
+		const size = this.memoryMap.get( texture ) || 0;
+		this.memoryMap.delete( texture );
+
+		this.memory.textures --;
+		this.memory.total -= size;
+		this.memory.texturesSize -= size;
+
+	}
+
+	/**
+	 * Tracks attribute memory explicitly, updating counts and byte tracking.
+	 *
+	 * @param {BufferAttribute} attribute
+	 * @param {string} type - type of attribute
+	 * @private
+	 */
+	_createAttribute( attribute, type ) {
+
+		const size = this._getAttributeMemorySize( attribute );
+		this.memoryMap.set( attribute, { size, type } );
+
+		this.memory[ type ] ++;
+		this.memory.total += size;
+		this.memory[ type + 'Size' ] += size;
+
+	}
+
+	/**
+	 * Tracks a regular attribute memory explicitly.
+	 *
+	 * @param {BufferAttribute} attribute - The attribute to track.
+	 */
+	createAttribute( attribute ) {
+
+		this._createAttribute( attribute, 'attributes' );
+
+	}
+
+	/**
+	 * Tracks an index attribute memory explicitly.
+	 *
+	 * @param {BufferAttribute} attribute - The index attribute to track.
+	 */
+	createIndexAttribute( attribute ) {
+
+		this._createAttribute( attribute, 'indexAttributes' );
+
+	}
+
+	/**
+	 * Tracks a storage attribute memory explicitly.
+	 *
+	 * @param {BufferAttribute} attribute - The storage attribute to track.
+	 */
+	createStorageAttribute( attribute ) {
+
+		this._createAttribute( attribute, 'storageAttributes' );
+
+	}
+
+	/**
+	 * Tracks an indirect storage attribute memory explicitly.
+	 *
+	 * @param {BufferAttribute} attribute - The indirect storage attribute to track.
+	 */
+	createIndirectStorageAttribute( attribute ) {
+
+		this._createAttribute( attribute, 'indirectStorageAttributes' );
+
+	}
+
+	/**
+	 * Tracks attribute memory explicitly, updating counts and byte tracking.
+	 *
+	 * @param {BufferAttribute} attribute
+	 */
+	destroyAttribute( attribute ) {
+
+		const data = this.memoryMap.get( attribute );
+
+		if ( data ) {
+
+			this.memoryMap.delete( attribute );
+
+			this.memory[ data.type ] --;
+			this.memory.total -= data.size;
+			this.memory[ data.type + 'Size' ] -= data.size;
+
+		}
+
+	}
+
+	/**
+	 * Calculates the memory size of a texture in bytes.
+	 *
+	 * @param {Texture} texture - The texture to calculate the size for.
+	 * @return {number} The calculated size in bytes.
+	 * @private
+	 */
+	_getTextureMemorySize( texture ) {
+
+		if ( texture.isCompressedTexture ) {
+
+			return 1; // Fallback estimate since exact format decompressed isn't readily available without format maps
+
+		}
+
+		let bytesPerChannel = 1;
+
+		if ( texture.type === ByteType || texture.type === UnsignedByteType ) bytesPerChannel = 1;
+		else if ( texture.type === ShortType || texture.type === UnsignedShortType || texture.type === HalfFloatType ) bytesPerChannel = 2;
+		else if ( texture.type === IntType || texture.type === UnsignedIntType || texture.type === FloatType ) bytesPerChannel = 4;
+
+		let channels = 4; // RGBA default
+
+		if ( texture.format === AlphaFormat || texture.format === RedFormat || texture.format === RedIntegerFormat || texture.format === DepthFormat || texture.format === DepthStencilFormat ) channels = 1;
+		else if ( texture.format === RGBFormat ) channels = 3;
+
+		let bytesPerPixel = bytesPerChannel * channels;
+
+		// Packed overrides
+		if ( texture.type === UnsignedShort4444Type || texture.type === UnsignedShort5551Type ) bytesPerPixel = 2;
+		else if ( texture.type === UnsignedInt248Type || texture.type === UnsignedInt5999Type || texture.type === UnsignedInt101111Type ) bytesPerPixel = 4;
+
+		const width = texture.width || 1;
+		const height = texture.height || 1;
+		const depth = texture.isCubeTexture ? 6 : ( texture.depth || 1 );
+
+		let size = width * height * depth * bytesPerPixel;
+		const mipmaps = texture.mipmaps;
+
+		if ( mipmaps && mipmaps.length > 0 ) {
+
+			let mipmapSize = 0;
+			for ( let i = 0; i < mipmaps.length; i ++ ) {
+
+				const mipmap = mipmaps[ i ];
+				if ( mipmap.data ) {
+
+					mipmapSize += mipmap.data.byteLength;
+
+				} else {
+
+					const mipWidth = mipmap.width || Math.max( 1, width >> i );
+					const mipHeight = mipmap.height || Math.max( 1, height >> i );
+					mipmapSize += mipWidth * mipHeight * depth * bytesPerPixel;
+
+				}
+
+			}
+
+			size += mipmapSize;
+
+		} else if ( texture.generateMipmaps ) {
+
+			size = size * 1.333; // MiP chain approximation
+
+		}
+
+		return Math.round( size );
+
+	}
+
+	/**
+	 * Calculates the memory size of an attribute in bytes.
+	 *
+	 * @param {BufferAttribute} attribute - The attribute to calculate the size for.
+	 * @return {number} The calculated size in bytes.
+	 * @private
+	 */
+	_getAttributeMemorySize( attribute ) {
+
+		if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;
+
+		if ( attribute.array ) {
+
+			return attribute.array.byteLength;
+
+		} else if ( attribute.count && attribute.itemSize ) {
+
+			return attribute.count * attribute.itemSize * 4; // Assume Float32
+
+		}
+
+		return 0;
 
 	}
 

+ 15 - 1
src/renderers/common/Pipelines.js

@@ -16,8 +16,9 @@ class Pipelines extends DataMap {
 	 *
 	 * @param {Backend} backend - The renderer's backend.
 	 * @param {NodeManager} nodes - Renderer component for managing nodes related logic.
+	 * @param {Info} info - Renderer component for managing metrics and monitoring data.
 	 */
-	constructor( backend, nodes ) {
+	constructor( backend, nodes, info ) {
 
 		super();
 
@@ -35,6 +36,13 @@ class Pipelines extends DataMap {
 		 */
 		this.nodes = nodes;
 
+		/**
+		 * Renderer component for managing metrics and monitoring data.
+		 *
+		 * @type {Info}
+		 */
+		this.info = info;
+
 		/**
 		 * A references to the bindings management component.
 		 * This reference will be set inside the `Bindings`
@@ -349,6 +357,8 @@ class Pipelines extends DataMap {
 
 			this.backend.createComputePipeline( pipeline, bindings );
 
+			this.info.memory.programs ++;
+
 		}
 
 		return pipeline;
@@ -388,6 +398,8 @@ class Pipelines extends DataMap {
 
 			this.backend.createRenderPipeline( renderObject, promises );
 
+			this.info.memory.programs ++;
+
 		}
 
 		return pipeline;
@@ -433,6 +445,8 @@ class Pipelines extends DataMap {
 
 		this.caches.delete( pipeline.cacheKey );
 
+		this.info.memory.programs --;
+
 	}
 
 	/**

+ 2 - 2
src/renderers/common/Renderer.js

@@ -787,11 +787,11 @@ class Renderer {
 
 			this._nodes = new NodeManager( this, backend );
 			this._animation = new Animation( this, this._nodes, this.info );
-			this._attributes = new Attributes( backend );
+			this._attributes = new Attributes( backend, this.info );
 			this._background = new Background( this, this._nodes );
 			this._geometries = new Geometries( this._attributes, this.info );
 			this._textures = new Textures( this, backend, this.info );
-			this._pipelines = new Pipelines( backend, this._nodes );
+			this._pipelines = new Pipelines( backend, this._nodes, this.info );
 			this._bindings = new Bindings( backend, this._nodes, this._textures, this._attributes, this._pipelines, this.info );
 			this._objects = new RenderObjects( this, this._nodes, this._geometries, this._pipelines, this._bindings, this.info );
 			this._renderLists = new RenderLists( this.lighting );

+ 6 - 2
src/renderers/common/Textures.js

@@ -162,6 +162,8 @@ class Textures extends DataMap {
 
 			renderTargetData.initialized = true;
 
+			this.info.memory.renderTargets ++;
+
 			// dispose
 
 			renderTargetData.onDispose = () => {
@@ -322,7 +324,7 @@ class Textures extends DataMap {
 
 			//
 
-			this.info.memory.textures ++;
+			this.info.createTexture( texture );
 
 			//
 
@@ -504,6 +506,8 @@ class Textures extends DataMap {
 			this.delete( renderTarget );
 			this.backend.delete( renderTarget );
 
+			this.info.memory.renderTargets --;
+
 		}
 
 	}
@@ -549,7 +553,7 @@ class Textures extends DataMap {
 
 			this.delete( texture );
 
-			this.info.memory.textures --;
+			this.info.destroyTexture( texture );
 
 		}
 

粤ICP备19079148号