@photivra/engine 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +156 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +19 -3
  4. package/README.md +6 -6
  5. package/SECURITY.md +1 -1
  6. package/dist/core/portable-math.d.ts +6 -0
  7. package/dist/core/portable-math.js +39 -0
  8. package/dist/core/portable-math.js.map +1 -1
  9. package/dist/core/version.d.ts +1 -1
  10. package/dist/core/version.js +1 -1
  11. package/dist/core/version.js.map +1 -1
  12. package/dist/facade/chart-material-layers.d.ts +115 -0
  13. package/dist/facade/chart-material-layers.js +224 -0
  14. package/dist/facade/chart-material-layers.js.map +1 -0
  15. package/dist/facade/chart-radiance-batch.d.ts +8 -2
  16. package/dist/facade/chart-radiance-batch.js +11 -4
  17. package/dist/facade/chart-radiance-batch.js.map +1 -1
  18. package/dist/facade/chart-scenes.d.ts +134 -0
  19. package/dist/facade/chart-scenes.js +32 -22
  20. package/dist/facade/chart-scenes.js.map +1 -1
  21. package/dist/facade/diffraction-provider.d.ts +8 -2
  22. package/dist/facade/diffraction-provider.js +71 -2
  23. package/dist/facade/diffraction-provider.js.map +1 -1
  24. package/dist/facade/display-view-gpu-session.d.ts +18 -0
  25. package/dist/facade/display-view-gpu-session.js +280 -0
  26. package/dist/facade/display-view-gpu-session.js.map +1 -0
  27. package/dist/facade/display-view-gpu.d.ts +146 -0
  28. package/dist/facade/display-view-gpu.js +59 -0
  29. package/dist/facade/display-view-gpu.js.map +1 -0
  30. package/dist/facade/display-view.d.ts +48 -1
  31. package/dist/facade/display-view.js +7 -7
  32. package/dist/facade/display-view.js.map +1 -1
  33. package/dist/facade/expected-signal-setup.d.ts +5 -0
  34. package/dist/facade/expected-signal-setup.js +16 -0
  35. package/dist/facade/expected-signal-setup.js.map +1 -1
  36. package/dist/facade/expected-signal.d.ts +48 -3
  37. package/dist/facade/expected-signal.js +8 -0
  38. package/dist/facade/expected-signal.js.map +1 -1
  39. package/dist/facade/scene-radiance.d.ts +4 -2
  40. package/dist/facade/scene-radiance.js +7 -4
  41. package/dist/facade/scene-radiance.js.map +1 -1
  42. package/dist/facade/simulate-capture.d.ts +108 -8
  43. package/dist/facade/simulate-capture.js +226 -33
  44. package/dist/facade/simulate-capture.js.map +1 -1
  45. package/dist/facade/simulate-display-view.d.ts +31 -3
  46. package/dist/facade/simulate-display-view.js +73 -13
  47. package/dist/facade/simulate-display-view.js.map +1 -1
  48. package/dist/gpu/chart-composite-gpu.d.ts +119 -0
  49. package/dist/gpu/chart-composite-gpu.js +572 -0
  50. package/dist/gpu/chart-composite-gpu.js.map +1 -0
  51. package/dist/gpu/chart-radiance-gpu.d.ts +6 -6
  52. package/dist/gpu/chart-radiance-gpu.js +9 -11
  53. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  54. package/dist/gpu/display-view-chart-material-module.d.ts +10 -0
  55. package/dist/gpu/display-view-chart-material-module.js +58 -0
  56. package/dist/gpu/display-view-chart-material-module.js.map +1 -0
  57. package/dist/gpu/display-view-composite-material-module.d.ts +68 -0
  58. package/dist/gpu/display-view-composite-material-module.js +248 -0
  59. package/dist/gpu/display-view-composite-material-module.js.map +1 -0
  60. package/dist/gpu/display-view-quality-gpu.d.ts +185 -0
  61. package/dist/gpu/display-view-quality-gpu.js +1000 -0
  62. package/dist/gpu/display-view-quality-gpu.js.map +1 -0
  63. package/dist/gpu/display-view-speed-gpu.d.ts +156 -0
  64. package/dist/gpu/display-view-speed-gpu.js +524 -0
  65. package/dist/gpu/display-view-speed-gpu.js.map +1 -0
  66. package/dist/gpu/image-kernel-accelerator.d.ts +23 -4
  67. package/dist/gpu/image-kernel-accelerator.js +58 -10
  68. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  69. package/dist/gpu/image-kernel-execution.d.ts +55 -3
  70. package/dist/gpu/image-kernel-execution.js.map +1 -1
  71. package/dist/gpu/sensor-stage-gpu.d.ts +11 -0
  72. package/dist/gpu/sensor-stage-gpu.js +42 -13
  73. package/dist/gpu/sensor-stage-gpu.js.map +1 -1
  74. package/dist/gpu/webgpu-runtime.d.ts +4 -2
  75. package/dist/gpu/webgpu-runtime.js +13 -4
  76. package/dist/gpu/webgpu-runtime.js.map +1 -1
  77. package/dist/gpu/webgpu-switch.d.ts +4 -0
  78. package/dist/gpu/webgpu-switch.js +29 -0
  79. package/dist/gpu/webgpu-switch.js.map +1 -0
  80. package/dist/gpu/wgsl-chart-composite-kernel.d.ts +52 -0
  81. package/dist/gpu/wgsl-chart-composite-kernel.js +515 -0
  82. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -0
  83. package/dist/gpu/wgsl-chart-kernel.d.ts +11 -9
  84. package/dist/gpu/wgsl-chart-kernel.js +33 -27
  85. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  86. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +95 -0
  87. package/dist/gpu/wgsl-display-view-quality-kernel.js +769 -0
  88. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -0
  89. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +75 -0
  90. package/dist/gpu/wgsl-display-view-speed-kernel.js +386 -0
  91. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -0
  92. package/dist/gpu/wgsl-sensor-kernel.d.ts +28 -9
  93. package/dist/gpu/wgsl-sensor-kernel.js +94 -50
  94. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  95. package/dist/index.d.ts +5 -2
  96. package/dist/index.js +2 -0
  97. package/dist/index.js.map +1 -1
  98. package/dist/optics/defocus-disc-kernel.d.ts +61 -0
  99. package/dist/optics/defocus-disc-kernel.js +191 -0
  100. package/dist/optics/defocus-disc-kernel.js.map +1 -0
  101. package/docs/API_REFERENCE.md +47 -1
  102. package/docs/QUICK_START.md +150 -5
  103. package/docs/USAGE.md +81 -3
  104. package/docs/api/core-version.md +1 -1
  105. package/docs/api/exports.json +127 -2
  106. package/docs/api/facade-chart-material-layers.md +137 -0
  107. package/docs/api/facade-chart-radiance-batch.md +7 -2
  108. package/docs/api/facade-display-view-gpu.md +94 -0
  109. package/docs/api/facade-scene-radiance.md +3 -2
  110. package/docs/api/facade-simulate-capture.md +95 -11
  111. package/docs/api/facade-simulate-display-view.md +17 -2
  112. package/docs/api/gpu-image-kernel-accelerator.md +10 -3
  113. package/docs/api/gpu-image-kernel-execution.md +71 -4
  114. package/docs/api/gpu-webgpu-switch.md +23 -0
  115. package/package.json +3 -1
