@zkmake/three-meter 0.1.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.
@@ -0,0 +1,263 @@
1
+ //#region src/core/gpu-timer.ts
2
+ var GpuTimer = class GpuTimer {
3
+ lastMs = 0;
4
+ webgpu;
5
+ webgpuInFlight = false;
6
+ gl;
7
+ ext;
8
+ free = [];
9
+ pending = [];
10
+ active = null;
11
+ constructor(webgpu, gl, ext, poolSize) {
12
+ this.webgpu = webgpu;
13
+ this.gl = gl;
14
+ this.ext = ext;
15
+ if (!gl) return;
16
+ for (let index = 0; index < poolSize; index += 1) {
17
+ const query = gl.createQuery();
18
+ if (query) this.free.push(query);
19
+ }
20
+ }
21
+ static create(renderer, poolSize) {
22
+ if (renderer.backend?.trackTimestamp && renderer.resolveTimestampsAsync) return new GpuTimer(renderer, null, null, poolSize);
23
+ const context = renderer.getContext?.();
24
+ if (!(context instanceof WebGL2RenderingContext)) return null;
25
+ const ext = context.getExtension("EXT_disjoint_timer_query_webgl2");
26
+ return ext ? new GpuTimer(null, context, ext, poolSize) : null;
27
+ }
28
+ begin() {
29
+ if (this.webgpu || !this.gl || !this.ext) return;
30
+ const query = this.free.pop();
31
+ if (!query) {
32
+ this.active = null;
33
+ return;
34
+ }
35
+ this.active = query;
36
+ this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, query);
37
+ }
38
+ end() {
39
+ if (this.webgpu || !this.gl || !this.ext || !this.active) return;
40
+ this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);
41
+ this.pending.push(this.active);
42
+ this.active = null;
43
+ }
44
+ poll() {
45
+ if (this.webgpu) {
46
+ this.pollWebgpu();
47
+ return;
48
+ }
49
+ this.pollWebgl();
50
+ }
51
+ getMs() {
52
+ return this.lastMs;
53
+ }
54
+ dispose() {
55
+ if (this.gl && this.ext && this.active) {
56
+ this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);
57
+ this.active = null;
58
+ }
59
+ if (this.gl) {
60
+ for (const query of this.free) this.gl.deleteQuery(query);
61
+ for (const query of this.pending) this.gl.deleteQuery(query);
62
+ }
63
+ this.free.length = 0;
64
+ this.pending.length = 0;
65
+ }
66
+ pollWebgpu() {
67
+ const resolve = this.webgpu?.resolveTimestampsAsync;
68
+ if (!resolve || this.webgpuInFlight) return;
69
+ this.webgpuInFlight = true;
70
+ resolve.call(this.webgpu, "render").then((ms) => {
71
+ if (typeof ms === "number" && Number.isFinite(ms)) this.lastMs = ms;
72
+ this.webgpuInFlight = false;
73
+ }).catch(() => {
74
+ this.webgpuInFlight = false;
75
+ });
76
+ }
77
+ pollWebgl() {
78
+ if (!this.gl || !this.ext) return;
79
+ if (this.gl.getParameter(this.ext.GPU_DISJOINT_EXT)) {
80
+ while (this.pending.length > 0) this.free.push(this.pending.shift());
81
+ return;
82
+ }
83
+ while (this.pending.length > 0) {
84
+ const query = this.pending[0];
85
+ if (!this.gl.getQueryParameter(query, this.gl.QUERY_RESULT_AVAILABLE)) break;
86
+ this.pending.shift();
87
+ const ns = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT);
88
+ this.lastMs = ns / 1e6;
89
+ this.free.push(query);
90
+ }
91
+ }
92
+ };
93
+ //#endregion
94
+ //#region src/core/ring-buffer.ts
95
+ /**
96
+ * Fixed-capacity numeric ring buffer. Backs the per-frame timing history the
97
+ * view reads to draw sparklines. Push is O(1); `toArray()` returns values in
98
+ * chronological (oldest → newest) order.
99
+ */
100
+ var RingBuffer = class {
101
+ data;
102
+ capacity;
103
+ head = 0;
104
+ count = 0;
105
+ constructor(capacity) {
106
+ this.capacity = capacity;
107
+ this.data = new Float32Array(capacity);
108
+ }
109
+ push(value) {
110
+ this.data[this.head] = value;
111
+ this.head = (this.head + 1) % this.capacity;
112
+ if (this.count < this.capacity) this.count += 1;
113
+ }
114
+ /** Values oldest → newest. Length grows up to capacity as samples accumulate. */
115
+ toArray() {
116
+ const out = Array.from({ length: this.count });
117
+ const start = (this.head - this.count + this.capacity) % this.capacity;
118
+ for (let index = 0; index < this.count; index += 1) out[index] = this.data[(start + index) % this.capacity];
119
+ return out;
120
+ }
121
+ clear() {
122
+ this.head = 0;
123
+ this.count = 0;
124
+ }
125
+ };
126
+ //#endregion
127
+ //#region src/core/performance-monitor.ts
128
+ const DEFAULT_GPU_POOL_SIZE = 5;
129
+ /** One monitor per renderer: a second would stack the `render` patch and double-count passes. */
130
+ const ATTACHED = /* @__PURE__ */ new WeakSet();
131
+ const DEFAULT_HISTORY_SIZE = 120;
132
+ const FPS_SMOOTHING = .1;
133
+ const frameCalls = (info) => info.render.drawCalls ?? info.render.calls;
134
+ const programCount = (info) => info.memory.programs ?? info.programs?.length ?? 0;
135
+ /**
136
+ * Collector of three.js frame metrics. Bracket each frame with {@link begin}/
137
+ * {@link end}, or let {@link wrapAnimationLoop} or the R3F `PerfSampler` do it.
138
+ * Works on WebGPURenderer (drawCalls, memory.programs, timestamp queries) and
139
+ * on a WebGL renderer (calls, programs[], timer-query ext).
140
+ *
141
+ * While attached, `renderer.info.autoReset` is off and `info` is reset in
142
+ * {@link begin}; host code that reads `renderer.info` mid-frame sees this
143
+ * frame's counts only. {@link dispose} restores both.
144
+ */
145
+ var PerformanceMonitor = class {
146
+ renderer;
147
+ gpuTimer;
148
+ originalRender;
149
+ renderPasses = 0;
150
+ fpsHistory;
151
+ cpuHistory;
152
+ gpuHistory;
153
+ cpuStart = 0;
154
+ lastBeginAt = null;
155
+ frameFps = 0;
156
+ smoothedFps = 0;
157
+ sample;
158
+ constructor(options) {
159
+ const { renderer, trackGPU = true, gpuQueryPoolSize = DEFAULT_GPU_POOL_SIZE, historySize = DEFAULT_HISTORY_SIZE } = options;
160
+ if (ATTACHED.has(renderer)) throw new Error("three-meter: a PerformanceMonitor is already attached to this renderer. dispose() it first.");
161
+ ATTACHED.add(renderer);
162
+ this.renderer = renderer;
163
+ renderer.info.autoReset = false;
164
+ this.originalRender = renderer.render;
165
+ renderer.render = (...args) => {
166
+ this.renderPasses += 1;
167
+ return this.originalRender.apply(renderer, args);
168
+ };
169
+ this.gpuTimer = trackGPU ? GpuTimer.create(renderer, gpuQueryPoolSize) : null;
170
+ this.fpsHistory = new RingBuffer(historySize);
171
+ this.cpuHistory = new RingBuffer(historySize);
172
+ this.gpuHistory = new RingBuffer(historySize);
173
+ this.sample = this.buildEmptySample();
174
+ }
175
+ begin() {
176
+ const now = performance.now();
177
+ if (this.lastBeginAt !== null) {
178
+ const frameMs = now - this.lastBeginAt;
179
+ this.frameFps = frameMs > 0 ? 1e3 / frameMs : 0;
180
+ }
181
+ this.lastBeginAt = now;
182
+ this.cpuStart = now;
183
+ this.renderPasses = 0;
184
+ this.renderer.info.reset();
185
+ this.gpuTimer?.begin();
186
+ }
187
+ end() {
188
+ const cpu = performance.now() - this.cpuStart;
189
+ this.gpuTimer?.end();
190
+ this.gpuTimer?.poll();
191
+ const gpuMs = this.gpuTimer?.getMs() ?? 0;
192
+ this.smoothedFps = this.smoothedFps === 0 ? this.frameFps : this.smoothedFps + (this.frameFps - this.smoothedFps) * FPS_SMOOTHING;
193
+ this.fpsHistory.push(this.frameFps);
194
+ this.cpuHistory.push(cpu);
195
+ this.gpuHistory.push(gpuMs);
196
+ const info = this.renderer.info;
197
+ this.sample = Object.freeze({
198
+ cpu,
199
+ fps: this.smoothedFps,
200
+ gpu: {
201
+ available: this.gpuTimer !== null,
202
+ ms: gpuMs
203
+ },
204
+ render: {
205
+ calls: frameCalls(info),
206
+ lines: info.render.lines,
207
+ passes: this.renderPasses,
208
+ points: info.render.points,
209
+ triangles: info.render.triangles
210
+ },
211
+ resources: {
212
+ geometries: info.memory.geometries,
213
+ programs: programCount(info),
214
+ textures: info.memory.textures
215
+ }
216
+ });
217
+ }
218
+ getSample() {
219
+ return this.sample;
220
+ }
221
+ getHistory(metric) {
222
+ switch (metric) {
223
+ case "fps": return this.fpsHistory.toArray();
224
+ case "cpu": return this.cpuHistory.toArray();
225
+ case "gpu": return this.gpuHistory.toArray();
226
+ }
227
+ }
228
+ dispose() {
229
+ ATTACHED.delete(this.renderer);
230
+ this.renderer.info.autoReset = true;
231
+ this.renderer.render = this.originalRender;
232
+ this.gpuTimer?.dispose();
233
+ this.fpsHistory.clear();
234
+ this.cpuHistory.clear();
235
+ this.gpuHistory.clear();
236
+ }
237
+ buildEmptySample() {
238
+ return Object.freeze({
239
+ cpu: 0,
240
+ fps: 0,
241
+ gpu: {
242
+ available: this.gpuTimer !== null,
243
+ ms: 0
244
+ },
245
+ render: {
246
+ calls: 0,
247
+ lines: 0,
248
+ passes: 0,
249
+ points: 0,
250
+ triangles: 0
251
+ },
252
+ resources: {
253
+ geometries: 0,
254
+ programs: 0,
255
+ textures: 0
256
+ }
257
+ });
258
+ }
259
+ };
260
+ //#endregion
261
+ export { RingBuffer as n, PerformanceMonitor as t };
262
+
263
+ //# sourceMappingURL=performance-monitor-qswCj9Nl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"performance-monitor-qswCj9Nl.js","names":[],"sources":["../src/core/gpu-timer.ts","../src/core/ring-buffer.ts","../src/core/performance-monitor.ts"],"sourcesContent":["/**\n * Real GPU frame timing.\n *\n * WebGPU: Three's `trackTimestamp` + `resolveTimestampsAsync` (ms, a frame or\n * two late). Never calls `getExtension`: that is what remounted the canvas.\n *\n * WebGL2 fallback (Safari without WebGPU, or a forced GL backend):\n * `EXT_disjoint_timer_query_webgl2`, same pool-of-queries pattern as the\n * platform-typing monitor.\n */\nimport type { PerfRenderer } from \"./types.ts\";\n\ntype DisjointTimerQueryExt = {\n readonly GPU_DISJOINT_EXT: number;\n readonly TIME_ELAPSED_EXT: number;\n};\n\nclass GpuTimer {\n private lastMs = 0;\n private readonly webgpu: PerfRenderer | null;\n private webgpuInFlight = false;\n private readonly gl: WebGL2RenderingContext | null;\n private readonly ext: DisjointTimerQueryExt | null;\n private readonly free: WebGLQuery[] = [];\n private readonly pending: WebGLQuery[] = [];\n private active: WebGLQuery | null = null;\n\n private constructor(\n webgpu: PerfRenderer | null,\n gl: WebGL2RenderingContext | null,\n ext: DisjointTimerQueryExt | null,\n poolSize: number,\n ) {\n this.webgpu = webgpu;\n this.gl = gl;\n this.ext = ext;\n\n if (!gl) {\n return;\n }\n\n for (let index = 0; index < poolSize; index += 1) {\n const query = gl.createQuery();\n\n if (query) {\n this.free.push(query);\n }\n }\n }\n\n static create(renderer: PerfRenderer, poolSize: number): GpuTimer | null {\n const backend = renderer.backend as { trackTimestamp?: boolean } | undefined;\n\n if (backend?.trackTimestamp && renderer.resolveTimestampsAsync) {\n return new GpuTimer(renderer, null, null, poolSize);\n }\n\n const context = renderer.getContext?.();\n\n if (!(context instanceof WebGL2RenderingContext)) {\n return null;\n }\n\n const ext = context.getExtension(\n \"EXT_disjoint_timer_query_webgl2\",\n ) as DisjointTimerQueryExt | null;\n\n return ext ? new GpuTimer(null, context, ext, poolSize) : null;\n }\n\n begin() {\n if (this.webgpu || !this.gl || !this.ext) {\n return;\n }\n\n const query = this.free.pop();\n\n if (!query) {\n this.active = null;\n\n return;\n }\n\n this.active = query;\n this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, query);\n }\n\n end() {\n if (this.webgpu || !this.gl || !this.ext || !this.active) {\n return;\n }\n\n this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);\n this.pending.push(this.active);\n this.active = null;\n }\n\n poll() {\n if (this.webgpu) {\n this.pollWebgpu();\n\n return;\n }\n\n this.pollWebgl();\n }\n\n getMs() {\n return this.lastMs;\n }\n\n dispose() {\n if (this.gl && this.ext && this.active) {\n this.gl.endQuery(this.ext.TIME_ELAPSED_EXT);\n this.active = null;\n }\n\n if (this.gl) {\n for (const query of this.free) {\n this.gl.deleteQuery(query);\n }\n\n for (const query of this.pending) {\n this.gl.deleteQuery(query);\n }\n }\n\n this.free.length = 0;\n this.pending.length = 0;\n }\n\n private pollWebgpu() {\n const resolve = this.webgpu?.resolveTimestampsAsync;\n\n if (!resolve || this.webgpuInFlight) {\n return;\n }\n\n this.webgpuInFlight = true;\n\n void resolve\n .call(this.webgpu, \"render\")\n .then((ms) => {\n if (typeof ms === \"number\" && Number.isFinite(ms)) {\n this.lastMs = ms;\n }\n\n this.webgpuInFlight = false;\n })\n .catch(() => {\n this.webgpuInFlight = false;\n });\n }\n\n private pollWebgl() {\n if (!this.gl || !this.ext) {\n return;\n }\n\n const disjoint = this.gl.getParameter(this.ext.GPU_DISJOINT_EXT) as boolean;\n\n if (disjoint) {\n while (this.pending.length > 0) {\n this.free.push(this.pending.shift()!);\n }\n\n return;\n }\n\n while (this.pending.length > 0) {\n const query = this.pending[0]!;\n const ready = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT_AVAILABLE) as boolean;\n\n if (!ready) {\n break;\n }\n\n this.pending.shift();\n const ns = this.gl.getQueryParameter(query, this.gl.QUERY_RESULT) as number;\n this.lastMs = ns / 1e6;\n this.free.push(query);\n }\n }\n}\n\nexport { GpuTimer };\n","/**\n * Fixed-capacity numeric ring buffer. Backs the per-frame timing history the\n * view reads to draw sparklines. Push is O(1); `toArray()` returns values in\n * chronological (oldest → newest) order.\n */\nclass RingBuffer {\n private readonly data: Float32Array;\n private readonly capacity: number;\n private head = 0;\n private count = 0;\n\n constructor(capacity: number) {\n this.capacity = capacity;\n this.data = new Float32Array(capacity);\n }\n\n push(value: number) {\n this.data[this.head] = value;\n this.head = (this.head + 1) % this.capacity;\n\n if (this.count < this.capacity) {\n this.count += 1;\n }\n }\n\n /** Values oldest → newest. Length grows up to capacity as samples accumulate. */\n toArray(): number[] {\n const out = Array.from<number>({ length: this.count });\n const start = (this.head - this.count + this.capacity) % this.capacity;\n\n for (let index = 0; index < this.count; index += 1) {\n out[index] = this.data[(start + index) % this.capacity]!;\n }\n\n return out;\n }\n\n clear() {\n this.head = 0;\n this.count = 0;\n }\n}\n\nexport { RingBuffer };\n","import { GpuTimer } from \"./gpu-timer.ts\";\nimport { RingBuffer } from \"./ring-buffer.ts\";\nimport type { PerfRenderer, PerformanceMonitorOptions, Sample, TimingMetric } from \"./types.ts\";\n\nconst DEFAULT_GPU_POOL_SIZE = 5;\n/** One monitor per renderer: a second would stack the `render` patch and double-count passes. */\nconst ATTACHED = new WeakSet<PerfRenderer>();\nconst DEFAULT_HISTORY_SIZE = 120;\nconst FPS_SMOOTHING = 0.1;\n\nconst frameCalls = (info: PerfRenderer[\"info\"]) => info.render.drawCalls ?? info.render.calls;\n\nconst programCount = (info: PerfRenderer[\"info\"]) =>\n info.memory.programs ?? info.programs?.length ?? 0;\n\n/**\n * Collector of three.js frame metrics. Bracket each frame with {@link begin}/\n * {@link end}, or let {@link wrapAnimationLoop} or the R3F `PerfSampler` do it.\n * Works on WebGPURenderer (drawCalls, memory.programs, timestamp queries) and\n * on a WebGL renderer (calls, programs[], timer-query ext).\n *\n * While attached, `renderer.info.autoReset` is off and `info` is reset in\n * {@link begin}; host code that reads `renderer.info` mid-frame sees this\n * frame's counts only. {@link dispose} restores both.\n */\nclass PerformanceMonitor {\n private readonly renderer: PerfRenderer;\n private readonly gpuTimer: GpuTimer | null;\n private readonly originalRender: PerfRenderer[\"render\"];\n private renderPasses = 0;\n private readonly fpsHistory: RingBuffer;\n private readonly cpuHistory: RingBuffer;\n private readonly gpuHistory: RingBuffer;\n private cpuStart = 0;\n private lastBeginAt: number | null = null;\n private frameFps = 0;\n private smoothedFps = 0;\n private sample: Sample;\n\n constructor(options: PerformanceMonitorOptions) {\n const {\n renderer,\n trackGPU = true,\n gpuQueryPoolSize = DEFAULT_GPU_POOL_SIZE,\n historySize = DEFAULT_HISTORY_SIZE,\n } = options;\n\n if (ATTACHED.has(renderer)) {\n throw new Error(\n \"three-meter: a PerformanceMonitor is already attached to this renderer. dispose() it first.\",\n );\n }\n\n ATTACHED.add(renderer);\n this.renderer = renderer;\n renderer.info.autoReset = false;\n this.originalRender = renderer.render;\n\n renderer.render = (...args: never[]) => {\n this.renderPasses += 1;\n\n return this.originalRender.apply(renderer, args);\n };\n\n this.gpuTimer = trackGPU ? GpuTimer.create(renderer, gpuQueryPoolSize) : null;\n this.fpsHistory = new RingBuffer(historySize);\n this.cpuHistory = new RingBuffer(historySize);\n this.gpuHistory = new RingBuffer(historySize);\n this.sample = this.buildEmptySample();\n }\n\n begin() {\n const now = performance.now();\n\n if (this.lastBeginAt !== null) {\n const frameMs = now - this.lastBeginAt;\n this.frameFps = frameMs > 0 ? 1000 / frameMs : 0;\n }\n\n this.lastBeginAt = now;\n this.cpuStart = now;\n this.renderPasses = 0;\n this.renderer.info.reset();\n this.gpuTimer?.begin();\n }\n\n end() {\n const now = performance.now();\n const cpu = now - this.cpuStart;\n\n this.gpuTimer?.end();\n this.gpuTimer?.poll();\n\n const gpuMs = this.gpuTimer?.getMs() ?? 0;\n\n this.smoothedFps =\n this.smoothedFps === 0\n ? this.frameFps\n : this.smoothedFps + (this.frameFps - this.smoothedFps) * FPS_SMOOTHING;\n\n this.fpsHistory.push(this.frameFps);\n this.cpuHistory.push(cpu);\n this.gpuHistory.push(gpuMs);\n\n const info = this.renderer.info;\n\n this.sample = Object.freeze({\n cpu,\n fps: this.smoothedFps,\n gpu: { available: this.gpuTimer !== null, ms: gpuMs },\n render: {\n calls: frameCalls(info),\n lines: info.render.lines,\n passes: this.renderPasses,\n points: info.render.points,\n triangles: info.render.triangles,\n },\n resources: {\n geometries: info.memory.geometries,\n programs: programCount(info),\n textures: info.memory.textures,\n },\n });\n }\n\n getSample(): Sample {\n return this.sample;\n }\n\n getHistory(metric: TimingMetric): readonly number[] {\n switch (metric) {\n case \"fps\":\n return this.fpsHistory.toArray();\n case \"cpu\":\n return this.cpuHistory.toArray();\n case \"gpu\":\n return this.gpuHistory.toArray();\n }\n }\n\n dispose() {\n ATTACHED.delete(this.renderer);\n this.renderer.info.autoReset = true;\n this.renderer.render = this.originalRender;\n this.gpuTimer?.dispose();\n this.fpsHistory.clear();\n this.cpuHistory.clear();\n this.gpuHistory.clear();\n }\n\n private buildEmptySample(): Sample {\n return Object.freeze({\n cpu: 0,\n fps: 0,\n gpu: { available: this.gpuTimer !== null, ms: 0 },\n render: { calls: 0, lines: 0, passes: 0, points: 0, triangles: 0 },\n resources: { geometries: 0, programs: 0, textures: 0 },\n });\n }\n}\n\nexport { PerformanceMonitor };\n"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import { i as PerformanceMonitorOptions, t as PerformanceMonitor } from "./performance-monitor-WGLWbxOZ.js";
2
+ import { t as MountPerfHudOptions } from "./mount-perf-hud-Dw8KNQTt.js";
3
+ //#region src/react/monitor-store.d.ts
4
+ type MonitorListener = (monitor: PerformanceMonitor | null) => void;
5
+ type PerfMonitorStore = {
6
+ get: () => PerformanceMonitor | null;
7
+ set: (monitor: PerformanceMonitor | null) => void;
8
+ subscribe: (listener: MonitorListener) => () => void;
9
+ };
10
+ declare const createPerfMonitorStore: () => PerfMonitorStore;
11
+ declare const defaultPerfMonitorStore: PerfMonitorStore;
12
+ //#endregion
13
+ //#region src/react/perf-hud.d.ts
14
+ type PerfHudProps = Omit<MountPerfHudOptions, "settings"> & {
15
+ /** Must match the `PerfSampler`'s. Default: the shared store. */
16
+ store?: PerfMonitorStore;
17
+ };
18
+ /**
19
+ * DOM overlay. Render it *outside* `<Canvas>`, or Fiber would treat its markup
20
+ * as three objects. Mounts the dockable HUD whenever a `PerfSampler` has
21
+ * published a monitor and tears it down when that sampler unmounts.
22
+ */
23
+ declare function PerfHud({ defaultPlacement, injectStyles, label, mode, parent, refreshHz, storageKey, store }: PerfHudProps): null;
24
+ //#endregion
25
+ //#region src/react/perf-sampler.d.ts
26
+ type PerfSamplerProps = Omit<PerformanceMonitorOptions, "renderer"> & {
27
+ /** Where the monitor is published for `PerfHud`. Default: the shared store. */
28
+ store?: PerfMonitorStore;
29
+ };
30
+ /**
31
+ * In-canvas collector. Renders nothing. Fiber runs every `useFrame` before
32
+ * `gl.render`, so the previous frame is closed here and the next one opens, and
33
+ * GPU queries then span the render that just ran. The monitor is created in
34
+ * an effect so StrictMode's double mount attaches and detaches cleanly.
35
+ */
36
+ declare function PerfSampler({ gpuQueryPoolSize, historySize, store, trackGPU }: PerfSamplerProps): null;
37
+ //#endregion
38
+ export { PerfHud, type PerfHudProps, type PerfMonitorStore, PerfSampler, type PerfSamplerProps, createPerfMonitorStore, defaultPerfMonitorStore };
39
+ //# sourceMappingURL=react.d.ts.map
package/dist/react.js ADDED
@@ -0,0 +1,114 @@
1
+ import { t as PerformanceMonitor } from "./performance-monitor-qswCj9Nl.js";
2
+ import { t as mountPerfHud } from "./mount-perf-hud-CduRNym5.js";
3
+ import { useEffect, useRef } from "react";
4
+ import { useFrame, useThree } from "@react-three/fiber";
5
+ //#region src/react/monitor-store.ts
6
+ const createPerfMonitorStore = () => {
7
+ let current = null;
8
+ const listeners = /* @__PURE__ */ new Set();
9
+ return {
10
+ get: () => current,
11
+ set: (monitor) => {
12
+ current = monitor;
13
+ for (const listener of listeners) listener(monitor);
14
+ },
15
+ subscribe: (listener) => {
16
+ listeners.add(listener);
17
+ return () => {
18
+ listeners.delete(listener);
19
+ };
20
+ }
21
+ };
22
+ };
23
+ const defaultPerfMonitorStore = createPerfMonitorStore();
24
+ //#endregion
25
+ //#region src/react/perf-hud.tsx
26
+ /**
27
+ * DOM overlay. Render it *outside* `<Canvas>`, or Fiber would treat its markup
28
+ * as three objects. Mounts the dockable HUD whenever a `PerfSampler` has
29
+ * published a monitor and tears it down when that sampler unmounts.
30
+ */
31
+ function PerfHud({ defaultPlacement, injectStyles, label, mode, parent, refreshHz, storageKey, store }) {
32
+ const source = store ?? defaultPerfMonitorStore;
33
+ useEffect(() => {
34
+ let dispose = null;
35
+ const attach = (monitor) => {
36
+ dispose?.();
37
+ dispose = null;
38
+ if (!monitor) return;
39
+ dispose = mountPerfHud(monitor, {
40
+ defaultPlacement,
41
+ injectStyles,
42
+ label,
43
+ mode,
44
+ parent,
45
+ refreshHz,
46
+ storageKey
47
+ }).dispose;
48
+ };
49
+ const unsubscribe = source.subscribe(attach);
50
+ attach(source.get());
51
+ return () => {
52
+ unsubscribe();
53
+ dispose?.();
54
+ };
55
+ }, [
56
+ defaultPlacement,
57
+ injectStyles,
58
+ label,
59
+ mode,
60
+ parent,
61
+ refreshHz,
62
+ source,
63
+ storageKey
64
+ ]);
65
+ return null;
66
+ }
67
+ //#endregion
68
+ //#region src/react/perf-sampler.tsx
69
+ /**
70
+ * In-canvas collector. Renders nothing. Fiber runs every `useFrame` before
71
+ * `gl.render`, so the previous frame is closed here and the next one opens, and
72
+ * GPU queries then span the render that just ran. The monitor is created in
73
+ * an effect so StrictMode's double mount attaches and detaches cleanly.
74
+ */
75
+ function PerfSampler({ gpuQueryPoolSize, historySize, store, trackGPU }) {
76
+ const renderer = useThree((state) => state.gl);
77
+ const monitorRef = useRef(null);
78
+ const startedRef = useRef(false);
79
+ const target = store ?? defaultPerfMonitorStore;
80
+ useEffect(() => {
81
+ const monitor = new PerformanceMonitor({
82
+ gpuQueryPoolSize,
83
+ historySize,
84
+ renderer,
85
+ trackGPU
86
+ });
87
+ monitorRef.current = monitor;
88
+ startedRef.current = false;
89
+ target.set(monitor);
90
+ return () => {
91
+ target.set(null);
92
+ monitorRef.current = null;
93
+ monitor.dispose();
94
+ };
95
+ }, [
96
+ gpuQueryPoolSize,
97
+ historySize,
98
+ renderer,
99
+ target,
100
+ trackGPU
101
+ ]);
102
+ useFrame(() => {
103
+ const monitor = monitorRef.current;
104
+ if (!monitor) return;
105
+ if (startedRef.current) monitor.end();
106
+ startedRef.current = true;
107
+ monitor.begin();
108
+ });
109
+ return null;
110
+ }
111
+ //#endregion
112
+ export { PerfHud, PerfSampler, createPerfMonitorStore, defaultPerfMonitorStore };
113
+
114
+ //# sourceMappingURL=react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.js","names":[],"sources":["../src/react/monitor-store.ts","../src/react/perf-hud.tsx","../src/react/perf-sampler.tsx"],"sourcesContent":["/**\n * Hands the in-canvas monitor to the DOM-side HUD. A React context can't do\n * it: R3F's `<Canvas>` is its own reconciler root, so a provider outside it\n * isn't visible inside without a bridge. A tiny external store crosses that\n * boundary for free. One default store; pass your own to both components to\n * run two canvases on one page.\n */\nimport type { PerformanceMonitor } from \"../core/performance-monitor.ts\";\n\ntype MonitorListener = (monitor: PerformanceMonitor | null) => void;\n\ntype PerfMonitorStore = {\n get: () => PerformanceMonitor | null;\n set: (monitor: PerformanceMonitor | null) => void;\n subscribe: (listener: MonitorListener) => () => void;\n};\n\nconst createPerfMonitorStore = (): PerfMonitorStore => {\n let current: PerformanceMonitor | null = null;\n const listeners = new Set<MonitorListener>();\n\n return {\n get: () => current,\n set: (monitor) => {\n current = monitor;\n\n for (const listener of listeners) {\n listener(monitor);\n }\n },\n subscribe: (listener) => {\n listeners.add(listener);\n\n return () => {\n listeners.delete(listener);\n };\n },\n };\n};\n\nconst defaultPerfMonitorStore = createPerfMonitorStore();\n\nexport { createPerfMonitorStore, defaultPerfMonitorStore };\nexport type { PerfMonitorStore };\n","import { useEffect } from \"react\";\n\nimport { mountPerfHud, type MountPerfHudOptions } from \"../ui/mount-perf-hud.ts\";\nimport { defaultPerfMonitorStore, type PerfMonitorStore } from \"./monitor-store.ts\";\n\ntype PerfHudProps = Omit<MountPerfHudOptions, \"settings\"> & {\n /** Must match the `PerfSampler`'s. Default: the shared store. */\n store?: PerfMonitorStore;\n};\n\n/**\n * DOM overlay. Render it *outside* `<Canvas>`, or Fiber would treat its markup\n * as three objects. Mounts the dockable HUD whenever a `PerfSampler` has\n * published a monitor and tears it down when that sampler unmounts.\n */\nfunction PerfHud({\n defaultPlacement,\n injectStyles,\n label,\n mode,\n parent,\n refreshHz,\n storageKey,\n store,\n}: PerfHudProps) {\n const source = store ?? defaultPerfMonitorStore;\n\n useEffect(() => {\n let dispose: (() => void) | null = null;\n\n const attach = (monitor: ReturnType<PerfMonitorStore[\"get\"]>) => {\n dispose?.();\n dispose = null;\n\n if (!monitor) {\n return;\n }\n\n dispose = mountPerfHud(monitor, {\n defaultPlacement,\n injectStyles,\n label,\n mode,\n parent,\n refreshHz,\n storageKey,\n }).dispose;\n };\n\n const unsubscribe = source.subscribe(attach);\n attach(source.get());\n\n return () => {\n unsubscribe();\n dispose?.();\n };\n }, [defaultPlacement, injectStyles, label, mode, parent, refreshHz, source, storageKey]);\n\n return null;\n}\n\nexport { PerfHud };\nexport type { PerfHudProps };\n","import { useFrame, useThree } from \"@react-three/fiber\";\nimport { useEffect, useRef } from \"react\";\n\nimport { PerformanceMonitor } from \"../core/performance-monitor.ts\";\nimport type { PerformanceMonitorOptions, PerfRenderer } from \"../core/types.ts\";\nimport { defaultPerfMonitorStore, type PerfMonitorStore } from \"./monitor-store.ts\";\n\ntype PerfSamplerProps = Omit<PerformanceMonitorOptions, \"renderer\"> & {\n /** Where the monitor is published for `PerfHud`. Default: the shared store. */\n store?: PerfMonitorStore;\n};\n\n/**\n * In-canvas collector. Renders nothing. Fiber runs every `useFrame` before\n * `gl.render`, so the previous frame is closed here and the next one opens, and\n * GPU queries then span the render that just ran. The monitor is created in\n * an effect so StrictMode's double mount attaches and detaches cleanly.\n */\nfunction PerfSampler({ gpuQueryPoolSize, historySize, store, trackGPU }: PerfSamplerProps) {\n const renderer = useThree((state) => state.gl);\n const monitorRef = useRef<PerformanceMonitor | null>(null);\n const startedRef = useRef(false);\n const target = store ?? defaultPerfMonitorStore;\n\n useEffect(() => {\n const monitor = new PerformanceMonitor({\n gpuQueryPoolSize,\n historySize,\n renderer: renderer as unknown as PerfRenderer,\n trackGPU,\n });\n monitorRef.current = monitor;\n startedRef.current = false;\n target.set(monitor);\n\n return () => {\n target.set(null);\n monitorRef.current = null;\n monitor.dispose();\n };\n }, [gpuQueryPoolSize, historySize, renderer, target, trackGPU]);\n\n useFrame(() => {\n const monitor = monitorRef.current;\n\n if (!monitor) {\n return;\n }\n\n if (startedRef.current) {\n monitor.end();\n }\n\n startedRef.current = true;\n monitor.begin();\n });\n\n return null;\n}\n\nexport { PerfSampler };\nexport type { PerfSamplerProps };\n"],"mappings":";;;;;AAiBA,MAAM,+BAAiD;CACrD,IAAI,UAAqC;CACzC,MAAM,4BAAY,IAAI,IAAqB;CAE3C,OAAO;EACL,WAAW;EACX,MAAM,YAAY;GAChB,UAAU;GAEV,KAAK,MAAM,YAAY,WACrB,SAAS,OAAO;EAEpB;EACA,YAAY,aAAa;GACvB,UAAU,IAAI,QAAQ;GAEtB,aAAa;IACX,UAAU,OAAO,QAAQ;GAC3B;EACF;CACF;AACF;AAEA,MAAM,0BAA0B,uBAAuB;;;;;;;;ACzBvD,SAAS,QAAQ,EACf,kBACA,cACA,OACA,MACA,QACA,WACA,YACA,SACe;CACf,MAAM,SAAS,SAAS;CAExB,gBAAgB;EACd,IAAI,UAA+B;EAEnC,MAAM,UAAU,YAAiD;GAC/D,UAAU;GACV,UAAU;GAEV,IAAI,CAAC,SACH;GAGF,UAAU,aAAa,SAAS;IAC9B;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC,CAAC,CAAC;EACL;EAEA,MAAM,cAAc,OAAO,UAAU,MAAM;EAC3C,OAAO,OAAO,IAAI,CAAC;EAEnB,aAAa;GACX,YAAY;GACZ,UAAU;EACZ;CACF,GAAG;EAAC;EAAkB;EAAc;EAAO;EAAM;EAAQ;EAAW;EAAQ;CAAU,CAAC;CAEvF,OAAO;AACT;;;;;;;;;ACzCA,SAAS,YAAY,EAAE,kBAAkB,aAAa,OAAO,YAA8B;CACzF,MAAM,WAAW,UAAU,UAAU,MAAM,EAAE;CAC7C,MAAM,aAAa,OAAkC,IAAI;CACzD,MAAM,aAAa,OAAO,KAAK;CAC/B,MAAM,SAAS,SAAS;CAExB,gBAAgB;EACd,MAAM,UAAU,IAAI,mBAAmB;GACrC;GACA;GACU;GACV;EACF,CAAC;EACD,WAAW,UAAU;EACrB,WAAW,UAAU;EACrB,OAAO,IAAI,OAAO;EAElB,aAAa;GACX,OAAO,IAAI,IAAI;GACf,WAAW,UAAU;GACrB,QAAQ,QAAQ;EAClB;CACF,GAAG;EAAC;EAAkB;EAAa;EAAU;EAAQ;CAAQ,CAAC;CAE9D,eAAe;EACb,MAAM,UAAU,WAAW;EAE3B,IAAI,CAAC,SACH;EAGF,IAAI,WAAW,SACb,QAAQ,IAAI;EAGd,WAAW,UAAU;EACrB,QAAQ,MAAM;CAChB,CAAC;CAED,OAAO;AACT"}