stats-gl 3.7.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/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
+ }