@vitreajs/vitrea 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3573 @@
1
+ import { DEFAULT_GROUP_UNION, LowPassHysteresisDriver, WGSL_RSUPN, WGSL_RSUP, resolveFromChannels, resolveConcentric, governorFieldParams, fieldParams, DEFAULT_MOTION_PROFILE } from './chunk-ST6GFEQT.js';
2
+
3
+ // ../renderer-webgpu/dist/wgsl/analysis.js
4
+ var ANALYSIS_WORKGROUP = 64;
5
+ var ANALYSIS_GRID = 64;
6
+ var ANALYSIS_STATS_FLOATS = 4;
7
+ var WGSL_ANALYSIS_PASS = `struct AnalysisUniforms {
8
+ /// grid.xy = sample grid size, grid.z = source level, grid.w = 1/(grid-1)
9
+ grid : vec4f,
10
+ /// texel.xy = one texel of the sampled level, in uv
11
+ texel : vec4f,
12
+ };
13
+
14
+ @group(0) @binding(0) var<uniform> au : AnalysisUniforms;
15
+ @group(0) @binding(1) var analysisSampler : sampler;
16
+ @group(0) @binding(2) var analysisTexture : texture_2d<f32>;
17
+ @group(0) @binding(3) var<storage, read_write> stats : array<f32>;
18
+
19
+ var<workgroup> partialLum : array<f32, ${ANALYSIS_WORKGROUP}>;
20
+ var<workgroup> partialSq : array<f32, ${ANALYSIS_WORKGROUP}>;
21
+ var<workgroup> partialEdge : array<f32, ${ANALYSIS_WORKGROUP}>;
22
+
23
+ fn lum_at(uv : vec2f) -> f32 {
24
+ let s = textureSampleLevel(analysisTexture, analysisSampler, uv, au.grid.z);
25
+ // Level 0 onwards is premultiplied linear; unpremultiplying keeps a
26
+ // transparent backdrop region from reading as black rather than as absent.
27
+ let colour = s.rgb / max(s.a, 1e-6);
28
+ return luminance(colour);
29
+ }
30
+
31
+ @compute @workgroup_size(${ANALYSIS_WORKGROUP})
32
+ fn cs_analysis(@builtin(local_invocation_index) lane : u32) {
33
+ let total = u32(au.grid.x * au.grid.y);
34
+ var sumLum = 0.0;
35
+ var sumSq = 0.0;
36
+ var sumEdge = 0.0;
37
+ var n = 0.0;
38
+
39
+ var i = lane;
40
+ loop {
41
+ if (i >= total) { break; }
42
+ let gx = f32(i % u32(au.grid.x));
43
+ let gy = f32(i / u32(au.grid.x));
44
+ let uv = vec2f(gx, gy) * au.grid.w;
45
+
46
+ let c = lum_at(uv);
47
+ let dx = lum_at(uv + vec2f(au.texel.x, 0.0)) - lum_at(uv - vec2f(au.texel.x, 0.0));
48
+ let dy = lum_at(uv + vec2f(0.0, au.texel.y)) - lum_at(uv - vec2f(0.0, au.texel.y));
49
+
50
+ sumLum = sumLum + c;
51
+ sumSq = sumSq + c * c;
52
+ sumEdge = sumEdge + length(vec2f(dx, dy)) * 0.5;
53
+ n = n + 1.0;
54
+
55
+ i = i + ${ANALYSIS_WORKGROUP}u;
56
+ }
57
+
58
+ partialLum[lane] = sumLum;
59
+ partialSq[lane] = sumSq;
60
+ partialEdge[lane] = sumEdge;
61
+ workgroupBarrier();
62
+
63
+ // Tree reduction. ${ANALYSIS_WORKGROUP} is a power of two, so no tail case.
64
+ var stride = ${ANALYSIS_WORKGROUP}u / 2u;
65
+ loop {
66
+ if (stride == 0u) { break; }
67
+ if (lane < stride) {
68
+ partialLum[lane] = partialLum[lane] + partialLum[lane + stride];
69
+ partialSq[lane] = partialSq[lane] + partialSq[lane + stride];
70
+ partialEdge[lane] = partialEdge[lane] + partialEdge[lane + stride];
71
+ }
72
+ workgroupBarrier();
73
+ stride = stride / 2u;
74
+ }
75
+
76
+ if (lane == 0u) {
77
+ let count = max(f32(total), 1.0);
78
+ let mean = partialLum[0] / count;
79
+ stats[0] = mean;
80
+ stats[1] = max(partialSq[0] / count - mean * mean, 0.0);
81
+ stats[2] = partialEdge[0] / count;
82
+ stats[3] = count;
83
+ }
84
+ }`;
85
+
86
+ // ../renderer-webgpu/dist/color.js
87
+ var BACKDROP_COLOR_SPACES = ["srgb", "display-p3"];
88
+ var BACKDROP_ALPHA_MODES = ["premultiplied", "unpremultiplied", "opaque"];
89
+ var WORKING_TEXTURE_FORMAT = "rgba16float";
90
+ var OUTPUT_TEXTURE_FORMAT = "rgba8unorm";
91
+ var SRGB_LINEAR_CUTOFF = 31308e-7;
92
+ var SRGB_ENCODED_CUTOFF = 0.04045;
93
+ var SRGB_SLOPE = 12.92;
94
+ var SRGB_ALPHA = 1.055;
95
+ var SRGB_OFFSET = 0.055;
96
+ var SRGB_GAMMA = 2.4;
97
+ function srgbToLinearChannel(c) {
98
+ if (c <= SRGB_ENCODED_CUTOFF)
99
+ return c / SRGB_SLOPE;
100
+ return Math.pow((c + SRGB_OFFSET) / SRGB_ALPHA, SRGB_GAMMA);
101
+ }
102
+ function linearToSrgbChannel(c) {
103
+ if (c <= SRGB_LINEAR_CUTOFF)
104
+ return c * SRGB_SLOPE;
105
+ return SRGB_ALPHA * Math.pow(c, 1 / SRGB_GAMMA) - SRGB_OFFSET;
106
+ }
107
+ var srgbToLinear = (c) => [
108
+ srgbToLinearChannel(c[0]),
109
+ srgbToLinearChannel(c[1]),
110
+ srgbToLinearChannel(c[2])
111
+ ];
112
+ var linearToSrgb = (c) => [
113
+ linearToSrgbChannel(c[0]),
114
+ linearToSrgbChannel(c[1]),
115
+ linearToSrgbChannel(c[2])
116
+ ];
117
+ var LUMINANCE_WEIGHTS = [0.2126, 0.7152, 0.0722];
118
+ function relativeLuminance(linear) {
119
+ return linear[0] * LUMINANCE_WEIGHTS[0] + linear[1] * LUMINANCE_WEIGHTS[1] + linear[2] * LUMINANCE_WEIGHTS[2];
120
+ }
121
+ var P3_TO_SRGB = [
122
+ [1.224940176280559, -0.224940176280559, 0],
123
+ [-0.042056973821316, 1.042056973821316, 0],
124
+ [-0.019637554590334, -0.078636046901105, 1.098273601491439]
125
+ ];
126
+ function displayP3ToSrgbLinear(linearP3) {
127
+ const out = [];
128
+ for (const row of P3_TO_SRGB) {
129
+ out.push(Math.min(1, Math.max(0, row[0] * linearP3[0] + row[1] * linearP3[1] + row[2] * linearP3[2])));
130
+ }
131
+ return [out[0], out[1], out[2]];
132
+ }
133
+ function importColorMatrix(space) {
134
+ if (space === "srgb") {
135
+ return new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
136
+ }
137
+ const rows = P3_TO_SRGB;
138
+ return new Float32Array([
139
+ ...rows[0],
140
+ ...rows[1],
141
+ ...rows[2]
142
+ ]);
143
+ }
144
+ function alphaNormalisationMode(mode) {
145
+ switch (mode) {
146
+ case "premultiplied":
147
+ return 0;
148
+ case "unpremultiplied":
149
+ return 1;
150
+ case "opaque":
151
+ return 2;
152
+ }
153
+ }
154
+ function encodeOutput(linear, alpha) {
155
+ const a = Math.min(1, Math.max(0, alpha));
156
+ const encoded = linearToSrgb(linear);
157
+ return [encoded[0] * a, encoded[1] * a, encoded[2] * a, a];
158
+ }
159
+ function encodeOutputBytes(linear, alpha) {
160
+ return encodeOutput(linear, alpha).map((c) => Math.round(Math.min(1, Math.max(0, c)) * 255));
161
+ }
162
+
163
+ // ../renderer-webgpu/dist/material.js
164
+ var REFRACTION_LADDER = ["none", "approximate", "true"];
165
+ function refractionRank(quality) {
166
+ return REFRACTION_LADDER.indexOf(quality);
167
+ }
168
+ function accessibilityRefractionCap(policy) {
169
+ switch (policy.refraction) {
170
+ case "nominal":
171
+ return "true";
172
+ case "reduced":
173
+ return "approximate";
174
+ case "none":
175
+ return "none";
176
+ }
177
+ }
178
+ function effectiveRefraction(a, b) {
179
+ return refractionRank(a) <= refractionRank(b) ? a : b;
180
+ }
181
+ var MATERIAL_VARIANTS = ["regular", "clear"];
182
+ var SRGB_WHITE_TINT = [1, 1, 1];
183
+ var INCREASED_OCCLUSION_LIFT = 0.4722;
184
+ function occlusionAlphaUnderPolicy(nominal, occlusion, lift = INCREASED_OCCLUSION_LIFT) {
185
+ switch (occlusion) {
186
+ case "nominal":
187
+ return nominal;
188
+ case "increased":
189
+ return nominal + lift * (1 - nominal);
190
+ case "opaque":
191
+ return 1;
192
+ }
193
+ }
194
+ var DEFAULT_MATERIAL_PROFILE = {
195
+ optics: {
196
+ // σ = 8 for the regular variant, which keeps this package's blur and
197
+ // `platform-web`'s CSS-tier blur on the same number — and makes core's 24 px
198
+ // `samplingPadding` advisory exactly the 3σ S1 measured.
199
+ regular: {
200
+ blurSigma: 8,
201
+ tint: srgbToLinear(SRGB_WHITE_TINT),
202
+ // MEASURED (C9a), against apple-macos-26.5-1x-light-standard. The advisory
203
+ // 0.28 made vitrea's regular material roughly half as opaque as the
204
+ // reference: regressing interior level against backdrop level across the
205
+ // calibration scenes puts Apple's transmission at 0.26 of the backdrop
206
+ // where vitrea's was 0.70. 0.62 is the minimiser of the declared tuning
207
+ // objective; see docs/doperpowers/specs/c9a-fidelity-claims.md, and note
208
+ // that no single value can be right for every size — Apple's opacity falls
209
+ // with surface span (0.88 at 32 px to 0.56 at 96 px) and this renderer has
210
+ // no size term on the tint.
211
+ tintAlpha: 0.62,
212
+ rimWidth: 1.5,
213
+ rimAlpha: 0.18,
214
+ specularPower: 6,
215
+ specularGain: 0.55,
216
+ shadowDepth: 0.35,
217
+ shadowAlpha: 0.55,
218
+ highlight: srgbToLinear(SRGB_WHITE_TINT)
219
+ },
220
+ // Persistently more transparent, so it frosts less and tints less.
221
+ clear: {
222
+ blurSigma: 4,
223
+ tint: srgbToLinear(SRGB_WHITE_TINT),
224
+ tintAlpha: 0.1,
225
+ rimWidth: 1.25,
226
+ rimAlpha: 0.14,
227
+ specularPower: 8,
228
+ specularGain: 0.45,
229
+ shadowDepth: 0.22,
230
+ shadowAlpha: 0.4,
231
+ highlight: srgbToLinear(SRGB_WHITE_TINT)
232
+ }
233
+ },
234
+ /*
235
+ * MEASURED (C9a): both ends are the same tint, which makes the crossover inert
236
+ * by default. That is a finding, not a shortcut.
237
+ *
238
+ * The two ends used to straddle the backdrop — white over a dark backdrop,
239
+ * near-black over a light one — so the material always contrasted with what was
240
+ * behind it. Apple's Regular material does not do that. Its interior rises
241
+ * monotonically with the backdrop across the whole canonical range (0.680 at a
242
+ * backdrop of 0.003, 0.932 at 0.891, light scheme), which is a fixed tint at
243
+ * partial transmission. What it keys on instead is the COLOUR SCHEME: over the
244
+ * same bright checkerboard the reference sits at 0.809 in light and 0.055 in
245
+ * dark. Leaving the inversion on cost more than the whole tint tune was worth —
246
+ * it drove the light-scheme error from 0.348 to 0.449 on the interior-level
247
+ * term alone.
248
+ *
249
+ * So the scheme picks the tint, and the calibration profiles carry one set of
250
+ * numbers per scheme (packages/calibration/profiles/). The crossover mechanism
251
+ * is untouched and still available to a profile that wants it; the default
252
+ * simply no longer claims a behaviour the reference does not have.
253
+ *
254
+ * Two limits worth naming. The ends are global rather than per-variant, so the
255
+ * clear variant inherits this — and clear has no calibration scenes at all, so
256
+ * it is uncalibrated either way. And nothing yet selects a profile from the
257
+ * scheme; that is C9a's parent-impact item.
258
+ */
259
+ adaptiveTintDark: srgbToLinear(SRGB_WHITE_TINT),
260
+ adaptiveTintLight: srgbToLinear(SRGB_WHITE_TINT),
261
+ adaptiveLuminanceLow: 0.12,
262
+ adaptiveLuminanceHigh: 0.42,
263
+ refractionScale: {
264
+ none: 0,
265
+ approximate: 0.45,
266
+ true: 1
267
+ },
268
+ lensSpanMin: 28,
269
+ lensSpanMax: 420,
270
+ lensSizeGainMax: 2.6,
271
+ lensBodyLodPerPx: 0.16,
272
+ lensRimLodBias: 2.5,
273
+ reducedTransparencyFrost: 1.75,
274
+ increasedOcclusionLift: INCREASED_OCCLUSION_LIFT,
275
+ strongBorderRim: { rimWidth: 2, rimAlpha: 0.95 },
276
+ reducedTintAdaptation: 0.35,
277
+ lightDirection: [-0.3714, -0.9285],
278
+ sweepBandRadians: 0.55,
279
+ glowRadiusCss: 44,
280
+ glowGain: 0.6,
281
+ sweepGain: 0.85
282
+ };
283
+ var REFRACTION_SCALE = DEFAULT_MATERIAL_PROFILE.refractionScale;
284
+ var MATERIAL_OPTICS = DEFAULT_MATERIAL_PROFILE.optics;
285
+ var ADAPTIVE_TINT_DARK = DEFAULT_MATERIAL_PROFILE.adaptiveTintDark;
286
+ var ADAPTIVE_TINT_LIGHT = DEFAULT_MATERIAL_PROFILE.adaptiveTintLight;
287
+ var ADAPTIVE_LUMINANCE_LOW = DEFAULT_MATERIAL_PROFILE.adaptiveLuminanceLow;
288
+ var ADAPTIVE_LUMINANCE_HIGH = DEFAULT_MATERIAL_PROFILE.adaptiveLuminanceHigh;
289
+ var LENS_SPAN_MIN = DEFAULT_MATERIAL_PROFILE.lensSpanMin;
290
+ var LENS_SPAN_MAX = DEFAULT_MATERIAL_PROFILE.lensSpanMax;
291
+ var LENS_SIZE_GAIN_MAX = DEFAULT_MATERIAL_PROFILE.lensSizeGainMax;
292
+ var LENS_BODY_LOD_PER_PX = DEFAULT_MATERIAL_PROFILE.lensBodyLodPerPx;
293
+ var LENS_RIM_LOD_BIAS = DEFAULT_MATERIAL_PROFILE.lensRimLodBias;
294
+ function withMaterialOverrides(base, patch) {
295
+ const optics = {};
296
+ for (const variant of MATERIAL_VARIANTS) {
297
+ optics[variant] = { ...base.optics[variant], ...patch.optics?.[variant] };
298
+ }
299
+ const refractionScale = {};
300
+ for (const rung of REFRACTION_LADDER) {
301
+ refractionScale[rung] = patch.refractionScale?.[rung] ?? base.refractionScale[rung];
302
+ }
303
+ return {
304
+ optics,
305
+ adaptiveTintDark: patch.adaptiveTintDark ?? base.adaptiveTintDark,
306
+ adaptiveTintLight: patch.adaptiveTintLight ?? base.adaptiveTintLight,
307
+ adaptiveLuminanceLow: patch.adaptiveLuminanceLow ?? base.adaptiveLuminanceLow,
308
+ adaptiveLuminanceHigh: patch.adaptiveLuminanceHigh ?? base.adaptiveLuminanceHigh,
309
+ refractionScale,
310
+ lensSpanMin: patch.lensSpanMin ?? base.lensSpanMin,
311
+ lensSpanMax: patch.lensSpanMax ?? base.lensSpanMax,
312
+ lensSizeGainMax: patch.lensSizeGainMax ?? base.lensSizeGainMax,
313
+ lensBodyLodPerPx: patch.lensBodyLodPerPx ?? base.lensBodyLodPerPx,
314
+ lensRimLodBias: patch.lensRimLodBias ?? base.lensRimLodBias,
315
+ reducedTransparencyFrost: patch.reducedTransparencyFrost ?? base.reducedTransparencyFrost,
316
+ increasedOcclusionLift: patch.increasedOcclusionLift ?? base.increasedOcclusionLift,
317
+ strongBorderRim: { ...base.strongBorderRim, ...patch.strongBorderRim },
318
+ reducedTintAdaptation: patch.reducedTintAdaptation ?? base.reducedTintAdaptation,
319
+ lightDirection: patch.lightDirection ?? base.lightDirection,
320
+ sweepBandRadians: patch.sweepBandRadians ?? base.sweepBandRadians,
321
+ glowRadiusCss: patch.glowRadiusCss ?? base.glowRadiusCss,
322
+ glowGain: patch.glowGain ?? base.glowGain,
323
+ sweepGain: patch.sweepGain ?? base.sweepGain
324
+ };
325
+ }
326
+ function opticsUnderPolicy(optics, policy, profile = DEFAULT_MATERIAL_PROFILE) {
327
+ let next = optics;
328
+ if (policy.frost === "increased") {
329
+ next = { ...next, blurSigma: next.blurSigma * profile.reducedTransparencyFrost };
330
+ } else if (policy.frost === "none") {
331
+ next = { ...next, blurSigma: 0 };
332
+ }
333
+ next = {
334
+ ...next,
335
+ tintAlpha: occlusionAlphaUnderPolicy(next.tintAlpha, policy.occlusion, profile.increasedOcclusionLift)
336
+ };
337
+ if (policy.border === "strong")
338
+ next = { ...next, ...profile.strongBorderRim };
339
+ return next;
340
+ }
341
+ function adaptationStrength(policy, analysisExact, profile = DEFAULT_MATERIAL_PROFILE) {
342
+ if (!analysisExact)
343
+ return 0;
344
+ switch (policy.ambientTint) {
345
+ case "nominal":
346
+ return 1;
347
+ case "reduced":
348
+ return profile.reducedTintAdaptation;
349
+ case "none":
350
+ return 0;
351
+ }
352
+ }
353
+ var smoothstep = (edge0, edge1, x) => {
354
+ if (edge1 <= edge0)
355
+ return x < edge0 ? 0 : 1;
356
+ const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
357
+ return t * t * (3 - 2 * t);
358
+ };
359
+ function lensSizeGain(spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
360
+ return 1 + (profile.lensSizeGainMax - 1) * smoothstep(profile.lensSpanMin, profile.lensSpanMax, spanPx);
361
+ }
362
+ function lensDepthPx(thicknessPx, spanPx, profile = DEFAULT_MATERIAL_PROFILE) {
363
+ const gain = lensSizeGain(spanPx, profile);
364
+ return Math.min(Math.max(thicknessPx, 0) * gain, spanPx * 0.5);
365
+ }
366
+ function bodyLod(lensDepth, maxLod, profile = DEFAULT_MATERIAL_PROFILE) {
367
+ return Math.min(Math.max(lensDepth * profile.lensBodyLodPerPx, 0), maxLod);
368
+ }
369
+
370
+ // ../renderer-webgpu/dist/analysis.js
371
+ var ZERO_STATS = {
372
+ luminance: 0,
373
+ variance: 0,
374
+ edgeDensity: 0,
375
+ sampleCount: 0
376
+ };
377
+ function statsFromBuffer(values) {
378
+ return {
379
+ luminance: values[0] ?? 0,
380
+ variance: values[1] ?? 0,
381
+ edgeDensity: values[2] ?? 0,
382
+ sampleCount: values[3] ?? 0
383
+ };
384
+ }
385
+ var LUMINANCE_CONFIG = {
386
+ kind: "low-pass-hysteresis",
387
+ timeConstantMs: 500,
388
+ hysteresis: 0.04,
389
+ restDistance: 1e-3
390
+ };
391
+ var VARIANCE_CONFIG = { ...LUMINANCE_CONFIG, hysteresis: 0.01 };
392
+ var EDGE_CONFIG = { ...LUMINANCE_CONFIG, hysteresis: 0.01 };
393
+ var smoothstep2 = (edge0, edge1, x) => {
394
+ if (edge1 <= edge0)
395
+ return x < edge0 ? 0 : 1;
396
+ const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));
397
+ return t * t * (3 - 2 * t);
398
+ };
399
+ function adaptiveTint(luminance, profile = DEFAULT_MATERIAL_PROFILE) {
400
+ const dark = profile.adaptiveTintDark;
401
+ const light = profile.adaptiveTintLight;
402
+ const t = smoothstep2(profile.adaptiveLuminanceLow, profile.adaptiveLuminanceHigh, luminance);
403
+ return [
404
+ dark[0] + (light[0] - dark[0]) * t,
405
+ dark[1] + (light[1] - dark[1]) * t,
406
+ dark[2] + (light[2] - dark[2]) * t
407
+ ];
408
+ }
409
+ function createAdaptationState(initial = ZERO_STATS, profile = DEFAULT_MATERIAL_PROFILE) {
410
+ const luminance = new LowPassHysteresisDriver(LUMINANCE_CONFIG, initial.luminance);
411
+ const variance = new LowPassHysteresisDriver(VARIANCE_CONFIG, initial.variance);
412
+ const edge = new LowPassHysteresisDriver(EDGE_CONFIG, initial.edgeDensity);
413
+ let observed = initial.sampleCount > 0;
414
+ return {
415
+ observe(stats) {
416
+ if (stats.sampleCount <= 0)
417
+ return;
418
+ observed = true;
419
+ luminance.retarget(stats.luminance);
420
+ variance.retarget(stats.variance);
421
+ edge.retarget(stats.edgeDensity);
422
+ },
423
+ advance(deltaMs) {
424
+ luminance.advance(deltaMs);
425
+ variance.advance(deltaMs);
426
+ edge.advance(deltaMs);
427
+ },
428
+ reset(stats) {
429
+ observed = stats.sampleCount > 0;
430
+ luminance.jumpTo(stats.luminance);
431
+ variance.jumpTo(stats.variance);
432
+ edge.jumpTo(stats.edgeDensity);
433
+ },
434
+ get values() {
435
+ return {
436
+ luminance: luminance.value,
437
+ variance: variance.value,
438
+ edgeDensity: edge.value,
439
+ tint: adaptiveTint(luminance.value, profile),
440
+ observed
441
+ };
442
+ },
443
+ get settled() {
444
+ return luminance.settled && variance.settled && edge.settled;
445
+ }
446
+ };
447
+ }
448
+ function readbackDue(lastAtMs, nowMs, cadenceHz) {
449
+ if (cadenceHz <= 0)
450
+ return false;
451
+ if (lastAtMs === void 0)
452
+ return true;
453
+ return nowMs - lastAtMs >= 1e3 / cadenceHz;
454
+ }
455
+
456
+ // ../renderer-webgpu/dist/errors.js
457
+ var RENDERER_ERROR_CODES = [
458
+ /** A `GPUTextureView`'s backing texture lacks a required usage flag. */
459
+ "texture-usage",
460
+ /** Format outside the set the sampling path can read. */
461
+ "texture-format",
462
+ /** Wrong view dimension, or a depth/array layer count the pass cannot bind. */
463
+ "texture-dimension",
464
+ /** Zero or absurd extent, or a size that exceeds the device's limits. */
465
+ "texture-size",
466
+ /** A source id registered twice, or an unknown id referenced. */
467
+ "source-identity",
468
+ /** The provider was asked for a frame it cannot produce (closed video, detached canvas). */
469
+ "source-unavailable",
470
+ /** No device is attached, or the attached one is lost. */
471
+ "device-unavailable",
472
+ /** A frame was acquired twice without a release, or released without an acquire. */
473
+ "frame-protocol",
474
+ /** A pass was asked to draw something the current resources cannot express. */
475
+ "pass-input"
476
+ ];
477
+ var RendererError = class extends Error {
478
+ code;
479
+ /** The source, group or node the failure is about, when there is one. */
480
+ subject;
481
+ constructor(code, message, subject) {
482
+ super(message);
483
+ this.name = "RendererError";
484
+ this.code = code;
485
+ this.subject = subject;
486
+ }
487
+ };
488
+ function rendererError(code, message, subject) {
489
+ return new RendererError(code, message, subject);
490
+ }
491
+
492
+ // ../renderer-webgpu/dist/backdrop.js
493
+ var BACKDROP_KINDS = [
494
+ "image",
495
+ "video",
496
+ "canvas",
497
+ "gradient",
498
+ "app-texture-view"
499
+ ];
500
+ var SUPPORTED_APP_TEXTURE_FORMATS = [
501
+ "rgba8unorm",
502
+ "rgba8unorm-srgb",
503
+ "bgra8unorm",
504
+ "bgra8unorm-srgb",
505
+ "rgba16float",
506
+ "rgba32float"
507
+ ];
508
+ var copyUsage = () => GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT;
509
+ function trackSize(tracker, width, height) {
510
+ if (tracker.width === width && tracker.height === height)
511
+ return;
512
+ tracker.width = width;
513
+ tracker.height = height;
514
+ tracker.epoch += 1;
515
+ }
516
+ function createCopyProvider(options) {
517
+ let device = options.device;
518
+ const size = { epoch: 0, width: options.width, height: options.height };
519
+ const live = options.live ?? options.kind === "canvas";
520
+ let dirty = true;
521
+ let texture;
522
+ let generation = options.generation ?? 0;
523
+ const ensureTexture = () => {
524
+ if (texture !== void 0 && texture.width === size.width && texture.height === size.height) {
525
+ return texture;
526
+ }
527
+ texture?.destroy();
528
+ texture = device.createTexture({
529
+ label: `vitrea:backdrop:${options.id}:upload`,
530
+ size: { width: size.width, height: size.height, depthOrArrayLayers: 1 },
531
+ // 8-bit is the honest storage for an image or a canvas: both are 8-bit
532
+ // sources, and the import pass promotes to float on the way into level 0.
533
+ format: "rgba8unorm",
534
+ usage: copyUsage()
535
+ });
536
+ return texture;
537
+ };
538
+ return {
539
+ id: options.id,
540
+ kind: options.kind,
541
+ get generation() {
542
+ return generation;
543
+ },
544
+ isDirty() {
545
+ return dirty || live;
546
+ },
547
+ acquire() {
548
+ if (size.width <= 0 || size.height <= 0) {
549
+ throw rendererError("source-unavailable", `Backdrop "${options.id}" has a zero extent (${size.width}\xD7${size.height}); nothing can be copied from it.`, options.id);
550
+ }
551
+ const target = ensureTexture();
552
+ if (dirty || live) {
553
+ device.queue.copyExternalImageToTexture({ source: options.source, flipY: false }, {
554
+ texture: target,
555
+ colorSpace: options.colorSpace ?? "srgb",
556
+ // X3: copied images arrive premultiplied. Declaring it here is what
557
+ // makes that true rather than assumed — the copy performs the multiply
558
+ // when the source is not already premultiplied.
559
+ premultipliedAlpha: true
560
+ }, { width: size.width, height: size.height });
561
+ }
562
+ return {
563
+ sourceId: options.id,
564
+ binding: { kind: "sampled", view: target.createView() },
565
+ width: size.width,
566
+ height: size.height,
567
+ sizeEpoch: size.epoch,
568
+ colorSpace: options.colorSpace ?? "srgb",
569
+ alphaMode: "premultiplied",
570
+ encoded: true
571
+ };
572
+ },
573
+ release() {
574
+ },
575
+ markImported() {
576
+ dirty = false;
577
+ },
578
+ invalidate(next, nextDevice) {
579
+ generation = next;
580
+ device = nextDevice;
581
+ texture?.destroy();
582
+ texture = void 0;
583
+ dirty = true;
584
+ },
585
+ resize(width, height) {
586
+ const changed = size.width !== width || size.height !== height;
587
+ trackSize(size, width, height);
588
+ if (changed)
589
+ dirty = true;
590
+ },
591
+ destroy() {
592
+ texture?.destroy();
593
+ texture = void 0;
594
+ }
595
+ };
596
+ }
597
+ function createVideoProvider(options) {
598
+ let device = options.device;
599
+ const size = { epoch: 0, width: 0, height: 0 };
600
+ let ownedFrame;
601
+ let acquired = false;
602
+ let generation = options.generation ?? 0;
603
+ return {
604
+ id: options.id,
605
+ kind: "video",
606
+ get generation() {
607
+ return generation;
608
+ },
609
+ isDirty() {
610
+ return true;
611
+ },
612
+ acquire() {
613
+ if (acquired) {
614
+ throw rendererError("frame-protocol", `Backdrop "${options.id}" was acquired twice without a release. X3 pairs one acquire per frame with one release after submit.`, options.id);
615
+ }
616
+ let source;
617
+ if (options.source.kind === "element") {
618
+ const element = options.source.element;
619
+ if (element.readyState < 2 || element.videoWidth === 0) {
620
+ throw rendererError("source-unavailable", `Backdrop "${options.id}" is a video with no decoded frame yet (readyState ${element.readyState}). Wait for "loadeddata" before registering it, or let the group render without a backdrop until it arrives.`, options.id);
621
+ }
622
+ trackSize(size, element.videoWidth, element.videoHeight);
623
+ source = element;
624
+ } else {
625
+ const frame = options.source.next();
626
+ if (frame === void 0) {
627
+ throw rendererError("source-unavailable", `Backdrop "${options.id}" produced no VideoFrame for this frame.`, options.id);
628
+ }
629
+ ownedFrame = frame;
630
+ trackSize(size, frame.displayWidth, frame.displayHeight);
631
+ source = frame;
632
+ }
633
+ let texture;
634
+ try {
635
+ texture = device.importExternalTexture({
636
+ label: `vitrea:backdrop:${options.id}:external`,
637
+ source,
638
+ colorSpace: "srgb"
639
+ });
640
+ } catch (error) {
641
+ ownedFrame?.close();
642
+ ownedFrame = void 0;
643
+ throw error;
644
+ }
645
+ acquired = true;
646
+ return {
647
+ sourceId: options.id,
648
+ binding: { kind: "external", texture },
649
+ width: size.width,
650
+ height: size.height,
651
+ sizeEpoch: size.epoch,
652
+ colorSpace: options.colorSpace ?? "srgb",
653
+ // X3: imported video arrives UNPREMULTIPLIED.
654
+ alphaMode: "unpremultiplied",
655
+ encoded: true
656
+ };
657
+ },
658
+ release() {
659
+ acquired = false;
660
+ ownedFrame?.close();
661
+ ownedFrame = void 0;
662
+ },
663
+ markImported() {
664
+ },
665
+ invalidate(next, nextDevice) {
666
+ generation = next;
667
+ device = nextDevice;
668
+ ownedFrame?.close();
669
+ ownedFrame = void 0;
670
+ acquired = false;
671
+ },
672
+ destroy() {
673
+ ownedFrame?.close();
674
+ ownedFrame = void 0;
675
+ acquired = false;
676
+ }
677
+ };
678
+ }
679
+ function createGradientProvider(options) {
680
+ const width = Math.max(1, options.width ?? 64);
681
+ const height = Math.max(1, options.height ?? 64);
682
+ const direction = options.direction ?? [0, 1];
683
+ const stops = [...options.stops].sort((a, b) => a.offset - b.offset);
684
+ if (stops.length === 0) {
685
+ throw rendererError("source-unavailable", `Gradient backdrop "${options.id}" has no stops.`, options.id);
686
+ }
687
+ const texels = new Uint8Array(width * height * 4);
688
+ const axisLength = Math.hypot(direction[0], direction[1]) || 1;
689
+ const ax = direction[0] / axisLength;
690
+ const ay = direction[1] / axisLength;
691
+ const sample = (t) => {
692
+ const clamped = Math.min(1, Math.max(0, t));
693
+ let lower = stops[0];
694
+ let upper = stops[stops.length - 1];
695
+ for (let i = 0; i < stops.length - 1; i += 1) {
696
+ const a = stops[i];
697
+ const b = stops[i + 1];
698
+ if (clamped >= a.offset && clamped <= b.offset) {
699
+ lower = a;
700
+ upper = b;
701
+ break;
702
+ }
703
+ }
704
+ const span = upper.offset - lower.offset;
705
+ const local = span <= 0 ? 0 : (clamped - lower.offset) / span;
706
+ const mix = (from, to) => from + (to - from) * local;
707
+ const channel = (index) => linearToSrgbChannel(mix(srgbToLinearChannel(lower.color[index]), srgbToLinearChannel(upper.color[index])));
708
+ return [channel(0), channel(1), channel(2), mix(lower.color[3], upper.color[3])];
709
+ };
710
+ for (let y = 0; y < height; y += 1) {
711
+ for (let x = 0; x < width; x += 1) {
712
+ const u = width === 1 ? 0 : x / (width - 1);
713
+ const v = height === 1 ? 0 : y / (height - 1);
714
+ const t = (u * ax + v * ay + Math.max(0, -ax) + Math.max(0, -ay)) / (Math.abs(ax) + Math.abs(ay) || 1);
715
+ const [r, g, b, a] = sample(t);
716
+ const index = (y * width + x) * 4;
717
+ texels[index + 0] = Math.round(Math.min(1, Math.max(0, r)) * 255);
718
+ texels[index + 1] = Math.round(Math.min(1, Math.max(0, g)) * 255);
719
+ texels[index + 2] = Math.round(Math.min(1, Math.max(0, b)) * 255);
720
+ texels[index + 3] = Math.round(Math.min(1, Math.max(0, a)) * 255);
721
+ }
722
+ }
723
+ let texture;
724
+ let uploaded = false;
725
+ let device = options.device;
726
+ let generation = options.generation ?? 0;
727
+ return {
728
+ id: options.id,
729
+ kind: "gradient",
730
+ get generation() {
731
+ return generation;
732
+ },
733
+ isDirty() {
734
+ return !uploaded;
735
+ },
736
+ acquire() {
737
+ texture ??= device.createTexture({
738
+ label: `vitrea:backdrop:${options.id}:gradient`,
739
+ size: { width, height, depthOrArrayLayers: 1 },
740
+ format: "rgba8unorm",
741
+ usage: copyUsage()
742
+ });
743
+ if (!uploaded) {
744
+ device.queue.writeTexture({ texture }, texels, { bytesPerRow: width * 4, rowsPerImage: height }, { width, height });
745
+ }
746
+ return {
747
+ sourceId: options.id,
748
+ binding: { kind: "sampled", view: texture.createView() },
749
+ width,
750
+ height,
751
+ sizeEpoch: 0,
752
+ colorSpace: "srgb",
753
+ alphaMode: "premultiplied",
754
+ encoded: true
755
+ };
756
+ },
757
+ release() {
758
+ },
759
+ markImported() {
760
+ uploaded = true;
761
+ },
762
+ invalidate(next, nextDevice) {
763
+ generation = next;
764
+ device = nextDevice;
765
+ texture?.destroy();
766
+ texture = void 0;
767
+ uploaded = false;
768
+ },
769
+ destroy() {
770
+ texture?.destroy();
771
+ texture = void 0;
772
+ uploaded = false;
773
+ }
774
+ };
775
+ }
776
+ function linearGradientStops(from, to) {
777
+ const encode = (c) => [linearToSrgbChannel(c[0]), linearToSrgbChannel(c[1]), linearToSrgbChannel(c[2]), 1];
778
+ return [
779
+ { offset: 0, color: encode(from) },
780
+ { offset: 1, color: encode(to) }
781
+ ];
782
+ }
783
+ function validateAppTexture(texture, limits, subject) {
784
+ if (texture.dimension !== "2d") {
785
+ throw rendererError("texture-dimension", `Backdrop "${subject}" supplied a ${texture.dimension} texture. The sampling path binds a 2d texture; register a 2d texture or a 2d view of one slice.`, subject);
786
+ }
787
+ if (texture.depthOrArrayLayers !== 1) {
788
+ throw rendererError("texture-dimension", `Backdrop "${subject}" supplied a texture with ${texture.depthOrArrayLayers} array layers. Bind a single-layer view.`, subject);
789
+ }
790
+ if ((texture.usage & GPUTextureUsage.TEXTURE_BINDING) === 0) {
791
+ throw rendererError("texture-usage", `Backdrop "${subject}" supplied a texture without GPUTextureUsage.TEXTURE_BINDING, so it cannot be sampled. Add TEXTURE_BINDING to the texture's usage when you create it.`, subject);
792
+ }
793
+ if (!SUPPORTED_APP_TEXTURE_FORMATS.includes(texture.format)) {
794
+ throw rendererError("texture-format", `Backdrop "${subject}" supplied a "${texture.format}" texture. Supported formats are ${SUPPORTED_APP_TEXTURE_FORMATS.join(", ")}.`, subject);
795
+ }
796
+ if (texture.width <= 0 || texture.height <= 0) {
797
+ throw rendererError("texture-size", `Backdrop "${subject}" supplied a ${texture.width}\xD7${texture.height} texture.`, subject);
798
+ }
799
+ const max = limits.maxTextureDimension2D;
800
+ if (texture.width > max || texture.height > max) {
801
+ throw rendererError("texture-size", `Backdrop "${subject}" supplied a ${texture.width}\xD7${texture.height} texture, past this adapter's maxTextureDimension2D of ${max}.`, subject);
802
+ }
803
+ }
804
+ function createAppTextureProvider(options) {
805
+ validateAppTexture(options.texture, options.device.limits, options.id);
806
+ const encoded = options.encoded ?? !options.texture.format.includes("float");
807
+ const size = {
808
+ epoch: 0,
809
+ width: options.texture.width,
810
+ height: options.texture.height
811
+ };
812
+ const builtGeneration = options.generation ?? 0;
813
+ let generation = builtGeneration;
814
+ return {
815
+ id: options.id,
816
+ kind: "app-texture-view",
817
+ get generation() {
818
+ return generation;
819
+ },
820
+ isDirty() {
821
+ return true;
822
+ },
823
+ acquire() {
824
+ if (generation !== builtGeneration) {
825
+ throw rendererError("source-unavailable", `Backdrop "${options.id}" wraps a GPUTexture from device generation ${builtGeneration}, and the renderer is now on generation ${generation}. WebGPU has no cross-device texture sharing: unregister this source and register the replacement texture the app made on the new device.`, options.id);
826
+ }
827
+ return {
828
+ sourceId: options.id,
829
+ binding: {
830
+ kind: "sampled",
831
+ view: options.view ?? options.texture.createView()
832
+ },
833
+ width: size.width,
834
+ height: size.height,
835
+ sizeEpoch: size.epoch,
836
+ colorSpace: options.colorSpace ?? "srgb",
837
+ alphaMode: options.alphaMode ?? "premultiplied",
838
+ encoded
839
+ };
840
+ },
841
+ release() {
842
+ },
843
+ markImported() {
844
+ },
845
+ invalidate(next) {
846
+ generation = next;
847
+ },
848
+ destroy() {
849
+ }
850
+ };
851
+ }
852
+
853
+ // ../renderer-webgpu/dist/device.js
854
+ function createDeviceHost(options = {}) {
855
+ let status = {
856
+ webgpu: "not-requested",
857
+ deviceHealth: "ok",
858
+ ownership: "vitrea",
859
+ device: void 0,
860
+ generation: 0,
861
+ replacementPending: false
862
+ };
863
+ const hooks = /* @__PURE__ */ new Set();
864
+ let destroyed = false;
865
+ let recovery;
866
+ const publish = (next) => {
867
+ status = next;
868
+ if (!destroyed)
869
+ options.onStatusChange?.(status);
870
+ };
871
+ const runTeardown = () => {
872
+ for (const hook of [...hooks]) {
873
+ try {
874
+ hook();
875
+ } catch {
876
+ }
877
+ }
878
+ };
879
+ const watchLoss = (device, generation) => {
880
+ void device.lost.then((info) => {
881
+ if (destroyed || status.generation !== generation)
882
+ return;
883
+ runTeardown();
884
+ const ownership = status.ownership;
885
+ publish({
886
+ webgpu: "available",
887
+ deviceHealth: "lost",
888
+ ownership,
889
+ device: void 0,
890
+ generation,
891
+ replacementPending: ownership === "app",
892
+ unavailableReason: "lost"
893
+ });
894
+ if (info.reason === "destroyed")
895
+ return;
896
+ if (ownership === "app") {
897
+ options.onReplacementNeeded?.();
898
+ return;
899
+ }
900
+ const reacquire = options.reacquire;
901
+ if (reacquire === void 0)
902
+ return;
903
+ recovery = (async () => {
904
+ const replacement = await reacquire();
905
+ if (destroyed) {
906
+ replacement?.destroy();
907
+ return;
908
+ }
909
+ if (replacement === void 0)
910
+ return;
911
+ attach(replacement, "vitrea");
912
+ })();
913
+ });
914
+ };
915
+ function attach(device, ownership) {
916
+ const generation = status.generation + 1;
917
+ publish({
918
+ webgpu: "available",
919
+ deviceHealth: "ok",
920
+ ownership,
921
+ device,
922
+ generation,
923
+ replacementPending: false
924
+ });
925
+ watchLoss(device, generation);
926
+ }
927
+ return {
928
+ get status() {
929
+ return status;
930
+ },
931
+ get capabilityInput() {
932
+ return { webgpu: status.webgpu, deviceHealth: status.deviceHealth };
933
+ },
934
+ requireDevice() {
935
+ const { device } = status;
936
+ if (device === void 0) {
937
+ throw rendererError("device-unavailable", status.replacementPending ? "The app-owned GPUDevice was lost and no replacement has been handed in yet. Call replaceDevice() and re-register the app's texture views before drawing again." : "No GPUDevice is attached. Attach one (platform-web's WebGPU lifecycle produces it) before building resources or drawing.");
938
+ }
939
+ return device;
940
+ },
941
+ attach,
942
+ replaceDevice(device) {
943
+ attach(device, status.ownership);
944
+ },
945
+ markUnavailable(reason) {
946
+ publish({
947
+ ...status,
948
+ webgpu: "unavailable",
949
+ device: void 0,
950
+ unavailableReason: reason
951
+ });
952
+ },
953
+ addTeardownHook(hook) {
954
+ hooks.add(hook);
955
+ return () => hooks.delete(hook);
956
+ },
957
+ async settled() {
958
+ await recovery;
959
+ },
960
+ destroy() {
961
+ destroyed = true;
962
+ runTeardown();
963
+ if (status.ownership === "vitrea")
964
+ status.device?.destroy();
965
+ hooks.clear();
966
+ status = { ...status, device: void 0 };
967
+ }
968
+ };
969
+ }
970
+
971
+ // ../renderer-webgpu/dist/governor.js
972
+ var NOMINAL_GOVERNOR = {
973
+ fieldFamily: "rsupn",
974
+ refractionResolutionScale: 1,
975
+ adaptationCadenceHz: 15
976
+ };
977
+ var GOVERNOR_LADDER = [
978
+ NOMINAL_GOVERNOR,
979
+ { fieldFamily: "rsup", refractionResolutionScale: 1, adaptationCadenceHz: 15 },
980
+ { fieldFamily: "rsup", refractionResolutionScale: 0.75, adaptationCadenceHz: 7.5 },
981
+ { fieldFamily: "rsup", refractionResolutionScale: 0.5, adaptationCadenceHz: 4 }
982
+ ];
983
+ var FAMILY_C_CROSS_CHECK = {
984
+ ran: true,
985
+ adapter: "apple/metal-3",
986
+ points: 5535,
987
+ maxAbsDiffPx: 3042e-8,
988
+ boundPx: 0.574
989
+ };
990
+ var clampScale = (s) => Math.min(1, Math.max(0.125, s));
991
+ function createGovernor(options = {}) {
992
+ let knobs = NOMINAL_GOVERNOR;
993
+ let verified = options.familyCVerified ?? FAMILY_C_CROSS_CHECK.ran;
994
+ const apply = (next) => {
995
+ const family = next.fieldFamily === "rsup" && !verified ? "rsupn" : next.fieldFamily;
996
+ knobs = {
997
+ fieldFamily: family,
998
+ refractionResolutionScale: clampScale(next.refractionResolutionScale),
999
+ adaptationCadenceHz: Math.max(0, next.adaptationCadenceHz)
1000
+ };
1001
+ options.onChange?.(knobs);
1002
+ return knobs;
1003
+ };
1004
+ return {
1005
+ get knobs() {
1006
+ return knobs;
1007
+ },
1008
+ get familyCVerified() {
1009
+ return verified;
1010
+ },
1011
+ set(patch) {
1012
+ return apply({ ...knobs, ...patch });
1013
+ },
1014
+ setLevel(level) {
1015
+ const index = Math.min(GOVERNOR_LADDER.length - 1, Math.max(0, Math.round(level)));
1016
+ return apply(GOVERNOR_LADDER[index]);
1017
+ },
1018
+ reset() {
1019
+ return apply(NOMINAL_GOVERNOR);
1020
+ },
1021
+ recordFamilyCVerified() {
1022
+ verified = true;
1023
+ }
1024
+ };
1025
+ }
1026
+
1027
+ // ../renderer-webgpu/dist/pipeline-cache.js
1028
+ function createPipelineCache(factory) {
1029
+ const modules = /* @__PURE__ */ new Map();
1030
+ const renderPipelines = /* @__PURE__ */ new Map();
1031
+ const computePipelines = /* @__PURE__ */ new Map();
1032
+ let hits = 0;
1033
+ let misses = 0;
1034
+ function memo(store, key, make) {
1035
+ const existing = store.get(key);
1036
+ if (existing !== void 0) {
1037
+ hits += 1;
1038
+ return existing;
1039
+ }
1040
+ misses += 1;
1041
+ const made = make();
1042
+ store.set(key, made);
1043
+ return made;
1044
+ }
1045
+ return {
1046
+ get stats() {
1047
+ return {
1048
+ modules: modules.size,
1049
+ renderPipelines: renderPipelines.size,
1050
+ computePipelines: computePipelines.size,
1051
+ hits,
1052
+ misses
1053
+ };
1054
+ },
1055
+ module(key, source) {
1056
+ return memo(modules, key, () => factory.createShaderModule({ label: key, code: source() }));
1057
+ },
1058
+ renderPipeline(key, describe) {
1059
+ return memo(renderPipelines, key, () => factory.createRenderPipeline(describe()));
1060
+ },
1061
+ computePipeline(key, describe) {
1062
+ return memo(computePipelines, key, () => factory.createComputePipeline(describe()));
1063
+ },
1064
+ clear() {
1065
+ modules.clear();
1066
+ renderPipelines.clear();
1067
+ computePipelines.clear();
1068
+ }
1069
+ };
1070
+ }
1071
+ var pipelineKey = {
1072
+ field: (family, format) => `field:${family}:${format}`,
1073
+ import: (kind, format) => `import:${kind}:${format}`,
1074
+ chain: (entry, format) => `chain:${entry}:${format}`,
1075
+ analysis: () => "analysis",
1076
+ optics: (format, blend) => `optics:${format}:${blend}`,
1077
+ highlight: (format, blend) => `highlight:${format}:${blend}`
1078
+ };
1079
+
1080
+ // ../renderer-webgpu/dist/texture-pool.js
1081
+ var sameShape = (a, b) => a.width === b.width && a.height === b.height && a.format === b.format && a.usage === b.usage && (a.mipLevelCount ?? 1) === (b.mipLevelCount ?? 1);
1082
+ function createTexturePool(allocator) {
1083
+ const entries = /* @__PURE__ */ new Map();
1084
+ let epoch = 0;
1085
+ let created = 0;
1086
+ let destroyed = 0;
1087
+ let reused = 0;
1088
+ const destroy = (entry) => {
1089
+ entry.texture.destroy();
1090
+ destroyed += 1;
1091
+ };
1092
+ return {
1093
+ get sizeEpoch() {
1094
+ return epoch;
1095
+ },
1096
+ get stats() {
1097
+ return { live: entries.size, created, destroyed, reused, epoch };
1098
+ },
1099
+ acquire(key, request) {
1100
+ const existing = entries.get(key);
1101
+ if (existing !== void 0) {
1102
+ if (existing.epoch === epoch && sameShape(existing.request, request)) {
1103
+ reused += 1;
1104
+ return existing.texture;
1105
+ }
1106
+ destroy(existing);
1107
+ }
1108
+ const texture = allocator.createTexture({
1109
+ size: {
1110
+ width: Math.max(1, Math.floor(request.width)),
1111
+ height: Math.max(1, Math.floor(request.height)),
1112
+ depthOrArrayLayers: 1
1113
+ },
1114
+ format: request.format,
1115
+ usage: request.usage,
1116
+ mipLevelCount: request.mipLevelCount ?? 1,
1117
+ ...request.label === void 0 ? {} : { label: request.label }
1118
+ });
1119
+ created += 1;
1120
+ entries.set(key, { texture, request, epoch });
1121
+ return texture;
1122
+ },
1123
+ peek(key) {
1124
+ return entries.get(key)?.texture;
1125
+ },
1126
+ release(key) {
1127
+ const entry = entries.get(key);
1128
+ if (entry === void 0)
1129
+ return;
1130
+ destroy(entry);
1131
+ entries.delete(key);
1132
+ },
1133
+ bumpSizeEpoch() {
1134
+ epoch += 1;
1135
+ return epoch;
1136
+ },
1137
+ sweep() {
1138
+ let swept = 0;
1139
+ for (const [key, entry] of [...entries]) {
1140
+ if (entry.epoch === epoch)
1141
+ continue;
1142
+ destroy(entry);
1143
+ entries.delete(key);
1144
+ swept += 1;
1145
+ }
1146
+ return swept;
1147
+ },
1148
+ clear() {
1149
+ for (const entry of entries.values())
1150
+ destroy(entry);
1151
+ entries.clear();
1152
+ }
1153
+ };
1154
+ }
1155
+ var poolKey = {
1156
+ backdropLevel0: (sourceId) => `backdrop:${sourceId}:level0`,
1157
+ backdropChain: (sourceId) => `backdrop:${sourceId}:chain`,
1158
+ backdropChainScratch: (sourceId, level) => `backdrop:${sourceId}:chain-scratch:${level}`,
1159
+ backdropBody: (sourceId) => `backdrop:${sourceId}:body`,
1160
+ backdropBodyScratch: (sourceId) => `backdrop:${sourceId}:body-scratch`,
1161
+ backdropUpload: (sourceId) => `backdrop:${sourceId}:upload`,
1162
+ groupField: (groupId) => `group:${groupId}:field`,
1163
+ groupAux: (groupId) => `group:${groupId}:aux`
1164
+ };
1165
+
1166
+ // ../renderer-webgpu/dist/gpu-context.js
1167
+ function createGpuContext(device, generation) {
1168
+ const pool = createTexturePool(device);
1169
+ const cache = createPipelineCache(device);
1170
+ const chainSampler = device.createSampler({
1171
+ label: "vitrea:sampler:chain",
1172
+ magFilter: "linear",
1173
+ minFilter: "linear",
1174
+ mipmapFilter: "linear",
1175
+ addressModeU: "clamp-to-edge",
1176
+ addressModeV: "clamp-to-edge"
1177
+ });
1178
+ const flatSampler = device.createSampler({
1179
+ label: "vitrea:sampler:flat",
1180
+ magFilter: "linear",
1181
+ minFilter: "linear",
1182
+ addressModeU: "clamp-to-edge",
1183
+ addressModeV: "clamp-to-edge"
1184
+ });
1185
+ return {
1186
+ device,
1187
+ generation,
1188
+ pool,
1189
+ cache,
1190
+ chainSampler,
1191
+ flatSampler,
1192
+ destroy() {
1193
+ pool.clear();
1194
+ cache.clear();
1195
+ }
1196
+ };
1197
+ }
1198
+ function createUniformSlot(device, floats, label) {
1199
+ const size = Math.ceil(floats * 4 / 16) * 16;
1200
+ const buffer = device.createBuffer({
1201
+ label,
1202
+ size,
1203
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
1204
+ });
1205
+ const data = new Float32Array(size / 4);
1206
+ return {
1207
+ buffer,
1208
+ data,
1209
+ write() {
1210
+ device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength);
1211
+ }
1212
+ };
1213
+ }
1214
+ function createStorageSlot(device, initialBytes, label) {
1215
+ let capacity = Math.max(initialBytes, 256);
1216
+ let buffer = device.createBuffer({
1217
+ label,
1218
+ size: capacity,
1219
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
1220
+ });
1221
+ return {
1222
+ get buffer() {
1223
+ return buffer;
1224
+ },
1225
+ ensure(byteLength) {
1226
+ if (byteLength <= capacity)
1227
+ return buffer;
1228
+ buffer.destroy();
1229
+ capacity = 1 << Math.ceil(Math.log2(byteLength));
1230
+ buffer = device.createBuffer({
1231
+ label,
1232
+ size: capacity,
1233
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
1234
+ });
1235
+ return buffer;
1236
+ },
1237
+ write(data, floatCount) {
1238
+ device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, floatCount * 4);
1239
+ },
1240
+ destroy() {
1241
+ buffer.destroy();
1242
+ }
1243
+ };
1244
+ }
1245
+
1246
+ // ../renderer-webgpu/dist/render-model.js
1247
+ var IDLE_CHANNELS = {
1248
+ press: 0,
1249
+ glow: 0,
1250
+ sweep: 0,
1251
+ lensStrength: 1
1252
+ };
1253
+
1254
+ // ../renderer-webgpu/dist/instances.js
1255
+ var INSTANCE_FLOATS = 16;
1256
+ var INSTANCE_BYTES = INSTANCE_FLOATS * 4;
1257
+ var channelsOf = (input) => ({
1258
+ ...IDLE_CHANNELS,
1259
+ ...input.channels
1260
+ });
1261
+ function compressedChannels(shape, press) {
1262
+ if (press <= 0)
1263
+ return shape;
1264
+ const scale = 1 - Math.min(1, Math.max(0, press)) * DEFAULT_MOTION_PROFILE.pressCompressionScale;
1265
+ return { ...shape, size: [shape.size[0] * scale, shape.size[1] * scale] };
1266
+ }
1267
+ function resolveSurfaces(group, family, profile = DEFAULT_MATERIAL_PROFILE) {
1268
+ const byId = /* @__PURE__ */ new Map();
1269
+ for (const surface of group.surfaces) {
1270
+ if (byId.has(surface.nodeId)) {
1271
+ throw rendererError("source-identity", `Group "${group.groupId}" lists surface "${surface.nodeId}" twice.`, surface.nodeId);
1272
+ }
1273
+ byId.set(surface.nodeId, surface);
1274
+ }
1275
+ const resolvedShapes = /* @__PURE__ */ new Map();
1276
+ const resolving = /* @__PURE__ */ new Set();
1277
+ const resolveShapeOf = (surface) => {
1278
+ const cached = resolvedShapes.get(surface.nodeId);
1279
+ if (cached !== void 0)
1280
+ return cached;
1281
+ if (resolving.has(surface.nodeId)) {
1282
+ throw rendererError("pass-input", `Concentric surfaces in group "${group.groupId}" form a cycle through "${surface.nodeId}". A level set needs a parent field that resolves without it.`, surface.nodeId);
1283
+ }
1284
+ resolving.add(surface.nodeId);
1285
+ const channels = channelsOf(surface);
1286
+ const reference = surface.reference ?? "apple-continuous";
1287
+ if (surface.concentricOf === void 0) {
1288
+ const shape2 = resolveFromChannels(compressedChannels(surface.shape, channels.press), reference, surface.family, { devMode: false });
1289
+ resolvedShapes.set(surface.nodeId, shape2);
1290
+ resolving.delete(surface.nodeId);
1291
+ return shape2;
1292
+ }
1293
+ const parentInput = byId.get(surface.concentricOf.nodeId);
1294
+ if (parentInput === void 0) {
1295
+ throw rendererError("pass-input", `Surface "${surface.nodeId}" is concentric to "${surface.concentricOf.nodeId}", which is not a member of group "${group.groupId}". X8 rider 2 renders a concentric child as a level set of its parent's field, so the parent has to be in the same field pass.`, surface.nodeId);
1296
+ }
1297
+ const shape = resolveConcentric(resolveShapeOf(parentInput), {
1298
+ inset: surface.concentricOf.inset
1299
+ }).shape;
1300
+ resolvedShapes.set(surface.nodeId, shape);
1301
+ resolving.delete(surface.nodeId);
1302
+ return shape;
1303
+ };
1304
+ const paramsFor = (shape) => family === "rsup" ? governorFieldParams(shape) : fieldParams(shape);
1305
+ return group.surfaces.filter((surface) => surface.fieldReferenceOnly !== true).map((surface) => {
1306
+ const channels = channelsOf(surface);
1307
+ const shape = resolveShapeOf(surface);
1308
+ const fieldSource = surface.concentricOf === void 0 ? shape : resolveShapeOf(byId.get(surface.concentricOf.nodeId));
1309
+ const inset = surface.concentricOf?.inset ?? 0;
1310
+ const spanPx = Math.min(shape.channels.size[0], shape.channels.size[1]);
1311
+ return {
1312
+ nodeId: surface.nodeId,
1313
+ shape,
1314
+ field: paramsFor(fieldSource),
1315
+ inset,
1316
+ centre: [fieldSource.channels.center[0], fieldSource.channels.center[1]],
1317
+ channels,
1318
+ spanPx,
1319
+ lensDepthPx: lensDepthPx(shape.channels.thickness, spanPx, profile)
1320
+ };
1321
+ });
1322
+ }
1323
+ function groupFieldRect(surfaces, union = DEFAULT_GROUP_UNION, rimWidthPx = 2) {
1324
+ if (surfaces.length === 0)
1325
+ return { x: 0, y: 0, width: 0, height: 0 };
1326
+ let minX = Number.POSITIVE_INFINITY;
1327
+ let minY = Number.POSITIVE_INFINITY;
1328
+ let maxX = Number.NEGATIVE_INFINITY;
1329
+ let maxY = Number.NEGATIVE_INFINITY;
1330
+ for (const surface of surfaces) {
1331
+ const [cx, cy] = surface.shape.channels.center;
1332
+ const [w, h] = surface.shape.channels.size;
1333
+ minX = Math.min(minX, cx - w / 2);
1334
+ minY = Math.min(minY, cy - h / 2);
1335
+ maxX = Math.max(maxX, cx + w / 2);
1336
+ maxY = Math.max(maxY, cy + h / 2);
1337
+ }
1338
+ const pad = rimWidthPx + union.maxBulge + 1;
1339
+ return {
1340
+ x: minX - pad,
1341
+ y: minY - pad,
1342
+ width: maxX - minX + 2 * pad,
1343
+ height: maxY - minY + 2 * pad
1344
+ };
1345
+ }
1346
+ function clipFieldRectToCanvas(snapped, devicePixelRatio, canvasDevice) {
1347
+ const left = Math.max(0, Math.round(snapped.x * devicePixelRatio));
1348
+ const top = Math.max(0, Math.round(snapped.y * devicePixelRatio));
1349
+ const right = Math.min(canvasDevice[0], Math.round((snapped.x + snapped.width) * devicePixelRatio));
1350
+ const bottom = Math.min(canvasDevice[1], Math.round((snapped.y + snapped.height) * devicePixelRatio));
1351
+ if (right <= left || bottom <= top)
1352
+ return void 0;
1353
+ return { x: left, y: top, width: right - left, height: bottom - top };
1354
+ }
1355
+ function snapRectToDevicePixels(rect, devicePixelRatio) {
1356
+ const x = Math.floor(rect.x * devicePixelRatio) / devicePixelRatio;
1357
+ const y = Math.floor(rect.y * devicePixelRatio) / devicePixelRatio;
1358
+ const right = Math.ceil((rect.x + rect.width) * devicePixelRatio) / devicePixelRatio;
1359
+ const bottom = Math.ceil((rect.y + rect.height) * devicePixelRatio) / devicePixelRatio;
1360
+ return { x, y, width: Math.max(right - x, 0), height: Math.max(bottom - y, 0) };
1361
+ }
1362
+ function packInstances(surfaces, origin, into) {
1363
+ const needed = Math.max(surfaces.length, 1) * INSTANCE_FLOATS;
1364
+ const data = into !== void 0 && into.length >= needed ? into : new Float32Array(needed);
1365
+ for (let i = 0; i < surfaces.length; i += 1) {
1366
+ const s = surfaces[i];
1367
+ const o = i * INSTANCE_FLOATS;
1368
+ data[o + 0] = s.centre[0] - origin[0];
1369
+ data[o + 1] = s.centre[1] - origin[1];
1370
+ data[o + 2] = s.field.halfW;
1371
+ data[o + 3] = s.field.halfH;
1372
+ data[o + 4] = s.field.reach;
1373
+ data[o + 5] = s.field.k[0];
1374
+ data[o + 6] = s.field.k[1];
1375
+ data[o + 7] = s.field.k[2];
1376
+ data[o + 8] = s.field.k[3];
1377
+ data[o + 9] = s.field.k[4];
1378
+ data[o + 10] = s.inset;
1379
+ data[o + 11] = s.shape.channels.thickness;
1380
+ data[o + 12] = s.channels.press;
1381
+ data[o + 13] = s.channels.glow;
1382
+ data[o + 14] = s.lensDepthPx * Math.min(1, Math.max(0, s.channels.lensStrength));
1383
+ data[o + 15] = 0;
1384
+ }
1385
+ return { data, count: surfaces.length };
1386
+ }
1387
+
1388
+ // ../renderer-webgpu/dist/timing.js
1389
+ function supportsTimestamps(device) {
1390
+ return device.features.has("timestamp-query");
1391
+ }
1392
+ function createTimingCollector(device, capacity = 64) {
1393
+ const querySet = device.createQuerySet({
1394
+ label: "vitrea:timing",
1395
+ type: "timestamp",
1396
+ count: capacity * 2
1397
+ });
1398
+ const byteLength = capacity * 2 * 8;
1399
+ const resolveBuffer = device.createBuffer({
1400
+ label: "vitrea:timing:resolve",
1401
+ size: byteLength,
1402
+ usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC
1403
+ });
1404
+ const staging = device.createBuffer({
1405
+ label: "vitrea:timing:staging",
1406
+ size: byteLength,
1407
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
1408
+ });
1409
+ let used = 0;
1410
+ let anomalies = 0;
1411
+ const labels = [];
1412
+ const take = (label) => {
1413
+ if (used >= capacity)
1414
+ return void 0;
1415
+ const index = used;
1416
+ used += 1;
1417
+ labels[index] = label;
1418
+ return { begin: index * 2, end: index * 2 + 1 };
1419
+ };
1420
+ return {
1421
+ capacity,
1422
+ get used() {
1423
+ return used;
1424
+ },
1425
+ get anomalies() {
1426
+ return anomalies;
1427
+ },
1428
+ renderSlot(label) {
1429
+ const slot = take(label);
1430
+ if (slot === void 0)
1431
+ return void 0;
1432
+ return {
1433
+ querySet,
1434
+ beginningOfPassWriteIndex: slot.begin,
1435
+ endOfPassWriteIndex: slot.end
1436
+ };
1437
+ },
1438
+ computeSlot(label) {
1439
+ const slot = take(label);
1440
+ if (slot === void 0)
1441
+ return void 0;
1442
+ return {
1443
+ querySet,
1444
+ beginningOfPassWriteIndex: slot.begin,
1445
+ endOfPassWriteIndex: slot.end
1446
+ };
1447
+ },
1448
+ resolve(encoder) {
1449
+ if (used === 0)
1450
+ return;
1451
+ encoder.resolveQuerySet(querySet, 0, used * 2, resolveBuffer, 0);
1452
+ encoder.copyBufferToBuffer(resolveBuffer, 0, staging, 0, used * 2 * 8);
1453
+ },
1454
+ async read() {
1455
+ const out = /* @__PURE__ */ new Map();
1456
+ if (used === 0)
1457
+ return out;
1458
+ await staging.mapAsync(GPUMapMode.READ, 0, used * 2 * 8);
1459
+ const view = new BigInt64Array(staging.getMappedRange(0, used * 2 * 8).slice(0));
1460
+ staging.unmap();
1461
+ for (let i = 0; i < used; i += 1) {
1462
+ const begin = view[i * 2] ?? 0n;
1463
+ const end = view[i * 2 + 1] ?? 0n;
1464
+ const label = labels[i] ?? `pass-${i}`;
1465
+ const elapsed = Number(end - begin);
1466
+ if (!Number.isFinite(elapsed) || elapsed < 0 || begin === 0n && end === 0n) {
1467
+ anomalies += 1;
1468
+ continue;
1469
+ }
1470
+ out.set(label, (out.get(label) ?? 0) + elapsed);
1471
+ }
1472
+ return out;
1473
+ },
1474
+ reset() {
1475
+ used = 0;
1476
+ labels.length = 0;
1477
+ },
1478
+ destroy() {
1479
+ querySet.destroy();
1480
+ resolveBuffer.destroy();
1481
+ staging.destroy();
1482
+ }
1483
+ };
1484
+ }
1485
+ var PASS_LABEL = {
1486
+ import: "backdrop-import",
1487
+ chain: "blur-chain",
1488
+ bodyBlur: "body-blur",
1489
+ analysis: "analysis",
1490
+ field: "group-field",
1491
+ optics: "optics",
1492
+ highlight: "highlight"
1493
+ };
1494
+
1495
+ // ../renderer-webgpu/dist/wgsl/cross-check.js
1496
+ var CROSS_CHECK_SHAPE_FLOATS = 12;
1497
+ var CROSS_CHECK_WORKGROUP = 64;
1498
+ var WGSL_CROSS_CHECK_PASS = `struct CheckShape {
1499
+ centre : vec2f,
1500
+ half : vec2f,
1501
+ re : f32,
1502
+ k0 : f32,
1503
+ k1 : f32,
1504
+ k2 : f32,
1505
+ k3 : f32,
1506
+ k4 : f32,
1507
+ _pad0 : f32,
1508
+ _pad1 : f32,
1509
+ };
1510
+
1511
+ /// point.xy = shape-local coordinates, point.z = shape index, point.w unused
1512
+ @group(0) @binding(0) var<storage, read> checkShapes : array<CheckShape>;
1513
+ @group(0) @binding(1) var<storage, read> checkPoints : array<vec4f>;
1514
+ @group(0) @binding(2) var<storage, read_write> outRsupn : array<f32>;
1515
+ @group(0) @binding(3) var<storage, read_write> outRsup : array<f32>;
1516
+ @group(0) @binding(4) var<uniform> checkCount : vec4u;
1517
+
1518
+ @compute @workgroup_size(${CROSS_CHECK_WORKGROUP})
1519
+ fn cs_cross_check(@builtin(global_invocation_id) gid : vec3u) {
1520
+ let i = gid.x;
1521
+ if (i >= checkCount.x) { return; }
1522
+
1523
+ let p = checkPoints[i];
1524
+ let s = checkShapes[u32(p.z)];
1525
+ let k = vec4f(s.k0, s.k1, s.k2, s.k3);
1526
+ let local = p.xy - s.centre;
1527
+
1528
+ outRsupn[i] = sd_rsupn(local, s.half, s.re, k, s.k4);
1529
+ outRsup[i] = sd_rsup(local, s.half, s.re, k, s.k4);
1530
+ }`;
1531
+
1532
+ // ../renderer-webgpu/dist/wgsl/backdrop.js
1533
+ var WGSL_IMPORT_PASS = `struct ImportUniforms {
1534
+ m0 : vec4f, // colour matrix row 0 (xyz), srcEncoded (w)
1535
+ m1 : vec4f, // row 1 (xyz), alphaMode (w)
1536
+ m2 : vec4f, // row 2 (xyz), unused (w)
1537
+ fit : vec4f, // uv scale (xy), uv offset (zw) \u2014 the source's fit into the target
1538
+ };
1539
+
1540
+ @group(0) @binding(0) var<uniform> iu : ImportUniforms;
1541
+ @group(0) @binding(1) var srcSampler : sampler;
1542
+ @group(0) @binding(2) var srcTexture : SRC_TEXTURE_TYPE;
1543
+
1544
+ fn sample_src(uv : vec2f) -> vec4f {
1545
+ return SRC_SAMPLE_EXPR;
1546
+ }
1547
+
1548
+ @fragment
1549
+ fn fs_import(in : FullscreenOut) -> @location(0) vec4f {
1550
+ let uv = in.uv * iu.fit.xy + iu.fit.zw;
1551
+ var raw = sample_src(clamp(uv, vec2f(0.0), vec2f(1.0)));
1552
+
1553
+ let alphaMode = iu.m1.w;
1554
+ var alpha = raw.a;
1555
+ var colour = raw.rgb;
1556
+
1557
+ if (alphaMode > 1.5) {
1558
+ alpha = 1.0;
1559
+ } else if (alphaMode > 0.5) {
1560
+ // Already unpremultiplied: nothing to undo.
1561
+ } else {
1562
+ // Premultiplied input \u2014 undo it before the non-linear decode. A fully
1563
+ // transparent texel carries zero colour, so the floored divide returns zero
1564
+ // there without a branch.
1565
+ colour = colour / max(alpha, 1e-6);
1566
+ }
1567
+
1568
+ if (iu.m0.w > 0.5) {
1569
+ colour = srgb_to_linear(clamp(colour, vec3f(0.0), vec3f(1.0)));
1570
+ }
1571
+
1572
+ // Colour-space conversion in linear light. Identity for sRGB sources.
1573
+ colour = vec3f(dot(iu.m0.xyz, colour), dot(iu.m1.xyz, colour), dot(iu.m2.xyz, colour));
1574
+
1575
+ return vec4f(max(colour, vec3f(0.0)) * alpha, alpha);
1576
+ }`;
1577
+ function importPassSource(kind) {
1578
+ const type = kind === "external" ? "texture_external" : "texture_2d<f32>";
1579
+ const expr = kind === "external" ? "textureSampleBaseClampToEdge(srcTexture, srcSampler, uv)" : "textureSampleLevel(srcTexture, srcSampler, uv, 0.0)";
1580
+ return WGSL_IMPORT_PASS.replace("SRC_TEXTURE_TYPE", type).replace("SRC_SAMPLE_EXPR", expr);
1581
+ }
1582
+ var WGSL_DOWNSAMPLE_PASS = `struct ChainUniforms {
1583
+ texel : vec4f, // 1/srcSize (xy), unused (zw)
1584
+ params : vec4f, // sigma in texels (x), direction (yz), unused (w)
1585
+ };
1586
+
1587
+ @group(0) @binding(0) var<uniform> cu : ChainUniforms;
1588
+ @group(0) @binding(1) var chainSampler : sampler;
1589
+ @group(0) @binding(2) var chainTexture : texture_2d<f32>;
1590
+
1591
+ fn tap(uv : vec2f, o : vec2f) -> vec4f {
1592
+ return textureSampleLevel(chainTexture, chainSampler, uv + o * cu.texel.xy, 0.0);
1593
+ }
1594
+
1595
+ @fragment
1596
+ fn fs_downsample(in : FullscreenOut) -> @location(0) vec4f {
1597
+ let uv = in.uv;
1598
+
1599
+ let a = tap(uv, vec2f(-2.0, 2.0));
1600
+ let b = tap(uv, vec2f( 0.0, 2.0));
1601
+ let c = tap(uv, vec2f( 2.0, 2.0));
1602
+ let d = tap(uv, vec2f(-2.0, 0.0));
1603
+ let e = tap(uv, vec2f( 0.0, 0.0));
1604
+ let f = tap(uv, vec2f( 2.0, 0.0));
1605
+ let g = tap(uv, vec2f(-2.0, -2.0));
1606
+ let h = tap(uv, vec2f( 0.0, -2.0));
1607
+ let i = tap(uv, vec2f( 2.0, -2.0));
1608
+ let j = tap(uv, vec2f(-1.0, 1.0));
1609
+ let k = tap(uv, vec2f( 1.0, 1.0));
1610
+ let l = tap(uv, vec2f(-1.0, -1.0));
1611
+ let m = tap(uv, vec2f( 1.0, -1.0));
1612
+
1613
+ // Weights: the centre cross carries 0.5 across four inner taps, the corners
1614
+ // and edges the remaining 0.5. Sums to exactly 1, so the chain neither gains
1615
+ // nor loses energy \u2014 which is what keeps the analysis pass's luminance honest
1616
+ // however many levels it reads through.
1617
+ var out = (j + k + l + m) * 0.5 * 0.25;
1618
+ out = out + (a + b + d + e) * 0.125 * 0.25;
1619
+ out = out + (b + c + e + f) * 0.125 * 0.25;
1620
+ out = out + (d + e + g + h) * 0.125 * 0.25;
1621
+ out = out + (e + f + h + i) * 0.125 * 0.25;
1622
+ return out;
1623
+ }
1624
+
1625
+ /// Separable Gaussian, nine taps, run once horizontally and once vertically to
1626
+ /// take the nearest chain level up to the material's exact sigma.
1627
+ @fragment
1628
+ fn fs_blur(in : FullscreenOut) -> @location(0) vec4f {
1629
+ let dir = cu.params.yz;
1630
+ let sigma = max(cu.params.x, 1e-4);
1631
+ let inv = -0.5 / (sigma * sigma);
1632
+
1633
+ var sum = vec4f(0.0);
1634
+ var weight = 0.0;
1635
+ for (var t = -4; t <= 4; t = t + 1) {
1636
+ let ft = f32(t);
1637
+ let w = exp(ft * ft * inv);
1638
+ sum = sum + textureSampleLevel(chainTexture, chainSampler, in.uv + dir * ft * cu.texel.xy, 0.0) * w;
1639
+ weight = weight + w;
1640
+ }
1641
+ return sum / weight;
1642
+ }`;
1643
+
1644
+ // ../renderer-webgpu/dist/wgsl/field.js
1645
+ var WGSL_INSTANCE_STRUCT = `struct Instance {
1646
+ centre : vec2f, // 0 group-local CSS px
1647
+ half : vec2f, // 8 half-extents of the field's OWN shape (the parent's, for a concentric child)
1648
+ re : f32, // 16 corner reach
1649
+ k0 : f32, // 20
1650
+ k1 : f32, // 24
1651
+ k2 : f32, // 28
1652
+ k3 : f32, // 32
1653
+ k4 : f32, // 36
1654
+ inset : f32, // 40 X8 rider 2: level-set offset. 0 for an ordinary surface.
1655
+ thick : f32, // 44 material thickness, CSS px
1656
+ press : f32, // 48 interaction channel value, 0..1
1657
+ glow : f32, // 52 interaction channel value, 0..1
1658
+ lensDepth : f32, // 56 CPU-resolved lens depth in CSS px (material.ts), already scaled by the lensStrength channel
1659
+ _pad : f32, // 60
1660
+ };`;
1661
+ var WGSL_FIELD_SAMPLE = `struct FieldSample {
1662
+ d : f32,
1663
+ g : vec2f,
1664
+ };`;
1665
+ var WGSL_RSUPN_GRAD = `fn sd_rsupn_grad(p : vec2f, half : vec2f, re : f32, k : vec4f, k4 : f32) -> FieldSample {
1666
+ let q = abs(p) - half + vec2f(re, re);
1667
+ let c = max(q, vec2f(0.0, 0.0));
1668
+ let r2 = max(dot(c, c), 1e-20);
1669
+ let inv = 1.0 / r2;
1670
+ let rho = sqrt(r2);
1671
+ let s2 = 2.0 * c.x * c.y * inv;
1672
+ let c2 = (c.x * c.x - c.y * c.y) * inv;
1673
+
1674
+ // A = sum k_i s2^i, B = sum (i+2) k_i s2^i, C = sum (i+1)(i+2) k_i s2^i.
1675
+ var accA = k4;
1676
+ accA = accA * s2 + k.w;
1677
+ accA = accA * s2 + k.z;
1678
+ accA = accA * s2 + k.y;
1679
+ accA = accA * s2 + k.x;
1680
+
1681
+ var accB = 6.0 * k4;
1682
+ accB = accB * s2 + 5.0 * k.w;
1683
+ accB = accB * s2 + 4.0 * k.z;
1684
+ accB = accB * s2 + 3.0 * k.y;
1685
+ accB = accB * s2 + 2.0 * k.x;
1686
+
1687
+ var accC = 30.0 * k4;
1688
+ accC = accC * s2 + 20.0 * k.w;
1689
+ accC = accC * s2 + 12.0 * k.z;
1690
+ accC = accC * s2 + 6.0 * k.y;
1691
+ accC = accC * s2 + 2.0 * k.x;
1692
+
1693
+ let R = re * (1.0 + s2 * s2 * accA);
1694
+ let dRds2 = re * s2 * accB;
1695
+ let d2Rds2 = re * accC;
1696
+ let dRdt = dRds2 * (2.0 * c2);
1697
+
1698
+ let mm = min(max(q.x, q.y), 0.0);
1699
+ let base = rho + mm - R;
1700
+ // Anchored at the contour radius, mirroring sd_rsupn: the normalization's
1701
+ // slope is read at the foot of the Newton step, never at a sample radius
1702
+ // inside the contour. See geometry's field.ts, "The normalization".
1703
+ let atRho = rho >= R;
1704
+ let w = select(R, rho, atRho);
1705
+ let g = select(dRdt / R, dRdt * inv * rho, atRho);
1706
+ let norm = sqrt(1.0 + g * g);
1707
+ let n = 1.0 / norm;
1708
+
1709
+ let ds2dcx = -2.0 * c.y * c2 * inv;
1710
+ let ds2dcy = 2.0 * c.x * c2 * inv;
1711
+ let dc2dcx = 2.0 * c.x * inv * (1.0 - c2);
1712
+ let dc2dcy = -2.0 * c.y * inv * (1.0 + c2);
1713
+
1714
+ let drhodcx = c.x / rho;
1715
+ let drhodcy = c.y / rho;
1716
+
1717
+ let dRdcx = dRds2 * ds2dcx;
1718
+ let dRdcy = dRds2 * ds2dcy;
1719
+
1720
+ let dTermdcx = 2.0 * (d2Rds2 * ds2dcx * c2 + dRds2 * dc2dcx);
1721
+ let dTermdcy = 2.0 * (d2Rds2 * ds2dcy * c2 + dRds2 * dc2dcy);
1722
+
1723
+ // w's derivative follows whichever of rho and R is larger. Exact across the
1724
+ // switch because the switch locus is rho == R, the contour, where base == 0
1725
+ // kills the only term the jump lives in.
1726
+ let dwdcx = select(dRdcx, drhodcx, atRho);
1727
+ let dwdcy = select(dRdcy, drhodcy, atRho);
1728
+
1729
+ let dgdcx = dTermdcx / w - (dRdt * dwdcx) / (w * w);
1730
+ let dgdcy = dTermdcy / w - (dRdt * dwdcy) / (w * w);
1731
+
1732
+ let n3 = n * n * n;
1733
+ let dddcx = (drhodcx - dRdcx) * n + base * (-g * n3 * dgdcx);
1734
+ let dddcy = (drhodcy - dRdcy) * n + base * (-g * n3 * dgdcy);
1735
+
1736
+ let mx = select(0.0, 1.0, q.x > 0.0);
1737
+ let my = select(0.0, 1.0, q.y > 0.0);
1738
+ let inBox = max(q.x, q.y) <= 0.0;
1739
+ let dmmdqx = select(0.0, 1.0, inBox && q.x >= q.y);
1740
+ let dmmdqy = select(0.0, 1.0, inBox && q.y > q.x);
1741
+
1742
+ let sx = select(1.0, -1.0, p.x < 0.0);
1743
+ let sy = select(1.0, -1.0, p.y < 0.0);
1744
+
1745
+ var out : FieldSample;
1746
+ out.d = base * n;
1747
+ out.g = vec2f((dddcx * mx + n * dmmdqx) * sx, (dddcy * my + n * dmmdqy) * sy);
1748
+ return out;
1749
+ }`;
1750
+ var WGSL_RSUP_GRAD = `fn sd_rsup_grad(p : vec2f, half : vec2f, re : f32, k : vec4f, k4 : f32) -> FieldSample {
1751
+ let q = abs(p) - half + vec2f(re, re);
1752
+ let c = max(q, vec2f(0.0, 0.0));
1753
+ let r2 = max(dot(c, c), 1e-20);
1754
+ let rho = sqrt(r2);
1755
+ let s2 = 2.0 * c.x * c.y / r2;
1756
+
1757
+ var accA = k4;
1758
+ accA = accA * s2 + k.w;
1759
+ accA = accA * s2 + k.z;
1760
+ accA = accA * s2 + k.y;
1761
+ accA = accA * s2 + k.x;
1762
+
1763
+ var accB = 6.0 * k4;
1764
+ accB = accB * s2 + 5.0 * k.w;
1765
+ accB = accB * s2 + 4.0 * k.z;
1766
+ accB = accB * s2 + 3.0 * k.y;
1767
+ accB = accB * s2 + 2.0 * k.x;
1768
+
1769
+ let c2 = (c.x * c.x - c.y * c.y) / r2;
1770
+ let R = re * (1.0 + s2 * s2 * accA);
1771
+
1772
+ let sx = select(1.0, -1.0, p.x < 0.0);
1773
+ let sy = select(1.0, -1.0, p.y < 0.0);
1774
+
1775
+ var out : FieldSample;
1776
+ out.d = rho + min(max(q.x, q.y), 0.0) - R;
1777
+
1778
+ if (c.x <= 0.0 && c.y <= 0.0) {
1779
+ // Straight-edge and deep-interior: the normal is the axis the box branch is
1780
+ // measuring along.
1781
+ out.g = select(vec2f(0.0, sy), vec2f(sx, 0.0), q.x > q.y);
1782
+ return out;
1783
+ }
1784
+
1785
+ let dRdt = re * s2 * accB * (2.0 * c2);
1786
+ let rhoHat = c / rho;
1787
+ let gg = dRdt / rho;
1788
+ let nx = rhoHat.x + gg * rhoHat.y;
1789
+ let ny = rhoHat.y - gg * rhoHat.x;
1790
+ let len = max(length(vec2f(nx, ny)), 1e-20);
1791
+ out.g = vec2f(sx * nx / len, sy * ny / len);
1792
+ return out;
1793
+ }`;
1794
+ var WGSL_SMOOTH_UNION = `fn union_blend(a : f32, b : f32, u : vec3f) -> vec2f {
1795
+ let neck = u.x;
1796
+ let maxBulge = u.y;
1797
+ let sep = max(u.z * 0.5, 1e-6);
1798
+
1799
+ let nearest = min(a, b);
1800
+ let capped = min(neck, 4.0 * maxBulge);
1801
+ let gate = 1.0 - smoothstep(0.0, sep, max(nearest, 0.0));
1802
+ let k = capped * gate;
1803
+
1804
+ if (k <= 0.0) {
1805
+ return vec2f(nearest, select(0.0, 1.0, a <= b));
1806
+ }
1807
+
1808
+ let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
1809
+ return vec2f(b + h * (a - b) - k * h * (1.0 - h), h);
1810
+ }`;
1811
+ var WGSL_FIELD_PASS = `struct FieldUniforms {
1812
+ /// screen.xy = target size in device px, screen.z = CSS px per device px,
1813
+ /// screen.w = coverage ramp width in CSS px
1814
+ screen : vec4f,
1815
+ /// neckWidth, maxBulge, separationThreshold, unused
1816
+ unionP : vec4f,
1817
+ counts : vec4u,
1818
+ };
1819
+
1820
+ // Per-surface optical scalars ride through the union in 'aux': the group is one
1821
+ // field but its members are not one size, and carrying them per pixel is what
1822
+ // lets a 40 px button and a 320 px platter share a field pass and still lens by
1823
+ // their own depth \u2014 parent acceptance #2 inside a GlassEffectContainer.
1824
+ // aux = (lensDepthPx, glow, thicknessPx, press)
1825
+
1826
+ @group(0) @binding(0) var<uniform> fu : FieldUniforms;
1827
+ @group(0) @binding(1) var<storage, read> instances : array<Instance>;
1828
+
1829
+ struct Member {
1830
+ d : f32,
1831
+ g : vec2f,
1832
+ aux : vec4f,
1833
+ };
1834
+
1835
+ fn eval_instance(i : u32, p : vec2f) -> Member {
1836
+ let s = instances[i];
1837
+ let k = vec4f(s.k0, s.k1, s.k2, s.k3);
1838
+ // X8 rider 2 in one line: the child is the parent's field, shifted. There is
1839
+ // no branch and no second path, so no caller can opt into the instantiated
1840
+ // shape whose offset error the rider rules out.
1841
+ let f = FIELD_FN(p - s.centre, s.half, s.re, k, s.k4);
1842
+ var m : Member;
1843
+ m.d = f.d + s.inset;
1844
+ m.g = f.g;
1845
+ m.aux = vec4f(s.lensDepth, s.glow, s.thick, s.press);
1846
+ return m;
1847
+ }
1848
+
1849
+ struct FieldOut {
1850
+ @location(0) field : vec4f,
1851
+ @location(1) aux : vec4f,
1852
+ };
1853
+
1854
+ @fragment
1855
+ fn fs_field(in : FullscreenOut) -> FieldOut {
1856
+ // GROUP-LOCAL CSS px. The instance buffer's centres were already made relative
1857
+ // to the group's rect on the CPU (f32 loses resolution at large magnitudes, so
1858
+ // a field evaluated at viewport y = 40000 would quantise its own corner), and
1859
+ // adding the origin back here would offset every shape by it twice.
1860
+ let p = in.uv * fu.screen.xy * fu.screen.z;
1861
+
1862
+ var out : FieldOut;
1863
+ let count = fu.counts.x;
1864
+ if (count == 0u) {
1865
+ // Large but FINITE: rgba16float overflows to +Inf past ~65504, and NaN
1866
+ // coverage downstream is not reliably zero.
1867
+ out.field = vec4f(65000.0, 0.0, -1.0, 0.0);
1868
+ out.aux = vec4f(0.0);
1869
+ return out;
1870
+ }
1871
+
1872
+ var acc = eval_instance(0u, p);
1873
+ var nearest = acc.d;
1874
+ for (var i = 1u; i < count; i = i + 1u) {
1875
+ let s = eval_instance(i, p);
1876
+ let blend = union_blend(acc.d, s.d, fu.unionP.xyz);
1877
+ let h = blend.y;
1878
+ acc.d = blend.x;
1879
+ acc.g = mix(s.g, acc.g, h);
1880
+ acc.aux = mix(s.aux, acc.aux, h);
1881
+ nearest = min(nearest, s.d);
1882
+ }
1883
+ // One clamp at the end of the fold, so |union - min| <= maxBulge holds for any
1884
+ // member count and any order rather than per blend step.
1885
+ acc.d = max(acc.d, nearest - fu.unionP.y);
1886
+
1887
+ let len = max(length(acc.g), 1e-6);
1888
+ let normal = acc.g / len;
1889
+ let coverage = clamp(0.5 - acc.d / max(fu.screen.w, 1e-6), 0.0, 1.0);
1890
+
1891
+ out.field = vec4f(acc.d, normal.x, normal.y, coverage);
1892
+ out.aux = acc.aux;
1893
+ return out;
1894
+ }`;
1895
+ function fieldPassSource(family) {
1896
+ const kernels = family === "rsupn" ? [WGSL_RSUPN, WGSL_RSUPN_GRAD] : [WGSL_RSUP, WGSL_RSUP_GRAD];
1897
+ const fn = family === "rsupn" ? "sd_rsupn_grad" : "sd_rsup_grad";
1898
+ return [
1899
+ WGSL_INSTANCE_STRUCT,
1900
+ WGSL_FIELD_SAMPLE,
1901
+ ...kernels,
1902
+ WGSL_SMOOTH_UNION,
1903
+ WGSL_FIELD_PASS.replace("FIELD_FN", fn)
1904
+ ].join("\n\n");
1905
+ }
1906
+ var WGSL_FIELD_KERNELS = [
1907
+ WGSL_INSTANCE_STRUCT,
1908
+ WGSL_FIELD_SAMPLE,
1909
+ WGSL_RSUPN,
1910
+ WGSL_RSUP,
1911
+ WGSL_RSUPN_GRAD,
1912
+ WGSL_RSUP_GRAD,
1913
+ WGSL_SMOOTH_UNION
1914
+ ].join("\n\n");
1915
+
1916
+ // ../renderer-webgpu/dist/wgsl/highlight.js
1917
+ var WGSL_HIGHLIGHT_PASS = `struct HighlightUniforms {
1918
+ /// viewport size in device px (xy), CSS px per device px (z), unused (w)
1919
+ screen : vec4f,
1920
+ /// sweep position 0..1 (x), band width in radians (y), sweep gain (z), rim width px (w)
1921
+ sweep : vec4f,
1922
+ /// press point in viewport CSS px (xy), glow radius px (z), glow gain (w)
1923
+ glow : vec4f,
1924
+ /// highlight colour, linear light (xyz), unused (w)
1925
+ colour : vec4f,
1926
+ /// fieldSize.xy, fieldUpsampled (z), unused (w)
1927
+ flags : vec4f,
1928
+ };
1929
+
1930
+ @group(0) @binding(0) var<uniform> hu : HighlightUniforms;
1931
+ @group(0) @binding(1) var fieldTexture : texture_2d<f32>;
1932
+ @group(0) @binding(2) var auxTexture : texture_2d<f32>;
1933
+ @group(0) @binding(3) var fieldSampler : sampler;
1934
+
1935
+ const TAU = 6.283185307179586;
1936
+
1937
+ /// Shortest angular distance, so the band wraps continuously past the seam
1938
+ /// instead of stalling there for one revolution.
1939
+ fn angle_delta(a : f32, b : f32) -> f32 {
1940
+ let raw = abs(a - b);
1941
+ return min(raw, TAU - raw);
1942
+ }
1943
+
1944
+ @fragment
1945
+ fn fs_highlight(in : FullscreenOut) -> @location(0) vec4f {
1946
+ // Exact load nominally; filtered when the governor's resolution knob had the
1947
+ // field rasterised below the group's rect. The sweep rides the rim, which is
1948
+ // the one place a nearest read of a coarse field would show its grid.
1949
+ var field : vec4f;
1950
+ var aux : vec4f;
1951
+ if (hu.flags.z > 0.5) {
1952
+ field = textureSampleLevel(fieldTexture, fieldSampler, in.uv, 0.0);
1953
+ aux = textureSampleLevel(auxTexture, fieldSampler, in.uv, 0.0);
1954
+ } else {
1955
+ let texel = vec2i(in.uv * hu.flags.xy);
1956
+ field = textureLoad(fieldTexture, texel, 0);
1957
+ aux = textureLoad(auxTexture, texel, 0);
1958
+ }
1959
+ let d = field.x;
1960
+ let normal = field.yz;
1961
+ let coverage = field.w;
1962
+
1963
+ if (coverage <= 0.0) {
1964
+ return vec4f(0.0);
1965
+ }
1966
+
1967
+ // Specular sweep: a travelling band in the rim's angular coordinate.
1968
+ let rim = clamp(1.0 - abs(d) / max(hu.sweep.w, 1e-4), 0.0, 1.0);
1969
+ let theta = atan2(normal.y, normal.x) + TAU * 0.5;
1970
+ let centre = hu.sweep.x * TAU;
1971
+ let width = max(hu.sweep.y, 1e-4);
1972
+ let band = exp(-pow(angle_delta(theta, centre) / width, 2.0));
1973
+ let sweep = rim * rim * band * hu.sweep.z;
1974
+
1975
+ // Press glow: radial, in CSS px, clipped by coverage so it cannot leak past
1976
+ // the material's edge.
1977
+ let posCss = in.position.xy * hu.screen.z;
1978
+ let dist = length(posCss - hu.glow.xy);
1979
+ let radial = clamp(1.0 - dist / max(hu.glow.z, 1e-4), 0.0, 1.0);
1980
+ // 'aux.y' is the per-pixel glow channel, unioned in the field pass, so a group
1981
+ // whose members glow independently does not need a pass each.
1982
+ let press = radial * radial * hu.glow.w * aux.y;
1983
+
1984
+ let intensity = clamp(sweep + press, 0.0, 1.0) * coverage;
1985
+ if (intensity <= 0.0) {
1986
+ return vec4f(0.0);
1987
+ }
1988
+ return encode_output(hu.colour.rgb, intensity);
1989
+ }`;
1990
+
1991
+ // ../renderer-webgpu/dist/wgsl/optics.js
1992
+ var WGSL_OPTICS_PASS = `struct OpticsUniforms {
1993
+ /// viewport size in device px (xy), CSS px per device px (z), coverage ramp px (w)
1994
+ screen : vec4f,
1995
+ /// backdrop uv transform on viewport-normalised coords: scale (xy), offset (zw)
1996
+ fit : vec4f,
1997
+ /// refractionScale, bodyLodPerPx, rimLodBias, chainMaxLod
1998
+ lens : vec4f,
1999
+ /// fixed tint colour, linear light (xyz), tint alpha (w)
2000
+ tint : vec4f,
2001
+ /// adapted tint colour, linear light (xyz), adaptation strength (w)
2002
+ adapt : vec4f,
2003
+ /// rimWidthPx, rimAlpha, specularPower, specularGain
2004
+ rim : vec4f,
2005
+ /// light direction, unit (xy), shadowDepth (z), shadowAlpha (w)
2006
+ light : vec4f,
2007
+ /// hasBackdrop, fieldSize.xy, fieldUpsampled
2008
+ flags : vec4f,
2009
+ };
2010
+
2011
+ @group(0) @binding(0) var<uniform> ou : OpticsUniforms;
2012
+ @group(0) @binding(1) var fieldTexture : texture_2d<f32>;
2013
+ @group(0) @binding(2) var auxTexture : texture_2d<f32>;
2014
+ @group(0) @binding(3) var backdropSampler : sampler;
2015
+ @group(0) @binding(4) var backdropChain : texture_2d<f32>;
2016
+ @group(0) @binding(5) var backdropBody : texture_2d<f32>;
2017
+ @group(0) @binding(6) var fieldSampler : sampler;
2018
+
2019
+ /// Rim proximity: 1 exactly on the contour, falling to 0 by 'width' on either
2020
+ /// side. Symmetric, so the rim is a band on the boundary rather than a plateau
2021
+ /// that keeps burning outward where coverage has already faded.
2022
+ fn rim_weight(d : f32, width : f32) -> f32 {
2023
+ let t = clamp(1.0 - abs(d) / max(width, 1e-4), 0.0, 1.0);
2024
+ return t * t;
2025
+ }
2026
+
2027
+ @fragment
2028
+ fn fs_optics(in : FullscreenOut) -> @location(0) vec4f {
2029
+ // Nominally the field is one texel per device pixel, so the read is an exact
2030
+ // load and no filter touches the distance or the normal. Under the governor's
2031
+ // 'refractionResolutionScale' the field was rasterised smaller than the group's
2032
+ // rect, and then it has to be filtered: a nearest read would quantise the
2033
+ // contour to the coarse grid and the rim would step along it.
2034
+ var field : vec4f;
2035
+ var aux : vec4f;
2036
+ if (ou.flags.w > 0.5) {
2037
+ field = textureSampleLevel(fieldTexture, fieldSampler, in.uv, 0.0);
2038
+ aux = textureSampleLevel(auxTexture, fieldSampler, in.uv, 0.0);
2039
+ } else {
2040
+ let texel = vec2i(in.uv * ou.flags.yz);
2041
+ field = textureLoad(fieldTexture, texel, 0);
2042
+ aux = textureLoad(auxTexture, texel, 0);
2043
+ }
2044
+
2045
+ let d = field.x;
2046
+ let normal = field.yz;
2047
+ let coverage = field.w;
2048
+ if (coverage <= 0.0) {
2049
+ return vec4f(0.0);
2050
+ }
2051
+
2052
+ let viewport01 = in.position.xy / ou.screen.xy;
2053
+ let viewportCss = ou.screen.xy * ou.screen.z;
2054
+
2055
+ // Per-pixel, unioned through the field pass. See the module note.
2056
+ let lensDepth = max(aux.x, 1e-4);
2057
+ // '-d' is depth inside the surface, so the profile runs 1 at the contour to 0
2058
+ // at 'lensDepth' inward, and the square makes the falloff read as curvature
2059
+ // rather than as a linear ramp.
2060
+ let depth = clamp(-d / lensDepth, 0.0, 1.0);
2061
+ let profile = (1.0 - depth) * (1.0 - depth);
2062
+
2063
+ let displaceCss = -normal * lensDepth * profile * ou.lens.x;
2064
+ let refracted01 = viewport01 + displaceCss / viewportCss;
2065
+
2066
+ let straightUv = clamp(viewport01 * ou.fit.xy + ou.fit.zw, vec2f(0.0), vec2f(1.0));
2067
+ let refractedUv = clamp(refracted01 * ou.fit.xy + ou.fit.zw, vec2f(0.0), vec2f(1.0));
2068
+
2069
+ let bodyLod = clamp(lensDepth * ou.lens.y, 0.0, ou.lens.w);
2070
+ let lod = clamp(bodyLod - ou.lens.z * profile, 0.0, ou.lens.w);
2071
+
2072
+ var backdrop = vec3f(0.0);
2073
+ if (ou.flags.x > 0.5) {
2074
+ let lensSample = textureSampleLevel(backdropChain, backdropSampler, refractedUv, lod);
2075
+ let bodySample = textureSampleLevel(backdropBody, backdropSampler, straightUv, 0.0);
2076
+ // Premultiplied linear in, straight colour out: the material composites over
2077
+ // whatever is behind it, so a partially transparent backdrop must not darken
2078
+ // the glass.
2079
+ let lensColour = lensSample.rgb / max(lensSample.a, 1e-6);
2080
+ let bodyColour = bodySample.rgb / max(bodySample.a, 1e-6);
2081
+ backdrop = mix(bodyColour, lensColour, profile);
2082
+ }
2083
+
2084
+ // Adaptive tint. 'adapt.w' is the strength the accessibility policy and the
2085
+ // group's analysis quality already agreed on; at 0 the fixed tint stands, which
2086
+ // is what a 'hint' or 'none' group gets.
2087
+ let tintColour = mix(ou.tint.rgb, ou.adapt.rgb, ou.adapt.w);
2088
+ var colour = mix(backdrop, tintColour, ou.tint.w);
2089
+
2090
+ // Inner shadow: the material's own occlusion, deepest where the lens is
2091
+ // strongest, which is what makes a thicker surface read as heavier.
2092
+ colour = colour * (1.0 - profile * ou.light.z * ou.light.w);
2093
+
2094
+ // Rim and specular from the gradient. The rim is unlit ambient edge brightness;
2095
+ // the specular term is the same edge lit from 'light.xy'.
2096
+ let rw = rim_weight(d, ou.rim.x);
2097
+ let facing = dot(normal, ou.light.xy);
2098
+ let spec = pow(clamp(facing, 0.0, 1.0), max(ou.rim.z, 1e-3)) * ou.rim.w;
2099
+ colour = colour + vec3f(rw * (ou.rim.y + spec));
2100
+
2101
+ return encode_output(max(colour, vec3f(0.0)), coverage);
2102
+ }`;
2103
+
2104
+ // ../renderer-webgpu/dist/wgsl/prelude.js
2105
+ var WGSL_PRELUDE = `// vitrea:wgsl-marker
2106
+ // ---------------------------------------------------------------------------
2107
+ // X5 colour pipeline. Piecewise sRGB, matching color.ts channel for channel:
2108
+ // a golden regenerated against one and asserted against the other must not
2109
+ // drift by a code unit, so the 2.2-gamma approximation is not used anywhere.
2110
+ // ---------------------------------------------------------------------------
2111
+
2112
+ fn srgb_to_linear(c : vec3f) -> vec3f {
2113
+ let lo = c / 12.92;
2114
+ let hi = pow((c + vec3f(0.055)) / 1.055, vec3f(2.4));
2115
+ return select(hi, lo, c <= vec3f(0.04045));
2116
+ }
2117
+
2118
+ fn linear_to_srgb(c : vec3f) -> vec3f {
2119
+ let lo = c * 12.92;
2120
+ let hi = 1.055 * pow(max(c, vec3f(0.0)), vec3f(1.0 / 2.4)) - vec3f(0.055);
2121
+ return select(hi, lo, c <= vec3f(0.0031308));
2122
+ }
2123
+
2124
+ /// Rec.709 weights on LINEAR light. Weighting encoded values would make the
2125
+ /// analysis pass measure something that is not energy.
2126
+ fn luminance(linear : vec3f) -> f32 {
2127
+ return dot(linear, vec3f(0.2126, 0.7152, 0.0722));
2128
+ }
2129
+
2130
+ /// The one output encoding: encode, then premultiply in the ENCODED space,
2131
+ /// because that is the space the browser composites a canvas in. Premultiplying
2132
+ /// in linear and encoding afterwards darkens every partially covered pixel.
2133
+ fn encode_output(linear : vec3f, alpha : f32) -> vec4f {
2134
+ let a = clamp(alpha, 0.0, 1.0);
2135
+ return vec4f(linear_to_srgb(linear) * a, a);
2136
+ }
2137
+
2138
+ // ---------------------------------------------------------------------------
2139
+ // Fullscreen triangle. Three vertices, no vertex buffer, no index buffer: the
2140
+ // oversized triangle is clipped to the target and costs one fewer primitive
2141
+ // than a quad, and every pass here writes exactly one screen-space region.
2142
+ // ---------------------------------------------------------------------------
2143
+
2144
+ struct FullscreenOut {
2145
+ @builtin(position) position : vec4f,
2146
+ @location(0) uv : vec2f,
2147
+ };
2148
+
2149
+ @vertex
2150
+ fn vs_fullscreen(@builtin(vertex_index) index : u32) -> FullscreenOut {
2151
+ // (-1,-1) (3,-1) (-1,3) in clip space; uv follows with y flipped so (0,0) is
2152
+ // the target's top-left, matching texture and viewport conventions.
2153
+ let x = f32((index << 1u) & 2u) * 2.0 - 1.0;
2154
+ let y = f32(index & 2u) * 2.0 - 1.0;
2155
+ var out : FullscreenOut;
2156
+ out.position = vec4f(x, y, 0.0, 1.0);
2157
+ out.uv = vec2f((x + 1.0) * 0.5, (1.0 - y) * 0.5);
2158
+ return out;
2159
+ }
2160
+ `;
2161
+
2162
+ // ../renderer-webgpu/dist/wgsl/index.js
2163
+ var withPrelude = (pass) => `${WGSL_PRELUDE}
2164
+ ${pass}
2165
+ `;
2166
+ var fieldModule = (family) => withPrelude(fieldPassSource(family));
2167
+ var importModule = (kind) => withPrelude(importPassSource(kind));
2168
+ var chainModule = () => withPrelude(WGSL_DOWNSAMPLE_PASS);
2169
+ var analysisModule = () => withPrelude(WGSL_ANALYSIS_PASS);
2170
+ var opticsModule = () => withPrelude(WGSL_OPTICS_PASS);
2171
+ var highlightModule = () => withPrelude(WGSL_HIGHLIGHT_PASS);
2172
+ var crossCheckKernelModule = () => withPrelude([WGSL_FIELD_KERNELS, WGSL_CROSS_CHECK_PASS].join("\n\n"));
2173
+ function allShaderSource() {
2174
+ return [
2175
+ WGSL_PRELUDE,
2176
+ fieldPassSource("rsupn"),
2177
+ fieldPassSource("rsup"),
2178
+ importPassSource("sampled"),
2179
+ importPassSource("external"),
2180
+ WGSL_DOWNSAMPLE_PASS,
2181
+ WGSL_ANALYSIS_PASS,
2182
+ WGSL_OPTICS_PASS,
2183
+ WGSL_HIGHLIGHT_PASS
2184
+ ].join("\n\n");
2185
+ }
2186
+
2187
+ // ../renderer-webgpu/dist/passes.js
2188
+ var fieldUsage = () => GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
2189
+ var PREMULTIPLIED_OVER = {
2190
+ color: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" },
2191
+ alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
2192
+ };
2193
+ function createPassRunner(context) {
2194
+ const { device, pool, cache } = context;
2195
+ const uniforms = /* @__PURE__ */ new Map();
2196
+ const storages = /* @__PURE__ */ new Map();
2197
+ const EMPTY_DISTANCE = 65e3;
2198
+ const placeholder = device.createTexture({
2199
+ label: "vitrea:placeholder",
2200
+ size: { width: 1, height: 1, depthOrArrayLayers: 1 },
2201
+ format: WORKING_TEXTURE_FORMAT,
2202
+ usage: fieldUsage()
2203
+ });
2204
+ const uniformSlot = (key, floats) => {
2205
+ let slot = uniforms.get(key);
2206
+ if (slot === void 0) {
2207
+ slot = createUniformSlot(device, floats, `vitrea:uniform:${key}`);
2208
+ uniforms.set(key, slot);
2209
+ }
2210
+ return slot;
2211
+ };
2212
+ const storageSlot = (key) => {
2213
+ let slot = storages.get(key);
2214
+ if (slot === void 0) {
2215
+ slot = createStorageSlot(device, 8 * INSTANCE_BYTES, `vitrea:instances:${key}`);
2216
+ storages.set(key, slot);
2217
+ }
2218
+ return slot;
2219
+ };
2220
+ const fieldPipeline = (family) => cache.renderPipeline(pipelineKey.field(family, WORKING_TEXTURE_FORMAT), () => {
2221
+ const module = cache.module(`module:field:${family}`, () => fieldModule(family));
2222
+ return {
2223
+ label: `vitrea:pipeline:field:${family}`,
2224
+ layout: "auto",
2225
+ vertex: { module, entryPoint: "vs_fullscreen" },
2226
+ fragment: {
2227
+ module,
2228
+ entryPoint: "fs_field",
2229
+ targets: [{ format: WORKING_TEXTURE_FORMAT }, { format: WORKING_TEXTURE_FORMAT }]
2230
+ },
2231
+ primitive: { topology: "triangle-list" }
2232
+ };
2233
+ });
2234
+ const opticsPipeline = (format) => cache.renderPipeline(pipelineKey.optics(format, "premultiplied-over"), () => {
2235
+ const module = cache.module("module:optics", opticsModule);
2236
+ return {
2237
+ label: "vitrea:pipeline:optics",
2238
+ layout: "auto",
2239
+ vertex: { module, entryPoint: "vs_fullscreen" },
2240
+ fragment: {
2241
+ module,
2242
+ entryPoint: "fs_optics",
2243
+ targets: [{ format, blend: PREMULTIPLIED_OVER }]
2244
+ },
2245
+ primitive: { topology: "triangle-list" }
2246
+ };
2247
+ });
2248
+ const highlightPipeline = (format) => cache.renderPipeline(pipelineKey.highlight(format, "premultiplied-over"), () => {
2249
+ const module = cache.module("module:highlight", highlightModule);
2250
+ return {
2251
+ label: "vitrea:pipeline:highlight",
2252
+ layout: "auto",
2253
+ vertex: { module, entryPoint: "vs_fullscreen" },
2254
+ fragment: {
2255
+ module,
2256
+ entryPoint: "fs_highlight",
2257
+ targets: [{ format, blend: PREMULTIPLIED_OVER }]
2258
+ },
2259
+ primitive: { topology: "triangle-list" }
2260
+ };
2261
+ });
2262
+ const scoped = (pass, rect) => {
2263
+ pass.setViewport(rect.x, rect.y, rect.width, rect.height, 0, 1);
2264
+ pass.setScissorRect(rect.x, rect.y, rect.width, rect.height);
2265
+ };
2266
+ const placeholderView = placeholder.createView();
2267
+ let timeline;
2268
+ const timed = (label) => {
2269
+ const slot = timeline?.renderSlot(label);
2270
+ return slot === void 0 ? {} : { timestampWrites: slot };
2271
+ };
2272
+ return {
2273
+ placeholderView,
2274
+ fieldPass(encoder, args) {
2275
+ const rectWidth = Math.max(1, Math.round(args.rectDevice.width));
2276
+ const rectHeight = Math.max(1, Math.round(args.rectDevice.height));
2277
+ const width = Math.max(1, Math.round(args.rectDevice.width * args.renderScale));
2278
+ const height = Math.max(1, Math.round(args.rectDevice.height * args.renderScale));
2279
+ const field = pool.acquire(poolKey.groupField(args.groupId), {
2280
+ width,
2281
+ height,
2282
+ format: WORKING_TEXTURE_FORMAT,
2283
+ usage: fieldUsage(),
2284
+ label: `vitrea:group:${args.groupId}:field`
2285
+ });
2286
+ const aux = pool.acquire(poolKey.groupAux(args.groupId), {
2287
+ width,
2288
+ height,
2289
+ format: WORKING_TEXTURE_FORMAT,
2290
+ usage: fieldUsage(),
2291
+ label: `vitrea:group:${args.groupId}:aux`
2292
+ });
2293
+ const slot = uniformSlot(`field:${args.groupId}`, 12);
2294
+ slot.data[0] = rectWidth;
2295
+ slot.data[1] = rectHeight;
2296
+ slot.data[2] = args.cssPerDevice;
2297
+ slot.data[3] = args.coverageRampCss * (rectWidth / width);
2298
+ slot.data[4] = args.union.neckWidth;
2299
+ slot.data[5] = args.union.maxBulge;
2300
+ slot.data[6] = args.union.separationThreshold;
2301
+ slot.data[7] = 0;
2302
+ slot.data[9] = 0;
2303
+ slot.data[10] = 0;
2304
+ slot.data[11] = 0;
2305
+ new Uint32Array(slot.data.buffer, slot.data.byteOffset + 32, 1)[0] = args.instanceCount;
2306
+ slot.write();
2307
+ const instances = storageSlot(args.groupId);
2308
+ instances.ensure(Math.max(args.instanceCount, 1) * INSTANCE_BYTES);
2309
+ instances.write(args.instances, Math.max(args.instanceCount, 1) * (INSTANCE_BYTES / 4));
2310
+ const pipeline = fieldPipeline(args.family);
2311
+ const pass = encoder.beginRenderPass({
2312
+ label: `vitrea:pass:field:${args.groupId}`,
2313
+ ...timed(PASS_LABEL.field),
2314
+ colorAttachments: [
2315
+ {
2316
+ view: field.createView(),
2317
+ loadOp: "clear",
2318
+ storeOp: "store",
2319
+ clearValue: { r: EMPTY_DISTANCE, g: 0, b: -1, a: 0 }
2320
+ },
2321
+ {
2322
+ view: aux.createView(),
2323
+ loadOp: "clear",
2324
+ storeOp: "store",
2325
+ clearValue: { r: 0, g: 0, b: 0, a: 0 }
2326
+ }
2327
+ ]
2328
+ });
2329
+ pass.setPipeline(pipeline);
2330
+ pass.setBindGroup(0, device.createBindGroup({
2331
+ layout: pipeline.getBindGroupLayout(0),
2332
+ entries: [
2333
+ { binding: 0, resource: { buffer: slot.buffer } },
2334
+ { binding: 1, resource: { buffer: instances.buffer } }
2335
+ ]
2336
+ }));
2337
+ pass.draw(3);
2338
+ pass.end();
2339
+ return {
2340
+ field,
2341
+ aux,
2342
+ width,
2343
+ height,
2344
+ upsampled: width !== rectWidth || height !== rectHeight
2345
+ };
2346
+ },
2347
+ opticsPass(encoder, args) {
2348
+ const slot = uniformSlot(`optics:${args.groupId}`, 32);
2349
+ const d = slot.data;
2350
+ d[0] = args.viewportDevice[0];
2351
+ d[1] = args.viewportDevice[1];
2352
+ d[2] = args.cssPerDevice;
2353
+ d[3] = args.coverageRampCss;
2354
+ d[4] = args.fit[0];
2355
+ d[5] = args.fit[1];
2356
+ d[6] = args.fit[2];
2357
+ d[7] = args.fit[3];
2358
+ d[8] = args.refractionScale;
2359
+ d[9] = args.bodyLodPerPx;
2360
+ d[10] = args.rimLodBias;
2361
+ d[11] = args.chainMaxLod;
2362
+ d[12] = args.tint[0];
2363
+ d[13] = args.tint[1];
2364
+ d[14] = args.tint[2];
2365
+ d[15] = args.tintAlpha;
2366
+ d[16] = args.adaptTint[0];
2367
+ d[17] = args.adaptTint[1];
2368
+ d[18] = args.adaptTint[2];
2369
+ d[19] = args.adaptStrength;
2370
+ d[20] = args.rimWidth;
2371
+ d[21] = args.rimAlpha;
2372
+ d[22] = args.specularPower;
2373
+ d[23] = args.specularGain;
2374
+ d[24] = args.lightDirection[0];
2375
+ d[25] = args.lightDirection[1];
2376
+ d[26] = args.shadowDepth;
2377
+ d[27] = args.shadowAlpha;
2378
+ d[28] = args.backdrop === void 0 ? 0 : 1;
2379
+ d[29] = args.fields.width;
2380
+ d[30] = args.fields.height;
2381
+ d[31] = args.fields.upsampled ? 1 : 0;
2382
+ slot.write();
2383
+ const chain = args.backdrop?.chain ?? placeholderView;
2384
+ const body = args.backdrop?.body ?? placeholderView;
2385
+ const pipeline = opticsPipeline(args.targetFormat);
2386
+ const pass = encoder.beginRenderPass({
2387
+ label: `vitrea:pass:optics:${args.groupId}`,
2388
+ ...timed(PASS_LABEL.optics),
2389
+ colorAttachments: [{ view: args.target, loadOp: "load", storeOp: "store" }]
2390
+ });
2391
+ pass.setPipeline(pipeline);
2392
+ scoped(pass, args.rectDevice);
2393
+ pass.setBindGroup(0, device.createBindGroup({
2394
+ layout: pipeline.getBindGroupLayout(0),
2395
+ entries: [
2396
+ { binding: 0, resource: { buffer: slot.buffer } },
2397
+ { binding: 1, resource: args.fields.field.createView() },
2398
+ { binding: 2, resource: args.fields.aux.createView() },
2399
+ { binding: 3, resource: context.chainSampler },
2400
+ { binding: 4, resource: chain },
2401
+ { binding: 5, resource: body },
2402
+ // Linear, no mips: the field targets have one level, and this is only
2403
+ // read at all when the governor shrank them below the group's rect.
2404
+ { binding: 6, resource: context.flatSampler }
2405
+ ]
2406
+ }));
2407
+ pass.draw(3);
2408
+ pass.end();
2409
+ },
2410
+ highlightPass(encoder, args) {
2411
+ const slot = uniformSlot(`highlight:${args.groupId}`, 20);
2412
+ const d = slot.data;
2413
+ d[0] = args.viewportDevice[0];
2414
+ d[1] = args.viewportDevice[1];
2415
+ d[2] = args.cssPerDevice;
2416
+ d[3] = 0;
2417
+ d[4] = args.sweep;
2418
+ d[5] = args.sweepBandRadians;
2419
+ d[6] = args.sweepGain;
2420
+ d[7] = args.rimWidth;
2421
+ d[8] = args.pressPointCss[0];
2422
+ d[9] = args.pressPointCss[1];
2423
+ d[10] = args.glowRadiusCss;
2424
+ d[11] = args.glowGain;
2425
+ d[12] = args.colour[0];
2426
+ d[13] = args.colour[1];
2427
+ d[14] = args.colour[2];
2428
+ d[15] = 0;
2429
+ d[16] = args.fields.width;
2430
+ d[17] = args.fields.height;
2431
+ d[18] = args.fields.upsampled ? 1 : 0;
2432
+ d[19] = 0;
2433
+ slot.write();
2434
+ const pipeline = highlightPipeline(args.targetFormat);
2435
+ const pass = encoder.beginRenderPass({
2436
+ label: `vitrea:pass:highlight:${args.groupId}`,
2437
+ ...timed(PASS_LABEL.highlight),
2438
+ colorAttachments: [{ view: args.target, loadOp: "load", storeOp: "store" }]
2439
+ });
2440
+ pass.setPipeline(pipeline);
2441
+ scoped(pass, args.rectDevice);
2442
+ pass.setBindGroup(0, device.createBindGroup({
2443
+ layout: pipeline.getBindGroupLayout(0),
2444
+ entries: [
2445
+ { binding: 0, resource: { buffer: slot.buffer } },
2446
+ { binding: 1, resource: args.fields.field.createView() },
2447
+ { binding: 2, resource: args.fields.aux.createView() },
2448
+ { binding: 3, resource: context.flatSampler }
2449
+ ]
2450
+ }));
2451
+ pass.draw(3);
2452
+ pass.end();
2453
+ },
2454
+ clearPass(encoder, target) {
2455
+ const pass = encoder.beginRenderPass({
2456
+ label: "vitrea:pass:clear",
2457
+ colorAttachments: [
2458
+ {
2459
+ view: target,
2460
+ loadOp: "clear",
2461
+ storeOp: "store",
2462
+ clearValue: { r: 0, g: 0, b: 0, a: 0 }
2463
+ }
2464
+ ]
2465
+ });
2466
+ pass.end();
2467
+ },
2468
+ setTimeline(next) {
2469
+ timeline = next;
2470
+ },
2471
+ forget(groupId) {
2472
+ pool.release(poolKey.groupField(groupId));
2473
+ pool.release(poolKey.groupAux(groupId));
2474
+ storages.get(groupId)?.destroy();
2475
+ storages.delete(groupId);
2476
+ for (const key of [`field:${groupId}`, `optics:${groupId}`, `highlight:${groupId}`]) {
2477
+ uniforms.get(key)?.buffer.destroy();
2478
+ uniforms.delete(key);
2479
+ }
2480
+ },
2481
+ destroy() {
2482
+ for (const slot of uniforms.values())
2483
+ slot.buffer.destroy();
2484
+ uniforms.clear();
2485
+ for (const slot of storages.values())
2486
+ slot.destroy();
2487
+ storages.clear();
2488
+ placeholder.destroy();
2489
+ }
2490
+ };
2491
+ }
2492
+ var CANVAS_FORMAT = OUTPUT_TEXTURE_FORMAT;
2493
+
2494
+ // ../renderer-webgpu/dist/pyramid-plan.js
2495
+ var MIN_LEVEL_EXTENT = 8;
2496
+ var MAX_CHAIN_LEVELS = 12;
2497
+ var ANALYSIS_TARGET_EXTENT = 96;
2498
+ function planPyramid(sourceWidth, sourceHeight, policy) {
2499
+ const scale = Math.max(policy.scale, 1e-3);
2500
+ let width = Math.max(1, Math.round(sourceWidth * scale));
2501
+ let height = Math.max(1, Math.round(sourceHeight * scale));
2502
+ const cap = Math.max(MIN_LEVEL_EXTENT, Math.floor(policy.maxDimension));
2503
+ const longest = Math.max(width, height);
2504
+ if (longest > cap) {
2505
+ const shrink = cap / longest;
2506
+ width = Math.max(1, Math.round(width * shrink));
2507
+ height = Math.max(1, Math.round(height * shrink));
2508
+ }
2509
+ const levels = [{ width, height }];
2510
+ while (levels.length < MAX_CHAIN_LEVELS) {
2511
+ const previous = levels[levels.length - 1];
2512
+ const next = {
2513
+ width: Math.max(1, previous.width >> 1),
2514
+ height: Math.max(1, previous.height >> 1)
2515
+ };
2516
+ if (Math.min(next.width, next.height) < MIN_LEVEL_EXTENT)
2517
+ break;
2518
+ if (next.width === previous.width && next.height === previous.height)
2519
+ break;
2520
+ levels.push(next);
2521
+ }
2522
+ let analysisLevel = 0;
2523
+ let bestDistance = Number.POSITIVE_INFINITY;
2524
+ for (let i = 0; i < levels.length; i += 1) {
2525
+ const level = levels[i];
2526
+ const shorter = Math.min(level.width, level.height);
2527
+ const distance = Math.abs(Math.log2(shorter / ANALYSIS_TARGET_EXTENT));
2528
+ if (distance < bestDistance) {
2529
+ bestDistance = distance;
2530
+ analysisLevel = i;
2531
+ }
2532
+ }
2533
+ return {
2534
+ width,
2535
+ height,
2536
+ levelCount: levels.length,
2537
+ maxLod: levels.length - 1,
2538
+ analysisLevel,
2539
+ levels
2540
+ };
2541
+ }
2542
+ var CHAIN_SIGMA_AT_LEVEL_1 = 1.2;
2543
+ function bodyBlurPlan(sigmaPx, plan) {
2544
+ if (sigmaPx <= 0)
2545
+ return { level: 0, residualSigmaTexels: 0 };
2546
+ const sigmaAt = (level2) => level2 === 0 ? 0 : CHAIN_SIGMA_AT_LEVEL_1 * Math.pow(2, level2 - 1);
2547
+ let level = 0;
2548
+ for (let i = 1; i < plan.levelCount; i += 1) {
2549
+ if (sigmaAt(i) > sigmaPx)
2550
+ break;
2551
+ level = i;
2552
+ }
2553
+ const covered = sigmaAt(level);
2554
+ const residualLevel0 = Math.sqrt(Math.max(sigmaPx * sigmaPx - covered * covered, 0));
2555
+ return { level, residualSigmaTexels: residualLevel0 / Math.pow(2, level) };
2556
+ }
2557
+
2558
+ // ../renderer-webgpu/dist/rebuild-ledger.js
2559
+ function createRebuildLedger() {
2560
+ const perFrame = /* @__PURE__ */ new Map();
2561
+ let frameId;
2562
+ let rebuilds = 0;
2563
+ let refused = 0;
2564
+ let clean = 0;
2565
+ let peak = 0;
2566
+ return {
2567
+ beginFrame(next) {
2568
+ if (frameId === next)
2569
+ return;
2570
+ frameId = next;
2571
+ perFrame.clear();
2572
+ },
2573
+ get frameId() {
2574
+ return frameId;
2575
+ },
2576
+ claim(sourceId) {
2577
+ const already = perFrame.get(sourceId) ?? 0;
2578
+ if (already > 0) {
2579
+ refused += 1;
2580
+ return false;
2581
+ }
2582
+ const count = already + 1;
2583
+ perFrame.set(sourceId, count);
2584
+ peak = Math.max(peak, count);
2585
+ rebuilds += 1;
2586
+ return true;
2587
+ },
2588
+ recordClean() {
2589
+ clean += 1;
2590
+ },
2591
+ countInFrame(sourceId) {
2592
+ return perFrame.get(sourceId) ?? 0;
2593
+ },
2594
+ get rebuilds() {
2595
+ return rebuilds;
2596
+ },
2597
+ get refusedDuplicates() {
2598
+ return refused;
2599
+ },
2600
+ get skippedClean() {
2601
+ return clean;
2602
+ },
2603
+ get peakPerSourcePerFrame() {
2604
+ return peak;
2605
+ }
2606
+ };
2607
+ }
2608
+
2609
+ // ../renderer-webgpu/dist/pyramid.js
2610
+ var chainUsage = () => GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
2611
+ function createPyramidStore(context) {
2612
+ const { device, pool, cache } = context;
2613
+ const resources = /* @__PURE__ */ new Map();
2614
+ const uniforms = /* @__PURE__ */ new Map();
2615
+ const readbacks = /* @__PURE__ */ new Map();
2616
+ const pendingRelease = [];
2617
+ const pendingStats = /* @__PURE__ */ new Map();
2618
+ const pendingMaps = [];
2619
+ let timeline;
2620
+ const timedRender = (label) => {
2621
+ const slot = timeline?.renderSlot(label);
2622
+ return slot === void 0 ? {} : { timestampWrites: slot };
2623
+ };
2624
+ const timedCompute = (label) => {
2625
+ const slot = timeline?.computeSlot(label);
2626
+ return slot === void 0 ? {} : { timestampWrites: slot };
2627
+ };
2628
+ let frameId = -1;
2629
+ let reallocations = 0;
2630
+ const ledger = createRebuildLedger();
2631
+ const uniformSlot = (key, floats) => {
2632
+ let slot = uniforms.get(key);
2633
+ if (slot === void 0) {
2634
+ slot = createUniformSlot(device, floats, `vitrea:uniform:${key}`);
2635
+ uniforms.set(key, slot);
2636
+ }
2637
+ return slot;
2638
+ };
2639
+ const importPipeline = (kind) => cache.renderPipeline(pipelineKey.import(kind, WORKING_TEXTURE_FORMAT), () => ({
2640
+ label: `vitrea:pipeline:import:${kind}`,
2641
+ layout: "auto",
2642
+ vertex: {
2643
+ module: cache.module(`module:import:${kind}`, () => importModule(kind)),
2644
+ entryPoint: "vs_fullscreen"
2645
+ },
2646
+ fragment: {
2647
+ module: cache.module(`module:import:${kind}`, () => importModule(kind)),
2648
+ entryPoint: "fs_import",
2649
+ targets: [{ format: WORKING_TEXTURE_FORMAT }]
2650
+ },
2651
+ primitive: { topology: "triangle-list" }
2652
+ }));
2653
+ const chainPipeline = (entry) => cache.renderPipeline(pipelineKey.chain(entry, WORKING_TEXTURE_FORMAT), () => ({
2654
+ label: `vitrea:pipeline:chain:${entry}`,
2655
+ layout: "auto",
2656
+ vertex: {
2657
+ module: cache.module("module:chain", chainModule),
2658
+ entryPoint: "vs_fullscreen"
2659
+ },
2660
+ fragment: {
2661
+ module: cache.module("module:chain", chainModule),
2662
+ entryPoint: entry,
2663
+ targets: [{ format: WORKING_TEXTURE_FORMAT }]
2664
+ },
2665
+ primitive: { topology: "triangle-list" }
2666
+ }));
2667
+ const analysisPipeline = () => cache.computePipeline(pipelineKey.analysis(), () => ({
2668
+ label: "vitrea:pipeline:analysis",
2669
+ layout: "auto",
2670
+ compute: {
2671
+ module: cache.module("module:analysis", analysisModule),
2672
+ entryPoint: "cs_analysis"
2673
+ }
2674
+ }));
2675
+ const liveResources = (sourceId) => {
2676
+ const target = resources.get(sourceId);
2677
+ if (target === void 0)
2678
+ return void 0;
2679
+ if (pool.peek(poolKey.backdropChain(sourceId)) !== target.chain)
2680
+ return void 0;
2681
+ if (pool.peek(poolKey.backdropBody(sourceId)) !== target.body)
2682
+ return void 0;
2683
+ return target;
2684
+ };
2685
+ const mipView = (texture, level) => texture.createView({ baseMipLevel: level, mipLevelCount: 1, dimension: "2d" });
2686
+ function allocate(sourceId, plan, bodyLevel, sizeEpoch, builtEpoch) {
2687
+ const existing = resources.get(sourceId);
2688
+ const bodyWidth = (plan.levels[bodyLevel] ?? plan.levels[0]).width;
2689
+ const bodyHeight = (plan.levels[bodyLevel] ?? plan.levels[0]).height;
2690
+ const chain = pool.acquire(poolKey.backdropChain(sourceId), {
2691
+ width: plan.width,
2692
+ height: plan.height,
2693
+ format: WORKING_TEXTURE_FORMAT,
2694
+ usage: chainUsage(),
2695
+ mipLevelCount: plan.levelCount,
2696
+ label: `vitrea:pyramid:${sourceId}:chain`
2697
+ });
2698
+ const body = pool.acquire(poolKey.backdropBody(sourceId), {
2699
+ width: bodyWidth,
2700
+ height: bodyHeight,
2701
+ format: WORKING_TEXTURE_FORMAT,
2702
+ usage: chainUsage(),
2703
+ label: `vitrea:pyramid:${sourceId}:body`
2704
+ });
2705
+ let stats = existing?.stats;
2706
+ if (stats === void 0) {
2707
+ stats = device.createBuffer({
2708
+ label: `vitrea:pyramid:${sourceId}:stats`,
2709
+ size: ANALYSIS_STATS_FLOATS * 4,
2710
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
2711
+ });
2712
+ }
2713
+ if (existing === void 0 || existing.chain !== chain || existing.body !== body) {
2714
+ reallocations += 1;
2715
+ }
2716
+ const next = {
2717
+ sourceId,
2718
+ plan,
2719
+ chain,
2720
+ body,
2721
+ stats,
2722
+ sizeEpoch,
2723
+ builtEpoch
2724
+ };
2725
+ resources.set(sourceId, next);
2726
+ return next;
2727
+ }
2728
+ function runImport(encoder, sourceId, frame, chain) {
2729
+ const kind = frame.binding.kind;
2730
+ const pipeline = importPipeline(kind);
2731
+ const slot = uniformSlot(`import:${sourceId}`, 16);
2732
+ const matrix = importColorMatrix(frame.colorSpace);
2733
+ slot.data[0] = matrix[0];
2734
+ slot.data[1] = matrix[1];
2735
+ slot.data[2] = matrix[2];
2736
+ slot.data[3] = frame.encoded ? 1 : 0;
2737
+ slot.data[4] = matrix[3];
2738
+ slot.data[5] = matrix[4];
2739
+ slot.data[6] = matrix[5];
2740
+ slot.data[7] = alphaNormalisationMode(frame.alphaMode);
2741
+ slot.data[8] = matrix[6];
2742
+ slot.data[9] = matrix[7];
2743
+ slot.data[10] = matrix[8];
2744
+ slot.data[11] = 0;
2745
+ slot.data[12] = 1;
2746
+ slot.data[13] = 1;
2747
+ slot.data[14] = 0;
2748
+ slot.data[15] = 0;
2749
+ slot.write();
2750
+ const entries = [
2751
+ { binding: 0, resource: { buffer: slot.buffer } },
2752
+ { binding: 1, resource: context.flatSampler },
2753
+ {
2754
+ binding: 2,
2755
+ resource: frame.binding.kind === "external" ? frame.binding.texture : frame.binding.view
2756
+ }
2757
+ ];
2758
+ const pass = encoder.beginRenderPass({
2759
+ label: `vitrea:pass:import:${sourceId}`,
2760
+ ...timedRender(PASS_LABEL.import),
2761
+ colorAttachments: [
2762
+ { view: mipView(chain, 0), loadOp: "clear", storeOp: "store", clearValue: { r: 0, g: 0, b: 0, a: 0 } }
2763
+ ]
2764
+ });
2765
+ pass.setPipeline(pipeline);
2766
+ pass.setBindGroup(0, device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries }));
2767
+ pass.draw(3);
2768
+ pass.end();
2769
+ }
2770
+ function runChain(encoder, sourceId, plan, chain) {
2771
+ const pipeline = chainPipeline("fs_downsample");
2772
+ for (let level = 1; level < plan.levelCount; level += 1) {
2773
+ const source = plan.levels[level - 1];
2774
+ const slot = uniformSlot(`chain:${sourceId}:${level}`, 8);
2775
+ slot.data[0] = 1 / source.width;
2776
+ slot.data[1] = 1 / source.height;
2777
+ slot.data[2] = 0;
2778
+ slot.data[3] = 0;
2779
+ slot.data[4] = 0;
2780
+ slot.data[5] = 0;
2781
+ slot.data[6] = 0;
2782
+ slot.data[7] = 0;
2783
+ slot.write();
2784
+ const pass = encoder.beginRenderPass({
2785
+ label: `vitrea:pass:downsample:${sourceId}:${level}`,
2786
+ ...timedRender(PASS_LABEL.chain),
2787
+ colorAttachments: [
2788
+ { view: mipView(chain, level), loadOp: "clear", storeOp: "store", clearValue: { r: 0, g: 0, b: 0, a: 0 } }
2789
+ ]
2790
+ });
2791
+ pass.setPipeline(pipeline);
2792
+ pass.setBindGroup(0, device.createBindGroup({
2793
+ layout: pipeline.getBindGroupLayout(0),
2794
+ entries: [
2795
+ { binding: 0, resource: { buffer: slot.buffer } },
2796
+ { binding: 1, resource: context.flatSampler },
2797
+ { binding: 2, resource: mipView(chain, level - 1) }
2798
+ ]
2799
+ }));
2800
+ pass.draw(3);
2801
+ pass.end();
2802
+ }
2803
+ }
2804
+ function runBodyBlur(encoder, sourceId, plan, chain, body, level, residualSigmaTexels) {
2805
+ const pipeline = chainPipeline("fs_blur");
2806
+ const size = plan.levels[level] ?? plan.levels[0];
2807
+ const scratch = pool.acquire(poolKey.backdropBodyScratch(sourceId), {
2808
+ width: size.width,
2809
+ height: size.height,
2810
+ format: WORKING_TEXTURE_FORMAT,
2811
+ usage: chainUsage(),
2812
+ label: `vitrea:pyramid:${sourceId}:body-scratch`
2813
+ });
2814
+ const stages = [
2815
+ { target: scratch, source: mipView(chain, level), dir: [1, 0], tag: "h" },
2816
+ { target: body, source: scratch.createView(), dir: [0, 1], tag: "v" }
2817
+ ];
2818
+ for (const stage of stages) {
2819
+ const slot = uniformSlot(`body:${sourceId}:${stage.tag}`, 8);
2820
+ slot.data[0] = 1 / size.width;
2821
+ slot.data[1] = 1 / size.height;
2822
+ slot.data[2] = 0;
2823
+ slot.data[3] = 0;
2824
+ slot.data[4] = residualSigmaTexels;
2825
+ slot.data[5] = stage.dir[0];
2826
+ slot.data[6] = stage.dir[1];
2827
+ slot.data[7] = 0;
2828
+ slot.write();
2829
+ const pass = encoder.beginRenderPass({
2830
+ label: `vitrea:pass:body-blur-${stage.tag}:${sourceId}`,
2831
+ ...timedRender(PASS_LABEL.bodyBlur),
2832
+ colorAttachments: [
2833
+ { view: stage.target.createView(), loadOp: "clear", storeOp: "store", clearValue: { r: 0, g: 0, b: 0, a: 0 } }
2834
+ ]
2835
+ });
2836
+ pass.setPipeline(pipeline);
2837
+ pass.setBindGroup(0, device.createBindGroup({
2838
+ layout: pipeline.getBindGroupLayout(0),
2839
+ entries: [
2840
+ { binding: 0, resource: { buffer: slot.buffer } },
2841
+ { binding: 1, resource: context.flatSampler },
2842
+ { binding: 2, resource: stage.source }
2843
+ ]
2844
+ }));
2845
+ pass.draw(3);
2846
+ pass.end();
2847
+ }
2848
+ }
2849
+ function runAnalysis(encoder, sourceId, plan, chain, stats) {
2850
+ const pipeline = analysisPipeline();
2851
+ const level = plan.levels[plan.analysisLevel] ?? plan.levels[0];
2852
+ const slot = uniformSlot(`analysis:${sourceId}`, 8);
2853
+ slot.data[0] = ANALYSIS_GRID;
2854
+ slot.data[1] = ANALYSIS_GRID;
2855
+ slot.data[2] = plan.analysisLevel;
2856
+ slot.data[3] = 1 / (ANALYSIS_GRID - 1);
2857
+ slot.data[4] = 1 / level.width;
2858
+ slot.data[5] = 1 / level.height;
2859
+ slot.data[6] = 0;
2860
+ slot.data[7] = 0;
2861
+ slot.write();
2862
+ const pass = encoder.beginComputePass({
2863
+ label: `vitrea:pass:analysis:${sourceId}`,
2864
+ ...timedCompute(PASS_LABEL.analysis)
2865
+ });
2866
+ pass.setPipeline(pipeline);
2867
+ pass.setBindGroup(0, device.createBindGroup({
2868
+ layout: pipeline.getBindGroupLayout(0),
2869
+ entries: [
2870
+ { binding: 0, resource: { buffer: slot.buffer } },
2871
+ { binding: 1, resource: context.flatSampler },
2872
+ { binding: 2, resource: chain.createView() },
2873
+ { binding: 3, resource: { buffer: stats } }
2874
+ ]
2875
+ }));
2876
+ pass.dispatchWorkgroups(1);
2877
+ pass.end();
2878
+ }
2879
+ return {
2880
+ instrumentation: {
2881
+ get rebuilds() {
2882
+ return ledger.rebuilds;
2883
+ },
2884
+ get refusedDuplicates() {
2885
+ return ledger.refusedDuplicates;
2886
+ },
2887
+ get skippedClean() {
2888
+ return ledger.skippedClean;
2889
+ },
2890
+ get reallocations() {
2891
+ return reallocations;
2892
+ },
2893
+ rebuildsInFrame(sourceId) {
2894
+ return ledger.countInFrame(sourceId);
2895
+ },
2896
+ get peakRebuildsPerSourcePerFrame() {
2897
+ return ledger.peakPerSourcePerFrame;
2898
+ }
2899
+ },
2900
+ /** The ledger the invariant is asserted against. See `rebuild-ledger.ts`. */
2901
+ ledger,
2902
+ beginFrame(next) {
2903
+ frameId = next;
2904
+ ledger.beginFrame(next);
2905
+ },
2906
+ setTimeline(next) {
2907
+ timeline = next;
2908
+ },
2909
+ build(request, provider, encoder) {
2910
+ const existing = liveResources(request.sourceId);
2911
+ if (existing !== void 0 && existing.builtEpoch >= request.epoch && !provider.isDirty()) {
2912
+ ledger.recordClean();
2913
+ return { status: "clean", resources: existing };
2914
+ }
2915
+ if (!ledger.claim(request.sourceId)) {
2916
+ return { status: "duplicate" };
2917
+ }
2918
+ let frame;
2919
+ try {
2920
+ frame = provider.acquire({ id: frameId, timeMs: 0 });
2921
+ } catch (error) {
2922
+ return {
2923
+ status: "unavailable",
2924
+ reason: error instanceof Error ? error.message : String(error)
2925
+ };
2926
+ }
2927
+ pendingRelease.push(provider);
2928
+ const plan = planPyramid(frame.width, frame.height, request.resolution);
2929
+ const [viewportW, viewportH] = request.viewportCss;
2930
+ const cover = viewportW > 0 && viewportH > 0 ? Math.max(frame.width / viewportW, frame.height / viewportH) : 1;
2931
+ const planScale = frame.width > 0 ? plan.width / frame.width : 1;
2932
+ const bodyPlan = bodyBlurPlan(request.bodySigmaCss * cover * planScale, plan);
2933
+ const target = allocate(request.sourceId, plan, bodyPlan.level, frame.sizeEpoch, request.epoch);
2934
+ runImport(encoder, request.sourceId, frame, target.chain);
2935
+ runChain(encoder, request.sourceId, plan, target.chain);
2936
+ runBodyBlur(encoder, request.sourceId, plan, target.chain, target.body, bodyPlan.level, bodyPlan.residualSigmaTexels);
2937
+ runAnalysis(encoder, request.sourceId, plan, target.chain, target.stats);
2938
+ provider.markImported();
2939
+ return { status: "built", resources: target };
2940
+ },
2941
+ releaseAcquired() {
2942
+ while (pendingRelease.length > 0) {
2943
+ const provider = pendingRelease.shift();
2944
+ try {
2945
+ provider?.release();
2946
+ } catch {
2947
+ }
2948
+ }
2949
+ },
2950
+ afterSubmit() {
2951
+ while (pendingMaps.length > 0) {
2952
+ const sourceId = pendingMaps.shift();
2953
+ if (sourceId === void 0)
2954
+ continue;
2955
+ const slot = readbacks.get(sourceId);
2956
+ if (slot === void 0)
2957
+ continue;
2958
+ const staging = slot.staging;
2959
+ pendingStats.set(sourceId, (async () => {
2960
+ try {
2961
+ await staging.mapAsync(GPUMapMode.READ);
2962
+ const values = new Float32Array(staging.getMappedRange().slice(0));
2963
+ staging.unmap();
2964
+ return statsFromBuffer(values);
2965
+ } catch {
2966
+ return void 0;
2967
+ } finally {
2968
+ slot.inFlight = false;
2969
+ }
2970
+ })());
2971
+ }
2972
+ },
2973
+ resources(sourceId) {
2974
+ return liveResources(sourceId);
2975
+ },
2976
+ requestStats(sourceId, encoder) {
2977
+ const target = liveResources(sourceId);
2978
+ if (target === void 0)
2979
+ return false;
2980
+ let readback = readbacks.get(sourceId);
2981
+ if (readback === void 0) {
2982
+ readback = {
2983
+ staging: device.createBuffer({
2984
+ label: `vitrea:pyramid:${sourceId}:stats-staging`,
2985
+ size: ANALYSIS_STATS_FLOATS * 4,
2986
+ usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
2987
+ }),
2988
+ inFlight: false
2989
+ };
2990
+ readbacks.set(sourceId, readback);
2991
+ }
2992
+ if (readback.inFlight)
2993
+ return false;
2994
+ encoder.copyBufferToBuffer(target.stats, 0, readback.staging, 0, ANALYSIS_STATS_FLOATS * 4);
2995
+ readback.inFlight = true;
2996
+ pendingMaps.push(sourceId);
2997
+ return true;
2998
+ },
2999
+ async collectStats() {
3000
+ const out = /* @__PURE__ */ new Map();
3001
+ const entries = [...pendingStats];
3002
+ pendingStats.clear();
3003
+ for (const [sourceId, promise] of entries) {
3004
+ const stats = await promise;
3005
+ if (stats !== void 0)
3006
+ out.set(sourceId, stats);
3007
+ }
3008
+ return out;
3009
+ },
3010
+ forget(sourceId) {
3011
+ pool.release(poolKey.backdropChain(sourceId));
3012
+ pool.release(poolKey.backdropBody(sourceId));
3013
+ pool.release(poolKey.backdropBodyScratch(sourceId));
3014
+ resources.get(sourceId)?.stats.destroy();
3015
+ resources.delete(sourceId);
3016
+ readbacks.get(sourceId)?.staging.destroy();
3017
+ readbacks.delete(sourceId);
3018
+ pendingStats.delete(sourceId);
3019
+ const queued = pendingMaps.indexOf(sourceId);
3020
+ if (queued >= 0)
3021
+ pendingMaps.splice(queued, 1);
3022
+ },
3023
+ destroy() {
3024
+ for (const sourceId of [...resources.keys()])
3025
+ this.forget(sourceId);
3026
+ for (const slot of uniforms.values())
3027
+ slot.buffer.destroy();
3028
+ uniforms.clear();
3029
+ pendingRelease.length = 0;
3030
+ pendingMaps.length = 0;
3031
+ }
3032
+ };
3033
+ }
3034
+ var ANALYSIS_DISPATCH = { workgroupSize: ANALYSIS_WORKGROUP, workgroups: 1 };
3035
+
3036
+ // ../renderer-webgpu/dist/renderer.js
3037
+ var OPTICS_PASS_ID = "vitrea.optics";
3038
+ var HIGHLIGHT_PASS_ID = "vitrea.highlight";
3039
+ var FIELD_PASS_ID = "vitrea.field";
3040
+ var BACKDROP_PASS_ID = "vitrea.backdrop";
3041
+ var ANALYSIS_PASS_ID = "vitrea.analysis";
3042
+ var RENDERER_PASS_IDS = [
3043
+ BACKDROP_PASS_ID,
3044
+ ANALYSIS_PASS_ID,
3045
+ FIELD_PASS_ID,
3046
+ OPTICS_PASS_ID,
3047
+ HIGHLIGHT_PASS_ID
3048
+ ];
3049
+ var NOMINAL_MATERIAL_POLICY = {
3050
+ glass: "material",
3051
+ frost: "nominal",
3052
+ refraction: "nominal",
3053
+ occlusion: "nominal",
3054
+ border: "nominal",
3055
+ ambientTint: "nominal",
3056
+ foreground: "adaptive"
3057
+ };
3058
+ function createWebGPURenderer(options = {}) {
3059
+ const providers = /* @__PURE__ */ new Map();
3060
+ const groups = /* @__PURE__ */ new Map();
3061
+ const adaptation = /* @__PURE__ */ new Map();
3062
+ const lastReadbackAt = /* @__PURE__ */ new Map();
3063
+ let accessibility = NOMINAL_MATERIAL_POLICY;
3064
+ let material = withMaterialOverrides(DEFAULT_MATERIAL_PROFILE, options.materialProfile ?? {});
3065
+ let viewport = options.viewport ?? {
3066
+ widthCss: 0,
3067
+ heightCss: 0,
3068
+ devicePixelRatio: 1
3069
+ };
3070
+ let context;
3071
+ let builtGeneration;
3072
+ let store;
3073
+ let runner;
3074
+ let framesDrawn = 0;
3075
+ let generations = 0;
3076
+ let lastFrameTimeMs;
3077
+ let targets;
3078
+ let pendingEncoder;
3079
+ let pendingRebuilds = 0;
3080
+ let pendingUnbuilt = [];
3081
+ let unbuiltFrameId;
3082
+ const recordUnbuilt = (frameId, ids) => {
3083
+ if (unbuiltFrameId !== frameId) {
3084
+ unbuiltFrameId = frameId;
3085
+ pendingUnbuilt = [];
3086
+ }
3087
+ for (const id of ids)
3088
+ if (!pendingUnbuilt.includes(id))
3089
+ pendingUnbuilt.push(id);
3090
+ };
3091
+ const governor = createGovernor({
3092
+ ...options.familyCVerified === void 0 ? {} : { familyCVerified: options.familyCVerified }
3093
+ });
3094
+ const dropContext = () => {
3095
+ store?.destroy();
3096
+ runner?.destroy();
3097
+ context?.destroy();
3098
+ store = void 0;
3099
+ runner = void 0;
3100
+ context = void 0;
3101
+ pendingEncoder = void 0;
3102
+ };
3103
+ const host = createDeviceHost({
3104
+ ...options.reacquire === void 0 ? {} : { reacquire: options.reacquire },
3105
+ ...options.onReplacementNeeded === void 0 ? {} : { onReplacementNeeded: options.onReplacementNeeded },
3106
+ onStatusChange: (status) => {
3107
+ if (status.generation !== generations)
3108
+ generations = status.generation;
3109
+ options.onDeviceStatusChange?.(status);
3110
+ }
3111
+ });
3112
+ host.addTeardownHook(() => {
3113
+ dropContext();
3114
+ adaptation.clear();
3115
+ lastReadbackAt.clear();
3116
+ });
3117
+ const ensureContext = () => {
3118
+ const device = host.requireDevice();
3119
+ const generation = host.status.generation;
3120
+ if (context === void 0 || context.generation !== generation) {
3121
+ const superseded = builtGeneration;
3122
+ dropContext();
3123
+ context = createGpuContext(device, generation);
3124
+ builtGeneration = generation;
3125
+ store = createPyramidStore(context);
3126
+ runner = createPassRunner(context);
3127
+ if (superseded !== void 0 && superseded !== generation) {
3128
+ for (const provider of providers.values()) {
3129
+ if (provider.generation !== generation)
3130
+ provider.invalidate(generation, device);
3131
+ }
3132
+ }
3133
+ }
3134
+ return {
3135
+ context,
3136
+ store,
3137
+ runner
3138
+ };
3139
+ };
3140
+ const adaptationFor = (sourceId) => {
3141
+ let state = adaptation.get(sourceId);
3142
+ if (state === void 0) {
3143
+ state = createAdaptationState(void 0, material);
3144
+ adaptation.set(sourceId, state);
3145
+ }
3146
+ return state;
3147
+ };
3148
+ const coverFit = (sourceWidth, sourceHeight) => {
3149
+ const viewportAspect = viewport.heightCss > 0 ? viewport.widthCss / viewport.heightCss : 1;
3150
+ const sourceAspect = sourceHeight > 0 ? sourceWidth / sourceHeight : 1;
3151
+ if (sourceAspect > viewportAspect) {
3152
+ const scaleX = viewportAspect / sourceAspect;
3153
+ return [scaleX, 1, (1 - scaleX) / 2, 0];
3154
+ }
3155
+ const scaleY = sourceAspect / viewportAspect;
3156
+ return [1, scaleY, 0, (1 - scaleY) / 2];
3157
+ };
3158
+ const unionOf = (input) => input.union ?? DEFAULT_GROUP_UNION;
3159
+ const variantOf = (input) => input.variant ?? "regular";
3160
+ const stateOf = (input, resolution) => {
3161
+ const resolved = resolution?.groups.find((group) => group.groupId === input.groupId);
3162
+ if (resolved === void 0) {
3163
+ return { refraction: input.refraction, analysisExact: input.analysisExact };
3164
+ }
3165
+ return {
3166
+ refraction: resolved.state.refraction,
3167
+ analysisExact: resolved.state.analysis === "exact"
3168
+ };
3169
+ };
3170
+ function rebuildRequests(explicit) {
3171
+ if (explicit !== void 0)
3172
+ return explicit;
3173
+ const sampled = /* @__PURE__ */ new Set();
3174
+ for (const entry of groups.values()) {
3175
+ const id = entry.input.backdropSourceId;
3176
+ if (id !== void 0)
3177
+ sampled.add(id);
3178
+ }
3179
+ const requests = [];
3180
+ for (const sourceId of sampled) {
3181
+ const provider = providers.get(sourceId);
3182
+ if (provider === void 0 || !provider.isDirty())
3183
+ continue;
3184
+ requests.push({
3185
+ sourceId,
3186
+ // Standalone, every dirty acquire is its own epoch: there is no core
3187
+ // keeping the books, so a monotonically rising number is the honest
3188
+ // stand-in and it keeps `build`'s clean-skip from firing spuriously.
3189
+ epoch: framesDrawn + 1,
3190
+ resolution: { scale: 1, maxDimension: 2048 },
3191
+ groupIds: [...groups.values()].filter((entry) => entry.input.backdropSourceId === sourceId).map((entry) => entry.input.groupId)
3192
+ });
3193
+ }
3194
+ return requests;
3195
+ }
3196
+ function runRebuilds(encoder, requests, frameTimeMs) {
3197
+ const { store: pyramids } = ensureContext();
3198
+ let built = 0;
3199
+ const unbuilt = [];
3200
+ for (const request of requests) {
3201
+ const provider = providers.get(request.sourceId);
3202
+ if (provider === void 0) {
3203
+ unbuilt.push(request.sourceId);
3204
+ continue;
3205
+ }
3206
+ const variant = variantOf([...groups.values()].find((entry) => entry.input.backdropSourceId === request.sourceId)?.input ?? { });
3207
+ const optics = opticsUnderPolicy(material.optics[variant], accessibility, material);
3208
+ const outcome = pyramids.build({
3209
+ sourceId: request.sourceId,
3210
+ epoch: request.epoch,
3211
+ resolution: request.resolution,
3212
+ bodySigmaCss: optics.blurSigma,
3213
+ viewportCss: [viewport.widthCss, viewport.heightCss]
3214
+ }, provider, encoder);
3215
+ if (outcome.status === "built")
3216
+ built += 1;
3217
+ else if (outcome.status === "unavailable")
3218
+ unbuilt.push(request.sourceId);
3219
+ }
3220
+ const cadence = governor.knobs.adaptationCadenceHz;
3221
+ for (const sourceId of providers.keys()) {
3222
+ if (pyramids.resources(sourceId) === void 0)
3223
+ continue;
3224
+ if (!readbackDue(lastReadbackAt.get(sourceId), frameTimeMs, cadence))
3225
+ continue;
3226
+ if (pyramids.requestStats(sourceId, encoder))
3227
+ lastReadbackAt.set(sourceId, frameTimeMs);
3228
+ }
3229
+ return { built, unbuilt };
3230
+ }
3231
+ function drawGroups(encoder, resolution) {
3232
+ const { store: pyramids, runner: passes } = ensureContext();
3233
+ const active = targets;
3234
+ if (active === void 0) {
3235
+ throw rendererError("pass-input", "No render targets are set. Call setTargets() (or pass views to drawFrame) before drawing.");
3236
+ }
3237
+ const dpr = Math.max(viewport.devicePixelRatio, 1e-3);
3238
+ const cssPerDevice = 1 / dpr;
3239
+ const viewportDevice = [
3240
+ Math.max(1, Math.round(viewport.widthCss * dpr)),
3241
+ Math.max(1, Math.round(viewport.heightCss * dpr))
3242
+ ];
3243
+ const skipped = [];
3244
+ let drawn = 0;
3245
+ for (const entry of groups.values()) {
3246
+ const input = entry.input;
3247
+ if (input.surfaces.length === 0)
3248
+ continue;
3249
+ let surfaces;
3250
+ try {
3251
+ surfaces = resolveSurfaces(input, governor.knobs.fieldFamily, material);
3252
+ } catch (error) {
3253
+ skipped.push({
3254
+ groupId: input.groupId,
3255
+ reason: error instanceof Error ? error.message : String(error)
3256
+ });
3257
+ continue;
3258
+ }
3259
+ const union = unionOf(input);
3260
+ const snapped = snapRectToDevicePixels(groupFieldRect(surfaces, union), dpr);
3261
+ const rectDevice = clipFieldRectToCanvas(snapped, dpr, viewportDevice);
3262
+ if (rectDevice === void 0)
3263
+ continue;
3264
+ const packed = packInstances(surfaces, [
3265
+ rectDevice.x * cssPerDevice,
3266
+ rectDevice.y * cssPerDevice
3267
+ ]);
3268
+ const fields = passes.fieldPass(encoder, {
3269
+ groupId: input.groupId,
3270
+ family: governor.knobs.fieldFamily,
3271
+ rectDevice,
3272
+ cssPerDevice,
3273
+ coverageRampCss: cssPerDevice,
3274
+ // Ladder rungs 2 and 3 turn this down; rung 0 and 1 leave it at 1, which
3275
+ // is the extent the group's rect already had.
3276
+ renderScale: governor.knobs.refractionResolutionScale,
3277
+ instances: packed.data,
3278
+ instanceCount: packed.count,
3279
+ union
3280
+ });
3281
+ const state = stateOf(input, resolution);
3282
+ const policy = resolution?.accessibility.material ?? accessibility;
3283
+ const variant = variantOf(input);
3284
+ const optics = opticsUnderPolicy(material.optics[variant], policy, material);
3285
+ const refraction = effectiveRefraction(accessibilityRefractionCap(policy), state.refraction);
3286
+ const refractionScale = material.refractionScale[refraction];
3287
+ const sourceId = input.backdropSourceId;
3288
+ const pyramid = sourceId === void 0 ? void 0 : pyramids.resources(sourceId);
3289
+ const adapt = sourceId === void 0 ? void 0 : adaptationFor(sourceId).values;
3290
+ passes.opticsPass(encoder, {
3291
+ groupId: input.groupId,
3292
+ target: active.optics,
3293
+ targetFormat: active.format,
3294
+ rectDevice,
3295
+ fields,
3296
+ viewportDevice,
3297
+ cssPerDevice,
3298
+ coverageRampCss: cssPerDevice,
3299
+ fit: pyramid === void 0 ? [1, 1, 0, 0] : coverFit(pyramid.plan.width, pyramid.plan.height),
3300
+ refractionScale,
3301
+ bodyLodPerPx: material.lensBodyLodPerPx,
3302
+ rimLodBias: material.lensRimLodBias,
3303
+ chainMaxLod: pyramid?.plan.maxLod ?? 0,
3304
+ tint: optics.tint,
3305
+ tintAlpha: optics.tintAlpha,
3306
+ adaptTint: adapt?.tint ?? optics.tint,
3307
+ adaptStrength: adapt?.observed === true ? adaptationStrength(policy, state.analysisExact, material) : 0,
3308
+ rimWidth: optics.rimWidth,
3309
+ rimAlpha: optics.rimAlpha,
3310
+ specularPower: optics.specularPower,
3311
+ specularGain: optics.specularGain,
3312
+ lightDirection: material.lightDirection,
3313
+ shadowDepth: optics.shadowDepth,
3314
+ shadowAlpha: optics.shadowAlpha,
3315
+ backdrop: pyramid === void 0 || policy.glass === "none" ? void 0 : {
3316
+ chain: pyramid.chain.createView(),
3317
+ body: pyramid.body.createView()
3318
+ }
3319
+ });
3320
+ if (active.highlight !== void 0) {
3321
+ const lead = surfaces.reduce((best, surface) => surface.channels.glow > best.channels.glow ? surface : best, surfaces[0]);
3322
+ passes.highlightPass(encoder, {
3323
+ groupId: input.groupId,
3324
+ target: active.highlight,
3325
+ targetFormat: active.format,
3326
+ rectDevice,
3327
+ fields,
3328
+ viewportDevice,
3329
+ cssPerDevice,
3330
+ sweep: lead.channels.sweep,
3331
+ sweepBandRadians: material.sweepBandRadians,
3332
+ // Reduced Motion removes shimmer travel outright rather than freezing it.
3333
+ sweepGain: policy.glass === "none" ? 0 : material.sweepGain,
3334
+ rimWidth: optics.rimWidth,
3335
+ pressPointCss: lead.channels.pressPoint ?? lead.centre,
3336
+ glowRadiusCss: material.glowRadiusCss,
3337
+ glowGain: material.glowGain,
3338
+ colour: optics.highlight
3339
+ });
3340
+ }
3341
+ drawn += 1;
3342
+ }
3343
+ return { groupsDrawn: drawn, rebuilds: pendingRebuilds, skipped, unbuilt: [...pendingUnbuilt] };
3344
+ }
3345
+ return {
3346
+ backend: "webgpu",
3347
+ get ready() {
3348
+ return host.status.device !== void 0 && host.status.deviceHealth === "ok";
3349
+ },
3350
+ passes: RENDERER_PASS_IDS,
3351
+ shaderSource: allShaderSource(),
3352
+ get deviceStatus() {
3353
+ return host.status;
3354
+ },
3355
+ get capabilityInput() {
3356
+ return host.capabilityInput;
3357
+ },
3358
+ governor,
3359
+ get unbuiltSources() {
3360
+ return pendingUnbuilt;
3361
+ },
3362
+ get instrumentation() {
3363
+ const pool = context?.pool.stats;
3364
+ const cache = context?.cache.stats;
3365
+ return {
3366
+ pyramid: store?.instrumentation ?? {
3367
+ rebuilds: 0,
3368
+ refusedDuplicates: 0,
3369
+ skippedClean: 0,
3370
+ reallocations: 0,
3371
+ rebuildsInFrame: () => 0,
3372
+ peakRebuildsPerSourcePerFrame: 0
3373
+ },
3374
+ texturePool: {
3375
+ live: pool?.live ?? 0,
3376
+ created: pool?.created ?? 0,
3377
+ destroyed: pool?.destroyed ?? 0
3378
+ },
3379
+ pipelines: {
3380
+ renderPipelines: cache?.renderPipelines ?? 0,
3381
+ computePipelines: cache?.computePipelines ?? 0
3382
+ },
3383
+ framesDrawn,
3384
+ deviceGenerations: generations
3385
+ };
3386
+ },
3387
+ attachDevice(device, ownership) {
3388
+ host.attach(device, ownership ?? options.ownership ?? (options.device === device ? "app" : "vitrea"));
3389
+ },
3390
+ replaceDevice(device) {
3391
+ host.replaceDevice(device);
3392
+ },
3393
+ markWebGPUUnavailable(reason) {
3394
+ host.markUnavailable(reason);
3395
+ },
3396
+ registerBackdrop(provider) {
3397
+ if (providers.has(provider.id)) {
3398
+ throw rendererError("source-identity", `Backdrop source "${provider.id}" is already registered. Unregister it first, or register the replacement under a new id.`, provider.id);
3399
+ }
3400
+ providers.set(provider.id, provider);
3401
+ },
3402
+ unregisterBackdrop(sourceId) {
3403
+ const provider = providers.get(sourceId);
3404
+ if (provider === void 0)
3405
+ return;
3406
+ provider.destroy();
3407
+ providers.delete(sourceId);
3408
+ store?.forget(sourceId);
3409
+ adaptation.delete(sourceId);
3410
+ lastReadbackAt.delete(sourceId);
3411
+ },
3412
+ backdrop(sourceId) {
3413
+ return providers.get(sourceId);
3414
+ },
3415
+ setViewport(next) {
3416
+ const changed = next.widthCss !== viewport.widthCss || next.heightCss !== viewport.heightCss || next.devicePixelRatio !== viewport.devicePixelRatio;
3417
+ viewport = next;
3418
+ if (!changed)
3419
+ return;
3420
+ context?.pool.bumpSizeEpoch();
3421
+ },
3422
+ get viewport() {
3423
+ return viewport;
3424
+ },
3425
+ setGroup(input) {
3426
+ groups.set(input.groupId, { input });
3427
+ },
3428
+ removeGroup(groupId) {
3429
+ groups.delete(groupId);
3430
+ runner?.forget(groupId);
3431
+ },
3432
+ setAccessibility(policy) {
3433
+ accessibility = policy;
3434
+ },
3435
+ setMaterialProfile(patch) {
3436
+ material = withMaterialOverrides(DEFAULT_MATERIAL_PROFILE, patch);
3437
+ adaptation.clear();
3438
+ },
3439
+ get materialProfile() {
3440
+ return material;
3441
+ },
3442
+ setTargets(next) {
3443
+ targets = {
3444
+ optics: next.optics,
3445
+ ...next.highlight === void 0 ? {} : { highlight: next.highlight },
3446
+ format: next.format ?? OUTPUT_TEXTURE_FORMAT
3447
+ };
3448
+ },
3449
+ drawFrame(args) {
3450
+ const { context: gpu, store: pyramids, runner: passes } = ensureContext();
3451
+ targets = {
3452
+ optics: args.optics,
3453
+ ...args.highlight === void 0 ? {} : { highlight: args.highlight },
3454
+ format: args.format ?? OUTPUT_TEXTURE_FORMAT
3455
+ };
3456
+ pyramids.beginFrame(args.frame.id);
3457
+ pyramids.setTimeline(args.timing);
3458
+ passes.setTimeline(args.timing);
3459
+ const encoder = gpu.device.createCommandEncoder({
3460
+ label: `vitrea:frame:${args.frame.id}`
3461
+ });
3462
+ let result;
3463
+ try {
3464
+ const rebuilt = runRebuilds(encoder, rebuildRequests(args.rebuild), args.frame.timeMs);
3465
+ pendingRebuilds = rebuilt.built;
3466
+ recordUnbuilt(args.frame.id, rebuilt.unbuilt);
3467
+ if (args.clear !== false) {
3468
+ passes.clearPass(encoder, args.optics);
3469
+ if (args.highlight !== void 0)
3470
+ passes.clearPass(encoder, args.highlight);
3471
+ }
3472
+ result = drawGroups(encoder, args.resolution);
3473
+ args.timing?.resolve(encoder);
3474
+ gpu.device.queue.submit([encoder.finish()]);
3475
+ pyramids.afterSubmit();
3476
+ } finally {
3477
+ pyramids.releaseAcquired();
3478
+ }
3479
+ const delta = lastFrameTimeMs === void 0 ? 0 : args.frame.timeMs - lastFrameTimeMs;
3480
+ lastFrameTimeMs = args.frame.timeMs;
3481
+ if (delta > 0) {
3482
+ for (const state of adaptation.values())
3483
+ state.advance(delta);
3484
+ }
3485
+ framesDrawn += 1;
3486
+ pyramids.setTimeline(void 0);
3487
+ passes.setTimeline(void 0);
3488
+ return result;
3489
+ },
3490
+ frameParticipant() {
3491
+ return {
3492
+ id: "vitrea.renderer-webgpu",
3493
+ write: (frameContext) => {
3494
+ if (host.status.device === void 0 || host.status.deviceHealth !== "ok")
3495
+ return;
3496
+ const { context: gpu, store: pyramids, runner: passes } = ensureContext();
3497
+ pyramids.beginFrame(frameContext.frame.id);
3498
+ pyramids.setTimeline(void 0);
3499
+ passes.setTimeline(void 0);
3500
+ pendingEncoder = gpu.device.createCommandEncoder({
3501
+ label: `vitrea:frame:${frameContext.frame.id}`
3502
+ });
3503
+ try {
3504
+ const rebuilt = runRebuilds(pendingEncoder, frameContext.consumeDirtyBackdropSources(), frameContext.frame.timeMs);
3505
+ pendingRebuilds = rebuilt.built;
3506
+ recordUnbuilt(frameContext.frame.id, rebuilt.unbuilt);
3507
+ } catch (error) {
3508
+ pendingEncoder = void 0;
3509
+ pyramids.releaseAcquired();
3510
+ throw error;
3511
+ }
3512
+ },
3513
+ render: (frameContext) => {
3514
+ const encoder = pendingEncoder;
3515
+ pendingEncoder = void 0;
3516
+ if (encoder === void 0 || context === void 0)
3517
+ return;
3518
+ const passes = runner;
3519
+ const pyramids = store;
3520
+ try {
3521
+ if (targets !== void 0) {
3522
+ passes.clearPass(encoder, targets.optics);
3523
+ if (targets.highlight !== void 0)
3524
+ passes.clearPass(encoder, targets.highlight);
3525
+ drawGroups(encoder, frameContext.resolution);
3526
+ }
3527
+ context.device.queue.submit([encoder.finish()]);
3528
+ pyramids.afterSubmit();
3529
+ } finally {
3530
+ pyramids.releaseAcquired();
3531
+ }
3532
+ const delta = lastFrameTimeMs === void 0 ? 0 : frameContext.frame.timeMs - lastFrameTimeMs;
3533
+ lastFrameTimeMs = frameContext.frame.timeMs;
3534
+ if (delta > 0) {
3535
+ for (const state of adaptation.values())
3536
+ state.advance(delta);
3537
+ }
3538
+ framesDrawn += 1;
3539
+ }
3540
+ };
3541
+ },
3542
+ async collectAdaptation() {
3543
+ if (store === void 0)
3544
+ return 0;
3545
+ const stats = await store.collectStats();
3546
+ for (const [sourceId, value] of stats) {
3547
+ const state = adaptationFor(sourceId);
3548
+ if (state.values.observed)
3549
+ state.observe(value);
3550
+ else
3551
+ state.reset(value);
3552
+ }
3553
+ return stats.size;
3554
+ },
3555
+ destroy() {
3556
+ for (const provider of providers.values())
3557
+ provider.destroy();
3558
+ providers.clear();
3559
+ groups.clear();
3560
+ adaptation.clear();
3561
+ lastReadbackAt.clear();
3562
+ pendingUnbuilt = [];
3563
+ unbuiltFrameId = void 0;
3564
+ builtGeneration = void 0;
3565
+ dropContext();
3566
+ host.destroy();
3567
+ }
3568
+ };
3569
+ }
3570
+
3571
+ export { ADAPTIVE_LUMINANCE_HIGH, ADAPTIVE_LUMINANCE_LOW, ADAPTIVE_TINT_DARK, ADAPTIVE_TINT_LIGHT, ANALYSIS_DISPATCH, ANALYSIS_GRID, ANALYSIS_PASS_ID, ANALYSIS_STATS_FLOATS, ANALYSIS_TARGET_EXTENT, ANALYSIS_WORKGROUP, BACKDROP_ALPHA_MODES, BACKDROP_COLOR_SPACES, BACKDROP_KINDS, BACKDROP_PASS_ID, CANVAS_FORMAT, CHAIN_SIGMA_AT_LEVEL_1, CROSS_CHECK_SHAPE_FLOATS, CROSS_CHECK_WORKGROUP, DEFAULT_MATERIAL_PROFILE, FAMILY_C_CROSS_CHECK, FIELD_PASS_ID, GOVERNOR_LADDER, HIGHLIGHT_PASS_ID, IDLE_CHANNELS, INCREASED_OCCLUSION_LIFT, INSTANCE_BYTES, INSTANCE_FLOATS, LENS_BODY_LOD_PER_PX, LENS_RIM_LOD_BIAS, LENS_SIZE_GAIN_MAX, LENS_SPAN_MAX, LENS_SPAN_MIN, LUMINANCE_WEIGHTS, MATERIAL_OPTICS, MATERIAL_VARIANTS, MAX_CHAIN_LEVELS, MIN_LEVEL_EXTENT, NOMINAL_GOVERNOR, NOMINAL_MATERIAL_POLICY, OPTICS_PASS_ID, OUTPUT_TEXTURE_FORMAT, PASS_LABEL, REFRACTION_LADDER, REFRACTION_SCALE, RENDERER_ERROR_CODES, RENDERER_PASS_IDS, RendererError, SUPPORTED_APP_TEXTURE_FORMATS, WGSL_ANALYSIS_PASS, WGSL_CROSS_CHECK_PASS, WGSL_DOWNSAMPLE_PASS, WGSL_FIELD_KERNELS, WGSL_FIELD_PASS, WGSL_FIELD_SAMPLE, WGSL_HIGHLIGHT_PASS, WGSL_IMPORT_PASS, WGSL_INSTANCE_STRUCT, WGSL_OPTICS_PASS, WGSL_PRELUDE, WGSL_RSUPN_GRAD, WGSL_RSUP_GRAD, WGSL_SMOOTH_UNION, WORKING_TEXTURE_FORMAT, ZERO_STATS, accessibilityRefractionCap, adaptationStrength, adaptiveTint, allShaderSource, alphaNormalisationMode, analysisModule, bodyBlurPlan, bodyLod, chainModule, clipFieldRectToCanvas, createAdaptationState, createAppTextureProvider, createCopyProvider, createDeviceHost, createGovernor, createGpuContext, createGradientProvider, createPassRunner, createPipelineCache, createPyramidStore, createRebuildLedger, createStorageSlot, createTexturePool, createTimingCollector, createUniformSlot, createVideoProvider, createWebGPURenderer, crossCheckKernelModule, displayP3ToSrgbLinear, effectiveRefraction, encodeOutput, encodeOutputBytes, fieldModule, fieldPassSource, groupFieldRect, highlightModule, importColorMatrix, importModule, importPassSource, lensDepthPx, lensSizeGain, linearGradientStops, linearToSrgb, linearToSrgbChannel, occlusionAlphaUnderPolicy, opticsModule, opticsUnderPolicy, packInstances, pipelineKey, planPyramid, poolKey, readbackDue, refractionRank, relativeLuminance, rendererError, resolveSurfaces, snapRectToDevicePixels, srgbToLinear, srgbToLinearChannel, statsFromBuffer, supportsTimestamps, validateAppTexture, withMaterialOverrides };
3572
+ //# sourceMappingURL=dist-FGJI5LQM.js.map
3573
+ //# sourceMappingURL=dist-FGJI5LQM.js.map