@@ -0,0 +1 @@
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
- * The bounds use u = 2^-24 and the WGSL accuracy table (W3C WGSL, "Floating Point Accuracy"): correctly
22
- * rounded + − ×, division 2.5 ULP, sqrt inherited from 1/inverseSqrt (≤ 4.5 ULP), exp 3 + 2|x| ULP,
23
- * log 3 ULP or 2^-21 absolute on [0.5, 2]. WGSL's built-in cos has only a 2^-11 absolute bound, so
24
- * the kernel uses its own range-reduced Taylor polynomials. Every margin is doubled (`MARGIN`).
25
- * See docs/WEBGPU.md.
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
- /** Auxiliary table layout (float32 words). */
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: 276;
39
- knots: 280;
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
- * The bounds use u = 2^-24 and the WGSL accuracy table (W3C WGSL, "Floating Point Accuracy"): correctly
23
- * rounded + − ×, division 2.5 ULP, sqrt inherited from 1/inverseSqrt (≤ 4.5 ULP), exp 3 + 2|x| ULP,
24
- * log 3 ULP or 2^-21 absolute on [0.5, 2]. WGSL's built-in cos has only a 2^-11 absolute bound, so
25
- * the kernel uses its own range-reduced Taylor polynomials. Every margin is doubled (`MARGIN`).
26
- * See docs/WEBGPU.md.
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
- /** Auxiliary table layout (float32 words). */
31
- export const SENSOR_AUX = Object.freeze({ stirlingRemainder: 0, sigma: 256, inversePieces: 272, digitalPieces: 276, knots: 280 });
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 U: f32 = 5.9604644775390625e-8;
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, pad1: f32, pad2: 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 <= 12u: WGSL log below 1/2, else 2·atanh(r) on the exact 1 - u.
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; |z_f32 − z_f64| <= 24u·radius.
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) + 130.0) * U * max(product, threshold));
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 * (abs(t) * 40.0 * U + 2.0 * U * (abs(t) + 1.0) + 4.5e-16 * (abs(t) + mean + 1.0));
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 * (16.0 * U + 2.0 * U * v)); }
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 = 60.0 * U + 4.76837158203125e-7 + 4.0 * U * abs(lhs);
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 = 2.0 * U * mean + CPU_REL * mean;
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 = 20.0 * U * abs(kg);
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 * (8.0 * U + 4.76837158203125e-7 + 5.0 * U * abs(lr)) + 3.0 * U * abs(dd) + U * abs(kg);
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 + 2.384185791015625e-7 + 2.0 * U * lnk + U + 0.6 * U + 3.0 * U * (abs(kg) + 0.5 * lnk + 1.0) +
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 non-negative float32 value split into integer and fractional parts.
248
- fn accumulate(value: f32) {
249
- let f = floor(value);
250
- ip = ip + i32(f);
251
- fp = fp + (value - f);
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 12-bit pieces: every product is exact.
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 = f32(n >> 12u);
256
- let lo = f32(n & 0xfffu);
290
+ let hi = n >> 12u;
291
+ let lo = n & 0xfffu;
257
292
  for (var j = 0u; j < 4u; j = j + 1u) {
258
- let piece = aux[base + j];
259
- accumulate(hi * piece * 4096.0);
260
- accumulate(lo * piece);
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 = ${"280u"};
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
- let ac = aux[base + low];
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 = U;
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
- noise_bound = noise_bound + sigma * radius * (28.0 + f32(params.components)) * U;
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 difference with its bound.
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 * (5.0 * U + U * abs(w) + U * abs(dcap) + CPU_REL * (f32(realized) + 1.0)));
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 + 3.0 * U + 2.0 * U * (abs(whole) + abs(noise) + 2.0) + CPU_REL * (f32(a_int) + abs(noise)) +
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, 272u);
399
+ scaled(a_int, ${SENSOR_AUX.inversePieces}u);
362
400
  let noise_codes = noise * params.inv_gain;
