@zkmake/three-meter 0.12.0 → 0.13.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 CHANGED
@@ -125,6 +125,7 @@ Canvas is its own React root. To run two canvases on one page, pass the same `st
125
125
  | `injectStyles` | `true` | Append the stylesheet once per document. |
126
126
  | `refreshHz` | `10` | Repaint rate. |
127
127
  | `budgets` | timing defaults | Limits past which a value turns amber. See [Budgets](#budgets). |
128
+ | `quality` | none | Adaptive quality to show. See [Adaptive quality](#adaptive-quality). |
128
129
  | `label` | `"Performance"` | Accessible name of the panel. |
129
130
 
130
131
  `PerformanceMonitor` and `PerfSampler` take `trackGPU` (default true), `gpuQueryPoolSize` (default 5),
@@ -194,6 +195,29 @@ mountPerfHud(monitor, { budgets });
194
195
 
195
196
  three-meter reads the file's shape (`AppBudgets`) and doesn't depend on three-audit.
196
197
 
198
+ ## Adaptive quality
199
+
200
+ A game that adapts its render quality at runtime with [three-quality](../three-quality) can show
201
+ what it's doing: pass the controller, or in React its store, as `quality`. The card gets one line
202
+ under the grid, `dpr 1.25 · adapting · shadows off`, and the full view a **Quality** row; hover
203
+ either for the level and the last window's p90 frame against the budget.
204
+
205
+ ```ts
206
+ const quality = adaptQuality(renderer); // @zkmake/three-quality
207
+
208
+ mountPerfHud(monitor, { quality });
209
+ ```
210
+
211
+ ```tsx
212
+ import { qualityStore } from "@zkmake/three-quality/react";
213
+
214
+ <PerfHud quality={qualityStore} />;
215
+ ```
216
+
217
+ three-meter reads the shape (`QualitySource`: a `state` with `dpr`, `phase`, `level`, `levels`
218
+ and `tiers`), so neither package depends on the other: the game ships three-quality and loads
219
+ three-meter behind `?debug`. `hud.setQuality(source)` swaps it later.
220
+
197
221
  ## Top costs
198
222
 
199
223
  The HUD tells you there are 400 draw calls; **top costs** tells you which objects they come from.
@@ -337,6 +361,25 @@ Styles inject at runtime by default. To link them instead, pass `injectStyles: f
337
361
 
338
362
  The package never reads `NODE_ENV` or `import.meta.env`. Whether the HUD exists in production is
339
363
  your call. Gate the import in the app with a `?debug=` query, a build flag or a dynamic `import()`.
364
+ [three-devtools](../three-devtools) is that gate, done once.
365
+
366
+ A dynamic `import()` only keeps the HUD out of the first load if the bundler leaves it in its own
367
+ chunk. A catch-all chunk group that sends every `node_modules` module to one `vendor` chunk (Vite's
368
+ `codeSplitting.groups`, `manualChunks`) pulls the meter into a chunk `index.html` preloads, so every
369
+ player downloads it. Leave `@zkmake` out of that group:
370
+
371
+ ```ts
372
+ // vite.config.ts, build.rolldownOptions.output.codeSplitting.groups
373
+ {
374
+ name: "vendor",
375
+ // Everything in node_modules but @zkmake, whose dev tools load behind ?debug.
376
+ test: /^(?!.*[\\/]node_modules[\\/]@zkmake[\\/]).*[\\/]node_modules[\\/]/,
377
+ priority: 10,
378
+ },
379
+ ```
380
+
381
+ Then check a production build: no chunk that `index.html` loads or preloads contains
382
+ `PerformanceMonitor`.
340
383
 
341
384
  ## Contract
342
385
 
@@ -160,6 +160,31 @@ declare class HudSettings {
160
160
  private load;
161
161
  private persistAndNotify;
162
162
  }
