@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.
- package/LICENSE +202 -0
- package/NOTICE +36 -0
- package/README.md +359 -0
- package/dist/chunk-ST6GFEQT.js +502 -0
- package/dist/chunk-ST6GFEQT.js.map +1 -0
- package/dist/dist-FGJI5LQM.js +3573 -0
- package/dist/dist-FGJI5LQM.js.map +1 -0
- package/dist/index.d.ts +2450 -0
- package/dist/index.js +1001 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|