Skip to content

Commit 39f49f5

Browse files
mvaligurskyMartin Valigurskycursoragent
authored
Improve MiniStats VRAM reporting and graph grouping. (playcanvas#8460)
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: Martin Valigursky <mvaligursky@snapchat.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent bdf2d94 commit 39f49f5

8 files changed

Lines changed: 105 additions & 71 deletions

File tree

examples/src/examples/gaussian-splatting/flipbook.example.mjs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,8 @@ assetListLoader.load(() => {
7676
roomEntity.setLocalScale(30, 30, 30);
7777
app.root.addChild(roomEntity);
7878

79-
// Mini-Stats: add VRAM on top of default stats
79+
// Mini-Stats with default options
8080
const msOptions = pc.MiniStats.getDefaultOptions();
81-
msOptions.stats.push({
82-
name: 'VRAM',
83-
stats: ['vram.tex'],
84-
decimalPlaces: 1,
85-
multiplier: 1 / (1024 * 1024),
86-
unitsName: 'MB',
87-
watermark: 1024
88-
});
8981
const miniStats = new pc.MiniStats(app, msOptions); // eslint-disable-line no-unused-vars
9082

9183
// Create an Entity with a camera component

examples/src/examples/gaussian-splatting/lod-streaming.example.mjs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,16 +119,8 @@ assetListLoader.load(() => {
119119
app.scene.skyboxMip = 1;
120120
app.scene.exposure = 1.5;
121121

122-
// Mini-Stats: add VRAM and gsplats on top of default stats
122+
// Mini-Stats: add gsplats on top of default stats
123123
const msOptions = pc.MiniStats.getDefaultOptions();
124-
msOptions.stats.push({
125-
name: 'VRAM',
126-
stats: ['vram.tex'],
127-
decimalPlaces: 1,
128-
multiplier: 1 / (1024 * 1024),
129-
unitsName: 'MB',
130-
watermark: 1024
131-
});
132124
msOptions.stats.push({
133125
name: 'GSplats',
134126
stats: ['frame.gsplats'],

examples/src/examples/misc/mini-stats.example.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@ options.stats = [
9797
unitsName: 'ms'
9898
},
9999

100-
// used VRAM, displayed using 2 colors - red for textures, green for geometry
100+
// used VRAM in MB
101101
{
102102
name: 'VRAM',
103-
stats: ['vram.tex', 'vram.geom'],
103+
stats: ['vram.totalUsed'],
104104
decimalPlaces: 1,
105105
multiplier: 1 / (1024 * 1024),
106106
unitsName: 'MB',

src/extras/mini-stats/graph.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class Graph {
4444
update(ms) {
4545
const timings = this.timer.timings;
4646

47-
// calculate stacked total
47+
// calculate total
4848
const total = timings.reduce((a, v) => a + v, 0);
4949

5050
// update averages and max
@@ -63,14 +63,11 @@ class Graph {
6363
}
6464

6565
if (this.enabled) {
66-
// update timings
67-
let value = 0;
66+
// update total timing sample
6867
const range = 1.5 * this.watermark;
69-
for (let i = 0; i < timings.length; ++i) {
70-
// scale the value into the range
71-
value += Math.floor(timings[i] / range * 255);
72-
this.sample[i] = value;
73-
}
68+
this.sample[0] = Math.floor(total / range * 255);
69+
this.sample[1] = 0;
70+
this.sample[2] = 0;
7471

7572
// .a store watermark
7673
this.sample[3] = this.watermark / range * 255;

src/extras/mini-stats/mini-stats.js

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ const delayedStartStats = new Set([
6868
* graphs. Defaults to 1.
6969
* @property {number} [cpuTimingMinSize] - Minimum size index at which to show CPU sub-timing
7070
* graphs (script, anim, physics, render). Defaults to 1.
71+
* @property {number} [vramTimingMinSize] - Minimum size index at which to show VRAM subcategory
72+
* graphs. Defaults to 1.
7173
*/
7274

7375
/**
@@ -118,10 +120,11 @@ class MiniStats {
118120
this.wordAtlas = new WordAtlas(device, words);
119121
this._activeSizeIndex = options.startSizeIndex;
120122

121-
// if GPU pass tracking or CPU timing is enabled, use the last width for medium/large sizes
123+
// if GPU pass tracking, CPU timing or VRAM detail is enabled, use the last width for medium/large sizes
122124
const gpuTimingMinSize = options.gpuTimingMinSize ?? 1;
123125
const cpuTimingMinSize = options.cpuTimingMinSize ?? 1;
124-
if (gpuTimingMinSize < this.sizes.length || cpuTimingMinSize < this.sizes.length) {
126+
const vramTimingMinSize = options.vramTimingMinSize ?? 1;
127+
if (gpuTimingMinSize < this.sizes.length || cpuTimingMinSize < this.sizes.length || vramTimingMinSize < this.sizes.length) {
125128
const lastWidth = this.sizes[this.sizes.length - 1].width;
126129
for (let i = 1; i < this.sizes.length - 1; i++) {
127130
this.sizes[i].width = lastWidth;
@@ -177,6 +180,10 @@ class MiniStats {
177180
this.cpuTimingMinSize = cpuTimingMinSize;
178181
this.cpuGraphs = new Map(); // Map<statName, { graph, lastNonZeroFrame }>
179182

183+
// VRAM subcategory tracking
184+
this.vramTimingMinSize = vramTimingMinSize;
185+
this.vramGraphs = new Map(); // Map<statName, { graph, lastNonZeroFrame }>
186+
180187
this.frameIndex = 0;
181188
this.textRefreshRate = options.textRefreshRate;
182189

@@ -197,6 +204,8 @@ class MiniStats {
197204

198205
this.graphs.forEach(graph => graph.destroy());
199206
this.gpuPassGraphs.clear();
207+
this.cpuGraphs.clear();
208+
this.vramGraphs.clear();
200209
this.wordAtlas.destroy();
201210
this.texture.destroy();
202211
this.div.remove();
@@ -210,6 +219,7 @@ class MiniStats {
210219
* - GPU utilization
211220
* - Overall frame time
212221
* - Draw call count
222+
* - Total VRAM usage
213223
*
214224
* @returns {object} The default options for MiniStats.
215225
* @example
@@ -267,14 +277,27 @@ class MiniStats {
267277
name: 'DrawCalls',
268278
stats: ['drawCalls.total'],
269279
watermark: 1000
280+
},
281+
282+
// used VRAM in MB
283+
{
284+
name: 'VRAM',
285+
stats: ['vram.totalUsed'],
286+
decimalPlaces: 1,
287+
multiplier: 1 / (1024 * 1024),
288+
unitsName: 'MB',
289+
watermark: 1024
270290
}
271291
],
272292

273293
// minimum size index to show GPU pass timing graphs
274294
gpuTimingMinSize: 1,
275295

276296
// minimum size index to show CPU sub-timing graphs
277-
cpuTimingMinSize: 1
297+
cpuTimingMinSize: 1,
298+
299+
// minimum size index to show VRAM subcategory graphs
300+
vramTimingMinSize: 1
278301
};
279302
}
280303

@@ -306,9 +329,9 @@ class MiniStats {
306329
}
307330
this.gpuPassGraphs.clear();
308331

309-
// reset main GPU graph to default background color
332+
// keep main GPU graph in GPU color group
310333
const gpuGraph = this.graphs.find(g => g.name === 'GPU');
311-
if (gpuGraph) gpuGraph.graphType = 0.0;
334+
if (gpuGraph) gpuGraph.graphType = 0.33;
312335
}
313336

314337
// delete CPU sub-timing graphs when switching below threshold
@@ -323,9 +346,9 @@ class MiniStats {
323346
}
324347
this.cpuGraphs.clear();
325348

326-
// reset main CPU graph to default background color
349+
// keep main CPU graph in CPU color group
327350
const cpuGraph = this.graphs.find(g => g.name === 'CPU');
328-
if (cpuGraph) cpuGraph.graphType = 0.0;
351+
if (cpuGraph) cpuGraph.graphType = 0.66;
329352
}
330353
}
331354

@@ -407,20 +430,37 @@ class MiniStats {
407430
initGraphs(app, device, options) {
408431
this.graphs = [];
409432

433+
// Add VRAM first so it appears at the bottom in the compact stacked view.
434+
// Graphs are rendered bottom-to-top.
435+
if (options.stats) {
436+
options.stats.forEach((entry) => {
437+
if (entry.name === 'VRAM') {
438+
const timer = new StatsTimer(app, entry.stats, entry.decimalPlaces, entry.unitsName, entry.multiplier);
439+
const graph = new Graph(entry.name, app, entry.watermark, options.textRefreshRate, timer);
440+
this.graphs.push(graph);
441+
}
442+
});
443+
}
444+
410445
if (options.cpu.enabled) {
411446
const timer = new CpuTimer(app);
412447
const graph = new Graph('CPU', app, options.cpu.watermark, options.textRefreshRate, timer);
448+
graph.graphType = 0.66;
413449
this.graphs.push(graph);
414450
}
415451

416452
if (options.gpu.enabled) {
417453
const timer = new GpuTimer(device);
418454
const graph = new Graph('GPU', app, options.gpu.watermark, options.textRefreshRate, timer);
455+
graph.graphType = 0.33;
419456
this.graphs.push(graph);
420457
}
421458

422459
if (options.stats) {
423460
options.stats.forEach((entry) => {
461+
if (entry.name === 'VRAM') {
462+
return;
463+
}
424464
const timer = new StatsTimer(app, entry.stats, entry.decimalPlaces, entry.unitsName, entry.multiplier);
425465
const graph = new Graph(entry.name, app, entry.watermark, options.textRefreshRate, timer);
426466
this.graphs.push(graph);
@@ -590,6 +630,7 @@ class MiniStats {
590630

591631
// scan for new sub-stats
592632
const statsEntries = (stats instanceof Map) ? stats : Object.entries(stats);
633+
const mainGraph = this.graphs.find(g => g.name === mainGraphName);
593634
for (const [statName, timing] of statsEntries) {
594635
if (!subGraphs.has(statName)) {
595636
// Skip creating graph for auto-hide stats with zero timing
@@ -607,11 +648,15 @@ class MiniStats {
607648
}
608649
const graphName = ` ${displayName}`; // indent with 2 spaces
609650

610-
// initial watermark (will be synced to main graph)
611-
const watermark = 10.0;
651+
// use main graph watermark when available
652+
const watermark = mainGraph?.watermark ?? 10.0;
653+
654+
const decimalPlaces = 1;
655+
const unitsName = statPathPrefix === 'vram' ? 'MB' : 'ms';
656+
const multiplier = statPathPrefix === 'vram' ? 1 / (1024 * 1024) : 1;
612657

613658
const statPath = `${statPathPrefix}.${statName}`;
614-
const timer = new StatsTimer(this.app, [statPath], 1, 'ms', 1);
659+
const timer = new StatsTimer(this.app, [statPath], decimalPlaces, unitsName, multiplier);
615660
const graph = new Graph(graphName, this.app, watermark, this.textRefreshRate, timer);
616661

617662
// Set graph type for background tinting
@@ -656,23 +701,10 @@ class MiniStats {
656701
}
657702

658703
// sync all sub-stat watermarks to match main graph
659-
const mainGraph = this.graphs.find(g => g.name === mainGraphName);
660704
if (mainGraph) {
661705
for (const statData of subGraphs.values()) {
662706
statData.graph.watermark = mainGraph.watermark;
663707
}
664-
665-
// set main graph background color to match sub-graphs when they exist
666-
if (subGraphs.size > 0) {
667-
if (statPathPrefix === 'gpu') {
668-
mainGraph.graphType = 0.33; // Match GPU sub-graphs
669-
} else if (statPathPrefix === 'frame') {
670-
mainGraph.graphType = 0.66; // Match CPU sub-graphs
671-
}
672-
} else {
673-
// reset to default background when no sub-graphs
674-
mainGraph.graphType = 0.0;
675-
}
676708
}
677709
}
678710

@@ -757,6 +789,19 @@ class MiniStats {
757789
};
758790
this.updateSubStats(this.cpuGraphs, 'CPU', cpuStats, 'frame', 240);
759791
}
792+
793+
// Update VRAM subcategory graphs when size index meets threshold
794+
if (this._activeSizeIndex >= this.vramTimingMinSize) {
795+
const vram = this.app.stats.vram;
796+
const vramStats = {
797+
tex: vram.tex,
798+
geom: vram.geom
799+
};
800+
if (this.device.isWebGPU) {
801+
vramStats.buffers = vram.buffers;
802+
}
803+
this.updateSubStats(this.vramGraphs, 'VRAM', vramStats, 'vram', 0);
804+
}
760805
}
761806

762807
this.frameIndex++;

src/extras/mini-stats/render2d.js

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import { VertexFormat } from '../../platform/graphics/vertex-format.js';
2121
import { ShaderMaterial } from '../../scene/materials/shader-material.js';
2222

2323
// Graph colors for MiniStats
24-
const graphColorRed = '1.0, 0.412, 0.380'; // Pastel Red
25-
const graphColorGreen = '0.467, 0.867, 0.467'; // Pastel Green
26-
const graphColorBlue = '0.424, 0.627, 0.863'; // Little Boy Blue
24+
const graphColorDefault = '1.0, 0.412, 0.380'; // Pastel Red
25+
const graphColorGpu = '0.467, 0.867, 0.467'; // Pastel Green
26+
const graphColorCpu = '0.424, 0.627, 0.863'; // Little Boy Blue
2727

2828
// Background colors for MiniStats graphs
2929
const mainBackgroundColor = '0.0, 0.0, 0.0';
@@ -62,8 +62,7 @@ const vertexShaderWGSL = /* wgsl */ `
6262

6363
// this fragment shader renders the bits required for text and graphs. The text is identified
6464
// in the texture by white color. The graph data is specified as a single row of pixels
65-
// where the R channel denotes the height of the 1st graph and the G channel the height
66-
// of the second graph and B channel the height of the last graph
65+
// where the R channel denotes the graph height
6766
const fragmentShaderGLSL = /* glsl */ `
6867
varying vec4 uv0;
6968
varying float wordFlag;
@@ -73,15 +72,18 @@ const fragmentShaderGLSL = /* glsl */ `
7372
uniform sampler2D wordsTex;
7473
7574
void main (void) {
75+
vec3 graphColor = vec3(${graphColorDefault});
76+
if (wordFlag > 0.5) {
77+
graphColor = vec3(${graphColorCpu});
78+
} else if (wordFlag > 0.2) {
79+
graphColor = vec3(${graphColorGpu});
80+
}
81+
7682
vec4 graphSample = texture2D(graphTex, uv0.xy);
7783
7884
vec4 graph;
7985
if (uv0.w < graphSample.r)
80-
graph = vec4(${graphColorRed}, 1.0);
81-
else if (uv0.w < graphSample.g)
82-
graph = vec4(${graphColorGreen}, 1.0);
83-
else if (uv0.w < graphSample.b)
84-
graph = vec4(${graphColorBlue}, 1.0);
86+
graph = vec4(graphColor, 1.0);
8587
else {
8688
vec3 bgColor = vec3(${mainBackgroundColor});
8789
if (wordFlag > 0.5) {
@@ -117,15 +119,18 @@ const fragmentShaderWGSL = /* wgsl */ `
117119
118120
@fragment fn fragmentMain(input : FragmentInput) -> FragmentOutput {
119121
var uv0: vec4f = input.uv0;
122+
var graphColor: vec3f = vec3f(${graphColorDefault});
123+
if (input.wordFlag > 0.5) {
124+
graphColor = vec3f(${graphColorCpu});
125+
} else if (input.wordFlag > 0.2) {
126+
graphColor = vec3f(${graphColorGpu});
127+
}
128+
120129
var graphSample: vec4f = textureSample(graphTex, graphTex_sampler, uv0.xy);
121130
122131
var graph: vec4f;
123132
if (uv0.w < graphSample.r) {
124-
graph = vec4f(${graphColorRed}, 1.0);
125-
} else if (uv0.w < graphSample.g) {
126-
graph = vec4f(${graphColorGreen}, 1.0);
127-
} else if (uv0.w < graphSample.b) {
128-
graph = vec4f(${graphColorBlue}, 1.0);
133+
graph = vec4f(graphColor, 1.0);
129134
} else {
130135
var bgColor: vec3f = vec3f(${mainBackgroundColor});
131136
if (input.wordFlag > 0.5) {

src/extras/mini-stats/stats-timer.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@ class StatsTimer {
44
this.app = app;
55
this.values = [];
66

7-
// supporting up to 3 stats
7+
// support one or more stats and accumulate them in the graph total
88
this.statNames = statNames;
9-
if (this.statNames.length > 3) {
10-
this.statNames.length = 3;
11-
}
129

1310
this.unitsName = unitsName;
1411
this.decimalPlaces = decimalPlaces;

src/framework/stats.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class ApplicationStats {
8888

8989
Object.defineProperty(this.vram, 'totalUsed', {
9090
get: function () {
91-
return this.tex + this.vb + this.ib;
91+
return this.tex + this.vb + this.ib + this.ub + this.sb;
9292
}
9393
});
9494

@@ -97,6 +97,12 @@ class ApplicationStats {
9797
return this.vb + this.ib;
9898
}
9999
});
100+
101+
Object.defineProperty(this.vram, 'buffers', {
102+
get: function () {
103+
return this.ub + this.sb;
104+
}
105+
});
100106
}
101107

102108
get scene() {

0 commit comments

Comments
 (0)