163
+ /**
164
+ * Adaptive quality, as the HUD shows it: one line, `dpr 1.25 · adapting · shadows off`. The source
165
+ * is `@zkmake/three-quality`'s controller (`adaptQuality`) or its React store (`qualityStore`),
166
+ * read by shape like `AppBudgets`, so three-meter needs no dependency and the runtime package none
167
+ * on this dev tool. Anything with a `state` of this shape works.
168
+ */
169
+ type QualityReading = {
170
+ /** The pixel ratio applied. */
171
+ readonly dpr: number;
172
+ /** What the controller is doing: `warming`, `adapting`, `steady`, `cpu-bound`, `pinned`. */
173
+ readonly phase: string;
174
+ /** 0 is the best rung. */
175
+ readonly level: number;
176
+ readonly levels: number;
177
+ /** The app's tiers by name, `true` while on. */
178
+ readonly tiers?: Readonly<Record<string, boolean>>;
179
+ /** The last window's 90th-percentile frame, ms. */
180
+ readonly p90Ms?: number | null;
181
+ readonly targetFps?: number;
182
+ };
183
+ /** What the HUD's `quality` option takes. `state` is read on each repaint; `null` shows `—`. */
184
+ type QualitySource = {
185
+ readonly state: QualityReading | null;
186
+ };
187
+ declare const formatQuality: (reading: QualityReading | null) => string;
163
188
  type PerformanceViewMode = "full" | "compact";
