@photivra/engine 2.10.0 → 2.12.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +156 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +19 -3
  4. package/README.md +6 -6
  5. package/SECURITY.md +1 -1
  6. package/dist/core/portable-math.d.ts +6 -0
  7. package/dist/core/portable-math.js +39 -0
  8. package/dist/core/portable-math.js.map +1 -1
  9. package/dist/core/version.d.ts +1 -1
  10. package/dist/core/version.js +1 -1
  11. package/dist/core/version.js.map +1 -1
  12. package/dist/facade/chart-material-layers.d.ts +115 -0
  13. package/dist/facade/chart-material-layers.js +224 -0
  14. package/dist/facade/chart-material-layers.js.map +1 -0
  15. package/dist/facade/chart-radiance-batch.d.ts +8 -2
  16. package/dist/facade/chart-radiance-batch.js +11 -4
  17. package/dist/facade/chart-radiance-batch.js.map +1 -1
  18. package/dist/facade/chart-scenes.d.ts +134 -0
  19. package/dist/facade/chart-scenes.js +32 -22
  20. package/dist/facade/chart-scenes.js.map +1 -1
  21. package/dist/facade/diffraction-provider.d.ts +8 -2
  22. package/dist/facade/diffraction-provider.js +71 -2
  23. package/dist/facade/diffraction-provider.js.map +1 -1
  24. package/dist/facade/display-view-gpu-session.d.ts +18 -0
  25. package/dist/facade/display-view-gpu-session.js +280 -0
  26. package/dist/facade/display-view-gpu-session.js.map +1 -0
  27. package/dist/facade/display-view-gpu.d.ts +146 -0
  28. package/dist/facade/display-view-gpu.js +59 -0
  29. package/dist/facade/display-view-gpu.js.map +1 -0
  30. package/dist/facade/display-view.d.ts +48 -1
  31. package/dist/facade/display-view.js +7 -7
  32. package/dist/facade/display-view.js.map +1 -1
  33. package/dist/facade/expected-signal-setup.d.ts +5 -0
  34. package/dist/facade/expected-signal-setup.js +16 -0
  35. package/dist/facade/expected-signal-setup.js.map +1 -1
  36. package/dist/facade/expected-signal.d.ts +48 -3
  37. package/dist/facade/expected-signal.js +8 -0
  38. package/dist/facade/expected-signal.js.map +1 -1
  39. package/dist/facade/scene-radiance.d.ts +4 -2
  40. package/dist/facade/scene-radiance.js +7 -4
  41. package/dist/facade/scene-radiance.js.map +1 -1
  42. package/dist/facade/simulate-capture.d.ts +108 -8
  43. package/dist/facade/simulate-capture.js +226 -33
  44. package/dist/facade/simulate-capture.js.map +1 -1
  45. package/dist/facade/simulate-display-view.d.ts +31 -3
  46. package/dist/facade/simulate-display-view.js +73 -13
  47. package/dist/facade/simulate-display-view.js.map +1 -1
  48. package/dist/gpu/chart-composite-gpu.d.ts +119 -0
  49. package/dist/gpu/chart-composite-gpu.js +572 -0
  50. package/dist/gpu/chart-composite-gpu.js.map +1 -0
  51. package/dist/gpu/chart-radiance-gpu.d.ts +6 -6
  52. package/dist/gpu/chart-radiance-gpu.js +9 -11
  53. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  54. package/dist/gpu/display-view-chart-material-module.d.ts +10 -0
  55. package/dist/gpu/display-view-chart-material-module.js +58 -0
  56. package/dist/gpu/display-view-chart-material-module.js.map +1 -0
  57. package/dist/gpu/display-view-composite-material-module.d.ts +68 -0
  58. package/dist/gpu/display-view-composite-material-module.js +248 -0
  59. package/dist/gpu/display-view-composite-material-module.js.map +1 -0
  60. package/dist/gpu/display-view-quality-gpu.d.ts +185 -0
  61. package/dist/gpu/display-view-quality-gpu.js +1000 -0
  62. package/dist/gpu/display-view-quality-gpu.js.map +1 -0
  63. package/dist/gpu/display-view-speed-gpu.d.ts +156 -0
  64. package/dist/gpu/display-view-speed-gpu.js +524 -0
  65. package/dist/gpu/display-view-speed-gpu.js.map +1 -0
  66. package/dist/gpu/image-kernel-accelerator.d.ts +23 -4
  67. package/dist/gpu/image-kernel-accelerator.js +58 -10
  68. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  69. package/dist/gpu/image-kernel-execution.d.ts +55 -3
  70. package/dist/gpu/image-kernel-execution.js.map +1 -1
  71. package/dist/gpu/sensor-stage-gpu.d.ts +11 -0
  72. package/dist/gpu/sensor-stage-gpu.js +42 -13
  73. package/dist/gpu/sensor-stage-gpu.js.map +1 -1
  74. package/dist/gpu/webgpu-runtime.d.ts +4 -2
  75. package/dist/gpu/webgpu-runtime.js +13 -4
  76. package/dist/gpu/webgpu-runtime.js.map +1 -1
  77. package/dist/gpu/webgpu-switch.d.ts +4 -0
  78. package/dist/gpu/webgpu-switch.js +29 -0
  79. package/dist/gpu/webgpu-switch.js.map +1 -0
  80. package/dist/gpu/wgsl-chart-composite-kernel.d.ts +52 -0
  81. package/dist/gpu/wgsl-chart-composite-kernel.js +515 -0
  82. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -0
  83. package/dist/gpu/wgsl-chart-kernel.d.ts +11 -9
  84. package/dist/gpu/wgsl-chart-kernel.js +33 -27
  85. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  86. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +95 -0
  87. package/dist/gpu/wgsl-display-view-quality-kernel.js +769 -0
  88. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -0
  89. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +75 -0
  90. package/dist/gpu/wgsl-display-view-speed-kernel.js +386 -0
  91. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -0
  92. package/dist/gpu/wgsl-sensor-kernel.d.ts +28 -9
  93. package/dist/gpu/wgsl-sensor-kernel.js +94 -50
  94. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  95. package/dist/index.d.ts +5 -2
  96. package/dist/index.js +2 -0
  97. package/dist/index.js.map +1 -1
  98. package/dist/optics/defocus-disc-kernel.d.ts +61 -0
  99. package/dist/optics/defocus-disc-kernel.js +191 -0
  100. package/dist/optics/defocus-disc-kernel.js.map +1 -0
  101. package/docs/API_REFERENCE.md +47 -1
  102. package/docs/QUICK_START.md +150 -5
  103. package/docs/USAGE.md +81 -3
  104. package/docs/api/core-version.md +1 -1
  105. package/docs/api/exports.json +127 -2
  106. package/docs/api/facade-chart-material-layers.md +137 -0
  107. package/docs/api/facade-chart-radiance-batch.md +7 -2
  108. package/docs/api/facade-display-view-gpu.md +94 -0
  109. package/docs/api/facade-scene-radiance.md +3 -2
  110. package/docs/api/facade-simulate-capture.md +95 -11
  111. package/docs/api/facade-simulate-display-view.md +17 -2
  112. package/docs/api/gpu-image-kernel-accelerator.md +10 -3
  113. package/docs/api/gpu-image-kernel-execution.md +71 -4
  114. package/docs/api/gpu-webgpu-switch.md +23 -0
  115. package/package.json +3 -1
