@threenative/core 0.3.2 → 0.3.3

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.
@@ -74,6 +74,13 @@ interface IAssetLoader {
74
74
  * stay 0 for a game with no manifest, where no size is knowable before the bytes arrive.
75
75
  */
76
76
  readonly progress: {
77
+ /**
78
+ * The logical paths asked for and not yet settled, in request order. A loading screen that
79
+ * only shows a ratio cannot say *what* it is waiting for, which is the difference between
80
+ * "still loading" and "stuck on `akagi.glb`" — and a stall report that names nothing is a
81
+ * bug report nobody can act on.
82
+ */
83
+ readonly pending: readonly string[];
77
84
  readonly requested: number;
78
85
  readonly requestedBytes: number;
79
86
  readonly settled: number;
@@ -14,6 +14,15 @@ interface IAudioBusOptions {
14
14
  readonly maxVoices?: number;
15
15
  }
16
16
  interface IAudioPlayOptions {
17
+ /**
18
+ * What this cue is, for anything reading back what the game played — a playtest above all.
19
+ *
20
+ * Nothing about the sound changes. Every other audio check answers "is the file right": that it
21
+ * exists, decodes, and is inside its byte budget. None of them can answer "did the game say the
22
+ * general-quarters line twice", which is the class of defect players actually report, so the bus
23
+ * keeps a ledger of the labels it was given.
24
+ */
25
+ readonly cue?: string;
17
26
  readonly fade?: number;
18
27
  readonly loop?: boolean;
19
28
  readonly volume?: number;
@@ -49,6 +58,21 @@ interface IAudioPlayOptions {
49
58
  readonly lowpassHz?: number;
50
59
  }
51
60
  interface IAudioRuntimeSnapshot {
61
+ /**
62
+ * How many times each labelled cue has sounded, across every live bus.
63
+ *
64
+ * This is the only observation that can answer "what did the game actually say, and how often".
65
+ * Every other audio check in the repository is about the *file* — that it exists, decodes and is
66
+ * inside its budget — and all of them stay green while a one-shot line plays a second time
67
+ * halfway through a match, which is the defect players report. A game opts a cue in by passing
68
+ * `cue` to `play`/`playAt`; unlabelled sounds never appear here.
69
+ */
70
+ readonly cues: Readonly<Record<string, number>>;
71
+ /** The most recent labelled cues in the order they sounded, bounded per bus. */
72
+ readonly recentCues: ReadonlyArray<{
73
+ readonly atMs: number;
74
+ readonly cue: string;
75
+ }>;
52
76
  readonly queued: number;
53
77
  readonly voices: number;
54
78
  /** Retired voices held for reuse. Bounded by peak concurrency, never by session length. */
@@ -67,6 +91,13 @@ interface IAudioRuntimeSnapshot {
67
91
  */
68
92
  readonly unsupported: readonly string[];
69
93
  }
94
+ /**
95
+ * Forgets every recorded cue.
96
+ *
97
+ * @situation clear the recorded audio cue counts between tests so one test cannot read another's plays
98
+ * @example resetAudioCueLedger();
99
+ */
100
+ declare function resetAudioCueLedger(): void;
70
101
  declare class AudioBus {
71
102
  #private;
72
103
  readonly listener: AudioListener;
@@ -153,4 +184,4 @@ declare class AudioBus {
153
184
  }
154
185
  declare function audioRuntimeSnapshot(): IAudioRuntimeSnapshot;
155
186
 
156
- export { AudioBus as A, type IAudioBusOptions as I, audioRuntimeSnapshot as a, type IAudioPlayOptions as b };
187
+ export { AudioBus as A, type IAudioBusOptions as I, audioRuntimeSnapshot as a, type IAudioPlayOptions as b, resetAudioCueLedger as r };
@@ -1,6 +1,6 @@
1
1
  import * as three from 'three';
2
2
  import { Camera, Vector2, Vector3, Scene, OrthographicCamera } from 'three';
3
- import { I as IRendererLike } from './renderer-C6hqZpoG.js';
3
+ import { I as IRendererLike } from './renderer-Cy4qeBOA.js';
4
4
 
5
5
  interface IViewportSize {
6
6
  readonly aspect: number;
@@ -1,7 +1,7 @@
1
- import { I as IAssetLoader, a as IAssetLoaderOptions } from './assets-kyoF7JlJ.js';
2
- import { h as IFramePhaseSample, i as IPipelineCensus, I as IRendererLike, a6 as IRendererOptions, g as IFrameBudgetWindow, e as IFrameBudgetOptions } from './renderer-C6hqZpoG.js';
1
+ import { I as IAssetLoader, a as IAssetLoaderOptions } from './assets-CYKk2WTu.js';
2
+ import { F as FramePassKind, H as IRenderPassSample, m as IFramePhaseSample, n as IPipelineCensus, I as IRendererLike, ad as IRendererOptions, a as IFrameBudgetWindow, i as IFrameBudgetOptions } from './renderer-Cy4qeBOA.js';
3
3
  import { Vector2, Vector3, Object3D, Camera, Intersection, Scene as Scene$1 } from 'three';
4
- import { V as Viewport, C as CanvasLayer, I as IViewportOptions } from './canvas-layer-BLVijiUJ.js';
4
+ import { V as Viewport, C as CanvasLayer, I as IViewportOptions } from './canvas-layer-C1SnMoJ-.js';
5
5
  import { StoreApi } from 'zustand/vanilla';
6
6
 
7
7
  type ThreeNativeOrientation = "landscape" | "portrait" | "sensor";
@@ -167,6 +167,76 @@ interface IThreeNativeModelsConfig {
167
167
  readonly simplifyRatio?: number;
168
168
  };
169
169
  }
170
+ /** Quality policy for automatic discrete LOD; it picks the projected pixel-error budget. */
171
+ type ThreeNativeLodPreset = "aggressive" | "balanced" | "quality";
172
+ /** What the `minTriangles` pre-filter is measured against. */
173
+ type ThreeNativeLodMinTrianglesScope = "primitive" | "asset";
174
+ /** Generation knobs shared by every preset. All are ceilings or filters, not promises. */
175
+ interface IThreeNativeLodGenerationConfig {
176
+ /**
177
+ * Increasing geometric-error targets in normalized mesh-extent units; 1–16 positive finite
178
+ * numbers that strictly increase. Default `[0.002, 0.006, 0.02, 0.06]`. Each is simplified from
179
+ * LOD0 independently and a target that cannot reduce is dropped.
180
+ */
181
+ readonly errorTargets?: readonly number[];
182
+ /** Levels in the chain **including LOD0**; integer 1–8, default 4. `1` emits nothing derived. */
183
+ readonly maxLevels?: number;
184
+ /**
185
+ * Fraction of its predecessor's triangles a derived level must save to be kept; finite in
186
+ * `[0, 1)`, default `0.2`. This is the benefit gate — a mesh that cannot reach it is skipped
187
+ * with `insufficient-reduction`, which is a normal outcome, not an error.
188
+ */
189
+ readonly minSaving?: number;
190
+ /**
191
+ * Cheap pre-filter floor in triangles; positive integer, default `128`. It only avoids
192
+ * clearly-pointless work (the simplifier's fixed per-call cost); `minSaving` is the real gate.
193
+ */
194
+ readonly minTriangles?: number;
195
+ /**
196
+ * What `minTriangles` is measured against; `"primitive"` or `"asset"`, default `"asset"`. The
197
+ * asset scope measures the whole model, so a model split into many small primitives is still
198
+ * eligible on its total.
199
+ */
200
+ readonly minTrianglesScope?: ThreeNativeLodMinTrianglesScope;
201
+ /**
202
+ * Opt-in far rung that joins a mesh's same-material primitives into one draw per material group.
203
+ * Default `false`: with no option the cook is byte-identical to today. A join never crosses a
204
+ * material, never touches a skinned, morph-target or animated node, and never leaves the mesh's
205
+ * own node, so authored LOD0, node identity, per-node visibility, picking and transforms are
206
+ * untouched; the joined geometry is an additional far rung beside LOD0.
207
+ */
208
+ readonly join?: boolean;
209
+ }
210
+ /** Screen-space selection knobs. */
211
+ interface IThreeNativeLodRuntimeConfig {
212
+ /** Projected geometric-error budget in raster pixels; positive finite. Default by preset. */
213
+ readonly maxPixelError?: number;
214
+ /** Fraction in `[0, 0.5)`, default 0.15, that stabilizes coarsening at a boundary. */
215
+ readonly hysteresis?: number;
216
+ }
217
+ /** A partial override for one asset; nested objects overlay, they never replace. */
218
+ interface IThreeNativeLodOverride {
219
+ readonly enabled?: boolean;
220
+ readonly generation?: IThreeNativeLodGenerationConfig;
221
+ readonly preset?: ThreeNativeLodPreset;
222
+ readonly runtime?: IThreeNativeLodRuntimeConfig;
223
+ }
224
+ /**
225
+ * Automatic discrete LOD policy. `{}` resolves to enabled/balanced and any explicit block turns
226
+ * generation on; `false` and `{ enabled: false }` are equivalent absolute kill switches that no
227
+ * per-asset override can re-enable. Overrides are keyed by canonical project-relative source asset
228
+ * (`/` separators).
229
+ *
230
+ * Omission currently bakes nothing: the default-on front door opens only after the qualification
231
+ * phase passes. Until then `assets.lod: {}` is the opt-in that resolves to enabled/balanced.
232
+ */
233
+ interface IThreeNativeLodConfig {
234
+ readonly enabled?: boolean;
235
+ readonly generation?: IThreeNativeLodGenerationConfig;
236
+ readonly overrides?: Readonly<Record<string, boolean | IThreeNativeLodOverride>>;
237
+ readonly preset?: ThreeNativeLodPreset;
238
+ readonly runtime?: IThreeNativeLodRuntimeConfig;
239
+ }
170
240
  interface IThreeNativeConfig {
171
241
  readonly app?: {
172
242
  readonly id?: string;
@@ -224,6 +294,13 @@ interface IThreeNativeConfig {
224
294
  };
225
295
  /** Source-relative globs omitted from builds; excluded bytes are still reported. */
226
296
  readonly exclude?: readonly string[];
297
+ /**
298
+ * Automatic discrete LOD. `assets.lod: {}` opts in with the balanced default; `false` or
299
+ * `{ enabled: false }` is the absolute kill switch, and per-asset overrides key off canonical
300
+ * source asset paths. Omission currently bakes nothing — the default-on front door opens only
301
+ * after the qualification phase passes. See {@link IThreeNativeLodConfig}.
302
+ */
303
+ readonly lod?: boolean | IThreeNativeLodConfig;
227
304
  readonly models?: "none" | IThreeNativeModelsConfig;
228
305
  readonly output?: string;
229
306
  readonly source?: string;
@@ -258,6 +335,46 @@ interface IThreeNativeConfig {
258
335
  * surface, and says so in `TN_ALPHA_ANTIALIASING` rather than reporting itself applied.
259
336
  */
260
337
  readonly alphaAntialiasing?: boolean;
338
+ /**
339
+ * Whether the engine may render an internal mirror of the scene to collapse repeated draws.
340
+ *
341
+ * On by default, which is the shipping behaviour and is what an unset option means. The mirror
342
+ * is opportunistic and correctness-preserving, but it pays a reconciliation cost per frame, so
343
+ * a game that has measured it as a loss — a scene whose draw count falls without its frame time
344
+ * following — can decline it with `false`. An opted-out game builds no mirror and runs no
345
+ * eligibility scan, so the opt-out costs nothing rather than declining each frame; the authored
346
+ * scene is what renders. The `TN_RENDER_PROJECTION` marker still reports it, with its own
347
+ * reason code rather than one of the measured declines.
348
+ */
349
+ readonly projection?: boolean;
350
+ /**
351
+ * Projected diameter, in raster pixels, below which the engine does not submit an object to
352
+ * the render camera. On by default at a conservative **0.5 px**: an object under half a pixel
353
+ * cannot light a whole pixel of the frame, so only unresolvable geometry is removed. Set a
354
+ * larger number for a tuned cut — a shipped game's ladder chose 2 px — or `false` to leave
355
+ * every object drawn. The measurement still runs with `false`; `TN_PROJECTION` reports how many
356
+ * objects were considered and skipped. Exempt a single object with `alwaysRender` from
357
+ * `@threenative/core`; the player's camera-attached objects and shadow casters are already kept.
358
+ */
359
+ readonly minimumProjectedPixels?: number | false;
360
+ /**
361
+ * How much of the scene graph the engine walks for world matrices each frame.
362
+ *
363
+ * `"visible"` — the default — does not recurse into a hidden subtree. three's own
364
+ * `updateMatrixWorld` walks every child whatever its `visible` flag, multiplying a world matrix
365
+ * for a full-detail body whose merged stand-in is showing, a hidden LOD level, a parked model —
366
+ * none of which can draw. The engine composes every visible node exactly as three does, and
367
+ * remembers a hidden node it skipped so the first frame that subtree shows again its whole
368
+ * chain is refreshed before anything reads it. A game that reads a **hidden** object's
369
+ * `matrixWorld` directly must not rely on the walk having reached it: use `getWorldPosition`
370
+ * (which updates the chain) or call `updateWorldMatrix(true, false)` first.
371
+ *
372
+ * `"all"` visits every node, exactly as three's own walk does — for a game that reads hidden
373
+ * world matrices without going through `getWorld*` and cannot say so per object. Both modes
374
+ * report the number of nodes walked in the `TN_PROJECTION` window, so the cost of the walk the
375
+ * default removed is measurable rather than asserted.
376
+ */
377
+ readonly matrixWorld?: "visible" | "all";
261
378
  /**
262
379
  * Android-only rendering overrides selected by the engine.
263
380
  *
@@ -279,7 +396,7 @@ interface IThreeNativeConfig {
279
396
  };
280
397
  readonly ui?: {
281
398
  /**
282
- * Which renderer draws `src/ui/`.
399
+ * Which renderer draws `src/ui/`. Defaults to `"web"`.
283
400
  *
284
401
  * `"web"` runs the same React DOM, Tailwind, CSS, SVG and fonts on every target, through
285
402
  * that platform's own browser-class renderer composited over the game surface. What is
@@ -298,6 +415,182 @@ interface IThreeNativeConfig {
298
415
  };
299
416
  }
300
417
 
418
+ /**
419
+ * Per-object geometry cost, measured from what the renderer actually submitted.
420
+ *
421
+ * A total triangle counter cannot name the offender: a main character can cost 500 triangles while
422
+ * a barely visible tree submits two million, and both land in the same number. `FrameBudget` stays
423
+ * authoritative for a frame's per-pass totals; this attributes a slice of those totals to the
424
+ * objects that caused them, and reconciles the two so the difference is visible rather than
425
+ * assumed away.
426
+ *
427
+ * Three properties make it a measurement rather than a guess:
428
+ *
429
+ * 1. **It counts submissions, not scene contents.** The instrument is `onBeforeRender`, which the
430
+ * renderer calls once per object, per material group, per pass, *after* frustum and projected-
431
+ * size culling. A `visible` flag is a game's intent; a submission is what the GPU was handed.
432
+ * 2. **It is armed only on request.** Nothing is installed, walked or counted until a consumer asks
433
+ * for one capture, and every hook is removed when that frame ends, is cancelled, or the scene
434
+ * exits. An idle game pays nothing.
435
+ * 3. **It says "unavailable" with a reason.** A packed batch's per-member draws, an indirect
436
+ * counter with no readback, a geometry with no recoverable source detail: each is named, never
437
+ * reported as zero and never quietly dropped, and the unattributed remainder of every pass is
438
+ * reported beside the measured total.
439
+ *
440
+ * @situation find out which scene object is submitting the frame's triangles
441
+ * @situation tell a cheap foreground character from an expensive distant prop
442
+ */
443
+
444
+ /** Rows returned when a request does not name a limit. */
445
+ declare const GEOMETRY_CAPTURE_DEFAULT_LIMIT = 50;
446
+ /** The most rows one request may ask for. A transport carries a report, not a scene dump. */
447
+ declare const GEOMETRY_CAPTURE_MAX_LIMIT = 500;
448
+ /** The inspection ceiling, shared with scene-node observation so one scene has one limit. */
449
+ declare const GEOMETRY_CAPTURE_WALK_CAP = 50000;
450
+ /** How long a request waits for a presented world frame before answering "unavailable". */
451
+ declare const GEOMETRY_CAPTURE_TIMEOUT_MS = 2000;
452
+ /** The `userData` key a loader stamps on a model root so a clone keeps its provenance. */
453
+ declare const GEOMETRY_ASSET_KEY = "tnAssetPath";
454
+ /** The orderings a request may ask for. Ranking happens over the inspected scope, then slices. */
455
+ declare const GEOMETRY_CAPTURE_SORTS: readonly ["triangles", "draws", "projected"];
456
+ type GeometryCaptureSort = (typeof GEOMETRY_CAPTURE_SORTS)[number];
457
+ interface IGeometryCaptureRequest {
458
+ /** Rows to return, 1 to `GEOMETRY_CAPTURE_MAX_LIMIT`. */
459
+ readonly limit?: number;
460
+ readonly sort?: GeometryCaptureSort;
461
+ readonly timeoutMs?: number;
462
+ }
463
+ /** What a rendered object is: the game's own object, or the mirror standing in for some of them. */
464
+ type GeometryOwnershipKind = "exact" | "instancedBatch" | "materialBatch";
465
+ /** Where a row's triangle number came from, so a derived number is never read as a measured one. */
466
+ type GeometryTriangleSource = "renderer" | "batchMembers";
467
+ interface IGeometryPassCost {
468
+ readonly draws: number;
469
+ readonly triangles: number;
470
+ }
471
+ interface IGeometryCapturePass extends IGeometryPassCost {
472
+ readonly kind: FramePassKind;
473
+ /** What the rows below account for. */
474
+ readonly attributedDraws: number;
475
+ readonly attributedTriangles: number;
476
+ /**
477
+ * Measured minus attributed. Negative means the rows over-claim — a derived batch sum, say —
478
+ * which is a finding, not something to clamp away.
479
+ */
480
+ readonly unattributedDraws: number;
481
+ readonly unattributedTriangles: number;
482
+ }
483
+ interface IGeometryCaptureMesh {
484
+ readonly id: string;
485
+ readonly name: string;
486
+ readonly path: string;
487
+ readonly type: string;
488
+ /** LOD0 triangles of this mesh, when the source detail is recoverable. */
489
+ readonly fullDetailTriangles?: number;
490
+ /** Triangles in the buffer the renderer was pointed at this frame. */
491
+ readonly selectedDetailTriangles?: number;
492
+ readonly submittedTriangles?: number;
493
+ readonly trianglesSource?: GeometryTriangleSource;
494
+ readonly draws: number;
495
+ readonly materials: number;
496
+ readonly instances?: number;
497
+ readonly submissions: Readonly<Partial<Record<FramePassKind, IGeometryPassCost>>>;
498
+ readonly batchOwner?: string;
499
+ readonly visible: boolean;
500
+ readonly inFrustum?: boolean;
501
+ readonly unavailable?: readonly string[];
502
+ }
503
+ interface IGeometryCaptureRow {
504
+ readonly id: string;
505
+ readonly generation: number;
506
+ readonly name: string;
507
+ readonly path: string;
508
+ readonly type: string;
509
+ /** The logical asset this row came from, when a loader stamped one. */
510
+ readonly asset?: string;
511
+ readonly fullDetailTriangles?: number;
512
+ readonly selectedDetailTriangles?: number;
513
+ readonly submittedTriangles?: number;
514
+ readonly trianglesSource?: GeometryTriangleSource;
515
+ readonly lod?: {
516
+ readonly level?: number;
517
+ readonly levels?: number;
518
+ };
519
+ /** Estimated projected diameter in drawing-buffer pixels. An estimate, never a pixel count. */
520
+ readonly projectedPixels?: number;
521
+ /**
522
+ * Where those bounds landed, in drawing-buffer pixels from the top-left. An overlay outlines
523
+ * this; it is the estimated bounds' centre, not a silhouette.
524
+ */
525
+ readonly projectedCenter?: readonly [number, number];
526
+ readonly viewportFraction?: number;
527
+ readonly cameraDistance?: number;
528
+ readonly inFrustum?: boolean;
529
+ readonly visibility: "submitted" | "notSubmitted";
530
+ /** Submitted copies of this row's geometry, instances included. */
531
+ readonly copies: number;
532
+ readonly instances?: number;
533
+ /** Triangles counted once per distinct geometry, for a unique-inventory reading. */
534
+ readonly uniqueTriangles?: number;
535
+ readonly draws: number;
536
+ readonly materials: number;
537
+ readonly submissions: Readonly<Partial<Record<FramePassKind, IGeometryPassCost>>>;
538
+ readonly batch?: {
539
+ readonly kind: GeometryOwnershipKind;
540
+ readonly owner: string;
541
+ readonly members: number;
542
+ readonly perMemberDrawsAvailable: boolean;
543
+ };
544
+ readonly unavailable?: readonly string[];
545
+ readonly meshes: readonly IGeometryCaptureMesh[];
546
+ }
547
+ interface IGeometryCaptureAsset {
548
+ readonly asset: string;
549
+ readonly objects: number;
550
+ readonly submittedTriangles?: number;
551
+ readonly draws: number;
552
+ /** Triangles of the distinct geometries this asset contributed, counted once each. */
553
+ readonly uniqueTriangles?: number;
554
+ readonly unavailable?: readonly string[];
555
+ }
556
+ interface IGeometryCaptureReport {
557
+ readonly status: "captured" | "unavailable";
558
+ /** Set when `status` is `unavailable`. A stale capture is never returned as a fresh one. */
559
+ readonly reason?: string;
560
+ readonly capturedAtMs?: number;
561
+ readonly durationMs?: number;
562
+ readonly generation?: number;
563
+ readonly tick?: number;
564
+ readonly frame?: number;
565
+ readonly backend?: string;
566
+ readonly camera?: {
567
+ readonly type: "perspective" | "orthographic" | "unknown";
568
+ readonly position: readonly [number, number, number];
569
+ readonly fov?: number;
570
+ readonly zoom?: number;
571
+ readonly near?: number;
572
+ readonly far?: number;
573
+ };
574
+ readonly viewport?: {
575
+ readonly width: number;
576
+ readonly height: number;
577
+ };
578
+ readonly sort?: GeometryCaptureSort;
579
+ readonly limit?: number;
580
+ /** Rows the inspected scope produced before the limit sliced them. */
581
+ readonly matched?: number;
582
+ readonly returned?: number;
583
+ /** True when the limit cut rows; distinct from an incomplete inspection. */
584
+ readonly rowsTruncated?: boolean;
585
+ /** False when the walk cap stopped the inspection, so no ranking is a global claim. */
586
+ readonly inspectionComplete?: boolean;
587
+ readonly partialRanking?: boolean;
588
+ readonly inspectedNodes?: number;
589
+ readonly passes?: readonly IGeometryCapturePass[];
590
+ readonly objects?: readonly IGeometryCaptureRow[];
591
+ readonly assets?: readonly IGeometryCaptureAsset[];
592
+ }
593
+
301
594
  /**
302
595
  * One action, read either as a button through `pressed`/`justPressed`, a 2D axis through
303
596
  * `vector`, or a scalar axis through `axis`. Which one you get depends on the fields you fill in,
@@ -438,6 +731,7 @@ interface IAfterPhysicsContext {
438
731
  declare function afterPhysics(context: IAfterPhysicsContext, callback: AfterPhysicsCallback): () => void;
439
732
  interface IRenderPerformanceMetrics {
440
733
  readonly drawCalls?: number;
734
+ readonly passes?: readonly IRenderPassSample[];
441
735
  readonly triangles?: number;
442
736
  }
443
737
  interface IRenderPerformanceSample extends IRenderPerformanceMetrics {
@@ -704,6 +998,33 @@ interface IWarmUpOptions {
704
998
  * This is opt-in because WebGPU does not expose a portable pipeline-serialization API.
705
999
  */
706
1000
  readonly cache?: IWarmUpCacheOptions;
1001
+ /**
1002
+ * Render the warm scene once after compiling, so every pass's pipelines exist before the first
1003
+ * frame. Default `true`.
1004
+ *
1005
+ * `compileAsync` builds only the main-pass pipelines. A shadow-casting light's shadow map and a
1006
+ * reflection pass each need their own variants, created synchronously on first use — on a native
1007
+ * Midway launch that was **44–107 pipelines after "ready"**, with freezes up to 615 ms. One
1008
+ * hidden render of the warm scene, with the shadow map forced to update, builds them while the
1009
+ * startup cover is still up. The render is invisible (it does not clear, and its output is
1010
+ * behind the loading layer) and restores every renderer and scene state it touches.
1011
+ *
1012
+ * Set `false` to skip it. The warm-up still reports `passes: []` and `passPipelines: 0`, because
1013
+ * turning a convention off must not turn its measurement off.
1014
+ */
1015
+ readonly renderPasses?: boolean;
1016
+ /**
1017
+ * Also render objects the scene has hidden for that one covered render. Default `false`.
1018
+ *
1019
+ * `renderPasses` already disables frustum culling, so every caster is submitted to the shadow
1020
+ * pass and every reflected object to the reflection pass even when the startup camera cannot see
1021
+ * it. A hidden LOD level or a parked model is still skipped, though: three never draws a subtree
1022
+ * whose ancestor's `visible` is false, so its shadow and reflection pipelines stay unbuilt. With
1023
+ * this on, the warm-up forces every hidden object visible for that one render — ancestors
1024
+ * included — and restores every original `visible` exactly afterwards. The count is reported as
1025
+ * `visibilityForced`, so a game can see how much of its scene the render had to un-hide.
1026
+ */
1027
+ readonly includeHidden?: boolean;
707
1028
  }
708
1029
  /** What the warm-up did, so a caller can report it rather than assume it. */
709
1030
  type WarmUpObservationStatus = "complete" | "incomplete" | "unavailable";
@@ -770,12 +1091,27 @@ interface IWarmUpReport {
770
1091
  readonly computeTimedOut?: boolean;
771
1092
  /** The optional persistent warm-up hint's outcome. */
772
1093
  readonly cache?: WarmUpCacheStatus;
1094
+ /**
1095
+ * The passes the hidden warm render exercised: `main` always, plus `shadow` when a shadow-casting
1096
+ * light is present and `reflection` when a reflector node is reachable from a material. Empty when
1097
+ * `renderPasses` was off, so an overridden convention still reports what it did. Absent when the
1098
+ * renderer exposes no `render` at all.
1099
+ */
1100
+ readonly passes?: readonly string[];
1101
+ /** Backend pipelines observed created by the hidden warm render alone. Absent with `passes`. */
1102
+ readonly passPipelines?: number;
1103
+ /** Objects whose `frustumCulled` the warm render turned off, so off-screen casters were submitted. */
1104
+ readonly cullingForced?: number;
1105
+ /** Hidden objects the warm render forced visible (`includeHidden`); zero when it was off. */
1106
+ readonly visibilityForced?: number;
773
1107
  }
774
1108
  /** The narrow slice of the renderer this needs. Structural so a test needs no renderer. */
775
1109
  interface IWarmUpRenderer {
776
1110
  compileAsync?: (scene: Object3D, camera: Camera, targetScene?: Object3D) => Promise<void>;
777
1111
  computeAsync?: (node: unknown) => Promise<void>;
778
1112
  pipelineCensus?: () => IPipelineCensus;
1113
+ /** The renderer's own render path, used to build every pass's pipelines once. */
1114
+ render?: (scene: Object3D, camera: Camera) => void;
779
1115
  raw?: unknown;
780
1116
  }
781
1117
  /**
@@ -861,6 +1197,10 @@ interface IStartupStatus {
861
1197
  * 0 to 1, monotonic and honest: the loader's settled/requested ratio carries the first 0.7
862
1198
  * while the start scene loads, 0.8 once the world is entered, 0.9 once first-use compilation
863
1199
  * settled, 1 when `whenReady()` resolves.
1200
+ *
1201
+ * Monotonic is enforced, not assumed: this is a high-water mark over the measured load state,
1202
+ * because that state can fall — requesting an asset after an earlier one settled shrinks the
1203
+ * ratio, and a bar that jumps backwards reads to a player as the load restarting.
864
1204
  */
865
1205
  readonly progress: number;
866
1206
  /** When each milestone happened; members appear as they are reached. */
@@ -925,6 +1265,12 @@ interface ICtx<TState extends Record<string, unknown> = Record<string, unknown>,
925
1265
  readonly after: (delay: number, callback: () => void) => ScheduleHandle;
926
1266
  /** Register a callback for the engine-owned phase after physics writes solved transforms. */
927
1267
  readonly afterPhysics: (callback: AfterPhysicsCallback) => () => void;
1268
+ /**
1269
+ * Register work that runs once per actual world render, after the frame's last fixed update and
1270
+ * before the projection reconciles and the renderer draws. A held, loader-only frame has no world
1271
+ * draw and therefore dispatches nothing.
1272
+ */
1273
+ readonly beforeRender: (callback: () => void) => () => void;
928
1274
  readonly every: (callback: (dt: number) => void) => ScheduleHandle;
929
1275
  readonly state: GameStore<TState>;
930
1276
  readonly tween: <T extends object>(target: T, properties: {
@@ -951,13 +1297,19 @@ interface ICtx<TState extends Record<string, unknown> = Record<string, unknown>,
951
1297
  type PluginCleanup = () => void;
952
1298
  interface IGameObservationSampleRequest {
953
1299
  readonly entities?: readonly string[];
1300
+ /** Asks for one armed per-object geometry capture. Absent means no capture is collected. */
1301
+ readonly geometry?: IGeometryCaptureRequest;
954
1302
  readonly include?: readonly string[];
955
1303
  readonly label?: string;
956
1304
  readonly resources?: readonly string[];
957
1305
  }
958
1306
  interface IGameObservationContribution {
959
1307
  readonly capabilities: readonly string[];
960
- readonly sample: (request: IGameObservationSampleRequest) => Readonly<Record<string, unknown>>;
1308
+ /**
1309
+ * May answer a promise: an observation that has to wait for the renderer — a geometry capture
1310
+ * waits for one presented world frame — cannot be produced inside the request that asked for it.
1311
+ */
1312
+ readonly sample: (request: IGameObservationSampleRequest) => Readonly<Record<string, unknown>> | Promise<Readonly<Record<string, unknown>>>;
961
1313
  }
962
1314
  interface IGameRuntimeObservations {
963
1315
  contribute(contribution: IGameObservationContribution): PluginCleanup;
@@ -1001,6 +1353,11 @@ interface IGamePluginRuntime {
1001
1353
  readonly startupTimeline?: () => IStartupTimeline;
1002
1354
  /** The renderer-owned bounded pipeline capture, when the renderer has not been opted out. */
1003
1355
  readonly pipelineCensus?: () => IPipelineCensus;
1356
+ /**
1357
+ * Arms one per-object geometry capture and answers its report after the next presented world
1358
+ * frame. Absent on a runtime with no render loop to arm.
1359
+ */
1360
+ readonly geometryCapture?: (request?: IGeometryCaptureRequest) => Promise<IGeometryCaptureReport>;
1004
1361
  readonly step: number;
1005
1362
  }
1006
1363
  interface IGamePlatformSource {
@@ -1100,6 +1457,7 @@ interface IGameConfig<TState extends Record<string, unknown> = Record<string, un
1100
1457
  */
1101
1458
  readonly step?: number;
1102
1459
  readonly start: string;
1460
+ /** Optional slower UI publication interval. Omitted publishes once per rendered frame. */
1103
1461
  readonly stateFlushMs?: number;
1104
1462
  }
1105
1463
  interface IPerspectiveCameraConfig {
@@ -1124,7 +1482,7 @@ type CameraConfig = IPerspectiveCameraConfig | IOrthogonalCameraConfig;
1124
1482
  * two channels through an in-process broker, which is what keeps one `src/ui/` honest: a HUD
1125
1483
  * that works here works on a phone.
1126
1484
  *
1127
- * Publication is automatic and throttled to the store's own published cadence, and it stops
1485
+ * Publication is automatic at the rendered frame cadence (or the named stateFlushMs override), and it stops
1128
1486
  * entirely when nothing is listening — a game whose `ui.renderer` is `native` pays nothing.
1129
1487
  */
1130
1488
  interface IGameUi {
@@ -1163,4 +1521,4 @@ interface IGame<TState extends Record<string, unknown> = Record<string, unknown>
1163
1521
  }
1164
1522
  declare function defineGame<TState extends Record<string, unknown>, TPhysics = undefined>(config: IGameConfig<TState, TPhysics>): IGame<TState, TPhysics>;
1165
1523
 
1166
- export { afterPhysics as $, type AfterPhysicsCallback as A, type IThreeNativeIconVariants as B, type ContextMenuPolicy as C, type IThreeNativeTexturesConfig as D, type ITweenOptions as E, type IWarmUpCacheOptions as F, type IWarmUpObservation as G, type IWarmUpOptions as H, type IGame as I, type IWarmUpProgress as J, type IWarmUpRenderer as K, type IWarmUpReport as L, type InputBindings as M, type InputPlatformSource as N, type PointerEvent3DType as O, type PointerEvent3DListener as P, PointerEvents3D as Q, type SceneFrame as R, Scene as S, ScenePicker as T, type ScheduleHandle as U, Scheduler as V, type ThreeNativeBackgroundMode as W, type ThreeNativeOrientation as X, type ThreeNativeUiRenderer as Y, type WarmUpCacheStatus as Z, type WarmUpObservationStatus as _, type IGamePluginRuntime as a, createRandom as a0, defineGame as a1, warmUpScene as a2, type IGamePluginHooks as b, type ICtx as c, type IGameObservationContribution as d, type IGameObservationSampleRequest as e, type IGamePlatformSource as f, type IInputAction as g, type IInputGamepad as h, type IPointerDragHandle as i, type IPointerEvent3D as j, type IPointerEvents3D as k, type IPointerEvents3DOptions as l, type IPointerEvents3DPicker as m, type IPointerState as n, type IRandom as o, type IRawInputPointer as p, type IRawInputPointerEdge as q, type IRawInputState as r, type IRaycastOptions as s, type IScenePickerOptions as t, type IThreeNativeAudioConfig as u, type IThreeNativeAudioLoop as v, type IThreeNativeAudioOverride as w, type IThreeNativeAudioSpectrum as x, type IThreeNativeBootSplash as y, type IThreeNativeConfig as z };
1524
+ export { type IWarmUpRenderer as $, type AfterPhysicsCallback as A, type IRandom as B, type ContextMenuPolicy as C, type IRawInputPointer as D, type IRawInputPointerEdge as E, type IRawInputState as F, GEOMETRY_ASSET_KEY as G, type IRaycastOptions as H, type IGame as I, type IScenePickerOptions as J, type IThreeNativeAudioConfig as K, type IThreeNativeAudioLoop as L, type IThreeNativeAudioOverride as M, type IThreeNativeAudioSpectrum as N, type IThreeNativeBootSplash as O, type IThreeNativeConfig as P, type IThreeNativeIconVariants as Q, type IThreeNativeLodConfig as R, type IThreeNativeLodGenerationConfig as S, type IThreeNativeLodOverride as T, type IThreeNativeLodRuntimeConfig as U, type IThreeNativeTexturesConfig as V, type ITweenOptions as W, type IWarmUpCacheOptions as X, type IWarmUpObservation as Y, type IWarmUpOptions as Z, type IWarmUpProgress as _, type IGamePluginRuntime as a, type IWarmUpReport as a0, type InputBindings as a1, type InputPlatformSource as a2, type PointerEvent3DListener as a3, type PointerEvent3DType as a4, PointerEvents3D as a5, Scene as a6, type SceneFrame as a7, ScenePicker as a8, type ScheduleHandle as a9, Scheduler as aa, type ThreeNativeBackgroundMode as ab, type ThreeNativeLodMinTrianglesScope as ac, type ThreeNativeLodPreset as ad, type ThreeNativeOrientation as ae, type ThreeNativeUiRenderer as af, type WarmUpCacheStatus as ag, type WarmUpObservationStatus as ah, afterPhysics as ai, createRandom as aj, defineGame as ak, warmUpScene as al, type IGamePluginHooks as b, GEOMETRY_CAPTURE_DEFAULT_LIMIT as c, GEOMETRY_CAPTURE_MAX_LIMIT as d, GEOMETRY_CAPTURE_SORTS as e, GEOMETRY_CAPTURE_TIMEOUT_MS as f, GEOMETRY_CAPTURE_WALK_CAP as g, type GeometryCaptureSort as h, type ICtx as i, type IGameObservationContribution as j, type IGameObservationSampleRequest as k, type IGamePlatformSource as l, type IGeometryCaptureAsset as m, type IGeometryCaptureMesh as n, type IGeometryCapturePass as o, type IGeometryCaptureReport as p, type IGeometryCaptureRequest as q, type IGeometryCaptureRow as r, type IInputAction as s, type IInputGamepad as t, type IPointerDragHandle as u, type IPointerEvent3D as v, type IPointerEvents3D as w, type IPointerEvents3DOptions as x, type IPointerEvents3DPicker as y, type IPointerState as z };
@@ -1,5 +1,5 @@
1
1
  import { Object3D } from 'three';
2
- import { I as IRendererLike } from './renderer-C6hqZpoG.js';
2
+ import { I as IRendererLike } from './renderer-Cy4qeBOA.js';
3
3
 
4
4
  /**
5
5
  * The lifecycle contract for a game-owned GPU simulation.
package/dist/hot.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { a as audioRuntimeSnapshot } from './audio-BFiGneTL.js';
2
- import { I as IGame } from './game-XGrTzapq.js';
1
+ import { a as audioRuntimeSnapshot } from './audio-7i3Xl0l3.js';
2
+ import { I as IGame } from './game-D_6r-k4Y.js';
3
3
  import 'three';
4
- import './assets-kyoF7JlJ.js';
5
- import './renderer-C6hqZpoG.js';
4
+ import './assets-CYKk2WTu.js';
5
+ import './renderer-Cy4qeBOA.js';
6
6
  import 'three/webgpu';
7
- import './canvas-layer-BLVijiUJ.js';
7
+ import './canvas-layer-C1SnMoJ-.js';
8
8
  import 'zustand/vanilla';
9
9
 
10
10
  interface IHotDiagnostics {
package/dist/hot.js CHANGED
@@ -1,21 +1,38 @@
1
1
  import 'three';
2
2
 
3
3
  // src/audio.ts
4
- var buses = /* @__PURE__ */ new Set();
4
+ var AUDIO_STATE = /* @__PURE__ */ Symbol.for("threenative.audio.runtime");
5
+ function audioState() {
6
+ const host = globalThis;
7
+ const existing = host[AUDIO_STATE];
8
+ if (existing !== void 0) return existing;
9
+ const created = { buses: /* @__PURE__ */ new Set(), cueCounts: /* @__PURE__ */ new Map(), cueLog: [] };
10
+ host[AUDIO_STATE] = created;
11
+ return created;
12
+ }
5
13
  function audioRuntimeSnapshot() {
6
14
  let queued = 0;
7
15
  let voices = 0;
8
16
  let pooled = 0;
9
17
  let paused = 0;
10
18
  const unsupported = /* @__PURE__ */ new Set();
11
- for (const bus of buses) {
19
+ const state = audioState();
20
+ for (const bus of state.buses) {
12
21
  queued += bus.queued;
13
22
  voices += bus.voices;
14
23
  pooled += bus.pooled;
15
24
  paused += bus.pausedVoices;
16
25
  for (const option of bus.unsupported) unsupported.add(option);
17
26
  }
18
- return { paused, pooled, queued, unsupported: [...unsupported].sort(), voices };
27
+ return {
28
+ cues: Object.fromEntries(state.cueCounts),
29
+ paused,
30
+ pooled,
31
+ queued,
32
+ recentCues: state.cueLog.map((entry) => ({ ...entry })),
33
+ unsupported: [...unsupported].sort(),
34
+ voices
35
+ };
19
36
  }
20
37
 
21
38
  // src/hot.ts