@zkmake/three-meter 0.6.0 → 0.8.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/README.md CHANGED
@@ -1,29 +1,38 @@
1
1
  # @zkmake/three-meter
2
2
 
3
- Frame metrics for a three.js renderer: FPS, CPU ms, GPU ms, draw calls, render passes, triangles,
4
- geometries, textures and programs, with a small dockable HUD. Zero dependencies. Works with
5
- `WebGLRenderer` and `WebGPURenderer`, with or without React.
3
+ Frame metrics for a three.js renderer, with a small dockable HUD: FPS, CPU and GPU time, stutter
4
+ (1% low, p99 frame time, hitches), draw calls, render passes, triangles, and the geometries,
5
+ textures and shaders in GPU memory. Zero dependencies. Works with `WebGLRenderer` and
6
+ `WebGPURenderer`, with or without React.
6
7
 
7
8
  ```sh
8
9
  bun add -d @zkmake/three-meter # or npm i -D / pnpm add -D
9
10
  ```
10
11
 
11
12
  Live demo: [three-meter.pages.dev](https://three-meter.pages.dev/), with a vanilla three and a
12
- React Three Fiber take on the same scene. Add `?webgpu` for the WebGPU renderer and `?count=5000` to
13
- load the scene up.
13
+ React Three Fiber take on the same scene. Add `?webgpu` for the WebGPU renderer and `?count=30000` to
14
+ load the scene up past the demo's triangle budget.
14
15
 
15
16
  <p align="center">
16
17
  <img
17
18
  src="https://raw.githubusercontent.com/zkmake/three-meter/main/docs/hud-full.png"
18
19
  width="320"
19
- alt="The HUD in full mode, dark theme. Option rows for theme (light, system, dark), dim on leave, explain metrics and a copy-report button; FPS, CPU and GPU sparklines with live values; then one row per metric, each with an icon, its value and a checkbox choosing whether the compact card shows it (FPS, calls, CPU, GPU, 1% low, frame p99, hitches, triangles, lines, points, render passes, geometries, textures, shaders); and a footer with the three-meter version, the three revision, a WebGL2 backend badge and the GPU name. Two discs beside it: a drag grip and the compact/full toggle."
20
+ alt="The HUD in full mode, dark theme. Option rows for theme (light, system, dark), dim on leave, explain metrics and a copy-report button; FPS, CPU and GPU sparklines with live values, the FPS graph showing a dashed budget line; then one row per metric, each with an icon, its value and a checkbox choosing whether the compact card shows it (FPS, calls, CPU, GPU, 1% low, frame p99, hitches, triangles, lines, points, render passes, geometries, textures, shaders), with the triangle count in amber for being over its budget; and a footer with the three-meter version, the three revision, a WebGL2 backend badge and the GPU name. Two discs beside it: a drag grip and the compact/full toggle."
20
21
  />
21
22
  </p>
22
23
 
23
- The sampler and the card are separate components, so toggling the HUD never remounts your canvas.
24
- GPU time is measured on WebGPU as well as WebGL. The counters are draw calls, render passes and
25
- resource counts, which is what you watch when a scene is instanced. The card docks to a screen edge,
26
- remembers where you put it, and hides its controls until the pointer comes near.
24
+ - **Spot problems at a glance.** Values past their budget turn amber, and every metric explains
25
+ itself on hover, so you don't need to know what a p99 is to see that one is bad.
26
+ - **See stutter, not just averages.** 1% low, p99 frame time and hitch counts catch the frame that
27
+ drops every few seconds while the FPS average still reads 60.
28
+ - **Real GPU time** from timer queries on WebGPU, and on WebGL in Chrome and Edge, not an estimate
29
+ from frame intervals.
30
+ - **Find what's expensive.** A breakdown of draw calls and triangles by mesh and material shows
31
+ which objects to instance or merge.
32
+ - **One-click bug reports.** Copy versions, backend, GPU and the current numbers as Markdown.
33
+ - **Stays out of the way.** The sampler and the card are separate components, so toggling the HUD
34
+ never remounts your canvas. The card docks to a screen edge, remembers where you put it, and hides
35
+ its controls until the pointer comes near.
27
36
 
28
37
  ## Entry points
29
38
 
@@ -89,60 +98,12 @@ Canvas is its own React root. To run two canvases on one page, pass the same `st
89
98
  | `parent` | `document.body` | Where the host element is appended. |
90
99
  | `injectStyles` | `true` | Append the stylesheet once per document. |
91
100
  | `refreshHz` | `10` | Repaint rate. |
101
+ | `budgets` | timing defaults | Limits past which a value turns amber. See [Budgets](#budgets). |
102
+ | `label` | `"Performance"` | Accessible name of the panel. |
92
103
 
93
104
  `PerformanceMonitor` and `PerfSampler` take `trackGPU` (default true), `gpuQueryPoolSize` (default 5),
94
105
  `historySize` (default 120) and `frameStatsSize` (default 1000).
95
106
 
96
- ## Theme
97
-
98
- The HUD ships a dark and a light palette. Two layers decide which shows:
99
-
100
- 1. **The panel's own pick.** The full view has a light / system / dark row. A pick there is kept
101
- with the other settings under `storageKey` and wins while set.
102
- 2. **Your `theme` option.** `dark`, `light`, or `system` (the default), which follows the OS and
103
- switches when it does. Applies whenever the person using the HUD hasn't picked anything.
104
-
105
- Change your layer later from the handle, or from the `PerfHud` prop in React, which applies without
106
- remounting:
107
-
108
- ```ts
109
- const hud = mountPerfHud(monitor, { theme: "system" });
110
- hud.setTheme("light"); // your layer: "dark" | "light" | "system"
111
- hud.getTheme(); // your layer, as asked for
112
- hud.settings.theme; // the panel's pick, or null
113
- hud.settings.setTheme(null); // clear the pick so your layer applies again
114
- hud.theme.effective; // whichever layer is in force
115
- hud.theme.resolved; // "dark" | "light", what is on screen right now
116
- hud.theme.subscribe(() => syncMyPageWith(hud.theme.resolved));
117
- ```
118
-
119
- The resolved theme lands on `data-theme` of `.perf-hud` and `.perf-monitor`. To restyle either
120
- palette, override the `--perf-*` custom properties (`bg`, `fg`, `fg-dim`, `muted`, `row`,
121
- `border`, `accent`, `shadow`) on those selectors.
122
-
123
- ## GPU timing
124
-
125
- On WebGL2 the monitor uses `EXT_disjoint_timer_query_webgl2`, which Chrome and Edge expose. Safari
126
- and Firefox don't, so the GPU row shows `—` and `sample.gpu.available` is `false`.
127
-
128
- On WebGPU, construct the renderer with `trackTimestamp: true`. Timings resolve a frame or two late.
129
-
130
- ## Environment
131
-
132
- The full view's footer has two rows: the three-meter version (linked to its release notes) and the
133
- three revision, then the backend and the GPU name. An amber `WebGL2 fallback` badge means
134
- `WebGPURenderer` couldn't get WebGPU. Its checkbox (off by default) shows the footer in the compact
135
- HUD too. The same data is on the monitor:
136
-
137
- ```ts
138
- monitor.getEnvironment();
139
- // { three: "186", backend: "webgpu", fallback: false, gpu: "Apple metal-3" }
140
- ```
141
-
142
- `backend` is `null` until `WebGPURenderer.init()` settles. `gpu` comes from WebGPU's adapter info or
143
- WebGL's unmasked renderer string, and is `null` where the browser hides it. Chrome's WebGPU gives
144
- vendor and architecture, not the model.
145
-
146
107
  ## Reading the panel
147
108
 
148
109
  Every row has an icon, its value, and a checkbox on the right that puts it in the compact HUD. Hover
@@ -166,30 +127,125 @@ monitor.getFrameStats();
166
127
  // { frames: 1000, lowFps: 97.6, p99Ms: 10.2, hitches: 3 }
167
128
  ```
168
129
 
130
+ ## Budgets
131
+
132
+ A value past its budget turns amber in the full view and the compact HUD, and its tooltip names the
133
+ budget. The FPS, CPU and GPU graphs draw the budget as a dashed line once the series reaches it; the
134
+ scale never stretches to fit it, so a quiet graph keeps its detail.
135
+
136
+ Timing budgets come from `targetFps` (60 by default): FPS at least 95% of it, 1% low at least half,
137
+ CPU and GPU within one frame (16.7 ms), frame p99 within one and a half. Counts have no default,
138
+ since what's reasonable depends on the scene. Set your own:
139
+
140
+ ```ts
141
+ mountPerfHud(monitor, { budgets: { targetFps: 120, calls: 500, triangles: 2_000_000 } });
142
+ hud.setBudgets({ gpu: null }); // null drops a default; false drops them all
143
+ ```
144
+
145
+ `PerfHud` takes the same `budgets` prop and applies changes live.
146
+
147
+ ## Top costs
148
+
149
+ The HUD tells you there are 400 draw calls; **top costs** tells you which objects they come from.
150
+ Tick it in the full view for the five biggest costs in the main render pass, grouped by **meshes** or
151
+ **materials**, sorted by draw calls or triangles. Click a row to log its three objects to the
152
+ console.
153
+
154
+ ```text
155
+ calls tris
156
+ tree ×300 300 9,600 ← 300 separate meshes: instance or merge them
157
+ house 6 12 ← one draw per material in the array
158
+ dome 2 7,936 ← transparent and double-sided draws twice
159
+ grass ×20,000 1 40,000 ← already instanced
160
+ ```
161
+
162
+ Meshes with the same name and geometry share a row, so unmerged copies stand out. The numbers are
163
+ estimated from the scene graph the way three walks it: hidden objects, other layers and anything
164
+ outside the camera's frustum are left out. Against three's own `renderer.info` on a test scene they
165
+ matched exactly. Shadow maps and post-processing passes aren't included; the main pass is the render
166
+ call that drew the most, so a full-screen quad rendered last doesn't stand in for the scene. It
167
+ walks the scene only while open, twice a second.
168
+
169
+ ```ts
170
+ monitor.getSceneCost();
171
+ // { calls: 309, triangles: 57548, meshes: [{ label: "tree", calls: 300, … }, …], materials: […] }
172
+ ```
173
+
169
174
  ## Bug reports
170
175
 
171
176
  The **report** row's copy button puts a Markdown snapshot on the clipboard: versions, backend, GPU,
172
- the current metrics, the stutter stats, the viewport and the user agent. Paste it into an issue.
177
+ the current metrics, the stutter stats, the top three meshes, the viewport and the user agent. Paste it into an issue.
173
178
  `formatReport(monitor)` from `@zkmake/three-meter/ui` returns the same text.
174
179
 
175
180
  ```text
176
- three-meter v0.5.0 · three r186 · WebGL2 · Apple M4 Max
181
+ three-meter v0.7.0 · three r186 · WebGL2 · Apple M4 Max
177
182
  FPS 120 · 1% low 98 · p99 10.2 ms · hitches 3 / 1,000 frames
178
183
  CPU 0.2 ms · GPU 0.7 ms
179
184
  Calls 1 · passes 1 · triangles 24,000 · lines 0 · points 0
180
185
  Geometries 1 · textures 1 · shaders 1
186
+ Top: tree ×300 (300 calls, 9,600 tris) · house (6 calls, 12 tris) · dome (2 calls, 7,936 tris)
181
187
  Viewport 1280×720 @2x
182
188
  Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) …
183
189
  ```
184
190
 
191
+ ## Environment
192
+
193
+ The full view's footer has two rows: the three-meter version (linked to its release notes) and the
194
+ three revision, then the backend and the GPU name. An amber `WebGL2 fallback` badge means
195
+ `WebGPURenderer` couldn't get WebGPU. Its checkbox (off by default) shows the footer in the compact
196
+ HUD too. The same data is on the monitor:
197
+
198
+ ```ts
199
+ monitor.getEnvironment();
200
+ // { three: "186", backend: "webgpu", fallback: false, gpu: "Apple metal-3" }
201
+ ```
202
+
203
+ `backend` is `null` until `WebGPURenderer.init()` settles. `gpu` comes from WebGPU's adapter info or
204
+ WebGL's unmasked renderer string, and is `null` where the browser hides it. Chrome's WebGPU gives
205
+ vendor and architecture, not the model.
206
+
207
+ ## GPU timing
208
+
209
+ On WebGL2 the monitor uses `EXT_disjoint_timer_query_webgl2`, which Chrome and Edge expose. Safari
210
+ and Firefox don't, so the GPU row shows `—` and `sample.gpu.available` is `false`.
211
+
212
+ On WebGPU, construct the renderer with `trackTimestamp: true`. Timings resolve a frame or two late.
213
+
214
+ ## Theme
215
+
216
+ The HUD ships a dark and a light palette. Two layers decide which shows:
217
+
218
+ 1. **The panel's own pick.** The full view has a light / system / dark row. A pick there is kept
219
+ with the other settings under `storageKey` and wins while set.
220
+ 2. **Your `theme` option.** `dark`, `light`, or `system` (the default), which follows the OS and
221
+ switches when it does. Applies whenever the person using the HUD hasn't picked anything.
222
+
223
+ Change your layer later from the handle, or from the `PerfHud` prop in React, which applies without
224
+ remounting:
225
+
226
+ ```ts
227
+ const hud = mountPerfHud(monitor, { theme: "system" });
228
+ hud.setTheme("light"); // your layer: "dark" | "light" | "system"
229
+ hud.getTheme(); // your layer, as asked for
230
+ hud.settings.theme; // the panel's pick, or null
231
+ hud.settings.setTheme(null); // clear the pick so your layer applies again
232
+ hud.theme.effective; // whichever layer is in force
233
+ hud.theme.resolved; // "dark" | "light", what is on screen right now
234
+ hud.theme.subscribe(() => syncMyPageWith(hud.theme.resolved));
235
+ ```
236
+
237
+ The resolved theme lands on `data-theme` of `.perf-hud` and `.perf-monitor`. To restyle either
238
+ palette, override the `--perf-*` custom properties (`bg`, `fg`, `fg-dim`, `muted`, `row`,
239
+ `border`, `accent`, `warn`, `shadow`) on those selectors. `warn` is the over-budget amber.
240
+
185
241
  ## Examples
186
242
 
187
243
  [`examples/site`](examples/site) is what runs at [three-meter.pages.dev](https://three-meter.pages.dev/):
188
244
  one Vite app with both integrations of the same scene, swapped from the header.
189
245
  [`src/demos/vanilla.ts`](examples/site/src/demos/vanilla.ts) is plain three with `mountPerfHud`;
190
246
  [`src/demos/r3f.tsx`](examples/site/src/demos/r3f.tsx) is React Three Fiber with `PerfSampler` and
191
- `PerfHud`. `?r3f` opens on Fiber, `?webgpu` uses `WebGPURenderer` in either, `?count=` scales the
192
- scene. The header's theme toggle sets the page theme and the HUD's `theme` layer together; the row
247
+ `PerfHud`. Both set a 250K triangle budget on top of the timing defaults. `?r3f` opens on Fiber,
248
+ `?webgpu` uses `WebGPURenderer` in either, `?count=` scales the scene. The header's theme toggle sets the page theme and the HUD's `theme` layer together; the row
193
249
  inside the panel overrides the HUD alone. Run `bun run dev` inside it.
194
250
 
195
251
  ## Shipping it
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as PerfRenderer, c as RenderCounts, d as TimingMetric, i as GpuTiming, l as ResourceCounts, n as Environment, o as PerformanceMonitorOptions, r as FrameStats, s as RenderBackend, t as PerformanceMonitor, u as Sample } from "./performance-monitor-CBN05_BC.js";
1
+ import { a as GpuTiming, c as RenderBackend, d as Sample, f as SceneCost, i as FrameStats, l as RenderCounts, n as CostEntry, o as PerfRenderer, p as TimingMetric, r as Environment, s as PerformanceMonitorOptions, t as PerformanceMonitor, u as ResourceCounts } from "./performance-monitor-DgHxeQ2X.js";
2
2
  //#region src/core/ring-buffer.d.ts
3
3
  /**
4
4
  * Fixed-capacity numeric ring buffer. Backs the per-frame timing history the
@@ -32,5 +32,5 @@ type AnimationLoop<Args extends unknown[]> = (...args: Args) => void;
32
32
  */
33
33
  declare function wrapAnimationLoop<Args extends unknown[]>(monitor: PerformanceMonitor, loop: AnimationLoop<Args>): AnimationLoop<Args>;
34
34
  //#endregion
35
- export { type Environment, type FrameStats, type GpuTiming, type PerfRenderer, PerformanceMonitor, type PerformanceMonitorOptions, type RenderBackend, type RenderCounts, type ResourceCounts, RingBuffer, type Sample, type TimingMetric, wrapAnimationLoop };
35
+ export { type CostEntry, type Environment, type FrameStats, type GpuTiming, type PerfRenderer, PerformanceMonitor, type PerformanceMonitorOptions, type RenderBackend, type RenderCounts, type ResourceCounts, RingBuffer, type Sample, type SceneCost, type TimingMetric, wrapAnimationLoop };
36
36
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as RingBuffer, t as PerformanceMonitor } from "./performance-monitor-yxsxs57v.js";
1
+ import { n as RingBuffer, t as PerformanceMonitor } from "./performance-monitor-DsBp13ts.js";
2
2
  //#region src/core/wrap-animation-loop.ts
3
3
  /**
4
4
  * Wrap a `renderer.setAnimationLoop` callback so each tick is one frame: