@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.
- package/CHANGELOG.md +156 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +19 -3
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/core/portable-math.d.ts +6 -0
- package/dist/core/portable-math.js +39 -0
- package/dist/core/portable-math.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/chart-material-layers.d.ts +115 -0
- package/dist/facade/chart-material-layers.js +224 -0
- package/dist/facade/chart-material-layers.js.map +1 -0
- package/dist/facade/chart-radiance-batch.d.ts +8 -2
- package/dist/facade/chart-radiance-batch.js +11 -4
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/chart-scenes.d.ts +134 -0
- package/dist/facade/chart-scenes.js +32 -22
- package/dist/facade/chart-scenes.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +8 -2
- package/dist/facade/diffraction-provider.js +71 -2
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.d.ts +18 -0
- package/dist/facade/display-view-gpu-session.js +280 -0
- package/dist/facade/display-view-gpu-session.js.map +1 -0
- package/dist/facade/display-view-gpu.d.ts +146 -0
- package/dist/facade/display-view-gpu.js +59 -0
- package/dist/facade/display-view-gpu.js.map +1 -0
- package/dist/facade/display-view.d.ts +48 -1
- package/dist/facade/display-view.js +7 -7
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +5 -0
- package/dist/facade/expected-signal-setup.js +16 -0
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +48 -3
- package/dist/facade/expected-signal.js +8 -0
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/scene-radiance.d.ts +4 -2
- package/dist/facade/scene-radiance.js +7 -4
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +108 -8
- package/dist/facade/simulate-capture.js +226 -33
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +31 -3
- package/dist/facade/simulate-display-view.js +73 -13
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.d.ts +119 -0
- package/dist/gpu/chart-composite-gpu.js +572 -0
- package/dist/gpu/chart-composite-gpu.js.map +1 -0
- package/dist/gpu/chart-radiance-gpu.d.ts +6 -6
- package/dist/gpu/chart-radiance-gpu.js +9 -11
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-chart-material-module.d.ts +10 -0
- package/dist/gpu/display-view-chart-material-module.js +58 -0
- package/dist/gpu/display-view-chart-material-module.js.map +1 -0
- package/dist/gpu/display-view-composite-material-module.d.ts +68 -0
- package/dist/gpu/display-view-composite-material-module.js +248 -0
- package/dist/gpu/display-view-composite-material-module.js.map +1 -0
- package/dist/gpu/display-view-quality-gpu.d.ts +185 -0
- package/dist/gpu/display-view-quality-gpu.js +1000 -0
- package/dist/gpu/display-view-quality-gpu.js.map +1 -0
- package/dist/gpu/display-view-speed-gpu.d.ts +156 -0
- package/dist/gpu/display-view-speed-gpu.js +524 -0
- package/dist/gpu/display-view-speed-gpu.js.map +1 -0
- package/dist/gpu/image-kernel-accelerator.d.ts +23 -4
- package/dist/gpu/image-kernel-accelerator.js +58 -10
- package/dist/gpu/image-kernel-accelerator.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +55 -3
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/gpu/sensor-stage-gpu.d.ts +11 -0
- package/dist/gpu/sensor-stage-gpu.js +42 -13
- package/dist/gpu/sensor-stage-gpu.js.map +1 -1
- package/dist/gpu/webgpu-runtime.d.ts +4 -2
- package/dist/gpu/webgpu-runtime.js +13 -4
- package/dist/gpu/webgpu-runtime.js.map +1 -1
- package/dist/gpu/webgpu-switch.d.ts +4 -0
- package/dist/gpu/webgpu-switch.js +29 -0
- package/dist/gpu/webgpu-switch.js.map +1 -0
- package/dist/gpu/wgsl-chart-composite-kernel.d.ts +52 -0
- package/dist/gpu/wgsl-chart-composite-kernel.js +515 -0
- package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -0
- package/dist/gpu/wgsl-chart-kernel.d.ts +11 -9
- package/dist/gpu/wgsl-chart-kernel.js +33 -27
- package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +95 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.js +769 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +75 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.js +386 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -0
- package/dist/gpu/wgsl-sensor-kernel.d.ts +28 -9
- package/dist/gpu/wgsl-sensor-kernel.js +94 -50
- package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/optics/defocus-disc-kernel.d.ts +61 -0
- package/dist/optics/defocus-disc-kernel.js +191 -0
- package/dist/optics/defocus-disc-kernel.js.map +1 -0
- package/docs/API_REFERENCE.md +47 -1
- package/docs/QUICK_START.md +150 -5
- package/docs/USAGE.md +81 -3
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +127 -2
- package/docs/api/facade-chart-material-layers.md +137 -0
- package/docs/api/facade-chart-radiance-batch.md +7 -2
- package/docs/api/facade-display-view-gpu.md +94 -0
- package/docs/api/facade-scene-radiance.md +3 -2
- package/docs/api/facade-simulate-capture.md +95 -11
- package/docs/api/facade-simulate-display-view.md +17 -2
- package/docs/api/gpu-image-kernel-accelerator.md +10 -3
- package/docs/api/gpu-image-kernel-execution.md +71 -4
- package/docs/api/gpu-webgpu-switch.md +23 -0
- package/package.json +3 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wgsl-display-view-speed-kernel.js","sourceRoot":"","sources":["../../src/gpu/wgsl-display-view-speed-kernel.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAC9C,6GAA6G;AAC7G,MAAM,CAAC,MAAM,wCAAwC,GAAG,CAAC,CAAC;AAC1D,MAAM,CAAC,MAAM,4BAA4B,GAAG,wCAAwC,GAAG,CAAC,CAAC;AACzF,iGAAiG;AACjG,MAAM,CAAC,MAAM,gCAAgC,GAAG,EAAE,CAAC;AACnD,yIAAyI;AACzI,MAAM,CAAC,MAAM,0BAA0B,GAAG,EAAE,CAAC;AAC7C,8DAA8D;AAC9D,MAAM,CAAC,MAAM,4BAA4B,GAAG,EAAE,CAAC;AAC/C,+CAA+C;AAC/C,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAC;IAC5C,yEAAyE;IACzE,OAAO,EAAE,CAAC;IACV,4FAA4F;IAC5F,IAAI,EAAE,EAAE;IACR,2DAA2D;IAC3D,SAAS,EAAE,GAAG;IACd,0DAA0D;IAC1D,aAAa,EAAE,GAAG;IAClB,IAAI,EAAE,GAAG;CACV,CAAC,CAAC;AACH,yEAAyE;AACzE,MAAM,CAAC,MAAM,wBAAwB,GAAG,EAAE,CAAC;AAC3C,qIAAqI;AACrI,MAAM,SAAS,GAAG,uBAAuB,CAAC;AAC1C;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,CAAC;AAC7C,MAAM,CAAC,MAAM,4BAA4B,GAAG,KAAK,CAAC;AAClD,8GAA8G;AAC9G,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAG,CAAC;AAChD,MAAM,CAAC,MAAM,gCAAgC,GAAG,GAAG,CAAC;AAEpD,MAAM,OAAO,GAAG,CAAC,KAAa,EAAU,EAAE;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACxC,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC;AACjD,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,UAAU,CAAC;iBACxC,SAAS;;;2BAGC,OAAO,CAAC,wBAAwB,CAAC;+BAC7B,OAAO,CAAC,4BAA4B,CAAC;sBAC9C,4BAA4B;sBAC5B,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iCA2GjB,gBAAgB,CAAC,aAAa;;iCAE9B,gBAAgB,CAAC,aAAa,GAAG,EAAE;;;;;;;;;;;;;;;;;;;;;;iBAsBnD,gBAAgB,CAAC,OAAO;;;;;;;;2BAQd,2BAA2B;;uCAEf,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAiE7C,gBAAgB,CAAC,IAAI;;;;;;;;;;kCAUR,gBAAgB,CAAC,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0DA8DF,gBAAgB,CAAC,SAAS;;;;;;;;;gBASpE,0BAA0B;;;;;;;CAOzC,CAAC;AAEF,iGAAiG;AACjG,MAAM,UAAU,4BAA4B,CAAC,kBAA0B;IACrE,OAAO,8BAA8B,GAAG,IAAI,GAAG,kBAAkB,GAAG,IAAI,CAAC;AAC3E,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * WGSL kernel of the display view's Speed path on the GPU (#478): one invocation per display pixel of the exact\n * area mean (docs/DISPLAY_VIEW.md, \"Exact area mean\") without diffraction, with the bilinear demosaic and chart\n * materials. It evaluates `renderDisplayView`'s per-sample loop (facade/display-view.ts) for every real pixel of the\n * display pixel's footprint, in the reference's operation order where the order matters, in float32:\n *\n * 1. the real pixels the footprint covers and their overlap weights a_j = w_j / A (the host computes the 1-D overlaps\n * divided by the scale in float64; the kernel multiplies the two axes);\n * 2. for every native site the real pixels' demosaic reads (the footprint plus one site of reach), the expected\n * electrons: the scene material at the pixel aperture's query offsets (the material module, `material_at`), the\n * material's channel signal from a table the host computed in float64 (Σ_λ A_c(λ)·L(λ)), summed over the offsets\n * in the reference's order, plus the dark electrons;\n * 3. the readout moments of each site (mean k·e, variance k²·e + c₀, third cumulant k³·e) and the clip at the white\n * level (the clipped-normal moments, in a cancellation-free standardized form);\n * 4. per real pixel the bilinear kernels, white balance and highlight clip, the colour matrix, the tone curve\n * (identity, or positive Reinhard by the reference's 4 × 16-point Gauss–Legendre rule) and the clip to [0, 1] with\n * the first-order skewness term;\n * 5. the display pixel's expected value Σ_j a_j·E[L_j], the linear (Stein) covariance Σ_c B_c·σ_c²·a_c·a_cᵀ with the\n * closed-form B_c of the footprint, and the independent residual Σ_j a_j²·r_j.\n *\n * It outputs, per display pixel, the three means, the three variances (linear plus residual), the three covariances\n * and two running error terms: the absolute error bound of the mean and of the variance that the clip steps\n * contribute (a fixed formula evaluated from the pixel's own float32 values; docs/WEBGPU.md, \"Display view (Speed)\").\n * The host adds the output quantization term, draws the noise, encodes and rounds (`display-view-speed-gpu.ts`), so\n * the noise draw is the CPU's bit for bit.\n *\n * Precision: float32 with an a-priori bound per step from the WGSL accuracy table (+ − × within one float32 rounding in\n * either direction, E = 2^-23; division 2.5 ULP, sqrt 4.5 ULP, exp 3 + 2|x| ULP; no trigonometric built-in is used). Every quotient that is a constant of\n * the request (the overlaps over the scale, the axis sums over the squared scale, 1/Σw² of the demosaic kernels) is\n * computed on the host in float64 and uploaded. Data-dependent quotients (z = (t − m)/s, the Abramowitz–Stegun t,\n * the skewness) carry the 2.5 ULP bound.\n */\n\nexport const DISPLAY_VIEW_WORKGROUP_SIZE = 64;\n/** Largest footprint per axis (real pixels): the site window is (n + 2)² ≤ 64 entries of private storage. */\nexport const DISPLAY_VIEW_GPU_MAXIMUM_PIXELS_PER_AXIS = 6;\nexport const DISPLAY_VIEW_GPU_WINDOW_SIDE = DISPLAY_VIEW_GPU_MAXIMUM_PIXELS_PER_AXIS + 2;\n/** Largest number of query offsets (pixel-aperture nodes × filter spots) the aux table holds. */\nexport const DISPLAY_VIEW_GPU_MAXIMUM_OFFSETS = 16;\n/** Floats per display pixel in the output buffer: m0 m1 m2, v00 v11 v22 (linear + residual), c01 c02 c12, mean bound, variance bound. */\nexport const DISPLAY_VIEW_OUTPUT_FLOATS = 11;\n/** Floats per column or row record of the geometry buffer. */\nexport const DISPLAY_VIEW_GEOMETRY_STRIDE = 20;\n/** Layout of the aux table (float32 words). */\nexport const DISPLAY_VIEW_AUX = Object.freeze({\n /** Per offset o: ou (0.5 + ox − W/2), ov (0.5 + oy − H/2), weight, 0. */\n offsets: 0,\n /** Per phase p, channel c, tap t: dx, dy, weight, 0 (4 taps; unused taps have weight 0). */\n taps: 64,\n /** Per phase p, channel c: 1 / Σ_t w_t² (host float64). */\n inverseW2: 256,\n /** 16 Gauss–Legendre nodes then 16 weights on [−1, 1]. */\n gaussLegendre: 272,\n size: 304\n});\n/** Uniform parameter words (see `Params` below): 40 words, 160 bytes. */\nexport const DISPLAY_VIEW_PARAM_WORDS = 40;\n/** The `E` of the bounds: one WGSL rounding of + − × in either direction, 2^-23 (W3C WGSL \"Floating Point Accuracy\"; #471, #487). */\nconst E_LITERAL = \"1.1920928955078125e-7\";\n/**\n * Clip-step constants of the running bound, in units of E (docs/WEBGPU.md, \"Display view (Speed)\", derivation): the\n * absolute error of the clipped-normal mean in units of the standard deviation (`CLIP_MEAN_U`) and of its variance in\n * units of the variance (`CLIP_VARIANCE_U`), for one clip side. The host doubles every bound (MARGIN).\n */\nexport const DISPLAY_VIEW_CLIP_MEAN_U = 1400;\nexport const DISPLAY_VIEW_CLIP_VARIANCE_U = 36000;\n/** The additional relative error of the Reinhard quadrature, in units of E, for the mean and the variance. */\nexport const DISPLAY_VIEW_REINHARD_MEAN_U = 256;\nexport const DISPLAY_VIEW_REINHARD_VARIANCE_U = 512;\n\nconst literal = (value: number): string => {\n const text = String(Math.fround(value));\n return /[.e]/u.test(text) ? text : `${text}.0`;\n};\n\n/**\n * The kernel's own WGSL, without the material module. `assembleDisplayViewSpeedWgsl` appends a module that defines\n * `fn material_at(p: vec2<f32>) -> u32` and `var<private> material_uncertain: bool` (set to true when the point's\n * classification is within its bound of a material boundary; the kernel then flags the display pixel for host\n * recomputation). The module may declare bindings in @group(1) and use the constants E and SLACK declared here.\n */\nexport const DISPLAY_VIEW_SPEED_KERNEL_WGSL = /* wgsl */ `\nconst E: f32 = ${E_LITERAL};\nconst SLACK: f32 = 1e-14;\nconst CLIP_REACH: f32 = 8.5;\nconst CLIP_MEAN_U: f32 = ${literal(DISPLAY_VIEW_CLIP_MEAN_U)};\nconst CLIP_VARIANCE_U: f32 = ${literal(DISPLAY_VIEW_CLIP_VARIANCE_U)};\nconst WINDOW: u32 = ${DISPLAY_VIEW_GPU_WINDOW_SIDE}u;\nconst STRIDE: u32 = ${DISPLAY_VIEW_GEOMETRY_STRIDE}u;\nstruct Params {\n count: u32, row0: u32, region_w: u32, region_x: u32,\n region_y: u32, margin: u32, native_w: u32, native_h: u32,\n columns: u32, tone: u32, offsets: u32, pad0: u32,\n k0: f32, k1: f32, k2: f32, reach2: f32,\n dark0: f32, dark1: f32, dark2: f32, clip_level: f32,\n c0_0: f32, c0_1: f32, c0_2: f32, pad1: f32,\n g0: f32, g1: f32, g2: f32, pad2: f32,\n m00: f32, m01: f32, m02: f32, m10: f32,\n m11: f32, m12: f32, m20: f32, m21: f32,\n m22: f32, scale_u: f32, scale_v: f32, pad3: f32,\n};\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> aux: array<f32>;\n@group(0) @binding(2) var<storage, read> geometry: array<f32>;\n@group(0) @binding(3) var<storage, read> site_table: array<f32>;\n@group(0) @binding(4) var<storage, read_write> out: array<f32>;\n@group(0) @binding(5) var<storage, read_write> flags: array<u32>;\n\nvar<private> site_mean: array<f32, 64>;\nvar<private> site_var: array<f32, 64>;\nvar<private> site_third: array<f32, 64>;\nvar<private> site_bound: array<f32, 64>;\n\nfn k_of(c: u32) -> f32 { if (c == 0u) { return params.k0; } if (c == 1u) { return params.k1; } return params.k2; }\nfn dark_of(c: u32) -> f32 { if (c == 0u) { return params.dark0; } if (c == 1u) { return params.dark1; } return params.dark2; }\nfn c0_of(c: u32) -> f32 { if (c == 0u) { return params.c0_0; } if (c == 1u) { return params.c0_1; } return params.c0_2; }\nfn g_of(c: u32) -> f32 { if (c == 0u) { return params.g0; } if (c == 1u) { return params.g1; } return params.g2; }\nfn m_of(r: u32, c: u32) -> f32 {\n let i = r * 3u + c;\n switch (i) {\n case 0u: { return params.m00; } case 1u: { return params.m01; } case 2u: { return params.m02; }\n case 3u: { return params.m10; } case 4u: { return params.m11; } case 5u: { return params.m12; }\n case 6u: { return params.m20; } case 7u: { return params.m21; } default: { return params.m22; }\n }\n}\n// Standard normal density, as the reference forms it: 0.3989422804014327·exp(−0.5·z·z).\nfn phi(z: f32) -> f32 { return 0.3989422804014327 * exp(-0.5 * z * z); }\n// Abramowitz & Stegun 26.2.17: returns (Φ(z), 1 − Φ(z)), each formed from the tail q directly where it is small.\nfn pq(z: f32) -> vec2<f32> {\n let x = abs(z);\n let t = 1.0 / (1.0 + 0.2316419 * x);\n let q = phi(x) * t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));\n if (z >= 0.0) { return vec2<f32>(1.0 - q, q); }\n return vec2<f32>(q, 1.0 - q);\n}\n// Moments of min(Y, top) for Y ~ N(m, v): (mean, variance, slope P). The variance is v·(P − zφ + z²Q − δ²) with\n// δ = φ − zQ: algebraically E[min²] − E[min]² without the m² cancellation.\nfn upper_clip(m: f32, v: f32, top: f32) -> vec3<f32> {\n if (!(v > 0.0)) { return vec3<f32>(min(m, top), 0.0, select(0.0, 1.0, m < top)); }\n let s = sqrt(v);\n let z = (top - m) / s;\n if (z > CLIP_REACH) { return vec3<f32>(m, v, 1.0); }\n if (z < -CLIP_REACH) { return vec3<f32>(top, 0.0, 0.0); }\n let p = pq(z);\n let d = phi(z);\n let delta = d - z * p.y;\n let mean = m - s * delta;\n let ratio = p.x - z * d + z * z * p.y - delta * delta;\n return vec3<f32>(mean, v * max(0.0, ratio), p.x);\n}\n// Moments of clip(Y, 0, 1) for Y with mean m, variance v and standardized skewness γ (|γ| ≤ 1): (mean, variance, inside).\n// With Z standard normal, W = m + s·g(Z), g = clip(Z, a, b): E[g] = aΦa + φa − φb + bQb, E[g²] = a²Φa + (Φb − Φa) −\n// (bφb − aφa) + b²Qb; the mean adds the reference's Edgeworth term (s·γ/6)·(aφa − bφb).\nfn unit_clip(m: f32, v: f32, skewness: f32) -> vec3<f32> {\n if (!(v > 0.0)) { return vec3<f32>(clamp(m, 0.0, 1.0), 0.0, select(0.0, 1.0, m > 0.0 && m < 1.0)); }\n let s = sqrt(v);\n let a = -m / s;\n let b = (1.0 - m) / s;\n if (a < -CLIP_REACH && b > CLIP_REACH) { return vec3<f32>(m, v, 1.0); }\n var pa = 0.0; var da = 0.0;\n if (!(a < -CLIP_REACH)) { pa = pq(a).x; da = phi(a); }\n var pb = 1.0; var qb = 0.0; var db = 0.0;\n if (!(b > CLIP_REACH)) { let p = pq(b); pb = p.x; qb = p.y; db = phi(b); }\n let inside = pb - pa;\n let gamma = clamp(skewness, -1.0, 1.0);\n let aa = a * da;\n let bb = b * db;\n let eg = a * pa + (da - db) + b * qb;\n let eg2 = a * a * pa + inside - (bb - aa) + b * b * qb;\n let normal_mean = m + s * eg;\n var mean = normal_mean;\n var skew_shift = 0.0;\n if (gamma != 0.0) { skew_shift = s * gamma / 6.0 * (aa - bb); mean = normal_mean + skew_shift; }\n // The reference forms E[Y²] − (E[Y] + shift)², so the shift also enters its variance: −shift·(2·E[Y] + shift).\n return vec3<f32>(mean, max(0.0, v * (eg2 - eg * eg) - skew_shift * (2.0 * normal_mean + skew_shift)), inside);\n}\n// Positive Reinhard per channel then the clip to [0, 1]: h(x) = x/(1 + x) for x > 0, else 0. Composite 16-point\n// Gauss–Legendre over [max(0, m − 8.5s), m + 8.5s] in 4 panels, as the reference; the variance is formed about h(m).\nfn reinhard_moments(m: f32, v: f32) -> vec3<f32> {\n if (!(v > 0.0)) {\n if (m > 0.0) { return vec3<f32>(m / (1.0 + m), 0.0, 1.0 / ((1.0 + m) * (1.0 + m))); }\n return vec3<f32>(0.0, 0.0, 0.0);\n }\n let s = sqrt(v);\n // The reference's panels [max(0, m − 8.5s), m + 8.5s] in standardized units z = (x − m)/s, so the node's z is formed\n // without the cancellation x − m: zlo = max(−m/s, −8.5), weight = zhalf·w_k·φ(z_k) (= half·w_k·φ((x − m)/s)/s), x = m + s·z.\n let zlo = max(-m / s, -CLIP_REACH);\n if (!(CLIP_REACH > zlo)) { return vec3<f32>(0.0, 0.0, 0.0); }\n let hm = select(0.0, m / (1.0 + m), m > 0.0);\n var e1 = 0.0; var e2 = 0.0; var ed = 0.0;\n let zwidth = (CLIP_REACH - zlo) / 4.0;\n for (var p = 0u; p < 4u; p = p + 1u) {\n let zc = zlo + (f32(p) + 0.5) * zwidth;\n let zhalf = zwidth / 2.0;\n for (var k = 0u; k < 16u; k = k + 1u) {\n let z = zc + zhalf * aux[${DISPLAY_VIEW_AUX.gaussLegendre}u + k];\n let x = m + s * z;\n let weight = zhalf * aux[${DISPLAY_VIEW_AUX.gaussLegendre + 16}u + k] * phi(z);\n let h = x / (1.0 + x);\n e1 = e1 + weight * h;\n e2 = e2 + weight * (h - hm) * (h - hm);\n ed = ed + weight / ((1.0 + x) * (1.0 + x));\n }\n }\n let shifted = e1 - hm;\n return vec3<f32>(e1, max(0.0, e2 - shifted * shifted), ed);\n}\nfn 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; }\nfn 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; }\nfn channel_of(x: i32, y: i32) -> u32 {\n if ((y & 1) == 0) { return select(1u, 0u, (x & 1) == 0); }\n return select(2u, 1u, (x & 1) == 0);\n}\n// The expected photo electrons of native site (x, y): Σ_o w_o·table[m_o, c] in offset order (directSite).\nfn site_electrons(x: i32, y: i32, c: u32) -> f32 {\n var sum = 0.0;\n let fx = f32(x);\n let fy = f32(y);\n for (var o = 0u; o < params.offsets; o = o + 1u) {\n let base = ${DISPLAY_VIEW_AUX.offsets}u + 4u * o;\n let u = (fx + aux[base]) * params.scale_u;\n let v = (fy + aux[base + 1u]) * params.scale_v;\n let m = material_at(vec2<f32>(u, v));\n sum = sum + aux[base + 2u] * site_table[m * 3u + c];\n }\n return sum;\n}\n@compute @workgroup_size(${DISPLAY_VIEW_WORKGROUP_SIZE})\nfn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) groups: vec3<u32>) {\n let i = gid.x + gid.y * groups.x * ${DISPLAY_VIEW_WORKGROUP_SIZE}u;\n if (i >= params.count) { return; }\n material_uncertain = false;\n let rx = i % params.region_w;\n let ry = params.row0 + i / params.region_w;\n let col = rx * STRIDE;\n let row = (params.columns + ry) * STRIDE;\n let col_first = bitcast<i32>(geometry[col]);\n let nx = bitcast<u32>(geometry[col + 1u]);\n let row_first = bitcast<i32>(geometry[row]);\n let ny = bitcast<u32>(geometry[row + 1u]);\n let margin = i32(params.margin);\n // 2. and 3.: the sites of the footprint plus one site of reach, once each.\n let x0 = col_first + margin - 1;\n let y0 = row_first + margin - 1;\n let window_w = nx + 2u;\n for (var sj = 0u; sj < ny + 2u; sj = sj + 1u) {\n let sy = reflect_y(y0 + i32(sj));\n for (var si = 0u; si < window_w; si = si + 1u) {\n let sx = reflect_x(x0 + i32(si));\n let c = channel_of(sx, sy);\n let e = site_electrons(sx, sy, c) + dark_of(c);\n let kc = k_of(c);\n let mean = kc * e;\n let variance = kc * kc * e + c0_of(c);\n let gap = 1.0 - mean;\n let slot = sj * window_w + si;\n site_third[slot] = kc * kc * kc * e;\n if (gap > 0.0 && gap * gap > params.reach2 * variance) {\n site_mean[slot] = mean; site_var[slot] = variance; site_bound[slot] = 0.0;\n } else {\n let clipped = upper_clip(mean, variance, 1.0);\n site_mean[slot] = clipped.x; site_var[slot] = clipped.y; site_bound[slot] = sqrt(variance);\n }\n }\n }\n // B_c of the footprint from the host's axis sums (each over its scale squared): red, green, blue.\n let xb_even = geometry[col + 8u]; let xb_odd = geometry[col + 9u];\n let xpp_even = geometry[col + 10u]; let xpp_odd = geometry[col + 11u];\n let xqq_even = geometry[col + 12u]; let xqq_odd = geometry[col + 13u];\n let xpq_even = geometry[col + 14u]; let xpq_odd = geometry[col + 15u];\n let yb_even = geometry[row + 8u]; let yb_odd = geometry[row + 9u];\n let ypp_even = geometry[row + 10u]; let ypp_odd = geometry[row + 11u];\n let yqq_even = geometry[row + 12u]; let yqq_odd = geometry[row + 13u];\n let ypq_even = geometry[row + 14u]; let ypq_odd = geometry[row + 15u];\n let b0 = xb_even * yb_even;\n 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) +\n (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);\n let b2 = xb_odd * yb_odd;\n let reinhard = params.tone == 1u;\n var s0 = 0.0; var s1 = 0.0; var s2 = 0.0;\n var c00 = 0.0; var c01 = 0.0; var c02 = 0.0; var c11 = 0.0; var c12 = 0.0; var c22 = 0.0;\n var r0 = 0.0; var r1 = 0.0; var r2 = 0.0;\n var bound_mean = 0.0; var bound_var = 0.0;\n // 4. and 5.: every real pixel of the footprint with its overlap weight.\n for (var j = 0u; j < ny; j = j + 1u) {\n let ay = geometry[row + 2u + j];\n for (var ii = 0u; ii < nx; ii = ii + 1u) {\n let aj = geometry[col + 2u + ii] * ay;\n let x = col_first + i32(ii) + margin;\n let y = row_first + i32(j) + margin;\n let phase = u32(y & 1) * 2u + u32(x & 1);\n var cm: array<f32, 3>; var cvv: array<f32, 3>; var cth: array<f32, 3>; var csv: array<f32, 3>; var cb: array<f32, 3>;\n for (var c = 0u; c < 3u; c = c + 1u) {\n var mean = 0.0; var variance = 0.0; var third = 0.0; var bnd = 0.0;\n let taps = ${DISPLAY_VIEW_AUX.taps}u + (phase * 3u + c) * 16u;\n for (var t = 0u; t < 4u; t = t + 1u) {\n let w = aux[taps + 4u * t + 2u];\n let slot = u32((i32(j) + 1 + i32(aux[taps + 4u * t + 1u])) * i32(window_w) + i32(ii) + 1 + i32(aux[taps + 4u * t]));\n mean = mean + w * site_mean[slot];\n variance = variance + w * w * site_var[slot];\n third = third + w * w * w * site_third[slot];\n bnd = max(bnd, select(0.0, site_bound[slot], w != 0.0));\n }\n cm[c] = mean; cvv[c] = variance; cth[c] = third; cb[c] = bnd;\n csv[c] = variance * aux[${DISPLAY_VIEW_AUX.inverseW2}u + phase * 3u + c];\n }\n // White balance and the highlight clip at the smallest gain: bm, bv, bs (slope), with the clip's bound terms.\n var bm: array<f32, 3>; var bv: array<f32, 3>; var bs: array<f32, 3>; var bbm: array<f32, 3>; var bbv: array<f32, 3>;\n for (var c = 0u; c < 3u; c = c + 1u) {\n let gc = g_of(c);\n let um = gc * cm[c];\n let uv = gc * gc * cvv[c];\n let gap = params.clip_level - um;\n bbm[c] = gc * CLIP_MEAN_U * cb[c];\n bbv[c] = gc * gc * CLIP_VARIANCE_U * cb[c] * cb[c];\n if (gap > 0.0 && gap * gap > params.reach2 * uv) { bm[c] = um; bv[c] = uv; bs[c] = gc; }\n else {\n let clipped = upper_clip(um, uv, params.clip_level);\n bm[c] = clipped.x; bv[c] = clipped.y; bs[c] = gc * clipped.z;\n bbm[c] = bbm[c] + CLIP_MEAN_U * sqrt(uv);\n bbv[c] = bbv[c] + CLIP_VARIANCE_U * uv;\n }\n }\n // Colour matrix, then the rendering of each component (tone curve, clip to [0, 1]).\n var hm: array<f32, 3>; var hv: array<f32, 3>; var hs: array<f32, 3>; var hbm: array<f32, 3>; var hbv: array<f32, 3>;\n for (var comp = 0u; comp < 3u; comp = comp + 1u) {\n let r0m = m_of(comp, 0u); let r1m = m_of(comp, 1u); let r2m = m_of(comp, 2u);\n let mean = r0m * bm[0] + r1m * bm[1] + r2m * bm[2];\n let variance = r0m * r0m * bv[0] + r1m * r1m * bv[1] + r2m * r2m * bv[2];\n var bm_c = abs(r0m) * bbm[0] + abs(r1m) * bbm[1] + abs(r2m) * bbm[2];\n var bv_c = r0m * r0m * bbv[0] + r1m * r1m * bbv[1] + r2m * r2m * bbv[2];\n if (!reinhard && mean > 0.0 && mean < 1.0 && mean * mean > params.reach2 * variance && (1.0 - mean) * (1.0 - mean) > params.reach2 * variance) {\n hm[comp] = mean; hv[comp] = variance; hs[comp] = 1.0;\n } else if (reinhard) {\n let moments = reinhard_moments(mean, variance);\n hm[comp] = moments.x; hv[comp] = moments.y; hs[comp] = moments.z;\n bm_c = bm_c + 2.0 * CLIP_MEAN_U * sqrt(variance);\n bv_c = bv_c + 2.0 * CLIP_VARIANCE_U * variance;\n } else {\n // κ₃ of the component: Σ_c (M_comp,c·Γ_c)³·Σ_t w_t³·κ₃(site) (the sites of one channel share the coefficient).\n let k0 = r0m * bs[0]; let k1 = r1m * bs[1]; let k2 = r2m * bs[2];\n let third = k0 * k0 * k0 * cth[0] + k1 * k1 * k1 * cth[1] + k2 * k2 * k2 * cth[2];\n let skew = select(0.0, third / (variance * sqrt(variance)), variance > 0.0);\n let moments = unit_clip(mean, variance, skew);\n hm[comp] = moments.x; hv[comp] = moments.y; hs[comp] = moments.z;\n bm_c = bm_c + 2.0 * CLIP_MEAN_U * sqrt(variance);\n bv_c = bv_c + 2.0 * CLIP_VARIANCE_U * variance;\n }\n hbm[comp] = bm_c; hbv[comp] = bv_c;\n }\n s0 = s0 + aj * hm[0]; s1 = s1 + aj * hm[1]; s2 = s2 + aj * hm[2];\n // Linear (Stein) part with B_c·σ_c², and the pixel's own linear variance for the residual.\n var l0 = 0.0; var l1 = 0.0; var l2 = 0.0;\n var lb0 = 0.0; var lb1 = 0.0; var lb2 = 0.0;\n for (var c = 0u; c < 3u; c = c + 1u) {\n let a0 = hs[0] * m_of(0u, c) * bs[c];\n let a1 = hs[1] * m_of(1u, c) * bs[c];\n let a2 = hs[2] * m_of(2u, c) * bs[c];\n var bc = b0; if (c == 1u) { bc = b1; } if (c == 2u) { bc = b2; }\n let wgt = aj * bc * csv[c];\n c00 = c00 + wgt * a0 * a0; c01 = c01 + wgt * a0 * a1; c02 = c02 + wgt * a0 * a2;\n c11 = c11 + wgt * a1 * a1; c12 = c12 + wgt * a1 * a2; c22 = c22 + wgt * a2 * a2;\n l0 = l0 + a0 * a0 * cvv[c]; l1 = l1 + a1 * a1 * cvv[c]; l2 = l2 + a2 * a2 * cvv[c];\n // The clip bound of each channel's variance enters the covariance through a_j·B_c·(Δσ_c²/Σw²)·a_i·a_j and the\n // residual's linear prediction through a²·Δσ_c².\n lb0 = lb0 + (a0 * a0) * bbv[c]; lb1 = lb1 + (a1 * a1) * bbv[c]; lb2 = lb2 + (a2 * a2) * bbv[c];\n bound_var = bound_var + aj * bc * (bbv[c] * aux[${DISPLAY_VIEW_AUX.inverseW2}u + phase * 3u + c]) * max(a0 * a0, max(a1 * a1, a2 * a2));\n }\n r0 = r0 + aj * aj * max(0.0, hv[0] - l0);\n r1 = r1 + aj * aj * max(0.0, hv[1] - l1);\n r2 = r2 + aj * aj * max(0.0, hv[2] - l2);\n bound_mean = bound_mean + aj * max(hbm[0], max(hbm[1], hbm[2]));\n bound_var = bound_var + aj * aj * (max(hbv[0], max(hbv[1], hbv[2])) + max(lb0, max(lb1, lb2)));\n }\n }\n let o = i * ${DISPLAY_VIEW_OUTPUT_FLOATS}u;\n out[o] = s0; out[o + 1u] = s1; out[o + 2u] = s2;\n out[o + 3u] = c00 + r0; out[o + 4u] = c11 + r1; out[o + 5u] = c22 + r2;\n out[o + 6u] = c01; out[o + 7u] = c02; out[o + 8u] = c12;\n out[o + 9u] = bound_mean * E; out[o + 10u] = bound_var * E;\n flags[i] = select(0u, 1u, material_uncertain);\n}\n`;\n\n/** The full shader: the kernel plus a material module (see `DISPLAY_VIEW_SPEED_KERNEL_WGSL`). */\nexport function assembleDisplayViewSpeedWgsl(materialModuleWgsl: string): string {\n return DISPLAY_VIEW_SPEED_KERNEL_WGSL + \"\\n\" + materialModuleWgsl + \"\\n\";\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WGSL kernel for the `webgpu-f32` sensor stage (#277), one invocation per native site. It evaluates the
|
|
2
|
+
* WGSL kernel for the `webgpu-f32` sensor stage (#277, #471), one invocation per native site. It evaluates the
|
|
3
3
|
* reference `simulatePackedSensorSite` (packed-sensor-raw.ts) with the same seeded noise model
|
|
4
4
|
* (`photivra-native-raw-noise` 0.4.0):
|
|
5
5
|
* - the MurmurHash3_x86_32 site seeds, fmix32 scrambling and xorshift32 streams are 32-bit integer
|
|
@@ -18,11 +18,21 @@
|
|
|
18
18
|
* charge stream after photo and dark charge, in declaration order; deterministic components enter as an
|
|
19
19
|
* exact integer part plus a float32 fraction, with their own bounds.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* the
|
|
25
|
-
*
|
|
21
|
+
* Arithmetic assumptions (W3C WGSL, "Floating Point Accuracy" and "Reassociation and Fusion"; every margin
|
|
22
|
+
* is derived in docs/WEBGPU.md, "Sensor-stage margins"):
|
|
23
|
+
* - E = 2^-23. WGSL specifies no rounding direction, so a "correctly rounded" operation (+ − ×, floor, f32(u32))
|
|
24
|
+
* is within one ULP of the exact value, that is ≤ E relative; `/` is 2.5 ULP (≤ 2.5E); `sqrt` inherits
|
|
25
|
+
* 1.0 / inverseSqrt(x) (≤ 4.5E); `exp` is (3 + 2|x|) ULP; `log` is 3 ULP outside [0.5, 2] and 2^-21
|
|
26
|
+
* absolute inside. WGSL's `cos` has only a 2^-11 absolute bound, so the kernel uses its own
|
|
27
|
+
* octant-reduced polynomials.
|
|
28
|
+
* - No decision depends on correctly rounded division: 1/gain and the digital gain arrive from the host as
|
|
29
|
+
* exact 12-bit pieces, INL segments as host-computed slopes, and every quotient that remains (Box–Muller
|
|
30
|
+
* logarithm, PTRS) carries the 2.5 ULP bound.
|
|
31
|
+
* - WGSL lets an implementation reassociate. The integer parts of the ADC input are therefore formed in
|
|
32
|
+
* integer arithmetic, and every floating-point sum carries the order-independent bound
|
|
33
|
+
* (n − 1)·E·Σ|terms| (Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., §4.2), so the
|
|
34
|
+
* sums' margins hold under any association. Fusing a multiply and an add only removes a rounding.
|
|
35
|
+
* - Every margin is then doubled (`MARGIN`), which covers the second-order terms dropped in the derivations.
|
|
26
36
|
*/
|
|
27
37
|
/** Output word: code in bits 0–15, saturation flags in bits 16–19, the flagged (uncertain) bit 20. */
|
|
28
38
|
export declare const SENSOR_OUTPUT: Readonly<{
|
|
@@ -30,14 +40,19 @@ export declare const SENSOR_OUTPUT: Readonly<{
|
|
|
30
40
|
flagShift: 16;
|
|
31
41
|
uncertainBit: number;
|
|
32
42
|
}>;
|
|
33
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Auxiliary table layout (float32 words). A gain's four 12-bit pieces are stored as `SENSOR_PIECE_WORDS` words
|
|
45
|
+
* each: the integer mantissa m (< 2^12), the exponent s and the exact scale 2^s, so that piece = m·2^s. The INL
|
|
46
|
+
* table holds the knot codes, the knot offsets and the host-computed segment slopes.
|
|
47
|
+
*/
|
|
34
48
|
export declare const SENSOR_AUX: Readonly<{
|
|
35
49
|
stirlingRemainder: 0;
|
|
36
50
|
sigma: 256;
|
|
37
51
|
inversePieces: 272;
|
|
38
|
-
digitalPieces:
|
|
39
|
-
knots:
|
|
52
|
+
digitalPieces: 284;
|
|
53
|
+
knots: 296;
|
|
40
54
|
}>;
|
|
55
|
+
export declare const SENSOR_PIECE_WORDS = 3;
|
|
41
56
|
export declare const SENSOR_WORKGROUP_SIZE = 256;
|
|
42
57
|
/**
|
|
43
58
|
* Per-site packed words: photo word, photo fraction bits, dark word, dark fraction bits; then 2 words per
|
|
@@ -59,6 +74,10 @@ export declare const SENSOR_MEAN_CATEGORY: Readonly<{
|
|
|
59
74
|
transformedRejection: 2;
|
|
60
75
|
normal: 3;
|
|
61
76
|
}>;
|
|
77
|
+
/** One WGSL rounding (one ULP, either direction) is at most this relative error: 2^-23. */
|
|
78
|
+
export declare const SENSOR_ROUNDING: number;
|
|
79
|
+
/** |z_f32 − z_f64| ≤ SENSOR_NORMAL_ERROR·E·radius for the Box–Muller normal (docs/WEBGPU.md). */
|
|
80
|
+
export declare const SENSOR_NORMAL_ERROR = 20;
|
|
62
81
|
/** Smallest integer m with (m + 0.5)/2^32 ≥ t, so `uS ≥ t` ⇔ `m ≥ result` exactly. */
|
|
63
82
|
export declare function uniformThresholdInteger(t: number): number;
|
|
64
83
|
export declare const SENSOR_RAW_WGSL: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
/**
|
|
3
|
-
* WGSL kernel for the `webgpu-f32` sensor stage (#277), one invocation per native site. It evaluates the
|
|
3
|
+
* WGSL kernel for the `webgpu-f32` sensor stage (#277, #471), one invocation per native site. It evaluates the
|
|
4
4
|
* reference `simulatePackedSensorSite` (packed-sensor-raw.ts) with the same seeded noise model
|
|
5
5
|
* (`photivra-native-raw-noise` 0.4.0):
|
|
6
6
|
* - the MurmurHash3_x86_32 site seeds, fmix32 scrambling and xorshift32 streams are 32-bit integer
|
|
@@ -19,16 +19,31 @@
|
|
|
19
19
|
* charge stream after photo and dark charge, in declaration order; deterministic components enter as an
|
|
20
20
|
* exact integer part plus a float32 fraction, with their own bounds.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* the
|
|
26
|
-
*
|
|
22
|
+
* Arithmetic assumptions (W3C WGSL, "Floating Point Accuracy" and "Reassociation and Fusion"; every margin
|
|
23
|
+
* is derived in docs/WEBGPU.md, "Sensor-stage margins"):
|
|
24
|
+
* - E = 2^-23. WGSL specifies no rounding direction, so a "correctly rounded" operation (+ − ×, floor, f32(u32))
|
|
25
|
+
* is within one ULP of the exact value, that is ≤ E relative; `/` is 2.5 ULP (≤ 2.5E); `sqrt` inherits
|
|
26
|
+
* 1.0 / inverseSqrt(x) (≤ 4.5E); `exp` is (3 + 2|x|) ULP; `log` is 3 ULP outside [0.5, 2] and 2^-21
|
|
27
|
+
* absolute inside. WGSL's `cos` has only a 2^-11 absolute bound, so the kernel uses its own
|
|
28
|
+
* octant-reduced polynomials.
|
|
29
|
+
* - No decision depends on correctly rounded division: 1/gain and the digital gain arrive from the host as
|
|
30
|
+
* exact 12-bit pieces, INL segments as host-computed slopes, and every quotient that remains (Box–Muller
|
|
31
|
+
* logarithm, PTRS) carries the 2.5 ULP bound.
|
|
32
|
+
* - WGSL lets an implementation reassociate. The integer parts of the ADC input are therefore formed in
|
|
33
|
+
* integer arithmetic, and every floating-point sum carries the order-independent bound
|
|
34
|
+
* (n − 1)·E·Σ|terms| (Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., §4.2), so the
|
|
35
|
+
* sums' margins hold under any association. Fusing a multiply and an add only removes a rounding.
|
|
36
|
+
* - Every margin is then doubled (`MARGIN`), which covers the second-order terms dropped in the derivations.
|
|
27
37
|
*/
|
|
28
38
|
/** Output word: code in bits 0–15, saturation flags in bits 16–19, the flagged (uncertain) bit 20. */
|
|
29
39
|
export const SENSOR_OUTPUT = Object.freeze({ codeMask: 0xffff, flagShift: 16, uncertainBit: 1 << 20 });
|
|
30
|
-
/**
|
|
31
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Auxiliary table layout (float32 words). A gain's four 12-bit pieces are stored as `SENSOR_PIECE_WORDS` words
|
|
42
|
+
* each: the integer mantissa m (< 2^12), the exponent s and the exact scale 2^s, so that piece = m·2^s. The INL
|
|
43
|
+
* table holds the knot codes, the knot offsets and the host-computed segment slopes.
|
|
44
|
+
*/
|
|
45
|
+
export const SENSOR_AUX = Object.freeze({ stirlingRemainder: 0, sigma: 256, inversePieces: 272, digitalPieces: 284, knots: 296 });
|
|
46
|
+
export const SENSOR_PIECE_WORDS = 3;
|
|
32
47
|
export const SENSOR_WORKGROUP_SIZE = 256;
|
|
33
48
|
/**
|
|
34
49
|
* Per-site packed words: photo word, photo fraction bits, dark word, dark fraction bits; then 2 words per
|
|
@@ -42,6 +57,10 @@ export const SENSOR_EXTRAS = Object.freeze({ deterministic: 1, fixedPattern: 2 }
|
|
|
42
57
|
export const SENSOR_REALIZED_CLAMP = 2 ** 30;
|
|
43
58
|
/** Mean categories in bits 30–31 of a mean word, mirroring `samplePoisson`'s branches. */
|
|
44
59
|
export const SENSOR_MEAN_CATEGORY = Object.freeze({ zero: 0, multiplication: 1, transformedRejection: 2, normal: 3 });
|
|
60
|
+
/** One WGSL rounding (one ULP, either direction) is at most this relative error: 2^-23. */
|
|
61
|
+
export const SENSOR_ROUNDING = 2 ** -23;
|
|
62
|
+
/** |z_f32 − z_f64| ≤ SENSOR_NORMAL_ERROR·E·radius for the Box–Muller normal (docs/WEBGPU.md). */
|
|
63
|
+
export const SENSOR_NORMAL_ERROR = 20;
|
|
45
64
|
/** Smallest integer m with (m + 0.5)/2^32 ≥ t, so `uS ≥ t` ⇔ `m ≥ result` exactly. */
|
|
46
65
|
export function uniformThresholdInteger(t) {
|
|
47
66
|
return Math.ceil(t * 2 ** 32 - 0.5);
|
|
@@ -53,11 +72,13 @@ const literal = (value) => {
|
|
|
53
72
|
return /[.e]/u.test(text) ? text : `${text}.0`;
|
|
54
73
|
};
|
|
55
74
|
export const SENSOR_RAW_WGSL = /* wgsl */ `
|
|
56
|
-
const
|
|
75
|
+
const E: f32 = 1.1920928955078125e-7;
|
|
57
76
|
const INV32: f32 = 2.3283064365386963e-10;
|
|
58
77
|
const TWO_PI_INV32: f32 = ${literal(2 * Math.PI / 2 ** 32)};
|
|
59
78
|
const MARGIN: f32 = 2.0;
|
|
60
79
|
const CPU_REL: f32 = 7.105427357601002e-15;
|
|
80
|
+
const LOG_ABS: f32 = 4.76837158203125e-7;
|
|
81
|
+
const Z_ERR: f32 = ${literal(SENSOR_NORMAL_ERROR)};
|
|
61
82
|
struct Params {
|
|
62
83
|
count: u32, row0: u32, region_w: u32, native_w: u32,
|
|
63
84
|
region_x: u32, region_y: u32, seed: u32, components: u32,
|
|
@@ -66,7 +87,7 @@ struct Params {
|
|
|
66
87
|
capacity_frac: f32, threshold_frac: f32, inv_gain: f32, inv_residual: f32,
|
|
67
88
|
digital_gain: f32, digital_residual: f32, inl_slope: f32, inl_offset: f32,
|
|
68
89
|
site_words: u32, poisson_count: u32, extras: u32, pad0: u32,
|
|
69
|
-
fpn_e_slack: f32, fpn_c_slack: f32,
|
|
90
|
+
fpn_e_slack: f32, fpn_c_slack: f32, inl_knot: f32, pad2: f32,
|
|
70
91
|
};
|
|
71
92
|
@group(0) @binding(0) var<uniform> params: Params;
|
|
72
93
|
@group(0) @binding(1) var<storage, read> means: array<u32>;
|
|
@@ -103,10 +124,12 @@ fn next_bits() -> u32 {
|
|
|
103
124
|
state = state ^ (state << 13u); state = state ^ (state >> 17u); state = state ^ (state << 5u);
|
|
104
125
|
return state;
|
|
105
126
|
}
|
|
127
|
+
// (bits + 0.5)/2^32 with two roundings (the conversion and the sum; the power-of-two scale is exact): ≤ 2E.
|
|
106
128
|
fn unit(bits: u32) -> f32 { return (f32(bits) + 0.5) * INV32; }
|
|
107
129
|
fn check(distance: f32, margin: f32) { if (abs(distance) <= margin) { uncertain = true; } }
|
|
108
130
|
|
|
109
|
-
// ln((bits + 0.5)/2^32), relative error
|
|
131
|
+
// ln((bits + 0.5)/2^32), relative error ≤ 11E: WGSL log below 1/2 (≤ 6E), else 2·atanh(r) with r = −w/(2 − w)
|
|
132
|
+
// on the exact 1 − unit (division 2.5E; ≤ 10.5E in all).
|
|
110
133
|
fn ln_unit(bits: u32) -> f32 {
|
|
111
134
|
if (bits < 0x80000000u) { return log(unit(bits)); }
|
|
112
135
|
let w = (f32(0xffffffffu - bits) + 0.5) * INV32;
|
|
@@ -117,7 +140,7 @@ fn ln_unit(bits: u32) -> f32 {
|
|
|
117
140
|
p = p * r2 + 1.0;
|
|
118
141
|
return 2.0 * r * p;
|
|
119
142
|
}
|
|
120
|
-
// cos(2π (bits + 0.5)/2^32): exact octant reduction on the integer, Taylor polynomials on [0, π/4].
|
|
143
|
+
// cos(2π (bits + 0.5)/2^32): exact octant reduction on the integer, Taylor polynomials on [0, π/4]; absolute error ≤ 8E.
|
|
121
144
|
fn cos_unit(bits: u32) -> f32 {
|
|
122
145
|
let octant = bits >> 29u;
|
|
123
146
|
let low = bits & 0x1fffffffu;
|
|
@@ -139,13 +162,16 @@ fn cos_unit(bits: u32) -> f32 {
|
|
|
139
162
|
if (q == 2u) { return -cb; }
|
|
140
163
|
return sb;
|
|
141
164
|
}
|
|
142
|
-
// Box–Muller, one value per two uniforms
|
|
165
|
+
// Box–Muller, one value per two uniforms: radius = sqrt(−2 ln u1) (≤ 10E relative), times cos (≤ 8E absolute),
|
|
166
|
+
// one product rounding: |z_f32 − z_f64| ≤ Z_ERR·E·radius with Z_ERR = 20 (derivation in docs/WEBGPU.md).
|
|
143
167
|
fn std_normal() -> f32 {
|
|
144
168
|
let b1 = next_bits();
|
|
145
169
|
let b2 = next_bits();
|
|
146
170
|
radius = sqrt(-2.0 * ln_unit(b1));
|
|
147
171
|
return radius * cos_unit(b2);
|
|
148
172
|
}
|
|
173
|
+
// Multiplication method: product of uniforms (3E per factor) against exp(−mean), whose relative error is
|
|
174
|
+
// (3 + 2·mean) ULP plus the float32 mean's absolute error E·(mean + 1): ≤ (4 + 3·mean)·E.
|
|
149
175
|
fn poisson_small(mean: f32) -> u32 {
|
|
150
176
|
let threshold = exp(-mean);
|
|
151
177
|
var product = 1.0;
|
|
@@ -153,7 +179,7 @@ fn poisson_small(mean: f32) -> u32 {
|
|
|
153
179
|
loop {
|
|
154
180
|
count = count + 1u;
|
|
155
181
|
product = product * unit(next_bits());
|
|
156
|
-
check(product - threshold, MARGIN * (3.0 * f32(count) +
|
|
182
|
+
check(product - threshold, MARGIN * (3.0 * f32(count) + 4.0 + 3.0 * mean) * E * max(product, threshold));
|
|
157
183
|
if (!(product > threshold)) { break; }
|
|
158
184
|
if (count >= 1000u) { uncertain = true; break; }
|
|
159
185
|
}
|
|
@@ -170,6 +196,7 @@ fn log1p_minus(t: f32) -> f32 {
|
|
|
170
196
|
return -(t * t) / d + 2.0 * s * s2 * p;
|
|
171
197
|
}
|
|
172
198
|
fn poisson_ptrs(mi: u32, mf: f32) -> u32 {
|
|
199
|
+
// mean: ≤ 2.1E relative; sq: ≤ 5.6E; b: ≤ 8.7E; a: ≤ 14E; inv_alpha: ≤ 3.5E; v_r: ≤ 6E absolute (docs/WEBGPU.md).
|
|
173
200
|
let mean = f32(mi) + mf;
|
|
174
201
|
let sq = sqrt(mean);
|
|
175
202
|
let b = 0.931 + 2.53 * sq;
|
|
@@ -186,26 +213,28 @@ fn poisson_ptrs(mi: u32, mf: f32) -> u32 {
|
|
|
186
213
|
let us = unit(m);
|
|
187
214
|
let u = sign * unit(0x7fffffffu - m);
|
|
188
215
|
let v = unit(sv);
|
|
216
|
+
// scale ≤ 19.5E, t = scale·u ≤ 22.5E relative; x = (t + mf) + 0.43 in any order adds 2E·(|t| + 1.43) + 2E.
|
|
189
217
|
let scale = 2.0 * a / us + b;
|
|
190
218
|
let t = scale * u;
|
|
191
219
|
let x = (t + mf) + 0.43;
|
|
192
220
|
let fl = floor(x);
|
|
193
|
-
let ex = MARGIN * (
|
|
221
|
+
let ex = MARGIN * (25.0 * E * abs(t) + 4.0 * E + 4.5e-16 * (abs(t) + mean + 1.0));
|
|
194
222
|
if (x - fl <= ex || fl + 1.0 - x <= ex) { uncertain = true; }
|
|
195
223
|
if (fl <= -1073741824.0 || fl >= 1073741824.0) { continue; }
|
|
196
224
|
let k = i32(mi) + i32(fl);
|
|
197
225
|
if (m >= ${uniformThresholdInteger(0.07)}u) {
|
|
198
|
-
if (k >= 0) { check(v - v_r, MARGIN * (
|
|
226
|
+
if (k >= 0) { check(v - v_r, MARGIN * (7.0 * E + 2.0 * E * v)); }
|
|
199
227
|
if (v <= v_r && k >= 0) { return u32(k); }
|
|
200
228
|
}
|
|
201
229
|
if (k < 0 || (m < ${uniformThresholdInteger(0.013)}u && sv > m)) { continue; }
|
|
230
|
+
// lhs: the argument carries ≤ 31.5E (two divisions), the logarithm 3 ULP or 2^-21.
|
|
202
231
|
let lhs = log(v * inv_alpha / (a / (us * us) + b));
|
|
203
|
-
let dl =
|
|
232
|
+
let dl = 32.0 * E + LOG_ABS + 3.0 * E * abs(lhs);
|
|
204
233
|
var rhs: f32;
|
|
205
234
|
var dr: f32;
|
|
206
235
|
if (k == 0) {
|
|
207
236
|
rhs = -mean;
|
|
208
|
-
dr =
|
|
237
|
+
dr = 3.0 * E * mean + CPU_REL * mean;
|
|
209
238
|
} else {
|
|
210
239
|
let kf = f32(k);
|
|
211
240
|
let dd = f32(i32(mi) - k) + mf;
|
|
@@ -214,17 +243,17 @@ fn poisson_ptrs(mi: u32, mf: f32) -> u32 {
|
|
|
214
243
|
var dk: f32;
|
|
215
244
|
if (abs(t2) <= 0.5) {
|
|
216
245
|
kg = kf * log1p_minus(t2);
|
|
217
|
-
dk =
|
|
246
|
+
dk = 25.0 * E * abs(kg) + E;
|
|
218
247
|
} else {
|
|
219
248
|
let lr = log(mean / kf);
|
|
220
249
|
kg = kf * lr - dd;
|
|
221
|
-
dk = kf * (
|
|
250
|
+
dk = kf * (6.0 * E + LOG_ABS + 5.0 * E * abs(lr)) + 2.0 * E * abs(dd) + E + E * abs(kg);
|
|
222
251
|
}
|
|
223
252
|
let lnk = log(kf);
|
|
224
253
|
var corr: f32;
|
|
225
254
|
if (k < 256) { corr = aux[u32(k)]; } else { corr = 1.0 / (12.0 * kf) - 1.0 / (360.0 * kf * kf * kf); }
|
|
226
255
|
rhs = ((kg - 0.5 * lnk) - 0.91893853320467274) - corr;
|
|
227
|
-
dr = dk +
|
|
256
|
+
dr = dk + 0.5 * LOG_ABS + 1.5 * E * lnk + 2.0 * E + 3.0 * E * (abs(kg) + 0.5 * lnk + 1.0) +
|
|
228
257
|
CPU_REL * (mean + kf * (abs(ln_mean) + lnk + 1.0));
|
|
229
258
|
}
|
|
230
259
|
check(lhs - rhs, MARGIN * (dl + dr));
|
|
@@ -244,20 +273,28 @@ fn poisson(word: u32, frac: f32) -> u32 {
|
|
|
244
273
|
let y = sqrt(f32(mi) + frac) * z + frac + 0.5;
|
|
245
274
|
return u32(max(0, i32(mi) + i32(floor(y))));
|
|
246
275
|
}
|
|
247
|
-
// Adds an exact
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
276
|
+
// Adds product·2^s for an exact integer product < 2^24: the integer part by an integer shift, the fraction as
|
|
277
|
+
// an exact float32 below 1 (the masked low bits times the exact power of two). No float split depends on order.
|
|
278
|
+
fn add_scaled(product: u32, s: i32, scale: f32) {
|
|
279
|
+
if (product == 0u) { return; }
|
|
280
|
+
// product·2^s is below 2^30 by the host's domain check, so the shift count is below 31.
|
|
281
|
+
if (s >= 0) { ip = ip + i32(product << u32(s)); return; }
|
|
282
|
+
if (s <= -24) { fp = fp + f32(product) * scale; return; }
|
|
283
|
+
let shift = u32(-s);
|
|
284
|
+
ip = ip + i32(product >> shift);
|
|
285
|
+
fp = fp + f32(product & ((1u << shift) - 1u)) * scale;
|
|
252
286
|
}
|
|
253
|
-
// n·(p0 + p1 + p2 + p3) for an integer 0 <= n < 2^24 and
|
|
287
|
+
// n·(p0 + p1 + p2 + p3) for an integer 0 <= n < 2^24 and pieces p_j = m_j·2^(s_j) with m_j < 2^12: the
|
|
288
|
+
// products hi·m_j and lo·m_j are exact 32-bit integers.
|
|
254
289
|
fn scaled(n: u32, base: u32) {
|
|
255
|
-
let hi =
|
|
256
|
-
let lo =
|
|
290
|
+
let hi = n >> 12u;
|
|
291
|
+
let lo = n & 0xfffu;
|
|
257
292
|
for (var j = 0u; j < 4u; j = j + 1u) {
|
|
258
|
-
let
|
|
259
|
-
|
|
260
|
-
|
|
293
|
+
let m = u32(aux[base + ${SENSOR_PIECE_WORDS}u * j]);
|
|
294
|
+
let s = i32(aux[base + ${SENSOR_PIECE_WORDS}u * j + 1u]);
|
|
295
|
+
let scale = aux[base + ${SENSOR_PIECE_WORDS}u * j + 2u];
|
|
296
|
+
add_scaled(hi * m, s + 12, scale * 4096.0);
|
|
297
|
+
add_scaled(lo * m, s, scale);
|
|
261
298
|
}
|
|
262
299
|
}
|
|
263
300
|
fn normalize() {
|
|
@@ -265,9 +302,10 @@ fn normalize() {
|
|
|
265
302
|
ip = ip + i32(f);
|
|
266
303
|
fp = fp - f;
|
|
267
304
|
}
|
|
305
|
+
// Piecewise-linear INL offset with host-computed segment slopes (no division): ao + (code − ac)·slope.
|
|
268
306
|
fn inl_offset(code: f32) -> f32 {
|
|
269
307
|
let n = params.knots;
|
|
270
|
-
let base = ${
|
|
308
|
+
let base = ${SENSOR_AUX.knots}u;
|
|
271
309
|
if (code <= aux[base]) { return aux[base + n]; }
|
|
272
310
|
if (code >= aux[base + n - 1u]) { return aux[base + 2u * n - 1u]; }
|
|
273
311
|
var low = 0u;
|
|
@@ -277,9 +315,7 @@ fn inl_offset(code: f32) -> f32 {
|
|
|
277
315
|
let middle = (low + high) >> 1u;
|
|
278
316
|
if (aux[base + middle] <= code) { low = middle; } else { high = middle; }
|
|
279
317
|
}
|
|
280
|
-
|
|
281
|
-
let ao = aux[base + n + low];
|
|
282
|
-
return ao + ((code - ac) / (aux[base + high] - ac)) * (aux[base + n + high] - ao);
|
|
318
|
+
return aux[base + n + low] + (code - aux[base + low]) * aux[base + 2u * n + low];
|
|
283
319
|
}
|
|
284
320
|
@compute @workgroup_size(${SENSOR_WORKGROUP_SIZE})
|
|
285
321
|
fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) groups: vec3<u32>) {
|
|
@@ -305,7 +341,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
|
|
|
305
341
|
if (deterministic) {
|
|
306
342
|
realized = min(realized + means[cursor], ${SENSOR_REALIZED_CLAMP}u);
|
|
307
343
|
r_frac = bitcast<f32>(means[cursor + 1u]);
|
|
308
|
-
det_slack =
|
|
344
|
+
det_slack = E;
|
|
309
345
|
cursor = cursor + 2u;
|
|
310
346
|
}
|
|
311
347
|
var t_floor = i32(params.threshold_floor);
|
|
@@ -324,7 +360,8 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
|
|
|
324
360
|
if (sigma != 0.0) {
|
|
325
361
|
let z = std_normal();
|
|
326
362
|
noise = noise + z * sigma;
|
|
327
|
-
|
|
363
|
+
// z's error, sigma's rounding, the product, and the order-independent sum over the component count.
|
|
364
|
+
noise_bound = noise_bound + sigma * radius * (Z_ERR + 2.0 + f32(params.components)) * E;
|
|
328
365
|
}
|
|
329
366
|
}
|
|
330
367
|
var flags = 0u;
|
|
@@ -333,16 +370,17 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
|
|
|
333
370
|
if (!deterministic) {
|
|
334
371
|
if (realized > params.capacity_floor) { flags = 1u; a_int = params.capacity_floor; a_frac = params.capacity_frac; }
|
|
335
372
|
} else {
|
|
336
|
-
// realized > capacity with a non-integer total: float32
|
|
373
|
+
// realized > capacity with a non-integer total: a three-term float32 sum (any order) with its bound.
|
|
337
374
|
let w = f32(i32(realized) - i32(params.capacity_floor));
|
|
338
375
|
let dcap = w + (r_frac - params.capacity_frac);
|
|
339
|
-
check(dcap, MARGIN * (
|
|
376
|
+
check(dcap, MARGIN * (3.0 * E * abs(w) + 6.0 * E + CPU_REL * (f32(realized) + 1.0)));
|
|
340
377
|
if (dcap > 0.0) { flags = 1u; a_int = params.capacity_floor; a_frac = params.capacity_frac; }
|
|
341
378
|
}
|
|
342
|
-
// Pre-ADC limit: afterPhysical + noise + offset > threshold, as afterPhysical − (threshold − offset) + noise > 0
|
|
379
|
+
// Pre-ADC limit: afterPhysical + noise + offset > threshold, as afterPhysical − (threshold − offset) + noise > 0,
|
|
380
|
+
// a four-term float32 sum (any order).
|
|
343
381
|
let whole = f32(i32(a_int) - t_floor);
|
|
344
382
|
let diff = (whole + (a_frac - t_frac)) + noise;
|
|
345
|
-
check(diff, MARGIN * (noise_bound +
|
|
383
|
+
check(diff, MARGIN * (noise_bound + 4.0 * E * abs(whole) + 3.0 * E * abs(noise) + 8.0 * E + CPU_REL * (f32(a_int) + abs(noise)) +
|
|
346
384
|
fpn_e_slack + det_slack));
|
|
347
385
|
var q: i32;
|
|
348
386
|
if (diff > 0.0) {
|
|
@@ -358,20 +396,24 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
|
|
|
358
396
|
fp = bitcast<f32>(means[cursor + 3u]);
|
|
359
397
|
fpn_c_slack = params.fpn_c_slack;
|
|
360
398
|
}
|
|
361
|
-
scaled(a_int,
|
|
399
|
+
scaled(a_int, ${SENSOR_AUX.inversePieces}u);
|
|
362
400
|
let noise_codes = noise * params.inv_gain;
|
|
363
|
-
|
|
401
|
+
let frac_codes = a_frac * params.inv_gain;
|
|
402
|
+
fp = fp + frac_codes + noise_codes;
|
|
364
403
|
let a_codes = f32(a_int) * params.inv_gain;
|
|
365
|
-
|
|
366
|
-
|
|
404
|
+
// The fraction sum (eleven terms below 1, frac_codes and noise_codes), the two normalizations and the
|
|
405
|
+
// half-code comparison in any association: 13E·Σ|terms|; then the term errors, the pieces' residual,
|
|
406
|
+
// the reference's float64 rounding and the packed fractions' rounding.
|
|
407
|
+
var margin = MARGIN * (13.0 * E * (13.0 + frac_codes + abs(noise_codes)) + 3.0 * E * frac_codes + 2.0 * E * abs(noise_codes) +
|
|
408
|
+
noise_bound * params.inv_gain + f32(a_int) * params.inv_residual +
|
|
367
409
|
CPU_REL * (a_codes + abs(noise_codes) + f32(params.black)) + fpn_c_slack + det_slack * params.inv_gain);
|
|
368
410
|
normalize();
|
|
369
411
|
if (params.knots > 0u) {
|
|
370
412
|
let ideal = f32(ip) + fp;
|
|
371
413
|
let offset = inl_offset(ideal);
|
|
372
414
|
fp = fp + offset;
|
|
373
|
-
margin = margin * (1.0 + params.inl_slope) + MARGIN * (params.inl_slope * 2.0 *
|
|
374
|
-
|
|
415
|
+
margin = margin * (1.0 + params.inl_slope) + MARGIN * (params.inl_slope * 2.0 * E * (abs(ideal) + 1.0 + params.inl_knot) +
|
|
416
|
+
11.0 * E * (params.inl_offset + 1.0) + CPU_REL * (abs(ideal) + params.inl_offset) * (1.0 + params.inl_slope));
|
|
375
417
|
normalize();
|
|
376
418
|
}
|
|
377
419
|
if ((params.digital & 4u) == 0u) { check(fp - 0.5, margin); }
|
|
@@ -388,8 +430,10 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
|
|
|
388
430
|
} else {
|
|
389
431
|
ip = 0;
|
|
390
432
|
fp = 0.0;
|
|
391
|
-
scaled(u32(abs(delta)),
|
|
392
|
-
|
|
433
|
+
scaled(u32(abs(delta)), ${SENSOR_AUX.digitalPieces}u);
|
|
434
|
+
// Eight exact fractions summed in any order, the floor split and the half comparison: ≤ 10 roundings on
|
|
435
|
+
// values below 9; the pieces' residual; the reference's float64 product.
|
|
436
|
+
if ((params.digital & 2u) == 0u) { check(fp - floor(fp) - 0.5, MARGIN * (90.0 * E + f32(abs(delta)) * params.digital_residual + CPU_REL * (abs(estimate) + f32(params.black)))); }
|
|
393
437
|
normalize();
|
|
394
438
|
var whole_part = ip;
|
|
395
439
|
var fraction = fp;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wgsl-sensor-kernel.js","sourceRoot":"","sources":["../../src/gpu/wgsl-sensor-kernel.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * WGSL kernel for the `webgpu-f32` sensor stage (#277), one invocation per native site. It evaluates the\n * reference `simulatePackedSensorSite` (packed-sensor-raw.ts) with the same seeded noise model\n * (`photivra-native-raw-noise` 0.4.0):\n * - the MurmurHash3_x86_32 site seeds, fmix32 scrambling and xorshift32 streams are 32-bit integer\n * arithmetic, so every random state is bit-identical to the reference;\n * - every comparison against a uniform value is made exactly on the integer state where the reference\n * value is an exact dyadic rational;\n * - every other decision (Poisson multiplication-method threshold, PTRS floor and acceptance tests,\n * physical and pre-ADC limits, ADC and digital-gain rounding) is evaluated in float32 together with an\n * a-priori bound on how far the float32 value can be from the float64 reference value. A site where any\n * decision falls inside its bound is flagged. An unflagged site therefore makes every decision exactly\n * as the reference does, and its code and flags equal the reference.\n * - Fixed-pattern noise and additional stored-charge components (2.0): the host evaluates the\n * fixed-pattern values in float64 with the model's own functions and packs, per site, the Poisson means\n * already multiplied by the PRNU and dark factors (exactly the reference's products), the pre-ADC limit\n * minus the readout offset and the offset in codes. Additional Poisson components are drawn from the\n * charge stream after photo and dark charge, in declaration order; deterministic components enter as an\n * exact integer part plus a float32 fraction, with their own bounds.\n *\n * The bounds use u = 2^-24 and the WGSL accuracy table (W3C WGSL, \"Floating Point Accuracy\"): correctly\n * rounded + − ×, division 2.5 ULP, sqrt inherited from 1/inverseSqrt (≤ 4.5 ULP), exp 3 + 2|x| ULP,\n * log 3 ULP or 2^-21 absolute on [0.5, 2]. WGSL's built-in cos has only a 2^-11 absolute bound, so\n * the kernel uses its own range-reduced Taylor polynomials. Every margin is doubled (`MARGIN`).\n * See docs/WEBGPU.md.\n */\n\n/** Output word: code in bits 0–15, saturation flags in bits 16–19, the flagged (uncertain) bit 20. */\nexport const SENSOR_OUTPUT = Object.freeze({ codeMask: 0xffff, flagShift: 16, uncertainBit: 1 << 20 });\n/** Auxiliary table layout (float32 words). */\nexport const SENSOR_AUX = Object.freeze({ stirlingRemainder: 0, sigma: 256, inversePieces: 272, digitalPieces: 276, knots: 280 });\nexport const SENSOR_WORKGROUP_SIZE = 256;\n/**\n * Per-site packed words: photo word, photo fraction bits, dark word, dark fraction bits; then 2 words per\n * additional Poisson component; then, with deterministic components, their sum (integer part, fraction\n * bits); then, with fixed-pattern noise, (threshold − offset) and offset/gain as (floor, fraction bits).\n */\nexport const SENSOR_SITE_WORDS = 4;\n/** Bits of `Params.extras`. */\nexport const SENSOR_EXTRAS = Object.freeze({ deterministic: 1, fixedPattern: 2 });\n/** Partial charge sums saturate here: above every supported capacity (2^24), so no decision changes. */\nexport const SENSOR_REALIZED_CLAMP = 2 ** 30;\n/** Mean categories in bits 30–31 of a mean word, mirroring `samplePoisson`'s branches. */\nexport const SENSOR_MEAN_CATEGORY = Object.freeze({ zero: 0, multiplication: 1, transformedRejection: 2, normal: 3 });\n\n/** Smallest integer m with (m + 0.5)/2^32 ≥ t, so `uS ≥ t` ⇔ `m ≥ result` exactly. */\nexport function uniformThresholdInteger(t: number): number {\n return Math.ceil(t * 2 ** 32 - 0.5);\n}\n\nconst SIN_COEFFICIENTS = [-1 / 6, 1 / 120, -1 / 5040, 1 / 362880, -1 / 39916800];\nconst COS_COEFFICIENTS = [-1 / 2, 1 / 24, -1 / 720, 1 / 40320, -1 / 3628800, 1 / 479001600];\nconst literal = (value: number): string => {\n const text = String(Math.fround(value));\n return /[.e]/u.test(text) ? text : `${text}.0`;\n};\n\nexport const SENSOR_RAW_WGSL = /* wgsl */ `\nconst U: f32 = 5.9604644775390625e-8;\nconst INV32: f32 = 2.3283064365386963e-10;\nconst TWO_PI_INV32: f32 = ${literal(2 * Math.PI / 2 ** 32)};\nconst MARGIN: f32 = 2.0;\nconst CPU_REL: f32 = 7.105427357601002e-15;\nstruct Params {\n count: u32, row0: u32, region_w: u32, native_w: u32,\n region_x: u32, region_y: u32, seed: u32, components: u32,\n knots: u32, digital: u32, saturation: u32, adc_max: u32,\n capacity_floor: u32, threshold_floor: u32, black: u32, sat_quantized: i32,\n capacity_frac: f32, threshold_frac: f32, inv_gain: f32, inv_residual: f32,\n digital_gain: f32, digital_residual: f32, inl_slope: f32, inl_offset: f32,\n site_words: u32, poisson_count: u32, extras: u32, pad0: u32,\n fpn_e_slack: f32, fpn_c_slack: f32, pad1: f32, pad2: f32,\n};\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> means: array<u32>;\n@group(0) @binding(2) var<storage, read> aux: array<f32>;\n@group(0) @binding(3) var<storage, read_write> out: array<u32>;\n\nvar<private> state: u32;\nvar<private> uncertain: bool;\nvar<private> radius: f32;\nvar<private> ip: i32;\nvar<private> fp: f32;\n\nfn fmix(value: u32) -> u32 {\n var h = value;\n h = h ^ (h >> 16u); h = h * 0x85ebca6bu; h = h ^ (h >> 13u); h = h * 0xc2b2ae35u; h = h ^ (h >> 16u);\n return h;\n}\nfn mix_block(hash: u32, block: u32) -> u32 {\n var k = block * 0xcc9e2d51u;\n k = (k << 15u) | (k >> 17u);\n k = k * 0x1b873593u;\n var h = hash ^ k;\n h = (h << 13u) | (h >> 19u);\n return h * 5u + 0xe6546b64u;\n}\nfn site_seed(seed: u32, index: u32, stream: u32) -> u32 {\n return fmix(mix_block(mix_block(mix_block(0u, seed), index), stream) ^ 12u);\n}\nfn start(seed: u32) {\n state = fmix(seed ^ 0x9e3779b9u);\n if (state == 0u) { state = 0x6d2b79f5u; }\n}\nfn next_bits() -> u32 {\n state = state ^ (state << 13u); state = state ^ (state >> 17u); state = state ^ (state << 5u);\n return state;\n}\nfn unit(bits: u32) -> f32 { return (f32(bits) + 0.5) * INV32; }\nfn check(distance: f32, margin: f32) { if (abs(distance) <= margin) { uncertain = true; } }\n\n// ln((bits + 0.5)/2^32), relative error <= 12u: WGSL log below 1/2, else 2·atanh(r) on the exact 1 - u.\nfn ln_unit(bits: u32) -> f32 {\n if (bits < 0x80000000u) { return log(unit(bits)); }\n let w = (f32(0xffffffffu - bits) + 0.5) * INV32;\n let r = -w / (2.0 - w);\n let r2 = r * r;\n var p = ${literal(1 / 23)};\n ${[21, 19, 17, 15, 13, 11, 9, 7, 5, 3].map(n => `p = p * r2 + ${literal(1 / n)};`).join(\" \")}\n p = p * r2 + 1.0;\n return 2.0 * r * p;\n}\n// cos(2π (bits + 0.5)/2^32): exact octant reduction on the integer, Taylor polynomials on [0, π/4].\nfn cos_unit(bits: u32) -> f32 {\n let octant = bits >> 29u;\n let low = bits & 0x1fffffffu;\n let even = (octant & 1u) == 0u;\n var g = (f32(0x1fffffffu - low) + 0.5) * TWO_PI_INV32;\n if (even) { g = (f32(low) + 0.5) * TWO_PI_INV32; }\n let g2 = g * g;\n var c = ${literal(COS_COEFFICIENTS[5]!)};\n ${[4, 3, 2, 1, 0].map(j => `c = c * g2 + ${literal(COS_COEFFICIENTS[j]!)};`).join(\" \")}\n c = c * g2 + 1.0;\n var s = ${literal(SIN_COEFFICIENTS[4]!)};\n ${[3, 2, 1, 0].map(j => `s = s * g2 + ${literal(SIN_COEFFICIENTS[j]!)};`).join(\" \")}\n s = g * (s * g2 + 1.0);\n let cb = select(s, c, even);\n let sb = select(c, s, even);\n let q = octant >> 1u;\n if (q == 0u) { return cb; }\n if (q == 1u) { return -sb; }\n if (q == 2u) { return -cb; }\n return sb;\n}\n// Box–Muller, one value per two uniforms; |z_f32 − z_f64| <= 24u·radius.\nfn std_normal() -> f32 {\n let b1 = next_bits();\n let b2 = next_bits();\n radius = sqrt(-2.0 * ln_unit(b1));\n return radius * cos_unit(b2);\n}\nfn poisson_small(mean: f32) -> u32 {\n let threshold = exp(-mean);\n var product = 1.0;\n var count = 0u;\n loop {\n count = count + 1u;\n product = product * unit(next_bits());\n check(product - threshold, MARGIN * (3.0 * f32(count) + 130.0) * U * max(product, threshold));\n if (!(product > threshold)) { break; }\n if (count >= 1000u) { uncertain = true; break; }\n }\n return count - 1u;\n}\n// g(t) = ln(1 + t) − t for |t| <= 1/2 without cancellation: −t²/(2+t) + 2(s³/3 + s⁵/5 + …), s = t/(2+t).\nfn log1p_minus(t: f32) -> f32 {\n let d = 2.0 + t;\n let s = t / d;\n let s2 = s * s;\n var p = ${literal(1 / 25)};\n ${[23, 21, 19, 17, 15, 13, 11, 9, 7, 5].map(n => `p = p * s2 + ${literal(1 / n)};`).join(\" \")}\n p = p * s2 + ${literal(1 / 3)};\n return -(t * t) / d + 2.0 * s * s2 * p;\n}\nfn poisson_ptrs(mi: u32, mf: f32) -> u32 {\n let mean = f32(mi) + mf;\n let sq = sqrt(mean);\n let b = 0.931 + 2.53 * sq;\n let a = -0.059 + 0.02483 * b;\n let inv_alpha = 1.1239 + 1.1328 / (b - 3.4);\n let v_r = 0.9277 - 3.6224 / (b - 2.0);\n let ln_mean = log(mean);\n for (var iteration = 0u; iteration < 64u; iteration = iteration + 1u) {\n let su = next_bits();\n let sv = next_bits();\n var m = su;\n var sign = -1.0;\n if (su >= 0x80000000u) { m = 0xffffffffu - su; sign = 1.0; }\n let us = unit(m);\n let u = sign * unit(0x7fffffffu - m);\n let v = unit(sv);\n let scale = 2.0 * a / us + b;\n let t = scale * u;\n let x = (t + mf) + 0.43;\n let fl = floor(x);\n let ex = MARGIN * (abs(t) * 40.0 * U + 2.0 * U * (abs(t) + 1.0) + 4.5e-16 * (abs(t) + mean + 1.0));\n if (x - fl <= ex || fl + 1.0 - x <= ex) { uncertain = true; }\n if (fl <= -1073741824.0 || fl >= 1073741824.0) { continue; }\n let k = i32(mi) + i32(fl);\n if (m >= ${uniformThresholdInteger(0.07)}u) {\n if (k >= 0) { check(v - v_r, MARGIN * (16.0 * U + 2.0 * U * v)); }\n if (v <= v_r && k >= 0) { return u32(k); }\n }\n if (k < 0 || (m < ${uniformThresholdInteger(0.013)}u && sv > m)) { continue; }\n let lhs = log(v * inv_alpha / (a / (us * us) + b));\n let dl = 60.0 * U + 4.76837158203125e-7 + 4.0 * U * abs(lhs);\n var rhs: f32;\n var dr: f32;\n if (k == 0) {\n rhs = -mean;\n dr = 2.0 * U * mean + CPU_REL * mean;\n } else {\n let kf = f32(k);\n let dd = f32(i32(mi) - k) + mf;\n let t2 = dd / kf;\n var kg: f32;\n var dk: f32;\n if (abs(t2) <= 0.5) {\n kg = kf * log1p_minus(t2);\n dk = 20.0 * U * abs(kg);\n } else {\n let lr = log(mean / kf);\n kg = kf * lr - dd;\n dk = kf * (8.0 * U + 4.76837158203125e-7 + 5.0 * U * abs(lr)) + 3.0 * U * abs(dd) + U * abs(kg);\n }\n let lnk = log(kf);\n var corr: f32;\n if (k < 256) { corr = aux[u32(k)]; } else { corr = 1.0 / (12.0 * kf) - 1.0 / (360.0 * kf * kf * kf); }\n rhs = ((kg - 0.5 * lnk) - 0.91893853320467274) - corr;\n dr = dk + 2.384185791015625e-7 + 2.0 * U * lnk + U + 0.6 * U + 3.0 * U * (abs(kg) + 0.5 * lnk + 1.0) +\n CPU_REL * (mean + kf * (abs(ln_mean) + lnk + 1.0));\n }\n check(lhs - rhs, MARGIN * (dl + dr));\n if (lhs <= rhs) { return u32(k); }\n }\n uncertain = true;\n return mi;\n}\nfn poisson(word: u32, frac: f32) -> u32 {\n let category = word >> 30u;\n let mi = word & 0x3fffffffu;\n if (category == 0u) { return 0u; }\n if (category == 1u) { return poisson_small(f32(mi) + frac); }\n if (category == 2u) { return poisson_ptrs(mi, frac); }\n // Means of 1e9 and above: the draw always exceeds the capacity (<= 2^24), so the code does not depend on it.\n let z = std_normal();\n let y = sqrt(f32(mi) + frac) * z + frac + 0.5;\n return u32(max(0, i32(mi) + i32(floor(y))));\n}\n// Adds an exact non-negative float32 value split into integer and fractional parts.\nfn accumulate(value: f32) {\n let f = floor(value);\n ip = ip + i32(f);\n fp = fp + (value - f);\n}\n// n·(p0 + p1 + p2 + p3) for an integer 0 <= n < 2^24 and 12-bit pieces: every product is exact.\nfn scaled(n: u32, base: u32) {\n let hi = f32(n >> 12u);\n let lo = f32(n & 0xfffu);\n for (var j = 0u; j < 4u; j = j + 1u) {\n let piece = aux[base + j];\n accumulate(hi * piece * 4096.0);\n accumulate(lo * piece);\n }\n}\nfn normalize() {\n let f = floor(fp);\n ip = ip + i32(f);\n fp = fp - f;\n}\nfn inl_offset(code: f32) -> f32 {\n let n = params.knots;\n let base = ${\"280u\"};\n if (code <= aux[base]) { return aux[base + n]; }\n if (code >= aux[base + n - 1u]) { return aux[base + 2u * n - 1u]; }\n var low = 0u;\n var high = n - 1u;\n loop {\n if (high - low <= 1u) { break; }\n let middle = (low + high) >> 1u;\n if (aux[base + middle] <= code) { low = middle; } else { high = middle; }\n }\n let ac = aux[base + low];\n let ao = aux[base + n + low];\n return ao + ((code - ac) / (aux[base + high] - ac)) * (aux[base + n + high] - ao);\n}\n@compute @workgroup_size(${SENSOR_WORKGROUP_SIZE})\nfn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) groups: vec3<u32>) {\n let i = gid.x + gid.y * groups.x * ${SENSOR_WORKGROUP_SIZE}u;\n if (i >= params.count) { return; }\n let row = params.row0 + i / params.region_w;\n let index = (params.region_y + row) * params.native_w + params.region_x + i % params.region_w;\n uncertain = false;\n let base = i * params.site_words;\n start(site_seed(params.seed, index, 0u));\n let photo = poisson(means[base], bitcast<f32>(means[base + 1u]));\n let dark = poisson(means[base + 2u], bitcast<f32>(means[base + 3u]));\n // Saturating sums: any total above the clamp is above every supported capacity, as in the reference.\n var realized = min(photo + dark, ${SENSOR_REALIZED_CLAMP}u);\n for (var k = 0u; k < params.poisson_count; k = k + 1u) {\n realized = min(realized + poisson(means[base + 4u + 2u * k], bitcast<f32>(means[base + 5u + 2u * k])), ${SENSOR_REALIZED_CLAMP}u);\n }\n var cursor = base + 4u + 2u * params.poisson_count;\n let deterministic = (params.extras & ${SENSOR_EXTRAS.deterministic}u) != 0u;\n let pattern = (params.extras & ${SENSOR_EXTRAS.fixedPattern}u) != 0u;\n var r_frac = 0.0;\n var det_slack = 0.0;\n if (deterministic) {\n realized = min(realized + means[cursor], ${SENSOR_REALIZED_CLAMP}u);\n r_frac = bitcast<f32>(means[cursor + 1u]);\n det_slack = U;\n cursor = cursor + 2u;\n }\n var t_floor = i32(params.threshold_floor);\n var t_frac = params.threshold_frac;\n var fpn_e_slack = 0.0;\n if (pattern) {\n t_floor = bitcast<i32>(means[cursor]);\n t_frac = bitcast<f32>(means[cursor + 1u]);\n fpn_e_slack = params.fpn_e_slack;\n }\n start(site_seed(params.seed, index, 1u));\n var noise = 0.0;\n var noise_bound = 0.0;\n for (var c = 0u; c < params.components; c = c + 1u) {\n let sigma = aux[256u + c];\n if (sigma != 0.0) {\n let z = std_normal();\n noise = noise + z * sigma;\n noise_bound = noise_bound + sigma * radius * (28.0 + f32(params.components)) * U;\n }\n }\n var flags = 0u;\n var a_int = realized;\n var a_frac = r_frac;\n if (!deterministic) {\n if (realized > params.capacity_floor) { flags = 1u; a_int = params.capacity_floor; a_frac = params.capacity_frac; }\n } else {\n // realized > capacity with a non-integer total: float32 difference with its bound.\n let w = f32(i32(realized) - i32(params.capacity_floor));\n let dcap = w + (r_frac - params.capacity_frac);\n check(dcap, MARGIN * (5.0 * U + U * abs(w) + U * abs(dcap) + CPU_REL * (f32(realized) + 1.0)));\n if (dcap > 0.0) { flags = 1u; a_int = params.capacity_floor; a_frac = params.capacity_frac; }\n }\n // Pre-ADC limit: afterPhysical + noise + offset > threshold, as afterPhysical − (threshold − offset) + noise > 0.\n let whole = f32(i32(a_int) - t_floor);\n let diff = (whole + (a_frac - t_frac)) + noise;\n check(diff, MARGIN * (noise_bound + 3.0 * U + 2.0 * U * (abs(whole) + abs(noise) + 2.0) + CPU_REL * (f32(a_int) + abs(noise)) +\n fpn_e_slack + det_slack));\n var q: i32;\n if (diff > 0.0) {\n flags = flags | 2u;\n q = params.sat_quantized;\n } else {\n // ideal = (afterPhysical + noise + offset)/gain + black, as an exact integer part plus a float32 fraction.\n ip = i32(params.black);\n fp = 0.0;\n var fpn_c_slack = 0.0;\n if (pattern) {\n ip = ip + bitcast<i32>(means[cursor + 2u]);\n fp = bitcast<f32>(means[cursor + 3u]);\n fpn_c_slack = params.fpn_c_slack;\n }\n scaled(a_int, 272u);\n let noise_codes = noise * params.inv_gain;\n fp = fp + a_frac * params.inv_gain + noise_codes;\n let a_codes = f32(a_int) * params.inv_gain;\n var margin = MARGIN * (10.0 * U * (10.0 + abs(noise_codes)) + 4.0 * U * a_frac * params.inv_gain + 3.0 * U * abs(noise_codes) +\n noise_bound * params.inv_gain + f32(a_int) * params.inv_residual + 2.0 * U +\n CPU_REL * (a_codes + abs(noise_codes) + f32(params.black)) + fpn_c_slack + det_slack * params.inv_gain);\n normalize();\n if (params.knots > 0u) {\n let ideal = f32(ip) + fp;\n let offset = inl_offset(ideal);\n fp = fp + offset;\n margin = margin * (1.0 + params.inl_slope) + MARGIN * (params.inl_slope * 2.0 * U * (abs(ideal) + 1.0) +\n 8.0 * U * (params.inl_offset + 1.0) + CPU_REL * (abs(ideal) + params.inl_offset) * (1.0 + params.inl_slope));\n normalize();\n }\n if ((params.digital & 4u) == 0u) { check(fp - 0.5, margin); }\n q = ip + select(0, 1, fp >= 0.5);\n }\n if ((params.digital & 1u) != 0u) {\n let adc = clamp(q, 0, i32(params.adc_max));\n let delta = adc - i32(params.black);\n let estimate = f32(delta) * params.digital_gain;\n if (estimate >= 16777216.0) {\n q = 16777216;\n } else if (estimate <= -16777216.0) {\n q = -16777216;\n } else {\n ip = 0;\n fp = 0.0;\n scaled(u32(abs(delta)), 276u);\n if ((params.digital & 2u) == 0u) { check(fp - floor(fp) - 0.5, MARGIN * (40.0 * U * 9.0 + f32(abs(delta)) * params.digital_residual + CPU_REL * (abs(estimate) + f32(params.black)))); }\n normalize();\n var whole_part = ip;\n var fraction = fp;\n if (delta < 0) {\n whole_part = -ip;\n if (fp > 0.0) { whole_part = -ip - 1; fraction = 1.0 - fp; }\n }\n q = whole_part + i32(params.black) + select(0, 1, fraction >= 0.5);\n }\n }\n if (q >= i32(params.saturation)) { flags = flags | 4u; }\n if (q < 0) { flags = flags | 8u; }\n let code = u32(clamp(q, 0, i32(params.saturation)));\n out[i] = code | (flags << 16u) | select(0u, ${1 << 20}u, uncertain);\n}\n`;\n"]}
|
|
1
|
+
{"version":3,"file":"wgsl-sensor-kernel.js","sourceRoot":"","sources":["../../src/gpu/wgsl-sensor-kernel.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * WGSL kernel for the `webgpu-f32` sensor stage (#277, #471), one invocation per native site. It evaluates the\n * reference `simulatePackedSensorSite` (packed-sensor-raw.ts) with the same seeded noise model\n * (`photivra-native-raw-noise` 0.4.0):\n * - the MurmurHash3_x86_32 site seeds, fmix32 scrambling and xorshift32 streams are 32-bit integer\n * arithmetic, so every random state is bit-identical to the reference;\n * - every comparison against a uniform value is made exactly on the integer state where the reference\n * value is an exact dyadic rational;\n * - every other decision (Poisson multiplication-method threshold, PTRS floor and acceptance tests,\n * physical and pre-ADC limits, ADC and digital-gain rounding) is evaluated in float32 together with an\n * a-priori bound on how far the float32 value can be from the float64 reference value. A site where any\n * decision falls inside its bound is flagged. An unflagged site therefore makes every decision exactly\n * as the reference does, and its code and flags equal the reference.\n * - Fixed-pattern noise and additional stored-charge components (2.0): the host evaluates the\n * fixed-pattern values in float64 with the model's own functions and packs, per site, the Poisson means\n * already multiplied by the PRNU and dark factors (exactly the reference's products), the pre-ADC limit\n * minus the readout offset and the offset in codes. Additional Poisson components are drawn from the\n * charge stream after photo and dark charge, in declaration order; deterministic components enter as an\n * exact integer part plus a float32 fraction, with their own bounds.\n *\n * Arithmetic assumptions (W3C WGSL, \"Floating Point Accuracy\" and \"Reassociation and Fusion\"; every margin\n * is derived in docs/WEBGPU.md, \"Sensor-stage margins\"):\n * - E = 2^-23. WGSL specifies no rounding direction, so a \"correctly rounded\" operation (+ − ×, floor, f32(u32))\n * is within one ULP of the exact value, that is ≤ E relative; `/` is 2.5 ULP (≤ 2.5E); `sqrt` inherits\n * 1.0 / inverseSqrt(x) (≤ 4.5E); `exp` is (3 + 2|x|) ULP; `log` is 3 ULP outside [0.5, 2] and 2^-21\n * absolute inside. WGSL's `cos` has only a 2^-11 absolute bound, so the kernel uses its own\n * octant-reduced polynomials.\n * - No decision depends on correctly rounded division: 1/gain and the digital gain arrive from the host as\n * exact 12-bit pieces, INL segments as host-computed slopes, and every quotient that remains (Box–Muller\n * logarithm, PTRS) carries the 2.5 ULP bound.\n * - WGSL lets an implementation reassociate. The integer parts of the ADC input are therefore formed in\n * integer arithmetic, and every floating-point sum carries the order-independent bound\n * (n − 1)·E·Σ|terms| (Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., §4.2), so the\n * sums' margins hold under any association. Fusing a multiply and an add only removes a rounding.\n * - Every margin is then doubled (`MARGIN`), which covers the second-order terms dropped in the derivations.\n */\n\n/** Output word: code in bits 0–15, saturation flags in bits 16–19, the flagged (uncertain) bit 20. */\nexport const SENSOR_OUTPUT = Object.freeze({ codeMask: 0xffff, flagShift: 16, uncertainBit: 1 << 20 });\n/**\n * Auxiliary table layout (float32 words). A gain's four 12-bit pieces are stored as `SENSOR_PIECE_WORDS` words\n * each: the integer mantissa m (< 2^12), the exponent s and the exact scale 2^s, so that piece = m·2^s. The INL\n * table holds the knot codes, the knot offsets and the host-computed segment slopes.\n */\nexport const SENSOR_AUX = Object.freeze({ stirlingRemainder: 0, sigma: 256, inversePieces: 272, digitalPieces: 284, knots: 296 });\nexport const SENSOR_PIECE_WORDS = 3;\nexport const SENSOR_WORKGROUP_SIZE = 256;\n/**\n * Per-site packed words: photo word, photo fraction bits, dark word, dark fraction bits; then 2 words per\n * additional Poisson component; then, with deterministic components, their sum (integer part, fraction\n * bits); then, with fixed-pattern noise, (threshold − offset) and offset/gain as (floor, fraction bits).\n */\nexport const SENSOR_SITE_WORDS = 4;\n/** Bits of `Params.extras`. */\nexport const SENSOR_EXTRAS = Object.freeze({ deterministic: 1, fixedPattern: 2 });\n/** Partial charge sums saturate here: above every supported capacity (2^24), so no decision changes. */\nexport const SENSOR_REALIZED_CLAMP = 2 ** 30;\n/** Mean categories in bits 30–31 of a mean word, mirroring `samplePoisson`'s branches. */\nexport const SENSOR_MEAN_CATEGORY = Object.freeze({ zero: 0, multiplication: 1, transformedRejection: 2, normal: 3 });\n/** One WGSL rounding (one ULP, either direction) is at most this relative error: 2^-23. */\nexport const SENSOR_ROUNDING = 2 ** -23;\n/** |z_f32 − z_f64| ≤ SENSOR_NORMAL_ERROR·E·radius for the Box–Muller normal (docs/WEBGPU.md). */\nexport const SENSOR_NORMAL_ERROR = 20;\n\n/** Smallest integer m with (m + 0.5)/2^32 ≥ t, so `uS ≥ t` ⇔ `m ≥ result` exactly. */\nexport function uniformThresholdInteger(t: number): number {\n return Math.ceil(t * 2 ** 32 - 0.5);\n}\n\nconst SIN_COEFFICIENTS = [-1 / 6, 1 / 120, -1 / 5040, 1 / 362880, -1 / 39916800];\nconst COS_COEFFICIENTS = [-1 / 2, 1 / 24, -1 / 720, 1 / 40320, -1 / 3628800, 1 / 479001600];\nconst literal = (value: number): string => {\n const text = String(Math.fround(value));\n return /[.e]/u.test(text) ? text : `${text}.0`;\n};\n\nexport const SENSOR_RAW_WGSL = /* wgsl */ `\nconst E: f32 = 1.1920928955078125e-7;\nconst INV32: f32 = 2.3283064365386963e-10;\nconst TWO_PI_INV32: f32 = ${literal(2 * Math.PI / 2 ** 32)};\nconst MARGIN: f32 = 2.0;\nconst CPU_REL: f32 = 7.105427357601002e-15;\nconst LOG_ABS: f32 = 4.76837158203125e-7;\nconst Z_ERR: f32 = ${literal(SENSOR_NORMAL_ERROR)};\nstruct Params {\n count: u32, row0: u32, region_w: u32, native_w: u32,\n region_x: u32, region_y: u32, seed: u32, components: u32,\n knots: u32, digital: u32, saturation: u32, adc_max: u32,\n capacity_floor: u32, threshold_floor: u32, black: u32, sat_quantized: i32,\n capacity_frac: f32, threshold_frac: f32, inv_gain: f32, inv_residual: f32,\n digital_gain: f32, digital_residual: f32, inl_slope: f32, inl_offset: f32,\n site_words: u32, poisson_count: u32, extras: u32, pad0: u32,\n fpn_e_slack: f32, fpn_c_slack: f32, inl_knot: f32, pad2: f32,\n};\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> means: array<u32>;\n@group(0) @binding(2) var<storage, read> aux: array<f32>;\n@group(0) @binding(3) var<storage, read_write> out: array<u32>;\n\nvar<private> state: u32;\nvar<private> uncertain: bool;\nvar<private> radius: f32;\nvar<private> ip: i32;\nvar<private> fp: f32;\n\nfn fmix(value: u32) -> u32 {\n var h = value;\n h = h ^ (h >> 16u); h = h * 0x85ebca6bu; h = h ^ (h >> 13u); h = h * 0xc2b2ae35u; h = h ^ (h >> 16u);\n return h;\n}\nfn mix_block(hash: u32, block: u32) -> u32 {\n var k = block * 0xcc9e2d51u;\n k = (k << 15u) | (k >> 17u);\n k = k * 0x1b873593u;\n var h = hash ^ k;\n h = (h << 13u) | (h >> 19u);\n return h * 5u + 0xe6546b64u;\n}\nfn site_seed(seed: u32, index: u32, stream: u32) -> u32 {\n return fmix(mix_block(mix_block(mix_block(0u, seed), index), stream) ^ 12u);\n}\nfn start(seed: u32) {\n state = fmix(seed ^ 0x9e3779b9u);\n if (state == 0u) { state = 0x6d2b79f5u; }\n}\nfn next_bits() -> u32 {\n state = state ^ (state << 13u); state = state ^ (state >> 17u); state = state ^ (state << 5u);\n return state;\n}\n// (bits + 0.5)/2^32 with two roundings (the conversion and the sum; the power-of-two scale is exact): ≤ 2E.\nfn unit(bits: u32) -> f32 { return (f32(bits) + 0.5) * INV32; }\nfn check(distance: f32, margin: f32) { if (abs(distance) <= margin) { uncertain = true; } }\n\n// ln((bits + 0.5)/2^32), relative error ≤ 11E: WGSL log below 1/2 (≤ 6E), else 2·atanh(r) with r = −w/(2 − w)\n// on the exact 1 − unit (division 2.5E; ≤ 10.5E in all).\nfn ln_unit(bits: u32) -> f32 {\n if (bits < 0x80000000u) { return log(unit(bits)); }\n let w = (f32(0xffffffffu - bits) + 0.5) * INV32;\n let r = -w / (2.0 - w);\n let r2 = r * r;\n var p = ${literal(1 / 23)};\n ${[21, 19, 17, 15, 13, 11, 9, 7, 5, 3].map(n => `p = p * r2 + ${literal(1 / n)};`).join(\" \")}\n p = p * r2 + 1.0;\n return 2.0 * r * p;\n}\n// cos(2π (bits + 0.5)/2^32): exact octant reduction on the integer, Taylor polynomials on [0, π/4]; absolute error ≤ 8E.\nfn cos_unit(bits: u32) -> f32 {\n let octant = bits >> 29u;\n let low = bits & 0x1fffffffu;\n let even = (octant & 1u) == 0u;\n var g = (f32(0x1fffffffu - low) + 0.5) * TWO_PI_INV32;\n if (even) { g = (f32(low) + 0.5) * TWO_PI_INV32; }\n let g2 = g * g;\n var c = ${literal(COS_COEFFICIENTS[5]!)};\n ${[4, 3, 2, 1, 0].map(j => `c = c * g2 + ${literal(COS_COEFFICIENTS[j]!)};`).join(\" \")}\n c = c * g2 + 1.0;\n var s = ${literal(SIN_COEFFICIENTS[4]!)};\n ${[3, 2, 1, 0].map(j => `s = s * g2 + ${literal(SIN_COEFFICIENTS[j]!)};`).join(\" \")}\n s = g * (s * g2 + 1.0);\n let cb = select(s, c, even);\n let sb = select(c, s, even);\n let q = octant >> 1u;\n if (q == 0u) { return cb; }\n if (q == 1u) { return -sb; }\n if (q == 2u) { return -cb; }\n return sb;\n}\n// Box–Muller, one value per two uniforms: radius = sqrt(−2 ln u1) (≤ 10E relative), times cos (≤ 8E absolute),\n// one product rounding: |z_f32 − z_f64| ≤ Z_ERR·E·radius with Z_ERR = 20 (derivation in docs/WEBGPU.md).\nfn std_normal() -> f32 {\n let b1 = next_bits();\n let b2 = next_bits();\n radius = sqrt(-2.0 * ln_unit(b1));\n return radius * cos_unit(b2);\n}\n// Multiplication method: product of uniforms (3E per factor) against exp(−mean), whose relative error is\n// (3 + 2·mean) ULP plus the float32 mean's absolute error E·(mean + 1): ≤ (4 + 3·mean)·E.\nfn poisson_small(mean: f32) -> u32 {\n let threshold = exp(-mean);\n var product = 1.0;\n var count = 0u;\n loop {\n count = count + 1u;\n product = product * unit(next_bits());\n check(product - threshold, MARGIN * (3.0 * f32(count) + 4.0 + 3.0 * mean) * E * max(product, threshold));\n if (!(product > threshold)) { break; }\n if (count >= 1000u) { uncertain = true; break; }\n }\n return count - 1u;\n}\n// g(t) = ln(1 + t) − t for |t| <= 1/2 without cancellation: −t²/(2+t) + 2(s³/3 + s⁵/5 + …), s = t/(2+t).\nfn log1p_minus(t: f32) -> f32 {\n let d = 2.0 + t;\n let s = t / d;\n let s2 = s * s;\n var p = ${literal(1 / 25)};\n ${[23, 21, 19, 17, 15, 13, 11, 9, 7, 5].map(n => `p = p * s2 + ${literal(1 / n)};`).join(\" \")}\n p = p * s2 + ${literal(1 / 3)};\n return -(t * t) / d + 2.0 * s * s2 * p;\n}\nfn poisson_ptrs(mi: u32, mf: f32) -> u32 {\n // mean: ≤ 2.1E relative; sq: ≤ 5.6E; b: ≤ 8.7E; a: ≤ 14E; inv_alpha: ≤ 3.5E; v_r: ≤ 6E absolute (docs/WEBGPU.md).\n let mean = f32(mi) + mf;\n let sq = sqrt(mean);\n let b = 0.931 + 2.53 * sq;\n let a = -0.059 + 0.02483 * b;\n let inv_alpha = 1.1239 + 1.1328 / (b - 3.4);\n let v_r = 0.9277 - 3.6224 / (b - 2.0);\n let ln_mean = log(mean);\n for (var iteration = 0u; iteration < 64u; iteration = iteration + 1u) {\n let su = next_bits();\n let sv = next_bits();\n var m = su;\n var sign = -1.0;\n if (su >= 0x80000000u) { m = 0xffffffffu - su; sign = 1.0; }\n let us = unit(m);\n let u = sign * unit(0x7fffffffu - m);\n let v = unit(sv);\n // scale ≤ 19.5E, t = scale·u ≤ 22.5E relative; x = (t + mf) + 0.43 in any order adds 2E·(|t| + 1.43) + 2E.\n let scale = 2.0 * a / us + b;\n let t = scale * u;\n let x = (t + mf) + 0.43;\n let fl = floor(x);\n let ex = MARGIN * (25.0 * E * abs(t) + 4.0 * E + 4.5e-16 * (abs(t) + mean + 1.0));\n if (x - fl <= ex || fl + 1.0 - x <= ex) { uncertain = true; }\n if (fl <= -1073741824.0 || fl >= 1073741824.0) { continue; }\n let k = i32(mi) + i32(fl);\n if (m >= ${uniformThresholdInteger(0.07)}u) {\n if (k >= 0) { check(v - v_r, MARGIN * (7.0 * E + 2.0 * E * v)); }\n if (v <= v_r && k >= 0) { return u32(k); }\n }\n if (k < 0 || (m < ${uniformThresholdInteger(0.013)}u && sv > m)) { continue; }\n // lhs: the argument carries ≤ 31.5E (two divisions), the logarithm 3 ULP or 2^-21.\n let lhs = log(v * inv_alpha / (a / (us * us) + b));\n let dl = 32.0 * E + LOG_ABS + 3.0 * E * abs(lhs);\n var rhs: f32;\n var dr: f32;\n if (k == 0) {\n rhs = -mean;\n dr = 3.0 * E * mean + CPU_REL * mean;\n } else {\n let kf = f32(k);\n let dd = f32(i32(mi) - k) + mf;\n let t2 = dd / kf;\n var kg: f32;\n var dk: f32;\n if (abs(t2) <= 0.5) {\n kg = kf * log1p_minus(t2);\n dk = 25.0 * E * abs(kg) + E;\n } else {\n let lr = log(mean / kf);\n kg = kf * lr - dd;\n dk = kf * (6.0 * E + LOG_ABS + 5.0 * E * abs(lr)) + 2.0 * E * abs(dd) + E + E * abs(kg);\n }\n let lnk = log(kf);\n var corr: f32;\n if (k < 256) { corr = aux[u32(k)]; } else { corr = 1.0 / (12.0 * kf) - 1.0 / (360.0 * kf * kf * kf); }\n rhs = ((kg - 0.5 * lnk) - 0.91893853320467274) - corr;\n dr = dk + 0.5 * LOG_ABS + 1.5 * E * lnk + 2.0 * E + 3.0 * E * (abs(kg) + 0.5 * lnk + 1.0) +\n CPU_REL * (mean + kf * (abs(ln_mean) + lnk + 1.0));\n }\n check(lhs - rhs, MARGIN * (dl + dr));\n if (lhs <= rhs) { return u32(k); }\n }\n uncertain = true;\n return mi;\n}\nfn poisson(word: u32, frac: f32) -> u32 {\n let category = word >> 30u;\n let mi = word & 0x3fffffffu;\n if (category == 0u) { return 0u; }\n if (category == 1u) { return poisson_small(f32(mi) + frac); }\n if (category == 2u) { return poisson_ptrs(mi, frac); }\n // Means of 1e9 and above: the draw always exceeds the capacity (<= 2^24), so the code does not depend on it.\n let z = std_normal();\n let y = sqrt(f32(mi) + frac) * z + frac + 0.5;\n return u32(max(0, i32(mi) + i32(floor(y))));\n}\n// Adds product·2^s for an exact integer product < 2^24: the integer part by an integer shift, the fraction as\n// an exact float32 below 1 (the masked low bits times the exact power of two). No float split depends on order.\nfn add_scaled(product: u32, s: i32, scale: f32) {\n if (product == 0u) { return; }\n // product·2^s is below 2^30 by the host's domain check, so the shift count is below 31.\n if (s >= 0) { ip = ip + i32(product << u32(s)); return; }\n if (s <= -24) { fp = fp + f32(product) * scale; return; }\n let shift = u32(-s);\n ip = ip + i32(product >> shift);\n fp = fp + f32(product & ((1u << shift) - 1u)) * scale;\n}\n// n·(p0 + p1 + p2 + p3) for an integer 0 <= n < 2^24 and pieces p_j = m_j·2^(s_j) with m_j < 2^12: the\n// products hi·m_j and lo·m_j are exact 32-bit integers.\nfn scaled(n: u32, base: u32) {\n let hi = n >> 12u;\n let lo = n & 0xfffu;\n for (var j = 0u; j < 4u; j = j + 1u) {\n let m = u32(aux[base + ${SENSOR_PIECE_WORDS}u * j]);\n let s = i32(aux[base + ${SENSOR_PIECE_WORDS}u * j + 1u]);\n let scale = aux[base + ${SENSOR_PIECE_WORDS}u * j + 2u];\n add_scaled(hi * m, s + 12, scale * 4096.0);\n add_scaled(lo * m, s, scale);\n }\n}\nfn normalize() {\n let f = floor(fp);\n ip = ip + i32(f);\n fp = fp - f;\n}\n// Piecewise-linear INL offset with host-computed segment slopes (no division): ao + (code − ac)·slope.\nfn inl_offset(code: f32) -> f32 {\n let n = params.knots;\n let base = ${SENSOR_AUX.knots}u;\n if (code <= aux[base]) { return aux[base + n]; }\n if (code >= aux[base + n - 1u]) { return aux[base + 2u * n - 1u]; }\n var low = 0u;\n var high = n - 1u;\n loop {\n if (high - low <= 1u) { break; }\n let middle = (low + high) >> 1u;\n if (aux[base + middle] <= code) { low = middle; } else { high = middle; }\n }\n return aux[base + n + low] + (code - aux[base + low]) * aux[base + 2u * n + low];\n}\n@compute @workgroup_size(${SENSOR_WORKGROUP_SIZE})\nfn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) groups: vec3<u32>) {\n let i = gid.x + gid.y * groups.x * ${SENSOR_WORKGROUP_SIZE}u;\n if (i >= params.count) { return; }\n let row = params.row0 + i / params.region_w;\n let index = (params.region_y + row) * params.native_w + params.region_x + i % params.region_w;\n uncertain = false;\n let base = i * params.site_words;\n start(site_seed(params.seed, index, 0u));\n let photo = poisson(means[base], bitcast<f32>(means[base + 1u]));\n let dark = poisson(means[base + 2u], bitcast<f32>(means[base + 3u]));\n // Saturating sums: any total above the clamp is above every supported capacity, as in the reference.\n var realized = min(photo + dark, ${SENSOR_REALIZED_CLAMP}u);\n for (var k = 0u; k < params.poisson_count; k = k + 1u) {\n realized = min(realized + poisson(means[base + 4u + 2u * k], bitcast<f32>(means[base + 5u + 2u * k])), ${SENSOR_REALIZED_CLAMP}u);\n }\n var cursor = base + 4u + 2u * params.poisson_count;\n let deterministic = (params.extras & ${SENSOR_EXTRAS.deterministic}u) != 0u;\n let pattern = (params.extras & ${SENSOR_EXTRAS.fixedPattern}u) != 0u;\n var r_frac = 0.0;\n var det_slack = 0.0;\n if (deterministic) {\n realized = min(realized + means[cursor], ${SENSOR_REALIZED_CLAMP}u);\n r_frac = bitcast<f32>(means[cursor + 1u]);\n det_slack = E;\n cursor = cursor + 2u;\n }\n var t_floor = i32(params.threshold_floor);\n var t_frac = params.threshold_frac;\n var fpn_e_slack = 0.0;\n if (pattern) {\n t_floor = bitcast<i32>(means[cursor]);\n t_frac = bitcast<f32>(means[cursor + 1u]);\n fpn_e_slack = params.fpn_e_slack;\n }\n start(site_seed(params.seed, index, 1u));\n var noise = 0.0;\n var noise_bound = 0.0;\n for (var c = 0u; c < params.components; c = c + 1u) {\n let sigma = aux[256u + c];\n if (sigma != 0.0) {\n let z = std_normal();\n noise = noise + z * sigma;\n // z's error, sigma's rounding, the product, and the order-independent sum over the component count.\n noise_bound = noise_bound + sigma * radius * (Z_ERR + 2.0 + f32(params.components)) * E;\n }\n }\n var flags = 0u;\n var a_int = realized;\n var a_frac = r_frac;\n if (!deterministic) {\n if (realized > params.capacity_floor) { flags = 1u; a_int = params.capacity_floor; a_frac = params.capacity_frac; }\n } else {\n // realized > capacity with a non-integer total: a three-term float32 sum (any order) with its bound.\n let w = f32(i32(realized) - i32(params.capacity_floor));\n let dcap = w + (r_frac - params.capacity_frac);\n check(dcap, MARGIN * (3.0 * E * abs(w) + 6.0 * E + CPU_REL * (f32(realized) + 1.0)));\n if (dcap > 0.0) { flags = 1u; a_int = params.capacity_floor; a_frac = params.capacity_frac; }\n }\n // Pre-ADC limit: afterPhysical + noise + offset > threshold, as afterPhysical − (threshold − offset) + noise > 0,\n // a four-term float32 sum (any order).\n let whole = f32(i32(a_int) - t_floor);\n let diff = (whole + (a_frac - t_frac)) + noise;\n check(diff, MARGIN * (noise_bound + 4.0 * E * abs(whole) + 3.0 * E * abs(noise) + 8.0 * E + CPU_REL * (f32(a_int) + abs(noise)) +\n fpn_e_slack + det_slack));\n var q: i32;\n if (diff > 0.0) {\n flags = flags | 2u;\n q = params.sat_quantized;\n } else {\n // ideal = (afterPhysical + noise + offset)/gain + black, as an exact integer part plus a float32 fraction.\n ip = i32(params.black);\n fp = 0.0;\n var fpn_c_slack = 0.0;\n if (pattern) {\n ip = ip + bitcast<i32>(means[cursor + 2u]);\n fp = bitcast<f32>(means[cursor + 3u]);\n fpn_c_slack = params.fpn_c_slack;\n }\n scaled(a_int, ${SENSOR_AUX.inversePieces}u);\n let noise_codes = noise * params.inv_gain;\n let frac_codes = a_frac * params.inv_gain;\n fp = fp + frac_codes + noise_codes;\n let a_codes = f32(a_int) * params.inv_gain;\n // The fraction sum (eleven terms below 1, frac_codes and noise_codes), the two normalizations and the\n // half-code comparison in any association: 13E·Σ|terms|; then the term errors, the pieces' residual,\n // the reference's float64 rounding and the packed fractions' rounding.\n var margin = MARGIN * (13.0 * E * (13.0 + frac_codes + abs(noise_codes)) + 3.0 * E * frac_codes + 2.0 * E * abs(noise_codes) +\n noise_bound * params.inv_gain + f32(a_int) * params.inv_residual +\n CPU_REL * (a_codes + abs(noise_codes) + f32(params.black)) + fpn_c_slack + det_slack * params.inv_gain);\n normalize();\n if (params.knots > 0u) {\n let ideal = f32(ip) + fp;\n let offset = inl_offset(ideal);\n fp = fp + offset;\n margin = margin * (1.0 + params.inl_slope) + MARGIN * (params.inl_slope * 2.0 * E * (abs(ideal) + 1.0 + params.inl_knot) +\n 11.0 * E * (params.inl_offset + 1.0) + CPU_REL * (abs(ideal) + params.inl_offset) * (1.0 + params.inl_slope));\n normalize();\n }\n if ((params.digital & 4u) == 0u) { check(fp - 0.5, margin); }\n q = ip + select(0, 1, fp >= 0.5);\n }\n if ((params.digital & 1u) != 0u) {\n let adc = clamp(q, 0, i32(params.adc_max));\n let delta = adc - i32(params.black);\n let estimate = f32(delta) * params.digital_gain;\n if (estimate >= 16777216.0) {\n q = 16777216;\n } else if (estimate <= -16777216.0) {\n q = -16777216;\n } else {\n ip = 0;\n fp = 0.0;\n scaled(u32(abs(delta)), ${SENSOR_AUX.digitalPieces}u);\n // Eight exact fractions summed in any order, the floor split and the half comparison: ≤ 10 roundings on\n // values below 9; the pieces' residual; the reference's float64 product.\n if ((params.digital & 2u) == 0u) { check(fp - floor(fp) - 0.5, MARGIN * (90.0 * E + f32(abs(delta)) * params.digital_residual + CPU_REL * (abs(estimate) + f32(params.black)))); }\n normalize();\n var whole_part = ip;\n var fraction = fp;\n if (delta < 0) {\n whole_part = -ip;\n if (fp > 0.0) { whole_part = -ip - 1; fraction = 1.0 - fp; }\n }\n q = whole_part + i32(params.black) + select(0, 1, fraction >= 0.5);\n }\n }\n if (q >= i32(params.saturation)) { flags = flags | 4u; }\n if (q < 0) { flags = flags | 8u; }\n let code = u32(clamp(q, 0, i32(params.saturation)));\n out[i] = code | (flags << 16u) | select(0u, ${1 << 20}u, uncertain);\n}\n`;\n"]}
|