@vitreajs/vitrea 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2450 @@
1
+ /**
2
+ * X8 — the shape channel set, and the vocabulary every other module speaks.
3
+ *
4
+ * `{ center, size, radii, smoothing, thickness }`. Every channel is numeric,
5
+ * which is what makes v1's morphs parametric rather than crossfaded: capsule
6
+ * <-> rounded rect, button <-> menu platter, indicator slides.
7
+ *
8
+ * Two conventions worth stating once, because mixing them up is the easiest way
9
+ * to get corner math subtly wrong:
10
+ *
11
+ * - `size` is the FULL width and height. Corner math works in half-extents
12
+ * (`halfW`, `halfH`), which is what the pseudo-SDF families are written in.
13
+ * - `smoothing` here is the value the author wrote. The budget clamp
14
+ * (§corner.ts) is applied at derivation and lands in `ResolvedCorner`, so
15
+ * shrinking a shape and growing it back is lossless (X8 rider 1).
16
+ */
17
+ type Vec2 = readonly [x: number, y: number];
18
+ /** Corner radii, clockwise from top-left. */
19
+ type CornerRadii = readonly [
20
+ topLeft: number,
21
+ topRight: number,
22
+ bottomRight: number,
23
+ bottomLeft: number
24
+ ];
25
+ /** The v1 shape families (§Geometry). Not "Apple's taxonomy" — vitrea's supported set. */
26
+ declare const SHAPE_FAMILIES: readonly ["fixed-rounded-rect", "capsule", "concentric-rounded-rect"];
27
+ type ShapeFamily = (typeof SHAPE_FAMILIES)[number];
28
+ /**
29
+ * Public sugar over the internal numeric corner profile: `"circular"` is
30
+ * `smoothing: 0`, `"continuous"` is the calibration-determined Apple-matching
31
+ * corner (§apple.ts). Authoring a number instead puts the shape on the Figma
32
+ * smoothing axis, which is the interpolable authoring family.
33
+ */
34
+ type CornerProfile = "continuous" | "circular";
35
+ /**
36
+ * X8 — the shape channel set. Every channel is numeric, which is what makes
37
+ * v1's parametric morphs total: capsule <-> rounded rect, button <-> platter.
38
+ */
39
+ interface ShapeChannels {
40
+ readonly center: Vec2;
41
+ readonly size: Vec2;
42
+ readonly radii: CornerRadii;
43
+ /** 0 = circular arc corners, 1 = maximum continuous-curvature smoothing. */
44
+ readonly smoothing: number;
45
+ /** Material thickness, in CSS px, driving lensing depth and shadow. */
46
+ readonly thickness: number;
47
+ }
48
+
49
+ /**
50
+ * ShapeSpec -> ResolvedShape: authoring input to the X8 channel vector plus
51
+ * everything derived from it.
52
+ *
53
+ * A resolved shape carries three layers, and keeping them separate is what makes
54
+ * morphs lossless:
55
+ *
56
+ * - `channels` — X8 exactly, holding the AUTHORED values. `smoothing` here is
57
+ * what the author wrote, never the clamped value.
58
+ * - `corner` — the derivation: budget, clamped radius, effective smoothing,
59
+ * corner reach, and the five fitted coefficients. For C6 this
60
+ * is the six derived floats the instance buffer widens by
61
+ * (`reach` + `k`), recomputed per frame during a morph.
62
+ * - `reference` — which curve the corner is fit against.
63
+ *
64
+ * ## Two corner references, and why there are two
65
+ *
66
+ * S2 measured that the reference curve, not the field family, is the fidelity
67
+ * bottleneck. That leaves a genuine tension the spec resolves in both
68
+ * directions at once, so the kernel carries both:
69
+ *
70
+ * - **`"figma-smoothing"`** — the interpolable authoring axis. `smoothing` is
71
+ * a free channel over [0, 1], the budget clamp applies to it, and this is
72
+ * the axis the declared error bound is measured on (§Geometry binds a
73
+ * continuous numeric corner profile clamped by a budget derived from size
74
+ * and radii).
75
+ * - **`"apple-continuous"`** — the Apple-direct fit. Apple's curve has no
76
+ * smoothing parameter, so `smoothing` is pinned at the seed and Apple's own
77
+ * budget policy applies (clamp the RADIUS, not the smoothing). This is what
78
+ * `profile: "continuous"` resolves to, per Decision Log #20.
79
+ *
80
+ * `profile: "circular"` is smoothing 0, which is the same exact circular corner
81
+ * under either reference and is therefore the member they share.
82
+ *
83
+ * The two references are not points on one axis and the kernel does not pretend
84
+ * otherwise: `morph.ts` refuses a morph that crosses them rather than inventing
85
+ * a blend the error bound does not cover. In practice v1's morph pairs share a
86
+ * profile, and `APPLE_BEST_FIGMA_SMOOTHING` is the documented way onto the
87
+ * interpolable axis for a caller that needs one.
88
+ */
89
+
90
+ type CornerReference = "figma-smoothing" | "apple-continuous";
91
+
92
+ /**
93
+ * Group union: bounded smooth-min over member fields.
94
+ *
95
+ * §Geometry: group rendering goes through a per-group field pass (instances ->
96
+ * group SDF/coverage field -> one optical pass), "which makes bounded smooth-min
97
+ * proximity union within a group nearly free; union aesthetics (neck width, max
98
+ * bulge, separation threshold) are **capped** and calibration-tuned so nothing
99
+ * reads as jelly."
100
+ *
101
+ * The three tunables the spec names, and what each one actually does:
102
+ *
103
+ * - **`neckWidth`** — the polynomial smooth-min's blend width `k`. Two members
104
+ * closer than about `k/2` grow a neck between them.
105
+ * - **`maxBulge`** — the cap. A quadratic smooth-min deviates from `min` by at
106
+ * most `k/4`, so capping the deviation caps `k` at `4 * maxBulge`; the total
107
+ * over an n-member fold is clamped once at the end, which makes the bound
108
+ * hold for any member count and any fold order.
109
+ * - **`separationThreshold`** — the gate. Without it, two members far apart
110
+ * still depress the field on the segment between them, because a smooth min
111
+ * of two equal values dips even when both are large. That depression is what
112
+ * reads as jelly. The gate switches blending off wherever the nearest member
113
+ * is farther than half the threshold, which is exactly where a neck could no
114
+ * longer form.
115
+ *
116
+ * Both gates are load-bearing and they catch different cases: `|a - b| >= k`
117
+ * saturates the blend next to one member when the other is far, and the
118
+ * separation gate kills the midpoint depression between two distant members.
119
+ * With both, the union is EXACTLY `min` everywhere except near a real seam.
120
+ */
121
+
122
+ interface GroupUnionParams {
123
+ /** Blend width in px. Larger means a wider, softer neck. */
124
+ readonly neckWidth: number;
125
+ /** Hard cap on how far the union may deviate from `min`, in px. */
126
+ readonly maxBulge: number;
127
+ /** Members whose gap exceeds this never blend, in px. */
128
+ readonly separationThreshold: number;
129
+ }
130
+
131
+ /**
132
+ * The binding driver-per-channel table (§Motion).
133
+ *
134
+ * Every animated channel names exactly one driver family. The table is the
135
+ * contract; the constants that configure each driver live in `tunables.ts` and
136
+ * are advisory until calibration (C7) replaces them.
137
+ */
138
+ /** Animated channels. Each has exactly one driver family (§Motion, binding). */
139
+ declare const MOTION_CHANNELS: readonly ["position", "size", "radius", "pressCompression", "lensStrength", "glow", "backdropAdaptation", "foregroundTone", "materialization", "disabled", "qualityTier"];
140
+ type MotionChannel = (typeof MOTION_CHANNELS)[number];
141
+ /**
142
+ * Driver families. `interruptible-spring` is the velocity-preserving one:
143
+ * a redirect continues from current position and velocity, never restarts.
144
+ */
145
+ declare const MOTION_DRIVER_KINDS: readonly ["interruptible-spring", "critically-damped", "attack-decay", "low-pass-hysteresis", "threshold-crossfade", "monotonic-ease", "step", "hysteresis-cooldown"];
146
+ type MotionDriverKind = (typeof MOTION_DRIVER_KINDS)[number];
147
+ declare const MOTION_DRIVER_BY_CHANNEL: Readonly<Record<MotionChannel, MotionDriverKind>>;
148
+
149
+ /** v1 interaction states (§Motion). Neighbor glow diffusion is post-v1. */
150
+ declare const INTERACTION_STATES: readonly ["idle", "hover", "pressed", "focused", "disabled", "morphing"];
151
+ type InteractionState = (typeof INTERACTION_STATES)[number];
152
+
153
+ /**
154
+ * The diagnostics channel — how core reports authoring problems it detects but
155
+ * must not silently fix.
156
+ *
157
+ * Core is passive and DOM-free (X4), so it neither writes to a console nor
158
+ * decides what a host does with a finding: the host supplies a sink. Findings
159
+ * are deduplicated by default, because several of them are produced by checks
160
+ * that run every frame (same-plane overlap, variant mixing) and a firehose is
161
+ * worse than silence.
162
+ *
163
+ * Structural mistakes — an unknown id, a duplicate id, a still-referenced
164
+ * source — are *not* diagnostics. Those throw `GlassSceneError`, because
165
+ * continuing past them would leave a half-built scene. Diagnostics carry the
166
+ * recoverable, per-frame, policy-level findings instead.
167
+ */
168
+ type DiagnosticSeverity = "warning" | "error";
169
+ /** Everything core can report. Owned here so a host can switch exhaustively. */
170
+ declare const DIAGNOSTIC_CODES: readonly ["same-plane-overlap", "variant-mixing", "merge-distance-below-padding", "group-proxy-overlap", "clear-variant-needs-dimming", "foreground-mode-illegal", "foreground-rate-clamped", "backdrop-hint-out-of-range", "backdrop-hint-redundant-estimator", "reduced-transparency-undetectable", "frame-phase-violation"];
171
+ type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];
172
+ interface Diagnostic {
173
+ readonly code: DiagnosticCode;
174
+ readonly severity: DiagnosticSeverity;
175
+ /**
176
+ * The ids this finding is about — group, node, or source. Together with
177
+ * `code` this is the dedupe key, so "these two nodes overlap" is one finding
178
+ * however many frames it survives.
179
+ */
180
+ readonly subjects: readonly string[];
181
+ readonly message: string;
182
+ }
183
+ type DiagnosticSink = (diagnostic: Diagnostic) => void;
184
+ interface DiagnosticsChannel {
185
+ report(diagnostic: Diagnostic): void;
186
+ /** Findings retained since construction or the last `clear()`. */
187
+ readonly reported: readonly Diagnostic[];
188
+ /** Forget what was seen, so a condition that returns is reported again. */
189
+ clear(): void;
190
+ }
191
+ interface DiagnosticsChannelOptions {
192
+ /** Where findings go. Omitted in tests and in hosts that only read `reported`. */
193
+ readonly sink?: DiagnosticSink;
194
+ /** Collapse repeats of the same code+subjects. Default true. */
195
+ readonly dedupe?: boolean;
196
+ }
197
+ declare function createDiagnosticsChannel(options?: DiagnosticsChannelOptions): DiagnosticsChannel;
198
+
199
+ /**
200
+ * Accessibility policy (§Accessibility policy, plus the Reduced Motion
201
+ * paragraph of §Motion).
202
+ *
203
+ * The spec puts this in `core` as *policy*: what an accessibility preference
204
+ * means for the material and for motion. `platform-web` reads the media queries
205
+ * and hands the answers in as plain booleans (X4 — core never touches a media
206
+ * query, a `window`, or anything else browser-shaped), and `vitrea-react`'s
207
+ * `GlassRoot` supplies the per-root prop overrides. The decision itself is one
208
+ * pure fold, so the same inputs always yield the same policy and a test can
209
+ * enumerate every input there is.
210
+ *
211
+ * §Accessibility states four consequences in prose. They are encoded here as a
212
+ * **table**, one row per preference, each row a partial override of the nominal
213
+ * policy — so the table can be diffed against the spec paragraph line by line
214
+ * instead of being reverse-engineered out of branching code. The resolver folds
215
+ * the active rows over `NOMINAL_ACCESSIBILITY_POLICY` in a fixed precedence
216
+ * order; adding a preference means adding a row, not editing a resolver.
217
+ *
218
+ * Every consequence is a small closed union rather than a number. The numbers —
219
+ * blur radii, tint strengths, spring constants, morph durations — are
220
+ * calibration-delegated unknowns owned by `@vitrea/geometry` and
221
+ * `@vitrea/motion` (§Calibration harness). Core's job is to say *which regime*
222
+ * applies; it would be inventing fidelity it has not measured if it said by how
223
+ * much.
224
+ */
225
+
226
+ /**
227
+ * What the platform detected. `platform-web` fills this from media queries;
228
+ * core never reads one (X4).
229
+ */
230
+ interface SystemAccessibilityPreferences {
231
+ readonly reducedTransparency: boolean;
232
+ readonly reducedMotion: boolean;
233
+ readonly increasedContrast: boolean;
234
+ readonly forcedColors: boolean;
235
+ /**
236
+ * Whether `prefers-reduced-transparency` is even queryable here. It is not
237
+ * Baseline, which is exactly why the explicit override is load-bearing rather
238
+ * than a courtesy (§Accessibility policy). Where this is `false`,
239
+ * `reducedTransparency` above is an absence of evidence, not evidence of
240
+ * absence — and the resolver says so through the diagnostics channel.
241
+ */
242
+ readonly reducedTransparencySupported: boolean;
243
+ }
244
+ /** The preferences the policy reasons about, and the table's row keys. */
245
+ declare const ACCESSIBILITY_FLAGS: readonly ["reducedTransparency", "reducedMotion", "increasedContrast", "forcedColors"];
246
+ type AccessibilityFlag = (typeof ACCESSIBILITY_FLAGS)[number];
247
+ /**
248
+ * The three preferences a `GlassRoot` may overrule (§Accessibility policy names
249
+ * exactly this prop set). `forcedColors` is absent by design: a forced-colors
250
+ * mandate comes from an operating-system accessibility setting that exists to
251
+ * override author styling, so it is not an app's to switch off. The type — not
252
+ * a runtime guard — is what makes that impossible to express.
253
+ */
254
+ type OverridableAccessibilityFlag = Exclude<AccessibilityFlag, "forcedColors">;
255
+ declare const OVERRIDABLE_ACCESSIBILITY_FLAGS: readonly ["reducedTransparency", "reducedMotion", "increasedContrast"];
256
+ /** `"system"` defers to the platform; a boolean states the answer outright. */
257
+ type AccessibilityOverride = "system" | boolean;
258
+ /** Per-`GlassRoot` overrides. An absent key means `"system"`. */
259
+ type AccessibilityOverrides = {
260
+ readonly [K in OverridableAccessibilityFlag]?: AccessibilityOverride;
261
+ };
262
+ /**
263
+ * How the material is allowed to behave.
264
+ *
265
+ * The axes are named for what §Accessibility talks about, and each carries the
266
+ * nominal regime, the regime its preference asks for, and — where forced-colors
267
+ * removes the glass entirely — the degenerate regime that leaves.
268
+ *
269
+ * `refraction` here is an *accessibility* ceiling, not the capability-derived
270
+ * `RefractionQuality` of X2 (state.ts). A group can be capped by either; the
271
+ * renderer honours whichever is lower.
272
+ */
273
+ interface ResolvedMaterialPolicy {
274
+ /** Whether a glass body is drawn at all, or a flat system-coloured surface replaces it. */
275
+ readonly glass: "material" | "none";
276
+ /** Where colour comes from: the adaptive material, or the platform's forced palette. */
277
+ readonly colorSource: "material" | "system";
278
+ /** Backdrop diffusion. Reduced transparency asks for *more* frosted, so `increased`. */
279
+ readonly frost: "nominal" | "increased" | "none";
280
+ /** Edge lensing. Reduced transparency asks for *less* refraction — reduced, not removed. */
281
+ readonly refraction: "nominal" | "reduced" | "none";
282
+ /**
283
+ * How much of the backdrop the surface hides. Reduced transparency raises it;
284
+ * forced-colors' flat system fill hides the backdrop completely, which is
285
+ * `opaque` rather than `none` — no glass means maximal occlusion, not minimal.
286
+ */
287
+ readonly occlusion: "nominal" | "increased" | "opaque";
288
+ /**
289
+ * Nominal glass is bounded by a rim highlight; `strong` is a drawn border.
290
+ * Increased contrast asks for stronger borders, and forced-colors needs a
291
+ * border because a flattened palette leaves nothing else to define the shape.
292
+ */
293
+ readonly border: "nominal" | "strong";
294
+ /** The material's colour cast picked up from the backdrop. */
295
+ readonly ambientTint: "nominal" | "reduced" | "none";
296
+ /**
297
+ * `adaptive` is the sampled/hinted light-dark foreground of §Foreground
298
+ * adaptation. `near-monochrome` is a flat high-contrast foreground — which is
299
+ * also what forced-colors gets, since adapting to a glass body that no longer
300
+ * exists is not available there; `colorSource` records that the flat colour
301
+ * comes from the system palette.
302
+ */
303
+ readonly foreground: "adaptive" | "near-monochrome";
304
+ }
305
+ /**
306
+ * How motion is allowed to behave (§Motion, Reduced Motion).
307
+ *
308
+ * Reduced Motion "shortens morphs to non-elastic interpolation": the shortening
309
+ * is a property of the driver kind, so `morph: "non-elastic"` carries both
310
+ * halves and `@vitrea/motion` owns the calibrated duration for each kind.
311
+ */
312
+ interface ResolvedMotionPolicy {
313
+ /** Spring overshoot past the target. */
314
+ readonly overshoot: "elastic" | "none";
315
+ /** Press compression and lensing deformation of the surface. */
316
+ readonly deformation: "nominal" | "none";
317
+ /** Travelling specular shimmer across the rim. */
318
+ readonly shimmer: "travel" | "none";
319
+ /** Morph driver family: an elastic spring, or plain shortened interpolation. */
320
+ readonly morph: "elastic" | "non-elastic";
321
+ /**
322
+ * Crossfading one *surface transition* into another. Nominal glass never
323
+ * does it — a morph is one continuous material transition, not two surfaces
324
+ * dissolving. Reduced Motion reserves it for large plane shifts. (The
325
+ * foreground light/dark crossfade of §Motion's driver table is a different,
326
+ * unconditional channel and is not governed here.)
327
+ */
328
+ readonly crossfade: "never" | "large-plane-shifts";
329
+ /**
330
+ * Reduced Motion "keeps direct-manipulation positional continuity": a surface
331
+ * being dragged still tracks the pointer. That holds under every preference,
332
+ * so it is an invariant of the model, typed as the literal `true` — there is
333
+ * no combination of inputs that can turn it off.
334
+ */
335
+ readonly positionalContinuity: true;
336
+ }
337
+ /**
338
+ * What renderers consume. The four resolved booleans travel with the
339
+ * consequences so a consumer (or `useGlassCapabilities()`) can see *what* was
340
+ * decided and *why*, in the same spirit as X2's `configuredSource` surviving a
341
+ * demotion.
342
+ */
343
+ interface ResolvedAccessibilityPolicy {
344
+ readonly reducedTransparency: boolean;
345
+ readonly reducedMotion: boolean;
346
+ readonly increasedContrast: boolean;
347
+ readonly forcedColors: boolean;
348
+ readonly material: ResolvedMaterialPolicy;
349
+ readonly motion: ResolvedMotionPolicy;
350
+ }
351
+ /**
352
+ * One table row: the axes a preference changes, and nothing else.
353
+ *
354
+ * `positionalContinuity` is excluded from the motion partial so no row can
355
+ * reach the invariant, whatever a future editor intends.
356
+ */
357
+ interface AccessibilityConsequences {
358
+ readonly material?: Partial<ResolvedMaterialPolicy>;
359
+ readonly motion?: Partial<Omit<ResolvedMotionPolicy, "positionalContinuity">>;
360
+ }
361
+ /** Full-fidelity glass: no preference detected, nothing capped. */
362
+ declare const NOMINAL_ACCESSIBILITY_POLICY: ResolvedAccessibilityPolicy;
363
+ /**
364
+ * §Accessibility, encoded. Each row is that paragraph's sentence for one
365
+ * preference and carries only the axes the sentence names:
366
+ *
367
+ * - "Reduced transparency → more frosted, less refraction, higher occlusion."
368
+ * - "Increased contrast → stronger borders, near-monochrome foregrounds,
369
+ * reduced ambient tint."
370
+ * - "`forced-colors` → system colors, borders, no glass."
371
+ * - "Reduced Motion removes elastic overshoot, deformation, and shimmer travel;
372
+ * keeps direct-manipulation positional continuity; shortens morphs to
373
+ * non-elastic interpolation; reserves crossfade for large plane shifts."
374
+ * (§Motion)
375
+ *
376
+ * Reduced Motion is the only motion row, and the other three are the only
377
+ * material rows — the spec draws no line between a colour preference and a
378
+ * motion one, so neither does the table.
379
+ */
380
+ declare const ACCESSIBILITY_BEHAVIOR_TABLE: {
381
+ readonly [K in AccessibilityFlag]: AccessibilityConsequences;
382
+ };
383
+ /**
384
+ * Fold order, weakest first.
385
+ *
386
+ * `reducedMotion` touches only motion axes, and `reducedTransparency` and
387
+ * `increasedContrast` touch disjoint material axes — those three commute, so
388
+ * two preferences at once compose instead of one silently erasing the other.
389
+ * `forcedColors` is the one row that spans every material axis, and it is last
390
+ * because a platform-level colour mandate outranks every softening an app or a
391
+ * softer preference asked for.
392
+ */
393
+ declare const ACCESSIBILITY_PRECEDENCE: readonly ["reducedMotion", "reducedTransparency", "increasedContrast", "forcedColors"];
394
+ /**
395
+ * Resolve one `GlassRoot`'s accessibility policy.
396
+ *
397
+ * Purely functional: no state, no I/O, no clock. `diagnostics` is a side
398
+ * channel — passing one never changes the policy that comes back.
399
+ */
400
+ declare function resolveAccessibilityPolicy(system: SystemAccessibilityPreferences, overrides?: AccessibilityOverrides, diagnostics?: DiagnosticsChannel): ResolvedAccessibilityPolicy;
401
+
402
+ /**
403
+ * X2 — the resolved-state model (§Backdrop & analysis contracts).
404
+ *
405
+ * Configuration and resolution are separate: an app *configures* a source, the
406
+ * runtime *resolves* one of an enumerated set of states. Only enumerated states
407
+ * are legal, every demotion names its reason, and `configuredSource` survives
408
+ * demotion so an app can always see what it asked for versus what it got.
409
+ *
410
+ * C1 ships the shape; C4 ships the resolver.
411
+ */
412
+ type ConfiguredSource = "texture" | "dom";
413
+ type ActiveRenderer = "webgpu" | "css";
414
+ type SamplingBackend = "gpu-texture" | "css-backdrop" | "none";
415
+ type RefractionQuality$1 = "true" | "approximate" | "none";
416
+ type AnalysisQuality = "exact" | "hint" | "none";
417
+ type GroupHealth = "ok" | "demoted";
418
+ declare const DEMOTION_REASONS: readonly ["no-webgpu", "no-backdrop-filter", "tainted-source", "incompatible-texture", "no-texture-supplied", "device-lost", "probe-failed", "governor"];
419
+ type DemotionReason = (typeof DEMOTION_REASONS)[number];
420
+ interface GlassGroupState {
421
+ /** What the app declared — never mutated by the runtime. */
422
+ readonly configuredSource: ConfiguredSource;
423
+ /** What is actually drawing. */
424
+ readonly activeRenderer: ActiveRenderer;
425
+ readonly samplingBackend: SamplingBackend;
426
+ readonly refraction: RefractionQuality$1;
427
+ readonly analysis: AnalysisQuality;
428
+ readonly health: GroupHealth;
429
+ readonly demotionReason?: DemotionReason;
430
+ }
431
+ declare function isHealthy(state: GlassGroupState): boolean;
432
+
433
+ /**
434
+ * X2 — the capability state machine (§Backdrop & analysis contracts, the
435
+ * honesty core).
436
+ *
437
+ * Capability is not a free tuple. The app *configures* a source; the runtime
438
+ * *resolves* one of an enumerated set of states from probe results that arrive
439
+ * as plain data. Nothing here touches a GPU, a browser or a clock: platform-web
440
+ * probes, C6 renders, this file only decides.
441
+ *
442
+ * ## Resolution rules
443
+ *
444
+ * 0. **WebGPU not requested is not a fault, and neither is WebGPU not ready
445
+ * yet.** A root configured for the CSS tier never had WebGPU in play;
446
+ * `platform.webgpu: "not-requested"` forces `activeRenderer: "css"` the same
447
+ * way a renderer fault would, but names no fault, so a group with nothing
448
+ * else wrong resolves `health: "ok"` — labeling intent as a fault would
449
+ * invert the honesty doctrine (X2's K1 amendment, Decision Log #21c).
450
+ * `"pending"` resolves identically, for the same reason read forwards: a root
451
+ * that asked for WebGPU and is still bringing it up has not failed at
452
+ * anything, and answering `no-webgpu` — whose recovery is honestly `"none"` —
453
+ * would be a terminal answer to a request still in flight.
454
+ * 1. **Renderer faults** — no WebGPU, a lost device, or a governor tier switch —
455
+ * drop `activeRenderer` to `"css"`. Nothing else can.
456
+ * 2. **Sampling faults** demote the backdrop path without touching the
457
+ * renderer. A tainted or incompatible texture leaves WebGPU drawing tint,
458
+ * rim and glow with nothing to sample (`samplingBackend: "none"`); it does
459
+ * *not* silently re-point the group at the DOM behind it, because the app
460
+ * asked for a texture and swapping backdrops underneath it would be exactly
461
+ * the pretence this model exists to prevent.
462
+ * 3. **Refraction follows what is actually sampled.** `"true"` needs GPU-texture
463
+ * sampling; `"approximate"` is the shader's rim-lensing over a CSS proxy;
464
+ * the CSS tier gets `"none"` because `backdrop-filter` blurs, it does not
465
+ * bend.
466
+ * 4. **`exact` analysis needs a GPU texture.** Otherwise a declared hint or an
467
+ * estimator provider yields `"hint"` (X6), and nothing yields `"none"`.
468
+ * 5. **One reason is reported, by precedence** — see `REASON_PRECEDENCE`.
469
+ * 6. **A texture source with no pixels behind it is a sampling fault.** The app
470
+ * declared a texture and the runtime has nothing to read, so the group draws
471
+ * tint, rim and glow over an unsampled backdrop and says so
472
+ * (`no-texture-supplied`). Reporting `gpu-texture` / `true` / `exact` for a
473
+ * source nobody supplied is the loudest possible version of the pretence this
474
+ * file exists to prevent.
475
+ */
476
+
477
+ /**
478
+ * Whether WebGPU is in play for this root at all, distinct from whether it
479
+ * *works* — a root configured for the CSS tier never asks, and that is a
480
+ * choice, not a fault (X2's K1 amendment, Decision Log #21c).
481
+ *
482
+ * The question is three-state on the way up, not two: between "asked for" and
483
+ * "answered" there is a startup window in which the answer is not known yet, and
484
+ * a host that has to publish *something* during it has only honest options if
485
+ * the union has a name for it. `"pending"` is that name.
486
+ */
487
+ declare const WEBGPU_AVAILABILITIES: readonly ["not-requested", "pending", "unavailable", "available"];
488
+ type WebGPUAvailability$1 = (typeof WEBGPU_AVAILABILITIES)[number];
489
+ /** Platform-wide probe results. platform-web produces these; core only reads them. */
490
+ interface PlatformProbe {
491
+ /**
492
+ * `"available"` — a WebGPU adapter and device were obtained *and* whatever
493
+ * draws with them is ready to paint. `"unavailable"` — WebGPU was requested
494
+ * but no adapter, device or renderer could be had. `"pending"` — requested,
495
+ * and the answer has not arrived yet; resolves exactly as `"not-requested"`
496
+ * does, so a group is on the CSS tier without being demoted while the GPU tier
497
+ * starts. `"not-requested"` — this root never asked for WebGPU at all (its
498
+ * renderer is CSS by choice), which resolves honestly rather than as a fault.
499
+ *
500
+ * The two CSS-without-a-fault values resolve alike and stay tellable apart on
501
+ * purpose: a host reads them back to distinguish "CSS by choice" from "CSS
502
+ * while WebGPU starts", which is the difference between a final answer and a
503
+ * provisional one.
504
+ */
505
+ readonly webgpu: WebGPUAvailability$1;
506
+ /** `backdrop-filter` is supported and actually filters. */
507
+ readonly backdropFilter: boolean;
508
+ /**
509
+ * S1's startup conformance probe: does a portaled masked proxy sample the
510
+ * same pixels as an in-place `backdrop-filter`? Filter Effects 2's Backdrop
511
+ * Root lacks WG consensus, so this is probed, never assumed.
512
+ */
513
+ readonly backdropProxyConformance: "pass" | "fail";
514
+ readonly deviceHealth: "ok" | "lost";
515
+ }
516
+ /** Per-source facts. Meaningful for `texture` sources only. */
517
+ interface SourceProbe {
518
+ /** CORS taint. A tainted source cannot be read into a GPU texture at all. */
519
+ readonly taint: "clean" | "tainted";
520
+ /** Whether an app-supplied view satisfies the declared usage/format/dimension requirements. */
521
+ readonly textureCompatibility: "compatible" | "incompatible";
522
+ /**
523
+ * Whether pixels have actually been handed over for this source.
524
+ *
525
+ * `TextureBackdropSource` declares that a source *is* a texture and carries no
526
+ * pixels — core may not know what an `HTMLCanvasElement` is (X4) — so the
527
+ * declaration and the supply are two separate events, and a group can sit
528
+ * between them for as long as the app takes. Only the platform layer knows
529
+ * which side of that gap a source is on, so it folds the fact in here, exactly
530
+ * as it folds a per-group proxy verdict into `backdropProxyConformance`
531
+ * (Decision Log #21a).
532
+ *
533
+ * Optional, defaulting to `"supplied"`: core cannot see the pixels either way,
534
+ * and a resolver that assumed absence would demote every source registered
535
+ * through a host that does not report this at all.
536
+ */
537
+ readonly supply?: "supplied" | "absent";
538
+ }
539
+ /**
540
+ * Quality-governor pressure (§Performance envelope). The governor degrades
541
+ * *within* a tier before it switches tiers, so only `"demote-tier"` is a
542
+ * demotion; `"degrade-in-tier"` changes render quality knobs C6 owns and leaves
543
+ * the resolved state untouched.
544
+ */
545
+ declare const GOVERNOR_PRESSURES: readonly ["none", "degrade-in-tier", "demote-tier"];
546
+ type GovernorPressure = (typeof GOVERNOR_PRESSURES)[number];
547
+ /**
548
+ * X6 — which of the one hint mechanism is in play. Both an author-declared
549
+ * `backdrop` prop and an estimator provider produce `analysis: "hint"`; the
550
+ * distinction is kept for developer-facing reporting, never smuggled into the
551
+ * state, whose shape is frozen.
552
+ */
553
+ declare const HINT_AVAILABILITIES: readonly ["none", "author-hint", "estimator"];
554
+ type HintAvailability = (typeof HINT_AVAILABILITIES)[number];
555
+ /** Everything the resolver needs. A texture group must declare its source probe. */
556
+ type CapabilityInputs = {
557
+ readonly configuredSource: "texture";
558
+ readonly platform: PlatformProbe;
559
+ readonly source: SourceProbe;
560
+ readonly governor: GovernorPressure;
561
+ readonly hint: HintAvailability;
562
+ } | {
563
+ readonly configuredSource: "dom";
564
+ readonly platform: PlatformProbe;
565
+ readonly source?: undefined;
566
+ readonly governor: GovernorPressure;
567
+ readonly hint: HintAvailability;
568
+ };
569
+ /** What clears a demotion. `"none"` is the honest answer for a platform fact. */
570
+ type RecoveryTrigger = "device-restored" | "source-replaced" | "probe-repassed" | "pressure-released" | "none";
571
+ interface RecoveryContract {
572
+ readonly trigger: RecoveryTrigger;
573
+ readonly explanation: string;
574
+ }
575
+ /**
576
+ * Every demotion names its recovery transition (§honesty core). This table is
577
+ * the contract; the resolver is stateless, so recovery *is* re-resolution once
578
+ * the named input changes.
579
+ */
580
+ declare const DEMOTION_RECOVERY: Readonly<Record<DemotionReason, RecoveryContract>>;
581
+ /**
582
+ * Resolve one group's state. Pure, total, and deterministic: the same inputs
583
+ * always produce the same state, and `configuredSource` is copied through
584
+ * untouched by every path.
585
+ */
586
+ declare function resolveGlassGroupState(inputs: CapabilityInputs): GlassGroupState;
587
+ /** How one group's state moved between two resolutions. */
588
+ type StateChange = {
589
+ readonly kind: "unchanged";
590
+ }
591
+ /** The group's first resolution — there was no previous state to move from. */
592
+ | {
593
+ readonly kind: "initial";
594
+ readonly reason?: DemotionReason;
595
+ } | {
596
+ readonly kind: "demoted";
597
+ readonly reason: DemotionReason;
598
+ } | {
599
+ readonly kind: "recovered";
600
+ readonly from: DemotionReason;
601
+ } | {
602
+ readonly kind: "changed";
603
+ readonly reason?: DemotionReason;
604
+ };
605
+ /**
606
+ * Name a transition, so a host can log "recovered from device-lost" rather than
607
+ * diff two records. Used by the scene when it re-resolves during a frame. A
608
+ * missing `previous` is a group's first resolution, not a change.
609
+ */
610
+ declare function classifyStateChange(previous: GlassGroupState | undefined, next: GlassGroupState): StateChange;
611
+
612
+ /**
613
+ * X6 — the hint contract (§Backdrop & analysis contracts).
614
+ *
615
+ * A `dom` group has no pixels vitrea may read, so its adaptation data comes
616
+ * from exactly one mechanism: an author-declared `backdrop` on the GlassGroup,
617
+ * or an estimator provider that supplies the same shape. Three parallel hint
618
+ * mechanisms were consolidated into this one on purpose (Decision Log #13).
619
+ *
620
+ * The word "estimator" is load-bearing. A built-in DOM estimator may read known
621
+ * background colours and images where CORS permits, and it is documented as an
622
+ * estimator everywhere it appears — never as pixel analysis, which vitrea does
623
+ * not promise for arbitrary DOM and never will.
624
+ */
625
+
626
+ /** Coarse light/dark/mixed classification. The one field a hint must carry. */
627
+ type BackdropTone = "light" | "dark" | "mixed";
628
+ interface BackdropHint {
629
+ readonly tone: BackdropTone;
630
+ /** Relative luminance of the backdrop under the group, 0..1. */
631
+ readonly luminance?: number;
632
+ /** Busyness of the backdrop, 0..1 — how hard the foreground has to fight. */
633
+ readonly complexity?: number;
634
+ }
635
+ /**
636
+ * The provider form of the same contract. `kind` is a literal so the shape is
637
+ * self-describing wherever it surfaces in developer tooling.
638
+ */
639
+ interface BackdropEstimatorProvider {
640
+ readonly kind: "estimator";
641
+ /** Names the estimator in diagnostics and developer output. */
642
+ readonly id: string;
643
+ /** Called per group. Returning `undefined` means "no opinion here". */
644
+ estimate(groupId: string): BackdropHint | undefined;
645
+ }
646
+ interface BackdropHintRequest {
647
+ readonly groupId: string;
648
+ readonly backdrop?: BackdropHint;
649
+ readonly estimator?: BackdropEstimatorProvider;
650
+ readonly diagnostics?: DiagnosticsChannel;
651
+ }
652
+ interface ResolvedBackdropHint {
653
+ readonly availability: HintAvailability;
654
+ /** Absent exactly when `availability` is `"none"`. */
655
+ readonly hint?: BackdropHint;
656
+ }
657
+ /**
658
+ * Resolve a group's hint. An explicit `backdrop` beats an estimator: the author
659
+ * stated a fact, the estimator guessed one. Configuring both is reported once,
660
+ * because it usually means the author forgot they had a provider installed.
661
+ */
662
+ declare function resolveBackdropHint(request: BackdropHintRequest): ResolvedBackdropHint;
663
+
664
+ /**
665
+ * §Foreground adaptation — the GPU → DOM data path's policy half.
666
+ *
667
+ * `ForegroundAdaptation = fixed | author-hint | sampled-async { rateHz,
668
+ * hysteresis }`. The interesting part is legality: `sampled-async` needs GPU
669
+ * classification results, so it is available only where `analysis: "exact"` —
670
+ * "exactly", per the honesty doctrine. An illegal combination is neither
671
+ * honoured nor silently dropped: it resolves to the nearest legal mode and the
672
+ * downgrade is both returned and reported.
673
+ *
674
+ * Nothing here reads a clock or schedules anything. `rateHz` is a declared
675
+ * cadence platform-web drives; core only validates it.
676
+ */
677
+
678
+ /** The three modes, ordered most adaptive first — the order the fallback walks. */
679
+ declare const FOREGROUND_MODES: readonly ["sampled-async", "author-hint", "fixed"];
680
+ type ForegroundMode = (typeof FOREGROUND_MODES)[number];
681
+ type ForegroundAdaptation = {
682
+ readonly mode: "fixed";
683
+ } | {
684
+ readonly mode: "author-hint";
685
+ } | {
686
+ readonly mode: "sampled-async";
687
+ /** Readback cadence. Low-frequency by contract — never per-frame. */
688
+ readonly rateHz: number;
689
+ /** Threshold band, 0..1, that keeps scrolling from pumping the foreground. */
690
+ readonly hysteresis: number;
691
+ };
692
+ /**
693
+ * Advisory bounds. `maxHz` exists to keep "low-frequency async readback" true
694
+ * rather than aspirational: a quarter of a 60Hz frame budget is already far
695
+ * more often than a foreground tone needs to change. Calibration (C7) may
696
+ * replace these numbers; the invariant that readback is not per-frame is not
697
+ * negotiable.
698
+ */
699
+ declare const SAMPLED_ASYNC_RATE_LIMITS: {
700
+ readonly minHz: 1;
701
+ readonly maxHz: 15;
702
+ readonly minHysteresis: 0.02;
703
+ readonly maxHysteresis: 0.5;
704
+ };
705
+ /** Advisory defaults, replaced by calibration profiles (§Calibration). */
706
+ declare const SAMPLED_ASYNC_DEFAULTS: {
707
+ readonly rateHz: 4;
708
+ readonly hysteresis: 0.06;
709
+ };
710
+ interface ForegroundResolutionOptions {
711
+ /**
712
+ * What a finding is about — a group id, or a node id where a surface
713
+ * overrides its group's mode. Also the dedupe subject, so a standing
714
+ * downgrade is reported once rather than once per frame.
715
+ */
716
+ readonly subject?: string;
717
+ readonly diagnostics?: DiagnosticsChannel;
718
+ }
719
+ interface ResolvedForegroundAdaptation {
720
+ readonly adaptation: ForegroundAdaptation;
721
+ /** Present only when the requested mode was not legal for this state. */
722
+ readonly downgraded?: {
723
+ readonly from: ForegroundMode;
724
+ readonly to: ForegroundMode;
725
+ };
726
+ }
727
+ /**
728
+ * Resolve a requested mode against a resolved group state. Total: every
729
+ * request produces a legal adaptation, because `fixed` is legal everywhere.
730
+ */
731
+ declare function resolveForegroundAdaptation(requested: ForegroundAdaptation, state: GlassGroupState, options?: ForegroundResolutionOptions): ResolvedForegroundAdaptation;
732
+ /**
733
+ * The mode a group gets when the app states none: adapt wherever that is
734
+ * honest, hint where the author supplied one, fixed tones for arbitrary DOM.
735
+ */
736
+ declare function defaultForegroundAdaptation(state: GlassGroupState): ForegroundAdaptation;
737
+
738
+ /**
739
+ * The frame vocabulary, shared by the scene and the scheduler.
740
+ *
741
+ * It lives in its own module because both need it: the scheduler drives the
742
+ * phases, and the scene uses the current phase to gate the operations that only
743
+ * make sense in one of them.
744
+ *
745
+ * The five phases are the pipeline the spec describes: collect what changed,
746
+ * read the DOM once in a batch, update the scene from those reads, write the GPU
747
+ * resources, then render. Their point is ordering discipline — every layout read
748
+ * in one place so the steady state performs none, and every scene mutation
749
+ * finished before the renderer starts walking the graph.
750
+ */
751
+ declare const FRAME_PHASES: readonly ["collect", "read", "update", "write", "render"];
752
+ type FramePhase = (typeof FRAME_PHASES)[number];
753
+ /**
754
+ * What a host knows about the frame it is driving. `timeMs` arrives as a number
755
+ * because core reads no clock (X4) — it is the host's monotonic timestamp.
756
+ */
757
+ interface FrameInfo {
758
+ /** Monotonically increasing. Also the scope for at-most-once-per-frame work. */
759
+ readonly id: number;
760
+ readonly timeMs: number;
761
+ }
762
+
763
+ /**
764
+ * §Material variants.
765
+ *
766
+ * `regular` is the adaptive default. `clear` is persistently more transparent
767
+ * with constrained adaptation, and it *requires* a dimming policy — without one
768
+ * there is nothing keeping foreground content legible over a busy backdrop.
769
+ *
770
+ * Two deliberate choices here:
771
+ *
772
+ * - A `clear` surface with no dimming policy resolves to `regular` and raises a
773
+ * dev-mode **error**. Applying an invented policy silently would be the one
774
+ * thing this codebase refuses: pretending to a capability the author never
775
+ * configured. Falling back to `regular` cannot produce an illegible surface,
776
+ * and `DEFAULT_CLEAR_DIMMING` makes satisfying the requirement a one-liner.
777
+ * - Mixing variants inside one GlassGroup **warns** and changes nothing. Apple's
778
+ * guidance is not to mix; coercing one of the two would silently discard an
779
+ * author's intent, which is worse than a surface that looks wrong on purpose.
780
+ */
781
+
782
+ declare const MATERIAL_VARIANTS$1: readonly ["regular", "clear"];
783
+ type MaterialVariant$1 = (typeof MATERIAL_VARIANTS$1)[number];
784
+ /** The scrim laid beneath clear glass so foreground content stays legible. */
785
+ interface DimmingPolicy {
786
+ /** Scrim opacity, 0..1. */
787
+ readonly scrim: number;
788
+ /** Which way the scrim pushes the backdrop — dark content over light, or the reverse. */
789
+ readonly direction: "darken" | "lighten";
790
+ }
791
+ /**
792
+ * Advisory default, named as a delegated unknown in §Calibration: the harness
793
+ * fits the real value. Its purpose here is to make the requirement cheap to
794
+ * satisfy, not to be correct.
795
+ */
796
+ declare const DEFAULT_CLEAR_DIMMING: DimmingPolicy;
797
+ /** A group's material defaults. A node inherits `variant` when it declares none. */
798
+ interface MaterialProfile$1 {
799
+ readonly variant: MaterialVariant$1;
800
+ /** Required for any clear surface in the group. */
801
+ readonly dimming?: DimmingPolicy;
802
+ }
803
+ interface ResolvedMaterial {
804
+ readonly variant: MaterialVariant$1;
805
+ /** Regular adapts to the backdrop; clear's response is deliberately constrained. */
806
+ readonly adaptation: "adaptive" | "constrained";
807
+ /** Present exactly when `variant` is `"clear"`. */
808
+ readonly dimming?: DimmingPolicy;
809
+ }
810
+ interface MaterialRequest {
811
+ readonly variant: MaterialVariant$1;
812
+ readonly dimming?: DimmingPolicy;
813
+ /** Named in diagnostics; also the dedupe subject. */
814
+ readonly nodeId?: string;
815
+ readonly diagnostics?: DiagnosticsChannel;
816
+ }
817
+ declare function resolveMaterial(request: MaterialRequest): ResolvedMaterial;
818
+ interface VariantMixingCheck {
819
+ readonly groupId: string;
820
+ readonly members: readonly {
821
+ readonly nodeId: string;
822
+ readonly variant: MaterialVariant$1;
823
+ }[];
824
+ readonly diagnostics?: DiagnosticsChannel;
825
+ }
826
+ /**
827
+ * Report a group whose members do not agree on a variant. Returns whether they
828
+ * were mixed; changes nothing either way.
829
+ */
830
+ declare function checkVariantMixing(check: VariantMixingCheck): boolean;
831
+
832
+ /**
833
+ * The z-slot model (§Core model: `z-slot (plane + order)`) and the geometry
834
+ * primitives core needs to reason about it.
835
+ *
836
+ * Plane identity lives here rather than in platform-web because it is part of
837
+ * the scene model: a GlassNode's slot is (plane, order), and the same-plane
838
+ * overlap check is a core invariant. platform-web owns the *DOM realisation* of
839
+ * a plane — the canvas pair and the paint order inside it (X1) — and re-exports
840
+ * these names so there is one vocabulary.
841
+ *
842
+ * `Rect` is plain data. The numbers arrive from platform-web's batched layout
843
+ * read; core never measures anything (X4).
844
+ */
845
+ /** v1 ships exactly two managed stacking planes (X1). More overlays are out of scope. */
846
+ declare const GLASS_PLANES: readonly ["base", "overlay"];
847
+ type GlassPlane = (typeof GLASS_PLANES)[number];
848
+ /**
849
+ * Where a node sits. `plane` picks the canvas pair; `order` sequences nodes
850
+ * within it. Nodes in one plane must not overlap (X1) — order breaks ties for
851
+ * paint sequence, it does not license stacking.
852
+ */
853
+ interface ZSlot {
854
+ readonly plane: GlassPlane;
855
+ readonly order: number;
856
+ }
857
+ /** Viewport-space rectangle in CSS px, measured by platform-web and handed in as data. */
858
+ interface Rect {
859
+ readonly x: number;
860
+ readonly y: number;
861
+ readonly width: number;
862
+ readonly height: number;
863
+ }
864
+ /** Back-to-front ordering: every base node before every overlay node, then by `order`. */
865
+ declare function compareZSlot(a: ZSlot, b: ZSlot): number;
866
+ /** Smallest rect containing both. */
867
+ declare function unionRect(a: Rect, b: Rect): Rect;
868
+ /** Grow a rect outwards on every side — how a group's proxy gets its padding. */
869
+ declare function inflateRect(rect: Rect, by: number): Rect;
870
+ /**
871
+ * Positive-area intersection. Touching edges are not an overlap — adjacent
872
+ * surfaces in a toolbar are the common case and are legal. A degenerate rect
873
+ * (an unmeasured or collapsed host) overlaps nothing.
874
+ */
875
+ declare function rectsOverlap(a: Rect, b: Rect): boolean;
876
+
877
+ /**
878
+ * The scene model (§Core model): the three registries and everything that
879
+ * follows from their references.
880
+ *
881
+ * ```
882
+ * BackdropSource GlassGroup GlassNode
883
+ * ├─ kind: texture | dom ├─ backdropSourceId ├─ shape family params
884
+ * ├─ raw texture/source ├─ morph namespace ├─ viewport bounds, clip
885
+ * ├─ blur pyramid ├─ material profile ├─ z-slot (plane + order)
886
+ * ├─ analysis maps ├─ adaptation policy ├─ variant
887
+ * ├─ dirty epoch ├─ mergeDistance ├─ interaction state
888
+ * └─ resolution policy └─ samplingPadding └─ foreground policy
889
+ * ```
890
+ *
891
+ * Core owns the *bookkeeping* half of each row. The raw texture, the blur
892
+ * pyramid and the analysis maps are GPU objects C6 owns; what lives here is the
893
+ * fact that they belong to the **source** and not to the group, plus the
894
+ * dirty-epoch accounting that makes the invariant enforceable: a dirty source
895
+ * yields **at most one rebuild per frame**, serving every group that samples it.
896
+ * That hand-out is provisional for as long as the frame is — a frame that throws
897
+ * gives its claims back (`rollbackDirtyBackdropSources`), because a spent claim
898
+ * with nothing built behind it would leave the source clean forever.
899
+ *
900
+ * Two failure modes are kept deliberately distinct:
901
+ *
902
+ * - **Structural** mistakes throw `GlassSceneError` — a duplicate id, a dangling
903
+ * reference, removing something still in use. Continuing past one leaves a
904
+ * scene whose references lie, so there is nothing to report and recover from.
905
+ * - **Policy** findings go to the diagnostics channel — overlap, variant mixing,
906
+ * an illegal foreground mode. Those are recoverable, often per-frame, and the
907
+ * host decides what to do about them.
908
+ *
909
+ * Nothing here measures, schedules, or draws. Viewport rects arrive as data from
910
+ * platform-web's batched read; frames are driven from outside (see
911
+ * `scheduler.ts`).
912
+ */
913
+
914
+ type GlassSceneErrorCode = "duplicate-id" | "unknown-id" | "in-use" | "wrong-source-kind";
915
+ /** A structural violation. Distinct from a diagnostic: the scene refuses the change. */
916
+ declare class GlassSceneError extends Error {
917
+ readonly code: GlassSceneErrorCode;
918
+ constructor(code: GlassSceneErrorCode, message: string);
919
+ }
920
+ /**
921
+ * How large the derived pyramids for a texture source are. Effect-texture
922
+ * resolution is decoupled from DOM DPR on purpose (§Performance envelope).
923
+ */
924
+ interface BackdropResolutionPolicy {
925
+ /** Effect-texture scale relative to CSS px. */
926
+ readonly scale: number;
927
+ /** Cap on a pyramid level's longest side, in texture px. */
928
+ readonly maxDimension: number;
929
+ }
930
+ /** Advisory default; the governor and calibration both move it. */
931
+ declare const DEFAULT_BACKDROP_RESOLUTION: BackdropResolutionPolicy;
932
+ /** How far a group samples beyond its member union, and how near members merge. */
933
+ interface GroupSamplingGeometry {
934
+ /** Padding on the proxy's border box, in CSS px. */
935
+ readonly samplingPadding: number;
936
+ /** Proximity-union threshold within the group, in CSS px. */
937
+ readonly mergeDistance: number;
938
+ }
939
+ /**
940
+ * Advisory defaults, chosen so X1's constraints hold out of the box rather than
941
+ * being numbers worth trusting.
942
+ *
943
+ * S1 measured that `samplingPadding` must be at least 3σ of the group's blur:
944
+ * Filter Effects 2 clips a filter's input to the filtered element's own border
945
+ * box, so an unpadded box starves its own blur at the edges. 24 is 3σ at
946
+ * σ = 8 CSS px, a plausible regular-material blur — calibration (C7) replaces
947
+ * it, and the *actual* 3σ check cannot live here because core carries no blur
948
+ * radius (see the note on `GlassGroupDescriptor.samplingPadding`).
949
+ *
950
+ * `mergeDistance` defaults to the same number because X1 requires
951
+ * `mergeDistance ≥ samplingPadding`: any two members close enough for their
952
+ * padded proxies to overlap must already have merged into one, or the filter
953
+ * applies twice — measured at 1.5625× for `brightness(1.25)`, paint-order
954
+ * dependent, drifting up to 17/255 even in legal 8px-gap geometry.
955
+ */
956
+ declare const DEFAULT_GROUP_SAMPLING: GroupSamplingGeometry;
957
+ interface TextureBackdropSource {
958
+ readonly id: string;
959
+ readonly kind: "texture";
960
+ /**
961
+ * Probed *before* registration. Reporting `analysis: "exact"` for a frame and
962
+ * then withdrawing it would be exactly the pretence X2 exists to prevent.
963
+ */
964
+ readonly probe: SourceProbe;
965
+ readonly resolution?: BackdropResolutionPolicy;
966
+ }
967
+ interface DomBackdropSource {
968
+ readonly id: string;
969
+ readonly kind: "dom";
970
+ }
971
+ type BackdropSourceDescriptor = TextureBackdropSource | DomBackdropSource;
972
+ interface BackdropSourceRecord {
973
+ readonly descriptor: BackdropSourceDescriptor;
974
+ /** Bumped whenever the source's content changes. */
975
+ readonly dirtyEpoch: number;
976
+ /** The epoch a rebuild was last handed out for. Dirty means `dirtyEpoch > builtEpoch`. */
977
+ readonly builtEpoch: number;
978
+ }
979
+ interface GlassGroupDescriptor {
980
+ readonly id: string;
981
+ readonly backdropSourceId: string;
982
+ /** Scope for matched-geometry ids, so two morph pairs cannot collide. */
983
+ readonly morphNamespace?: string;
984
+ readonly material?: MaterialProfile$1;
985
+ /** Requested adaptation. Resolved against the group's state, never assumed legal. */
986
+ readonly foreground?: ForegroundAdaptation;
987
+ /**
988
+ * Proximity-union threshold within the group, in CSS px (§Geometry). X1
989
+ * requires it to be at least `samplingPadding`; core checks that, since both
990
+ * numbers live here.
991
+ */
992
+ readonly mergeDistance?: number;
993
+ /**
994
+ * Padding on the group proxy's border box, in CSS px. X1 also requires it to
995
+ * be at least 3σ of the group's blur — core cannot check *that* one, because
996
+ * it carries no blur radius; platform-web owns it, where the material's blur
997
+ * is known.
998
+ */
999
+ readonly samplingPadding?: number;
1000
+ /** X6: the author's declared hint. */
1001
+ readonly backdrop?: BackdropHint;
1002
+ /** X6: the provider form of the same contract. */
1003
+ readonly estimator?: BackdropEstimatorProvider;
1004
+ }
1005
+ interface GlassGroupRecord {
1006
+ readonly descriptor: GlassGroupDescriptor;
1007
+ /** Set once the group has been resolved at least once. */
1008
+ readonly state?: GlassGroupState;
1009
+ /** Per-group governor override; falls back to the scene-wide pressure. */
1010
+ readonly governor?: GovernorPressure;
1011
+ }
1012
+ interface GlassNodeDescriptor {
1013
+ readonly id: string;
1014
+ readonly groupId: string;
1015
+ readonly shapeFamily: ShapeFamily;
1016
+ readonly shape: ShapeChannels;
1017
+ readonly zSlot: ZSlot;
1018
+ /** Inherits the group's material profile when absent. */
1019
+ readonly variant?: MaterialVariant$1;
1020
+ readonly interaction?: InteractionState;
1021
+ /** Overrides the group's adaptation for this surface. */
1022
+ readonly foreground?: ForegroundAdaptation;
1023
+ }
1024
+ interface GlassNodeRecord {
1025
+ readonly descriptor: GlassNodeDescriptor;
1026
+ /** Measured by platform-web in the read phase; absent until then. */
1027
+ readonly bounds?: Rect;
1028
+ /** Ancestor clip chain, viewport space. */
1029
+ readonly clip?: readonly Rect[];
1030
+ }
1031
+ /** One pyramid rebuild. `groupIds` is why there is one request and not one per group. */
1032
+ interface BackdropRebuildRequest {
1033
+ readonly sourceId: string;
1034
+ /** The dirty epoch this rebuild satisfies. */
1035
+ readonly epoch: number;
1036
+ readonly resolution: BackdropResolutionPolicy;
1037
+ /** Every group the rebuild serves. */
1038
+ readonly groupIds: readonly string[];
1039
+ }
1040
+ interface ResolvedGroup {
1041
+ readonly groupId: string;
1042
+ readonly state: GlassGroupState;
1043
+ readonly hint: ResolvedBackdropHint;
1044
+ readonly foreground: ResolvedForegroundAdaptation;
1045
+ /** Defaults filled in, so a consumer reads one pair of numbers and not two optionals. */
1046
+ readonly sampling: GroupSamplingGeometry;
1047
+ }
1048
+ interface ResolvedNode {
1049
+ readonly nodeId: string;
1050
+ readonly groupId: string;
1051
+ readonly material: ResolvedMaterial;
1052
+ readonly foreground: ResolvedForegroundAdaptation;
1053
+ }
1054
+ interface GroupStateChange {
1055
+ readonly groupId: string;
1056
+ /** Absent on a group's first resolution. */
1057
+ readonly previous?: GlassGroupState;
1058
+ readonly next: GlassGroupState;
1059
+ readonly change: StateChange;
1060
+ }
1061
+ interface SceneResolution {
1062
+ readonly groups: readonly ResolvedGroup[];
1063
+ readonly nodes: readonly ResolvedNode[];
1064
+ /** Only the groups whose state moved, plus every group's first resolution. */
1065
+ readonly changes: readonly GroupStateChange[];
1066
+ /**
1067
+ * One policy for the whole root, carried here so a renderer has everything a
1068
+ * frame decided in one object rather than two.
1069
+ */
1070
+ readonly accessibility: ResolvedAccessibilityPolicy;
1071
+ }
1072
+ /** One overlapping pair of surfaces inside one plane (X1). */
1073
+ interface PlaneOverlap {
1074
+ readonly plane: GlassPlane;
1075
+ readonly nodeIds: readonly [string, string];
1076
+ }
1077
+ /** Two groups whose padded backdrop proxies would overlap in one plane (X1). */
1078
+ interface ProxyOverlap {
1079
+ readonly plane: GlassPlane;
1080
+ readonly groupIds: readonly [string, string];
1081
+ }
1082
+ interface GlassSceneOptions {
1083
+ readonly platform: PlatformProbe;
1084
+ readonly accessibility?: SystemAccessibilityPreferences;
1085
+ readonly accessibilityOverrides?: AccessibilityOverrides;
1086
+ readonly diagnostics?: DiagnosticsChannel;
1087
+ /**
1088
+ * Dev-only checks — same-plane overlap, variant mixing — run only here.
1089
+ * Default true: they are cheap at v1's surface counts, and what they catch is
1090
+ * invisible otherwise.
1091
+ */
1092
+ readonly devMode?: boolean;
1093
+ }
1094
+ interface GlassScene {
1095
+ readonly diagnostics: DiagnosticsChannel;
1096
+ /**
1097
+ * The phase of the frame in flight, or `undefined` outside one. The scheduler
1098
+ * sets it; the scene uses it to gate phase-scoped operations. Outside a frame
1099
+ * the scene has no opinion — a host may register and measure whenever it likes.
1100
+ */
1101
+ readonly framePhase: FramePhase | undefined;
1102
+ setFramePhase(phase: FramePhase | undefined): void;
1103
+ registerBackdropSource(descriptor: BackdropSourceDescriptor): void;
1104
+ /** Only the resolution policy is patchable: `kind` is identity, the probe has its own setter. */
1105
+ updateBackdropSource(id: string, patch: {
1106
+ readonly resolution: BackdropResolutionPolicy;
1107
+ }): void;
1108
+ removeBackdropSource(id: string): void;
1109
+ backdropSource(id: string): BackdropSourceRecord | undefined;
1110
+ registerGlassGroup(descriptor: GlassGroupDescriptor): void;
1111
+ /** A key present with `undefined` clears that override; an absent key keeps it. */
1112
+ updateGlassGroup(id: string, patch: DescriptorPatch<Omit<GlassGroupDescriptor, "id">>): void;
1113
+ removeGlassGroup(id: string): void;
1114
+ glassGroup(id: string): GlassGroupRecord | undefined;
1115
+ groupsOfSource(sourceId: string): readonly GlassGroupRecord[];
1116
+ registerGlassNode(descriptor: GlassNodeDescriptor): void;
1117
+ /** A key present with `undefined` clears that override; an absent key keeps it. */
1118
+ updateGlassNode(id: string, patch: DescriptorPatch<Omit<GlassNodeDescriptor, "id">>): void;
1119
+ removeGlassNode(id: string): void;
1120
+ glassNode(id: string): GlassNodeRecord | undefined;
1121
+ nodesOfGroup(groupId: string): readonly GlassNodeRecord[];
1122
+ /** Measured viewport geometry, from the read phase. */
1123
+ setNodeBounds(id: string, bounds: Rect, clip?: readonly Rect[]): void;
1124
+ setPlatformProbe(probe: PlatformProbe): void;
1125
+ setSourceProbe(sourceId: string, probe: SourceProbe): void;
1126
+ /** Scene-wide by default; per group when `groupId` is given. */
1127
+ setGovernorPressure(pressure: GovernorPressure, groupId?: string): void;
1128
+ setSystemAccessibility(preferences: SystemAccessibilityPreferences): void;
1129
+ setAccessibilityOverrides(overrides: AccessibilityOverrides): void;
1130
+ accessibilityPolicy(): ResolvedAccessibilityPolicy;
1131
+ markBackdropSourceDirty(id: string): void;
1132
+ /** Peek without consuming. */
1133
+ dirtyBackdropSources(): readonly BackdropSourceRecord[];
1134
+ /**
1135
+ * Hand out the frame's rebuilds. The frame id *is* the scope: a second call
1136
+ * with the same id returns nothing, which is the §Core model invariant.
1137
+ */
1138
+ consumeDirtyBackdropSources(frameId: number): readonly BackdropRebuildRequest[];
1139
+ /**
1140
+ * Give back what `frameId` took, because the frame did not finish.
1141
+ *
1142
+ * Consuming commits `builtEpoch` at hand-out rather than at completion — core
1143
+ * has no view of the wire and cannot wait for one. That is right while the
1144
+ * frame runs to its end, and a lie the moment it does not: a claim spent on a
1145
+ * frame that threw leaves the source sitting clean at an epoch whose pixels
1146
+ * were never imported, and nothing ever marks it dirty again. Restoring the
1147
+ * pre-consume epochs makes the commit provisional for exactly as long as the
1148
+ * frame is.
1149
+ *
1150
+ * Returns the source ids restored. Idempotent, and a no-op for any frame id
1151
+ * other than the one that consumed.
1152
+ */
1153
+ rollbackDirtyBackdropSources(frameId: number): readonly string[];
1154
+ resolve(): SceneResolution;
1155
+ checkSamePlaneOverlap(): readonly PlaneOverlap[];
1156
+ /**
1157
+ * The cross-group half of X1's proxy geometry. `mergeDistance` only unions
1158
+ * members *within* a group, so two neighbouring groups can still put two
1159
+ * padded proxies over the same pixels — which S1 measured double-filtering.
1160
+ */
1161
+ checkGroupProxyOverlap(): readonly ProxyOverlap[];
1162
+ }
1163
+ /**
1164
+ * A descriptor patch. Unlike `Partial`, a key present with `undefined` is
1165
+ * meaningful: it *clears* the override and restores the inherited default.
1166
+ * Without that, a declarative binding could never take a prop back — React
1167
+ * re-rendering `<GlassGroup backdrop={undefined}>` would have no way to say
1168
+ * "this group no longer declares a hint" short of tearing the entry down.
1169
+ */
1170
+ type DescriptorPatch<D> = {
1171
+ readonly [K in keyof D]?: D[K] | undefined;
1172
+ };
1173
+ declare function createGlassScene(options: GlassSceneOptions): GlassScene;
1174
+
1175
+ /**
1176
+ * The frame-phase contract, and a reference implementation of it.
1177
+ *
1178
+ * Core is **passive**. There is no timer, no `requestAnimationFrame`, and no
1179
+ * clock read anywhere here — a host calls `runFrame` and supplies the frame's
1180
+ * time as a number. platform-web owns the rAF loop and drives this; the same
1181
+ * scheduler runs in a test with hand-written frame ids and no browser at all.
1182
+ *
1183
+ * The pipeline is fixed:
1184
+ *
1185
+ * ```
1186
+ * collect what changed since the last frame is marked dirty
1187
+ * read every layout measurement, batched, so the steady state performs none
1188
+ * update the scene resolves: capability state, policies, dev-mode checks
1189
+ * write GPU resources are brought up to date — where rebuilds are handed out
1190
+ * render drawing, with the graph frozen
1191
+ * ```
1192
+ *
1193
+ * `update` resolves the scene *before* the first update hook runs, and that
1194
+ * ordering is the contract rather than an implementation detail: every phase from
1195
+ * `update` onward reads a resolution describing the graph as it stood at the top
1196
+ * of that phase. So the graph is structurally frozen from `update` on — the scene
1197
+ * reports a register or a remove there as a `frame-phase-violation`, because a
1198
+ * hook that restructures the graph leaves the frame's resolution describing a
1199
+ * graph that no longer exists. Structural reconciliation belongs in `collect`;
1200
+ * `update` is where reads become resolved state.
1201
+ *
1202
+ * Phase discipline is enforced, not documented. Consuming the dirty set outside
1203
+ * `write`, measuring outside `read`, and restructuring the graph once it has
1204
+ * resolved each report a `frame-phase-violation`. None of them throws: a violated
1205
+ * frame still draws, and a diagnostic that costs a frame is one a host will
1206
+ * silence rather than fix.
1207
+ */
1208
+
1209
+ /** What a hook is handed. Valid for the phase it was passed to, and no longer. */
1210
+ interface FrameContext {
1211
+ readonly frame: FrameInfo;
1212
+ readonly phase: FramePhase;
1213
+ readonly scene: GlassScene;
1214
+ /** The frame's resolution. Absent before the update phase has produced it. */
1215
+ readonly resolution?: SceneResolution;
1216
+ /**
1217
+ * The backdrop sources needing a pyramid rebuild this frame. Legal in the
1218
+ * `write` phase, and at most once per frame however many participants ask —
1219
+ * the §Core model invariant, enforced rather than asserted.
1220
+ */
1221
+ consumeDirtyBackdropSources(): readonly BackdropRebuildRequest[];
1222
+ }
1223
+ /**
1224
+ * A hook per phase; every one optional. platform-web registers one participant
1225
+ * for the DOM host layer and the renderer registers another, so neither has to
1226
+ * know the other exists.
1227
+ */
1228
+ interface FrameParticipant {
1229
+ readonly id: string;
1230
+ readonly collect?: (context: FrameContext) => void;
1231
+ readonly read?: (context: FrameContext) => void;
1232
+ readonly update?: (context: FrameContext) => void;
1233
+ readonly write?: (context: FrameContext) => void;
1234
+ readonly render?: (context: FrameContext) => void;
1235
+ }
1236
+ interface FrameReport {
1237
+ readonly frame: FrameInfo;
1238
+ readonly resolution: SceneResolution;
1239
+ /** Same-plane surface overlaps found after the read phase, in dev mode. */
1240
+ readonly overlaps: readonly PlaneOverlap[];
1241
+ /** Groups whose padded proxies would cover the same pixels — X1's other half. */
1242
+ readonly proxyOverlaps: readonly ProxyOverlap[];
1243
+ /**
1244
+ * The rebuilds a write participant actually claimed — nothing more. The
1245
+ * scheduler never consumes on the frame's own behalf, because consuming
1246
+ * advances the source's `builtEpoch`: a rebuild taken so the report could be
1247
+ * "complete" would leave the source looking clean with no renderer having built
1248
+ * anything, and the pyramid would never be rebuilt. A frame with nobody to do
1249
+ * the work — no renderer registered yet, or one sitting out a device-loss
1250
+ * recovery — must therefore hand out nothing at all.
1251
+ */
1252
+ readonly rebuilds: readonly BackdropRebuildRequest[];
1253
+ /**
1254
+ * The source ids still dirty when the frame ended: whatever no participant
1255
+ * claimed, plus sources no group samples yet. Read with the non-consuming peek,
1256
+ * so a frame is never charged a rebuild for being reported on. A rebuild
1257
+ * survives here until a renderer takes it, which is precisely what lets
1258
+ * device-loss recovery pick the work back up instead of losing it.
1259
+ */
1260
+ readonly pendingSources: readonly string[];
1261
+ }
1262
+ interface FrameScheduler {
1263
+ /** Registering an existing id replaces that participant. */
1264
+ addParticipant(participant: FrameParticipant): void;
1265
+ removeParticipant(id: string): void;
1266
+ readonly participants: readonly FrameParticipant[];
1267
+ runFrame(frame: FrameInfo): FrameReport;
1268
+ }
1269
+ interface FrameSchedulerOptions {
1270
+ readonly scene: GlassScene;
1271
+ }
1272
+ declare function createFrameScheduler(options: FrameSchedulerOptions): FrameScheduler;
1273
+
1274
+ /**
1275
+ * X5 — the colour pipeline, CPU side.
1276
+ *
1277
+ * The contract in one line: **internal optical maths in linear light,
1278
+ * compositing premultiplied, output sRGB, v1 locked to the sRGB gamut.** Without
1279
+ * that lock golden images destabilise across GPUs and browsers and blur energy
1280
+ * drifts from the reference, which is the whole reason §Color pipeline exists.
1281
+ *
1282
+ * Three concrete consequences the rest of the renderer leans on:
1283
+ *
1284
+ * 1. **Every internal texture is `rgba16float` and holds linear light.** 8-bit
1285
+ * linear would band visibly in a blur pyramid; `rgba16float` is renderable and
1286
+ * filterable everywhere WebGPU is, so no adapter feature is required.
1287
+ * 2. **Alpha is normalised at import** (X3). `copyExternalImageToTexture`
1288
+ * delivers premultiplied; `importExternalTexture` delivers unpremultiplied.
1289
+ * Whichever arrives, level 0 of the pyramid is premultiplied linear — one
1290
+ * invariant, checked once at import, instead of an alpha mode threaded
1291
+ * through every downstream pass.
1292
+ * 3. **Output is encoded once, at the end of the last pass.** The optics and
1293
+ * highlight canvases are configured `alphaMode: "premultiplied"` in a plain
1294
+ * (non-`-srgb`) 8-bit format, and the browser composites those bytes as sRGB.
1295
+ * So the final step is `encode(linear)` and then `* alpha` — premultiplying in
1296
+ * the *encoded* space, which is what canvas compositing expects. Premultiplying
1297
+ * in linear and then encoding would darken every edge pixel.
1298
+ *
1299
+ * The transfer functions here are the exact piecewise sRGB curve, not the 2.2
1300
+ * approximation: the WGSL uses the same piecewise form, and a test asserts the
1301
+ * two agree, so a golden regenerated on one and asserted on the other cannot
1302
+ * drift by a code unit.
1303
+ */
1304
+ /**
1305
+ * The colour spaces v1 accepts on an imported backdrop.
1306
+ *
1307
+ * `display-p3` is *accepted and tagged*, never silently treated as sRGB — but v1
1308
+ * converts it into the sRGB working space rather than carrying a wide gamut
1309
+ * through the pipeline (§Color pipeline: "Display-P3 and HDR/extended-range are
1310
+ * future profiles"). Tagging it is what makes the future profile a data change.
1311
+ */
1312
+ declare const BACKDROP_COLOR_SPACES: readonly ["srgb", "display-p3"];
1313
+ type BackdropColorSpace = (typeof BACKDROP_COLOR_SPACES)[number];
1314
+ /** How a provider's pixels arrive. Normalised to `premultiplied` at import. */
1315
+ declare const BACKDROP_ALPHA_MODES: readonly ["premultiplied", "unpremultiplied", "opaque"];
1316
+ type BackdropAlphaMode = (typeof BACKDROP_ALPHA_MODES)[number];
1317
+ type Rgb = readonly [r: number, g: number, b: number];
1318
+
1319
+ /**
1320
+ * The optical constants, and the two foldings that decide what the shader
1321
+ * actually gets: the dual cap, and the size-parameterised lens.
1322
+ *
1323
+ * **Every number here is advisory and calibration-delegated (C7).** They are
1324
+ * chosen so the material is coherent out of the box — a plausible glass, not a
1325
+ * measured one — and §Calibration names exactly this kind of value as a delegated
1326
+ * unknown. They live in one profile object (`MaterialProfile`) so replacing them
1327
+ * with fitted values is a data change (`withMaterialOverrides`), and no shader
1328
+ * carries a literal of its own. The named constants below are re-exports of that
1329
+ * profile's defaults, kept because a reader wants a name for σ = 8 more often
1330
+ * than a whole profile.
1331
+ *
1332
+ * ## The dual cap (Decision Log #19)
1333
+ *
1334
+ * Two independent things cap refraction: the accessibility policy's regime
1335
+ * (`nominal | reduced | none`) and the group's resolved capability state
1336
+ * (`true | approximate | none` — what the sampling backend can actually deliver).
1337
+ * **The lower of the two wins**, and this module folds them into one scalar before
1338
+ * anything reaches a uniform, so the shader has no way to honour the wrong one.
1339
+ *
1340
+ * The ordering mirrors `platform-web`'s `REFRACTION_LADDER`, which serves the CSS
1341
+ * tier. It is restated rather than imported because this package sits *below*
1342
+ * core in the dependency graph and platform-web sits above it. That the two
1343
+ * copies must agree is a real (if small) seam — see the note in the C6 report.
1344
+ */
1345
+
1346
+ /** X2's `RefractionQuality`, restated. Weakest first — the declaration order IS the ladder. */
1347
+ declare const REFRACTION_LADDER: readonly ["none", "approximate", "true"];
1348
+ type RefractionQuality = (typeof REFRACTION_LADDER)[number];
1349
+ /**
1350
+ * The slice of core's `ResolvedAccessibilityPolicy["material"]` the renderer
1351
+ * reads. core's type is assignable to this; a test pins that.
1352
+ */
1353
+ interface MaterialPolicyView {
1354
+ readonly glass: "material" | "none";
1355
+ readonly frost: "nominal" | "increased" | "none";
1356
+ readonly refraction: "nominal" | "reduced" | "none";
1357
+ readonly occlusion: "nominal" | "increased" | "opaque";
1358
+ readonly border: "nominal" | "strong";
1359
+ readonly ambientTint: "nominal" | "reduced" | "none";
1360
+ readonly foreground: "adaptive" | "near-monochrome";
1361
+ }
1362
+ /** The two variants, declared as data so a profile merge can walk them. */
1363
+ declare const MATERIAL_VARIANTS: readonly ["regular", "clear"];
1364
+ type MaterialVariant = (typeof MATERIAL_VARIANTS)[number];
1365
+ interface MaterialOptics {
1366
+ /** Body blur σ in CSS px. Matches `platform-web`'s `MATERIAL_OPTICS.blurRadius`. */
1367
+ readonly blurSigma: number;
1368
+ /** Tint over the blurred backdrop, linear light. */
1369
+ readonly tint: Rgb;
1370
+ readonly tintAlpha: number;
1371
+ /** Rim band half-width in CSS px, and its ambient brightness. */
1372
+ readonly rimWidth: number;
1373
+ readonly rimAlpha: number;
1374
+ /** Specular exponent and gain on the rim. */
1375
+ readonly specularPower: number;
1376
+ readonly specularGain: number;
1377
+ /** Inner-shadow depth (0..1) and how much of it is applied. */
1378
+ readonly shadowDepth: number;
1379
+ readonly shadowAlpha: number;
1380
+ /** Highlight colour for the sweep and press glow, linear light. */
1381
+ readonly highlight: Rgb;
1382
+ }
1383
+ /** The rim a `border: "strong"` policy substitutes, whatever the variant asked for. */
1384
+ interface MaterialRim {
1385
+ readonly rimWidth: number;
1386
+ readonly rimAlpha: number;
1387
+ }
1388
+ /**
1389
+ * Every number the material runs on, in one place.
1390
+ *
1391
+ * The same seam `@vitrea/motion`'s `MotionProfile` opens for the drivers, for the
1392
+ * optics: C7's harness measures these against `apple-macos-26.5-*` fixtures and
1393
+ * replaces what is here, so nothing downstream may hard-code an optical constant
1394
+ * of its own. A profile overrides every one of them (`withMaterialOverrides`),
1395
+ * which makes landing a calibrated set a data change rather than a code change.
1396
+ *
1397
+ * Units: CSS px for distance, linear light for colour, viewport coordinates with
1398
+ * y pointing down for direction.
1399
+ */
1400
+ interface MaterialProfile {
1401
+ /** Per-variant optics. `clear` is persistently more transparent than `regular`. */
1402
+ readonly optics: Readonly<Record<MaterialVariant, MaterialOptics>>;
1403
+ /** The two ends of adaptation: what the tint becomes over a dark and a light backdrop. */
1404
+ readonly adaptiveTintDark: Rgb;
1405
+ readonly adaptiveTintLight: Rgb;
1406
+ /** Luminance band the tint crosses over. Hysteresis in time is the driver's job. */
1407
+ readonly adaptiveLuminanceLow: number;
1408
+ readonly adaptiveLuminanceHigh: number;
1409
+ /**
1410
+ * How much of the lens the shader is allowed to apply, per rung.
1411
+ *
1412
+ * `approximate` is not "half of true": it is the rim-lensing approximation, a
1413
+ * shallower bend confined nearer the edge, which is what a group sampling a CSS
1414
+ * proxy can honestly claim. Reduced transparency lands here too, which is the
1415
+ * point of the ladder having three rungs and not two.
1416
+ */
1417
+ readonly refractionScale: Readonly<Record<RefractionQuality, number>>;
1418
+ /**
1419
+ * The size-parameterised lens depth — parent acceptance #2's mechanism.
1420
+ *
1421
+ * Below `lensSpanMin` a surface gets its authored thickness and nothing more;
1422
+ * above `lensSpanMax` it gets `lensSizeGainMax` times it. The final clamp to the
1423
+ * shorter *half* extent is what keeps a small control from being all lens: a
1424
+ * 24 px-tall button cannot bend more than 12 px of backdrop however thick it is
1425
+ * authored.
1426
+ *
1427
+ * A smoothstep rather than a straight ratio, so two surfaces of nearly the same
1428
+ * size never read as differently thick, and so the gain saturates instead of
1429
+ * growing without bound on a full-width platter.
1430
+ */
1431
+ readonly lensSpanMin: number;
1432
+ readonly lensSpanMax: number;
1433
+ readonly lensSizeGainMax: number;
1434
+ /** Chain LOD per CSS px of lens depth, and how much sharper the rim samples. */
1435
+ readonly lensBodyLodPerPx: number;
1436
+ readonly lensRimLodBias: number;
1437
+ /**
1438
+ * What each accessibility regime does to the numbers above. The multipliers
1439
+ * match `platform-web`'s CSS tier so the two renderers degrade the same way
1440
+ * under the same preference.
1441
+ */
1442
+ readonly reducedTransparencyFrost: number;
1443
+ /**
1444
+ * How much of the *remaining* transparency reduced transparency closes.
1445
+ *
1446
+ * **Relative, not absolute (Decision Log #32(d)).** This was
1447
+ * `increasedOcclusionAlpha`, an absolute floor of 0.62 applied as
1448
+ * `Math.max(nominal, floor)` — a real lift while nominal was the advisory 0.28,
1449
+ * and a no-op from the moment C9a measured nominal at 0.62. The policy died
1450
+ * without being touched and nothing noticed for a whole child. A fraction of the
1451
+ * headroom cannot die that way: it lifts strictly for every nominal below 1,
1452
+ * whatever a later tuning pass moves nominal to.
1453
+ *
1454
+ * The fraction is the pre-C9a lift, restored rather than invented:
1455
+ * (0.62 − 0.28) / (1 − 0.28) = 0.4722, which reproduces the old floor exactly at
1456
+ * the old nominal. At today's nominal it reads 0.62 → 0.799.
1457
+ *
1458
+ * Mirrored by `@vitrea/platform-web`'s `INCREASED_OCCLUSION_LIFT`, and pinned in
1459
+ * both directions by `packages/calibration/test/tier-coherence.test.ts`.
1460
+ */
1461
+ readonly increasedOcclusionLift: number;
1462
+ readonly strongBorderRim: MaterialRim;
1463
+ readonly reducedTintAdaptation: number;
1464
+ /**
1465
+ * Advisory light direction, in viewport coordinates with y pointing down: a
1466
+ * little left of straight overhead, which is where Apple's material reads its
1467
+ * specular from.
1468
+ */
1469
+ readonly lightDirection: readonly [number, number];
1470
+ /** Specular sweep band width in radians, and the press glow's reach in CSS px. */
1471
+ readonly sweepBandRadians: number;
1472
+ readonly glowRadiusCss: number;
1473
+ readonly glowGain: number;
1474
+ readonly sweepGain: number;
1475
+ }
1476
+ /**
1477
+ * A profile patch: any subset, to any depth, of what a profile holds.
1478
+ *
1479
+ * A colour is one leaf, not three: patching a tint means naming the whole triple,
1480
+ * because two channels of a fitted colour and one of the default is not a colour
1481
+ * anybody measured.
1482
+ */
1483
+ interface MaterialProfilePatch {
1484
+ readonly optics?: Readonly<Partial<Record<MaterialVariant, Readonly<Partial<MaterialOptics>>>>>;
1485
+ readonly adaptiveTintDark?: Rgb;
1486
+ readonly adaptiveTintLight?: Rgb;
1487
+ readonly adaptiveLuminanceLow?: number;
1488
+ readonly adaptiveLuminanceHigh?: number;
1489
+ readonly refractionScale?: Readonly<Partial<Record<RefractionQuality, number>>>;
1490
+ readonly lensSpanMin?: number;
1491
+ readonly lensSpanMax?: number;
1492
+ readonly lensSizeGainMax?: number;
1493
+ readonly lensBodyLodPerPx?: number;
1494
+ readonly lensRimLodBias?: number;
1495
+ readonly reducedTransparencyFrost?: number;
1496
+ readonly increasedOcclusionLift?: number;
1497
+ readonly strongBorderRim?: Readonly<Partial<MaterialRim>>;
1498
+ readonly reducedTintAdaptation?: number;
1499
+ readonly lightDirection?: readonly [number, number];
1500
+ readonly sweepBandRadians?: number;
1501
+ readonly glowRadiusCss?: number;
1502
+ readonly glowGain?: number;
1503
+ readonly sweepGain?: number;
1504
+ }
1505
+
1506
+ /**
1507
+ * Backdrop analysis on the CPU side: the stats a reduction produced, and the
1508
+ * temporal hysteresis that makes them usable.
1509
+ *
1510
+ * §Motion binds `backdrop-adaptation values` to the *exponential low-pass +
1511
+ * hysteresis* driver, and §Motion also puts every driver on the CPU. So the shape
1512
+ * is: the GPU reduces (exactly — X2's `analysis: "exact"`), the result is read
1513
+ * back at a low rate, and `LowPassHysteresisDriver` from `@vitrea/motion` — the
1514
+ * same class the rest of the runtime uses, not a reimplementation — smooths it.
1515
+ * The shader reads the driver's output as a uniform and never sees the raw stat.
1516
+ *
1517
+ * ## Why the readback is not a per-frame cost
1518
+ *
1519
+ * §Foreground adaptation caps the readback at an advisory 15 Hz (Decision Log
1520
+ * #19), and the governor lowers it further. That is not a performance
1521
+ * concession — it is what keeps the adaptation honest. A per-frame readback would
1522
+ * either stall the pipeline waiting for the map, or produce a value two frames old
1523
+ * and pretend it was current. At 15 Hz with a 500 ms low-pass the staleness is far
1524
+ * inside the filter's own time constant, so it is invisible in the output.
1525
+ *
1526
+ * ## The band is on the input, not the output
1527
+ *
1528
+ * `LowPassHysteresisDriver` commits a new observation only once it departs the
1529
+ * *committed* value by more than the band, then low-passes the committed
1530
+ * staircase. Measuring against the moving output instead would let a slow drift
1531
+ * leak through one small step at a time, which is exactly the foreground pumping
1532
+ * §Foreground adaptation rules out. That property is the driver's, and it is the
1533
+ * reason to use the driver rather than an `exp()` here.
1534
+ */
1535
+
1536
+ /** The four floats the reduction writes. */
1537
+ interface BackdropStats {
1538
+ /** Mean linear luminance, 0..1-ish (an HDR-ish source can exceed 1). */
1539
+ readonly luminance: number;
1540
+ /** Variance of linear luminance. */
1541
+ readonly variance: number;
1542
+ /** Mean luminance-gradient magnitude per texel — the edge-density measure. */
1543
+ readonly edgeDensity: number;
1544
+ /** Samples the reduction actually took. 0 means "never reduced". */
1545
+ readonly sampleCount: number;
1546
+ }
1547
+
1548
+ /**
1549
+ * X3 — the BackdropFrame acquisition protocol, and a provider for every source
1550
+ * kind §Backdrop contracts names.
1551
+ *
1552
+ * The spec calls this contract "operational, not aspirational", and the reason is
1553
+ * a hard WebGPU fact: **there is no cross-device texture sharing.** So every
1554
+ * source, however it arrives, is normalised through one protocol —
1555
+ * `acquire(frame)` at frame start, `release()` after submit — and the frame
1556
+ * carries the four things a consumer cannot infer: the binding, the size epoch,
1557
+ * the colour space, and the alpha mode.
1558
+ *
1559
+ * ## The five kinds, and what is actually different about each
1560
+ *
1561
+ * | kind | mechanism | alpha | re-acquired |
1562
+ * | --- | --- | --- | --- |
1563
+ * | `image` | `copyExternalImageToTexture` | premultiplied | only when dirty |
1564
+ * | `video` | `importExternalTexture` | **unpremultiplied** | **every sampling frame** |
1565
+ * | `canvas` | `copyExternalImageToTexture` | premultiplied | every frame by default |
1566
+ * | `gradient` | `queue.writeTexture` of CPU-generated texels | opaque | never |
1567
+ * | `app-texture-view` | the app's own `GPUTexture` | declared by the app | every frame |
1568
+ *
1569
+ * Two of those cells are WebGPU semantics rather than choices, and §Surprises
1570
+ * records them as such: `importExternalTexture` handles **expire at task end**, so
1571
+ * a video must be re-imported on every frame that samples it; and imported video
1572
+ * arrives **unpremultiplied** while copied images arrive premultiplied. The import
1573
+ * pass normalises both to premultiplied linear, so nothing downstream branches on
1574
+ * either fact.
1575
+ *
1576
+ * ## VideoFrame ownership
1577
+ *
1578
+ * X3: "`VideoFrame` close ownership held by the provider." When the app hands in a
1579
+ * `VideoFrame`, this provider closes it in `release()` — after submit, because an
1580
+ * imported external texture must outlive the submission that samples it. When the
1581
+ * app hands in an `HTMLVideoElement` the browser owns the frames and the provider
1582
+ * closes nothing. Getting that backwards leaks decoder buffers until playback
1583
+ * stalls, which is why the two cases are separate provider inputs rather than one
1584
+ * permissive union.
1585
+ *
1586
+ * ## Device generations
1587
+ *
1588
+ * The same WebGPU fact has a second consequence, and it is the one easiest to get
1589
+ * wrong: a provider closes over the device it was built with, so when that device
1590
+ * dies the provider holds storage nothing can bind and a texture handle nothing
1591
+ * will complain about. `invalidate(generation, device)` is how a provider crosses
1592
+ * that boundary — new device adopted, old storage dropped, re-import armed — and
1593
+ * `generation` is the tag that says which side of it the provider is on. The
1594
+ * app-texture provider is the exception on purpose: the renderer cannot re-make
1595
+ * someone else's texture, so it refuses rather than binding a foreign-device view
1596
+ * that would render nothing and report nothing.
1597
+ *
1598
+ * ## Validation at registration
1599
+ *
1600
+ * Also X3: app-supplied views "must satisfy declared usage/format/dimension
1601
+ * requirements — validated at registration with a typed error, never discovered at
1602
+ * draw time." `registerAppTexture` therefore takes the `GPUTexture`, not just a
1603
+ * view: a `GPUTextureView` exposes none of its own properties, so a contract that
1604
+ * accepted only a view could not be checked at all.
1605
+ */
1606
+
1607
+ declare const BACKDROP_KINDS: readonly ["image", "video", "canvas", "gradient", "app-texture-view"];
1608
+ type BackdropKind = (typeof BACKDROP_KINDS)[number];
1609
+ /** How the import pass binds the source. `external` needs its own pipeline. */
1610
+ type BackdropBinding = {
1611
+ readonly kind: "sampled";
1612
+ readonly view: GPUTextureView;
1613
+ } | {
1614
+ readonly kind: "external";
1615
+ readonly texture: GPUExternalTexture;
1616
+ };
1617
+ interface BackdropFrame {
1618
+ readonly sourceId: string;
1619
+ readonly binding: BackdropBinding;
1620
+ /** Source extent in texture px. */
1621
+ readonly width: number;
1622
+ readonly height: number;
1623
+ /** Bumped when the extent changes; invalidates every dependent allocation. */
1624
+ readonly sizeEpoch: number;
1625
+ readonly colorSpace: BackdropColorSpace;
1626
+ readonly alphaMode: BackdropAlphaMode;
1627
+ /** True when the sampled values are sRGB-encoded rather than linear. */
1628
+ readonly encoded: boolean;
1629
+ }
1630
+ /** The frame facts a provider is handed. Matches core's `FrameInfo`. */
1631
+ interface FrameInfoView {
1632
+ readonly id: number;
1633
+ readonly timeMs: number;
1634
+ }
1635
+ interface BackdropProvider {
1636
+ readonly id: string;
1637
+ readonly kind: BackdropKind;
1638
+ /**
1639
+ * The device generation this provider's storage belongs to (`device.ts`'s
1640
+ * counter). A provider built under one generation holds nothing a later
1641
+ * generation can bind, and the failure is silent — so the number is here to be
1642
+ * compared, cheaply, before anything of the provider's is used.
1643
+ */
1644
+ readonly generation: number;
1645
+ /** True when content may have changed since the last successful import. */
1646
+ isDirty(): boolean;
1647
+ /** X3: at frame start. Throws `source-unavailable` if the source cannot serve. */
1648
+ acquire(frame: FrameInfoView): BackdropFrame;
1649
+ /** X3: after submit. Releases whatever the acquire took ownership of. */
1650
+ release(): void;
1651
+ /** Called once the import pass has consumed the frame's pixels. */
1652
+ markImported(): void;
1653
+ /**
1654
+ * Adopt `device` at `generation`: the device that built this provider's storage
1655
+ * is gone.
1656
+ *
1657
+ * Every provider closes over the device it was built with, so a reset that only
1658
+ * cleared the dirty flags would re-import onto the dead device and hand the new
1659
+ * one a foreign texture. Hence the device travels with the call.
1660
+ *
1661
+ * A provider whose storage the renderer allocated drops it and arms a
1662
+ * re-import, so recovery flows through the ordinary dirty path. A provider
1663
+ * wrapping the *app's* own texture cannot be re-pointed at all — WebGPU has no
1664
+ * cross-device sharing, and the app owns the texture — so it refuses every
1665
+ * later `acquire` with a typed error instead of binding a foreign-device view.
1666
+ */
1667
+ invalidate(generation: number, device: BackdropDevice): void;
1668
+ /**
1669
+ * Re-declare the source's extent. Implemented where the renderer allocates the
1670
+ * storage (image, canvas); absent where the source declares its own size.
1671
+ */
1672
+ resize?(width: number, height: number): void;
1673
+ destroy(): void;
1674
+ }
1675
+ /** The slice of `GPUDevice` the providers need. `GPUDevice` satisfies it as-is. */
1676
+ interface BackdropDevice {
1677
+ readonly queue: GPUQueue;
1678
+ readonly limits: GPUSupportedLimits;
1679
+ createTexture(descriptor: GPUTextureDescriptor): GPUTexture;
1680
+ importExternalTexture(descriptor: GPUExternalTextureDescriptor): GPUExternalTexture;
1681
+ }
1682
+ /** Anything `copyExternalImageToTexture` accepts as a source. */
1683
+ type CopyableSource = ImageBitmap | HTMLImageElement | HTMLCanvasElement | OffscreenCanvas | ImageData | VideoFrame;
1684
+ interface CopyProviderOptions {
1685
+ readonly id: string;
1686
+ readonly kind: "image" | "canvas";
1687
+ readonly device: BackdropDevice;
1688
+ readonly source: CopyableSource;
1689
+ readonly width: number;
1690
+ readonly height: number;
1691
+ readonly colorSpace?: BackdropColorSpace;
1692
+ /**
1693
+ * Whether content changes every frame. `true` for a live canvas, `false` for a
1694
+ * decoded image — which is what makes a static backdrop rebuild nothing at all
1695
+ * (§Core model invariant).
1696
+ */
1697
+ readonly live?: boolean;
1698
+ /** The device generation this provider is being built under. See `BackdropProvider.generation`. */
1699
+ readonly generation?: number;
1700
+ }
1701
+ interface VideoProviderOptions {
1702
+ readonly id: string;
1703
+ readonly device: BackdropDevice;
1704
+ /**
1705
+ * Either a live element the browser keeps frames for, or a callback producing a
1706
+ * `VideoFrame` this provider then owns and closes. The two are separate because
1707
+ * their ownership rules are opposite — see the module note.
1708
+ */
1709
+ readonly source: {
1710
+ readonly kind: "element";
1711
+ readonly element: HTMLVideoElement;
1712
+ } | {
1713
+ readonly kind: "frames";
1714
+ readonly next: () => VideoFrame | undefined;
1715
+ };
1716
+ readonly colorSpace?: BackdropColorSpace;
1717
+ /** The device generation this provider is being built under. See `BackdropProvider.generation`. */
1718
+ readonly generation?: number;
1719
+ }
1720
+
1721
+ /**
1722
+ * Device ownership, loss teardown, and rebuild (§GPU device ownership).
1723
+ *
1724
+ * `platform-web` owns the *browser* half of the story — is there an adapter, get
1725
+ * a device, notice when it goes away. This module owns the *resource* half: what
1726
+ * has to be thrown away when a device dies, when it is safe to build again, and
1727
+ * what core's capability inputs should read while that is in flight.
1728
+ *
1729
+ * ## The two ownership modes, and why they are not symmetric
1730
+ *
1731
+ * - **vitrea-owned** — this module can re-request a device by itself, so loss is
1732
+ * a transient it recovers from: tear down, re-request, re-attach, rebuild.
1733
+ * Every backdrop source is re-imported on the next frame because loss marks
1734
+ * them all dirty, which routes recovery through the same
1735
+ * one-rebuild-per-dirty-source-per-frame path a content change takes rather
1736
+ * than through a special case.
1737
+ * - **app-owned** — the app owns the resources that would have to be
1738
+ * re-registered, so this module reports the loss, raises
1739
+ * `replacementPending`, and **waits**. Groups stay demoted until
1740
+ * `replaceDevice` arrives and the re-registration handshake completes. Inventing
1741
+ * a device the app did not give us would break the one guarantee app-ownership
1742
+ * exists for: that every texture the renderer samples came from the app's own
1743
+ * device, because WebGPU has no cross-device sharing.
1744
+ *
1745
+ * `info.reason === "destroyed"` is our own teardown and is not recovered from —
1746
+ * re-requesting there would resurrect a renderer the host just destroyed.
1747
+ *
1748
+ * ## Generations
1749
+ *
1750
+ * Every attach bumps a generation counter, and every GPU object this package
1751
+ * holds is tagged with the generation it was made under. A resource from a lost
1752
+ * device is unusable forever, and the failure mode is silent — bindings simply
1753
+ * produce nothing — so "is this from the current device" has to be a cheap
1754
+ * integer comparison somewhere. It is here.
1755
+ */
1756
+ /**
1757
+ * Structurally identical to `vitrea`'s `WebGPUAvailability` (X2's K1
1758
+ * amendment, Decision Log #21c). Declared here rather than imported because this
1759
+ * package sits *below* core in the dependency graph — core reaches the renderer
1760
+ * through a dynamic import, so an import back would close a cycle. A test pins
1761
+ * the two unions to the same members.
1762
+ */
1763
+ type WebGPUAvailability = "not-requested" | "unavailable" | "available";
1764
+ type DeviceOwnership = "vitrea" | "app";
1765
+ interface RendererDeviceStatus {
1766
+ /** Feeds core's `PlatformProbe.webgpu` unchanged. */
1767
+ readonly webgpu: WebGPUAvailability;
1768
+ readonly deviceHealth: "ok" | "lost";
1769
+ readonly ownership: DeviceOwnership;
1770
+ readonly device: GPUDevice | undefined;
1771
+ /** Bumped on every attach. Tags every resource built under it. */
1772
+ readonly generation: number;
1773
+ /** True while an app-owned device is lost and no replacement has arrived. */
1774
+ readonly replacementPending: boolean;
1775
+ /** Why there is no device, when there is a reason worth reporting. */
1776
+ readonly unavailableReason?: "no-adapter" | "device-request-failed" | "lost";
1777
+ }
1778
+ /** The capability facts a host folds into core's `PlatformProbe`. */
1779
+ interface DeviceCapabilityInput {
1780
+ readonly webgpu: WebGPUAvailability;
1781
+ readonly deviceHealth: "ok" | "lost";
1782
+ }
1783
+
1784
+ /**
1785
+ * The governor's knobs.
1786
+ *
1787
+ * §Performance envelope: "The governor degrades **within** a tier first
1788
+ * (refraction resolution, adaptation cadence, edge analysis) and switches tiers
1789
+ * only with long hysteresis and cooldown." Decision Log #19 adds that intra-tier
1790
+ * degradation is **not a state change** — a group under `degrade-in-tier`
1791
+ * pressure keeps `activeRenderer: "webgpu"` and its whole resolved state.
1792
+ *
1793
+ * So the split is: **the policy lives in core, the knobs live here.** This module
1794
+ * exposes what can be turned and publishes a suggested ladder as *data*; it never
1795
+ * decides to turn anything, never reads a clock, and never applies hysteresis.
1796
+ * A governor that lived in the renderer would be a governor that could not see
1797
+ * the scene it is governing.
1798
+ *
1799
+ * ## The three knobs, weakest cost first
1800
+ *
1801
+ * 1. **`fieldFamily`** — `rsupn` → `rsup`. S2 priced this at 29% of the field's
1802
+ * cost for a bound that degrades from 0.170 px / 2.91° to 0.574 px / 4.26°.
1803
+ * It is the *first* step because it is one uniform and one pipeline: no
1804
+ * resolution change, so nothing resamples and nothing shimmers as it engages.
1805
+ * **Conditional on the f32 cross-check** (Decision Log #20) — and the check
1806
+ * has now been run, so the condition is met. See `FAMILY_C_CROSS_CHECK`.
1807
+ * 2. **`refractionResolutionScale`** — the group's **field** targets are
1808
+ * rasterised at a fraction of device resolution, and the optics and highlight
1809
+ * passes filter them instead of indexing them (`passes.ts`, and the
1810
+ * `fieldUpsampled` flag both shaders branch on). Quadratic saving on the pass
1811
+ * that evaluates the pseudo-SDF union per pixel per member, and the most
1812
+ * visible step, so it comes second.
1813
+ *
1814
+ * What it does **not** yet do is shrink the optics and highlight passes
1815
+ * themselves: those still render at full device resolution into the plane
1816
+ * canvases, because rendering them smaller means an offscreen target and a
1817
+ * resolve pass rather than a change of extent. So rungs 2 and 3 deliver the
1818
+ * field's quadratic saving and the cadence saving, not the full quadratic
1819
+ * saving on both heavy passes. Recorded here rather than implied, because a
1820
+ * ladder whose rungs are priced for savings they do not deliver is a ladder
1821
+ * core's policy will walk too far down.
1822
+ * 3. **`adaptationCadenceHz`** — how often analysis is reduced and read back.
1823
+ * Cheapest of the three in visual terms because the values it feeds are
1824
+ * already low-passed over hundreds of milliseconds; dropping the cadence
1825
+ * mostly changes how quickly a scroll's new backdrop is noticed.
1826
+ */
1827
+ type FieldFamily = "rsupn" | "rsup";
1828
+ interface GovernorKnobs {
1829
+ /** Which pseudo-SDF family the field pass compiles. */
1830
+ readonly fieldFamily: FieldFamily;
1831
+ /**
1832
+ * Device-resolution fraction the group's field targets are rasterised at,
1833
+ * 0 < s <= 1. The optics and highlight passes upsample what they read. See the
1834
+ * module note for what this does and does not save.
1835
+ */
1836
+ readonly refractionResolutionScale: number;
1837
+ /** Analysis reduction + readback rate. 0 disables adaptation entirely. */
1838
+ readonly adaptationCadenceHz: number;
1839
+ }
1840
+ interface Governor {
1841
+ readonly knobs: GovernorKnobs;
1842
+ /** True when `fieldFamily: "rsup"` is permitted to engage. */
1843
+ readonly familyCVerified: boolean;
1844
+ set(patch: Partial<GovernorKnobs>): GovernorKnobs;
1845
+ /** Move to a rung of the suggested ladder. Out-of-range clamps to the ends. */
1846
+ setLevel(level: number): GovernorKnobs;
1847
+ reset(): GovernorKnobs;
1848
+ /** Record that the cross-check passed, unlocking family C. */
1849
+ recordFamilyCVerified(): void;
1850
+ }
1851
+
1852
+ /**
1853
+ * What the renderer consumes each frame, and why it is declared here rather than
1854
+ * imported from `vitrea`.
1855
+ *
1856
+ * The renderer sits **below** core in the dependency graph: core reaches it
1857
+ * through a dynamic import (X7's lazy seam), so importing core back would close a
1858
+ * cycle that neither `pnpm -r build` nor `tsc` can order. The types below are
1859
+ * therefore declared as the *minimal structural contract* the renderer reads, and
1860
+ * core's own types satisfy them without an adapter — `SceneResolution`,
1861
+ * `FrameContext` and `FrameParticipant` are assignable to the views here, which
1862
+ * `test/core-contract.test.ts` asserts against core's real modules.
1863
+ *
1864
+ * Two things genuinely do not come from core, and both are flagged as
1865
+ * parent-impact rather than papered over:
1866
+ *
1867
+ * - **The corner reference.** `GlassNodeDescriptor` carries `shapeFamily` and
1868
+ * the X8 channel vector but not which of the two corner references the shape
1869
+ * is fit against (Decision Log #22a made those two separate references, not
1870
+ * one axis). It defaults to `"apple-continuous"`, which is what the public
1871
+ * `profile: "continuous"` — the default — resolves to.
1872
+ * - **The concentric parent link.** X8 rider 2 binds the renderer to draw a
1873
+ * concentric child as a level set *of its parent's field*, which needs to know
1874
+ * which surface the parent is. core's node descriptor has no parent edge, so it
1875
+ * arrives as a render input.
1876
+ *
1877
+ * Interaction values arrive as **numbers**, never as time. §Motion puts the
1878
+ * drivers on the CPU and the spec is explicit that this package consumes their
1879
+ * outputs; there is no clock anywhere in this package for the same reason there is
1880
+ * none in core.
1881
+ */
1882
+
1883
+ /** Motion-driver outputs for one surface. Every one is a value, never a time. */
1884
+ interface SurfaceChannels {
1885
+ /** `pressCompression`, 0..1. Scaled by motion's `pressCompressionScale`. */
1886
+ readonly press: number;
1887
+ /** `glow`, 0..1, from the fast-attack / slow-decay driver. */
1888
+ readonly glow: number;
1889
+ /** Specular sweep position, 0..1 around the contour. */
1890
+ readonly sweep: number;
1891
+ /** `lensStrength`, 0..1. Multiplies the resolved refraction scale. */
1892
+ readonly lensStrength: number;
1893
+ /** Press point in viewport CSS px. Defaults to the surface's centre. */
1894
+ readonly pressPoint?: readonly [number, number];
1895
+ }
1896
+ interface SurfaceInput {
1897
+ readonly nodeId: string;
1898
+ readonly family: ShapeFamily;
1899
+ /** X8, in viewport CSS px. `centre` is the surface's centre, `size` its full extent. */
1900
+ readonly shape: ShapeChannels;
1901
+ /** Defaults to `"apple-continuous"` — see the module note. */
1902
+ readonly reference?: CornerReference;
1903
+ readonly variant?: MaterialVariant;
1904
+ readonly channels?: Partial<SurfaceChannels>;
1905
+ /**
1906
+ * X8 rider 2. When present this surface renders as `parentField + inset`, and
1907
+ * the parent must be declared in the same group — as a member, or as a
1908
+ * `fieldReferenceOnly` shape.
1909
+ */
1910
+ readonly concentricOf?: {
1911
+ readonly nodeId: string;
1912
+ readonly inset: number;
1913
+ };
1914
+ /**
1915
+ * Declared so a concentric child can be a level set of it, but not itself
1916
+ * drawn.
1917
+ *
1918
+ * This exists because of what a union is: a concentric child's field is its
1919
+ * parent's plus a positive inset, so inside the parent the child is always the
1920
+ * larger value and `min` discards it. A child nested in the same union as its
1921
+ * parent is therefore invisible *by construction* — correct, and not what an
1922
+ * inset indicator inside a segmented control's track wants. There, the track
1923
+ * and the indicator are separate surfaces with separate material, and the
1924
+ * indicator's group needs the track's geometry only as the field its own
1925
+ * contour is offset from.
1926
+ */
1927
+ readonly fieldReferenceOnly?: boolean;
1928
+ }
1929
+ interface GroupRenderInput {
1930
+ readonly groupId: string;
1931
+ readonly surfaces: readonly SurfaceInput[];
1932
+ /** The backdrop source to sample. Absent means the group draws with no backdrop. */
1933
+ readonly backdropSourceId?: string;
1934
+ /** X2's resolved refraction quality for this group. Half of the dual cap. */
1935
+ readonly refraction: RefractionQuality;
1936
+ /** True where X2 resolved `analysis: "exact"`; gates adaptive tint. */
1937
+ readonly analysisExact: boolean;
1938
+ readonly variant?: MaterialVariant;
1939
+ /** Overrides the calibration-delegated union defaults. */
1940
+ readonly union?: GroupUnionParams;
1941
+ }
1942
+ /**
1943
+ * The structural view of core's `SceneResolution` the renderer reads, so a host
1944
+ * can hand core's own object straight through.
1945
+ */
1946
+ interface SceneResolutionView {
1947
+ readonly groups: readonly {
1948
+ readonly groupId: string;
1949
+ readonly state: {
1950
+ readonly refraction: RefractionQuality;
1951
+ readonly analysis: "exact" | "hint" | "none";
1952
+ readonly samplingBackend: "gpu-texture" | "css-backdrop" | "none";
1953
+ };
1954
+ }[];
1955
+ readonly accessibility: {
1956
+ readonly material: MaterialPolicyView;
1957
+ };
1958
+ }
1959
+ /** The structural view of core's `BackdropRebuildRequest`. */
1960
+ interface RebuildRequestView {
1961
+ readonly sourceId: string;
1962
+ readonly epoch: number;
1963
+ readonly resolution: {
1964
+ readonly scale: number;
1965
+ readonly maxDimension: number;
1966
+ };
1967
+ readonly groupIds: readonly string[];
1968
+ }
1969
+ /** The structural view of core's `FrameContext`. */
1970
+ interface FrameContextView {
1971
+ readonly frame: {
1972
+ readonly id: number;
1973
+ readonly timeMs: number;
1974
+ };
1975
+ readonly phase: "collect" | "read" | "update" | "write" | "render";
1976
+ readonly resolution?: SceneResolutionView;
1977
+ consumeDirtyBackdropSources(): readonly RebuildRequestView[];
1978
+ }
1979
+ /** The structural view of core's `FrameParticipant`. */
1980
+ interface FrameParticipantView {
1981
+ readonly id: string;
1982
+ readonly write?: (context: FrameContextView) => void;
1983
+ readonly render?: (context: FrameContextView) => void;
1984
+ }
1985
+
1986
+ /**
1987
+ * Pass-by-pass timing, for the benchmark §Performance envelope pins the ~2 ms
1988
+ * hypothesis to.
1989
+ *
1990
+ * The spec asks for the budget to be "measured per pass (backdrop import, blur,
1991
+ * analysis, body, highlight, composite) alongside browser end-to-end frame time",
1992
+ * which needs GPU-side timestamps: wall-clock around `submit()` measures the
1993
+ * queue, not the work. WebGPU's only timestamp mechanism is `timestampWrites` on
1994
+ * a pass descriptor — `encoder.writeTimestamp` was removed from the spec — so the
1995
+ * timeline hands out begin/end query indices by label and each pass spreads them
1996
+ * into its descriptor.
1997
+ *
1998
+ * Where `timestamp-query` is unavailable the timeline is simply absent and the
1999
+ * benchmark falls back to wall-clock totals, reported as such. A number labelled
2000
+ * as a GPU pass time that was actually a CPU submit time would be worse than no
2001
+ * number, and §Risks already names timestamp-query availability as an engine
2002
+ * variance to record rather than to assume.
2003
+ *
2004
+ * One caveat worth stating because it changes how the results read: Chrome
2005
+ * quantises timestamp results to 100 µs by default, so a single short pass reads
2006
+ * as 0. The benchmark works around it the way S2's harness did — by launching with
2007
+ * `--disable-dawn-features=timestamp_quantization`, and by measuring many
2008
+ * repetitions when it cannot.
2009
+ */
2010
+ interface PassTimeline {
2011
+ /** Query indices for a render pass, or undefined when timing is off. */
2012
+ renderSlot(label: string): GPURenderPassTimestampWrites | undefined;
2013
+ computeSlot(label: string): GPUComputePassTimestampWrites | undefined;
2014
+ }
2015
+ interface TimingCollector extends PassTimeline {
2016
+ readonly capacity: number;
2017
+ readonly used: number;
2018
+ /** Queue the resolve + copy. Call once, after the last pass of the frame. */
2019
+ resolve(encoder: GPUCommandEncoder): void;
2020
+ /** Read the resolved timings. Labels map to elapsed nanoseconds. */
2021
+ read(): Promise<ReadonlyMap<string, number>>;
2022
+ /**
2023
+ * Slots whose resolved pair was not a positive duration, since the collector
2024
+ * was created. A non-zero count means some number reported here is missing a
2025
+ * pass, and saying so is better than quietly averaging it in.
2026
+ */
2027
+ readonly anomalies: number;
2028
+ reset(): void;
2029
+ destroy(): void;
2030
+ }
2031
+
2032
+ interface PyramidPlan {
2033
+ /** Level 0 size in texture px. */
2034
+ readonly width: number;
2035
+ readonly height: number;
2036
+ readonly levelCount: number;
2037
+ /** The highest LOD the optics pass may sample: `levelCount - 1`. */
2038
+ readonly maxLod: number;
2039
+ /** Which level the analysis reduction reads. */
2040
+ readonly analysisLevel: number;
2041
+ /** Per-level sizes, index 0 = level 0. */
2042
+ readonly levels: readonly {
2043
+ readonly width: number;
2044
+ readonly height: number;
2045
+ }[];
2046
+ }
2047
+ interface ResolutionPolicyView {
2048
+ readonly scale: number;
2049
+ readonly maxDimension: number;
2050
+ }
2051
+
2052
+ /**
2053
+ * The §Core model invariant, as a testable object.
2054
+ *
2055
+ * > blur/analysis pyramids belong to `BackdropSource`, rebuilt **at most once per
2056
+ * > dirty source per frame** — never per group.
2057
+ *
2058
+ * core enforces one half of that: `consumeDirtyBackdropSources(frameId)` hands out
2059
+ * one pass over the dirty set per frame id, so a second caller in the same frame
2060
+ * gets nothing. What core cannot see is the renderer's side — a renderer that also
2061
+ * rebuilt lazily on first draw, or that rebuilt once per group from a single
2062
+ * request, would satisfy core's guard and still violate the invariant.
2063
+ *
2064
+ * So the ledger lives here, apart from the GPU work it guards, for one reason: it
2065
+ * makes the invariant assertable against core's real scheduler with no adapter and
2066
+ * no device. `pyramid.ts` claims through it before it encodes a single pass, and
2067
+ * `test/dirty-epoch.test.ts` drives core's own scene and scheduler over this same
2068
+ * object. An invariant tested only through a GPU is an invariant tested only where
2069
+ * a GPU exists.
2070
+ */
2071
+ interface RebuildLedger {
2072
+ /**
2073
+ * Begin a frame. Clears the per-frame tally — unless `frameId` is the frame
2074
+ * already being recorded, which is not a new frame and must not reopen the
2075
+ * tally. The renderer draws one plane per call with the same `FrameInfo`, so
2076
+ * that case is the ordinary one rather than the exception.
2077
+ */
2078
+ beginFrame(frameId: number): void;
2079
+ readonly frameId: number | undefined;
2080
+ /**
2081
+ * Claim the one rebuild this source is allowed this frame. `false` means it has
2082
+ * already had it — the caller must not encode a second one.
2083
+ */
2084
+ claim(sourceId: string): boolean;
2085
+ /** Record that a claim did no work because the source was clean. */
2086
+ recordClean(): void;
2087
+ /** Rebuilds recorded for `sourceId` in the frame being recorded. */
2088
+ countInFrame(sourceId: string): number;
2089
+ readonly rebuilds: number;
2090
+ readonly refusedDuplicates: number;
2091
+ readonly skippedClean: number;
2092
+ /**
2093
+ * Highest per-source rebuild count seen in any single frame. The invariant is
2094
+ * exactly the statement that this never exceeds 1.
2095
+ */
2096
+ readonly peakPerSourcePerFrame: number;
2097
+ }
2098
+
2099
+ /**
2100
+ * The blur/analysis pyramid, one per `BackdropSource`, and the ledger that proves
2101
+ * the §Core model invariant.
2102
+ *
2103
+ * > **Invariant:** blur/analysis pyramids belong to `BackdropSource`, rebuilt **at
2104
+ * > most once per dirty source per frame** — never per group. Static backdrops
2105
+ * > rebuild nothing.
2106
+ *
2107
+ * core enforces half of that already: `consumeDirtyBackdropSources(frameId)`
2108
+ * hands out one pass over the dirty set per frame id, so however many
2109
+ * participants ask, the second call returns nothing. What core *cannot* see is
2110
+ * this side of the wire — a renderer that also rebuilt lazily on first draw, or
2111
+ * that rebuilt once per group from one request, would satisfy core's guard and
2112
+ * violate the invariant. So the ledger here counts rebuilds per source per frame
2113
+ * on the renderer's own books, refuses a second one, and exposes the counters.
2114
+ * That is what makes the invariant *instrumented* rather than asserted.
2115
+ *
2116
+ * ## Pass structure per rebuild
2117
+ *
2118
+ * ```
2119
+ * import provider frame -> chain mip 0 (premultiplied linear, X5)
2120
+ * downsample mip n-1 -> chain mip n (13-tap, one pass per level)
2121
+ * blur x2 chain[bodyLvl] -> body (separable, residual sigma)
2122
+ * analysis chain[anaLvl] -> stats buffer (compute, one workgroup)
2123
+ * ```
2124
+ *
2125
+ * Reading mip n-1 while rendering into mip n is legal because they are distinct
2126
+ * subresources, and every view here is created with an explicit
2127
+ * `baseMipLevel`/`mipLevelCount: 1` so that is true by construction rather than by
2128
+ * the driver's interpretation of a full-texture view.
2129
+ */
2130
+
2131
+ interface PyramidResources {
2132
+ readonly sourceId: string;
2133
+ readonly plan: PyramidPlan;
2134
+ readonly chain: GPUTexture;
2135
+ readonly body: GPUTexture;
2136
+ readonly stats: GPUBuffer;
2137
+ /** Source size epoch this allocation was made for. */
2138
+ readonly sizeEpoch: number;
2139
+ /** The dirty epoch the last successful rebuild satisfied. */
2140
+ readonly builtEpoch: number;
2141
+ }
2142
+ interface PyramidInstrumentation {
2143
+ /** Successful rebuilds since the store was created. */
2144
+ readonly rebuilds: number;
2145
+ /** Rebuild attempts refused because the source had already rebuilt this frame. */
2146
+ readonly refusedDuplicates: number;
2147
+ /** Rebuilds skipped because the source was clean. */
2148
+ readonly skippedClean: number;
2149
+ readonly reallocations: number;
2150
+ /** Rebuild count for one source within the frame currently being recorded. */
2151
+ rebuildsInFrame(sourceId: string): number;
2152
+ /** Highest per-source rebuild count seen in any single frame. Must stay <= 1. */
2153
+ readonly peakRebuildsPerSourcePerFrame: number;
2154
+ }
2155
+ interface PyramidBuildRequest {
2156
+ readonly sourceId: string;
2157
+ readonly epoch: number;
2158
+ readonly resolution: ResolutionPolicyView;
2159
+ /**
2160
+ * The material's body blur, in **CSS px**, with the viewport it is measured
2161
+ * against.
2162
+ *
2163
+ * Not in source texels, because the conversion between the two is a property of
2164
+ * the frame that has not been acquired yet: a 3840-wide video behind a 390 px
2165
+ * viewport packs ten source texels into every CSS px, so a σ of 8 texels would
2166
+ * be a σ of 0.8 CSS px on screen — a tenth of the frost the material asked for.
2167
+ * The build resolves it once the frame's real extent is known.
2168
+ */
2169
+ readonly bodySigmaCss: number;
2170
+ readonly viewportCss: readonly [number, number];
2171
+ }
2172
+ type PyramidBuildOutcome = {
2173
+ readonly status: "built";
2174
+ readonly resources: PyramidResources;
2175
+ } | {
2176
+ readonly status: "duplicate";
2177
+ } | {
2178
+ readonly status: "clean";
2179
+ readonly resources: PyramidResources;
2180
+ } | {
2181
+ readonly status: "unavailable";
2182
+ readonly reason: string;
2183
+ };
2184
+ interface PyramidStore {
2185
+ readonly instrumentation: PyramidInstrumentation;
2186
+ /** The invariant's ledger, exposed so a test can drive it without a device. */
2187
+ readonly ledger: RebuildLedger;
2188
+ /** Start recording a new frame. Clears the per-frame rebuild tally. */
2189
+ beginFrame(frameId: number): void;
2190
+ /** Attach a timing collector for this frame, or `undefined` to time nothing. */
2191
+ setTimeline(timeline: PassTimeline | undefined): void;
2192
+ build(request: PyramidBuildRequest, provider: BackdropProvider, encoder: GPUCommandEncoder): PyramidBuildOutcome;
2193
+ /**
2194
+ * Release every provider acquired this frame.
2195
+ *
2196
+ * Split from `afterSubmit` because the two halves have opposite failure rules.
2197
+ * A release is owed whether or not the frame reached the queue — an acquired
2198
+ * `VideoFrame` held across a frame stalls decoding — so this belongs in a
2199
+ * `finally` around the whole encode/submit. Call it after `queue.submit` on the
2200
+ * success path: an imported external texture must outlive the submission that
2201
+ * samples it.
2202
+ */
2203
+ releaseAcquired(): void;
2204
+ /**
2205
+ * Start the analysis readback maps. **Success path only**: `mapAsync` makes a
2206
+ * buffer unavailable to submits from the moment it is called, so starting a map
2207
+ * for a copy that was never submitted is its own bug — see `requestStats`.
2208
+ */
2209
+ afterSubmit(): void;
2210
+ /**
2211
+ * The source's pyramid, or `undefined` when it has none the pool still owns.
2212
+ * A caller may bind what this returns without checking anything further.
2213
+ */
2214
+ resources(sourceId: string): PyramidResources | undefined;
2215
+ /** Copy a source's stats into a staging buffer and map it. Cadence-gated by the caller. */
2216
+ requestStats(sourceId: string, encoder: GPUCommandEncoder): boolean;
2217
+ /** Resolve any completed stats readbacks. Returns what arrived. */
2218
+ collectStats(): Promise<ReadonlyMap<string, BackdropStats>>;
2219
+ forget(sourceId: string): void;
2220
+ destroy(): void;
2221
+ }
2222
+
2223
+ /**
2224
+ * The renderer: device, resources, and the frame.
2225
+ *
2226
+ * Two entry points, deliberately:
2227
+ *
2228
+ * - **`drawFrame`** — draw one frame into two texture views. No scene, no
2229
+ * scheduler, no DOM. This is what the golden suite and the benchmark drive, and
2230
+ * what makes the optical maths testable without standing up the whole runtime.
2231
+ * - **`frameParticipant`** — the same work split across core's `write` and
2232
+ * `render` phases, so `vitrea`'s scheduler drives it alongside
2233
+ * platform-web's DOM participant. Pyramid rebuilds land in `write` because that
2234
+ * is the phase core hands them out in; drawing lands in `render`, with the graph
2235
+ * frozen.
2236
+ *
2237
+ * The participant reads the frame's `resolution` when core supplies one, and
2238
+ * prefers it over the group input's own `refraction`/`analysis` — core is the
2239
+ * authority on resolved state (X2), and it is the frame's resolution that has been
2240
+ * through the whole transition table. The group input's copies are the fallback for
2241
+ * `drawFrame`, where there is no core.
2242
+ *
2243
+ * ## Device generations
2244
+ *
2245
+ * Everything GPU-shaped hangs off a `GpuContext` tagged with the device generation
2246
+ * that made it. On loss the context is dropped whole, and the first frame on the
2247
+ * replacement re-points every registered provider at the new device and marks it
2248
+ * for re-import — so recovery flows through the ordinary
2249
+ * one-rebuild-per-dirty-source-per-frame path instead of a special case, which
2250
+ * means the recovery path is exercised by the same tests as the steady state.
2251
+ *
2252
+ * `ensureContext` is where that happens rather than the loss teardown, and the
2253
+ * reason is timing: the teardown runs while there is no device to adopt, and this
2254
+ * is the one place that knows both that a generation was superseded and what
2255
+ * replaced it. It also runs before anything acquires on the new device, which the
2256
+ * teardown cannot promise.
2257
+ *
2258
+ * The one provider that cannot be re-pointed is a backdrop over the *app's* own
2259
+ * texture: WebGPU has no cross-device sharing and the renderer does not own the
2260
+ * texture, so that provider refuses instead, and the refused rebuild is reported
2261
+ * through `unbuiltSources` for the host to act on.
2262
+ */
2263
+
2264
+ interface ViewportState {
2265
+ /** Viewport size in CSS px. */
2266
+ readonly widthCss: number;
2267
+ readonly heightCss: number;
2268
+ readonly devicePixelRatio: number;
2269
+ }
2270
+ interface RendererInstrumentation {
2271
+ readonly pyramid: PyramidStore["instrumentation"];
2272
+ readonly texturePool: {
2273
+ readonly live: number;
2274
+ readonly created: number;
2275
+ readonly destroyed: number;
2276
+ };
2277
+ readonly pipelines: {
2278
+ readonly renderPipelines: number;
2279
+ readonly computePipelines: number;
2280
+ };
2281
+ readonly framesDrawn: number;
2282
+ readonly deviceGenerations: number;
2283
+ }
2284
+ interface DrawFrameArgs {
2285
+ readonly frame: {
2286
+ readonly id: number;
2287
+ readonly timeMs: number;
2288
+ };
2289
+ /** The optics canvas' current texture view. */
2290
+ readonly optics: GPUTextureView;
2291
+ /** The highlight canvas' view. Omit to skip the highlight pass entirely. */
2292
+ readonly highlight?: GPUTextureView;
2293
+ readonly format?: GPUTextureFormat;
2294
+ /**
2295
+ * The rebuild requests core handed out. Omitted means "ask every registered
2296
+ * provider whether it is dirty", which is what `drawFrame` does standalone.
2297
+ */
2298
+ readonly rebuild?: readonly RebuildRequestView[];
2299
+ /** Core's resolved state for this frame. Overrides the group inputs' copies. */
2300
+ readonly resolution?: SceneResolutionView;
2301
+ readonly timing?: TimingCollector;
2302
+ /** Clear the targets first. Default true. */
2303
+ readonly clear?: boolean;
2304
+ }
2305
+ interface DrawFrameResult {
2306
+ readonly groupsDrawn: number;
2307
+ readonly rebuilds: number;
2308
+ readonly skipped: readonly {
2309
+ readonly groupId: string;
2310
+ readonly reason: string;
2311
+ }[];
2312
+ /**
2313
+ * Source ids core handed out a rebuild for that this frame did **not** build.
2314
+ * The same list as `GlassRenderer.unbuiltSources` — see it for what a caller
2315
+ * owes.
2316
+ */
2317
+ readonly unbuilt: readonly string[];
2318
+ }
2319
+ interface GlassRenderer {
2320
+ readonly backend: "webgpu";
2321
+ /** True only with a live, unlost device attached. */
2322
+ readonly ready: boolean;
2323
+ readonly passes: readonly string[];
2324
+ readonly shaderSource: string;
2325
+ readonly deviceStatus: RendererDeviceStatus;
2326
+ /** The facts a host folds into core's `PlatformProbe`. */
2327
+ readonly capabilityInput: DeviceCapabilityInput;
2328
+ readonly governor: Governor;
2329
+ readonly instrumentation: RendererInstrumentation;
2330
+ /**
2331
+ * The source ids the most recent frame was handed a rebuild for and did not
2332
+ * build — because no provider is registered under the id, or because the
2333
+ * provider could not serve a frame.
2334
+ *
2335
+ * This exists because core commits `builtEpoch` when it *hands out* the
2336
+ * request, not when the renderer finishes: a request dropped here is a claim
2337
+ * spent on nothing, and the source would sit clean at an epoch whose pixels
2338
+ * were never imported. Core has no view of this side of the wire, so the
2339
+ * renderer names the losses and the platform layer re-dirties them — one frame
2340
+ * of latency, and no new core surface.
2341
+ *
2342
+ * Accumulated across the frame's planes, not per `drawFrame` call: a host draws
2343
+ * one plane per call with the same frame id and hands the dirty set to the
2344
+ * first of them only, so a later plane's empty answer must not erase what the
2345
+ * first one found. Read after the frame's last plane, or after the
2346
+ * participant's `render` phase. Empty on every ordinary frame.
2347
+ */
2348
+ readonly unbuiltSources: readonly string[];
2349
+ attachDevice(device: GPUDevice, ownership?: DeviceOwnership): void;
2350
+ replaceDevice(device: GPUDevice): void;
2351
+ markWebGPUUnavailable(reason: "no-adapter" | "device-request-failed"): void;
2352
+ registerBackdrop(provider: BackdropProvider): void;
2353
+ unregisterBackdrop(sourceId: string): void;
2354
+ backdrop(sourceId: string): BackdropProvider | undefined;
2355
+ setViewport(viewport: ViewportState): void;
2356
+ readonly viewport: ViewportState;
2357
+ setGroup(input: GroupRenderInput): void;
2358
+ removeGroup(groupId: string): void;
2359
+ setAccessibility(policy: MaterialPolicyView): void;
2360
+ /**
2361
+ * Replace the optical tunables. The patch is applied to
2362
+ * `DEFAULT_MATERIAL_PROFILE`, never to the profile currently in force, so two
2363
+ * calls do not compound — a profile is a set of measurements, and half of one
2364
+ * measurement set over half of another describes no material.
2365
+ */
2366
+ setMaterialProfile(patch: MaterialProfilePatch): void;
2367
+ /** The tunables in force. Read-only; `setMaterialProfile` is the way in. */
2368
+ readonly materialProfile: MaterialProfile;
2369
+ drawFrame(args: DrawFrameArgs): DrawFrameResult;
2370
+ /** Targets for the participant path, where core drives the phases. */
2371
+ setTargets(targets: {
2372
+ readonly optics: GPUTextureView;
2373
+ readonly highlight?: GPUTextureView;
2374
+ readonly format?: GPUTextureFormat;
2375
+ }): void;
2376
+ frameParticipant(): FrameParticipantView;
2377
+ /** Resolve any completed analysis readbacks into the adaptation drivers. */
2378
+ collectAdaptation(): Promise<number>;
2379
+ destroy(): void;
2380
+ }
2381
+
2382
+ /**
2383
+ * X7 — the lazy renderer seam.
2384
+ *
2385
+ * The static import below is type-only, so it is erased at build time; the
2386
+ * dynamic import is the only edge from core to the WebGPU renderer. That keeps
2387
+ * every byte of WGSL out of core's entry chunk, so a CSS-tier consumer never
2388
+ * downloads a shader. `packages/core/test/bundle-shape.test.ts` asserts it on
2389
+ * the built artifact rather than trusting this comment.
2390
+ */
2391
+
2392
+ /**
2393
+ * The renderer's constructors, as the seam's consumers need them.
2394
+ *
2395
+ * A host that attaches the renderer to real canvases also has to build the
2396
+ * backdrop providers that feed it, and both must come through the *same* dynamic
2397
+ * import — a second entry edge would emit a second chunk and split the renderer
2398
+ * in two. Declared as an interface rather than `typeof import(...)` so the seam
2399
+ * states exactly what it promises, and the real module is checked against it.
2400
+ */
2401
+ interface WebGPURendererModule {
2402
+ createWebGPURenderer(): GlassRenderer;
2403
+ createCopyProvider(options: CopyProviderOptions): BackdropProvider;
2404
+ createVideoProvider(options: VideoProviderOptions): BackdropProvider;
2405
+ }
2406
+ /** Resolve the renderer module. Call only after a capability probe says yes. */
2407
+ declare function loadWebGPURendererModule(): Promise<WebGPURendererModule>;
2408
+ /** Resolve the WebGPU renderer. Call only after a capability probe says yes. */
2409
+ declare function loadWebGPURenderer(): Promise<GlassRenderer>;
2410
+
2411
+ /**
2412
+ * vitrea — the platform-free heart of the runtime.
2413
+ *
2414
+ * One of the two published packages (X7). The internal @vitrea/* packages are
2415
+ * bundled into this artifact at build time, so this package's published
2416
+ * dependency list is empty.
2417
+ *
2418
+ * Pure and passive: no DOM, no Node built-ins (X4), no timers and no clocks.
2419
+ * Every probe result, media-query answer and layout rect arrives as plain data;
2420
+ * @vitrea/platform-web owns the browser and drives the frames.
2421
+ *
2422
+ * The modules, roughly in dependency order:
2423
+ *
2424
+ * - `state`, `capability` — X2's resolved-state model and the transition table
2425
+ * that produces it: what the app configured versus what it actually got.
2426
+ * - `diagnostics` — how core reports what it will not silently fix.
2427
+ * - `planes`, `frame` — the z-slot vocabulary and the frame-phase vocabulary.
2428
+ * - `backdrop-hint` (X6), `foreground`, `material`, `accessibility` — the four
2429
+ * policy resolvers.
2430
+ * - `scene` — the three registries, their references, and the dirty-epoch
2431
+ * bookkeeping behind the one-rebuild-per-dirty-source-per-frame invariant.
2432
+ * - `scheduler` — the frame-phase contract plus a reference implementation.
2433
+ * - `renderer-seam` — the lazy edge to the WebGPU renderer (X7).
2434
+ */
2435
+
2436
+ /**
2437
+ * The contract sets core owns, re-exported so a consumer never installs an
2438
+ * internal package. Reading these values here is also what makes the internal
2439
+ * packages part of core's bundle — the property X7's bundle test checks.
2440
+ */
2441
+ declare const VITREA_CONTRACTS: {
2442
+ readonly shapeFamilies: readonly ShapeFamily[];
2443
+ readonly interactionStates: readonly InteractionState[];
2444
+ readonly motionDrivers: typeof MOTION_DRIVER_BY_CHANNEL;
2445
+ };
2446
+ /** Renderer tiers in v1's ladder. WebGL2 is out of scope; SVG displacement is a reserved seam. */
2447
+ declare const RENDERER_TIERS: readonly ["webgpu", "css"];
2448
+ type RendererTier = (typeof RENDERER_TIERS)[number];
2449
+
2450
+ export { ACCESSIBILITY_BEHAVIOR_TABLE, ACCESSIBILITY_FLAGS, ACCESSIBILITY_PRECEDENCE, type AccessibilityConsequences, type AccessibilityFlag, type AccessibilityOverride, type AccessibilityOverrides, type ActiveRenderer, type AnalysisQuality, type BackdropEstimatorProvider, type BackdropHint, type BackdropHintRequest, type BackdropProvider, type BackdropRebuildRequest, type BackdropResolutionPolicy, type BackdropSourceDescriptor, type BackdropSourceRecord, type BackdropTone, type CapabilityInputs, type ConfiguredSource, type CopyProviderOptions, type CornerProfile, type CornerRadii, DEFAULT_BACKDROP_RESOLUTION, DEFAULT_CLEAR_DIMMING, DEFAULT_GROUP_SAMPLING, DEMOTION_REASONS, DEMOTION_RECOVERY, DIAGNOSTIC_CODES, type DemotionReason, type DescriptorPatch, type Diagnostic, type DiagnosticCode, type DiagnosticSeverity, type DiagnosticSink, type DiagnosticsChannel, type DiagnosticsChannelOptions, type DimmingPolicy, type DomBackdropSource, FOREGROUND_MODES, FRAME_PHASES, type ForegroundAdaptation, type ForegroundMode, type ForegroundResolutionOptions, type FrameContext, type FrameInfo, type FrameParticipant, type FramePhase, type FrameReport, type FrameScheduler, type FrameSchedulerOptions, GLASS_PLANES, GOVERNOR_PRESSURES, type GlassGroupDescriptor, type GlassGroupRecord, type GlassGroupState, type GlassNodeDescriptor, type GlassNodeRecord, type GlassPlane, type GlassRenderer, type GlassScene, GlassSceneError, type GlassSceneErrorCode, type GlassSceneOptions, type GovernorPressure, type GroupHealth, type GroupSamplingGeometry, type GroupStateChange, HINT_AVAILABILITIES, type HintAvailability, type InteractionState, MATERIAL_VARIANTS$1 as MATERIAL_VARIANTS, type MaterialProfile$1 as MaterialProfile, type MaterialRequest, type MaterialVariant$1 as MaterialVariant, type MotionChannel, type MotionDriverKind, NOMINAL_ACCESSIBILITY_POLICY, OVERRIDABLE_ACCESSIBILITY_FLAGS, type OverridableAccessibilityFlag, type PlaneOverlap, type PlatformProbe, type ProxyOverlap, RENDERER_TIERS, type RecoveryContract, type RecoveryTrigger, type Rect, type RefractionQuality$1 as RefractionQuality, type RendererTier, type ResolvedAccessibilityPolicy, type ResolvedBackdropHint, type ResolvedForegroundAdaptation, type ResolvedGroup, type ResolvedMaterial, type ResolvedMaterialPolicy, type ResolvedMotionPolicy, type ResolvedNode, SAMPLED_ASYNC_DEFAULTS, SAMPLED_ASYNC_RATE_LIMITS, type SamplingBackend, type SceneResolution, type ShapeChannels, type ShapeFamily, type SourceProbe, type StateChange, type SystemAccessibilityPreferences, type TextureBackdropSource, VITREA_CONTRACTS, type VariantMixingCheck, type Vec2, type VideoProviderOptions, WEBGPU_AVAILABILITIES, type WebGPUAvailability$1 as WebGPUAvailability, type WebGPURendererModule, type ZSlot, checkVariantMixing, classifyStateChange, compareZSlot, createDiagnosticsChannel, createFrameScheduler, createGlassScene, defaultForegroundAdaptation, inflateRect, isHealthy, loadWebGPURenderer, loadWebGPURendererModule, rectsOverlap, resolveAccessibilityPolicy, resolveBackdropHint, resolveForegroundAdaptation, resolveGlassGroupState, resolveMaterial, unionRect };