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/panel.ts CHANGED
@@ -105,7 +105,7 @@ class Panel {
105
105
  }
106
106
 
107
107
  // Update only text portion
108
- public update(value: number, maxValue: number, decimals: number = 0) {
108
+ public update(value: number, maxValue: number, decimals: number = 0, suffix: string = '') {
109
109
  if (!this.context || !this.gradient) return;
110
110
 
111
111
  const min = Math.min(Infinity, value);
@@ -116,11 +116,25 @@ class Panel {
116
116
  this.context.fillStyle = this.bg;
117
117
  this.context.fillRect(0, 0, this.WIDTH, this.GRAPH_Y);
118
118
 
119
- // Draw text
119
+ // Draw value and name
120
+ const valueAndName = `${value.toFixed(decimals)} ${this.name}`;
121
+ this.context.fillStyle = this.fg;
122
+ this.context.fillText(valueAndName, this.TEXT_X, this.TEXT_Y);
123
+
124
+ let textX = this.TEXT_X + this.context.measureText(valueAndName).width;
125
+
126
+ // Draw suffix in orange if present
127
+ if (suffix) {
128
+ this.context.fillStyle = '#f90';
129
+ this.context.fillText(suffix, textX, this.TEXT_Y);
130
+ textX += this.context.measureText(suffix).width;
131
+ }
132
+
133
+ // Draw range
120
134
  this.context.fillStyle = this.fg;
121
135
  this.context.fillText(
122
- `${value.toFixed(decimals)} ${this.name} (${min.toFixed(decimals)}-${parseFloat(max.toFixed(decimals))})`,
123
- this.TEXT_X,
136
+ ` (${min.toFixed(decimals)}-${parseFloat(max.toFixed(decimals))})`,
137
+ textX,
124
138
  this.TEXT_Y
125
139
  );
126
140
  }
@@ -0,0 +1,122 @@
1
+ import { Panel } from './panel';
2
+
3
+ class PanelTexture extends Panel {
4
+ private currentBitmap: ImageBitmap | null = null;
5
+ private sourceAspect: number = 1; // Source texture aspect ratio (width/height)
6
+
7
+ constructor(name: string) {
8
+ super(name, '#fff', '#111');
9
+ this.initializeCanvas();
10
+ }
11
+
12
+ public override initializeCanvas() {
13
+ if (!this.context) return;
14
+
15
+ this.context.imageSmoothingEnabled = true;
16
+ this.context.font = 'bold ' + (9 * this.PR) + 'px Helvetica,Arial,sans-serif';
17
+ this.context.textBaseline = 'top';
18
+
19
+ // Fill entire panel with black background for texture display
20
+ this.context.fillStyle = '#000';
21
+ this.context.fillRect(0, 0, this.WIDTH, this.HEIGHT);
22
+
23
+ // Draw label overlay
24
+ this.drawLabelOverlay();
25
+ }
26
+
27
+ private drawLabelOverlay(): void {
28
+ if (!this.context) return;
29
+
30
+ // Semi-transparent background for label
31
+ this.context.fillStyle = 'rgba(0, 0, 0, 0.5)';
32
+ this.context.fillRect(0, 0, this.WIDTH, this.GRAPH_Y);
33
+
34
+ // Draw label text
35
+ this.context.fillStyle = this.fg;
36
+ this.context.fillText(this.name, this.TEXT_X, this.TEXT_Y);
37
+ }
38
+
39
+ /**
40
+ * Set the source texture aspect ratio for proper display
41
+ * @param width - Source texture width
42
+ * @param height - Source texture height
43
+ */
44
+ public setSourceSize(width: number, height: number): void {
45
+ this.sourceAspect = width / height;
46
+ }
47
+
48
+ public updateTexture(bitmap: ImageBitmap): void {
49
+ if (!this.context) return;
50
+
51
+ // Close previous bitmap to release GPU resources
52
+ if (this.currentBitmap) {
53
+ this.currentBitmap.close();
54
+ }
55
+ this.currentBitmap = bitmap;
56
+
57
+ // Clear entire panel
58
+ this.context.fillStyle = '#000';
59
+ this.context.fillRect(0, 0, this.WIDTH, this.HEIGHT);
60
+
61
+ // Calculate destination rect maintaining aspect ratio for full panel
62
+ const panelAspect = this.WIDTH / this.HEIGHT;
63
+ let destWidth: number;
64
+ let destHeight: number;
65
+ let destX: number;
66
+ let destY: number;
67
+
68
+ if (this.sourceAspect > panelAspect) {
69
+ // Source is wider than panel - fit to width, letterbox top/bottom
70
+ destWidth = this.WIDTH;
71
+ destHeight = this.WIDTH / this.sourceAspect;
72
+ destX = 0;
73
+ destY = (this.HEIGHT - destHeight) / 2;
74
+ } else {
75
+ // Source is taller than panel - fit to height, pillarbox left/right
76
+ destHeight = this.HEIGHT;
77
+ destWidth = this.HEIGHT * this.sourceAspect;
78
+ destX = (this.WIDTH - destWidth) / 2;
79
+ destY = 0;
80
+ }
81
+
82
+ // Draw the bitmap with proper aspect ratio
83
+ this.context.drawImage(
84
+ bitmap,
85
+ destX,
86
+ destY,
87
+ destWidth,
88
+ destHeight
89
+ );
90
+
91
+ // Draw label overlay on top
92
+ this.drawLabelOverlay();
93
+ }
94
+
95
+ public setLabel(label: string): void {
96
+ this.name = label;
97
+ // Redraw label overlay with new name
98
+ this.drawLabelOverlay();
99
+ }
100
+
101
+ // Override update - not used for texture panels
102
+ public override update(_value: number, _maxValue: number, _decimals: number = 0, _suffix: string = ''): void {
103
+ // No-op for texture panels
104
+ }
105
+
106
+ // Override updateGraph - not used for texture panels
107
+ public override updateGraph(_valueGraph: number, _maxGraph: number): void {
108
+ // No-op for texture panels
109
+ }
110
+
111
+ /**
112
+ * Dispose of resources
113
+ */
114
+ public dispose(): void {
115
+ if (this.currentBitmap) {
116
+ this.currentBitmap.close();
117
+ this.currentBitmap = null;
118
+ }
119
+ }
120
+ }
121
+
122
+ export { PanelTexture };
@@ -0,0 +1,124 @@
1
+ import { StatsCore, StatsCoreOptions, StatsData } from './core';
2
+ import {
3
+ TextureCaptureWebGL,
4
+ TextureCaptureWebGPU,
5
+ extractWebGLSource,
6
+ extractWebGPUSource,
7
+ ThreeTextureSource
8
+ } from './textureCapture';
9
+
10
+ export interface StatsProfilerOptions extends StatsCoreOptions {}
11
+
12
+ export class StatsProfiler extends StatsCore {
13
+ private textureCaptureWebGL: TextureCaptureWebGL | null = null;
14
+ private textureCaptureWebGPU: TextureCaptureWebGPU | null = null;
15
+
16
+ constructor(options: StatsProfilerOptions = {}) {
17
+ super(options);
18
+ }
19
+
20
+ public update(): void {
21
+ this.endProfiling('cpu-started', 'cpu-finished', 'cpu-duration');
22
+
23
+ if (!this.info) {
24
+ this.processGpuQueries();
25
+ } else {
26
+ this.processWebGPUTimestamps();
27
+ }
28
+
29
+ const fps = this.calculateFps();
30
+ this.addToAverage(fps, this.averageFps);
31
+
32
+ this.updateAverages();
33
+ this.resetCounters();
34
+ }
35
+
36
+ public getData(): StatsData {
37
+ return super.getData();
38
+ }
39
+
40
+ /**
41
+ * Capture a texture/render target to ImageBitmap for transfer to main thread
42
+ * @param source - Three.js RenderTarget, GPUTexture, or WebGLFramebuffer with dimensions
43
+ * @param sourceId - Unique identifier for this texture source (for per-source PBO buffering)
44
+ * @returns ImageBitmap suitable for postMessage transfer
45
+ */
46
+ public async captureTexture(
47
+ source: ThreeTextureSource | { framebuffer: WebGLFramebuffer; width: number; height: number } | any,
48
+ sourceId: string = 'default'
49
+ ): Promise<ImageBitmap | null> {
50
+ // Handle WebGL sources
51
+ if (this.gl) {
52
+ if (!this.textureCaptureWebGL) {
53
+ this.textureCaptureWebGL = new TextureCaptureWebGL(this.gl);
54
+ }
55
+
56
+ // Three.js WebGLRenderTarget
57
+ if ((source as ThreeTextureSource).isWebGLRenderTarget) {
58
+ const webglSource = extractWebGLSource(source as ThreeTextureSource, this.gl);
59
+ if (webglSource) {
60
+ return this.textureCaptureWebGL.capture(
61
+ webglSource.framebuffer,
62
+ webglSource.width,
63
+ webglSource.height,
64
+ sourceId
65
+ );
66
+ }
67
+ }
68
+
69
+ // Raw framebuffer with dimensions
70
+ if (source.framebuffer && source.width && source.height) {
71
+ return this.textureCaptureWebGL.capture(
72
+ source.framebuffer,
73
+ source.width,
74
+ source.height,
75
+ sourceId
76
+ );
77
+ }
78
+ }
79
+
80
+ // Handle WebGPU sources
81
+ if (this.gpuDevice) {
82
+ if (!this.textureCaptureWebGPU) {
83
+ this.textureCaptureWebGPU = new TextureCaptureWebGPU(this.gpuDevice);
84
+ }
85
+
86
+ // Three.js WebGPU RenderTarget
87
+ if ((source as ThreeTextureSource).isRenderTarget && this.gpuBackend) {
88
+ const gpuTexture = extractWebGPUSource(source as ThreeTextureSource, this.gpuBackend);
89
+ if (gpuTexture) {
90
+ return this.textureCaptureWebGPU.capture(gpuTexture);
91
+ }
92
+ }
93
+
94
+ // Raw GPUTexture
95
+ if (source && typeof source.createView === 'function') {
96
+ return this.textureCaptureWebGPU.capture(source);
97
+ }
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ /**
104
+ * Dispose texture capture resources
105
+ */
106
+ public disposeTextureCapture(): void {
107
+ if (this.textureCaptureWebGL) {
108
+ this.textureCaptureWebGL.dispose();
109
+ this.textureCaptureWebGL = null;
110
+ }
111
+ if (this.textureCaptureWebGPU) {
112
+ this.textureCaptureWebGPU.dispose();
113
+ this.textureCaptureWebGPU = null;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Dispose of all resources
119
+ */
120
+ public override dispose(): void {
121
+ this.disposeTextureCapture();
122
+ super.dispose();
123
+ }
124
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * TSL Node capture utilities for stats-gl
3
+ *
4
+ * For WebGPU TSL node capture with proper Node integration, use the addon:
5
+ * import { statsGL } from 'stats-gl/addons/StatsGLNode.js';
6
+ *
7
+ * This file provides a simpler capture system that doesn't require
8
+ * extending Three.js Node class.
9
+ */
10
+
11
+ interface StatsGLNodeData {
12
+ canvas: HTMLCanvasElement | OffscreenCanvas;
13
+ canvasTarget: any;
14
+ quad: any;
15
+ material: any;
16
+ node: any;
17
+ }
18
+
19
+ /**
20
+ * Manages TSL node capture for stats-gl (used internally)
21
+ */
22
+ export class StatsGLCapture {
23
+ nodes: Map<string, StatsGLNodeData> = new Map();
24
+ width = 90;
25
+ height = 48;
26
+ private THREE: any;
27
+
28
+ constructor(THREE: any, width = 90, height = 48) {
29
+ this.THREE = THREE;
30
+ this.width = width;
31
+ this.height = height;
32
+ }
33
+
34
+ /**
35
+ * Update capture dimensions (e.g., on resize)
36
+ */
37
+ resize(width: number, height: number): void {
38
+ this.width = width;
39
+ this.height = height;
40
+
41
+ // Update all existing nodes
42
+ for (const [name, data] of this.nodes) {
43
+ if (data.canvas instanceof HTMLCanvasElement) {
44
+ data.canvas.width = width;
45
+ data.canvas.height = height;
46
+ } else if (data.canvas instanceof OffscreenCanvas) {
47
+ // OffscreenCanvas can't be resized - recreate
48
+ const newCanvas = new OffscreenCanvas(width, height);
49
+ data.canvas = newCanvas;
50
+ data.canvasTarget.setCanvas?.(newCanvas);
51
+ }
52
+ data.canvasTarget.setSize(width, height);
53
+ }
54
+ }
55
+
56
+ register(name: string, targetNode: any): StatsGLNodeData {
57
+ if (this.nodes.has(name)) return this.nodes.get(name)!;
58
+
59
+ const { CanvasTarget, NodeMaterial, QuadMesh, NoToneMapping, LinearSRGBColorSpace } = this.THREE;
60
+ const { renderOutput, vec3, vec4 } = this.THREE;
61
+
62
+ const canvas = typeof OffscreenCanvas !== 'undefined'
63
+ ? new OffscreenCanvas(this.width, this.height)
64
+ : document.createElement('canvas');
65
+ if (canvas instanceof HTMLCanvasElement) {
66
+ canvas.width = this.width;
67
+ canvas.height = this.height;
68
+ }
69
+
70
+ const canvasTarget = new CanvasTarget(canvas);
71
+ canvasTarget.setSize(this.width, this.height);
72
+
73
+ const material = new NodeMaterial();
74
+ material.outputNode = renderOutput(
75
+ vec4(vec3(targetNode), 1),
76
+ NoToneMapping,
77
+ LinearSRGBColorSpace
78
+ );
79
+
80
+ const quad = new QuadMesh(material);
81
+ const data: StatsGLNodeData = { canvas, canvasTarget, quad, material, node: targetNode };
82
+ this.nodes.set(name, data);
83
+ return data;
84
+ }
85
+
86
+ async capture(name: string, renderer: any): Promise<ImageBitmap | null> {
87
+ const data = this.nodes.get(name);
88
+ if (!data) return null;
89
+
90
+ try {
91
+ data.quad.render(renderer, data.canvasTarget);
92
+ return await createImageBitmap(data.canvas);
93
+ } catch (e) {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ remove(name: string): void {
99
+ const data = this.nodes.get(name);
100
+ if (data) {
101
+ // Dispose material if it has dispose method
102
+ if (data.material && data.material.dispose) {
103
+ data.material.dispose();
104
+ }
105
+ // Remove canvas from DOM if attached
106
+ if (data.canvas instanceof HTMLCanvasElement && data.canvas.parentNode) {
107
+ data.canvas.parentNode.removeChild(data.canvas);
108
+ }
109
+ this.nodes.delete(name);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Dispose all capture resources
115
+ */
116
+ dispose(): void {
117
+ // Copy keys to array to avoid modifying map while iterating
118
+ const names = Array.from(this.nodes.keys());
119
+ for (const name of names) {
120
+ this.remove(name);
121
+ }
122
+ this.nodes.clear();
123
+ }
124
+ }