@@ -0,0 +1,769 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * WGSL kernels of the display view's Quality path on the GPU (#479): the exact area mean with diffraction kernels, the
4
+ * anti-aliasing filter's spots, the AHD candidates and the low-signal correction, in three passes over one band of display
5
+ * rows (docs/WEBGPU.md, "Display view (Quality) on the GPU"; the float64 reference is `renderDisplayView` with
6
+ * `createDisplayViewExpectedSignal`, facade/display-view.ts and facade/expected-signal.ts):
7
+ *
8
+ * 1. **Lattice** (`DISPLAY_VIEW_QUALITY_LATTICE_WGSL` plus the material module): one invocation per lattice point of each
9
+ * lattice group (offsets that share a fractional shift, as the reference groups them): the chart material at
10
+ * (p + 0.5 + f_x − W/2)·g, −(q + 0.5 + f_y − H/2)·g (the reference's lattice formula, an exact float32 sum times the
11
+ * scale rounded once), with the module's uncertainty flag in the top bit.
12
+ * 2. **Sites** (`DISPLAY_VIEW_QUALITY_SITES_WGSL`): one invocation per native site of the band's window: the expected
13
+ * electrons Σ_t V_c(m_t, t) over the taps t of every group (the combined kernel values per material and channel, formed on
14
+ * the host in float64 as the reference forms them: Σ_members w·Σ_λ A_c(λ)·K_λ·L_m(λ); without diffraction the offset
15
+ * weights times the material's channel signal), plus the dark electrons; the readout moments (mean k·e, variance k²e + c₀,
16
+ * third cumulant k³e) and the clip at the white level; and each quantity's running absolute error bound. With the defocus
17
+ * disc (#492) the variant `DISPLAY_VIEW_QUALITY_SITES_BLOCKED_WGSL` reads a factored table (one neutral row, one row per
18
+ * spectral material) and sums each group's dense tap rectangle in blocks, so its bound grows with the kernel's width.
19
+ * 3. **Pixels** (`DISPLAY_VIEW_QUALITY_PIXELS_WGSL`): one invocation per display pixel of the band: for every real pixel of
20
+ * the footprint, the demosaic candidates (one bilinear, or AHD's two directional linear candidates, each with its full
21
+ * camera-channel covariance), white balance and the highlight clip, the colour matrix, the tone curve and the clip to
22
+ * [0, 1] with the skewness term, the candidates' average, the Stein covariance with the closed-form B_c, the independent
23
+ * residual, AHD's choice variance ¼·Var(H − V), and the low-signal correction interpolated trilinearly between the grid
24
+ * states of a table the host uploaded (the states themselves are computed on the host, #447; the kernel records every
25
+ * grid key it needs in a bitmap and flags a display pixel whose state the table lacks).
26
+ *
27
+ * **Running bounds.** Every quantity after the site table is carried as a pair (value, absolute bound) in float32: the bound
28
+ * of a sum is the sum of the bounds plus one rounding of the result, of a product |a|·Δb + |b|·Δa plus one rounding, of a
29
+ * square root Δv/(2s) plus 4.5 ULP, of a quotient 2.5 ULP plus the propagated terms; the clipped-normal moments add the
30
+ * Speed kernel's clip constants (CLIP_MEAN_U, CLIP_VARIANCE_U per standard deviation or variance) and propagate their inputs
31
+ * with the moments' first-order derivatives at the pixel's own standardized clip levels (∂E/∂m = P(inside), ∂E/∂s = φa − φb,
32
+ * ∂Var/∂m = −s(ρ_a + ρ_b), ∂Var/∂s = s(2ρ − aρ_a − bρ_b) with ρ_a = 2Φa(a − E[g]), ρ_b = 2Qb(b − E[g]), and the slope's
33
+ * (φa − φb)/s and (aφa − bφb)/s; without noise a slope is exact unless the mean's interval straddles a clip level); the
34
+ * Reinhard quadrature adds REINHARD_MEAN_U and REINHARD_VARIANCE_U. Mixed-sign sums (the AHD
35
+ * kernels, the colour matrix) are therefore bounded by the magnitudes of their terms, not relative to their result. The
36
+ * pixel's expected value and variance leave the kernel with their absolute bounds; the host doubles them (MARGIN) and adds
37
+ * the footprint sum's relative term. Second-order terms (products of bounds) and the bound arithmetic's own float32
38
+ * roundings are what the doubling covers.
39
+ *
40
+ * Precision assumptions are those of the Speed kernel: the W3C WGSL accuracy table (+ − × one rounding in either direction,
41
+ * E = 2^-23; division 2.5 ULP, sqrt 4.5 ULP, exp (3 + 2|x|) ULP, log2 2^-21 absolute on [0.5, 2] and 3 ULP outside; no
42
+ * trigonometric built-in).
43
+ */
44
+ import { DISPLAY_VIEW_CLIP_MEAN_U, DISPLAY_VIEW_CLIP_VARIANCE_U, DISPLAY_VIEW_REINHARD_MEAN_U, DISPLAY_VIEW_REINHARD_VARIANCE_U } from "./wgsl-display-view-speed-kernel.js";
45
+ import { AHD_KEY_FLOOR, AHD_KEY_LEVELS, AHD_KEY_OCTAVES, AHD_KEY_STEPS } from "../facade/display-view.js";
46
+ export const DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE = 64;
47
+ /** Largest footprint per axis (real pixels), as the Speed kernel. */
48
+ export const DISPLAY_VIEW_QUALITY_MAXIMUM_PIXELS_PER_AXIS = 6;
49
+ /** Uniform parameter words (see `Params`): 56 words, 224 bytes. */
50
+ export const DISPLAY_VIEW_QUALITY_PARAM_WORDS = 56;
51
+ /** Words per lattice-group record: lx0, ly0, lw, lh, lbase, first tap (the blocked site pass, #492), cu (f32), cv (f32). */
52
+ export const DISPLAY_VIEW_QUALITY_GROUP_WORDS = 8;
53
+ /** Words per tap record: group, dp offset, dq offset, pad. */
54
+ export const DISPLAY_VIEW_QUALITY_TAP_WORDS = 4;
55
+ /** Floats per native site of the site buffer: mean, variance, third cumulant, their three bounds, flag (u32 bits), electrons. */
56
+ export const DISPLAY_VIEW_QUALITY_SITE_FLOATS = 8;
57
+ /** Floats per display pixel in the output buffer: m0 m1 m2, v00 v11 v22, c01 c02 c12, mean bound, variance bound (absolute). */
58
+ export const DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS = 11;
59
+ /** Floats per column or row record of the geometry buffer (the Speed layout). */
60
+ export const DISPLAY_VIEW_QUALITY_GEOMETRY_STRIDE = 20;
61
+ /** Candidate table: entries per phase (the 7 × 7 window of AHD's reach 3) and floats per entry (dx, dy, dh0, dh1, dh2, dv0, dv1, dv2). */
62
+ export const DISPLAY_VIEW_QUALITY_CANDIDATE_ENTRIES = 49;
63
+ export const DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS = 8;
64
+ const CANDIDATE_PHASE_STRIDE = 4 + DISPLAY_VIEW_QUALITY_CANDIDATE_ENTRIES * DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS;
65
+ /** Layout of the aux table (float32 words). */
66
+ export const DISPLAY_VIEW_QUALITY_AUX = Object.freeze({
67
+ /** Per phase p, channel c, tap t: dx, dy, weight, 0 (the bilinear kernels, 4 taps; unused taps have weight 0). */
68
+ bilinear: 0,
69
+ /** Per phase p, channel c: 1 / Σ_t w_t² of the bilinear kernel (host float64). */
70
+ inverseW2: 192,
71
+ /** 16 Gauss–Legendre nodes then 16 weights on [−1, 1]. */
72
+ gaussLegendre: 208,
73
+ /** Per phase: the entry count (as u32 bits) and 3 pad words, then the entries (dx, dy, dense weights of candidate 0 and 1 per channel). */
74
+ candidates: 240,
75
+ candidatePhaseStride: CANDIDATE_PHASE_STRIDE,
76
+ size: 240 + 4 * CANDIDATE_PHASE_STRIDE
77
+ });
78
+ /** Pixel flag bits of the output flag word. */
79
+ export const DISPLAY_VIEW_QUALITY_FLAG = Object.freeze({
80
+ /** A site the pixel read had an uncertain lattice material (recompute on the host). */
81
+ materialUncertain: 1,
82
+ /** A site the pixel read lies outside the band's site window (a host geometry error; recompute). */
83
+ siteOutsideWindow: 2,
84
+ /** The correction needed a grid state the table does not hold (recompute, or compute the state and dispatch again). */
85
+ stateMissing: 4,
86
+ /** A sample's pixel variance lies within its bound of the correction threshold (recompute). */
87
+ thresholdTie: 8,
88
+ /** Informational: the pixel's expected value includes the low-signal correction. */
89
+ corrected: 16,
90
+ /** The bits that require the host's recomputation. */
91
+ recompute: 15
92
+ });
93
+ /** Words of the grid-key bitmap: one bit per grid key (AHD_KEY_LEVELS³ keys). */
94
+ export const DISPLAY_VIEW_QUALITY_BITMAP_WORDS = Math.ceil(AHD_KEY_LEVELS ** 3 / 32);
95
+ /**
96
+ * A site whose uncertain lattice points (the material module's flag) could move its electrons by more than this fraction of
97
+ * their value is flagged for host recomputation; a smaller uncertainty enters the site's bound instead (the tap weights times
98
+ * the largest value difference between any two materials at that tap).
99
+ */
100
+ export const DISPLAY_VIEW_QUALITY_UNCERTAINTY_LIMIT = 2 ** -10;
101
+ /** Bound constant of a clipped-normal slope Φ(z) (in units of E): z's 8E through φ·|z| ≤ 0.24 plus the Abramowitz–Stegun 147E. */
102
+ export const DISPLAY_VIEW_QUALITY_SLOPE_U = 200;
103
+ /** Relative bound of the Reinhard slope E[h′] (in units of E). */
104
+ const REINHARD_SLOPE_U = 64;
105
+ /** The `E` of the bounds: one WGSL rounding of + − × in either direction, 2^-23. */
106
+ const E_LITERAL = "1.1920928955078125e-7";
107
+ const literal = (value) => {
108
+ const text = String(Math.fround(value));
109
+ return /[.e]/u.test(text) ? text : `${text}.0`;
110
+ };
111
+ /** The parameter struct and the bound helpers shared by the three passes. */
112
+ const COMMON_WGSL = /* wgsl */ `
113
+ const E: f32 = ${E_LITERAL};
114
+ const SLACK: f32 = 1e-14;
115
+ const CLIP_REACH: f32 = 8.5;
116
+ const CLIP_MEAN_U: f32 = ${literal(DISPLAY_VIEW_CLIP_MEAN_U)};
117
+ const CLIP_VARIANCE_U: f32 = ${literal(DISPLAY_VIEW_CLIP_VARIANCE_U)};
118
+ const CLIP_SLOPE_U: f32 = ${literal(DISPLAY_VIEW_QUALITY_SLOPE_U)};
119
+ const REINHARD_MEAN_U: f32 = ${literal(DISPLAY_VIEW_REINHARD_MEAN_U)};
120
+ const REINHARD_VARIANCE_U: f32 = ${literal(DISPLAY_VIEW_REINHARD_VARIANCE_U)};
121
+ const REINHARD_SLOPE_U: f32 = ${literal(REINHARD_SLOPE_U)};
122
+ const UNCERTAINTY_LIMIT: f32 = ${literal(2 ** -10)};
123
+ const STRIDE: u32 = ${DISPLAY_VIEW_QUALITY_GEOMETRY_STRIDE}u;
124
+ const SITE: u32 = ${DISPLAY_VIEW_QUALITY_SITE_FLOATS}u;
125
+ const GROUP: u32 = ${DISPLAY_VIEW_QUALITY_GROUP_WORDS}u;
126
+ const TAP: u32 = ${DISPLAY_VIEW_QUALITY_TAP_WORDS}u;
127
+ struct Params {
128
+ count: u32, row0: u32, region_w: u32, region_x: u32,
129
+ region_y: u32, margin: u32, native_w: u32, native_h: u32,
130
+ columns: u32, tone: u32, reach: u32, candidates: u32,
131
+ site_x0: i32, site_y0: i32, site_w: u32, site_h: u32,
132
+ correction: u32, state_count: u32, taps: u32, groups: u32,
133
+ lattice_total: u32, gain_at_clip0: u32, gain_at_clip1: u32, gain_at_clip2: u32,
134
+ k0: f32, k1: f32, k2: f32, reach2: f32,
135
+ dark0: f32, dark1: f32, dark2: f32, clip_level: f32,
136
+ c0_0: f32, c0_1: f32, c0_2: f32, threshold: f32,
137
+ g0: f32, g1: f32, g2: f32, scale_u: f32,
138
+ m00: f32, m01: f32, m02: f32, m10: f32,
139
+ m11: f32, m12: f32, m20: f32, m21: f32,
140
+ m22: f32, scale_v: f32, site_gamma: f32, site_first: u32,
141
+ site_tiny: f32, pad_f: f32, pad_g: f32, pad_h: f32,
142
+ };
143
+ @group(0) @binding(0) var<uniform> params: Params;
144
+
145
+ fn k_of(c: u32) -> f32 { if (c == 0u) { return params.k0; } if (c == 1u) { return params.k1; } return params.k2; }
146
+ fn dark_of(c: u32) -> f32 { if (c == 0u) { return params.dark0; } if (c == 1u) { return params.dark1; } return params.dark2; }
147
+ fn c0_of(c: u32) -> f32 { if (c == 0u) { return params.c0_0; } if (c == 1u) { return params.c0_1; } return params.c0_2; }
148
+ fn g_of(c: u32) -> f32 { if (c == 0u) { return params.g0; } if (c == 1u) { return params.g1; } return params.g2; }
149
+ // Whether the channel's white-balance gain is the clip level itself (the smallest gain), exactly, in the reference's float64.
150
+ fn gain_at_clip(c: u32) -> bool { if (c == 0u) { return params.gain_at_clip0 == 1u; } if (c == 1u) { return params.gain_at_clip1 == 1u; } return params.gain_at_clip2 == 1u; }
151
+ fn m_of(r: u32, c: u32) -> f32 {
152
+ let i = r * 3u + c;
153
+ switch (i) {
154
+ case 0u: { return params.m00; } case 1u: { return params.m01; } case 2u: { return params.m02; }
155
+ case 3u: { return params.m10; } case 4u: { return params.m11; } case 5u: { return params.m12; }
156
+ case 6u: { return params.m20; } case 7u: { return params.m21; } default: { return params.m22; }
157
+ }
158
+ }
159
+ fn channel_of(x: i32, y: i32) -> u32 {
160
+ if ((y & 1) == 0) { return select(1u, 0u, (x & 1) == 0); }
161
+ return select(2u, 1u, (x & 1) == 0);
162
+ }
163
+ // A value with its absolute error bound (x: value, y: bound). First-order rules; the host doubles the final bound.
164
+ fn b_add(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> { let v = a.x + b.x; return vec2<f32>(v, a.y + b.y + E * abs(v)); }
165
+ fn b_sub(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> { let v = a.x - b.x; return vec2<f32>(v, a.y + b.y + E * abs(v)); }
166
+ fn b_mul(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> { let v = a.x * b.x; return vec2<f32>(v, abs(a.x) * b.y + abs(b.x) * a.y + E * abs(v)); }
167
+ // Times an exact constant (a kernel weight, an integer, a power of two).
168
+ fn b_scale(a: vec2<f32>, k: f32) -> vec2<f32> { let v = a.x * k; return vec2<f32>(v, a.y * abs(k) + E * abs(v)); }
169
+ // A host constant rounded once to float32 (a gain, a matrix entry).
170
+ fn b_rounded(k: f32) -> vec2<f32> { return vec2<f32>(k, E * abs(k)); }
171
+ fn b_div(a: vec2<f32>, b: vec2<f32>) -> vec2<f32> { let v = a.x / b.x; return vec2<f32>(v, (a.y + abs(v) * b.y) / abs(b.x) + 2.5 * E * abs(v)); }
172
+ // |sqrt(v + d) - sqrt(v)| <= min(d / (2 sqrt v), sqrt d), plus the 4.5 ULP of the built-in.
173
+ fn b_sqrt(a: vec2<f32>) -> vec2<f32> {
174
+ let s = sqrt(max(a.x, 0.0));
175
+ if (s > 0.0) { return vec2<f32>(s, min(a.y / (2.0 * s), sqrt(a.y)) + 4.5 * E * s); }
176
+ return vec2<f32>(0.0, sqrt(a.y));
177
+ }
178
+ // Standard normal density, as the reference forms it, and Abramowitz & Stegun 26.2.17 as (Φ, 1 − Φ).
179
+ fn phi(z: f32) -> f32 { return 0.3989422804014327 * exp(-0.5 * z * z); }
180
+ fn pq(z: f32) -> vec2<f32> {
181
+ let x = abs(z);
182
+ let t = 1.0 / (1.0 + 0.2316419 * x);
183
+ let q = phi(x) * t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
184
+ if (z >= 0.0) { return vec2<f32>(1.0 - q, q); }
185
+ return vec2<f32>(q, 1.0 - q);
186
+ }
187
+ struct Moments { mean: vec2<f32>, variance: vec2<f32>, slope: vec2<f32> }
188
+ // Moments of min(Y, top) for Y ~ N(m, v) with their bounds: Var = v·(Φ − zφ + z²Q − δ²), δ = φ − zQ (the Speed forms).
189
+ fn upper_clip(m: vec2<f32>, v: vec2<f32>, top: f32) -> Moments {
190
+ var out: Moments;
191
+ if (!(v.x > 0.0)) {
192
+ // No noise on either side (a zero bound): the reference's slope is 1 or 0 by m < top, decided when m's interval lies on one side.
193
+ let decided = v.y == 0.0 && (m.x - m.y >= top || m.x + m.y < top);
194
+ out.mean = vec2<f32>(min(m.x, top), m.y); out.variance = vec2<f32>(0.0, v.y); out.slope = vec2<f32>(select(0.0, 1.0, m.x < top), select(1.0, 0.0, decided));
195
+ return out;
196
+ }
197
+ let s = b_sqrt(v);
198
+ let z = (top - m.x) / s.x;
199
+ if (z > CLIP_REACH) { out.mean = m; out.variance = v; out.slope = vec2<f32>(1.0, 0.0); return out; }
200
+ if (z < -CLIP_REACH) { out.mean = vec2<f32>(top, 0.0); out.variance = vec2<f32>(0.0, 0.0); out.slope = vec2<f32>(0.0, 0.0); return out; }
201
+ let p = pq(z);
202
+ let d = phi(z);
203
+ let delta = d - z * p.y;
204
+ let ratio = p.x - z * d + z * z * p.y - delta * delta;
205
+ // First-order propagation of the inputs' bounds: with g = min(Z, z), E[g] = −δ and ρ = Var(g) = ratio, ∂E/∂m = Φ, ∂E/∂s = φ,
206
+ // ∂Var/∂m = −s·ρ_z, ∂Var/∂s = s·(2ρ − z·ρ_z) with ρ_z = 2Q·(z + δ), ∂Φ/∂m = −φ/s and ∂Φ/∂s = −zφ/s.
207
+ let rho_z = 2.0 * p.y * (z + delta);
208
+ out.mean = vec2<f32>(m.x - s.x * delta, p.x * m.y + d * s.y + CLIP_MEAN_U * E * s.x);
209
+ out.variance = vec2<f32>(v.x * max(0.0, ratio), s.x * abs(rho_z) * m.y + s.x * abs(2.0 * ratio - z * rho_z) * s.y + CLIP_VARIANCE_U * E * v.x);
210
+ out.slope = vec2<f32>(p.x, min(1.0, (d * m.y + abs(z) * d * s.y) / s.x + CLIP_SLOPE_U * E));
211
+ return out;
212
+ }
213
+ // Moments of clip(Y, 0, 1) for Y with mean m, variance v and standardized skewness γ (|γ| ≤ 1), with their bounds (the
214
+ // Speed forms: W = m + s·g(Z), E[g] = aΦa + φa − φb + bQb, E[g²] = a²Φa + (Φb − Φa) − (bφb − aφa) + b²Qb, the Edgeworth
215
+ // shift (s·γ/6)·(aφa − bφb) on the mean and −shift·(2·E[Y] + shift) on the variance).
216
+ fn unit_clip(m: vec2<f32>, v: vec2<f32>, skew: vec2<f32>) -> Moments {
217
+ var out: Moments;
218
+ if (!(v.x > 0.0)) {
219
+ let decided = v.y == 0.0 && ((m.x - m.y > 0.0 && m.x + m.y < 1.0) || m.x + m.y <= 0.0 || m.x - m.y >= 1.0);
220
+ out.mean = vec2<f32>(clamp(m.x, 0.0, 1.0), m.y); out.variance = vec2<f32>(0.0, v.y); out.slope = vec2<f32>(select(0.0, 1.0, m.x > 0.0 && m.x < 1.0), select(1.0, 0.0, decided));
221
+ return out;
222
+ }
223
+ let s = b_sqrt(v);
224
+ let a = -m.x / s.x;
225
+ let b = (1.0 - m.x) / s.x;
226
+ if (a < -CLIP_REACH && b > CLIP_REACH) { out.mean = m; out.variance = v; out.slope = vec2<f32>(1.0, 0.0); return out; }
227
+ var pa = 0.0; var da = 0.0;
228
+ if (!(a < -CLIP_REACH)) { pa = pq(a).x; da = phi(a); }
229
+ var pb = 1.0; var qb = 0.0; var db = 0.0;
230
+ if (!(b > CLIP_REACH)) { let p = pq(b); pb = p.x; qb = p.y; db = phi(b); }
231
+ let inside = pb - pa;
232
+ let gamma = clamp(skew.x, -1.0, 1.0);
233
+ let aa = a * da;
234
+ let bb = b * db;
235
+ let eg = a * pa + (da - db) + b * qb;
236
+ let eg2 = a * a * pa + inside - (bb - aa) + b * b * qb;
237
+ let normal_mean = m.x + s.x * eg;
238
+ var mean = normal_mean;
239
+ var shift = 0.0;
240
+ if (gamma != 0.0) { shift = s.x * gamma / 6.0 * (aa - bb); mean = normal_mean + shift; }
241
+ // |aφa − bφb| ≤ 0.48, so |shift| ≤ 0.08·s·|γ| and a wrong γ moves it by at most 0.16·s.
242
+ let d_shift = 0.1 * s.x * min(2.0, skew.y) + 0.1 * abs(gamma) * s.y + E * abs(shift);
243
+ // First-order propagation of the inputs' bounds: with ρ = E[g²] − E[g]², ∂E[g]/∂a = Φa, ∂E[g]/∂b = Qb, ∂E[g²]/∂a = 2aΦa,
244
+ // ∂E[g²]/∂b = 2bQb, so ρ_a = 2Φa(a − E[g]), ρ_b = 2Qb(b − E[g]); ∂E/∂m = inside, ∂E/∂s = φa − φb, ∂Var/∂m = −s(ρ_a + ρ_b),
245
+ // ∂Var/∂s = s(2ρ − aρ_a − bρ_b); the slope's derivatives are (φa − φb)/s and (aφa − bφb)/s.
246
+ let rho = eg2 - eg * eg;
247
+ let rho_a = 2.0 * pa * (a - eg);
248
+ let rho_b = 2.0 * qb * (b - eg);
249
+ let d_mean = m.y * inside + abs(da - db) * s.y + 2.0 * CLIP_MEAN_U * E * s.x + d_shift;
250
+ // The normal part X = v·ρ and its bound; the reference's variance is max(0, X − shift·(2·E[Y] + shift)), so it lies in
251
+ // [0, X_cpu] when the shift term is non-negative on both sides (shift > Δshift), which caps the bound by max(Var, X + ΔX).
252
+ let normal_variance = v.x * rho;
253
+ let d_normal = s.x * abs(rho_a + rho_b) * m.y + s.x * abs(2.0 * rho - a * rho_a - b * rho_b) * s.y + 2.0 * CLIP_VARIANCE_U * E * v.x;
254
+ let variance = max(0.0, normal_variance - shift * (2.0 * normal_mean + shift));
255
+ var d_var = d_normal + 2.0 * (d_shift * (abs(normal_mean) + abs(shift)) + abs(shift) * d_mean);
256
+ if (shift > d_shift) { d_var = min(d_var, max(variance, max(0.0, normal_variance) + d_normal)); }
257
+ out.mean = vec2<f32>(mean, d_mean);
258
+ out.variance = vec2<f32>(variance, d_var);
259
+ out.slope = vec2<f32>(inside, min(1.0, (abs(da - db) * m.y + abs(aa - bb) * s.y) / s.x + 2.0 * CLIP_SLOPE_U * E));
260
+ return out;
261
+ }
262
+ `;
263
+ /** The Gauss–Legendre Reinhard moments read the aux table, so they live in the pixel kernel. */
264
+ const REINHARD_WGSL = /* wgsl */ `
265
+ // Positive Reinhard per channel then the clip to [0, 1]: h(x) = x/(1 + x) for x > 0, else 0, by the reference's composite
266
+ // 16-point Gauss–Legendre rule in standardized units (the Speed forms), with the bounds of the quadrature (REINHARD_*_U)
267
+ // and of its inputs (|h′| ≤ 1, |h″| ≤ 2).
268
+ fn reinhard_moments(m: vec2<f32>, v: vec2<f32>) -> Moments {
269
+ var out: Moments;
270
+ if (!(v.x > 0.0)) {
271
+ if (m.x > 0.0) {
272
+ let h = m.x / (1.0 + m.x);
273
+ out.mean = vec2<f32>(h, m.y + 4.0 * E * h); out.variance = vec2<f32>(0.0, v.y); out.slope = vec2<f32>(1.0 / ((1.0 + m.x) * (1.0 + m.x)), min(1.0, 2.0 * m.y + 6.0 * E));
274
+ return out;
275
+ }
276
+ out.mean = vec2<f32>(0.0, m.y); out.variance = vec2<f32>(0.0, v.y); out.slope = vec2<f32>(0.0, 1.0);
277
+ return out;
278
+ }
279
+ let s = b_sqrt(v);
280
+ let zlo = max(-m.x / s.x, -CLIP_REACH);
281
+ if (!(CLIP_REACH > zlo)) { out.mean = vec2<f32>(0.0, m.y + s.y); out.variance = vec2<f32>(0.0, v.y); out.slope = vec2<f32>(0.0, 1.0); return out; }
282
+ let hm = select(0.0, m.x / (1.0 + m.x), m.x > 0.0);
283
+ var e1 = 0.0; var e2 = 0.0; var ed = 0.0;
284
+ let zwidth = (CLIP_REACH - zlo) / 4.0;
285
+ for (var p = 0u; p < 4u; p = p + 1u) {
286
+ let zc = zlo + (f32(p) + 0.5) * zwidth;
287
+ let zhalf = zwidth / 2.0;
288
+ for (var k = 0u; k < 16u; k = k + 1u) {
289
+ let z = zc + zhalf * aux[${DISPLAY_VIEW_QUALITY_AUX.gaussLegendre}u + k];
290
+ let x = m.x + s.x * z;
291
+ let weight = zhalf * aux[${DISPLAY_VIEW_QUALITY_AUX.gaussLegendre + 16}u + k] * phi(z);
292
+ let h = x / (1.0 + x);
293
+ e1 = e1 + weight * h;
294
+ e2 = e2 + weight * (h - hm) * (h - hm);
295
+ ed = ed + weight / ((1.0 + x) * (1.0 + x));
296
+ }
297
+ }
298
+ let shifted = e1 - hm;
299
+ let variance = max(0.0, e2 - shifted * shifted);
300
+ out.mean = vec2<f32>(e1, m.y + s.y + REINHARD_MEAN_U * E * abs(e1));
301
+ out.variance = vec2<f32>(variance, 2.0 * s.x * m.y + 2.0 * v.y + REINHARD_VARIANCE_U * E * variance);
302
+ out.slope = vec2<f32>(ed, min(1.0, 2.0 * m.y + 2.0 * s.y + REINHARD_SLOPE_U * E));
303
+ return out;
304
+ }
305
+ `;
306
+ /**
307
+ * Pass 1: the material of every lattice point of every group (the module's `material_at`, its uncertainty in bit 31; the output
308
+ * is `lattice_out`, since a material module may declare its own `lattice`, as #491's composite module does). The group
309
+ * records hold each group's lattice window (lx0, ly0, lw, lh), its first index in the lattice buffer and the point constants
310
+ * c_u = 0.5 + f_x − W/2 and c_v = 0.5 + f_y − H/2 (exact in float32, checked by the host).
311
+ */
312
+ export const DISPLAY_VIEW_QUALITY_LATTICE_WGSL = /* wgsl */ `${COMMON_WGSL}
313
+ @group(0) @binding(1) var<storage, read> groups: array<u32>;
314
+ @group(0) @binding(2) var<storage, read_write> lattice_out: array<u32>;
315
+ @compute @workgroup_size(${DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE})
316
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) wg: vec3<u32>) {
317
+ let i = gid.x + gid.y * wg.x * ${DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE}u;
318
+ if (i >= params.lattice_total) { return; }
319
+ var g = 0u;
320
+ for (var k = 1u; k < params.groups; k = k + 1u) { if (i >= groups[k * GROUP + 4u]) { g = k; } }
321
+ let base = g * GROUP;
322
+ let lx0 = bitcast<i32>(groups[base]);
323
+ let ly0 = bitcast<i32>(groups[base + 1u]);
324
+ let lw = groups[base + 2u];
325
+ let local = i - groups[base + 4u];
326
+ let q = ly0 + i32(local / lw);
327
+ let p = lx0 + i32(local % lw);
328
+ let u = (f32(p) + bitcast<f32>(groups[base + 6u])) * params.scale_u;
329
+ let v = (f32(q) + bitcast<f32>(groups[base + 7u])) * params.scale_v;
330
+ material_uncertain = false;
331
+ let m = material_at(vec2<f32>(u, v));
332
+ lattice_out[i] = m | select(0u, 0x80000000u, material_uncertain);
333
+ }
334
+ `;
335
+ /**
336
+ * The end of pass 2, shared by both site passes: from the site's electrons e and their absolute bound de, the readout moments
337
+ * (mean k·e, variance k²e + c₀, third cumulant k³e), the clip at the white level and their bounds, written to site i.
338
+ */
339
+ const SITE_TAIL_WGSL = /* wgsl */ `
340
+ fn write_site(i: u32, c: u32, e: f32, de: f32, flag: u32) {
341
+ let kc = k_of(c);
342
+ let mean = kc * e;
343
+ let variance = kc * kc * e + c0_of(c);
344
+ let third = kc * kc * kc * e;
345
+ var m = vec2<f32>(mean, kc * de + 2.0 * E * mean);
346
+ var v = vec2<f32>(variance, kc * kc * de + 4.0 * E * variance);
347
+ let gap = 1.0 - mean;
348
+ if (!(gap > 0.0 && gap * gap > params.reach2 * variance)) {
349
+ let clipped = upper_clip(m, v, 1.0);
350
+ m = clipped.mean; v = clipped.variance;
351
+ }
352
+ let o = i * SITE;
353
+ sites[o] = m.x; sites[o + 1u] = v.x; sites[o + 2u] = third;
354
+ sites[o + 3u] = m.y; sites[o + 4u] = v.y; sites[o + 5u] = kc * kc * kc * de + 5.0 * E * third;
355
+ sites[o + 6u] = bitcast<f32>(flag); sites[o + 7u] = e;
356
+ }
357
+ `;
358
+ /**
359
+ * The bindings of the site passes (pass 2); the blocked pass reads no tap records, so it declares no binding 2 (an automatic
360
+ * layout leaves out a binding the entry point does not use, and the host binds it only for the tap-list pass).
361
+ */
362
+ const siteBindingsWgsl = (tapRecords) => /* wgsl */ `
363
+ @group(0) @binding(1) var<storage, read> groups: array<u32>;${tapRecords ? `
364
+ @group(0) @binding(2) var<storage, read> taps: array<u32>;` : ""}
365
+ @group(0) @binding(3) var<storage, read> table: array<f32>;
366
+ @group(0) @binding(4) var<storage, read> lattice: array<u32>;
367
+ @group(0) @binding(5) var<storage, read_write> sites: array<f32>;
368
+ @group(0) @binding(6) var<storage, read> spread: array<f32>;
369
+ `;
370
+ /**
371
+ * Pass 2: the expected electrons, readout moments and white clip of every native site of the band's window, with their bounds.
372
+ * The site's electrons are Σ_t table[(m_t·3 + c)·T + t] + dark_c, every term non-negative and rounded once on the host, so the
373
+ * sum carries (T + 3)·E relative (`params.site_gamma`, the host's value). An uncertain lattice point adds the tap's value spread
374
+ * over the materials (`spread`, host float64) to the site's uncertainty, which enters its bounds, or flags the site when it
375
+ * exceeds UNCERTAINTY_LIMIT of the electrons. One dispatch covers the `params.count` sites from `params.site_first` (#492: the
376
+ * host splits a band's sites over several dispatches so that no dispatch gathers more than its tap budget).
377
+ */
378
+ export const DISPLAY_VIEW_QUALITY_SITES_WGSL = /* wgsl */ `${COMMON_WGSL}${siteBindingsWgsl(true)}${SITE_TAIL_WGSL}
379
+ @compute @workgroup_size(${DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE})
380
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) wg: vec3<u32>) {
381
+ let local = gid.x + gid.y * wg.x * ${DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE}u;
382
+ if (local >= params.count) { return; }
383
+ let i = params.site_first + local;
384
+ let ii = i % params.site_w;
385
+ let jj = i / params.site_w;
386
+ let x = params.site_x0 + i32(ii);
387
+ let y = params.site_y0 + i32(jj);
388
+ let c = channel_of(x, y);
389
+ var sum = 0.0;
390
+ var uncertainty = 0.0;
391
+ let T = params.taps;
392
+ for (var t = 0u; t < T; t = t + 1u) {
393
+ let tb = t * TAP;
394
+ let g = taps[tb] * GROUP;
395
+ let index = groups[g + 4u] + (jj + taps[tb + 2u]) * groups[g + 2u] + ii + taps[tb + 1u];
396
+ let raw = lattice[index];
397
+ if ((raw >> 31u) != 0u) { uncertainty = uncertainty + spread[c * T + t]; }
398
+ sum = sum + table[((raw & 0x7fffffffu) * 3u + c) * T + t];
399
+ }
400
+ let e = sum + dark_of(c);
401
+ let flag = select(0u, 1u, uncertainty > UNCERTAINTY_LIMIT * e);
402
+ let de = params.site_gamma * e + uncertainty;
403
+ write_site(i, c, e, de, flag);
404
+ }
405
+ `;
406
+ /**
407
+ * Pass 2 with the defocus disc (#492): the same site electrons from a factored table, summed in blocks.
408
+ *
409
+ * - **Factored table.** A neutral material's value is its reflectance times the white's combined kernel (the reference forms it
410
+ * so: reflectance[m]·neutral_c(t)), so the table holds one neutral row per channel and one row per spectral material, and
411
+ * `materials` holds per material its row (0 for neutral, 1 + s for spectral material s) and its reflectance as float32 bits
412
+ * (1 for a spectral material, an exact product). A scene of thousands of neutral materials (#491's composite: 4 143) then
413
+ * needs 3·T values instead of 3·T per material.
414
+ * - **Blocked sum.** Every group's taps form a dense cw × ch rectangle (the combined kernel's window, row-major from the group's
415
+ * first tap, `groups[g + 5]`), so a site sums each row of a group into a row partial, the rows into a group partial and the
416
+ * groups into the total. No tap records are read: the tap's lattice offset within its group is its (dx, dy).
417
+ *
418
+ * Every term is non-negative and carries two host roundings (u each, u = E/2: the row value and the reflectance), one product
419
+ * (E) and at most cw + ch + G additions, plus the dark electrons' addition, so the electrons carry (cw_max + ch_max + G + 3)·E
420
+ * relative (`params.site_gamma`; recursive summation of the up to 4·(2·96 + 2)² = 150 544 taps of a combined radius of 96 would
421
+ * carry (T + 3)·E ≈ 1.8 % instead). The uncertainty, summed the same way, is scaled by 1 + site_gamma, and `params.site_tiny`
422
+ * (host float64, rounded up) bounds the terms below the float32 normal range, which a device may flush to zero.
423
+ */
424
+ export const DISPLAY_VIEW_QUALITY_SITES_BLOCKED_WGSL = /* wgsl */ `${COMMON_WGSL}${siteBindingsWgsl(false)}
425
+ @group(0) @binding(7) var<storage, read> materials: array<u32>;
426
+ ${SITE_TAIL_WGSL}
427
+ @compute @workgroup_size(${DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE})
428
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) wg: vec3<u32>) {
429
+ let local = gid.x + gid.y * wg.x * ${DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE}u;
430
+ if (local >= params.count) { return; }
431
+ let i = params.site_first + local;
432
+ let ii = i % params.site_w;
433
+ let jj = i / params.site_w;
434
+ let x = params.site_x0 + i32(ii);
435
+ let y = params.site_y0 + i32(jj);
436
+ let c = channel_of(x, y);
437
+ let T = params.taps;
438
+ var sum = 0.0;
439
+ var uncertainty = 0.0;
440
+ for (var k = 0u; k < params.groups; k = k + 1u) {
441
+ let g = k * GROUP;
442
+ let lw = groups[g + 2u];
443
+ let cw = lw - params.site_w + 1u;
444
+ let ch = groups[g + 3u] - params.site_h + 1u;
445
+ let lattice0 = groups[g + 4u] + jj * lw + ii;
446
+ let tap0 = groups[g + 5u];
447
+ var group_sum = 0.0;
448
+ var group_uncertainty = 0.0;
449
+ for (var dy = 0u; dy < ch; dy = dy + 1u) {
450
+ let row = lattice0 + dy * lw;
451
+ let row_tap = tap0 + dy * cw;
452
+ var row_sum = 0.0;
453
+ var row_uncertainty = 0.0;
454
+ for (var dx = 0u; dx < cw; dx = dx + 1u) {
455
+ let raw = lattice[row + dx];
456
+ let t = row_tap + dx;
457
+ let m = 2u * (raw & 0x7fffffffu);
458
+ if ((raw >> 31u) != 0u) { row_uncertainty = row_uncertainty + spread[c * T + t]; }
459
+ row_sum = row_sum + bitcast<f32>(materials[m + 1u]) * table[(materials[m] * 3u + c) * T + t];
460
+ }
461
+ group_sum = group_sum + row_sum;
462
+ group_uncertainty = group_uncertainty + row_uncertainty;
463
+ }
464
+ sum = sum + group_sum;
465
+ uncertainty = uncertainty + group_uncertainty;
466
+ }
467
+ let e = sum + dark_of(c);
468
+ let flag = select(0u, 1u, uncertainty > UNCERTAINTY_LIMIT * e);
469
+ let de = params.site_gamma * e + uncertainty * (1.0 + params.site_gamma) + params.site_tiny;
470
+ write_site(i, c, e, de, flag);
471
+ }
472
+ `;
473
+ /** Pass 3: the display pixels (see the module comment). */
474
+ export const DISPLAY_VIEW_QUALITY_PIXELS_WGSL = /* wgsl */ `${COMMON_WGSL}
475
+ const KEY_LEVELS: u32 = ${AHD_KEY_LEVELS}u;
476
+ const KEY_STEPS: f32 = ${literal(AHD_KEY_STEPS)};
477
+ const KEY_OCTAVES: f32 = ${literal(AHD_KEY_OCTAVES)};
478
+ const KEY_FLOOR: f32 = ${literal(AHD_KEY_FLOOR)};
479
+ const KEY_TOP: u32 = ${AHD_KEY_OCTAVES * AHD_KEY_STEPS - 1}u;
480
+ const CAND_BASE: u32 = ${DISPLAY_VIEW_QUALITY_AUX.candidates}u;
481
+ const CAND_STRIDE: u32 = ${CANDIDATE_PHASE_STRIDE}u;
482
+ const FLAG_UNCERTAIN: u32 = ${DISPLAY_VIEW_QUALITY_FLAG.materialUncertain}u;
483
+ const FLAG_OUTSIDE: u32 = ${DISPLAY_VIEW_QUALITY_FLAG.siteOutsideWindow}u;
484
+ const FLAG_STATE: u32 = ${DISPLAY_VIEW_QUALITY_FLAG.stateMissing}u;
485
+ const FLAG_TIE: u32 = ${DISPLAY_VIEW_QUALITY_FLAG.thresholdTie}u;
486
+ const FLAG_CORRECTED: u32 = ${DISPLAY_VIEW_QUALITY_FLAG.corrected}u;
487
+ @group(0) @binding(1) var<storage, read> aux: array<f32>;
488
+ @group(0) @binding(2) var<storage, read> geometry: array<f32>;
489
+ @group(0) @binding(3) var<storage, read> sites: array<f32>;
490
+ @group(0) @binding(4) var<storage, read> state_keys: array<i32>;
491
+ @group(0) @binding(5) var<storage, read> state_values: array<f32>;
492
+ @group(0) @binding(6) var<storage, read_write> bitmap: array<atomic<u32>>;
493
+ @group(0) @binding(7) var<storage, read_write> out: array<f32>;
494
+ @group(0) @binding(8) var<storage, read_write> flags: array<u32>;
495
+
496
+ var<private> pixel_flags: u32;
497
+ ${REINHARD_WGSL}
498
+ fn reflect_x(x: i32) -> i32 { if (x < 0) { return -x; } if (x >= i32(params.native_w)) { return 2 * i32(params.native_w) - 2 - x; } return x; }
499
+ fn reflect_y(y: i32) -> i32 { if (y < 0) { return -y; } if (y >= i32(params.native_h)) { return 2 * i32(params.native_h) - 2 - y; } return y; }
500
+ // The site buffer offset of native site (x, y) (reflected at the raster's borders), or a flag when it lies outside the window.
501
+ fn site_at(x: i32, y: i32) -> u32 {
502
+ let rx = reflect_x(x) - params.site_x0;
503
+ let ry = reflect_y(y) - params.site_y0;
504
+ if (rx < 0 || ry < 0 || rx >= i32(params.site_w) || ry >= i32(params.site_h)) { pixel_flags = pixel_flags | FLAG_OUTSIDE; return 0u; }
505
+ let o = (u32(ry) * params.site_w + u32(rx)) * SITE;
506
+ pixel_flags = pixel_flags | (bitcast<u32>(sites[o + 6u]) & 1u);
507
+ return o;
508
+ }
509
+ // The offset of a grid key in the sorted state table, or state_count when absent.
510
+ fn find_state(key: i32) -> u32 {
511
+ var lo = 0u; var hi = params.state_count;
512
+ while (lo < hi) {
513
+ let mid = (lo + hi) / 2u;
514
+ if (state_keys[mid] < key) { lo = mid + 1u; } else { hi = mid; }
515
+ }
516
+ if (lo < params.state_count && state_keys[lo] == key) { return lo; }
517
+ return params.state_count;
518
+ }
519
+ // The flat-neighbourhood correction at channel means (m0, m1, m2) with its bound: trilinear between the eight grid states
520
+ // around (log2(clamp(m, 2^-12, 1)) + 12)·8; a missing state flags the pixel. The bound of each axis's coordinate times the
521
+ // largest state difference along that axis bounds the interpolation's dependence on the coordinates' error.
522
+ fn flat_correction(m0: vec2<f32>, m1: vec2<f32>, m2: vec2<f32>, corr: ptr<function, array<vec2<f32>, 3>>) {
523
+ var base: array<u32, 3>; var frac: array<f32, 3>; var dg: array<f32, 3>;
524
+ for (var i = 0u; i < 3u; i = i + 1u) {
525
+ var mi = m0; if (i == 1u) { mi = m1; } if (i == 2u) { mi = m2; }
526
+ let clamped = clamp(mi.x, KEY_FLOOR, 1.0);
527
+ let g = (log2(clamped) + KEY_OCTAVES) * KEY_STEPS;
528
+ let b = min(u32(max(floor(g), 0.0)), KEY_TOP);
529
+ base[i] = b; frac[i] = g - f32(b);
530
+ // log2: 1/ln 2 per relative input error, 2^-21 absolute or 3 ULP (≤ 36E on [2^-12, 1]), the sum and the product.
531
+ var slope = 0.0; if (mi.x > KEY_FLOOR && mi.x < 1.0) { slope = 1.4426950408889634 * mi.y / clamped; }
532
+ dg[i] = KEY_STEPS * (slope + 4.76837158203125e-7 + 36.0 * E) + 2.0 * E * (g + 1.0);
533
+ }
534
+ var values: array<f32, 24>;
535
+ var missing = false;
536
+ for (var corner = 0u; corner < 8u; corner = corner + 1u) {
537
+ let i0 = base[0] + (corner & 1u); let i1 = base[1] + ((corner >> 1u) & 1u); let i2 = base[2] + ((corner >> 2u) & 1u);
538
+ let key = i32((i0 * KEY_LEVELS + i1) * KEY_LEVELS + i2);
539
+ var weight = 1.0;
540
+ for (var i = 0u; i < 3u; i = i + 1u) { weight = weight * select(1.0 - frac[i], frac[i], ((corner >> i) & 1u) == 1u); }
541
+ if (weight == 0.0) { for (var c = 0u; c < 3u; c = c + 1u) { values[corner * 3u + c] = 0.0; } continue; }
542
+ atomicOr(&bitmap[u32(key) / 32u], 1u << (u32(key) % 32u));
543
+ let at = find_state(key);
544
+ if (at >= params.state_count) { missing = true; continue; }
545
+ for (var c = 0u; c < 3u; c = c + 1u) { values[corner * 3u + c] = state_values[at * 3u + c]; }
546
+ }
547
+ if (missing) { pixel_flags = pixel_flags | FLAG_STATE; for (var c = 0u; c < 3u; c = c + 1u) { (*corr)[c] = vec2<f32>(0.0, 0.0); } return; }
548
+ for (var c = 0u; c < 3u; c = c + 1u) {
549
+ var sum = 0.0; var mag = 0.0;
550
+ for (var corner = 0u; corner < 8u; corner = corner + 1u) {
551
+ var weight = 1.0;
552
+ for (var i = 0u; i < 3u; i = i + 1u) { weight = weight * select(1.0 - frac[i], frac[i], ((corner >> i) & 1u) == 1u); }
553
+ sum = sum + weight * values[corner * 3u + c];
554
+ mag = mag + abs(weight * values[corner * 3u + c]);
555
+ }
556
+ // The largest state difference along each axis (over the four edges), times that axis's coordinate bound.
557
+ var slope_term = 0.0;
558
+ for (var i = 0u; i < 3u; i = i + 1u) {
559
+ var largest = 0.0;
560
+ for (var corner = 0u; corner < 8u; corner = corner + 1u) {
561
+ if (((corner >> i) & 1u) == 1u) { continue; }
562
+ largest = max(largest, abs(values[(corner | (1u << i)) * 3u + c] - values[corner * 3u + c]));
563
+ }
564
+ slope_term = slope_term + dg[i] * largest;
565
+ }
566
+ (*corr)[c] = vec2<f32>(sum, slope_term + 12.0 * E * mag);
567
+ }
568
+ }
569
+ @compute @workgroup_size(${DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE})
570
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) wg: vec3<u32>) {
571
+ let i = gid.x + gid.y * wg.x * ${DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE}u;
572
+ if (i >= params.count) { return; }
573
+ pixel_flags = 0u;
574
+ let rx = i % params.region_w;
575
+ let ry = params.row0 + i / params.region_w;
576
+ let col = rx * STRIDE;
577
+ let row = (params.columns + ry) * STRIDE;
578
+ let col_first = bitcast<i32>(geometry[col]);
579
+ let nx = bitcast<u32>(geometry[col + 1u]);
580
+ let row_first = bitcast<i32>(geometry[row]);
581
+ let ny = bitcast<u32>(geometry[row + 1u]);
582
+ let margin = i32(params.margin);
583
+ let reinhard = params.tone == 1u;
584
+ let count = params.candidates;
585
+ let share = select(1.0, 0.5, count == 2u);
586
+ // B_c of the footprint from the host's axis sums (each over its scale squared): red, green, blue.
587
+ let xb_even = geometry[col + 8u]; let xb_odd = geometry[col + 9u];
588
+ let xpp_even = geometry[col + 10u]; let xpp_odd = geometry[col + 11u];
589
+ let xqq_even = geometry[col + 12u]; let xqq_odd = geometry[col + 13u];
590
+ let xpq_even = geometry[col + 14u]; let xpq_odd = geometry[col + 15u];
591
+ let yb_even = geometry[row + 8u]; let yb_odd = geometry[row + 9u];
592
+ let ypp_even = geometry[row + 10u]; let ypp_odd = geometry[row + 11u];
593
+ let yqq_even = geometry[row + 12u]; let yqq_odd = geometry[row + 13u];
594
+ let ypq_even = geometry[row + 14u]; let ypq_odd = geometry[row + 15u];
595
+ let b0 = xb_even * yb_even;
596
+ let b1 = (xpp_odd * ypp_even + xqq_odd * ypp_even + xpp_odd * yqq_even + 2.0 * xpq_odd * ypp_even + 2.0 * xpp_odd * ypq_even + 2.0 * xpq_odd * ypq_even) +
597
+ (xpp_even * ypp_odd + xqq_even * ypp_odd + xpp_even * yqq_odd + 2.0 * xpq_even * ypp_odd + 2.0 * xpp_even * ypq_odd + 2.0 * xpq_even * ypq_odd);
598
+ let b2 = xb_odd * yb_odd;
599
+ var s = array<vec2<f32>, 3>(vec2<f32>(0.0), vec2<f32>(0.0), vec2<f32>(0.0));
600
+ var cov = array<vec2<f32>, 6>(vec2<f32>(0.0), vec2<f32>(0.0), vec2<f32>(0.0), vec2<f32>(0.0), vec2<f32>(0.0), vec2<f32>(0.0));
601
+ var res = array<vec2<f32>, 3>(vec2<f32>(0.0), vec2<f32>(0.0), vec2<f32>(0.0));
602
+ for (var j = 0u; j < ny; j = j + 1u) {
603
+ let ay = geometry[row + 2u + j];
604
+ for (var ii = 0u; ii < nx; ii = ii + 1u) {
605
+ // The overlap weight a_j: two host roundings (the overlaps over the scale) and the product.
606
+ let aj = vec2<f32>(geometry[col + 2u + ii] * ay, 3.0 * E * abs(geometry[col + 2u + ii] * ay));
607
+ let x = col_first + i32(ii) + margin;
608
+ let y = row_first + i32(j) + margin;
609
+ let phase = u32(y & 1) * 2u + u32(x & 1);
610
+ // Site variance of each channel through the bilinear kernels, over Σw² (for B_c).
611
+ var csv: array<vec2<f32>, 3>;
612
+ for (var c = 0u; c < 3u; c = c + 1u) {
613
+ var variance = vec2<f32>(0.0, 0.0);
614
+ let tb = ${DISPLAY_VIEW_QUALITY_AUX.bilinear}u + (phase * 3u + c) * 16u;
615
+ for (var t = 0u; t < 4u; t = t + 1u) {
616
+ let w = aux[tb + 4u * t + 2u];
617
+ if (w == 0.0) { continue; }
618
+ let o = site_at(x + i32(aux[tb + 4u * t]), y + i32(aux[tb + 4u * t + 1u]));
619
+ variance = b_add(variance, b_scale(vec2<f32>(sites[o + 1u], sites[o + 4u]), w * w));
620
+ }
621
+ csv[c] = b_mul(variance, b_rounded(aux[${DISPLAY_VIEW_QUALITY_AUX.inverseW2}u + phase * 3u + c]));
622
+ }
623
+ let cb = CAND_BASE + phase * CAND_STRIDE;
624
+ let entries = bitcast<u32>(aux[cb]);
625
+ var e_mean = array<vec2<f32>, 3>(vec2<f32>(0.0), vec2<f32>(0.0), vec2<f32>(0.0));
626
+ var cm_average = array<vec2<f32>, 3>(vec2<f32>(0.0), vec2<f32>(0.0), vec2<f32>(0.0));
627
+ var pixel_variance = vec2<f32>(0.0, 0.0);
628
+ var bs: array<vec2<f32>, 3>; var hs: array<vec2<f32>, 3>;
629
+ var hv: array<vec2<f32>, 3>;
630
+ for (var d = 0u; d < count; d = d + 1u) {
631
+ // Camera-channel mean and covariance of candidate d from its dense weights over the window.
632
+ var cm: array<vec2<f32>, 3>;
633
+ var C: array<vec2<f32>, 6>;
634
+ // Whether every site a channel reads is exactly at white (a zero bound: the reference's site is exactly 1 too); the
635
+ // dyadic kernel weights then sum exactly on both sides, so the channel's mean carries no error (the host checks the weights).
636
+ var exact = array<bool, 3>(true, true, true);
637
+ for (var c = 0u; c < 3u; c = c + 1u) { cm[c] = vec2<f32>(0.0, 0.0); }
638
+ for (var k = 0u; k < 6u; k = k + 1u) { C[k] = vec2<f32>(0.0, 0.0); }
639
+ for (var n = 0u; n < entries; n = n + 1u) {
640
+ let eb = cb + 4u + n * ${DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS}u + 2u + d * 3u;
641
+ let w0 = aux[eb]; let w1 = aux[eb + 1u]; let w2 = aux[eb + 2u];
642
+ if (w0 == 0.0 && w1 == 0.0 && w2 == 0.0) { continue; }
643
+ let o = site_at(x + i32(aux[cb + 4u + n * ${DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS}u]), y + i32(aux[cb + 4u + n * ${DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS}u + 1u]));
644
+ let sm = vec2<f32>(sites[o], sites[o + 3u]);
645
+ let sv = vec2<f32>(sites[o + 1u], sites[o + 4u]);
646
+ let white = sm.y == 0.0 && sm.x == 1.0;
647
+ exact[0] = exact[0] && (white || w0 == 0.0); exact[1] = exact[1] && (white || w1 == 0.0); exact[2] = exact[2] && (white || w2 == 0.0);
648
+ cm[0] = b_add(cm[0], b_scale(sm, w0)); cm[1] = b_add(cm[1], b_scale(sm, w1)); cm[2] = b_add(cm[2], b_scale(sm, w2));
649
+ C[0] = b_add(C[0], b_scale(sv, w0 * w0)); C[1] = b_add(C[1], b_scale(sv, w0 * w1)); C[2] = b_add(C[2], b_scale(sv, w0 * w2));
650
+ C[3] = b_add(C[3], b_scale(sv, w1 * w1)); C[4] = b_add(C[4], b_scale(sv, w1 * w2)); C[5] = b_add(C[5], b_scale(sv, w2 * w2));
651
+ }
652
+ for (var c = 0u; c < 3u; c = c + 1u) { if (exact[c]) { cm[c].y = 0.0; } cm_average[c] = b_add(cm_average[c], b_scale(cm[c], share)); }
653
+ // White balance and the highlight clip at the smallest gain. A channel exactly at white whose gain is the clip level is
654
+ // exactly at the clip level on both sides.
655
+ var bm: array<vec2<f32>, 3>; var bv: array<vec2<f32>, 3>;
656
+ for (var c = 0u; c < 3u; c = c + 1u) {
657
+ let gc = b_rounded(g_of(c));
658
+ var um = b_mul(gc, cm[c]);
659
+ if (exact[c] && cm[c].x == 1.0 && gain_at_clip(c)) { um = vec2<f32>(params.clip_level, 0.0); }
660
+ let uv = b_mul(b_mul(gc, gc), C[select(select(0u, 3u, c == 1u), 5u, c == 2u)]);
661
+ let gap = params.clip_level - um.x;
662
+ if (gap > 0.0 && gap * gap > params.reach2 * uv.x) { bm[c] = um; bv[c] = uv; bs[c] = gc; }
663
+ else { let clipped = upper_clip(um, uv, params.clip_level); bm[c] = clipped.mean; bv[c] = clipped.variance; bs[c] = b_mul(gc, clipped.slope); }
664
+ }
665
+ // Colour matrix with the full channel covariance, then the rendering of each component.
666
+ var hm: array<vec2<f32>, 3>;
667
+ for (var comp = 0u; comp < 3u; comp = comp + 1u) {
668
+ let r0 = b_rounded(m_of(comp, 0u)); let r1 = b_rounded(m_of(comp, 1u)); let r2 = b_rounded(m_of(comp, 2u));
669
+ let mean = b_add(b_add(b_mul(r0, bm[0]), b_mul(r1, bm[1])), b_mul(r2, bm[2]));
670
+ var variance = b_add(b_add(b_mul(b_mul(r0, r0), bv[0]), b_mul(b_mul(r1, r1), bv[1])), b_mul(b_mul(r2, r2), bv[2]));
671
+ let x01 = b_mul(b_mul(b_mul(r0, r1), b_mul(bs[0], bs[1])), C[1]);
672
+ let x02 = b_mul(b_mul(b_mul(r0, r2), b_mul(bs[0], bs[2])), C[2]);
673
+ let x12 = b_mul(b_mul(b_mul(r1, r2), b_mul(bs[1], bs[2])), C[4]);
674
+ variance = b_add(variance, b_scale(b_add(b_add(x01, x02), x12), 2.0));
675
+ var moments: Moments;
676
+ if (!reinhard && mean.x > 0.0 && mean.x < 1.0 && mean.x * mean.x > params.reach2 * variance.x && (1.0 - mean.x) * (1.0 - mean.x) > params.reach2 * variance.x) {
677
+ moments.mean = mean; moments.variance = variance; moments.slope = vec2<f32>(1.0, 0.0);
678
+ } else if (reinhard) {
679
+ moments = reinhard_moments(mean, variance);
680
+ } else {
681
+ // κ₃ of the component: Σ_sites (Σ_c M_comp,c·Γ_c·w_c,site)³·κ₃(site), over the candidate's dense weights.
682
+ var third = vec2<f32>(0.0, 0.0);
683
+ let k0 = b_mul(r0, bs[0]); let k1 = b_mul(r1, bs[1]); let k2 = b_mul(r2, bs[2]);
684
+ for (var n = 0u; n < entries; n = n + 1u) {
685
+ let eb = cb + 4u + n * ${DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS}u + 2u + d * 3u;
686
+ let w0 = aux[eb]; let w1 = aux[eb + 1u]; let w2 = aux[eb + 2u];
687
+ if (w0 == 0.0 && w1 == 0.0 && w2 == 0.0) { continue; }
688
+ let o = site_at(x + i32(aux[cb + 4u + n * ${DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS}u]), y + i32(aux[cb + 4u + n * ${DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS}u + 1u]));
689
+ let coefficient = b_add(b_add(b_scale(k0, w0), b_scale(k1, w1)), b_scale(k2, w2));
690
+ let cube = b_mul(b_mul(coefficient, coefficient), coefficient);
691
+ third = b_add(third, b_mul(cube, vec2<f32>(sites[o + 2u], sites[o + 5u])));
692
+ }
693
+ var skew = vec2<f32>(0.0, 0.0);
694
+ if (variance.x > 0.0) { skew = b_div(third, b_mul(variance, b_sqrt(variance))); }
695
+ moments = unit_clip(mean, variance, skew);
696
+ }
697
+ hm[comp] = moments.mean; hv[comp] = moments.variance; hs[comp] = moments.slope;
698
+ e_mean[comp] = b_add(e_mean[comp], b_scale(moments.mean, share));
699
+ pixel_variance = vec2<f32>(max(pixel_variance.x, moments.variance.x), max(pixel_variance.y, moments.variance.y));
700
+ }
701
+ // Linear (Stein) part with B_c·σ_c², and the pixel's own linear variance aᵀCa for the residual.
702
+ var an: array<vec2<f32>, 9>;
703
+ for (var c = 0u; c < 3u; c = c + 1u) {
704
+ an[c] = b_mul(b_mul(hs[0], b_rounded(m_of(0u, c))), bs[c]);
705
+ an[3u + c] = b_mul(b_mul(hs[1], b_rounded(m_of(1u, c))), bs[c]);
706
+ an[6u + c] = b_mul(b_mul(hs[2], b_rounded(m_of(2u, c))), bs[c]);
707
+ var bc = b0; if (c == 1u) { bc = b1; } if (c == 2u) { bc = b2; }
708
+ let wgt = b_mul(b_mul(b_scale(aj, share), b_rounded(bc)), csv[c]);
709
+ cov[0] = b_add(cov[0], b_mul(b_mul(wgt, an[c]), an[c])); cov[1] = b_add(cov[1], b_mul(b_mul(wgt, an[c]), an[3u + c])); cov[2] = b_add(cov[2], b_mul(b_mul(wgt, an[c]), an[6u + c]));
710
+ cov[3] = b_add(cov[3], b_mul(b_mul(wgt, an[3u + c]), an[3u + c])); cov[4] = b_add(cov[4], b_mul(b_mul(wgt, an[3u + c]), an[6u + c])); cov[5] = b_add(cov[5], b_mul(b_mul(wgt, an[6u + c]), an[6u + c]));
711
+ }
712
+ for (var comp = 0u; comp < 3u; comp = comp + 1u) {
713
+ var l = vec2<f32>(0.0, 0.0);
714
+ for (var p = 0u; p < 3u; p = p + 1u) {
715
+ for (var q = 0u; q < 3u; q = q + 1u) {
716
+ var cij = C[0]; if (p + q == 1u) { cij = C[1]; } if ((p == 0u && q == 2u) || (p == 2u && q == 0u)) { cij = C[2]; } if (p == 1u && q == 1u) { cij = C[3]; }
717
+ if ((p == 1u && q == 2u) || (p == 2u && q == 1u)) { cij = C[4]; } if (p == 2u && q == 2u) { cij = C[5]; }
718
+ l = b_add(l, b_mul(b_mul(an[comp * 3u + p], an[comp * 3u + q]), cij));
719
+ }
720
+ }
721
+ let t = vec2<f32>(share * max(0.0, hv[comp].x - l.x), share * (hv[comp].y + l.y));
722
+ res[comp] = b_add(res[comp], b_mul(b_mul(aj, aj), t));
723
+ }
724
+ }
725
+ for (var comp = 0u; comp < 3u; comp = comp + 1u) { s[comp] = b_add(s[comp], b_mul(aj, e_mean[comp])); }
726
+ if (count == 2u) {
727
+ // AHD's per-pixel choice: ¼·Var(H − V) at the last candidate's slopes, linear in the sites.
728
+ var x0 = vec2<f32>(0.0, 0.0); var x1 = vec2<f32>(0.0, 0.0); var x2 = vec2<f32>(0.0, 0.0);
729
+ for (var n = 0u; n < entries; n = n + 1u) {
730
+ let eb = cb + 4u + n * ${DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS}u;
731
+ let o = site_at(x + i32(aux[eb]), y + i32(aux[eb + 1u]));
732
+ let d0 = b_scale(bs[0], aux[eb + 2u] - aux[eb + 5u]);
733
+ let d1 = b_scale(bs[1], aux[eb + 3u] - aux[eb + 6u]);
734
+ let d2 = b_scale(bs[2], aux[eb + 4u] - aux[eb + 7u]);
735
+ let sv = vec2<f32>(sites[o + 1u], sites[o + 4u]);
736
+ let a = b_mul(hs[0], b_add(b_add(b_mul(b_rounded(params.m00), d0), b_mul(b_rounded(params.m01), d1)), b_mul(b_rounded(params.m02), d2)));
737
+ let b = b_mul(hs[1], b_add(b_add(b_mul(b_rounded(params.m10), d0), b_mul(b_rounded(params.m11), d1)), b_mul(b_rounded(params.m12), d2)));
738
+ let c = b_mul(hs[2], b_add(b_add(b_mul(b_rounded(params.m20), d0), b_mul(b_rounded(params.m21), d1)), b_mul(b_rounded(params.m22), d2)));
739
+ x0 = b_add(x0, b_mul(b_mul(a, a), sv)); x1 = b_add(x1, b_mul(b_mul(b, b), sv)); x2 = b_add(x2, b_mul(b_mul(c, c), sv));
740
+ }
741
+ let ajaj = b_mul(aj, aj);
742
+ res[0] = b_add(res[0], b_mul(ajaj, b_scale(x0, 0.25))); res[1] = b_add(res[1], b_mul(ajaj, b_scale(x1, 0.25))); res[2] = b_add(res[2], b_mul(ajaj, b_scale(x2, 0.25)));
743
+ }
744
+ if (params.correction == 1u) {
745
+ // The low-signal correction where the pixel noise exceeds the threshold; a decision within its bound is flagged.
746
+ if (abs(pixel_variance.x - params.threshold) <= pixel_variance.y) { pixel_flags = pixel_flags | FLAG_TIE; }
747
+ if (pixel_variance.x > params.threshold) {
748
+ var corr: array<vec2<f32>, 3>;
749
+ flat_correction(cm_average[0], cm_average[1], cm_average[2], &corr);
750
+ for (var comp = 0u; comp < 3u; comp = comp + 1u) { s[comp] = b_add(s[comp], b_mul(aj, corr[comp])); }
751
+ pixel_flags = pixel_flags | FLAG_CORRECTED;
752
+ }
753
+ }
754
+ }
755
+ }
756
+ let o = i * ${DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS}u;
757
+ out[o] = s[0].x; out[o + 1u] = s[1].x; out[o + 2u] = s[2].x;
758
+ out[o + 3u] = cov[0].x + res[0].x; out[o + 4u] = cov[3].x + res[1].x; out[o + 5u] = cov[5].x + res[2].x;
759
+ out[o + 6u] = cov[1].x; out[o + 7u] = cov[2].x; out[o + 8u] = cov[4].x;
760
+ out[o + 9u] = max(s[0].y, max(s[1].y, s[2].y));
761
+ out[o + 10u] = max(max(cov[0].y + res[0].y, cov[3].y + res[1].y), max(cov[5].y + res[2].y, max(cov[1].y, max(cov[2].y, cov[4].y))));
762
+ flags[i] = pixel_flags;
763
+ }
764
+ `;
765
+ /** The lattice pass with a material module appended (`fn material_at(p: vec2<f32>) -> u32`, `var<private> material_uncertain: bool`). */
766
+ export function assembleDisplayViewQualityLatticeWgsl(materialModuleWgsl) {
767
+ return DISPLAY_VIEW_QUALITY_LATTICE_WGSL + "\n" + materialModuleWgsl + "\n";
768
+ }
769
+ //# sourceMappingURL=wgsl-display-view-quality-kernel.js.map