stats-gl 3.8.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +360 -86
- package/addons/StatsGLNode.js +201 -0
- package/addons/StatsGLNodeWorker.js +198 -0
- package/dist/Flamingo.glb +0 -0
- package/dist/LittlestTokyo.glb +0 -0
- package/dist/core.cjs +421 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.js +421 -0
- package/dist/core.js.map +1 -0
- package/dist/main.cjs +406 -241
- package/dist/main.cjs.map +1 -1
- package/dist/main.js +404 -240
- package/dist/main.js.map +1 -1
- package/dist/panel.cjs +12 -3
- package/dist/panel.cjs.map +1 -1
- package/dist/panel.js +12 -3
- package/dist/panel.js.map +1 -1
- package/dist/panelTexture.cjs +93 -0
- package/dist/panelTexture.cjs.map +1 -0
- package/dist/panelTexture.js +93 -0
- package/dist/panelTexture.js.map +1 -0
- package/dist/profiler.cjs +95 -0
- package/dist/profiler.cjs.map +1 -0
- package/dist/profiler.js +95 -0
- package/dist/profiler.js.map +1 -0
- package/dist/stats-gl.d.ts +332 -71
- package/dist/statsGLNode.cjs +89 -0
- package/dist/statsGLNode.cjs.map +1 -0
- package/dist/statsGLNode.js +89 -0
- package/dist/statsGLNode.js.map +1 -0
- package/dist/textureCapture.cjs +283 -0
- package/dist/textureCapture.cjs.map +1 -0
- package/dist/textureCapture.js +283 -0
- package/dist/textureCapture.js.map +1 -0
- package/lib/core.ts +579 -0
- package/lib/main.ts +506 -401
- package/lib/panel.ts +18 -4
- package/lib/panelTexture.ts +122 -0
- package/lib/profiler.ts +124 -0
- package/lib/statsGLNode.ts +124 -0
- package/lib/textureCapture.ts +403 -0
- package/lib/webgpu.d.ts +190 -0
- package/package.json +9 -5
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
|
|
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
|
-
|
|
123
|
-
|
|
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 };
|
package/lib/profiler.ts
ADDED
|
@@ -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
|
+
}
|