stats-gl 3.8.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +360 -86
- package/addons/StatsGLNode.js +201 -0
- package/addons/StatsGLNodeWorker.js +198 -0
- package/dist/core.cjs +421 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.js +421 -0
- package/dist/core.js.map +1 -0
- package/dist/main.cjs +406 -241
- package/dist/main.cjs.map +1 -1
- package/dist/main.js +404 -240
- package/dist/main.js.map +1 -1
- package/dist/panel.cjs +12 -3
- package/dist/panel.cjs.map +1 -1
- package/dist/panel.js +12 -3
- package/dist/panel.js.map +1 -1
- package/dist/panelTexture.cjs +93 -0
- package/dist/panelTexture.cjs.map +1 -0
- package/dist/panelTexture.js +93 -0
- package/dist/panelTexture.js.map +1 -0
- package/dist/profiler.cjs +95 -0
- package/dist/profiler.cjs.map +1 -0
- package/dist/profiler.js +95 -0
- package/dist/profiler.js.map +1 -0
- package/dist/stats-gl.d.ts +332 -71
- package/dist/statsGLNode.cjs +89 -0
- package/dist/statsGLNode.cjs.map +1 -0
- package/dist/statsGLNode.js +89 -0
- package/dist/statsGLNode.js.map +1 -0
- package/dist/textureCapture.cjs +283 -0
- package/dist/textureCapture.cjs.map +1 -0
- package/dist/textureCapture.js +283 -0
- package/dist/textureCapture.js.map +1 -0
- package/lib/core.ts +579 -0
- package/lib/main.ts +506 -401
- package/lib/panel.ts +18 -4
- package/lib/panelTexture.ts +122 -0
- package/lib/profiler.ts +124 -0
- package/lib/statsGLNode.ts +124 -0
- package/lib/textureCapture.ts +403 -0
- package/lib/webgpu.d.ts +190 -0
- package/package.json +6 -4
package/lib/core.ts
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
export interface StatsCoreOptions {
|
|
2
|
+
trackGPU?: boolean;
|
|
3
|
+
trackCPT?: boolean;
|
|
4
|
+
trackHz?: boolean;
|
|
5
|
+
trackFPS?: boolean;
|
|
6
|
+
logsPerSecond?: number;
|
|
7
|
+
graphsPerSecond?: number;
|
|
8
|
+
samplesLog?: number;
|
|
9
|
+
samplesGraph?: number;
|
|
10
|
+
precision?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface QueryInfo {
|
|
14
|
+
query: WebGLQuery;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AverageData {
|
|
18
|
+
logs: number[];
|
|
19
|
+
graph: number[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface InfoData {
|
|
23
|
+
render: {
|
|
24
|
+
timestamp: number;
|
|
25
|
+
};
|
|
26
|
+
compute: {
|
|
27
|
+
timestamp: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface StatsData {
|
|
32
|
+
fps: number;
|
|
33
|
+
cpu: number;
|
|
34
|
+
gpu: number;
|
|
35
|
+
gpuCompute: number;
|
|
36
|
+
isWorker?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class StatsCore {
|
|
40
|
+
public trackGPU: boolean;
|
|
41
|
+
public trackHz: boolean;
|
|
42
|
+
public trackFPS: boolean;
|
|
43
|
+
public trackCPT: boolean;
|
|
44
|
+
public samplesLog: number;
|
|
45
|
+
public samplesGraph: number;
|
|
46
|
+
public precision: number;
|
|
47
|
+
public logsPerSecond: number;
|
|
48
|
+
public graphsPerSecond: number;
|
|
49
|
+
|
|
50
|
+
public gl: WebGL2RenderingContext | null = null;
|
|
51
|
+
public ext: any | null = null;
|
|
52
|
+
public info?: InfoData;
|
|
53
|
+
public gpuDevice: GPUDevice | null = null;
|
|
54
|
+
public gpuBackend: any | null = null;
|
|
55
|
+
public renderer: any | null = null;
|
|
56
|
+
protected activeQuery: WebGLQuery | null = null;
|
|
57
|
+
protected gpuQueries: QueryInfo[] = [];
|
|
58
|
+
protected threeRendererPatched = false;
|
|
59
|
+
|
|
60
|
+
// Native WebGPU timing support
|
|
61
|
+
protected webgpuNative: boolean = false;
|
|
62
|
+
protected gpuQuerySet: GPUQuerySet | null = null;
|
|
63
|
+
protected gpuResolveBuffer: GPUBuffer | null = null;
|
|
64
|
+
protected gpuReadBuffers: GPUBuffer[] = [];
|
|
65
|
+
protected gpuWriteBufferIndex: number = 0; // Buffer to write to this frame
|
|
66
|
+
protected gpuFrameCount: number = 0; // Track frames for first-frame skip
|
|
67
|
+
protected pendingResolve: Promise<number> | null = null;
|
|
68
|
+
|
|
69
|
+
protected beginTime: number;
|
|
70
|
+
protected prevCpuTime: number;
|
|
71
|
+
protected frameTimes: number[] = [];
|
|
72
|
+
|
|
73
|
+
protected renderCount = 0;
|
|
74
|
+
|
|
75
|
+
protected totalCpuDuration = 0;
|
|
76
|
+
protected totalGpuDuration = 0;
|
|
77
|
+
protected totalGpuDurationCompute = 0;
|
|
78
|
+
|
|
79
|
+
public averageFps: AverageData = { logs: [], graph: [] };
|
|
80
|
+
public averageCpu: AverageData = { logs: [], graph: [] };
|
|
81
|
+
public averageGpu: AverageData = { logs: [], graph: [] };
|
|
82
|
+
public averageGpuCompute: AverageData = { logs: [], graph: [] };
|
|
83
|
+
|
|
84
|
+
protected prevGraphTime: number;
|
|
85
|
+
protected prevTextTime: number;
|
|
86
|
+
|
|
87
|
+
constructor({
|
|
88
|
+
trackGPU = false,
|
|
89
|
+
trackCPT = false,
|
|
90
|
+
trackHz = false,
|
|
91
|
+
trackFPS = true,
|
|
92
|
+
logsPerSecond = 4,
|
|
93
|
+
graphsPerSecond = 30,
|
|
94
|
+
samplesLog = 40,
|
|
95
|
+
samplesGraph = 10,
|
|
96
|
+
precision = 2
|
|
97
|
+
}: StatsCoreOptions = {}) {
|
|
98
|
+
this.trackGPU = trackGPU;
|
|
99
|
+
this.trackCPT = trackCPT;
|
|
100
|
+
this.trackHz = trackHz;
|
|
101
|
+
this.trackFPS = trackFPS;
|
|
102
|
+
this.samplesLog = samplesLog;
|
|
103
|
+
this.samplesGraph = samplesGraph;
|
|
104
|
+
this.precision = precision;
|
|
105
|
+
this.logsPerSecond = logsPerSecond;
|
|
106
|
+
this.graphsPerSecond = graphsPerSecond;
|
|
107
|
+
|
|
108
|
+
const now = performance.now();
|
|
109
|
+
this.prevGraphTime = now;
|
|
110
|
+
this.beginTime = now;
|
|
111
|
+
this.prevTextTime = now;
|
|
112
|
+
this.prevCpuTime = now;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public async init(
|
|
116
|
+
canvasOrGL: WebGL2RenderingContext | HTMLCanvasElement | OffscreenCanvas | GPUDevice | any
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
if (!canvasOrGL) {
|
|
119
|
+
console.error('Stats: The "canvas" parameter is undefined.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (this.handleThreeRenderer(canvasOrGL)) return;
|
|
124
|
+
if (await this.handleWebGPURenderer(canvasOrGL)) return;
|
|
125
|
+
|
|
126
|
+
// Handle native GPUDevice
|
|
127
|
+
if (this.handleNativeWebGPU(canvasOrGL)) return;
|
|
128
|
+
|
|
129
|
+
if (this.initializeWebGL(canvasOrGL)) {
|
|
130
|
+
if (this.trackGPU) {
|
|
131
|
+
this.initializeGPUTracking();
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
} else {
|
|
135
|
+
console.error('Stats-gl: Failed to initialize WebGL context');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
protected handleNativeWebGPU(device: any): boolean {
|
|
140
|
+
// Check if this is a GPUDevice by looking for characteristic properties
|
|
141
|
+
if (device && typeof device.createCommandEncoder === 'function' &&
|
|
142
|
+
typeof device.createQuerySet === 'function' && device.queue) {
|
|
143
|
+
this.gpuDevice = device;
|
|
144
|
+
this.webgpuNative = true;
|
|
145
|
+
|
|
146
|
+
if (this.trackGPU && device.features?.has('timestamp-query')) {
|
|
147
|
+
this.initializeWebGPUTiming();
|
|
148
|
+
this.onWebGPUTimestampSupported();
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
protected initializeWebGPUTiming(): void {
|
|
156
|
+
if (!this.gpuDevice) return;
|
|
157
|
+
|
|
158
|
+
// Create query set for 2 timestamps (begin + end)
|
|
159
|
+
this.gpuQuerySet = this.gpuDevice.createQuerySet({
|
|
160
|
+
type: 'timestamp',
|
|
161
|
+
count: 2
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Buffer to resolve query results (2 * 8 bytes for BigInt64)
|
|
165
|
+
this.gpuResolveBuffer = this.gpuDevice.createBuffer({
|
|
166
|
+
size: 16,
|
|
167
|
+
usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Double-buffered read buffers for async readback
|
|
171
|
+
for (let i = 0; i < 2; i++) {
|
|
172
|
+
this.gpuReadBuffers.push(this.gpuDevice.createBuffer({
|
|
173
|
+
size: 16,
|
|
174
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
protected handleThreeRenderer(renderer: any): boolean {
|
|
180
|
+
if (renderer.isWebGLRenderer && !this.threeRendererPatched) {
|
|
181
|
+
this.patchThreeRenderer(renderer);
|
|
182
|
+
this.gl = renderer.getContext();
|
|
183
|
+
|
|
184
|
+
if (this.trackGPU) {
|
|
185
|
+
this.initializeGPUTracking();
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
protected async handleWebGPURenderer(renderer: any): Promise<boolean> {
|
|
193
|
+
if (renderer.isWebGPURenderer) {
|
|
194
|
+
this.renderer = renderer;
|
|
195
|
+
|
|
196
|
+
if (this.trackGPU || this.trackCPT) {
|
|
197
|
+
renderer.backend.trackTimestamp = true;
|
|
198
|
+
if (!renderer._initialized) {
|
|
199
|
+
await renderer.init();
|
|
200
|
+
}
|
|
201
|
+
if (renderer.hasFeature('timestamp-query')) {
|
|
202
|
+
this.onWebGPUTimestampSupported();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
this.info = renderer.info;
|
|
206
|
+
// Store WebGPU device and backend for texture capture
|
|
207
|
+
this.gpuBackend = renderer.backend;
|
|
208
|
+
this.gpuDevice = renderer.backend?.device || null;
|
|
209
|
+
this.patchThreeWebGPU(renderer);
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
protected onWebGPUTimestampSupported(): void {
|
|
216
|
+
// Override in subclass to create panels
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
protected initializeWebGL(
|
|
220
|
+
canvasOrGL: WebGL2RenderingContext | HTMLCanvasElement | OffscreenCanvas
|
|
221
|
+
): boolean {
|
|
222
|
+
if (canvasOrGL instanceof WebGL2RenderingContext) {
|
|
223
|
+
this.gl = canvasOrGL;
|
|
224
|
+
} else if (
|
|
225
|
+
canvasOrGL instanceof HTMLCanvasElement ||
|
|
226
|
+
canvasOrGL instanceof OffscreenCanvas
|
|
227
|
+
) {
|
|
228
|
+
this.gl = canvasOrGL.getContext('webgl2');
|
|
229
|
+
if (!this.gl) {
|
|
230
|
+
console.error('Stats: Unable to obtain WebGL2 context.');
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
console.error(
|
|
235
|
+
'Stats: Invalid input type. Expected WebGL2RenderingContext, HTMLCanvasElement, or OffscreenCanvas.'
|
|
236
|
+
);
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
protected initializeGPUTracking(): void {
|
|
243
|
+
if (this.gl) {
|
|
244
|
+
this.ext = this.gl.getExtension('EXT_disjoint_timer_query_webgl2');
|
|
245
|
+
if (this.ext) {
|
|
246
|
+
this.onGPUTrackingInitialized();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
protected onGPUTrackingInitialized(): void {
|
|
252
|
+
// Override in subclass to create panels
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get timestampWrites configuration for WebGPU render pass.
|
|
257
|
+
* Use this when creating your render pass descriptor.
|
|
258
|
+
* @returns timestampWrites object or undefined if not tracking GPU
|
|
259
|
+
*/
|
|
260
|
+
public getTimestampWrites(): GPURenderPassTimestampWrites | undefined {
|
|
261
|
+
if (!this.webgpuNative || !this.gpuQuerySet) return undefined;
|
|
262
|
+
return {
|
|
263
|
+
querySet: this.gpuQuerySet,
|
|
264
|
+
beginningOfPassWriteIndex: 0,
|
|
265
|
+
endOfPassWriteIndex: 1
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public begin(encoder?: GPUCommandEncoder): void {
|
|
270
|
+
this.beginProfiling('cpu-started');
|
|
271
|
+
|
|
272
|
+
// For native WebGPU, timing is handled via timestampWrites in render pass
|
|
273
|
+
if (this.webgpuNative) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!this.gl || !this.ext) return;
|
|
278
|
+
|
|
279
|
+
if (this.activeQuery) {
|
|
280
|
+
this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.activeQuery = this.gl.createQuery();
|
|
284
|
+
if (this.activeQuery) {
|
|
285
|
+
this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, this.activeQuery);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public end(encoder?: GPUCommandEncoder): void {
|
|
290
|
+
this.renderCount++;
|
|
291
|
+
|
|
292
|
+
// Handle native WebGPU timing - resolve query and copy to read buffer
|
|
293
|
+
if (this.webgpuNative && encoder && this.gpuQuerySet && this.gpuResolveBuffer && this.gpuReadBuffers.length > 0) {
|
|
294
|
+
// Track frame count for first-frame skip
|
|
295
|
+
this.gpuFrameCount++;
|
|
296
|
+
|
|
297
|
+
// Write to current buffer (will read from other buffer in resolve)
|
|
298
|
+
const writeBuffer = this.gpuReadBuffers[this.gpuWriteBufferIndex];
|
|
299
|
+
|
|
300
|
+
// Only add resolve commands if the target buffer is unmapped
|
|
301
|
+
if (writeBuffer.mapState === 'unmapped') {
|
|
302
|
+
encoder.resolveQuerySet(this.gpuQuerySet, 0, 2, this.gpuResolveBuffer, 0);
|
|
303
|
+
encoder.copyBufferToBuffer(this.gpuResolveBuffer, 0, writeBuffer, 0, 16);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.endProfiling('cpu-started', 'cpu-finished', 'cpu-duration');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (this.gl && this.ext && this.activeQuery) {
|
|
311
|
+
this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);
|
|
312
|
+
this.gpuQueries.push({ query: this.activeQuery });
|
|
313
|
+
this.activeQuery = null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
this.endProfiling('cpu-started', 'cpu-finished', 'cpu-duration');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Resolve WebGPU timestamp queries. Call this after queue.submit().
|
|
321
|
+
* Returns a promise that resolves to the GPU duration in milliseconds.
|
|
322
|
+
*/
|
|
323
|
+
public async resolveTimestampsAsync(): Promise<number> {
|
|
324
|
+
if (!this.webgpuNative || this.gpuReadBuffers.length === 0) {
|
|
325
|
+
return this.totalGpuDuration;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// If there's already a pending resolve, wait for it
|
|
329
|
+
if (this.pendingResolve) {
|
|
330
|
+
return this.pendingResolve;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Read from the OTHER buffer (written in previous frame)
|
|
334
|
+
// Current frame writes to gpuWriteBufferIndex, so read from the other one
|
|
335
|
+
const readBufferIndex = (this.gpuWriteBufferIndex + 1) % 2;
|
|
336
|
+
const readBuffer = this.gpuReadBuffers[readBufferIndex];
|
|
337
|
+
|
|
338
|
+
// Toggle write buffer for next frame
|
|
339
|
+
this.gpuWriteBufferIndex = (this.gpuWriteBufferIndex + 1) % 2;
|
|
340
|
+
|
|
341
|
+
// Skip first frame (no previous data to read)
|
|
342
|
+
if (this.gpuFrameCount < 2) {
|
|
343
|
+
return this.totalGpuDuration;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Only attempt to map if buffer is unmapped
|
|
347
|
+
if (readBuffer.mapState !== 'unmapped') {
|
|
348
|
+
return this.totalGpuDuration;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this.pendingResolve = this._resolveTimestamps(readBuffer);
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const result = await this.pendingResolve;
|
|
355
|
+
return result;
|
|
356
|
+
} finally {
|
|
357
|
+
this.pendingResolve = null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private async _resolveTimestamps(readBuffer: GPUBuffer): Promise<number> {
|
|
362
|
+
try {
|
|
363
|
+
await readBuffer.mapAsync(GPUMapMode.READ);
|
|
364
|
+
const data = new BigInt64Array(readBuffer.getMappedRange());
|
|
365
|
+
const startTime = data[0];
|
|
366
|
+
const endTime = data[1];
|
|
367
|
+
readBuffer.unmap();
|
|
368
|
+
|
|
369
|
+
// Convert nanoseconds to milliseconds
|
|
370
|
+
const durationNs = Number(endTime - startTime);
|
|
371
|
+
this.totalGpuDuration = durationNs / 1_000_000;
|
|
372
|
+
return this.totalGpuDuration;
|
|
373
|
+
} catch (_) {
|
|
374
|
+
// Buffer may have been destroyed or mapping failed
|
|
375
|
+
return this.totalGpuDuration;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
protected processGpuQueries(): void {
|
|
380
|
+
if (!this.gl || !this.ext) return;
|
|
381
|
+
|
|
382
|
+
this.totalGpuDuration = 0;
|
|
383
|
+
|
|
384
|
+
// Iterate in reverse to safely remove while iterating
|
|
385
|
+
for (let i = this.gpuQueries.length - 1; i >= 0; i--) {
|
|
386
|
+
const queryInfo = this.gpuQueries[i];
|
|
387
|
+
const available = this.gl.getQueryParameter(queryInfo.query, this.gl.QUERY_RESULT_AVAILABLE);
|
|
388
|
+
const disjoint = this.gl.getParameter(this.ext.GPU_DISJOINT_EXT);
|
|
389
|
+
|
|
390
|
+
if (available && !disjoint) {
|
|
391
|
+
const elapsed = this.gl.getQueryParameter(queryInfo.query, this.gl.QUERY_RESULT);
|
|
392
|
+
const duration = elapsed * 1e-6;
|
|
393
|
+
this.totalGpuDuration += duration;
|
|
394
|
+
this.gl.deleteQuery(queryInfo.query);
|
|
395
|
+
this.gpuQueries.splice(i, 1);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
protected processWebGPUTimestamps(): void {
|
|
401
|
+
this.totalGpuDuration = this.info!.render.timestamp;
|
|
402
|
+
this.totalGpuDurationCompute = this.info!.compute.timestamp;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
protected beginProfiling(marker: string): void {
|
|
406
|
+
if (typeof performance !== 'undefined') {
|
|
407
|
+
try {
|
|
408
|
+
performance.clearMarks(marker);
|
|
409
|
+
performance.mark(marker);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.debug('Stats: Performance marking failed:', error);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
protected endProfiling(startMarker: string | PerformanceMeasureOptions | undefined, endMarker: string | undefined, measureName: string): void {
|
|
417
|
+
if (typeof performance === 'undefined' || !endMarker || !startMarker) return;
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const entries = performance.getEntriesByName(startMarker as string, 'mark');
|
|
421
|
+
if (entries.length === 0) {
|
|
422
|
+
this.beginProfiling(startMarker as string);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
performance.clearMarks(endMarker);
|
|
426
|
+
performance.mark(endMarker);
|
|
427
|
+
|
|
428
|
+
performance.clearMeasures(measureName);
|
|
429
|
+
|
|
430
|
+
const cpuMeasure = performance.measure(measureName, startMarker, endMarker);
|
|
431
|
+
this.totalCpuDuration += cpuMeasure.duration;
|
|
432
|
+
|
|
433
|
+
performance.clearMarks(startMarker as string);
|
|
434
|
+
performance.clearMarks(endMarker);
|
|
435
|
+
performance.clearMeasures(measureName);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.debug('Stats: Performance measurement failed:', error);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
protected calculateFps(): number {
|
|
442
|
+
const currentTime = performance.now();
|
|
443
|
+
|
|
444
|
+
this.frameTimes.push(currentTime);
|
|
445
|
+
|
|
446
|
+
while (this.frameTimes.length > 0 && this.frameTimes[0] <= currentTime - 1000) {
|
|
447
|
+
this.frameTimes.shift();
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return Math.round(this.frameTimes.length);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
protected updateAverages(): void {
|
|
454
|
+
this.addToAverage(this.totalCpuDuration, this.averageCpu);
|
|
455
|
+
this.addToAverage(this.totalGpuDuration, this.averageGpu);
|
|
456
|
+
if (this.info && this.totalGpuDurationCompute !== undefined) {
|
|
457
|
+
this.addToAverage(this.totalGpuDurationCompute, this.averageGpuCompute);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
protected addToAverage(value: number, averageArray: { logs: any; graph: any; }): void {
|
|
462
|
+
averageArray.logs.push(value);
|
|
463
|
+
while (averageArray.logs.length > this.samplesLog) {
|
|
464
|
+
averageArray.logs.shift();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
averageArray.graph.push(value);
|
|
468
|
+
while (averageArray.graph.length > this.samplesGraph) {
|
|
469
|
+
averageArray.graph.shift();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
protected resetCounters(): void {
|
|
474
|
+
this.renderCount = 0;
|
|
475
|
+
this.totalCpuDuration = 0;
|
|
476
|
+
this.beginTime = performance.now();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
public getData(): StatsData {
|
|
480
|
+
const fpsLogs = this.averageFps.logs;
|
|
481
|
+
const cpuLogs = this.averageCpu.logs;
|
|
482
|
+
const gpuLogs = this.averageGpu.logs;
|
|
483
|
+
const gpuComputeLogs = this.averageGpuCompute.logs;
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
fps: fpsLogs.length > 0 ? fpsLogs[fpsLogs.length - 1] : 0,
|
|
487
|
+
cpu: cpuLogs.length > 0 ? cpuLogs[cpuLogs.length - 1] : 0,
|
|
488
|
+
gpu: gpuLogs.length > 0 ? gpuLogs[gpuLogs.length - 1] : 0,
|
|
489
|
+
gpuCompute: gpuComputeLogs.length > 0 ? gpuComputeLogs[gpuComputeLogs.length - 1] : 0
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
protected patchThreeWebGPU(renderer: any): void {
|
|
494
|
+
const originalAnimationLoop = renderer.info.reset;
|
|
495
|
+
const statsInstance = this;
|
|
496
|
+
|
|
497
|
+
renderer.info.reset = function () {
|
|
498
|
+
statsInstance.beginProfiling('cpu-started');
|
|
499
|
+
originalAnimationLoop.call(this);
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
protected patchThreeRenderer(renderer: any): void {
|
|
504
|
+
const originalRenderMethod = renderer.render;
|
|
505
|
+
const statsInstance = this;
|
|
506
|
+
|
|
507
|
+
renderer.render = function (scene: any, camera: any) {
|
|
508
|
+
statsInstance.begin();
|
|
509
|
+
originalRenderMethod.call(this, scene, camera);
|
|
510
|
+
statsInstance.end();
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
this.threeRendererPatched = true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Dispose of all resources. Call when done using the stats instance.
|
|
518
|
+
*/
|
|
519
|
+
public dispose(): void {
|
|
520
|
+
// Clean up any pending GPU queries
|
|
521
|
+
if (this.gl) {
|
|
522
|
+
// End active query if any
|
|
523
|
+
if (this.activeQuery && this.ext) {
|
|
524
|
+
try {
|
|
525
|
+
this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);
|
|
526
|
+
} catch (_) {
|
|
527
|
+
// Query may not be active
|
|
528
|
+
}
|
|
529
|
+
this.gl.deleteQuery(this.activeQuery);
|
|
530
|
+
this.activeQuery = null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Delete all pending queries
|
|
534
|
+
for (const queryInfo of this.gpuQueries) {
|
|
535
|
+
this.gl.deleteQuery(queryInfo.query);
|
|
536
|
+
}
|
|
537
|
+
this.gpuQueries.length = 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Clean up WebGPU resources
|
|
541
|
+
if (this.gpuQuerySet) {
|
|
542
|
+
this.gpuQuerySet.destroy();
|
|
543
|
+
this.gpuQuerySet = null;
|
|
544
|
+
}
|
|
545
|
+
if (this.gpuResolveBuffer) {
|
|
546
|
+
this.gpuResolveBuffer.destroy();
|
|
547
|
+
this.gpuResolveBuffer = null;
|
|
548
|
+
}
|
|
549
|
+
for (const buffer of this.gpuReadBuffers) {
|
|
550
|
+
if (buffer.mapState === 'mapped') {
|
|
551
|
+
buffer.unmap();
|
|
552
|
+
}
|
|
553
|
+
buffer.destroy();
|
|
554
|
+
}
|
|
555
|
+
this.gpuReadBuffers.length = 0;
|
|
556
|
+
this.gpuFrameCount = 0;
|
|
557
|
+
this.pendingResolve = null;
|
|
558
|
+
this.webgpuNative = false;
|
|
559
|
+
|
|
560
|
+
// Clear references
|
|
561
|
+
this.gl = null;
|
|
562
|
+
this.ext = null;
|
|
563
|
+
this.info = undefined;
|
|
564
|
+
this.gpuDevice = null;
|
|
565
|
+
this.gpuBackend = null;
|
|
566
|
+
this.renderer = null;
|
|
567
|
+
|
|
568
|
+
// Clear arrays
|
|
569
|
+
this.frameTimes.length = 0;
|
|
570
|
+
this.averageFps.logs.length = 0;
|
|
571
|
+
this.averageFps.graph.length = 0;
|
|
572
|
+
this.averageCpu.logs.length = 0;
|
|
573
|
+
this.averageCpu.graph.length = 0;
|
|
574
|
+
this.averageGpu.logs.length = 0;
|
|
575
|
+
this.averageGpu.graph.length = 0;
|
|
576
|
+
this.averageGpuCompute.logs.length = 0;
|
|
577
|
+
this.averageGpuCompute.graph.length = 0;
|
|
578
|
+
}
|
|
579
|
+
}
|