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