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/main.ts CHANGED
@@ -1,95 +1,59 @@
1
- import type * as THREE from 'three';
1
+ import { StatsCore, StatsCoreOptions, StatsData, AverageData } from './core';
2
2
  import { Panel } from './panel';
3
3
  import { PanelVSync } from './panelVsync';
4
-
5
- interface StatsOptions {
6
- trackGPU?: boolean;
7
- trackCPT?: boolean;
8
- trackHz?: boolean;
9
- logsPerSecond?: number;
10
- graphsPerSecond?: number;
11
- samplesLog?: number;
12
- samplesGraph?: number;
13
- precision?: number;
4
+ import { PanelTexture } from './panelTexture';
5
+ import {
6
+ TextureCaptureWebGL,
7
+ TextureCaptureWebGPU,
8
+ extractWebGLSource,
9
+ extractWebGPUSource,
10
+ ThreeTextureSource,
11
+ DEFAULT_PREVIEW_WIDTH,
12
+ DEFAULT_PREVIEW_HEIGHT
13
+ } from './textureCapture';
14
+
15
+ interface StatsOptions extends StatsCoreOptions {
14
16
  minimal?: boolean;
15
17
  horizontal?: boolean;
16
18
  mode?: number;
17
19
  }
18
20
 
19
- interface QueryInfo {
20
- query: WebGLQuery;
21
- }
22
-
23
- interface AverageData {
24
- logs: number[];
25
- graph: number[];
26
- }
27
-
28
-
29
21
  interface VSyncInfo {
30
22
  refreshRate: number;
31
23
  frameTime: number;
32
24
  }
33
25
 
34
-
35
- interface InfoData {
36
- render: {
37
- timestamp: number;
38
- };
39
- compute: {
40
- timestamp: number;
41
- };
42
- }
43
-
44
- class Stats {
26
+ class Stats extends StatsCore {
45
27
  public dom: HTMLDivElement;
46
28
  public mode: number;
47
29
  public horizontal: boolean;
48
30
  public minimal: boolean;
49
- public trackGPU: boolean;
50
- public trackHz: boolean;
51
- public trackCPT: boolean;
52
- public samplesLog: number;
53
- public samplesGraph: number;
54
- public precision: number;
55
- public logsPerSecond: number;
56
- public graphsPerSecond: number;
57
-
58
- public gl: WebGL2RenderingContext | null = null;
59
- public ext: any | null = null;
60
- public info?: InfoData;
61
- private activeQuery: WebGLQuery | null = null;
62
- private gpuQueries: QueryInfo[] = [];
63
- private threeRendererPatched = false;
64
-
65
- private beginTime: number;
66
- private prevCpuTime: number;
67
- private frameTimes: number[] = []; // Store frame timestamps
68
-
69
- private renderCount = 0;
70
-
71
- private totalCpuDuration = 0;
72
- private totalGpuDuration = 0;
73
- private totalGpuDurationCompute = 0;
74
31
 
75
32
  private _panelId: number;
76
- private fpsPanel: Panel;
77
- private msPanel: Panel;
33
+ private fpsPanel: Panel | null = null;
34
+ private msPanel: Panel | null = null;
78
35
  private gpuPanel: Panel | null = null;
79
36
  private gpuPanelCompute: Panel | null = null;
80
37
  private vsyncPanel: PanelVSync | null = null;
81
-
82
- public averageFps: AverageData = { logs: [], graph: [] };
83
- public averageCpu: AverageData = { logs: [], graph: [] };
84
- public averageGpu: AverageData = { logs: [], graph: [] };
85
- public averageGpuCompute: AverageData = { logs: [], graph: [] };
38
+ private workerCpuPanel: Panel | null = null;
39
+
40
+ // Texture panel support
41
+ public texturePanels: Map<string, PanelTexture> = new Map();
42
+ private texturePanelRow: HTMLDivElement | null = null;
43
+ private textureCaptureWebGL: TextureCaptureWebGL | null = null;
44
+ private textureCaptureWebGPU: TextureCaptureWebGPU | null = null;
45
+ private textureSourcesWebGL: Map<string, { target: ThreeTextureSource; framebuffer: WebGLFramebuffer; width: number; height: number }> = new Map();
46
+ private textureSourcesWebGPU: Map<string, any> = new Map(); // GPUTexture
47
+ private texturePreviewWidth = DEFAULT_PREVIEW_WIDTH;
48
+ private texturePreviewHeight = DEFAULT_PREVIEW_HEIGHT;
49
+ private lastRendererWidth = 0;
50
+ private lastRendererHeight = 0;
51
+ private textureUpdatePending = false;
86
52
 
87
53
  private updateCounter = 0;
88
- private prevGraphTime: number;
89
54
  private lastMin: { [key: string]: number } = {};
90
55
  private lastMax: { [key: string]: number } = {};
91
56
  private lastValue: { [key: string]: number } = {};
92
- private prevTextTime: number;
93
57
 
94
58
  private readonly VSYNC_RATES: VSyncInfo[] = [
95
59
  { refreshRate: 60, frameTime: 16.67 },
@@ -102,18 +66,23 @@ class Stats {
102
66
  ];
103
67
  private detectedVSync: VSyncInfo | null = null;
104
68
  private frameTimeHistory: number[] = [];
105
- private readonly HISTORY_SIZE = 120; // 2 seconds worth of frames at 60fps
106
- private readonly VSYNC_THRESHOLD = 0.05; // 5% tolerance
69
+ private readonly HISTORY_SIZE = 120;
70
+ private readonly VSYNC_THRESHOLD = 0.05;
107
71
  private lastFrameTime: number = 0;
108
72
 
109
-
73
+ private externalData: StatsData | null = null;
74
+ private hasNewExternalData = false;
75
+ private isWorker = false;
76
+ private averageWorkerCpu: AverageData = { logs: [], graph: [] };
110
77
 
111
78
  static Panel = Panel;
79
+ static PanelTexture = PanelTexture;
112
80
 
113
81
  constructor({
114
82
  trackGPU = false,
115
83
  trackCPT = false,
116
84
  trackHz = false,
85
+ trackFPS = true,
117
86
  logsPerSecond = 4,
118
87
  graphsPerSecond = 30,
119
88
  samplesLog = 40,
@@ -123,34 +92,39 @@ class Stats {
123
92
  horizontal = true,
124
93
  mode = 0
125
94
  }: StatsOptions = {}) {
95
+ super({
96
+ trackGPU,
97
+ trackCPT,
98
+ trackHz,
99
+ trackFPS,
100
+ logsPerSecond,
101
+ graphsPerSecond,
102
+ samplesLog,
103
+ samplesGraph,
104
+ precision
105
+ });
106
+
126
107
  this.mode = mode;
127
108
  this.horizontal = horizontal;
128
109
  this.minimal = minimal;
129
- this.trackGPU = trackGPU;
130
- this.trackCPT = trackCPT;
131
- this.trackHz = trackHz;
132
- this.samplesLog = samplesLog;
133
- this.samplesGraph = samplesGraph;
134
- this.precision = precision;
135
- this.logsPerSecond = logsPerSecond;
136
- this.graphsPerSecond = graphsPerSecond;
137
- const prevGraphTime = performance.now();
138
- this.prevGraphTime = prevGraphTime
139
-
140
- // Initialize DOM
110
+
141
111
  this.dom = document.createElement('div');
142
112
  this.initializeDOM();
143
113
 
144
- // Initialize timing
145
- this.beginTime = performance.now();
146
- this.prevTextTime = this.beginTime;
114
+ this._panelId = 0;
147
115
 
148
- this.prevCpuTime = this.beginTime;
116
+ if (this.trackFPS) {
117
+ this.fpsPanel = this.addPanel(new Stats.Panel('FPS', '#0ff', '#002'));
118
+ this.msPanel = this.addPanel(new Stats.Panel('CPU', '#0f0', '#020'));
119
+ }
149
120
 
150
- this._panelId = 0
151
- // Create panels
152
- this.fpsPanel = this.addPanel(new Stats.Panel('FPS', '#0ff', '#002'));
153
- this.msPanel = this.addPanel(new Stats.Panel('CPU', '#0f0', '#020'));
121
+ if (this.trackGPU) {
122
+ this.gpuPanel = this.addPanel(new Stats.Panel('GPU', '#ff0', '#220'));
123
+ }
124
+
125
+ if (this.trackCPT) {
126
+ this.gpuPanelCompute = this.addPanel(new Stats.Panel('CPT', '#e1e1e1', '#212121'));
127
+ }
154
128
 
155
129
  if (this.trackHz === true) {
156
130
  this.vsyncPanel = new PanelVSync('', '#f0f', '#202');
@@ -161,7 +135,6 @@ class Stats {
161
135
  this.setupEventListeners();
162
136
  }
163
137
 
164
-
165
138
  private initializeDOM(): void {
166
139
  this.dom.style.cssText = `
167
140
  position: fixed;
@@ -188,167 +161,234 @@ class Stats {
188
161
  };
189
162
 
190
163
  private handleResize = (): void => {
191
- this.resizePanel(this.fpsPanel);
192
- this.resizePanel(this.msPanel);
164
+ if (this.fpsPanel) this.resizePanel(this.fpsPanel);
165
+ if (this.msPanel) this.resizePanel(this.msPanel);
166
+ if (this.workerCpuPanel) this.resizePanel(this.workerCpuPanel);
193
167
  if (this.gpuPanel) this.resizePanel(this.gpuPanel);
194
168
  if (this.gpuPanelCompute) this.resizePanel(this.gpuPanelCompute);
195
169
  };
196
170
 
197
- public async init(
198
- canvasOrGL: WebGL2RenderingContext | HTMLCanvasElement | OffscreenCanvas | any
199
- ): Promise<void> {
200
- if (!canvasOrGL) {
201
- console.error('Stats: The "canvas" parameter is undefined.');
171
+ /**
172
+ * Compute and update texture preview dimensions based on renderer aspect ratio
173
+ */
174
+ private updateTexturePreviewDimensions(): void {
175
+ if (!this.renderer) return;
176
+
177
+ const rendererWidth = this.renderer.domElement?.width || 0;
178
+ const rendererHeight = this.renderer.domElement?.height || 0;
179
+
180
+ // Skip if dimensions unchanged
181
+ if (rendererWidth === this.lastRendererWidth && rendererHeight === this.lastRendererHeight) {
202
182
  return;
203
183
  }
184
+ if (rendererWidth === 0 || rendererHeight === 0) return;
204
185
 
205
- if (this.handleThreeRenderer(canvasOrGL)) return;
206
- if (await this.handleWebGPURenderer(canvasOrGL)) return;
186
+ this.lastRendererWidth = rendererWidth;
187
+ this.lastRendererHeight = rendererHeight;
207
188
 
208
- if (this.initializeWebGL(canvasOrGL)) {
209
- if (this.trackGPU) {
210
- this.initializeGPUTracking();
211
- }
212
- return;
189
+ // Compute preview size maintaining aspect ratio
190
+ // Base dimensions: 90x48 panel, compute to fit source aspect
191
+ const sourceAspect = rendererWidth / rendererHeight;
192
+ const panelAspect = DEFAULT_PREVIEW_WIDTH / DEFAULT_PREVIEW_HEIGHT;
193
+
194
+ let newWidth: number;
195
+ let newHeight: number;
196
+
197
+ if (sourceAspect > panelAspect) {
198
+ // Source wider than panel - fit to width
199
+ newWidth = DEFAULT_PREVIEW_WIDTH;
200
+ newHeight = Math.round(DEFAULT_PREVIEW_WIDTH / sourceAspect);
213
201
  } else {
214
- console.error('Stats-gl: Failed to initialize WebGL context');
202
+ // Source taller than panel - fit to height
203
+ newHeight = DEFAULT_PREVIEW_HEIGHT;
204
+ newWidth = Math.round(DEFAULT_PREVIEW_HEIGHT * sourceAspect);
215
205
  }
216
- }
217
206
 
218
- private handleThreeRenderer(renderer: any): boolean {
219
- if (renderer.isWebGLRenderer && !this.threeRendererPatched) {
220
- this.patchThreeRenderer(renderer);
221
- this.gl = renderer.getContext();
207
+ // Ensure minimum dimensions
208
+ newWidth = Math.max(newWidth, 16);
209
+ newHeight = Math.max(newHeight, 16);
222
210
 
223
- if (this.trackGPU) {
224
- this.initializeGPUTracking();
211
+ if (newWidth !== this.texturePreviewWidth || newHeight !== this.texturePreviewHeight) {
212
+ this.texturePreviewWidth = newWidth;
213
+ this.texturePreviewHeight = newHeight;
214
+
215
+ // Resize capture helpers
216
+ if (this.textureCaptureWebGL) {
217
+ this.textureCaptureWebGL.resize(newWidth, newHeight);
218
+ }
219
+ if (this.textureCaptureWebGPU) {
220
+ this.textureCaptureWebGPU.resize(newWidth, newHeight);
225
221
  }
226
- return true;
227
- }
228
- return false;
229
- }
230
222
 
231
- private async handleWebGPURenderer(renderer: any): Promise<boolean> {
232
- if (renderer.isWebGPURenderer) {
233
- if (this.trackGPU || this.trackCPT) {
234
- renderer.backend.trackTimestamp = true;
235
- if (!renderer._initialized) {
236
- await renderer.init();
237
- }
238
- if (renderer.hasFeature('timestamp-query')) {
239
- this.initializeWebGPUPanels();
240
- }
223
+ // Update panel source sizes
224
+ for (const panel of this.texturePanels.values()) {
225
+ panel.setSourceSize(rendererWidth, rendererHeight);
241
226
  }
242
- this.info = renderer.info;
243
- this.patchThreeWebGPU(renderer);
244
- return true;
245
227
  }
246
- return false;
247
228
  }
248
229
 
249
- private initializeWebGPUPanels(): void {
250
- if (this.trackGPU) {
251
- this.gpuPanel = this.addPanel(new Stats.Panel('GPU', '#ff0', '#220'));
252
- }
253
- if (this.trackCPT) {
254
- this.gpuPanelCompute = this.addPanel(new Stats.Panel('CPT', '#e1e1e1', '#212121'));
255
- }
230
+ protected override onWebGPUTimestampSupported(): void {
231
+ // Panels already created in constructor
256
232
  }
257
233
 
258
- private initializeWebGL(
259
- canvasOrGL: WebGL2RenderingContext | HTMLCanvasElement | OffscreenCanvas
260
- ): boolean {
261
- if (canvasOrGL instanceof WebGL2RenderingContext) {
262
- this.gl = canvasOrGL;
263
- } else if (
264
- canvasOrGL instanceof HTMLCanvasElement ||
265
- canvasOrGL instanceof OffscreenCanvas
266
- ) {
267
- this.gl = canvasOrGL.getContext('webgl2');
268
- if (!this.gl) {
269
- console.error('Stats: Unable to obtain WebGL2 context.');
270
- return false;
271
- }
272
- } else {
273
- console.error(
274
- 'Stats: Invalid input type. Expected WebGL2RenderingContext, HTMLCanvasElement, or OffscreenCanvas.'
275
- );
276
- return false;
277
- }
278
- return true;
234
+ protected override onGPUTrackingInitialized(): void {
235
+ // Panel already created in constructor
279
236
  }
280
237
 
281
- private initializeGPUTracking(): void {
282
- if (this.gl) {
283
- this.ext = this.gl.getExtension('EXT_disjoint_timer_query_webgl2');
284
- if (this.ext) {
285
- this.gpuPanel = this.addPanel(new Stats.Panel('GPU', '#ff0', '#220'));
286
- }
287
- }
288
- }
238
+ public setData(data: StatsData): void {
239
+ this.externalData = data;
240
+ this.hasNewExternalData = true;
289
241
 
290
- public begin(): void {
291
- this.beginProfiling('cpu-started');
242
+ // Dynamically add worker CPU panel right after main CPU panel
243
+ if (!this.isWorker && this.msPanel) {
244
+ this.isWorker = true;
292
245
 
293
- if (!this.gl || !this.ext) return;
246
+ this.workerCpuPanel = new Stats.Panel('WRK', '#f90', '#220');
247
+ const insertPosition = this.msPanel.id + 1;
248
+ this.workerCpuPanel.id = insertPosition;
294
249
 
295
- if (this.activeQuery) {
296
- this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);
297
- }
250
+ // Shift IDs of panels that come after
251
+ if (this.gpuPanel && this.gpuPanel.id >= insertPosition) {
252
+ this.gpuPanel.id++;
253
+ this.resizePanel(this.gpuPanel);
254
+ }
255
+ if (this.gpuPanelCompute && this.gpuPanelCompute.id >= insertPosition) {
256
+ this.gpuPanelCompute.id++;
257
+ this.resizePanel(this.gpuPanelCompute);
258
+ }
298
259
 
299
- this.activeQuery = this.gl.createQuery();
300
- if (this.activeQuery) {
301
- this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, this.activeQuery);
260
+ // Insert canvas after msPanel in DOM
261
+ const msCanvas = this.msPanel.canvas;
262
+ if (msCanvas.nextSibling) {
263
+ this.dom.insertBefore(this.workerCpuPanel.canvas, msCanvas.nextSibling);
264
+ } else {
265
+ this.dom.appendChild(this.workerCpuPanel.canvas);
266
+ }
267
+
268
+ this.resizePanel(this.workerCpuPanel);
269
+ this._panelId++;
302
270
  }
303
271
  }
304
272
 
305
- public end(): void {
306
- this.renderCount++;
307
- if (this.gl && this.ext && this.activeQuery) {
308
- this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);
309
- this.gpuQueries.push({ query: this.activeQuery });
310
- this.activeQuery = null;
273
+ public update(): void {
274
+ if (this.externalData) {
275
+ this.updateFromExternalData();
276
+ } else {
277
+ this.updateFromInternalData();
311
278
  }
279
+ }
312
280
 
281
+ private updateFromExternalData(): void {
282
+ const data = this.externalData!;
283
+
284
+ // Track main thread CPU (measures from begin() call if user called it)
313
285
  this.endProfiling('cpu-started', 'cpu-finished', 'cpu-duration');
314
- }
286
+ this.addToAverage(this.totalCpuDuration, this.averageCpu);
287
+ this.totalCpuDuration = 0;
315
288
 
316
- public update(): void {
289
+ // Only add worker data when new message arrived
290
+ if (this.hasNewExternalData) {
291
+ this.addToAverage(data.cpu, this.averageWorkerCpu);
292
+ this.addToAverage(data.fps, this.averageFps);
293
+ this.addToAverage(data.gpu, this.averageGpu);
294
+ this.addToAverage(data.gpuCompute, this.averageGpuCompute);
295
+ this.hasNewExternalData = false;
296
+ }
317
297
 
298
+ this.renderPanels();
299
+ }
318
300
 
301
+ private updateFromInternalData(): void {
319
302
  this.endProfiling('cpu-started', 'cpu-finished', 'cpu-duration');
320
303
 
321
- if (!this.info) {
304
+ if (this.webgpuNative) {
305
+ // Native WebGPU: resolve timestamps async
306
+ this.resolveTimestampsAsync();
307
+ } else if (!this.info) {
322
308
  this.processGpuQueries();
323
309
  } else {
324
310
  this.processWebGPUTimestamps();
325
311
  }
326
312
 
327
- this.updateAverages()
313
+ this.updateAverages();
328
314
  this.resetCounters();
315
+ this.renderPanels();
329
316
  }
330
317
 
331
- private processWebGPUTimestamps(): void {
332
- this.totalGpuDuration = this.info!.render.timestamp;
333
- this.totalGpuDurationCompute = this.info!.compute.timestamp;
318
+ private renderPanels(): void {
319
+ const currentTime = performance.now();
320
+
321
+ // Only calculate FPS locally when not using worker data
322
+ if (!this.isWorker) {
323
+ this.frameTimes.push(currentTime);
324
+
325
+ while (this.frameTimes.length > 0 && this.frameTimes[0] <= currentTime - 1000) {
326
+ this.frameTimes.shift();
327
+ }
328
+
329
+ const fps = Math.round(this.frameTimes.length);
330
+ this.addToAverage(fps, this.averageFps);
331
+ }
332
+
333
+ const shouldUpdateText = currentTime >= this.prevTextTime + 1000 / this.logsPerSecond;
334
+ const shouldUpdateGraph = currentTime >= this.prevGraphTime + 1000 / this.graphsPerSecond;
335
+
336
+ const suffix = this.isWorker ? ' ⛭' : '';
337
+ this.updatePanelComponents(this.fpsPanel, this.averageFps, 0, shouldUpdateText, shouldUpdateGraph, suffix);
338
+ // Main thread CPU (no suffix)
339
+ this.updatePanelComponents(this.msPanel, this.averageCpu, this.precision, shouldUpdateText, shouldUpdateGraph, '');
340
+ // Worker CPU panel (with ⛭ suffix)
341
+ if (this.workerCpuPanel && this.isWorker) {
342
+ this.updatePanelComponents(this.workerCpuPanel, this.averageWorkerCpu, this.precision, shouldUpdateText, shouldUpdateGraph, ' ⛭');
343
+ }
344
+ if (this.gpuPanel) {
345
+ this.updatePanelComponents(this.gpuPanel, this.averageGpu, this.precision, shouldUpdateText, shouldUpdateGraph, suffix);
346
+ }
347
+ if (this.trackCPT && this.gpuPanelCompute) {
348
+ this.updatePanelComponents(this.gpuPanelCompute, this.averageGpuCompute, this.precision, shouldUpdateText, shouldUpdateGraph, suffix);
349
+ }
350
+
351
+ if (shouldUpdateText) {
352
+ this.prevTextTime = currentTime;
353
+ }
354
+ if (shouldUpdateGraph) {
355
+ this.prevGraphTime = currentTime;
356
+
357
+ // Update texture panels at graph rate (prevent overlapping async updates)
358
+ if (this.texturePanels.size > 0 && !this.textureUpdatePending) {
359
+ this.textureUpdatePending = true;
360
+ this.updateTexturePanels().finally(() => {
361
+ this.textureUpdatePending = false;
362
+ });
363
+ }
364
+
365
+ // Capture StatsGL nodes (registered by addon)
366
+ this.captureStatsGLNodes();
367
+ }
368
+
369
+ if (this.vsyncPanel !== null) {
370
+ this.detectVSync(currentTime);
371
+
372
+ const vsyncValue = this.detectedVSync?.refreshRate || 0;
373
+
374
+ if (shouldUpdateText && vsyncValue > 0) {
375
+ this.vsyncPanel.update(vsyncValue, vsyncValue);
376
+ }
377
+ }
334
378
  }
335
379
 
336
- private resetCounters(): void {
380
+ protected override resetCounters(): void {
337
381
  this.renderCount = 0;
338
382
  this.totalCpuDuration = 0;
339
- this.beginTime = this.endInternal();
383
+ this.beginTime = performance.now();
340
384
  }
341
385
 
342
386
  resizePanel(panel: Panel) {
343
-
344
387
  panel.canvas.style.position = 'absolute';
345
388
 
346
389
  if (this.minimal) {
347
-
348
390
  panel.canvas.style.display = 'none';
349
-
350
391
  } else {
351
-
352
392
  panel.canvas.style.display = 'block';
353
393
  if (this.horizontal) {
354
394
  panel.canvas.style.top = '0px';
@@ -356,96 +396,267 @@ class Stats {
356
396
  } else {
357
397
  panel.canvas.style.left = '0px';
358
398
  panel.canvas.style.top = panel.id * panel.HEIGHT / panel.PR + 'px';
359
-
360
399
  }
361
400
  }
362
-
363
401
  }
364
- addPanel(panel: Panel) {
365
402
 
403
+ addPanel(panel: Panel) {
366
404
  if (panel.canvas) {
367
-
368
405
  this.dom.appendChild(panel.canvas);
369
406
  panel.id = this._panelId;
370
407
  this.resizePanel(panel);
371
-
372
408
  this._panelId++;
373
409
  }
374
-
375
410
  return panel;
376
-
377
411
  }
378
412
 
379
413
  showPanel(id: number) {
380
-
381
414
  for (let i = 0; i < this.dom.children.length; i++) {
382
415
  const child = this.dom.children[i] as HTMLElement;
383
-
384
416
  child.style.display = i === id ? 'block' : 'none';
385
-
386
417
  }
387
-
388
418
  this.mode = id;
419
+ }
420
+
421
+ // ==========================================================================
422
+ // Texture Panel API
423
+ // ==========================================================================
424
+
425
+ /**
426
+ * Add a new texture preview panel
427
+ * @param name - Label for the texture panel
428
+ * @returns The created PanelTexture instance
429
+ */
430
+ public addTexturePanel(name: string): PanelTexture {
431
+ // Create texture panel row if not exists
432
+ if (!this.texturePanelRow) {
433
+ this.texturePanelRow = document.createElement('div');
434
+ this.texturePanelRow.style.cssText = `
435
+ position: absolute;
436
+ top: 48px;
437
+ left: 0;
438
+ display: flex;
439
+ flex-direction: row;
440
+ `;
441
+ this.dom.appendChild(this.texturePanelRow);
442
+ }
443
+
444
+ const panel = new PanelTexture(name);
445
+ panel.canvas.style.position = 'relative';
446
+ panel.canvas.style.left = '';
447
+ panel.canvas.style.top = '';
448
+ this.texturePanelRow.appendChild(panel.canvas);
449
+ this.texturePanels.set(name, panel);
389
450
 
451
+ return panel;
390
452
  }
391
453
 
392
- processGpuQueries() {
454
+ /**
455
+ * Set texture source for a panel (Three.js render target)
456
+ * Auto-detects WebGL/WebGPU and extracts native handles
457
+ * @param name - Panel name
458
+ * @param source - Three.js RenderTarget or native texture
459
+ */
460
+ public setTexture(name: string, source: ThreeTextureSource | any): void {
461
+ // Update preview dimensions based on current renderer
462
+ this.updateTexturePreviewDimensions();
463
+
464
+ // Initialize capture helpers if needed
465
+ if (this.gl && !this.textureCaptureWebGL) {
466
+ this.textureCaptureWebGL = new TextureCaptureWebGL(this.gl, this.texturePreviewWidth, this.texturePreviewHeight);
467
+ }
468
+ if (this.gpuDevice && !this.textureCaptureWebGPU) {
469
+ this.textureCaptureWebGPU = new TextureCaptureWebGPU(this.gpuDevice, this.texturePreviewWidth, this.texturePreviewHeight);
470
+ }
471
+
472
+ const panel = this.texturePanels.get(name);
473
+
474
+ // Handle Three.js WebGLRenderTarget
475
+ if ((source as ThreeTextureSource).isWebGLRenderTarget && this.gl) {
476
+ const webglSource = extractWebGLSource(source as ThreeTextureSource, this.gl);
477
+ if (webglSource) {
478
+ this.textureSourcesWebGL.set(name, {
479
+ target: source as ThreeTextureSource,
480
+ ...webglSource
481
+ });
482
+ // Set source aspect ratio on panel
483
+ if (panel) {
484
+ panel.setSourceSize(webglSource.width, webglSource.height);
485
+ }
486
+ }
487
+ return;
488
+ }
393
489
 
490
+ // Handle Three.js WebGPU RenderTarget
491
+ if ((source as ThreeTextureSource).isRenderTarget && this.gpuBackend) {
492
+ const gpuTexture = extractWebGPUSource(source as ThreeTextureSource, this.gpuBackend);
493
+ if (gpuTexture) {
494
+ this.textureSourcesWebGPU.set(name, gpuTexture);
495
+ // Set source aspect ratio on panel (use source dimensions if available)
496
+ if (panel && (source as ThreeTextureSource).width && (source as ThreeTextureSource).height) {
497
+ panel.setSourceSize((source as ThreeTextureSource).width!, (source as ThreeTextureSource).height!);
498
+ }
499
+ }
500
+ return;
501
+ }
394
502
 
395
- if (!this.gl || !this.ext) return;
503
+ // Handle raw GPUTexture (check for createView method)
504
+ if (source && typeof source.createView === 'function') {
505
+ this.textureSourcesWebGPU.set(name, source);
506
+ return;
507
+ }
396
508
 
397
- this.totalGpuDuration = 0;
509
+ // Handle raw WebGLFramebuffer (need width/height from user)
510
+ // For raw FBOs, user should call setTextureWebGL directly
511
+ }
398
512
 
399
- this.gpuQueries.forEach((queryInfo, index) => {
400
- if (this.gl) {
401
- const available = this.gl.getQueryParameter(queryInfo.query, this.gl.QUERY_RESULT_AVAILABLE);
402
- const disjoint = this.gl.getParameter(this.ext.GPU_DISJOINT_EXT);
513
+ /**
514
+ * Set WebGL framebuffer source with explicit dimensions
515
+ * @param name - Panel name
516
+ * @param framebuffer - WebGL framebuffer
517
+ * @param width - Texture width
518
+ * @param height - Texture height
519
+ */
520
+ public setTextureWebGL(name: string, framebuffer: WebGLFramebuffer, width: number, height: number): void {
521
+ // Update preview dimensions based on current renderer
522
+ this.updateTexturePreviewDimensions();
523
+
524
+ if (this.gl && !this.textureCaptureWebGL) {
525
+ this.textureCaptureWebGL = new TextureCaptureWebGL(this.gl, this.texturePreviewWidth, this.texturePreviewHeight);
526
+ }
527
+ this.textureSourcesWebGL.set(name, {
528
+ target: { isWebGLRenderTarget: true } as ThreeTextureSource,
529
+ framebuffer,
530
+ width,
531
+ height
532
+ });
533
+ // Set source aspect ratio on panel
534
+ const panel = this.texturePanels.get(name);
535
+ if (panel) {
536
+ panel.setSourceSize(width, height);
537
+ }
538
+ }
403
539
 
404
- if (available && !disjoint) {
405
- const elapsed = this.gl.getQueryParameter(queryInfo.query, this.gl.QUERY_RESULT);
406
- const duration = elapsed * 1e-6; // Convert nanoseconds to milliseconds
407
- this.totalGpuDuration += duration;
408
- this.gl.deleteQuery(queryInfo.query);
409
- this.gpuQueries.splice(index, 1); // Remove the processed query
540
+ /**
541
+ * Set texture from ImageBitmap (for worker mode)
542
+ * @param name - Panel name
543
+ * @param bitmap - ImageBitmap transferred from worker
544
+ * @param sourceWidth - Optional source texture width for aspect ratio
545
+ * @param sourceHeight - Optional source texture height for aspect ratio
546
+ */
547
+ public setTextureBitmap(name: string, bitmap: ImageBitmap, sourceWidth?: number, sourceHeight?: number): void {
548
+ const panel = this.texturePanels.get(name);
549
+ if (panel) {
550
+ // Set source size for proper aspect ratio if provided
551
+ if (sourceWidth !== undefined && sourceHeight !== undefined) {
552
+ panel.setSourceSize(sourceWidth, sourceHeight);
553
+ }
554
+ panel.updateTexture(bitmap);
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Remove a texture panel
560
+ * @param name - Panel name to remove
561
+ */
562
+ public removeTexturePanel(name: string): void {
563
+ const panel = this.texturePanels.get(name);
564
+ if (panel) {
565
+ panel.dispose();
566
+ panel.canvas.remove();
567
+ this.texturePanels.delete(name);
568
+ this.textureSourcesWebGL.delete(name);
569
+ this.textureSourcesWebGPU.delete(name);
570
+ }
571
+ }
572
+
573
+ /**
574
+ * Capture and update all texture panels
575
+ * Called automatically during renderPanels at graphsPerSecond rate
576
+ */
577
+ private async updateTexturePanels(): Promise<void> {
578
+ // Check for renderer dimension changes every frame
579
+ this.updateTexturePreviewDimensions();
580
+
581
+ // Update WebGL textures
582
+ if (this.textureCaptureWebGL) {
583
+ for (const [name, source] of this.textureSourcesWebGL) {
584
+ const panel = this.texturePanels.get(name);
585
+ if (panel) {
586
+ // Re-extract framebuffer for Three.js targets (may change each frame)
587
+ let framebuffer = source.framebuffer;
588
+ let width = source.width;
589
+ let height = source.height;
590
+
591
+ if (source.target.isWebGLRenderTarget && source.target.__webglFramebuffer) {
592
+ framebuffer = source.target.__webglFramebuffer;
593
+ width = source.target.width || width;
594
+ height = source.target.height || height;
595
+ }
596
+
597
+ const bitmap = await this.textureCaptureWebGL.capture(framebuffer, width, height, name);
598
+ if (bitmap) {
599
+ panel.updateTexture(bitmap);
600
+ }
410
601
  }
411
602
  }
412
- });
603
+ }
413
604
 
605
+ // Update WebGPU textures
606
+ if (this.textureCaptureWebGPU) {
607
+ for (const [name, gpuTexture] of this.textureSourcesWebGPU) {
608
+ const panel = this.texturePanels.get(name);
609
+ if (panel) {
610
+ const bitmap = await this.textureCaptureWebGPU.capture(gpuTexture);
611
+ if (bitmap) {
612
+ panel.updateTexture(bitmap);
613
+ }
614
+ }
615
+ }
616
+ }
414
617
  }
618
+
619
+ /**
620
+ * Capture StatsGL nodes registered by the addon
621
+ */
622
+ private captureStatsGLNodes(): void {
623
+ const captures = (this as any)._statsGLCaptures as Map<string, any> | undefined;
624
+ if (!captures || captures.size === 0 || !this.renderer) return;
625
+
626
+ for (const captureData of captures.values()) {
627
+ if (captureData.capture) {
628
+ captureData.capture(this.renderer);
629
+ }
630
+ }
631
+ }
632
+
415
633
  private detectVSync(currentTime: number): void {
416
634
  if (this.lastFrameTime === 0) {
417
635
  this.lastFrameTime = currentTime;
418
636
  return;
419
637
  }
420
638
 
421
- // Calculate frame time
422
639
  const frameTime = currentTime - this.lastFrameTime;
423
640
  this.lastFrameTime = currentTime;
424
641
 
425
- // Add to histories
426
642
  this.frameTimeHistory.push(frameTime);
427
643
  if (this.frameTimeHistory.length > this.HISTORY_SIZE) {
428
644
  this.frameTimeHistory.shift();
429
645
  }
430
646
 
431
- // Only start detection when we have enough samples
432
647
  if (this.frameTimeHistory.length < 60) return;
433
648
 
434
- // Calculate average frame time
435
649
  const avgFrameTime = this.frameTimeHistory.reduce((a, b) => a + b) / this.frameTimeHistory.length;
436
650
 
437
- // Calculate frame time stability (standard deviation)
438
651
  const variance = this.frameTimeHistory.reduce((acc, time) =>
439
652
  acc + Math.pow(time - avgFrameTime, 2), 0) / this.frameTimeHistory.length;
440
653
  const stability = Math.sqrt(variance);
441
654
 
442
- // Only proceed if frame timing is relatively stable
443
- if (stability > 2) { // 2ms stability threshold
655
+ if (stability > 2) {
444
656
  this.detectedVSync = null;
445
657
  return;
446
658
  }
447
659
 
448
- // Find the closest VSync rate based on frame time
449
660
  let closestMatch: VSyncInfo | null = null;
450
661
  let smallestDiff = Infinity;
451
662
 
@@ -462,54 +673,6 @@ class Stats {
462
673
  } else {
463
674
  this.detectedVSync = null;
464
675
  }
465
-
466
- }
467
-
468
- endInternal() {
469
- const currentTime = performance.now();
470
-
471
- this.frameTimes.push(currentTime);
472
-
473
- // Remove frames older than 1 second
474
- while (this.frameTimes.length > 0 && this.frameTimes[0] <= currentTime - 1000) {
475
- this.frameTimes.shift();
476
- }
477
-
478
- // Calculate FPS based on frames in the last second
479
- const fps = Math.round(this.frameTimes.length);
480
-
481
- this.addToAverage(fps, this.averageFps);
482
-
483
- const shouldUpdateText = currentTime >= this.prevTextTime + 1000 / this.logsPerSecond;
484
- const shouldUpdateGraph = currentTime >= this.prevGraphTime + 1000 / this.graphsPerSecond;
485
-
486
- this.updatePanelComponents(this.fpsPanel, this.averageFps, 0, shouldUpdateText, shouldUpdateGraph);
487
- this.updatePanelComponents(this.msPanel, this.averageCpu, this.precision, shouldUpdateText, shouldUpdateGraph);
488
- if (this.gpuPanel) {
489
- this.updatePanelComponents(this.gpuPanel, this.averageGpu, this.precision, shouldUpdateText, shouldUpdateGraph);
490
- }
491
- if (this.trackCPT && this.gpuPanelCompute) {
492
- this.updatePanelComponents(this.gpuPanelCompute, this.averageGpuCompute, this.precision, shouldUpdateText, shouldUpdateGraph);
493
- }
494
-
495
- if (shouldUpdateText) {
496
- this.prevTextTime = currentTime;
497
- }
498
- if (shouldUpdateGraph) {
499
- this.prevGraphTime = currentTime;
500
- }
501
-
502
- if (this.vsyncPanel !== null) {
503
- this.detectVSync(currentTime);
504
-
505
- const vsyncValue = this.detectedVSync?.refreshRate || 0;
506
-
507
- if (shouldUpdateText && vsyncValue > 0) {
508
- this.vsyncPanel.update(vsyncValue, vsyncValue);
509
- }
510
- }
511
-
512
- return currentTime;
513
676
  }
514
677
 
515
678
  private updatePanelComponents(
@@ -517,25 +680,26 @@ class Stats {
517
680
  averageArray: { logs: number[], graph: number[] },
518
681
  precision: number,
519
682
  shouldUpdateText: boolean,
520
- shouldUpdateGraph: boolean
683
+ shouldUpdateGraph: boolean,
684
+ suffix = ''
521
685
  ) {
522
686
  if (!panel || averageArray.logs.length === 0) return;
523
687
 
524
- // Initialize tracking for this panel if not exists
525
- if (!(panel.name in this.lastMin)) {
526
- this.lastMin[panel.name] = Infinity;
527
- this.lastMax[panel.name] = 0;
528
- this.lastValue[panel.name] = 0;
688
+ // Use panel.id as key to avoid collision between panels with same name
689
+ const key = String(panel.id);
690
+
691
+ if (!(key in this.lastMin)) {
692
+ this.lastMin[key] = Infinity;
693
+ this.lastMax[key] = 0;
694
+ this.lastValue[key] = 0;
529
695
  }
530
696
 
531
697
  const currentValue = averageArray.logs[averageArray.logs.length - 1];
532
698
 
533
- this.lastMax[panel.name] = Math.max(...averageArray.logs);
534
- this.lastMin[panel.name] = Math.min(this.lastMin[panel.name], currentValue);
535
- // Smooth the display value
536
- this.lastValue[panel.name] = this.lastValue[panel.name] * 0.7 + currentValue * 0.3;
699
+ this.lastMax[key] = Math.max(...averageArray.logs);
700
+ this.lastMin[key] = Math.min(this.lastMin[key], currentValue);
701
+ this.lastValue[key] = this.lastValue[key] * 0.7 + currentValue * 0.3;
537
702
 
538
- // Calculate graph max considering both recent values and graph history
539
703
  const graphMax = Math.max(
540
704
  Math.max(...averageArray.logs),
541
705
  ...averageArray.graph.slice(-this.samplesGraph)
@@ -543,16 +707,15 @@ class Stats {
543
707
 
544
708
  this.updateCounter++;
545
709
 
546
- // Update text if it's time
547
710
  if (shouldUpdateText) {
548
711
  panel.update(
549
- this.lastValue[panel.name],
550
- this.lastMax[panel.name],
551
- precision
712
+ this.lastValue[key],
713
+ this.lastMax[key],
714
+ precision,
715
+ suffix
552
716
  );
553
717
  }
554
718
 
555
- // Update graph if it's time
556
719
  if (shouldUpdateGraph) {
557
720
  panel.updateGraph(
558
721
  currentValue,
@@ -561,84 +724,35 @@ class Stats {
561
724
  }
562
725
  }
563
726
 
564
- private beginProfiling(marker: string): void {
565
- if (window.performance) {
566
- try {
567
- window.performance.clearMarks(marker);
568
- window.performance.mark(marker);
569
- } catch (error) {
570
- console.debug('Stats: Performance marking failed:', error);
571
- }
572
- }
573
- }
574
-
575
- private endProfiling(startMarker: string | PerformanceMeasureOptions | undefined, endMarker: string | undefined, measureName: string): void {
576
- if (!window.performance || !endMarker || !startMarker) return;
577
-
578
- try {
579
- // First check if the start mark exists
580
- const entries = window.performance.getEntriesByName(startMarker as string, 'mark');
581
- if (entries.length === 0) {
582
- // If start mark doesn't exist, create it now with the same timestamp as end
583
- this.beginProfiling(startMarker as string);
584
- }
585
-
586
- // Create the end mark
587
- window.performance.clearMarks(endMarker);
588
- window.performance.mark(endMarker);
589
-
590
- // Clear any existing measure with the same name
591
- window.performance.clearMeasures(measureName);
592
-
593
- // Create the measurement
594
- const cpuMeasure = performance.measure(measureName, startMarker, endMarker);
595
- this.totalCpuDuration += cpuMeasure.duration;
596
-
597
- // Clean up
598
- window.performance.clearMarks(startMarker as string);
599
- window.performance.clearMarks(endMarker);
600
- window.performance.clearMeasures(measureName);
601
- } catch (error) {
602
- console.debug('Stats: Performance measurement failed:', error);
603
- }
604
- }
605
-
606
727
  updatePanel(panel: { update: any; updateGraph: any; name: string; } | null, averageArray: { logs: number[], graph: number[] }, precision = 2) {
607
728
  if (!panel || averageArray.logs.length === 0) return;
608
729
 
609
730
  const currentTime = performance.now();
610
731
 
611
- // Initialize tracking for this panel if not exists
612
732
  if (!(panel.name in this.lastMin)) {
613
733
  this.lastMin[panel.name] = Infinity;
614
734
  this.lastMax[panel.name] = 0;
615
735
  this.lastValue[panel.name] = 0;
616
736
  }
617
737
 
618
- // Get the current value and recent max
619
738
  const currentValue = averageArray.logs[averageArray.logs.length - 1];
620
739
  const recentMax = Math.max(...averageArray.logs.slice(-30));
621
740
 
622
- // Update running statistics
623
741
  this.lastMin[panel.name] = Math.min(this.lastMin[panel.name], currentValue);
624
742
  this.lastMax[panel.name] = Math.max(this.lastMax[panel.name], currentValue);
625
743
 
626
- // Smooth the display value
627
744
  this.lastValue[panel.name] = this.lastValue[panel.name] * 0.7 + currentValue * 0.3;
628
745
 
629
- // Calculate graph scaling value
630
746
  const graphMax = Math.max(recentMax, ...averageArray.graph.slice(-this.samplesGraph));
631
747
 
632
748
  this.updateCounter++;
633
749
 
634
- // Reset min/max periodically
635
750
  if (this.updateCounter % (this.logsPerSecond * 2) === 0) {
636
751
  this.lastMax[panel.name] = recentMax;
637
752
  this.lastMin[panel.name] = currentValue;
638
753
  }
639
754
 
640
755
  if (panel.update) {
641
- // Check if it's time to update the text (based on logsPerSecond)
642
756
  if (currentTime >= this.prevCpuTime + 1000 / this.logsPerSecond) {
643
757
  panel.update(
644
758
  this.lastValue[panel.name],
@@ -649,7 +763,6 @@ class Stats {
649
763
  );
650
764
  }
651
765
 
652
- // Check if it's time to update the graph (based on graphsPerSecond)
653
766
  if (currentTime >= this.prevGraphTime + 1000 / this.graphsPerSecond) {
654
767
  panel.updateGraph(
655
768
  currentValue,
@@ -660,82 +773,80 @@ class Stats {
660
773
  }
661
774
  }
662
775
 
663
- private updateAverages(): void {
664
-
665
- this.addToAverage(this.totalCpuDuration, this.averageCpu);
666
- this.addToAverage(this.totalGpuDuration, this.averageGpu);
667
- // Add GPU Compute to the main update flow
668
- if (this.info && this.totalGpuDurationCompute !== undefined) {
669
- this.addToAverage(this.totalGpuDurationCompute, this.averageGpuCompute);
670
- }
671
- }
672
-
673
- addToAverage(value: number, averageArray: { logs: any; graph: any; }) {
674
- // Validate value
675
- // if (value === undefined || value === null || isNaN(value) || value === 0) {
676
- // return;
677
- // }
678
-
679
- // Store raw values for logs
680
- averageArray.logs.push(value);
681
- if (averageArray.logs.length > this.samplesLog) {
682
- averageArray.logs = averageArray.logs.slice(-this.samplesLog);
683
- }
684
-
685
- // For graph, store raw values
686
- averageArray.graph.push(value);
687
- if (averageArray.graph.length > this.samplesGraph) {
688
- averageArray.graph = averageArray.graph.slice(-this.samplesGraph);
689
- }
690
- }
691
-
692
776
  get domElement() {
693
- // patch for some use case in threejs
694
777
  return this.dom;
695
-
696
778
  }
697
779
 
698
- patchThreeWebGPU(renderer: any) {
699
-
700
- const originalAnimationLoop = renderer.info.reset
701
-
702
- const statsInstance = this;
703
-
704
- renderer.info.reset = function () {
705
-
706
- statsInstance.beginProfiling('cpu-started');
707
-
708
- originalAnimationLoop.call(this);
709
-
780
+ /**
781
+ * Dispose of all resources. Call when done using Stats.
782
+ */
783
+ public override dispose(): void {
784
+ // Remove event listeners
785
+ if (this.minimal) {
786
+ this.dom.removeEventListener('click', this.handleClick);
787
+ } else {
788
+ window.removeEventListener('resize', this.handleResize);
710
789
  }
711
790
 
712
- }
713
-
714
- patchThreeRenderer(renderer: any) {
715
-
716
- // Store the original render method
717
- const originalRenderMethod = renderer.render;
718
-
719
- // Reference to the stats instance
720
- const statsInstance = this;
721
-
722
- // Override the render method on the prototype
723
- renderer.render = function (scene: THREE.Scene, camera: THREE.Camera) {
724
-
725
- statsInstance.begin(); // Start tracking for this render call
791
+ // Dispose texture capture helpers
792
+ if (this.textureCaptureWebGL) {
793
+ this.textureCaptureWebGL.dispose();
794
+ this.textureCaptureWebGL = null;
795
+ }
796
+ if (this.textureCaptureWebGPU) {
797
+ this.textureCaptureWebGPU.dispose();
798
+ this.textureCaptureWebGPU = null;
799
+ }
726
800
 
727
- // Call the original render method
728
- originalRenderMethod.call(this, scene, camera);
801
+ // Dispose all texture panels
802
+ for (const panel of this.texturePanels.values()) {
803
+ panel.dispose();
804
+ }
805
+ this.texturePanels.clear();
806
+ this.textureSourcesWebGL.clear();
807
+ this.textureSourcesWebGPU.clear();
729
808
 
730
- statsInstance.end(); // End tracking for this render call
809
+ // Dispose StatsGL captures if any
810
+ const captures = (this as any)._statsGLCaptures as Map<string, any> | undefined;
811
+ if (captures) {
812
+ for (const captureData of captures.values()) {
813
+ if (captureData.dispose) {
814
+ captureData.dispose();
815
+ }
816
+ }
817
+ captures.clear();
818
+ }
731
819
 
732
- };
820
+ // Remove DOM element
821
+ if (this.texturePanelRow) {
822
+ this.texturePanelRow.remove();
823
+ this.texturePanelRow = null;
824
+ }
825
+ this.dom.remove();
733
826
 
827
+ // Clear panel references
828
+ this.fpsPanel = null;
829
+ this.msPanel = null;
830
+ this.gpuPanel = null;
831
+ this.gpuPanelCompute = null;
832
+ this.vsyncPanel = null;
833
+ this.workerCpuPanel = null;
734
834
 
735
- this.threeRendererPatched = true;
835
+ // Clear tracking arrays
836
+ this.frameTimeHistory.length = 0;
837
+ this.averageWorkerCpu.logs.length = 0;
838
+ this.averageWorkerCpu.graph.length = 0;
736
839
 
840
+ // Call parent dispose
841
+ super.dispose();
737
842
  }
738
843
  }
739
844
 
740
845
 
741
846
  export default Stats;
847
+ export type { StatsData } from './core';
848
+ export { StatsProfiler } from './profiler';
849
+ export { PanelTexture } from './panelTexture';
850
+ export { TextureCaptureWebGL, TextureCaptureWebGPU } from './textureCapture';
851
+ export { StatsGLCapture } from './statsGLNode';
852
+