164
189
  type PerformanceViewOptions = {
165
190
  monitor: PerformanceMonitor;
@@ -173,6 +198,11 @@ type PerformanceViewOptions = {
173
198
  * budgets file (`AppBudgets`) applies its `frame`.
174
199
  */
175
200
  budgets?: BudgetsInput | false;
201
+ /**
202
+ * Adaptive quality to show, one line in the card and a row in the list: three-quality's
203
+ * controller or React store, read by shape. See {@link QualitySource}.
204
+ */
205
+ quality?: QualitySource;
176
206
  /** Share one between views to keep them in sync; a fresh instance otherwise. */
177
207
  settings?: HudSettings;
178
208
  /**
@@ -215,6 +245,12 @@ declare class PerformanceView {
215
245
  private budgets;
216
246
  private budgetsInput;
217
247
  private detectedHz;
248
+ private quality;
249
+ private lastQualityText;
250
+ private hudQualityEl;
251
+ private hudQualityValueEl;
252
+ private qualityRowEl;
253
+ private qualityValueEl;
218
254
  private readonly hider;
219
255
  private readonly hiddenRows;
220
256
  private rafId;
@@ -236,6 +272,8 @@ declare class PerformanceView {
236
272
  getMode(): PerformanceViewMode;
237
273
  /** Replace the budgets; `false` turns them all off. Applies on the next paint. */
238
274
  setBudgets(budgets: BudgetsInput | false | undefined): void;
275
+ /** Show another quality source, or none. Applies on the next paint. */
276
+ setQuality(quality: QualitySource | undefined): void;
239
277
  dispose(): void;
240
278
  private frame;
241
279
  private render;
@@ -252,6 +290,9 @@ declare class PerformanceView {
252
290
  private costsNote;
253
291
  /** The monitor caches the environment once its backend settles; until then this re-reads. */
254
292
  private renderFooter;
293
+ /** One string for both the card's line and the list's row; the DOM is touched only on change. */
294
+ private renderQuality;
295
+ private applyQuality;
255
296
  /** Budgets follow the detected refresh rate unless `targetFps` was given. */
256
297
  private followRefresh;
257
298
  private renderHud;
@@ -302,6 +343,11 @@ type MountPerfHudOptions = {
302
343
  * Change later with the handle's `setBudgets`.
303
344
  */
304
345
  budgets?: BudgetsInput | false;
346
+ /**
347
+ * Adaptive quality to show: `@zkmake/three-quality`'s controller (`adaptQuality(…)`) or its React
348
+ * store (`qualityStore`), read by shape, no dependency. Change later with `setQuality`.
349
+ */
350
+ quality?: QualitySource;
305
351
  /** Accessible name of the panel. Default `Performance`. */
306
352
  label?: string;
307
353
  };
@@ -323,8 +369,10 @@ type PerfHudHandle = {
323
369
  setTheme: (mode: ThemeMode) => void;
324
370
  /** Replace the budgets; `false` turns them all off. */
325
371
  setBudgets: (budgets: BudgetsInput | false | undefined) => void;
372
+ /** Show another quality source, or none. */
373
+ setQuality: (quality: QualitySource | undefined) => void;
326
374
  dispose: () => void;
327
375
  };
328
376
  declare const mountPerfHud: (monitor: PerformanceMonitor, options?: MountPerfHudOptions) => PerfHudHandle;
329
- export { dockPanel as C, BudgetsInput as D, Budgets as E, ResolvedBudgets as O, ScreenEdge as S, BudgetKey as T, DefaultPlacement as _, PerformanceViewMode as a, DockPanelOptions as b, HudDefaults as c, HudSettingsOptions as d, HudTheme as f, isThemeMode as g, ThemeMode as h, PerformanceView as i, resolveBudgets as k, HudSelection as l, THEME_MODES as m, PerfHudHandle as n, PerformanceViewOptions as o, ResolvedTheme as p, mountPerfHud as r, DEFAULT_STORAGE_KEY as s, MountPerfHudOptions as t, HudSettings as u, DockAlign as v, AppBudgets as w, Placement as x, DockHandle as y };
330
- //# sourceMappingURL=mount-perf-hud-Cg_iAuFQ.d.ts.map
377
+ export { BudgetsInput as A, DockPanelOptions as C, AppBudgets as D, dockPanel as E, resolveBudgets as M, BudgetKey as O, DockHandle as S, ScreenEdge as T, THEME_MODES as _, PerformanceViewMode as a, DefaultPlacement as b, QualitySource as c, HudDefaults as d, HudSelection as f, ResolvedTheme as g, HudTheme as h, PerformanceView as i, ResolvedBudgets as j, Budgets as k, formatQuality as l, HudSettingsOptions as m, PerfHudHandle as n, PerformanceViewOptions as o, HudSettings as p, mountPerfHud as r, QualityReading as s, MountPerfHudOptions as t, DEFAULT_STORAGE_KEY as u, ThemeMode as v, Placement as w, DockAlign as x, isThemeMode as y };
378
+ //# sourceMappingURL=mount-perf-hud-C0yuW5JI.d.ts.map
@@ -969,6 +969,34 @@ const PERF_HUD_STYLES = `
969
969
  text-align: right;
970
970
  }
971
971
 
972
+ /* Adaptive quality (three-quality), under the grid: one muted line, the reading in the foreground. */
973
+ .perf-monitor__hud-quality {
974
+ display: flex;
975
+ align-items: center;
976
+ gap: 4px;
977
+ min-width: 0;
978
+ color: var(--perf-muted);
979
+ font-size: 10px;
980
+ }
981
+
982
+ .perf-monitor__hud-quality[hidden],
983
+ .perf-monitor__row[hidden] {
984
+ display: none;
985
+ }
986
+
987
+ .perf-monitor__hud-quality .perf-monitor__icon {
988
+ width: 11px;
989
+ height: 11px;
990
+ }
991
+
992
+ .perf-monitor__hud-quality-value {
993
+ overflow: hidden;
994
+ color: var(--perf-fg);
995
+ font-variant-numeric: tabular-nums lining-nums;
996
+ text-overflow: ellipsis;
997
+ white-space: nowrap;
998
+ }
999
+
972
1000
  .perf-monitor__footer {
973
1001
  display: flex;
974
1002
  align-items: flex-start;
@@ -1400,7 +1428,7 @@ const mountDevPanel = (options) => {
1400
1428
  };
1401
1429
  // package.json
1402
1430
  var name = "@zkmake/three-meter";
1403
- var version = "0.12.0";
1431
+ var version = "0.13.1";
1404
1432
  // src/ui/clipboard.ts
1405
1433
  /**
1406
1434
  * Copy text from a click handler. The async Clipboard API needs a secure
@@ -1511,6 +1539,23 @@ const METRIC_HELP = {
1511
1539
  textures: "Textures held in GPU memory. Should level off; a steady climb is a leak, so dispose() what you remove.",
1512
1540
  triangles: "Triangles drawn this frame. Mostly GPU cost: use lower-poly models or level of detail to cut it."
1513
1541
  };
1542
+ // src/ui/quality.ts
1543
+ const ratio = (dpr) => Number(dpr.toFixed(2)).toString();
1544
+ const tiersOff = (reading) => Object.entries(reading.tiers ?? {}).filter(([, on]) => !on).map(([name]) => name);
1545
+ const formatQuality = (reading) => {
1546
+ if (!reading) return "—";
1547
+ const off = tiersOff(reading);
1548
+ return [
1549
+ `dpr ${ratio(reading.dpr)}`,
1550
+ reading.phase,
1551
+ off.length ? `${off.join(", ")} off` : ""
1552
+ ].filter(Boolean).join(" · ");
1553
+ };
1554
+ const qualityTitle = (reading) => {
1555
+ if (!reading) return "Adaptive quality: no controller mounted.";
1556
+ const frame = typeof reading.p90Ms === "number" ? `; p90 frame ${reading.p90Ms.toFixed(1)} ms${reading.targetFps ? ` against ${(1e3 / reading.targetFps).toFixed(1)} ms` : ""}` : "";
1557
+ return `Adaptive quality: level ${reading.level + 1} of ${reading.levels} (1 is the best)${frame}.`;
1558
+ };
1514
1559
  // src/ui/report.ts
1515
1560
  /**
1516
1561
  * A plain-text snapshot of the monitor for bug reports: versions, backend and
@@ -1908,6 +1953,12 @@ var PerformanceView = class {
1908
1953
  budgets;
1909
1954
  budgetsInput;
1910
1955
  detectedHz = null;
1956
+ quality;
1957
+ lastQualityText = "";
1958
+ hudQualityEl;
1959
+ hudQualityValueEl;
1960
+ qualityRowEl;
1961
+ qualityValueEl;
1911
1962
  hider = new Hider();
1912
1963
  hiddenRows = /* @__PURE__ */ new Map();
1913
1964
  rafId = null;
@@ -1932,11 +1983,13 @@ var PerformanceView = class {
1932
1983
  this.minIntervalMs = 1e3 / (options.refreshHz ?? 10);
1933
1984
  this.budgetsInput = options.budgets;
1934
1985
  this.budgets = resolveBudgets(options.budgets);
1986
+ this.quality = options.quality;
1935
1987
  this.element = document.createElement("div");
1936
1988
  this.element.className = "perf-monitor";
1937
1989
  this.applyModeClass();
1938
1990
  this.resizeObserver = new ResizeObserver(() => this.resizeCanvases());
1939
1991
  this.build();
1992
+ this.applyQuality();
1940
1993
  this.applyInfoClass();
1941
1994
  this.rebuildHud();
1942
1995
  this.onThemeChanged();
@@ -1967,6 +2020,11 @@ var PerformanceView = class {
1967
2020
  this.budgets = resolveBudgets(budgets, this.detectedHz);
1968
2021
  this.applyBudgetTitles();
1969
2022
  }
2023
+ /** Show another quality source, or none. Applies on the next paint. */
2024
+ setQuality(quality) {
2025
+ this.quality = quality;
2026
+ this.applyQuality();
2027
+ }
1970
2028
  dispose() {
1971
2029
  this.stop();
1972
2030
  this.showAllHidden();
@@ -1986,6 +2044,7 @@ var PerformanceView = class {
1986
2044
  const sample = this.monitor.getSample();
1987
2045
  const stats = this.monitor.getFrameStats();
1988
2046
  this.followRefresh(stats.refreshHz);
2047
+ this.renderQuality();
1989
2048
  if (this.mode === "compact") {
1990
2049
  this.renderHud(sample, stats);
1991
2050
  if (this.settings.info) this.renderFooter();
@@ -2120,6 +2179,25 @@ var PerformanceView = class {
2120
2179
  this.footerGpuEl.title = gpu;
2121
2180
  this.footerHardwareEl.hidden = backend === "" && gpu === "";
2122
2181
  }
2182
+ /** One string for both the card's line and the list's row; the DOM is touched only on change. */
2183
+ renderQuality() {
2184
+ if (!this.quality) return;
2185
+ const reading = this.quality.state;
2186
+ const text = formatQuality(reading);
2187
+ const title = qualityTitle(reading);
2188
+ if (`${text}\n${title}` === this.lastQualityText) return;
2189
+ this.lastQualityText = `${text}\n${title}`;
2190
+ this.hudQualityValueEl.textContent = text;
2191
+ this.qualityValueEl.textContent = text;
2192
+ this.hudQualityEl.title = title;
2193
+ this.qualityRowEl.title = title;
2194
+ }
2195
+ applyQuality() {
2196
+ this.hudQualityEl.hidden = !this.quality;
2197
+ this.qualityRowEl.hidden = !this.quality;
2198
+ this.lastQualityText = "";
2199
+ this.renderQuality();
2200
+ }
2123
2201
  /** Budgets follow the detected refresh rate unless `targetFps` was given. */
2124
2202
  followRefresh(hz) {
2125
2203
  if (hz === this.detectedHz) return;
@@ -2171,7 +2249,12 @@ var PerformanceView = class {
2171
2249
  this.hudGraphsEl.className = "perf-monitor__hud-graphs";
2172
2250
  this.hudGridEl = document.createElement("div");
2173
2251
  this.hudGridEl.className = "perf-monitor__hud-grid";
2174
- hud.append(this.hudGraphsEl, this.hudGridEl);
2252
+ this.hudQualityEl = document.createElement("div");
2253
+ this.hudQualityEl.className = "perf-monitor__hud-quality";
2254
+ this.hudQualityValueEl = document.createElement("span");
2255
+ this.hudQualityValueEl.className = "perf-monitor__hud-quality-value";
2256
+ this.hudQualityEl.append(createIcon("sliders", "perf-monitor__icon"), this.hudQualityValueEl);
2257
+ hud.append(this.hudGraphsEl, this.hudGridEl, this.hudQualityEl);
2175
2258
  const options = document.createElement("div");
2176
2259
  options.className = "perf-monitor__section perf-monitor__options";
2177
2260
  const themeRow = document.createElement("div");
@@ -2246,6 +2329,15 @@ var PerformanceView = class {
2246
2329
  }
2247
2330
  const stats = document.createElement("div");
2248
2331
  stats.className = "perf-monitor__section";
2332
+ this.qualityRowEl = document.createElement("div");
2333
+ this.qualityRowEl.className = "perf-monitor__row perf-monitor__row--static";
2334
+ const qualityLabel = document.createElement("span");
2335
+ qualityLabel.className = "perf-monitor__label";
2336
+ qualityLabel.textContent = "Quality";
2337
+ this.qualityValueEl = document.createElement("span");
2338
+ this.qualityValueEl.className = "perf-monitor__value";
2339
+ this.qualityValueEl.textContent = "—";
2340
+ this.qualityRowEl.append(createIcon("sliders", "perf-monitor__icon"), qualityLabel, this.qualityValueEl);
2249
2341
  for (const config of NUMBERS) {
2250
2342
  const rowEl = document.createElement("label");
2251
2343
  rowEl.className = "perf-monitor__row";
@@ -2266,6 +2358,7 @@ var PerformanceView = class {
2266
2358
  rowEl.append(createIcon(config.icon, "perf-monitor__icon"), label, value, checkbox, hint);
2267
2359
  stats.append(rowEl);
2268
2360
  }
2361
+ stats.append(this.qualityRowEl);
2269
2362
  const footer = document.createElement("div");
2270
2363
  footer.className = "perf-monitor__footer";
2271
2364
  this.infoCheckbox = this.buildCheckbox(this.settings.info, "Show version and environment in HUD", (on) => this.settings.setInfo(on));
@@ -2502,6 +2595,7 @@ const mountPerfHud = (monitor, options = {}) => {
2502
2595
  budgets: options.budgets,
2503
2596
  mode: options.mode ?? "compact",
2504
2597
  monitor,
2598
+ quality: options.quality,
2505
2599
  refreshHz: options.refreshHz,
2506
2600
  settings,
2507
2601
  theme
@@ -2544,6 +2638,7 @@ const mountPerfHud = (monitor, options = {}) => {
2544
2638
  getMode: () => view.getMode(),
2545
2639
  setBudgets: (budgets) => view.setBudgets(budgets),
2546
2640
  setMode,
2641
+ setQuality: (quality) => view.setQuality(quality),
2547
2642
  getTheme: () => theme.mode,
2548
2643
  setTheme: (mode) => theme.setMode(mode),
2549
2644
  settings,
@@ -2551,6 +2646,6 @@ const mountPerfHud = (monitor, options = {}) => {
2551
2646
  view
2552
2647
  };
2553
2648
  };
2554
- export { PERF_HUD_STYLES as a, DEFAULT_STORAGE_KEY as c, THEME_MODES as d, isThemeMode as f, mountDevPanel as i, HudSettings as l, PerformanceView as n, injectStyles as o, resolveBudgets as p, formatReport as r, dockPanel as s, mountPerfHud as t, HudTheme as u };
2649
+ export { mountDevPanel as a, dockPanel as c, HudTheme as d, THEME_MODES as f, formatQuality as i, DEFAULT_STORAGE_KEY as l, resolveBudgets as m, PerformanceView as n, PERF_HUD_STYLES as o, isThemeMode as p, formatReport as r, injectStyles as s, mountPerfHud as t, HudSettings as u };
2555
2650
 
2556
- //# sourceMappingURL=mount-perf-hud-CJju0oTc.js.map
2651
+ //# sourceMappingURL=mount-perf-hud-DZ1_LUZ8.js.map