@threenative/core 0.3.2 → 0.3.4

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.
@@ -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-CqvE429w.js';
2
+ import { F as FramePassKind, K as IRenderPassSample, n as IFramePhaseSample, o as IPipelineCensus, I as IRendererLike, as as IRendererOptions, a as IFrameBudgetWindow, j as IFrameBudgetOptions } from './renderer-CfsS2hxi.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-DDmC_VVF.js';
5
5
  import { StoreApi } from 'zustand/vanilla';
6
6
 
7
7
  type ThreeNativeOrientation = "landscape" | "portrait" | "sensor";
@@ -101,6 +101,23 @@ interface IThreeNativeModelPassesConfig {
101
101
  }
102
102
  /** Model optimization options for the asset compile step; `"none"` ships sources verbatim. */
103
103
  interface IThreeNativeModelsConfig {
104
+ /**
105
+ * Lossless scene-graph compaction: flatten empty transform chains, join sibling primitives
106
+ * that share a material, and batch a mesh several nodes reuse as `EXT_mesh_gpu_instancing`.
107
+ *
108
+ * On by default; `false` ships the scene graph as authored. A node matching `protectedPattern`,
109
+ * named in `protectedNames`, targeted by an animation or a skin joint is never merged or
110
+ * instanced, so an exact-name lookup or a bone-driven node survives.
111
+ */
112
+ readonly compact?: boolean | {
113
+ readonly flatten?: boolean;
114
+ readonly instance?: boolean | {
115
+ readonly min?: number;
116
+ };
117
+ readonly join?: boolean;
118
+ readonly protectedNames?: readonly string[];
119
+ readonly protectedPattern?: string;
120
+ };
104
121
  /** Standard glTF TEXCOORD_1 atlas generation for offline static-light assets. */
105
122
  readonly lightmap?: {
106
123
  readonly atlasSize: number;
@@ -167,6 +184,113 @@ interface IThreeNativeModelsConfig {
167
184
  readonly simplifyRatio?: number;
168
185
  };
169
186
  }
187
+ /** Quality policy for automatic discrete LOD; it picks the projected pixel-error budget. */
188
+ type ThreeNativeLodPreset = "aggressive" | "balanced" | "quality";
189
+ /** What the `minTriangles` pre-filter is measured against. */
190
+ type ThreeNativeLodMinTrianglesScope = "primitive" | "asset";
191
+ /** Generation knobs shared by every preset. All are ceilings or filters, not promises. */
192
+ interface IThreeNativeLodGenerationConfig {
193
+ /**
194
+ * Increasing geometric-error targets in normalized mesh-extent units; 1–16 positive finite
195
+ * numbers that strictly increase. Default `[0.002, 0.006, 0.02, 0.06]`. Each is simplified from
196
+ * LOD0 independently and a target that cannot reduce is dropped.
197
+ */
198
+ readonly errorTargets?: readonly number[];
199
+ /** Levels in the chain **including LOD0**; integer 1–8, default 4. `1` emits nothing derived. */
200
+ readonly maxLevels?: number;
201
+ /**
202
+ * Fraction of its predecessor's triangles a derived level must save to be kept; finite in
203
+ * `[0, 1)`, default `0.2`. This is the benefit gate — a mesh that cannot reach it is skipped
204
+ * with `insufficient-reduction`, which is a normal outcome, not an error.
205
+ */
206
+ readonly minSaving?: number;
207
+ /**
208
+ * Cheap pre-filter floor in triangles; positive integer, default `128`. It only avoids
209
+ * clearly-pointless work (the simplifier's fixed per-call cost); `minSaving` is the real gate.
210
+ */
211
+ readonly minTriangles?: number;
212
+ /**
213
+ * What `minTriangles` is measured against; `"primitive"` or `"asset"`, default `"asset"`. The
214
+ * asset scope measures the whole model, so a model split into many small primitives is still
215
+ * eligible on its total.
216
+ */
217
+ readonly minTrianglesScope?: ThreeNativeLodMinTrianglesScope;
218
+ /**
219
+ * Opt-in far rung that joins a mesh's same-material primitives into one draw per material group.
220
+ * Default `false`: with no option the cook is byte-identical to today. A join never crosses a
221
+ * material, never touches a skinned, morph-target or animated node, and never leaves the mesh's
222
+ * own node, so authored LOD0, node identity, per-node visibility, picking and transforms are
223
+ * untouched; the joined geometry is an additional far rung beside LOD0.
224
+ */
225
+ readonly join?: boolean;
226
+ }
227
+ /** Screen-space selection knobs. */
228
+ interface IThreeNativeLodRuntimeConfig {
229
+ /** Projected geometric-error budget in raster pixels; positive finite. Default by preset. */
230
+ readonly maxPixelError?: number;
231
+ /** Fraction in `[0, 0.5)`, default 0.15, that stabilizes coarsening at a boundary. */
232
+ readonly hysteresis?: number;
233
+ }
234
+ /** A partial override for one asset; nested objects overlay, they never replace. */
235
+ interface IThreeNativeLodOverride {
236
+ readonly enabled?: boolean;
237
+ readonly generation?: IThreeNativeLodGenerationConfig;
238
+ readonly preset?: ThreeNativeLodPreset;
239
+ readonly runtime?: IThreeNativeLodRuntimeConfig;
240
+ }
241
+ /**
242
+ * Automatic discrete LOD policy. `{}` resolves to enabled/balanced and any explicit block turns
243
+ * generation on; `false` and `{ enabled: false }` are equivalent absolute kill switches that no
244
+ * per-asset override can re-enable. Overrides are keyed by canonical project-relative source asset
245
+ * (`/` separators).
246
+ *
247
+ * Omission currently bakes nothing: the default-on front door opens only after the qualification
248
+ * phase passes. Until then `assets.lod: {}` is the opt-in that resolves to enabled/balanced.
249
+ */
250
+ interface IThreeNativeLodConfig {
251
+ readonly enabled?: boolean;
252
+ readonly generation?: IThreeNativeLodGenerationConfig;
253
+ readonly overrides?: Readonly<Record<string, boolean | IThreeNativeLodOverride>>;
254
+ readonly preset?: ThreeNativeLodPreset;
255
+ readonly runtime?: IThreeNativeLodRuntimeConfig;
256
+ }
257
+ /** One measured byte ceiling on a produced artifact, and what crossing it does. */
258
+ interface IThreeNativeArtifactBudgetLimit {
259
+ /** Bytes, exclusive: a build measuring exactly the limit is inside it. */
260
+ readonly limit: number;
261
+ /** `"error"` refuses the build and keeps the previous artifact; `"warn"` prints and publishes. */
262
+ readonly severity: "error" | "warn";
263
+ }
264
+ /**
265
+ * Runtime ceilings a profile wants every playtest of the artifact it builds to hold.
266
+ *
267
+ * The fields are the playtest harness's own `assert.performance` fields, one name for one meaning:
268
+ * a budget declared here is merged into each scenario's performance assertion, so a budget and a
269
+ * scenario bound the same number instead of two vocabularies for one measurement. Spelled out
270
+ * rather than imported — the harness runs against plain Three.js with no dependency on this
271
+ * package, and core must not invert that. A closed key list validated in both places is the price;
272
+ * `create-threenative/__tests__/build-report.spec.ts` fails if the two lists drift.
273
+ */
274
+ interface IThreeNativePerformanceBudget {
275
+ /** Per-pass draw-call ceilings, keyed by pass kind: main, shadow, reflection, nested. */
276
+ readonly maxPassDrawCalls?: Readonly<Partial<Record<ThreeNativeRenderPassKind, number>>>;
277
+ /** Per-pass triangle ceilings, keyed by pass kind: main, shadow, reflection, nested. */
278
+ readonly maxPassTriangles?: Readonly<Partial<Record<ThreeNativeRenderPassKind, number>>>;
279
+ /** Per-phase millisecond ceilings at nearest-rank p95: hostGap, update, render, overlay, residual. */
280
+ readonly maxPhaseMsP95?: Readonly<Partial<Record<ThreeNativeFramePhase, number>>>;
281
+ /** Maximum renderer draw-call count across every pass combined. */
282
+ readonly maxDrawCalls?: number;
283
+ /** Maximum nearest-rank 95th-percentile frame time in milliseconds. */
284
+ readonly maxFrameMsP95?: number;
285
+ /** Maximum renderer triangle count across every pass combined. */
286
+ readonly maxTriangles?: number;
287
+ /** Frame-budget floor: the median presented frame must sustain at least this many frames a second. */
288
+ readonly minFps?: number;
289
+ }
290
+ /** The engine's frame-budget phases, as a budget spells them. */
291
+ type ThreeNativeFramePhase = "hostGap" | "overlay" | "render" | "residual" | "update";
292
+ /** The render-pass kinds a per-pass budget bounds. */
293
+ type ThreeNativeRenderPassKind = "main" | "shadow" | "reflection" | "nested";
170
294
  interface IThreeNativeConfig {
171
295
  readonly app?: {
172
296
  readonly id?: string;
@@ -181,7 +305,10 @@ interface IThreeNativeConfig {
181
305
  readonly fullscreen?: boolean;
182
306
  readonly keepScreenOn?: boolean;
183
307
  /**
184
- * Maximum native presentation rate in frames per second. Defaults to 60; `0` removes the
308
+ * Maximum native presentation rate in frames per second. Left out, it follows the display's own
309
+ * refresh rate capped at 120 on desktop and web, and stays 60 on mobile where the ceiling is
310
+ * power and heat; the resolved value and its source are reported on every `TN_FRAME_BUDGET`
311
+ * line as `targetFps` and `targetSource`. A number set here wins outright, and `0` removes the
185
312
  * software ceiling. Android also submits this value as the surface's preferred frame rate,
186
313
  * which the display policy may decline because of hardware, power, or thermal state. Android
187
314
  * uses non-blocking presentation above 60 fps so a missed high-refresh interval does not fall
@@ -207,6 +334,43 @@ interface IThreeNativeConfig {
207
334
  readonly maximized?: boolean;
208
335
  readonly resizable?: boolean;
209
336
  };
337
+ /**
338
+ * Named cook profiles: one authored asset tree, one compiler, a different representation per
339
+ * artifact. `--profile` on `threenative build` wins over `defaults[target]`; with neither, the
340
+ * `assets` block is used exactly as declared. An overlay changes resource-processing options
341
+ * only — the source root, the output root, worker concurrency and exclusions stay in `assets`.
342
+ */
343
+ readonly buildProfiles?: {
344
+ readonly defaults?: {
345
+ readonly android?: string;
346
+ readonly desktop?: string;
347
+ readonly ios?: string;
348
+ readonly web?: string;
349
+ };
350
+ readonly profiles: Readonly<Record<string, {
351
+ /**
352
+ * Byte ceilings on what this profile actually produced, measured after packaging and
353
+ * checked before the artifact is published. `artifactBytes` is the artifact itself (a
354
+ * file, or the recursive sum of a directory, `.app` bundle or outDir);
355
+ * `packagedAssetBytes` is the sum of the asset files that survived the packaging
356
+ * selector. `"error"` refuses the build and leaves the previous artifact in place;
357
+ * `"warn"` prints and publishes.
358
+ */
359
+ readonly artifactBudget?: {
360
+ readonly artifactBytes?: IThreeNativeArtifactBudgetLimit;
361
+ readonly packagedAssetBytes?: IThreeNativeArtifactBudgetLimit;
362
+ };
363
+ /**
364
+ * Runtime ceilings this profile's artifact must hold, published into the
365
+ * `<artifact>.build-report.json` a build writes beside it and merged by
366
+ * `threenative-playtest --build-report` into every scenario's `assert.performance`. The
367
+ * build measures none of it — it cannot; the numbers exist only once a runtime drew
368
+ * frames — so a budget never refuses a build and never passes one either.
369
+ */
370
+ readonly performanceBudget?: IThreeNativePerformanceBudget;
371
+ readonly assets?: Pick<NonNullable<IThreeNativeConfig["assets"]>, "audio" | "budget" | "lod" | "models" | "targets" | "textures">;
372
+ }>>;
373
+ };
210
374
  readonly assets?: {
211
375
  /**
212
376
  * Audio conditioning options, or `"none"` to ship every clip exactly as committed. Absent
@@ -224,6 +388,18 @@ interface IThreeNativeConfig {
224
388
  };
225
389
  /** Source-relative globs omitted from builds; excluded bytes are still reported. */
226
390
  readonly exclude?: readonly string[];
391
+ /**
392
+ * How many cook workers a bake may run at once; absent means the driver's default,
393
+ * min(4, cores - 1). Not part of any cache key, so changing it re-cooks nothing.
394
+ */
395
+ readonly concurrency?: number;
396
+ /**
397
+ * Automatic discrete LOD. `assets.lod: {}` opts in with the balanced default; `false` or
398
+ * `{ enabled: false }` is the absolute kill switch, and per-asset overrides key off canonical
399
+ * source asset paths. Omission currently bakes nothing — the default-on front door opens only
400
+ * after the qualification phase passes. See {@link IThreeNativeLodConfig}.
401
+ */
402
+ readonly lod?: boolean | IThreeNativeLodConfig;
227
403
  readonly models?: "none" | IThreeNativeModelsConfig;
228
404
  readonly output?: string;
229
405
  readonly source?: string;
@@ -258,6 +434,63 @@ interface IThreeNativeConfig {
258
434
  * surface, and says so in `TN_ALPHA_ANTIALIASING` rather than reporting itself applied.
259
435
  */
260
436
  readonly alphaAntialiasing?: boolean;
437
+ /**
438
+ * Whether the engine may render an internal mirror of the scene to collapse repeated draws,
439
+ * and how often that mirror proves a batched material still matches its group's shared draw.
440
+ *
441
+ * On by default, which is the shipping behaviour and is what an unset option means. The mirror
442
+ * is opportunistic and correctness-preserving, but it pays a reconciliation cost per frame, so
443
+ * a game that has measured it as a loss — a scene whose draw count falls without its frame time
444
+ * following — can decline it with `false`. An opted-out game builds no mirror and runs no
445
+ * eligibility scan, so the opt-out costs nothing rather than declining each frame; the authored
446
+ * scene is what renders. The `TN_RENDER_PROJECTION` marker still reports it, with its own
447
+ * reason code rather than one of the measured declines.
448
+ *
449
+ * An object instead of `false` names the material check:
450
+ *
451
+ * - `materialChecks: "spread"` — **the default.** A bounded slice of the batched materials is
452
+ * proved per frame instead of all of them, so a frame of 4,096 colour-only materials costs
453
+ * 512 checks rather than 4,096. A material that gains a roughness, a map or a define still
454
+ * leaves its group and is drawn exactly; it is caught up to `materialCheckStaleFrames` frames
455
+ * later, and `TN_RENDER_PROJECTION` reports that bound. A base-colour edit never waits: the
456
+ * per-instance colour is O(1) per member and always exact.
457
+ * - `materialChecks: "everyFrame"` — the named alternative, and the check exactly as it shipped
458
+ * before the sweep existed: every material proved every frame, at about 1.1 µs a material. Use
459
+ * it when a game would rather pay the per-frame cost than accept the bound.
460
+ *
461
+ * Any other value throws at startup rather than falling back to a default nobody asked for.
462
+ */
463
+ readonly projection?: boolean | {
464
+ readonly materialChecks?: "spread" | "everyFrame";
465
+ };
466
+ /**
467
+ * Projected diameter, in raster pixels, below which the engine does not submit an object to
468
+ * the render camera. On by default at a conservative **0.5 px**: an object under half a pixel
469
+ * cannot light a whole pixel of the frame, so only unresolvable geometry is removed. Set a
470
+ * larger number for a tuned cut — a shipped game's ladder chose 2 px — or `false` to leave
471
+ * every object drawn. The measurement still runs with `false`; `TN_PROJECTION` reports how many
472
+ * objects were considered and skipped. Exempt a single object with `alwaysRender` from
473
+ * `@threenative/core`; the player's camera-attached objects and shadow casters are already kept.
474
+ */
475
+ readonly minimumProjectedPixels?: number | false;
476
+ /**
477
+ * How much of the scene graph the engine walks for world matrices each frame.
478
+ *
479
+ * `"visible"` — the default — does not recurse into a hidden subtree. three's own
480
+ * `updateMatrixWorld` walks every child whatever its `visible` flag, multiplying a world matrix
481
+ * for a full-detail body whose merged stand-in is showing, a hidden LOD level, a parked model —
482
+ * none of which can draw. The engine composes every visible node exactly as three does, and
483
+ * remembers a hidden node it skipped so the first frame that subtree shows again its whole
484
+ * chain is refreshed before anything reads it. A game that reads a **hidden** object's
485
+ * `matrixWorld` directly must not rely on the walk having reached it: use `getWorldPosition`
486
+ * (which updates the chain) or call `updateWorldMatrix(true, false)` first.
487
+ *
488
+ * `"all"` visits every node, exactly as three's own walk does — for a game that reads hidden
489
+ * world matrices without going through `getWorld*` and cannot say so per object. Both modes
490
+ * report the number of nodes walked in the `TN_PROJECTION` window, so the cost of the walk the
491
+ * default removed is measurable rather than asserted.
492
+ */
493
+ readonly matrixWorld?: "visible" | "all";
261
494
  /**
262
495
  * Android-only rendering overrides selected by the engine.
263
496
  *
@@ -279,7 +512,7 @@ interface IThreeNativeConfig {
279
512
  };
280
513
  readonly ui?: {
281
514
  /**
282
- * Which renderer draws `src/ui/`.
515
+ * Which renderer draws `src/ui/`. Defaults to `"web"`.
283
516
  *
284
517
  * `"web"` runs the same React DOM, Tailwind, CSS, SVG and fonts on every target, through
285
518
  * that platform's own browser-class renderer composited over the game surface. What is
@@ -298,6 +531,182 @@ interface IThreeNativeConfig {
298
531
  };
299
532
  }
300
533
 
534
+ /**
535
+ * Per-object geometry cost, measured from what the renderer actually submitted.
536
+ *
537
+ * A total triangle counter cannot name the offender: a main character can cost 500 triangles while
538
+ * a barely visible tree submits two million, and both land in the same number. `FrameBudget` stays
539
+ * authoritative for a frame's per-pass totals; this attributes a slice of those totals to the
540
+ * objects that caused them, and reconciles the two so the difference is visible rather than
541
+ * assumed away.
542
+ *
543
+ * Three properties make it a measurement rather than a guess:
544
+ *
545
+ * 1. **It counts submissions, not scene contents.** The instrument is `onBeforeRender`, which the
546
+ * renderer calls once per object, per material group, per pass, *after* frustum and projected-
547
+ * size culling. A `visible` flag is a game's intent; a submission is what the GPU was handed.
548
+ * 2. **It is armed only on request.** Nothing is installed, walked or counted until a consumer asks
549
+ * for one capture, and every hook is removed when that frame ends, is cancelled, or the scene
550
+ * exits. An idle game pays nothing.
551
+ * 3. **It says "unavailable" with a reason.** A packed batch's per-member draws, an indirect
552
+ * counter with no readback, a geometry with no recoverable source detail: each is named, never
553
+ * reported as zero and never quietly dropped, and the unattributed remainder of every pass is
554
+ * reported beside the measured total.
555
+ *
556
+ * @situation find out which scene object is submitting the frame's triangles
557
+ * @situation tell a cheap foreground character from an expensive distant prop
558
+ */
559
+
560
+ /** Rows returned when a request does not name a limit. */
561
+ declare const GEOMETRY_CAPTURE_DEFAULT_LIMIT = 50;
562
+ /** The most rows one request may ask for. A transport carries a report, not a scene dump. */
563
+ declare const GEOMETRY_CAPTURE_MAX_LIMIT = 500;
564
+ /** The inspection ceiling, shared with scene-node observation so one scene has one limit. */
565
+ declare const GEOMETRY_CAPTURE_WALK_CAP = 50000;
566
+ /** How long a request waits for a presented world frame before answering "unavailable". */
567
+ declare const GEOMETRY_CAPTURE_TIMEOUT_MS = 2000;
568
+ /** The `userData` key a loader stamps on a model root so a clone keeps its provenance. */
569
+ declare const GEOMETRY_ASSET_KEY = "tnAssetPath";
570
+ /** The orderings a request may ask for. Ranking happens over the inspected scope, then slices. */
571
+ declare const GEOMETRY_CAPTURE_SORTS: readonly ["triangles", "draws", "projected"];
572
+ type GeometryCaptureSort = (typeof GEOMETRY_CAPTURE_SORTS)[number];
573
+ interface IGeometryCaptureRequest {
574
+ /** Rows to return, 1 to `GEOMETRY_CAPTURE_MAX_LIMIT`. */
575
+ readonly limit?: number;
576
+ readonly sort?: GeometryCaptureSort;
577
+ readonly timeoutMs?: number;
578
+ }
579
+ /** What a rendered object is: the game's own object, or the mirror standing in for some of them. */
580
+ type GeometryOwnershipKind = "exact" | "instancedBatch" | "materialBatch";
581
+ /** Where a row's triangle number came from, so a derived number is never read as a measured one. */
582
+ type GeometryTriangleSource = "renderer" | "batchMembers";
583
+ interface IGeometryPassCost {
584
+ readonly draws: number;
585
+ readonly triangles: number;
586
+ }
587
+ interface IGeometryCapturePass extends IGeometryPassCost {
588
+ readonly kind: FramePassKind;
589
+ /** What the rows below account for. */
590
+ readonly attributedDraws: number;
591
+ readonly attributedTriangles: number;
592
+ /**
593
+ * Measured minus attributed. Negative means the rows over-claim — a derived batch sum, say —
594
+ * which is a finding, not something to clamp away.
595
+ */
596
+ readonly unattributedDraws: number;
597
+ readonly unattributedTriangles: number;
598
+ }
599
+ interface IGeometryCaptureMesh {
600
+ readonly id: string;
601
+ readonly name: string;
602
+ readonly path: string;
603
+ readonly type: string;
604
+ /** LOD0 triangles of this mesh, when the source detail is recoverable. */
605
+ readonly fullDetailTriangles?: number;
606
+ /** Triangles in the buffer the renderer was pointed at this frame. */
607
+ readonly selectedDetailTriangles?: number;
608
+ readonly submittedTriangles?: number;
609
+ readonly trianglesSource?: GeometryTriangleSource;
610
+ readonly draws: number;
611
+ readonly materials: number;
612
+ readonly instances?: number;
613
+ readonly submissions: Readonly<Partial<Record<FramePassKind, IGeometryPassCost>>>;
614
+ readonly batchOwner?: string;
615
+ readonly visible: boolean;
616
+ readonly inFrustum?: boolean;
617
+ readonly unavailable?: readonly string[];
618
+ }
619
+ interface IGeometryCaptureRow {
620
+ readonly id: string;
621
+ readonly generation: number;
622
+ readonly name: string;
623
+ readonly path: string;
624
+ readonly type: string;
625
+ /** The logical asset this row came from, when a loader stamped one. */
626
+ readonly asset?: string;
627
+ readonly fullDetailTriangles?: number;
628
+ readonly selectedDetailTriangles?: number;
629
+ readonly submittedTriangles?: number;
630
+ readonly trianglesSource?: GeometryTriangleSource;
631
+ readonly lod?: {
632
+ readonly level?: number;
633
+ readonly levels?: number;
634
+ };
635
+ /** Estimated projected diameter in drawing-buffer pixels. An estimate, never a pixel count. */
636
+ readonly projectedPixels?: number;
637
+ /**
638
+ * Where those bounds landed, in drawing-buffer pixels from the top-left. An overlay outlines
639
+ * this; it is the estimated bounds' centre, not a silhouette.
640
+ */
641
+ readonly projectedCenter?: readonly [number, number];
642
+ readonly viewportFraction?: number;
643
+ readonly cameraDistance?: number;
644
+ readonly inFrustum?: boolean;
645
+ readonly visibility: "submitted" | "notSubmitted";
646
+ /** Submitted copies of this row's geometry, instances included. */
647
+ readonly copies: number;
648
+ readonly instances?: number;
649
+ /** Triangles counted once per distinct geometry, for a unique-inventory reading. */
650
+ readonly uniqueTriangles?: number;
651
+ readonly draws: number;
652
+ readonly materials: number;
653
+ readonly submissions: Readonly<Partial<Record<FramePassKind, IGeometryPassCost>>>;
654
+ readonly batch?: {
655
+ readonly kind: GeometryOwnershipKind;
656
+ readonly owner: string;
657
+ readonly members: number;
658
+ readonly perMemberDrawsAvailable: boolean;
659
+ };
660
+ readonly unavailable?: readonly string[];
661
+ readonly meshes: readonly IGeometryCaptureMesh[];
662
+ }
663
+ interface IGeometryCaptureAsset {
664
+ readonly asset: string;
665
+ readonly objects: number;
666
+ readonly submittedTriangles?: number;
667
+ readonly draws: number;
668
+ /** Triangles of the distinct geometries this asset contributed, counted once each. */
669
+ readonly uniqueTriangles?: number;
670
+ readonly unavailable?: readonly string[];
671
+ }
672
+ interface IGeometryCaptureReport {
673
+ readonly status: "captured" | "unavailable";
674
+ /** Set when `status` is `unavailable`. A stale capture is never returned as a fresh one. */
675
+ readonly reason?: string;
676
+ readonly capturedAtMs?: number;
677
+ readonly durationMs?: number;
678
+ readonly generation?: number;
679
+ readonly tick?: number;
680
+ readonly frame?: number;
681
+ readonly backend?: string;
682
+ readonly camera?: {
683
+ readonly type: "perspective" | "orthographic" | "unknown";
684
+ readonly position: readonly [number, number, number];
685
+ readonly fov?: number;
686
+ readonly zoom?: number;
687
+ readonly near?: number;
688
+ readonly far?: number;
689
+ };
690
+ readonly viewport?: {
691
+ readonly width: number;
692
+ readonly height: number;
693
+ };
694
+ readonly sort?: GeometryCaptureSort;
695
+ readonly limit?: number;
696
+ /** Rows the inspected scope produced before the limit sliced them. */
697
+ readonly matched?: number;
698
+ readonly returned?: number;
699
+ /** True when the limit cut rows; distinct from an incomplete inspection. */
700
+ readonly rowsTruncated?: boolean;
701
+ /** False when the walk cap stopped the inspection, so no ranking is a global claim. */
702
+ readonly inspectionComplete?: boolean;
703
+ readonly partialRanking?: boolean;
704
+ readonly inspectedNodes?: number;
705
+ readonly passes?: readonly IGeometryCapturePass[];
706
+ readonly objects?: readonly IGeometryCaptureRow[];
707
+ readonly assets?: readonly IGeometryCaptureAsset[];
708
+ }
709
+
301
710
  /**
302
711
  * One action, read either as a button through `pressed`/`justPressed`, a 2D axis through
303
712
  * `vector`, or a scalar axis through `axis`. Which one you get depends on the fields you fill in,
@@ -398,6 +807,14 @@ interface IRawInputState {
398
807
  buttons: readonly boolean[];
399
808
  };
400
809
  }
810
+ /**
811
+ * Ask the browser to lock the pointer to `target`, the capture every first-person mouse look needs.
812
+ *
813
+ * Returns the platform's own promise when it has one, so the caller can wait for the lock the way
814
+ * the browser reports it, and `undefined` when the platform answered synchronously. A target that
815
+ * cannot capture at all throws rather than leaving a game with a camera that never turns.
816
+ */
817
+ declare function captureMouse(target: EventTarget): Promise<void> | undefined;
401
818
  declare class InputMap {
402
819
  #private;
403
820
  readonly raw: IRawInputState;
@@ -438,6 +855,7 @@ interface IAfterPhysicsContext {
438
855
  declare function afterPhysics(context: IAfterPhysicsContext, callback: AfterPhysicsCallback): () => void;
439
856
  interface IRenderPerformanceMetrics {
440
857
  readonly drawCalls?: number;
858
+ readonly passes?: readonly IRenderPassSample[];
441
859
  readonly triangles?: number;
442
860
  }
443
861
  interface IRenderPerformanceSample extends IRenderPerformanceMetrics {
@@ -704,6 +1122,33 @@ interface IWarmUpOptions {
704
1122
  * This is opt-in because WebGPU does not expose a portable pipeline-serialization API.
705
1123
  */
706
1124
  readonly cache?: IWarmUpCacheOptions;
1125
+ /**
1126
+ * Render the warm scene once after compiling, so every pass's pipelines exist before the first
1127
+ * frame. Default `true`.
1128
+ *
1129
+ * `compileAsync` builds only the main-pass pipelines. A shadow-casting light's shadow map and a
1130
+ * reflection pass each need their own variants, created synchronously on first use — on a native
1131
+ * Midway launch that was **44–107 pipelines after "ready"**, with freezes up to 615 ms. One
1132
+ * hidden render of the warm scene, with the shadow map forced to update, builds them while the
1133
+ * startup cover is still up. The render is invisible (it does not clear, and its output is
1134
+ * behind the loading layer) and restores every renderer and scene state it touches.
1135
+ *
1136
+ * Set `false` to skip it. The warm-up still reports `passes: []` and `passPipelines: 0`, because
1137
+ * turning a convention off must not turn its measurement off.
1138
+ */
1139
+ readonly renderPasses?: boolean;
1140
+ /**
1141
+ * Also render objects the scene has hidden for that one covered render. Default `false`.
1142
+ *
1143
+ * `renderPasses` already disables frustum culling, so every caster is submitted to the shadow
1144
+ * pass and every reflected object to the reflection pass even when the startup camera cannot see
1145
+ * it. A hidden LOD level or a parked model is still skipped, though: three never draws a subtree
1146
+ * whose ancestor's `visible` is false, so its shadow and reflection pipelines stay unbuilt. With
1147
+ * this on, the warm-up forces every hidden object visible for that one render — ancestors
1148
+ * included — and restores every original `visible` exactly afterwards. The count is reported as
1149
+ * `visibilityForced`, so a game can see how much of its scene the render had to un-hide.
1150
+ */
1151
+ readonly includeHidden?: boolean;
707
1152
  }
708
1153
  /** What the warm-up did, so a caller can report it rather than assume it. */
709
1154
  type WarmUpObservationStatus = "complete" | "incomplete" | "unavailable";
@@ -770,12 +1215,27 @@ interface IWarmUpReport {
770
1215
  readonly computeTimedOut?: boolean;
771
1216
  /** The optional persistent warm-up hint's outcome. */
772
1217
  readonly cache?: WarmUpCacheStatus;
1218
+ /**
1219
+ * The passes the hidden warm render exercised: `main` always, plus `shadow` when a shadow-casting
1220
+ * light is present and `reflection` when a reflector node is reachable from a material. Empty when
1221
+ * `renderPasses` was off, so an overridden convention still reports what it did. Absent when the
1222
+ * renderer exposes no `render` at all.
1223
+ */
1224
+ readonly passes?: readonly string[];
1225
+ /** Backend pipelines observed created by the hidden warm render alone. Absent with `passes`. */
1226
+ readonly passPipelines?: number;
1227
+ /** Objects whose `frustumCulled` the warm render turned off, so off-screen casters were submitted. */
1228
+ readonly cullingForced?: number;
1229
+ /** Hidden objects the warm render forced visible (`includeHidden`); zero when it was off. */
1230
+ readonly visibilityForced?: number;
773
1231
  }
774
1232
  /** The narrow slice of the renderer this needs. Structural so a test needs no renderer. */
775
1233
  interface IWarmUpRenderer {
776
1234
  compileAsync?: (scene: Object3D, camera: Camera, targetScene?: Object3D) => Promise<void>;
777
1235
  computeAsync?: (node: unknown) => Promise<void>;
778
1236
  pipelineCensus?: () => IPipelineCensus;
1237
+ /** The renderer's own render path, used to build every pass's pipelines once. */
1238
+ render?: (scene: Object3D, camera: Camera) => void;
779
1239
  raw?: unknown;
780
1240
  }
781
1241
  /**
@@ -861,6 +1321,10 @@ interface IStartupStatus {
861
1321
  * 0 to 1, monotonic and honest: the loader's settled/requested ratio carries the first 0.7
862
1322
  * while the start scene loads, 0.8 once the world is entered, 0.9 once first-use compilation
863
1323
  * settled, 1 when `whenReady()` resolves.
1324
+ *
1325
+ * Monotonic is enforced, not assumed: this is a high-water mark over the measured load state,
1326
+ * because that state can fall — requesting an asset after an earlier one settled shrinks the
1327
+ * ratio, and a bar that jumps backwards reads to a player as the load restarting.
864
1328
  */
865
1329
  readonly progress: number;
866
1330
  /** When each milestone happened; members appear as they are reached. */
@@ -925,6 +1389,12 @@ interface ICtx<TState extends Record<string, unknown> = Record<string, unknown>,
925
1389
  readonly after: (delay: number, callback: () => void) => ScheduleHandle;
926
1390
  /** Register a callback for the engine-owned phase after physics writes solved transforms. */
927
1391
  readonly afterPhysics: (callback: AfterPhysicsCallback) => () => void;
1392
+ /**
1393
+ * Register work that runs once per actual world render, after the frame's last fixed update and
1394
+ * before the projection reconciles and the renderer draws. A held, loader-only frame has no world
1395
+ * draw and therefore dispatches nothing.
1396
+ */
1397
+ readonly beforeRender: (callback: () => void) => () => void;
928
1398
  readonly every: (callback: (dt: number) => void) => ScheduleHandle;
929
1399
  readonly state: GameStore<TState>;
930
1400
  readonly tween: <T extends object>(target: T, properties: {
@@ -951,13 +1421,19 @@ interface ICtx<TState extends Record<string, unknown> = Record<string, unknown>,
951
1421
  type PluginCleanup = () => void;
952
1422
  interface IGameObservationSampleRequest {
953
1423
  readonly entities?: readonly string[];
1424
+ /** Asks for one armed per-object geometry capture. Absent means no capture is collected. */
1425
+ readonly geometry?: IGeometryCaptureRequest;
954
1426
  readonly include?: readonly string[];
955
1427
  readonly label?: string;
956
1428
  readonly resources?: readonly string[];
957
1429
  }
958
1430
  interface IGameObservationContribution {
959
1431
  readonly capabilities: readonly string[];
960
- readonly sample: (request: IGameObservationSampleRequest) => Readonly<Record<string, unknown>>;
1432
+ /**
1433
+ * May answer a promise: an observation that has to wait for the renderer — a geometry capture
1434
+ * waits for one presented world frame — cannot be produced inside the request that asked for it.
1435
+ */
1436
+ readonly sample: (request: IGameObservationSampleRequest) => Readonly<Record<string, unknown>> | Promise<Readonly<Record<string, unknown>>>;
961
1437
  }
962
1438
  interface IGameRuntimeObservations {
963
1439
  contribute(contribution: IGameObservationContribution): PluginCleanup;
@@ -974,12 +1450,22 @@ interface IGamePluginRuntime {
974
1450
  readonly enableRuntimeDiagnostics?: () => void;
975
1451
  /** The frame's cost attribution so far, or undefined when the game turned the budget off. */
976
1452
  readonly frameBudgetWindow?: () => IFrameBudgetWindow | undefined;
1453
+ /**
1454
+ * Stop the live clock, so a rendered frame simulates nothing and banks no wall-clock time.
1455
+ *
1456
+ * A run that counts fixed-step ticks must not also accumulate real seconds into the same
1457
+ * simulation, and the frames before its first `advance()` are exactly where a boot's seconds
1458
+ * used to land. Optional: a runtime without a clock just never freezes one.
1459
+ */
1460
+ readonly freezeClock?: () => void;
977
1461
  /**
978
1462
  * Hold start-scene entry until `gate` settles.
979
1463
  *
980
- * The returned promise settles after `Scene.enter()` has run. A runner can therefore release the
981
- * gate after applying pre-entry setup, then await the returned promise before describing
982
- * entity-derived capabilities. The frame loop remains held throughout.
1464
+ * The returned promise settles after the scene that owns the world has entered: the start scene
1465
+ * unless it navigated out of its own `enter()`, in which case it is the scene it navigated to,
1466
+ * loaded and entered. A runner can therefore release the gate after applying pre-entry setup,
1467
+ * then await the returned promise before describing entity-derived capabilities — the entities
1468
+ * they read are the ones the world actually has. The frame loop remains held throughout.
983
1469
  */
984
1470
  readonly holdStart?: (gate: Promise<void>) => Promise<void>;
985
1471
  readonly observations: IGameRuntimeObservations;
@@ -1001,6 +1487,11 @@ interface IGamePluginRuntime {
1001
1487
  readonly startupTimeline?: () => IStartupTimeline;
1002
1488
  /** The renderer-owned bounded pipeline capture, when the renderer has not been opted out. */
1003
1489
  readonly pipelineCensus?: () => IPipelineCensus;
1490
+ /**
1491
+ * Arms one per-object geometry capture and answers its report after the next presented world
1492
+ * frame. Absent on a runtime with no render loop to arm.
1493
+ */
1494
+ readonly geometryCapture?: (request?: IGeometryCaptureRequest) => Promise<IGeometryCaptureReport>;
1004
1495
  readonly step: number;
1005
1496
  }
1006
1497
  interface IGamePlatformSource {
@@ -1100,6 +1591,7 @@ interface IGameConfig<TState extends Record<string, unknown> = Record<string, un
1100
1591
  */
1101
1592
  readonly step?: number;
1102
1593
  readonly start: string;
1594
+ /** Optional slower UI publication interval. Omitted publishes once per rendered frame. */
1103
1595
  readonly stateFlushMs?: number;
1104
1596
  }
1105
1597
  interface IPerspectiveCameraConfig {
@@ -1124,7 +1616,7 @@ type CameraConfig = IPerspectiveCameraConfig | IOrthogonalCameraConfig;
1124
1616
  * two channels through an in-process broker, which is what keeps one `src/ui/` honest: a HUD
1125
1617
  * that works here works on a phone.
1126
1618
  *
1127
- * Publication is automatic and throttled to the store's own published cadence, and it stops
1619
+ * Publication is automatic at the rendered frame cadence (or the named stateFlushMs override), and it stops
1128
1620
  * entirely when nothing is listening — a game whose `ui.renderer` is `native` pays nothing.
1129
1621
  */
1130
1622
  interface IGameUi {
@@ -1163,4 +1655,4 @@ interface IGame<TState extends Record<string, unknown> = Record<string, unknown>
1163
1655
  }
1164
1656
  declare function defineGame<TState extends Record<string, unknown>, TPhysics = undefined>(config: IGameConfig<TState, TPhysics>): IGame<TState, TPhysics>;
1165
1657
 
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 };
1658
+ 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, InputMap as a2, type InputPlatformSource as a3, type PointerEvent3DListener as a4, type PointerEvent3DType as a5, PointerEvents3D as a6, Scene as a7, type SceneFrame as a8, ScenePicker as a9, type ScheduleHandle as aa, Scheduler as ab, type ThreeNativeBackgroundMode as ac, type ThreeNativeLodMinTrianglesScope as ad, type ThreeNativeLodPreset as ae, type ThreeNativeOrientation as af, type ThreeNativeUiRenderer as ag, type WarmUpCacheStatus as ah, type WarmUpObservationStatus as ai, afterPhysics as aj, captureMouse as ak, createRandom as al, defineGame as am, warmUpScene as an, 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 };