From 642b83a0af576f7a8038dd3deaf44792506a3296 Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Fri, 13 Feb 2026 15:06:51 +0000 Subject: [PATCH] Improve MiniStats VRAM reporting and graph grouping. Enable default VRAM total tracking with expandable VRAM subcategories, remove stacked graph rendering, and align CPU/GPU/main category coloring and example configurations. Co-authored-by: Cursor --- .../gaussian-splatting/flipbook.example.mjs | 10 +- .../lod-streaming.example.mjs | 10 +- .../src/examples/misc/mini-stats.example.mjs | 4 +- src/extras/mini-stats/graph.js | 13 +-- src/extras/mini-stats/mini-stats.js | 91 ++++++++++++++----- src/extras/mini-stats/render2d.js | 35 ++++--- src/extras/mini-stats/stats-timer.js | 5 +- src/framework/stats.js | 8 +- 8 files changed, 105 insertions(+), 71 deletions(-) diff --git a/examples/src/examples/gaussian-splatting/flipbook.example.mjs b/examples/src/examples/gaussian-splatting/flipbook.example.mjs index e13de67161a..a89920c7cc6 100644 --- a/examples/src/examples/gaussian-splatting/flipbook.example.mjs +++ b/examples/src/examples/gaussian-splatting/flipbook.example.mjs @@ -76,16 +76,8 @@ assetListLoader.load(() => { roomEntity.setLocalScale(30, 30, 30); app.root.addChild(roomEntity); - // Mini-Stats: add VRAM on top of default stats + // Mini-Stats with default options const msOptions = pc.MiniStats.getDefaultOptions(); - msOptions.stats.push({ - name: 'VRAM', - stats: ['vram.tex'], - decimalPlaces: 1, - multiplier: 1 / (1024 * 1024), - unitsName: 'MB', - watermark: 1024 - }); const miniStats = new pc.MiniStats(app, msOptions); // eslint-disable-line no-unused-vars // Create an Entity with a camera component diff --git a/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs b/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs index a916140955c..a8836692e14 100644 --- a/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs +++ b/examples/src/examples/gaussian-splatting/lod-streaming.example.mjs @@ -119,16 +119,8 @@ assetListLoader.load(() => { app.scene.skyboxMip = 1; app.scene.exposure = 1.5; - // Mini-Stats: add VRAM and gsplats on top of default stats + // Mini-Stats: add gsplats on top of default stats const msOptions = pc.MiniStats.getDefaultOptions(); - msOptions.stats.push({ - name: 'VRAM', - stats: ['vram.tex'], - decimalPlaces: 1, - multiplier: 1 / (1024 * 1024), - unitsName: 'MB', - watermark: 1024 - }); msOptions.stats.push({ name: 'GSplats', stats: ['frame.gsplats'], diff --git a/examples/src/examples/misc/mini-stats.example.mjs b/examples/src/examples/misc/mini-stats.example.mjs index 33303524ab8..7ab901de3d6 100644 --- a/examples/src/examples/misc/mini-stats.example.mjs +++ b/examples/src/examples/misc/mini-stats.example.mjs @@ -97,10 +97,10 @@ options.stats = [ unitsName: 'ms' }, - // used VRAM, displayed using 2 colors - red for textures, green for geometry + // used VRAM in MB { name: 'VRAM', - stats: ['vram.tex', 'vram.geom'], + stats: ['vram.totalUsed'], decimalPlaces: 1, multiplier: 1 / (1024 * 1024), unitsName: 'MB', diff --git a/src/extras/mini-stats/graph.js b/src/extras/mini-stats/graph.js index 571294b9070..fe8a433848c 100644 --- a/src/extras/mini-stats/graph.js +++ b/src/extras/mini-stats/graph.js @@ -44,7 +44,7 @@ class Graph { update(ms) { const timings = this.timer.timings; - // calculate stacked total + // calculate total const total = timings.reduce((a, v) => a + v, 0); // update averages and max @@ -63,14 +63,11 @@ class Graph { } if (this.enabled) { - // update timings - let value = 0; + // update total timing sample const range = 1.5 * this.watermark; - for (let i = 0; i < timings.length; ++i) { - // scale the value into the range - value += Math.floor(timings[i] / range * 255); - this.sample[i] = value; - } + this.sample[0] = Math.floor(total / range * 255); + this.sample[1] = 0; + this.sample[2] = 0; // .a store watermark this.sample[3] = this.watermark / range * 255; diff --git a/src/extras/mini-stats/mini-stats.js b/src/extras/mini-stats/mini-stats.js index fbfe25a32f9..41fb77f3daa 100644 --- a/src/extras/mini-stats/mini-stats.js +++ b/src/extras/mini-stats/mini-stats.js @@ -68,6 +68,8 @@ const delayedStartStats = new Set([ * graphs. Defaults to 1. * @property {number} [cpuTimingMinSize] - Minimum size index at which to show CPU sub-timing * graphs (script, anim, physics, render). Defaults to 1. + * @property {number} [vramTimingMinSize] - Minimum size index at which to show VRAM subcategory + * graphs. Defaults to 1. */ /** @@ -118,10 +120,11 @@ class MiniStats { this.wordAtlas = new WordAtlas(device, words); this._activeSizeIndex = options.startSizeIndex; - // if GPU pass tracking or CPU timing is enabled, use the last width for medium/large sizes + // if GPU pass tracking, CPU timing or VRAM detail is enabled, use the last width for medium/large sizes const gpuTimingMinSize = options.gpuTimingMinSize ?? 1; const cpuTimingMinSize = options.cpuTimingMinSize ?? 1; - if (gpuTimingMinSize < this.sizes.length || cpuTimingMinSize < this.sizes.length) { + const vramTimingMinSize = options.vramTimingMinSize ?? 1; + if (gpuTimingMinSize < this.sizes.length || cpuTimingMinSize < this.sizes.length || vramTimingMinSize < this.sizes.length) { const lastWidth = this.sizes[this.sizes.length - 1].width; for (let i = 1; i < this.sizes.length - 1; i++) { this.sizes[i].width = lastWidth; @@ -177,6 +180,10 @@ class MiniStats { this.cpuTimingMinSize = cpuTimingMinSize; this.cpuGraphs = new Map(); // Map + // VRAM subcategory tracking + this.vramTimingMinSize = vramTimingMinSize; + this.vramGraphs = new Map(); // Map + this.frameIndex = 0; this.textRefreshRate = options.textRefreshRate; @@ -197,6 +204,8 @@ class MiniStats { this.graphs.forEach(graph => graph.destroy()); this.gpuPassGraphs.clear(); + this.cpuGraphs.clear(); + this.vramGraphs.clear(); this.wordAtlas.destroy(); this.texture.destroy(); this.div.remove(); @@ -210,6 +219,7 @@ class MiniStats { * - GPU utilization * - Overall frame time * - Draw call count + * - Total VRAM usage * * @returns {object} The default options for MiniStats. * @example @@ -267,6 +277,16 @@ class MiniStats { name: 'DrawCalls', stats: ['drawCalls.total'], watermark: 1000 + }, + + // used VRAM in MB + { + name: 'VRAM', + stats: ['vram.totalUsed'], + decimalPlaces: 1, + multiplier: 1 / (1024 * 1024), + unitsName: 'MB', + watermark: 1024 } ], @@ -274,7 +294,10 @@ class MiniStats { gpuTimingMinSize: 1, // minimum size index to show CPU sub-timing graphs - cpuTimingMinSize: 1 + cpuTimingMinSize: 1, + + // minimum size index to show VRAM subcategory graphs + vramTimingMinSize: 1 }; } @@ -306,9 +329,9 @@ class MiniStats { } this.gpuPassGraphs.clear(); - // reset main GPU graph to default background color + // keep main GPU graph in GPU color group const gpuGraph = this.graphs.find(g => g.name === 'GPU'); - if (gpuGraph) gpuGraph.graphType = 0.0; + if (gpuGraph) gpuGraph.graphType = 0.33; } // delete CPU sub-timing graphs when switching below threshold @@ -323,9 +346,9 @@ class MiniStats { } this.cpuGraphs.clear(); - // reset main CPU graph to default background color + // keep main CPU graph in CPU color group const cpuGraph = this.graphs.find(g => g.name === 'CPU'); - if (cpuGraph) cpuGraph.graphType = 0.0; + if (cpuGraph) cpuGraph.graphType = 0.66; } } @@ -407,20 +430,37 @@ class MiniStats { initGraphs(app, device, options) { this.graphs = []; + // Add VRAM first so it appears at the bottom in the compact stacked view. + // Graphs are rendered bottom-to-top. + if (options.stats) { + options.stats.forEach((entry) => { + if (entry.name === 'VRAM') { + const timer = new StatsTimer(app, entry.stats, entry.decimalPlaces, entry.unitsName, entry.multiplier); + const graph = new Graph(entry.name, app, entry.watermark, options.textRefreshRate, timer); + this.graphs.push(graph); + } + }); + } + if (options.cpu.enabled) { const timer = new CpuTimer(app); const graph = new Graph('CPU', app, options.cpu.watermark, options.textRefreshRate, timer); + graph.graphType = 0.66; this.graphs.push(graph); } if (options.gpu.enabled) { const timer = new GpuTimer(device); const graph = new Graph('GPU', app, options.gpu.watermark, options.textRefreshRate, timer); + graph.graphType = 0.33; this.graphs.push(graph); } if (options.stats) { options.stats.forEach((entry) => { + if (entry.name === 'VRAM') { + return; + } const timer = new StatsTimer(app, entry.stats, entry.decimalPlaces, entry.unitsName, entry.multiplier); const graph = new Graph(entry.name, app, entry.watermark, options.textRefreshRate, timer); this.graphs.push(graph); @@ -590,6 +630,7 @@ class MiniStats { // scan for new sub-stats const statsEntries = (stats instanceof Map) ? stats : Object.entries(stats); + const mainGraph = this.graphs.find(g => g.name === mainGraphName); for (const [statName, timing] of statsEntries) { if (!subGraphs.has(statName)) { // Skip creating graph for auto-hide stats with zero timing @@ -607,11 +648,15 @@ class MiniStats { } const graphName = ` ${displayName}`; // indent with 2 spaces - // initial watermark (will be synced to main graph) - const watermark = 10.0; + // use main graph watermark when available + const watermark = mainGraph?.watermark ?? 10.0; + + const decimalPlaces = 1; + const unitsName = statPathPrefix === 'vram' ? 'MB' : 'ms'; + const multiplier = statPathPrefix === 'vram' ? 1 / (1024 * 1024) : 1; const statPath = `${statPathPrefix}.${statName}`; - const timer = new StatsTimer(this.app, [statPath], 1, 'ms', 1); + const timer = new StatsTimer(this.app, [statPath], decimalPlaces, unitsName, multiplier); const graph = new Graph(graphName, this.app, watermark, this.textRefreshRate, timer); // Set graph type for background tinting @@ -656,23 +701,10 @@ class MiniStats { } // sync all sub-stat watermarks to match main graph - const mainGraph = this.graphs.find(g => g.name === mainGraphName); if (mainGraph) { for (const statData of subGraphs.values()) { statData.graph.watermark = mainGraph.watermark; } - - // set main graph background color to match sub-graphs when they exist - if (subGraphs.size > 0) { - if (statPathPrefix === 'gpu') { - mainGraph.graphType = 0.33; // Match GPU sub-graphs - } else if (statPathPrefix === 'frame') { - mainGraph.graphType = 0.66; // Match CPU sub-graphs - } - } else { - // reset to default background when no sub-graphs - mainGraph.graphType = 0.0; - } } } @@ -757,6 +789,19 @@ class MiniStats { }; this.updateSubStats(this.cpuGraphs, 'CPU', cpuStats, 'frame', 240); } + + // Update VRAM subcategory graphs when size index meets threshold + if (this._activeSizeIndex >= this.vramTimingMinSize) { + const vram = this.app.stats.vram; + const vramStats = { + tex: vram.tex, + geom: vram.geom + }; + if (this.device.isWebGPU) { + vramStats.buffers = vram.buffers; + } + this.updateSubStats(this.vramGraphs, 'VRAM', vramStats, 'vram', 0); + } } this.frameIndex++; diff --git a/src/extras/mini-stats/render2d.js b/src/extras/mini-stats/render2d.js index 740c6d3ca35..f392e85adff 100644 --- a/src/extras/mini-stats/render2d.js +++ b/src/extras/mini-stats/render2d.js @@ -21,9 +21,9 @@ import { VertexFormat } from '../../platform/graphics/vertex-format.js'; import { ShaderMaterial } from '../../scene/materials/shader-material.js'; // Graph colors for MiniStats -const graphColorRed = '1.0, 0.412, 0.380'; // Pastel Red -const graphColorGreen = '0.467, 0.867, 0.467'; // Pastel Green -const graphColorBlue = '0.424, 0.627, 0.863'; // Little Boy Blue +const graphColorDefault = '1.0, 0.412, 0.380'; // Pastel Red +const graphColorGpu = '0.467, 0.867, 0.467'; // Pastel Green +const graphColorCpu = '0.424, 0.627, 0.863'; // Little Boy Blue // Background colors for MiniStats graphs const mainBackgroundColor = '0.0, 0.0, 0.0'; @@ -62,8 +62,7 @@ const vertexShaderWGSL = /* wgsl */ ` // this fragment shader renders the bits required for text and graphs. The text is identified // in the texture by white color. The graph data is specified as a single row of pixels -// where the R channel denotes the height of the 1st graph and the G channel the height -// of the second graph and B channel the height of the last graph +// where the R channel denotes the graph height const fragmentShaderGLSL = /* glsl */ ` varying vec4 uv0; varying float wordFlag; @@ -73,15 +72,18 @@ const fragmentShaderGLSL = /* glsl */ ` uniform sampler2D wordsTex; void main (void) { + vec3 graphColor = vec3(${graphColorDefault}); + if (wordFlag > 0.5) { + graphColor = vec3(${graphColorCpu}); + } else if (wordFlag > 0.2) { + graphColor = vec3(${graphColorGpu}); + } + vec4 graphSample = texture2D(graphTex, uv0.xy); vec4 graph; if (uv0.w < graphSample.r) - graph = vec4(${graphColorRed}, 1.0); - else if (uv0.w < graphSample.g) - graph = vec4(${graphColorGreen}, 1.0); - else if (uv0.w < graphSample.b) - graph = vec4(${graphColorBlue}, 1.0); + graph = vec4(graphColor, 1.0); else { vec3 bgColor = vec3(${mainBackgroundColor}); if (wordFlag > 0.5) { @@ -117,15 +119,18 @@ const fragmentShaderWGSL = /* wgsl */ ` @fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput { var uv0: vec4f = input.uv0; + var graphColor: vec3f = vec3f(${graphColorDefault}); + if (input.wordFlag > 0.5) { + graphColor = vec3f(${graphColorCpu}); + } else if (input.wordFlag > 0.2) { + graphColor = vec3f(${graphColorGpu}); + } + var graphSample: vec4f = textureSample(graphTex, graphTex_sampler, uv0.xy); var graph: vec4f; if (uv0.w < graphSample.r) { - graph = vec4f(${graphColorRed}, 1.0); - } else if (uv0.w < graphSample.g) { - graph = vec4f(${graphColorGreen}, 1.0); - } else if (uv0.w < graphSample.b) { - graph = vec4f(${graphColorBlue}, 1.0); + graph = vec4f(graphColor, 1.0); } else { var bgColor: vec3f = vec3f(${mainBackgroundColor}); if (input.wordFlag > 0.5) { diff --git a/src/extras/mini-stats/stats-timer.js b/src/extras/mini-stats/stats-timer.js index b765ca3802f..c355eeb6c49 100644 --- a/src/extras/mini-stats/stats-timer.js +++ b/src/extras/mini-stats/stats-timer.js @@ -4,11 +4,8 @@ class StatsTimer { this.app = app; this.values = []; - // supporting up to 3 stats + // support one or more stats and accumulate them in the graph total this.statNames = statNames; - if (this.statNames.length > 3) { - this.statNames.length = 3; - } this.unitsName = unitsName; this.decimalPlaces = decimalPlaces; diff --git a/src/framework/stats.js b/src/framework/stats.js index 6c9c9e12493..3c1226c81c6 100644 --- a/src/framework/stats.js +++ b/src/framework/stats.js @@ -88,7 +88,7 @@ class ApplicationStats { Object.defineProperty(this.vram, 'totalUsed', { get: function () { - return this.tex + this.vb + this.ib; + return this.tex + this.vb + this.ib + this.ub + this.sb; } }); @@ -97,6 +97,12 @@ class ApplicationStats { return this.vb + this.ib; } }); + + Object.defineProperty(this.vram, 'buffers', { + get: function () { + return this.ub + this.sb; + } + }); } get scene() {