363
- fp = fp + a_frac * params.inv_gain + noise_codes;
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
- 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) +
366
- noise_bound * params.inv_gain + f32(a_int) * params.inv_residual + 2.0 * U +
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 * U * (abs(ideal) + 1.0) +
374
- 8.0 * U * (params.inl_offset + 1.0) + CPU_REL * (abs(ideal) + params.inl_offset) * (1.0 + params.inl_slope));
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)), 276u);
392
- 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)))); }
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":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,sGAAsG;AACtG,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;AACvG;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;AAClI,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AACpC,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AACzC;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,+BAA+B;AAC/B,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC,CAAC;AAClF,wGAAwG;AACxG,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE,CAAC;AAC7C,0FAA0F;AAC1F,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,oBAAoB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;AACtH,2FAA2F;AAC3F,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;AACxC,iGAAiG;AACjG,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAEtC,sFAAsF;AACtF,MAAM,UAAU,uBAAuB,CAAC,CAAS;IAC/C,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,gBAAgB,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC;AACjF,MAAM,gBAAgB,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,SAAS,CAAC,CAAC;AAC5F,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,MAAM,CAAC,MAAM,eAAe,GAAG,UAAU,CAAC;;;4BAGd,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;;;;qBAIrC,OAAO,CAAC,mBAAmB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAyDrC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC;IACvB,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,gBAAgB,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;;;;;;;;;;;;YAYlF,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAE,CAAC;IACrC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,gBAAgB,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;;YAE5E,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAE,CAAC;IACrC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,gBAAgB,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAsCzE,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC;IACvB,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,gBAAgB,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;iBAC9E,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eA8BhB,uBAAuB,CAAC,IAAI,CAAC;;;;wBAIpB,uBAAuB,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6BAgEzB,kBAAkB;6BAClB,kBAAkB;6BAClB,kBAAkB;;;;;;;;;;;;;eAahC,UAAU,CAAC,KAAK;;;;;;;;;;;;2BAYJ,qBAAqB;;uCAET,qBAAqB;;;;;;;;;;qCAUvB,qBAAqB;;6GAEmD,qBAAqB;;;yCAGzF,aAAa,CAAC,aAAa;mCACjC,aAAa,CAAC,YAAY;;;;+CAId,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAyDhD,UAAU,CAAC,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAkCZ,UAAU,CAAC,aAAa;;;;;;;;;;;;;;;;;gDAiBR,CAAC,IAAI,EAAE;;CAEtD,CAAC","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"]}