@photivra/engine 2.12.0 → 2.14.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 (148) hide show
  1. package/CHANGELOG.md +144 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -7
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +3 -0
  6. package/dist/camera/lens-projection.js +2 -1
  7. package/dist/camera/lens-projection.js.map +1 -1
  8. package/dist/capture/dng-import.js +1 -1
  9. package/dist/capture/dng-import.js.map +1 -1
  10. package/dist/capture/export-jpeg.d.ts +1 -1
  11. package/dist/capture/export-jpeg.js +6 -4
  12. package/dist/capture/export-jpeg.js.map +1 -1
  13. package/dist/capture/host-yield-cadence.js +1 -1
  14. package/dist/capture/host-yield-cadence.js.map +1 -1
  15. package/dist/capture/native-environment-optical-stage.js +1 -1
  16. package/dist/capture/native-environment-optical-stage.js.map +1 -1
  17. package/dist/capture/native-raw.js +1 -1
  18. package/dist/capture/native-raw.js.map +1 -1
  19. package/dist/capture/packed-sensor-raw.js +3 -3
  20. package/dist/capture/packed-sensor-raw.js.map +1 -1
  21. package/dist/capture/sensor-raw-producer.js +1 -1
  22. package/dist/capture/sensor-raw-producer.js.map +1 -1
  23. package/dist/color/capture-color.js +3 -3
  24. package/dist/color/capture-color.js.map +1 -1
  25. package/dist/color/chromatic-adaptation.js +2 -2
  26. package/dist/color/chromatic-adaptation.js.map +1 -1
  27. package/dist/color/color-matching-functions.js +1 -1
  28. package/dist/color/color-matching-functions.js.map +1 -1
  29. package/dist/color/colorimetry-math.js +2 -2
  30. package/dist/color/colorimetry-math.js.map +1 -1
  31. package/dist/color/illuminants.js +3 -3
  32. package/dist/color/illuminants.js.map +1 -1
  33. package/dist/color/rgb-color-spaces.js +1 -1
  34. package/dist/color/rgb-color-spaces.js.map +1 -1
  35. package/dist/color/sensor-color-development.js +1 -1
  36. package/dist/color/sensor-color-development.js.map +1 -1
  37. package/dist/core/record-validation.js +3 -3
  38. package/dist/core/record-validation.js.map +1 -1
  39. package/dist/core/version.d.ts +1 -1
  40. package/dist/core/version.js +1 -1
  41. package/dist/core/version.js.map +1 -1
  42. package/dist/equipment/generic-prime-lenses.js +3 -3
  43. package/dist/equipment/generic-prime-lenses.js.map +1 -1
  44. package/dist/equipment/generic-tier-assets.d.ts +23 -1
  45. package/dist/equipment/generic-tier-assets.js +58 -30
  46. package/dist/equipment/generic-tier-assets.js.map +1 -1
  47. package/dist/equipment/generic-tier-presets.d.ts +6 -3
  48. package/dist/equipment/generic-tier-presets.js +26 -13
  49. package/dist/equipment/generic-tier-presets.js.map +1 -1
  50. package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
  51. package/dist/exposure/auto-iso-slowest-shutter.js +217 -0
  52. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -0
  53. package/dist/exposure/exposure-mode-resolver.d.ts +57 -1
  54. package/dist/exposure/exposure-mode-resolver.js +238 -33
  55. package/dist/exposure/exposure-mode-resolver.js.map +1 -1
  56. package/dist/exposure/flash-ttl.js +1 -1
  57. package/dist/exposure/flash-ttl.js.map +1 -1
  58. package/dist/exposure/flash.js +1 -1
  59. package/dist/exposure/flash.js.map +1 -1
  60. package/dist/facade/chart-material-layers.js +2 -2
  61. package/dist/facade/chart-material-layers.js.map +1 -1
  62. package/dist/facade/chart-radiance-batch.js +1 -1
  63. package/dist/facade/chart-radiance-batch.js.map +1 -1
  64. package/dist/facade/chart-scenes.d.ts +45 -8
  65. package/dist/facade/chart-scenes.js +101 -44
  66. package/dist/facade/chart-scenes.js.map +1 -1
  67. package/dist/facade/color-patches.d.ts +129 -0
  68. package/dist/facade/color-patches.js +209 -0
  69. package/dist/facade/color-patches.js.map +1 -0
  70. package/dist/facade/diffraction-provider.js +12 -13
  71. package/dist/facade/diffraction-provider.js.map +1 -1
  72. package/dist/facade/display-view-gpu.js +1 -1
  73. package/dist/facade/display-view-gpu.js.map +1 -1
  74. package/dist/facade/display-view.d.ts +27 -0
  75. package/dist/facade/display-view.js +234 -65
  76. package/dist/facade/display-view.js.map +1 -1
  77. package/dist/facade/scene-radiance.js +2 -2
  78. package/dist/facade/scene-radiance.js.map +1 -1
  79. package/dist/facade/simulate-capture.d.ts +139 -12
  80. package/dist/facade/simulate-capture.js +209 -36
  81. package/dist/facade/simulate-capture.js.map +1 -1
  82. package/dist/facade/simulate-display-view.js +2 -2
  83. package/dist/facade/simulate-display-view.js.map +1 -1
  84. package/dist/gpu/chart-composite-gpu.js +2 -2
  85. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  86. package/dist/gpu/display-view-composite-material-module.js +1 -1
  87. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  88. package/dist/gpu/display-view-quality-gpu.d.ts +2 -1
  89. package/dist/gpu/display-view-quality-gpu.js +24 -14
  90. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  91. package/dist/gpu/image-kernel-execution.js +2 -2
  92. package/dist/gpu/image-kernel-execution.js.map +1 -1
  93. package/dist/gpu/temporal-sum-gpu.js +23 -23
  94. package/dist/gpu/webgpu-runtime.js +1 -1
  95. package/dist/gpu/webgpu-runtime.js.map +1 -1
  96. package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
  97. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
  98. package/dist/gpu/wgsl-chart-kernel.js +117 -117
  99. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  100. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +13 -1
  101. package/dist/gpu/wgsl-display-view-quality-kernel.js +522 -488
  102. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  103. package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
  104. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  105. package/dist/gpu/wgsl-kernels.js +203 -203
  106. package/dist/gpu/wgsl-kernels.js.map +1 -1
  107. package/dist/gpu/wgsl-sensor-kernel.js +302 -302
  108. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  109. package/dist/index.d.ts +8 -5
  110. package/dist/index.js +6 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/motion/temporal-exposure-sum.js +1 -1
  113. package/dist/motion/temporal-exposure-sum.js.map +1 -1
  114. package/dist/optics/diffraction-psf-profile.js +4 -4
  115. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  116. package/dist/optics/front-of-lens-filter.js +1 -1
  117. package/dist/optics/front-of-lens-filter.js.map +1 -1
  118. package/dist/optics/irradiance-convolution.d.ts +10 -0
  119. package/dist/optics/irradiance-convolution.js +49 -14
  120. package/dist/optics/irradiance-convolution.js.map +1 -1
  121. package/dist/output/native-capture-sdr.js +1 -1
  122. package/dist/output/native-capture-sdr.js.map +1 -1
  123. package/dist/output/png-encoder.js +2 -2
  124. package/dist/output/png-encoder.js.map +1 -1
  125. package/dist/output/print-jpeg.js +13 -10
  126. package/dist/output/print-jpeg.js.map +1 -1
  127. package/dist/output/text-metadata.d.ts +22 -2
  128. package/dist/output/text-metadata.js +58 -9
  129. package/dist/output/text-metadata.js.map +1 -1
  130. package/dist/sensor/fixed-pattern-noise.js +1 -1
  131. package/dist/sensor/fixed-pattern-noise.js.map +1 -1
  132. package/dist/sensor/readout-exposure-linkage.js +1 -1
  133. package/dist/sensor/readout-exposure-linkage.js.map +1 -1
  134. package/docs/API_REFERENCE.md +49 -1
  135. package/docs/QUICK_START.md +104 -9
  136. package/docs/USAGE.md +241 -2
  137. package/docs/api/core-version.md +1 -1
  138. package/docs/api/equipment-generic-tier-assets.md +41 -1
  139. package/docs/api/equipment-generic-tier-presets.md +11 -3
  140. package/docs/api/exports.json +172 -2
  141. package/docs/api/exposure-auto-iso-slowest-shutter.md +139 -0
  142. package/docs/api/exposure-exposure-mode-resolver.md +68 -0
  143. package/docs/api/facade-chart-scenes.md +36 -5
  144. package/docs/api/facade-color-patches.md +150 -0
  145. package/docs/api/facade-simulate-capture.md +140 -9
  146. package/docs/api/output-text-metadata.md +34 -0
  147. package/llms.txt +1 -0
  148. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"wgsl-chart-composite-kernel.js","sourceRoot":"","sources":["../../src/gpu/wgsl-chart-composite-kernel.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * WGSL kernel of the `webgpu-f32` chart-radiance stage for composite charts, the 2.3 chart elements and\n * declared `material-layers` scenes (#477), one invocation per query point. It resolves the point to a mask\n * layer or a composite element, classifies the material there with the element's own geometry (the float64\n * constants `resolveChartScene` decided with, uploaded once per prepared scene), and reads the material's\n * value from the host's float64 table. The eight 2.0 presets as plain charts keep their own kernel\n * (wgsl-chart-kernel.ts); as composite elements they run here.\n *\n * **Bound arithmetic.** Every float32 value carries an a-priori bound on its distance from the exact\n * value of the same expression: `B(v, e)` with |v − exact| ≤ e. A constant the host rounded from float64\n * carries E·|c| (E = 2^-23: one WGSL rounding of `+ − ×`, which the spec lets fall in either direction,\n * W3C WGSL \"Floating Point Accuracy\"); `+ − ×` add E·|v| each; `/` adds the WGSL 2.5-ULP bound (2.5·E·|v|);\n * a point carries its low float32 part (its float64 coordinate to 2^-48). A decision (sign, floor,\n * comparison) whose value lies within SAFETY (4) times its bound, plus 10^-14 relative slack for the\n * reference's own float64 rounding, flags the point, and the host recomputes it with the reference. So\n * every unflagged point has exactly the reference's material.\n *\n * **Double-float32 for the chirps.** The zone plate's phase k·r² and the sine star's angle would be decided\n * to about 2^-24 relative in plain float32, which at hundreds of cycles is wider than a 1024-level step\n * and would flag most points. Points are therefore uploaded as float32 pairs (hi + lo, the float64 point to\n * 2^-48), and the element frames, r², k·r² and the star's cross products are evaluated in double-float32\n * (TwoSum, Veltkamp split and Dekker TwoProduct; Joldes, Muller & Popescu, \"Tight and rigorous error\n * bounds for basic building blocks of double-word arithmetic\", ACM TOMS 44(2), 2017, free author copy on\n * HAL: each operation is within a few u² relative), with a declared bound of 2^-40 relative per result.\n * These algorithms need the written evaluation order and round-to-nearest `+ − ×`, and WGSL lets an\n * implementation reassociate and fuse (W3C WGSL § 15.7.5 \"Reassociation and Fusion\": \"An implementation may fuse\n * operations if the transformed expression is at least as accurate as the original formulation\"), round in either\n * direction (§ 15.7.4), and evaluate `fma` unfused (§ 15.7.2, § 17.5.32). WGSL has no way to forbid a fusion, so\n * the host probes the compiled building blocks before using them (`CHART_COMPOSITE_PROBE_WGSL`) with inputs whose\n * low parts are non-zero and whose sums and products round up or down to the nearest float32.\n *\n * **Two product paths (2.11.0).** Dekker's TwoProduct and the final Fast2Sum of the double-word product depend on\n * every rounded product being rounded before it is added: a fusion of `a*b - p`, of Veltkamp's `t - (t - a)` with\n * t = 4097·a, or of `p.h + l` with p.h = a.h·b.h changes them (the last adds the product's error twice). The fma path\n * (`two_prod_fma`, `d_mul_fma`) never adds a rounded product: TwoProduct is fma(a, b, −p) (exact for a fused fma;\n * Joldes, Muller & Popescu 2017, Algorithm 2), and the double-word product forms its hi part as fma(a.h, b.h, t) and\n * its error as fma(a.h, b.h, −s) + t, where only t = a.h·b.l + a.l·b.h, an ordinary bounded sum, may be fused. It\n * is within 9u² (first order) of the exact product (docs/WEBGPU.md), inside the declared 2^-40. The probe runs both\n * paths; the kernel is compiled with Dekker's when it passes (so an unfused device keeps its results bit for bit),\n * else with the fma one when it passes, else the device is refused. TwoSum, Fast2Sum and the double-word sum hold no\n * product and so cannot be fused; the zone plate's phase reduction subtracts 2·round(psi.h/2), an exact product, so\n * a fusion leaves it unchanged; the star's cross products and the lattice-point recovery are double-word products\n * and sums. The bound arithmetic stays valid under fusion: a fused multiply-add rounds once, so its result is\n * within E·|v| of the exact value of the same expression, the term `+` already adds (the product's own E·|v| term\n * is then a bound on a rounding that did not happen).\n *\n * **The rounding barrier (2.11.0).** Metal's compiler reassociated the error-free transforms on an Apple M5 Pro (the probe\n * of October 27, 2026: class `reassociated`, every differing low part zero, the value (a + b) − a → b gives), which neither\n * product path survives. Every operation of the transforms, and every value of the bound arithmetic, is therefore wrapped in\n * `ob`: the result's bits XORed with a word the host sets to 0 (a uniform: `params.zero`, `lattice.zero`, the probe's\n * `probe_zero`), which a compiler cannot see through, so it can neither regroup nor fuse across it and each operation rounds\n * once, as written. On a device that already evaluates as written it changes no value. On the M5 Pro the probe then reports\n * `exact` on the Dekker path (docs/WEBGPU.md). The probe still decides on every device.\n *\n * **No `sin`, `cos` or `atan2` decides anything.** The zone plate reduces its phase to [−1, 1] cycles\n * exactly (Sterbenz) and evaluates cos by quadrant reduction and a Taylor polynomial in bound arithmetic\n * (truncation ≤ 4·10^-13 on [0, π/4 + ε], the Lagrange remainder). A Siemens star (bar or sine profile) is\n * a table of its sector boundary directions, with the level of each sector read from the reference itself;\n * the sector is found by a rough `atan2` and a bucket table, then decided exactly by double-float32 cross\n * products with the two bracketing boundary directions (a point the rough guess misplaces beyond the\n * search window is flagged, never misclassified). See docs/WEBGPU.md, \"Scene radiance on the GPU\".\n */\n\nimport { DIGIT_SEGMENTS, NUMERAL_ADVANCE, SEGMENTS } from \"../facade/chart-scenes.js\";\n\nexport const CHART_COMPOSITE_WORKGROUP_SIZE = 256;\n\n/** Element kind codes of the record headers. */\nexport const CHART_ELEMENT_KIND = Object.freeze({ uniform: 0, \"slanted-edge\": 1, \"siemens-star\": 2, \"step-wedge\": 3, checkerboard: 5, \"point-grid\": 6, patches: 7,\n \"dead-leaves\": 8, \"bar-groups\": 9, \"hyperbolic-wedge\": 10, \"zone-plate\": 11, \"slanted-squares\": 12, \"line-grid\": 13, fiducial: 14 } as const);\n/** Placement modes of an element record: the background fills the frame; elements are rectangles or ellipses. */\nexport const CHART_PLACEMENT = Object.freeze({ frame: 0, rectangle: 1, ellipse: 2 } as const);\n/** Words per element header, layer header and shape header in the u32 record array. */\nexport const CHART_RECORD_STRIDE = Object.freeze({ element: 8, layer: 16, shape: 4, placementFloats: 10, barGroupFloats: 16 } as const);\n\nconst SEGMENT_ORDER = \"abcdefg\";\nconst segmentRows = SEGMENT_ORDER.split(\"\").map((s) => `vec4<f32>(${SEGMENTS[s]!.map((x) => x.toFixed(2)).join(\", \")})`).join(\", \");\nconst digitMasks = DIGIT_SEGMENTS.map((segments) => segments.split(\"\").reduce((mask, s) => mask | (1 << SEGMENT_ORDER.indexOf(s)), 0)).map((m) => m + \"u\").join(\", \");\n\nconst COMMON_TEMPLATE = /* wgsl */ `\nconst E: f32 = 1.1920928955078125e-7;\nconst DW: f32 = 9.094947017729282e-13;\nconst TINY: f32 = 7.52316384526264e-37;\nconst SAFETY: f32 = 4.0;\nconst SLACK: f32 = 1e-14;\nconst PI: f32 = 3.14159265358979;\nconst HALF_PI: f32 = 1.5707963267948966;\nconst QUARTER_PI: f32 = 0.7853981633974483;\n\nstruct D { h: f32, l: f32 }\nstruct B { v: f32, e: f32 }\n\nvar<private> flagged: bool;\n\n// --- double-float32 (evaluation order matters; the host probes it) ---\n// The rounding barrier (2.11.0): ob(x) is x, its bits XORed with a word the host sets to 0 (a uniform), so a\n// compiler cannot know it is x. Every operation of the error-free transforms is wrapped, so none can be fused with or\n// reassociated across its neighbours (W3C WGSL § 15.7.5 permits both; Metal's compiler simplifies (a + b) - a to b, the M5 Pro\n// probe of October 27, 2026), and each rounds once, as written. On a device that already evaluates as written it changes nothing.\nfn ob(x: f32) -> f32 { return bitcast<f32>(bitcast<u32>(x) ^ BARRIER_ZERO); }\nfn two_sum(a: f32, b: f32) -> D { let s = ob(a + b); let bb = ob(s - a); let e = ob(ob(a - ob(s - bb)) + ob(b - bb)); return D(s, e); }\nfn quick_two_sum(a: f32, b: f32) -> D { let s = ob(a + b); let e = ob(b - ob(s - a)); return D(s, e); }\n// The Dekker product path: Veltkamp's split and Dekker's TwoProduct, exact only when every + - x is evaluated unfused as written.\nfn split(a: f32) -> D { let t = ob(4097.0 * a); let h = ob(t - ob(t - a)); return D(h, ob(a - h)); }\nfn two_prod_dekker(a: f32, b: f32) -> D {\n let p = ob(a * b); let sa = split(a); let sb = split(b);\n let e = ob(ob(ob(ob(ob(sa.h * sb.h) - p) + ob(sa.h * sb.l)) + ob(sa.l * sb.h)) + ob(sa.l * sb.l));\n return D(p, e);\n}\nfn d_mul_dekker(a: D, b: D) -> D {\n let p = two_prod_dekker(a.h, b.h);\n let t = ob(ob(a.h * b.l) + ob(a.l * b.h));\n let l = ob(p.l + t);\n return quick_two_sum(p.h, l);\n}\n// The fma product path, for a device whose fma is fused (the probe checks it): TwoProduct as fma(a, b, -p), and a double-word\n// product in which no rounded product is an operand of an addition (the hi part is fma(a.h, b.h, t), its error fma(a.h, b.h, -s)),\n// so a contraction of a*b + c into one rounding cannot change it; only t, an ordinary bounded sum, may be contracted.\nfn two_prod_fma(a: f32, b: f32) -> D { let p = ob(a * b); return D(p, ob(fma(a, b, -p))); }\nfn d_mul_fma(a: D, b: D) -> D {\n let t = ob(ob(a.h * b.l) + ob(a.l * b.h));\n let s = ob(fma(a.h, b.h, t));\n return D(s, ob(ob(fma(a.h, b.h, -s)) + t));\n}\nfn d_add(a: D, b: D) -> D {\n let s = two_sum(a.h, b.h); let t = two_sum(a.l, b.l);\n let c = ob(s.l + t.h); let v = quick_two_sum(s.h, c);\n let w = ob(v.l + t.l); return quick_two_sum(v.h, w);\n}\nfn d_neg(a: D) -> D { return D(-a.h, -a.l); }\nfn d_sub(a: D, b: D) -> D { return d_add(a, d_neg(b)); }\nfn d_mul(a: D, b: D) -> D { return d_mul_PRODUCT_PATH(a, b); }\n// A double-float32 result as a bounded value: its high part, with the low part and the declared 2^-40 error, relative to the\n// result and to \\`scale\\`, the magnitude of the operands it was formed from (a difference of nearly equal terms keeps their error).\nfn bds(a: D, scale: f32) -> B { return B(a.h, abs(a.l) + DW * (abs(a.h) + abs(a.l) + scale) + TINY); }\nfn bd(a: D) -> B { return bds(a, 0.0); }\n\n// --- bound arithmetic (each value rounds once, as written: the barrier keeps a compiler from regrouping them) ---\nfn bc(c: f32) -> B { return B(c, E * abs(c) + TINY); }\nfn bx(c: f32) -> B { return B(c, 0.0); }\nfn badd(a: B, b: B) -> B { let v = ob(a.v + b.v); return B(v, a.e + b.e + E * abs(v)); }\nfn bsub(a: B, b: B) -> B { let v = ob(a.v - b.v); return B(v, a.e + b.e + E * abs(v)); }\nfn bmul(a: B, b: B) -> B { let v = ob(a.v * b.v); return B(v, abs(a.v) * b.e + abs(b.v) * a.e + a.e * b.e + E * abs(v)); }\nfn bneg(a: B) -> B { return B(-a.v, a.e); }\nfn babs(a: B) -> B { return B(abs(a.v), a.e); }\n// WGSL division: 2.5 ULP (at most 5u relative) for a denominator within the normal range; a denominator within its bound of 0 flags.\nfn bdiv(a: B, b: B) -> B {\n let v = ob(a.v / b.v);\n let d = abs(b.v) - SAFETY * b.e;\n if (!(d > 0.0)) { flagged = true; return B(v, 3.0e38); }\n let e = (a.e + (abs(a.v) + a.e) * b.e / d) / abs(b.v) + 5.0 * E * abs(v);\n return B(v, e);\n}\nfn margin(x: B) -> f32 { return SAFETY * x.e + SLACK * (abs(x.v) + 1.0); }\n// x >= 0 for the exact value; flags when the sign is not decided by the bound.\nfn ge0(x: B) -> bool { if (!(abs(x.v) > margin(x))) { flagged = true; } return x.v >= 0.0; }\nfn ge(a: B, b: B) -> bool { return ge0(bsub(a, b)); }\nfn gt(a: B, b: B) -> bool { return !ge0(bsub(b, a)); }\nfn lt(a: B, b: B) -> bool { return !ge0(bsub(a, b)); }\nfn le(a: B, b: B) -> bool { return ge0(bsub(b, a)); }\n// floor of the exact value; flags when x lies within its margin of an integer in [lo, hi] (or when the margin is a half or more).\nfn bfloor(x: B, lo: f32, hi: f32) -> f32 {\n let n = floor(x.v); let m = round(x.v); let g = margin(x);\n if (g >= 0.5) { flagged = true; return n; }\n if (m >= lo && m <= hi && !(abs(x.v - m) > g)) { flagged = true; }\n return n;\n}\nfn parity_even(i: i32, j: i32) -> bool { return ((i + j) & 1) == 0; }\n`;\n\n/** The product path of the double-float32 arithmetic: Dekker's (unfused evaluation as written) or the fma one (a fused `fma`). */\nexport type ChartCompositeProductPath = \"dekker\" | \"fma\";\n/** `zero`: the WGSL expression of the host's zero word the rounding barrier reads (a uniform the compiler cannot see). */\nconst commonWgsl = (path: ChartCompositeProductPath, zero: string): string => COMMON_TEMPLATE.replace(\"d_mul_PRODUCT_PATH\", \"d_mul_\" + path).replaceAll(\"BARRIER_ZERO\", zero);\n\n/** The composite kernel compiled with one product path (`d_mul`): the probe's choice for the device. */\nexport function chartCompositeWgsl(path: ChartCompositeProductPath): string {\n return /* wgsl */ `${commonWgsl(path, \"params.zero\")}\n// zero: 0, set by the host; the rounding barrier's word.\nstruct Params { count: u32, columns: u32, wavelength_mode: u32, zero: u32 };\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> points: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> table: array<f32>;\n@group(0) @binding(3) var<storage, read> af: array<f32>;\n@group(0) @binding(4) var<storage, read> ai: array<u32>;\n@group(0) @binding(5) var<storage, read> selectors: array<u32>;\n@group(0) @binding(6) var<storage, read_write> values: array<f32>;\n@group(0) @binding(7) var<storage, read_write> flags: array<u32>;\n\nconst SEGMENT_BOX = array<vec4<f32>, 7>(${segmentRows});\nconst DIGIT_MASK = array<u32, 10>(${digitMasks});\nconst NUMERAL_ADVANCE: f32 = ${NUMERAL_ADVANCE};\n\n// --- Taylor cos and sin on [0, pi/4 + eps] in bound arithmetic (remainders 4e-13 and 2e-14) ---\nfn bcos_poly(y: B) -> B {\n let t = bmul(y, y);\n var p = bc(2.08767569878681e-9);\n p = badd(bmul(p, t), bc(-2.755731922398589e-7));\n p = badd(bmul(p, t), bc(2.48015873015873e-5));\n p = badd(bmul(p, t), bc(-0.001388888888888889));\n p = badd(bmul(p, t), bc(0.041666666666666664));\n p = badd(bmul(p, t), bc(-0.5));\n p = badd(bmul(p, t), bc(1.0));\n return B(p.v, p.e + 4.0e-13);\n}\nfn bsin_poly(z: B) -> B {\n let t = bmul(z, z);\n var p = bc(1.605904383682161e-10);\n p = badd(bmul(p, t), bc(-2.505210838544172e-8));\n p = badd(bmul(p, t), bc(2.7557319223985893e-6));\n p = badd(bmul(p, t), bc(-0.0001984126984126984));\n p = badd(bmul(p, t), bc(0.008333333333333333));\n p = badd(bmul(p, t), bc(-0.16666666666666666));\n p = badd(bmul(p, t), bc(1.0));\n let s = bmul(p, z);\n return B(s.v, s.e + 2.0e-14);\n}\n// cos(x) for |x| <= pi + eps: fold to [0, pi/2], then to [0, pi/4] through sin(pi/2 - y). The Taylor coefficients are rationals\n// rounded once to float32 (bc: u relative each).\nfn bcos(x: B) -> B {\n var y = babs(x);\n var negate = false;\n if (y.v > HALF_PI) { y = bsub(bc(PI), y); negate = true; }\n var c: B;\n if (y.v > QUARTER_PI) { c = bsin_poly(bsub(bc(HALF_PI), y)); } else { c = bcos_poly(y); }\n if (negate) { c = bneg(c); }\n return c;\n}\n\nfn bars_material(x: B, origin: f32, w: f32) -> u32 {\n let t = bdiv(bsub(x, bc(origin)), bc(w));\n let n = bfloor(t, -3.0e38, 3.0e38);\n return select(1u, 0u, parity_even(i32(n), 0));\n}\nfn numeral_ink(u: B, v: B, lu0: f32, lv0: f32, h: f32, digits: u32) -> bool {\n let x = bdiv(bsub(u, bc(lu0)), bc(h));\n let y = bdiv(bsub(v, bc(lv0)), bc(h));\n if (lt(y, bx(0.0)) || gt(y, bx(1.0)) || lt(x, bx(0.0))) { return false; }\n let count = ai[digits];\n let k = bfloor(bdiv(x, bc(NUMERAL_ADVANCE)), 0.0, f32(count));\n if (k >= f32(count)) { return false; }\n let cx = bsub(x, bmul(bx(k), bc(NUMERAL_ADVANCE)));\n let mask = DIGIT_MASK[ai[digits + 1u + u32(k)]];\n for (var s = 0u; s < 7u; s++) {\n if ((mask & (1u << s)) == 0u) { continue; }\n let box = SEGMENT_BOX[s];\n if (ge(cx, bc(box.x)) && le(cx, bc(box.z)) && ge(y, bc(box.y)) && le(y, bc(box.w))) { return true; }\n }\n return false;\n}\n\n// Sign of the cross product of boundary k's direction with the point: >= 0 when the point's angle is at or past the boundary.\nfn star_cross(dirs: u32, k: u32, x: D, y: D) -> B {\n let o = dirs + 4u * k;\n let cx = D(af[o], af[o + 1u]); let sx = D(af[o + 2u], af[o + 3u]);\n return bds(d_sub(d_mul(cx, y), d_mul(sx, x)), abs(cx.h * y.h) + abs(sx.h * x.h));\n}\n\nfn classify(kind: u32, ioff: u32, foff: u32, xd: D, yd: D) -> u32 {\n let u = bd(xd); let v = bd(yd);\n switch (kind) {\n case 0u: { return 0u; }\n case 1u: {\n let d = bsub(bmul(u, bc(af[foff])), bmul(v, bc(af[foff + 1u])));\n return select(0u, 1u, ge0(d));\n }\n case 2u: {\n let n = ai[ioff]; let dirs = ai[ioff + 1u]; let levels = ai[ioff + 2u]; let buckets = ai[ioff + 3u]; let m = ai[ioff + 4u]; let w = i32(ai[ioff + 5u]);\n let q = bd(d_add(d_mul(xd, xd), d_mul(yd, yd)));\n if (gt(q, bc(af[foff]))) { return ai[ioff + 6u]; }\n let a = atan2(yd.h, xd.h);\n let j = u32(clamp(floor((a + PI) * f32(m) / (2.0 * PI)), 0.0, f32(m - 1u)));\n let k0 = i32(ai[buckets + j]);\n var lo = k0 - w; var hi = k0 + w + 1;\n let ni = i32(n);\n let clo = star_cross(dirs, u32((lo % ni + ni) % ni), xd, yd);\n let chi = star_cross(dirs, u32((hi % ni + ni) % ni), xd, yd);\n if (!ge0(clo) || ge0(chi)) { flagged = true; return 0u; }\n for (var step = 0u; step < 32u && hi - lo > 1; step++) {\n let mid = (lo + hi) / 2;\n let c = star_cross(dirs, u32((mid % ni + ni) % ni), xd, yd);\n if (c.v >= 0.0) { lo = mid; } else { hi = mid; }\n }\n if (hi - lo > 1) { flagged = true; }\n let kl = u32((lo % ni + ni) % ni); let kh = u32((hi % ni + ni) % ni);\n if (!ge0(star_cross(dirs, kl, xd, yd)) || ge0(star_cross(dirs, kh, xd, yd))) { flagged = true; }\n return ai[levels + kl];\n }\n case 3u: {\n let steps = f32(ai[ioff]);\n let t = bmul(bmul(badd(u, bx(1.0)), bx(0.5)), bx(steps));\n return u32(clamp(bfloor(t, 1.0, steps - 1.0), 0.0, steps - 1.0));\n }\n case 5u: {\n let size = bc(af[foff]); let aspect = bc(af[foff + 1u]);\n let i = bfloor(bdiv(badd(u, bx(1.0)), size), -3.0e38, 3.0e38);\n let j = bfloor(bdiv(bsub(aspect, v), size), -3.0e38, 3.0e38);\n return select(0u, 1u, parity_even(i32(i), i32(j)));\n }\n case 6u: {\n let columns = ai[ioff]; let rows = ai[ioff + 1u]; let centers = ai[ioff + 2u];\n let i = u32(clamp(bfloor(bdiv(badd(u, bx(1.0)), bc(af[foff])), 1.0, f32(columns - 1u)), 0.0, f32(columns - 1u)));\n let j = u32(clamp(bfloor(bdiv(bsub(bc(af[foff + 2u]), v), bc(af[foff + 1u])), 1.0, f32(rows - 1u)), 0.0, f32(rows - 1u)));\n let c = centers + 2u * (j * columns + i);\n let dx = bsub(u, bc(af[c])); let dy = bsub(v, bc(af[c + 1u]));\n let d2 = badd(bmul(dx, dx), bmul(dy, dy));\n return select(0u, 1u, le(d2, bc(af[foff + 3u])));\n }\n case 7u: {\n let columns = ai[ioff]; let rows = ai[ioff + 1u]; let count = ai[ioff + 2u]; let background = ai[ioff + 3u];\n let fx = bdiv(badd(u, bx(1.0)), bc(af[foff])); let fy = bdiv(bsub(bc(af[foff + 2u]), v), bc(af[foff + 1u]));\n let i = bfloor(fx, -3.0e38, 3.0e38); let j = bfloor(fy, -3.0e38, 3.0e38);\n if (i < 0.0 || j < 0.0 || i >= f32(columns) || j >= f32(rows)) { return background; }\n let index = u32(j) * columns + u32(i);\n let x = bsub(fx, bx(i)); let y = bsub(fy, bx(j));\n let g0 = bc(af[foff + 3u]); let g1 = bc(af[foff + 4u]);\n if (index >= count || lt(x, g0) || gt(x, g1) || lt(y, g0) || gt(y, g1)) { return background; }\n return index;\n }\n case 8u: {\n let nx = ai[ioff]; let ny = ai[ioff + 1u]; let csr = ai[ioff + 2u]; let entries = ai[ioff + 3u]; let disks = ai[ioff + 4u]; let mean = ai[ioff + 5u];\n let cell = bc(af[foff + 2u]);\n let i = bfloor(bdiv(bsub(u, bc(af[foff])), cell), -3.0e38, 3.0e38);\n let j = bfloor(bdiv(bsub(v, bc(af[foff + 1u])), cell), -3.0e38, 3.0e38);\n if (i < 0.0 || j < 0.0 || i >= f32(nx) || j >= f32(ny)) { return mean; }\n let c = u32(j) * nx + u32(i);\n let start = ai[csr + c]; let end = ai[csr + c + 1u];\n for (var e = start; e < end; e++) {\n let d = disks + 4u * ai[entries + e];\n let dx = bsub(u, bc(af[d])); let dy = bsub(v, bc(af[d + 1u]));\n let d2 = badd(bmul(dx, dx), bmul(dy, dy));\n if (le(d2, bc(af[d + 2u]))) { return u32(af[d + 3u]); }\n }\n return mean;\n }\n case 9u: {\n let groups = ai[ioff]; let gf = ai[ioff + 1u]; let digits = ai[ioff + 2u];\n for (var g = 0u; g < groups; g++) {\n let o = gf + ${CHART_RECORD_STRIDE.barGroupFloats}u * g;\n if (lt(u, bc(af[o])) || gt(u, bc(af[o + 1u]))) { continue; }\n if (ge(u, bc(af[o + 2u])) && lt(u, bc(af[o + 4u])) && ge(v, bc(af[o + 3u])) && le(v, bc(af[o + 5u]))) { return bars_material(u, af[o + 2u], af[o + 10u]); }\n if (ge(u, bc(af[o + 6u])) && le(u, bc(af[o + 8u])) && gt(v, bc(af[o + 7u])) && le(v, bc(af[o + 9u]))) {\n let t = bdiv(bsub(bc(af[o + 9u]), v), bc(af[o + 10u]));\n let n = bfloor(t, -3.0e38, 3.0e38);\n return select(1u, 0u, parity_even(i32(n), 0));\n }\n if (af[o + 11u] != 0.0 && numeral_ink(u, v, af[o + 12u], af[o + 13u], af[o + 14u], digits + u32(af[o + 15u]))) { return 0u; }\n return 1u;\n }\n return 1u;\n }\n case 10u: {\n let horizontal = ai[ioff] == 0u; let lines = f32(ai[ioff + 1u]);\n var s: B; var y: B;\n if (horizontal) { s = bmul(badd(u, bx(1.0)), bx(0.5)); y = v; } else { s = bdiv(badd(v, bc(af[foff])), bc(af[foff + 4u])); y = u; }\n if (lt(s, bx(0.0)) || gt(s, bx(1.0))) { return 1u; }\n let w = bdiv(bc(af[foff + 1u]), badd(bc(af[foff + 2u]), bmul(bc(af[foff + 3u]), s)));\n let half = bmul(w, bx((2.0 * lines - 1.0) * 0.5));\n if (lt(y, bneg(half)) || ge(y, half)) { return 1u; }\n let n = bfloor(bdiv(badd(y, half), w), -3.0e38, 3.0e38);\n return select(1u, 0u, parity_even(i32(n), 0));\n }\n case 11u: {\n let q = d_add(d_mul(xd, xd), d_mul(yd, yd));\n if (gt(bd(q), bc(af[foff]))) { return 1024u; }\n let psi = d_mul(q, D(af[foff + 1u], af[foff + 2u]));\n if (!(abs(psi.h) < 1048576.0)) { flagged = true; return 0u; }\n let n = 2.0 * round(psi.h * 0.5);\n let r = (psi.h - n) + psi.l;\n let phase = B(r, DW * (abs(psi.h) + abs(psi.l)) + E * abs(r) + TINY);\n let c = bcos(bmul(bc(PI), phase));\n let t = bmul(bx(512.0), badd(bx(1.0), c));\n return u32(clamp(bfloor(t, 1.0, 1023.0), 0.0, 1023.0));\n }\n case 12u: {\n let columns = ai[ioff]; let rows = ai[ioff + 1u];\n let cellW = bc(af[foff]); let cellH = bc(af[foff + 1u]); let aspect = bc(af[foff + 2u]);\n let i = clamp(bfloor(bdiv(badd(u, bx(1.0)), cellW), 1.0, f32(columns - 1u)), 0.0, f32(columns - 1u));\n let j = clamp(bfloor(bdiv(bsub(aspect, v), cellH), 1.0, f32(rows - 1u)), 0.0, f32(rows - 1u));\n let du = bsub(u, badd(bx(-1.0), bmul(bx(i + 0.5), cellW)));\n let dv = bsub(v, bsub(aspect, bmul(bx(j + 0.5), cellH)));\n let c = bc(af[foff + 3u]); let s = bc(af[foff + 4u]); let half = bc(af[foff + 5u]);\n let x = bsub(bmul(du, c), bmul(dv, s)); let y = badd(bmul(du, s), bmul(dv, c));\n return select(1u, 0u, le(babs(x), half) && le(babs(y), half));\n }\n case 13u: {\n let spacing = bc(af[foff]); let half = bc(af[foff + 1u]);\n let nu = round(u.v / spacing.v); let nv = round(v.v / spacing.v);\n let du = babs(bsub(u, bmul(spacing, bx(nu)))); let dv = babs(bsub(v, bmul(spacing, bx(nv))));\n return select(0u, 1u, le(du, half) || le(dv, half));\n }\n case 14u: {\n let inverted = ai[ioff] != 0u;\n let q = badd(bmul(u, u), bmul(v, v));\n if (gt(q, bc(af[foff]))) { return 2u; }\n if (gt(q, bc(af[foff + 1u]))) { return 0u; }\n let same = ge0(u) == ge0(v);\n return select(1u, 0u, same != inverted);\n }\n default: { flagged = true; return 0u; }\n }\n}\n\n// Whether the point (mask units, bounded) is inside the shape; a point outside the bounding box by more than its margin is not.\nfn shape_contains(shape: u32, x: B, y: B) -> bool {\n let kind = ai[shape]; let data = ai[shape + 1u]; let edges = ai[shape + 2u];\n let mx = margin(x); let my = margin(y);\n if (x.v + mx < af[data] || x.v - mx > af[data + 2u] || y.v + my < af[data + 1u] || y.v - my > af[data + 3u]) { return false; }\n if (kind == 1u) {\n let dx = bsub(x, bc(af[data + 4u])); let dy = bsub(y, bc(af[data + 5u]));\n let d2 = badd(bmul(dx, dx), bmul(dy, dy));\n return ge(d2, bc(af[data + 6u])) && le(d2, bc(af[data + 7u]));\n }\n var winding = 0;\n for (var k = 0u; k < edges; k++) {\n let o = data + 4u + 4u * k;\n let x0 = bc(af[o]); let y0 = bc(af[o + 1u]); let x1 = bc(af[o + 2u]); let y1 = bc(af[o + 3u]);\n if (ge(y, y0)) {\n if (gt(y1, y)) {\n let side = bsub(bmul(bsub(x1, x0), bsub(y, y0)), bmul(bsub(x, x0), bsub(y1, y0)));\n if (gt(side, bx(0.0))) { winding = winding + 1; }\n }\n } else if (le(y1, y)) {\n let side = bsub(bmul(bsub(x1, x0), bsub(y, y0)), bmul(bsub(x, x0), bsub(y1, y0)));\n if (lt(side, bx(0.0))) { winding = winding - 1; }\n }\n }\n return winding != 0;\n}\n\n// The material of a point: the topmost layer holding it, else the topmost element holding it, else the background.\nfn material(p: vec4<f32>) -> u32 {\n let ud = D(p.x, p.y); let vd = D(p.z, p.w);\n let u = bd(ud); let v = bd(vd);\n let layers = ai[1]; let hl = ai[3];\n for (var l = layers; l > 0u; l--) {\n let h = hl + ${CHART_RECORD_STRIDE.layer}u * (l - 1u);\n let f = ai[h + 6u];\n let u0 = bc(af[f]); let v0 = bc(af[f + 1u]); let u1 = bc(af[f + 2u]); let v1 = bc(af[f + 3u]);\n if (lt(u, u0) || gt(u, u1) || lt(v, v0) || gt(v, v1)) { continue; }\n let x = bmul(bsub(u, u0), bc(af[f + 4u])); let y = bmul(bsub(v1, v), bc(af[f + 5u]));\n let cellsX = ai[h]; let cellsY = ai[h + 1u];\n let i = u32(clamp(floor(x.v / af[f + 6u]), 0.0, f32(cellsX - 1u)));\n let j = u32(clamp(floor(y.v / af[f + 7u]), 0.0, f32(cellsY - 1u)));\n let cell = j * cellsX + i;\n let start = ai[ai[h + 2u] + cell]; let end = ai[ai[h + 2u] + cell + 1u];\n for (var e = start; e < end; e++) {\n let shape = ai[h + 4u] + ${CHART_RECORD_STRIDE.shape}u * ai[ai[h + 3u] + e];\n if (shape_contains(shape, x, y)) { return ai[h + 8u]; }\n }\n return ai[h + 7u];\n }\n let elements = ai[0]; let he = ai[2];\n for (var i = elements; i > 0u; i--) {\n let h = he + ${CHART_RECORD_STRIDE.element}u * (i - 1u);\n let kind = ai[h]; let offset = ai[h + 1u]; let ioff = ai[h + 2u]; let foff = ai[h + 3u]; let placement = ai[h + 4u];\n if (placement == ${CHART_PLACEMENT.frame}u) { return offset + classify(kind, ioff, foff + ${CHART_RECORD_STRIDE.placementFloats}u, ud, vd); }\n let du = d_sub(ud, D(af[foff], af[foff + 1u])); let dv = d_sub(vd, D(af[foff + 2u], af[foff + 3u]));\n let hw = bc(af[foff + 4u]); let hh = bc(af[foff + 5u]);\n let bu = bds(du, abs(ud.h) + abs(af[foff])); let bv = bds(dv, abs(vd.h) + abs(af[foff + 2u]));\n if (lt(bu, bneg(hw)) || gt(bu, hw) || lt(bv, bneg(hh)) || gt(bv, hh)) { continue; }\n if (placement == ${CHART_PLACEMENT.ellipse}u) {\n let a = bdiv(bu, hw); let b = bdiv(bv, hh);\n if (gt(badd(bmul(a, a), bmul(b, b)), bx(1.0))) { continue; }\n }\n let scale = D(af[foff + 6u], af[foff + 7u]);\n return offset + classify(kind, ioff, foff + ${CHART_RECORD_STRIDE.placementFloats}u, d_mul(du, scale), d_mul(dv, scale));\n }\n flagged = true;\n return 0u;\n}\n\n@compute @workgroup_size(${CHART_COMPOSITE_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 * ${CHART_COMPOSITE_WORKGROUP_SIZE}u;\n if (i >= params.count) { return; }\n flagged = false;\n let m = material(points[i]);\n if (params.wavelength_mode != 0u) {\n values[i] = table[m * params.columns + selectors[i]];\n } else {\n for (var c = 0u; c < params.columns; c++) { values[i * params.columns + c] = table[m * params.columns + c]; }\n }\n flags[i] = select(0u, 1u, flagged);\n}\n`;\n}\n/** The composite kernel with the Dekker product path (unchanged since #477). */\nexport const CHART_COMPOSITE_WGSL = chartCompositeWgsl(\"dekker\");\n\n/**\n * Probe of the double-float32 building blocks on the compiled device, both product paths: for each input (x, y, z, w) it writes\n * TwoSum(x, y), the Dekker and the fma TwoProduct(x, y) low parts, the double-word sum (x, y) + (z, w) low part, and both paths'\n * double-word products (x, y)·(z, w), hi and lo. The host (`classifyChartCompositeProbe`) compares them with the exact values and\n * with the round-to-nearest evaluation in the written order, names the components that differ, classifies the device and picks\n * the product path the composite kernel is compiled with. The functions are the kernel's own (`COMMON_TEMPLATE`).\n */\nexport const CHART_COMPOSITE_PROBE_WGSL = /* wgsl */ `${commonWgsl(\"dekker\", \"probe_zero.x\")}\n@group(0) @binding(0) var<storage, read> input: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read_write> output: array<vec4<f32>>;\n// x: 0, set by the host; the rounding barrier's word.\n@group(0) @binding(2) var<uniform> probe_zero: vec4<u32>;\n@compute @workgroup_size(1)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= arrayLength(&input)) { return; }\n let p = input[i];\n let s = two_sum(p.x, p.y);\n let m = two_prod_dekker(p.x, p.y);\n let a = d_add(D(p.x, p.y), D(p.z, p.w));\n let mf = two_prod_fma(p.x, p.y);\n let q = d_mul_dekker(D(p.x, p.y), D(p.z, p.w));\n let qf = d_mul_fma(D(p.x, p.y), D(p.z, p.w));\n output[2u * i] = vec4<f32>(s.l, m.l, a.l, mf.l);\n output[2u * i + 1u] = vec4<f32>(q.h, q.l, qf.h, qf.l);\n}\n`;\n/** Floats the probe writes per input (two vec4). */\nexport const CHART_COMPOSITE_PROBE_OUTPUTS = 8;\nexport const CHART_COMPOSITE_PROBE_LABEL = \"photivra-chart-composite-probe\";\n/** Pipeline label of the composite kernel per product path (the pipeline caches key on it). */\nexport function chartCompositeLabel(path: ChartCompositeProductPath): string { return path === \"dekker\" ? \"photivra-chart-composite\" : \"photivra-chart-composite-fma\"; }\nexport const CHART_COMPOSITE_LABEL = chartCompositeLabel(\"dekker\");\n"]}
1
+ {"version":3,"file":"wgsl-chart-composite-kernel.js","sourceRoot":"","sources":["../../src/gpu/wgsl-chart-composite-kernel.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * WGSL kernel of the `webgpu-f32` chart-radiance stage for composite charts, the 2.3 chart elements and\n * declared `material-layers` scenes (#477), one invocation per query point. It resolves the point to a mask\n * layer or a composite element, classifies the material there with the element's own geometry (the float64\n * constants `resolveChartScene` decided with, uploaded once per prepared scene), and reads the material's\n * value from the host's float64 table. The eight 2.0 presets as plain charts keep their own kernel\n * (wgsl-chart-kernel.ts); as composite elements they run here.\n *\n * **Bound arithmetic.** Every float32 value carries an a-priori bound on its distance from the exact\n * value of the same expression: `B(v, e)` with |v − exact| ≤ e. A constant the host rounded from float64\n * carries E·|c| (E = 2^-23: one WGSL rounding of `+ − ×`, which the spec lets fall in either direction,\n * W3C WGSL \"Floating Point Accuracy\"); `+ − ×` add E·|v| each; `/` adds the WGSL 2.5-ULP bound (2.5·E·|v|);\n * a point carries its low float32 part (its float64 coordinate to 2^-48). A decision (sign, floor,\n * comparison) whose value lies within SAFETY (4) times its bound, plus 10^-14 relative slack for the\n * reference's own float64 rounding, flags the point, and the host recomputes it with the reference. So\n * every unflagged point has exactly the reference's material.\n *\n * **Double-float32 for the chirps.** The zone plate's phase k·r² and the sine star's angle would be decided\n * to about 2^-24 relative in plain float32, which at hundreds of cycles is wider than a 1024-level step\n * and would flag most points. Points are therefore uploaded as float32 pairs (hi + lo, the float64 point to\n * 2^-48), and the element frames, r², k·r² and the star's cross products are evaluated in double-float32\n * (TwoSum, Veltkamp split and Dekker TwoProduct; Joldes, Muller & Popescu, \"Tight and rigorous error\n * bounds for basic building blocks of double-word arithmetic\", ACM TOMS 44(2), 2017, free author copy on\n * HAL: each operation is within a few u² relative), with a declared bound of 2^-40 relative per result.\n * These algorithms need the written evaluation order and round-to-nearest `+ − ×`, and WGSL lets an\n * implementation reassociate and fuse (W3C WGSL § 15.7.5 \"Reassociation and Fusion\": \"An implementation may fuse\n * operations if the transformed expression is at least as accurate as the original formulation\"), round in either\n * direction (§ 15.7.4), and evaluate `fma` unfused (§ 15.7.2, § 17.5.32). WGSL has no way to forbid a fusion, so\n * the host probes the compiled building blocks before using them (`CHART_COMPOSITE_PROBE_WGSL`) with inputs whose\n * low parts are non-zero and whose sums and products round up or down to the nearest float32.\n *\n * **Two product paths (2.11.0).** Dekker's TwoProduct and the final Fast2Sum of the double-word product depend on\n * every rounded product being rounded before it is added: a fusion of `a*b - p`, of Veltkamp's `t - (t - a)` with\n * t = 4097·a, or of `p.h + l` with p.h = a.h·b.h changes them (the last adds the product's error twice). The fma path\n * (`two_prod_fma`, `d_mul_fma`) never adds a rounded product: TwoProduct is fma(a, b, −p) (exact for a fused fma;\n * Joldes, Muller & Popescu 2017, Algorithm 2), and the double-word product forms its hi part as fma(a.h, b.h, t) and\n * its error as fma(a.h, b.h, −s) + t, where only t = a.h·b.l + a.l·b.h, an ordinary bounded sum, may be fused. It\n * is within 9u² (first order) of the exact product (docs/WEBGPU.md), inside the declared 2^-40. The probe runs both\n * paths; the kernel is compiled with Dekker's when it passes (so an unfused device keeps its results bit for bit),\n * else with the fma one when it passes, else the device is refused. TwoSum, Fast2Sum and the double-word sum hold no\n * product and so cannot be fused; the zone plate's phase reduction subtracts 2·round(psi.h/2), an exact product, so\n * a fusion leaves it unchanged; the star's cross products and the lattice-point recovery are double-word products\n * and sums. The bound arithmetic stays valid under fusion: a fused multiply-add rounds once, so its result is\n * within E·|v| of the exact value of the same expression, the term `+` already adds (the product's own E·|v| term\n * is then a bound on a rounding that did not happen).\n *\n * **The rounding barrier (2.11.0).** Metal's compiler reassociated the error-free transforms on an Apple M5 Pro (the probe\n * of October 27, 2026: class `reassociated`, every differing low part zero, the value (a + b) − a → b gives), which neither\n * product path survives. Every operation of the transforms, and every value of the bound arithmetic, is therefore wrapped in\n * `ob`: the result's bits XORed with a word the host sets to 0 (a uniform: `params.zero`, `lattice.zero`, the probe's\n * `probe_zero`), which a compiler cannot see through, so it can neither regroup nor fuse across it and each operation rounds\n * once, as written. On a device that already evaluates as written it changes no value. On the M5 Pro the probe then reports\n * `exact` on the Dekker path (docs/WEBGPU.md). The probe still decides on every device.\n *\n * **No `sin`, `cos` or `atan2` decides anything.** The zone plate reduces its phase to [−1, 1] cycles\n * exactly (Sterbenz) and evaluates cos by quadrant reduction and a Taylor polynomial in bound arithmetic\n * (truncation ≤ 4·10^-13 on [0, π/4 + ε], the Lagrange remainder). A Siemens star (bar or sine profile) is\n * a table of its sector boundary directions, with the level of each sector read from the reference itself;\n * the sector is found by a rough `atan2` and a bucket table, then decided exactly by double-float32 cross\n * products with the two bracketing boundary directions (a point the rough guess misplaces beyond the\n * search window is flagged, never misclassified). See docs/WEBGPU.md, \"Scene radiance on the GPU\".\n */\n\nimport { DIGIT_SEGMENTS, NUMERAL_ADVANCE, SEGMENTS } from \"../facade/chart-scenes.js\";\n\nexport const CHART_COMPOSITE_WORKGROUP_SIZE = 256;\n\n/** Element kind codes of the record headers. */\nexport const CHART_ELEMENT_KIND = /* @__PURE__ */ Object.freeze({ uniform: 0, \"slanted-edge\": 1, \"siemens-star\": 2, \"step-wedge\": 3, checkerboard: 5, \"point-grid\": 6, patches: 7,\n \"dead-leaves\": 8, \"bar-groups\": 9, \"hyperbolic-wedge\": 10, \"zone-plate\": 11, \"slanted-squares\": 12, \"line-grid\": 13, fiducial: 14 } as const);\n/** Placement modes of an element record: the background fills the frame; elements are rectangles or ellipses. */\nexport const CHART_PLACEMENT = /* @__PURE__ */ Object.freeze({ frame: 0, rectangle: 1, ellipse: 2 } as const);\n/** Words per element header, layer header and shape header in the u32 record array. */\nexport const CHART_RECORD_STRIDE = /* @__PURE__ */ Object.freeze({ element: 8, layer: 16, shape: 4, placementFloats: 10, barGroupFloats: 16 } as const);\n\nconst SEGMENT_ORDER = \"abcdefg\";\nconst segmentRows = SEGMENT_ORDER.split(\"\").map((s) => `vec4<f32>(${SEGMENTS[s]!.map((x) => x.toFixed(2)).join(\", \")})`).join(\", \");\nconst digitMasks = DIGIT_SEGMENTS.map((segments) => segments.split(\"\").reduce((mask, s) => mask | (1 << SEGMENT_ORDER.indexOf(s)), 0)).map((m) => m + \"u\").join(\", \");\n\nconst COMMON_TEMPLATE = /* wgsl */ `\nconst E: f32 = 1.1920928955078125e-7;\nconst DW: f32 = 9.094947017729282e-13;\nconst TINY: f32 = 7.52316384526264e-37;\nconst SAFETY: f32 = 4.0;\nconst SLACK: f32 = 1e-14;\nconst PI: f32 = 3.14159265358979;\nconst HALF_PI: f32 = 1.5707963267948966;\nconst QUARTER_PI: f32 = 0.7853981633974483;\n\nstruct D { h: f32, l: f32 }\nstruct B { v: f32, e: f32 }\n\nvar<private> flagged: bool;\n\n// --- double-float32 (evaluation order matters; the host probes it) ---\n// The rounding barrier (2.11.0): ob(x) is x, its bits XORed with a word the host sets to 0 (a uniform), so a\n// compiler cannot know it is x. Every operation of the error-free transforms is wrapped, so none can be fused with or\n// reassociated across its neighbours (W3C WGSL § 15.7.5 permits both; Metal's compiler simplifies (a + b) - a to b, the M5 Pro\n// probe of October 27, 2026), and each rounds once, as written. On a device that already evaluates as written it changes nothing.\nfn ob(x: f32) -> f32 { return bitcast<f32>(bitcast<u32>(x) ^ BARRIER_ZERO); }\nfn two_sum(a: f32, b: f32) -> D { let s = ob(a + b); let bb = ob(s - a); let e = ob(ob(a - ob(s - bb)) + ob(b - bb)); return D(s, e); }\nfn quick_two_sum(a: f32, b: f32) -> D { let s = ob(a + b); let e = ob(b - ob(s - a)); return D(s, e); }\n// The Dekker product path: Veltkamp's split and Dekker's TwoProduct, exact only when every + - x is evaluated unfused as written.\nfn split(a: f32) -> D { let t = ob(4097.0 * a); let h = ob(t - ob(t - a)); return D(h, ob(a - h)); }\nfn two_prod_dekker(a: f32, b: f32) -> D {\n let p = ob(a * b); let sa = split(a); let sb = split(b);\n let e = ob(ob(ob(ob(ob(sa.h * sb.h) - p) + ob(sa.h * sb.l)) + ob(sa.l * sb.h)) + ob(sa.l * sb.l));\n return D(p, e);\n}\nfn d_mul_dekker(a: D, b: D) -> D {\n let p = two_prod_dekker(a.h, b.h);\n let t = ob(ob(a.h * b.l) + ob(a.l * b.h));\n let l = ob(p.l + t);\n return quick_two_sum(p.h, l);\n}\n// The fma product path, for a device whose fma is fused (the probe checks it): TwoProduct as fma(a, b, -p), and a double-word\n// product in which no rounded product is an operand of an addition (the hi part is fma(a.h, b.h, t), its error fma(a.h, b.h, -s)),\n// so a contraction of a*b + c into one rounding cannot change it; only t, an ordinary bounded sum, may be contracted.\nfn two_prod_fma(a: f32, b: f32) -> D { let p = ob(a * b); return D(p, ob(fma(a, b, -p))); }\nfn d_mul_fma(a: D, b: D) -> D {\n let t = ob(ob(a.h * b.l) + ob(a.l * b.h));\n let s = ob(fma(a.h, b.h, t));\n return D(s, ob(ob(fma(a.h, b.h, -s)) + t));\n}\nfn d_add(a: D, b: D) -> D {\n let s = two_sum(a.h, b.h); let t = two_sum(a.l, b.l);\n let c = ob(s.l + t.h); let v = quick_two_sum(s.h, c);\n let w = ob(v.l + t.l); return quick_two_sum(v.h, w);\n}\nfn d_neg(a: D) -> D { return D(-a.h, -a.l); }\nfn d_sub(a: D, b: D) -> D { return d_add(a, d_neg(b)); }\nfn d_mul(a: D, b: D) -> D { return d_mul_PRODUCT_PATH(a, b); }\n// A double-float32 result as a bounded value: its high part, with the low part and the declared 2^-40 error, relative to the\n// result and to \\`scale\\`, the magnitude of the operands it was formed from (a difference of nearly equal terms keeps their error).\nfn bds(a: D, scale: f32) -> B { return B(a.h, abs(a.l) + DW * (abs(a.h) + abs(a.l) + scale) + TINY); }\nfn bd(a: D) -> B { return bds(a, 0.0); }\n\n// --- bound arithmetic (each value rounds once, as written: the barrier keeps a compiler from regrouping them) ---\nfn bc(c: f32) -> B { return B(c, E * abs(c) + TINY); }\nfn bx(c: f32) -> B { return B(c, 0.0); }\nfn badd(a: B, b: B) -> B { let v = ob(a.v + b.v); return B(v, a.e + b.e + E * abs(v)); }\nfn bsub(a: B, b: B) -> B { let v = ob(a.v - b.v); return B(v, a.e + b.e + E * abs(v)); }\nfn bmul(a: B, b: B) -> B { let v = ob(a.v * b.v); return B(v, abs(a.v) * b.e + abs(b.v) * a.e + a.e * b.e + E * abs(v)); }\nfn bneg(a: B) -> B { return B(-a.v, a.e); }\nfn babs(a: B) -> B { return B(abs(a.v), a.e); }\n// WGSL division: 2.5 ULP (at most 5u relative) for a denominator within the normal range; a denominator within its bound of 0 flags.\nfn bdiv(a: B, b: B) -> B {\n let v = ob(a.v / b.v);\n let d = abs(b.v) - SAFETY * b.e;\n if (!(d > 0.0)) { flagged = true; return B(v, 3.0e38); }\n let e = (a.e + (abs(a.v) + a.e) * b.e / d) / abs(b.v) + 5.0 * E * abs(v);\n return B(v, e);\n}\nfn margin(x: B) -> f32 { return SAFETY * x.e + SLACK * (abs(x.v) + 1.0); }\n// x >= 0 for the exact value; flags when the sign is not decided by the bound.\nfn ge0(x: B) -> bool { if (!(abs(x.v) > margin(x))) { flagged = true; } return x.v >= 0.0; }\nfn ge(a: B, b: B) -> bool { return ge0(bsub(a, b)); }\nfn gt(a: B, b: B) -> bool { return !ge0(bsub(b, a)); }\nfn lt(a: B, b: B) -> bool { return !ge0(bsub(a, b)); }\nfn le(a: B, b: B) -> bool { return ge0(bsub(b, a)); }\n// floor of the exact value; flags when x lies within its margin of an integer in [lo, hi] (or when the margin is a half or more).\nfn bfloor(x: B, lo: f32, hi: f32) -> f32 {\n let n = floor(x.v); let m = round(x.v); let g = margin(x);\n if (g >= 0.5) { flagged = true; return n; }\n if (m >= lo && m <= hi && !(abs(x.v - m) > g)) { flagged = true; }\n return n;\n}\nfn parity_even(i: i32, j: i32) -> bool { return ((i + j) & 1) == 0; }\n`;\n\n/** The product path of the double-float32 arithmetic: Dekker's (unfused evaluation as written) or the fma one (a fused `fma`). */\nexport type ChartCompositeProductPath = \"dekker\" | \"fma\";\n/** `zero`: the WGSL expression of the host's zero word the rounding barrier reads (a uniform the compiler cannot see). */\nconst commonWgsl = (path: ChartCompositeProductPath, zero: string): string => COMMON_TEMPLATE.replace(\"d_mul_PRODUCT_PATH\", \"d_mul_\" + path).replaceAll(\"BARRIER_ZERO\", zero);\n\n/** The composite kernel compiled with one product path (`d_mul`): the probe's choice for the device. */\nexport function chartCompositeWgsl(path: ChartCompositeProductPath): string {\n return /* wgsl */ `${commonWgsl(path, \"params.zero\")}\n// zero: 0, set by the host; the rounding barrier's word.\nstruct Params { count: u32, columns: u32, wavelength_mode: u32, zero: u32 };\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> points: array<vec4<f32>>;\n@group(0) @binding(2) var<storage, read> table: array<f32>;\n@group(0) @binding(3) var<storage, read> af: array<f32>;\n@group(0) @binding(4) var<storage, read> ai: array<u32>;\n@group(0) @binding(5) var<storage, read> selectors: array<u32>;\n@group(0) @binding(6) var<storage, read_write> values: array<f32>;\n@group(0) @binding(7) var<storage, read_write> flags: array<u32>;\n\nconst SEGMENT_BOX = array<vec4<f32>, 7>(${segmentRows});\nconst DIGIT_MASK = array<u32, 10>(${digitMasks});\nconst NUMERAL_ADVANCE: f32 = ${NUMERAL_ADVANCE};\n\n// --- Taylor cos and sin on [0, pi/4 + eps] in bound arithmetic (remainders 4e-13 and 2e-14) ---\nfn bcos_poly(y: B) -> B {\n let t = bmul(y, y);\n var p = bc(2.08767569878681e-9);\n p = badd(bmul(p, t), bc(-2.755731922398589e-7));\n p = badd(bmul(p, t), bc(2.48015873015873e-5));\n p = badd(bmul(p, t), bc(-0.001388888888888889));\n p = badd(bmul(p, t), bc(0.041666666666666664));\n p = badd(bmul(p, t), bc(-0.5));\n p = badd(bmul(p, t), bc(1.0));\n return B(p.v, p.e + 4.0e-13);\n}\nfn bsin_poly(z: B) -> B {\n let t = bmul(z, z);\n var p = bc(1.605904383682161e-10);\n p = badd(bmul(p, t), bc(-2.505210838544172e-8));\n p = badd(bmul(p, t), bc(2.7557319223985893e-6));\n p = badd(bmul(p, t), bc(-0.0001984126984126984));\n p = badd(bmul(p, t), bc(0.008333333333333333));\n p = badd(bmul(p, t), bc(-0.16666666666666666));\n p = badd(bmul(p, t), bc(1.0));\n let s = bmul(p, z);\n return B(s.v, s.e + 2.0e-14);\n}\n// cos(x) for |x| <= pi + eps: fold to [0, pi/2], then to [0, pi/4] through sin(pi/2 - y). The Taylor coefficients are rationals\n// rounded once to float32 (bc: u relative each).\nfn bcos(x: B) -> B {\n var y = babs(x);\n var negate = false;\n if (y.v > HALF_PI) { y = bsub(bc(PI), y); negate = true; }\n var c: B;\n if (y.v > QUARTER_PI) { c = bsin_poly(bsub(bc(HALF_PI), y)); } else { c = bcos_poly(y); }\n if (negate) { c = bneg(c); }\n return c;\n}\n\nfn bars_material(x: B, origin: f32, w: f32) -> u32 {\n let t = bdiv(bsub(x, bc(origin)), bc(w));\n let n = bfloor(t, -3.0e38, 3.0e38);\n return select(1u, 0u, parity_even(i32(n), 0));\n}\nfn numeral_ink(u: B, v: B, lu0: f32, lv0: f32, h: f32, digits: u32) -> bool {\n let x = bdiv(bsub(u, bc(lu0)), bc(h));\n let y = bdiv(bsub(v, bc(lv0)), bc(h));\n if (lt(y, bx(0.0)) || gt(y, bx(1.0)) || lt(x, bx(0.0))) { return false; }\n let count = ai[digits];\n let k = bfloor(bdiv(x, bc(NUMERAL_ADVANCE)), 0.0, f32(count));\n if (k >= f32(count)) { return false; }\n let cx = bsub(x, bmul(bx(k), bc(NUMERAL_ADVANCE)));\n let mask = DIGIT_MASK[ai[digits + 1u + u32(k)]];\n for (var s = 0u; s < 7u; s++) {\n if ((mask & (1u << s)) == 0u) { continue; }\n let box = SEGMENT_BOX[s];\n if (ge(cx, bc(box.x)) && le(cx, bc(box.z)) && ge(y, bc(box.y)) && le(y, bc(box.w))) { return true; }\n }\n return false;\n}\n\n// Sign of the cross product of boundary k's direction with the point: >= 0 when the point's angle is at or past the boundary.\nfn star_cross(dirs: u32, k: u32, x: D, y: D) -> B {\n let o = dirs + 4u * k;\n let cx = D(af[o], af[o + 1u]); let sx = D(af[o + 2u], af[o + 3u]);\n return bds(d_sub(d_mul(cx, y), d_mul(sx, x)), abs(cx.h * y.h) + abs(sx.h * x.h));\n}\n\nfn classify(kind: u32, ioff: u32, foff: u32, xd: D, yd: D) -> u32 {\n let u = bd(xd); let v = bd(yd);\n switch (kind) {\n case 0u: { return 0u; }\n case 1u: {\n let d = bsub(bmul(u, bc(af[foff])), bmul(v, bc(af[foff + 1u])));\n return select(0u, 1u, ge0(d));\n }\n case 2u: {\n let n = ai[ioff]; let dirs = ai[ioff + 1u]; let levels = ai[ioff + 2u]; let buckets = ai[ioff + 3u]; let m = ai[ioff + 4u]; let w = i32(ai[ioff + 5u]);\n let q = bd(d_add(d_mul(xd, xd), d_mul(yd, yd)));\n if (gt(q, bc(af[foff]))) { return ai[ioff + 6u]; }\n let a = atan2(yd.h, xd.h);\n let j = u32(clamp(floor((a + PI) * f32(m) / (2.0 * PI)), 0.0, f32(m - 1u)));\n let k0 = i32(ai[buckets + j]);\n var lo = k0 - w; var hi = k0 + w + 1;\n let ni = i32(n);\n let clo = star_cross(dirs, u32((lo % ni + ni) % ni), xd, yd);\n let chi = star_cross(dirs, u32((hi % ni + ni) % ni), xd, yd);\n if (!ge0(clo) || ge0(chi)) { flagged = true; return 0u; }\n for (var step = 0u; step < 32u && hi - lo > 1; step++) {\n let mid = (lo + hi) / 2;\n let c = star_cross(dirs, u32((mid % ni + ni) % ni), xd, yd);\n if (c.v >= 0.0) { lo = mid; } else { hi = mid; }\n }\n if (hi - lo > 1) { flagged = true; }\n let kl = u32((lo % ni + ni) % ni); let kh = u32((hi % ni + ni) % ni);\n if (!ge0(star_cross(dirs, kl, xd, yd)) || ge0(star_cross(dirs, kh, xd, yd))) { flagged = true; }\n return ai[levels + kl];\n }\n case 3u: {\n let steps = f32(ai[ioff]);\n let t = bmul(bmul(badd(u, bx(1.0)), bx(0.5)), bx(steps));\n return u32(clamp(bfloor(t, 1.0, steps - 1.0), 0.0, steps - 1.0));\n }\n case 5u: {\n let size = bc(af[foff]); let aspect = bc(af[foff + 1u]);\n let i = bfloor(bdiv(badd(u, bx(1.0)), size), -3.0e38, 3.0e38);\n let j = bfloor(bdiv(bsub(aspect, v), size), -3.0e38, 3.0e38);\n return select(0u, 1u, parity_even(i32(i), i32(j)));\n }\n case 6u: {\n let columns = ai[ioff]; let rows = ai[ioff + 1u]; let centers = ai[ioff + 2u];\n let i = u32(clamp(bfloor(bdiv(badd(u, bx(1.0)), bc(af[foff])), 1.0, f32(columns - 1u)), 0.0, f32(columns - 1u)));\n let j = u32(clamp(bfloor(bdiv(bsub(bc(af[foff + 2u]), v), bc(af[foff + 1u])), 1.0, f32(rows - 1u)), 0.0, f32(rows - 1u)));\n let c = centers + 2u * (j * columns + i);\n let dx = bsub(u, bc(af[c])); let dy = bsub(v, bc(af[c + 1u]));\n let d2 = badd(bmul(dx, dx), bmul(dy, dy));\n return select(0u, 1u, le(d2, bc(af[foff + 3u])));\n }\n case 7u: {\n let columns = ai[ioff]; let rows = ai[ioff + 1u]; let count = ai[ioff + 2u]; let background = ai[ioff + 3u];\n let fx = bdiv(badd(u, bx(1.0)), bc(af[foff])); let fy = bdiv(bsub(bc(af[foff + 2u]), v), bc(af[foff + 1u]));\n let i = bfloor(fx, -3.0e38, 3.0e38); let j = bfloor(fy, -3.0e38, 3.0e38);\n if (i < 0.0 || j < 0.0 || i >= f32(columns) || j >= f32(rows)) { return background; }\n let index = u32(j) * columns + u32(i);\n let x = bsub(fx, bx(i)); let y = bsub(fy, bx(j));\n let g0 = bc(af[foff + 3u]); let g1 = bc(af[foff + 4u]);\n if (index >= count || lt(x, g0) || gt(x, g1) || lt(y, g0) || gt(y, g1)) { return background; }\n return index;\n }\n case 8u: {\n let nx = ai[ioff]; let ny = ai[ioff + 1u]; let csr = ai[ioff + 2u]; let entries = ai[ioff + 3u]; let disks = ai[ioff + 4u]; let mean = ai[ioff + 5u];\n let cell = bc(af[foff + 2u]);\n let i = bfloor(bdiv(bsub(u, bc(af[foff])), cell), -3.0e38, 3.0e38);\n let j = bfloor(bdiv(bsub(v, bc(af[foff + 1u])), cell), -3.0e38, 3.0e38);\n if (i < 0.0 || j < 0.0 || i >= f32(nx) || j >= f32(ny)) { return mean; }\n let c = u32(j) * nx + u32(i);\n let start = ai[csr + c]; let end = ai[csr + c + 1u];\n for (var e = start; e < end; e++) {\n let d = disks + 4u * ai[entries + e];\n let dx = bsub(u, bc(af[d])); let dy = bsub(v, bc(af[d + 1u]));\n let d2 = badd(bmul(dx, dx), bmul(dy, dy));\n if (le(d2, bc(af[d + 2u]))) { return u32(af[d + 3u]); }\n }\n return mean;\n }\n case 9u: {\n let groups = ai[ioff]; let gf = ai[ioff + 1u]; let digits = ai[ioff + 2u];\n for (var g = 0u; g < groups; g++) {\n let o = gf + ${CHART_RECORD_STRIDE.barGroupFloats}u * g;\n if (lt(u, bc(af[o])) || gt(u, bc(af[o + 1u]))) { continue; }\n if (ge(u, bc(af[o + 2u])) && lt(u, bc(af[o + 4u])) && ge(v, bc(af[o + 3u])) && le(v, bc(af[o + 5u]))) { return bars_material(u, af[o + 2u], af[o + 10u]); }\n if (ge(u, bc(af[o + 6u])) && le(u, bc(af[o + 8u])) && gt(v, bc(af[o + 7u])) && le(v, bc(af[o + 9u]))) {\n let t = bdiv(bsub(bc(af[o + 9u]), v), bc(af[o + 10u]));\n let n = bfloor(t, -3.0e38, 3.0e38);\n return select(1u, 0u, parity_even(i32(n), 0));\n }\n if (af[o + 11u] != 0.0 && numeral_ink(u, v, af[o + 12u], af[o + 13u], af[o + 14u], digits + u32(af[o + 15u]))) { return 0u; }\n return 1u;\n }\n return 1u;\n }\n case 10u: {\n let horizontal = ai[ioff] == 0u; let lines = f32(ai[ioff + 1u]);\n var s: B; var y: B;\n if (horizontal) { s = bmul(badd(u, bx(1.0)), bx(0.5)); y = v; } else { s = bdiv(badd(v, bc(af[foff])), bc(af[foff + 4u])); y = u; }\n if (lt(s, bx(0.0)) || gt(s, bx(1.0))) { return 1u; }\n let w = bdiv(bc(af[foff + 1u]), badd(bc(af[foff + 2u]), bmul(bc(af[foff + 3u]), s)));\n let half = bmul(w, bx((2.0 * lines - 1.0) * 0.5));\n if (lt(y, bneg(half)) || ge(y, half)) { return 1u; }\n let n = bfloor(bdiv(badd(y, half), w), -3.0e38, 3.0e38);\n return select(1u, 0u, parity_even(i32(n), 0));\n }\n case 11u: {\n let q = d_add(d_mul(xd, xd), d_mul(yd, yd));\n if (gt(bd(q), bc(af[foff]))) { return 1024u; }\n let psi = d_mul(q, D(af[foff + 1u], af[foff + 2u]));\n if (!(abs(psi.h) < 1048576.0)) { flagged = true; return 0u; }\n let n = 2.0 * round(psi.h * 0.5);\n let r = (psi.h - n) + psi.l;\n let phase = B(r, DW * (abs(psi.h) + abs(psi.l)) + E * abs(r) + TINY);\n let c = bcos(bmul(bc(PI), phase));\n let t = bmul(bx(512.0), badd(bx(1.0), c));\n return u32(clamp(bfloor(t, 1.0, 1023.0), 0.0, 1023.0));\n }\n case 12u: {\n let columns = ai[ioff]; let rows = ai[ioff + 1u];\n let cellW = bc(af[foff]); let cellH = bc(af[foff + 1u]); let aspect = bc(af[foff + 2u]);\n let i = clamp(bfloor(bdiv(badd(u, bx(1.0)), cellW), 1.0, f32(columns - 1u)), 0.0, f32(columns - 1u));\n let j = clamp(bfloor(bdiv(bsub(aspect, v), cellH), 1.0, f32(rows - 1u)), 0.0, f32(rows - 1u));\n let du = bsub(u, badd(bx(-1.0), bmul(bx(i + 0.5), cellW)));\n let dv = bsub(v, bsub(aspect, bmul(bx(j + 0.5), cellH)));\n let c = bc(af[foff + 3u]); let s = bc(af[foff + 4u]); let half = bc(af[foff + 5u]);\n let x = bsub(bmul(du, c), bmul(dv, s)); let y = badd(bmul(du, s), bmul(dv, c));\n return select(1u, 0u, le(babs(x), half) && le(babs(y), half));\n }\n case 13u: {\n let spacing = bc(af[foff]); let half = bc(af[foff + 1u]);\n let nu = round(u.v / spacing.v); let nv = round(v.v / spacing.v);\n let du = babs(bsub(u, bmul(spacing, bx(nu)))); let dv = babs(bsub(v, bmul(spacing, bx(nv))));\n return select(0u, 1u, le(du, half) || le(dv, half));\n }\n case 14u: {\n let inverted = ai[ioff] != 0u;\n let q = badd(bmul(u, u), bmul(v, v));\n if (gt(q, bc(af[foff]))) { return 2u; }\n if (gt(q, bc(af[foff + 1u]))) { return 0u; }\n let same = ge0(u) == ge0(v);\n return select(1u, 0u, same != inverted);\n }\n default: { flagged = true; return 0u; }\n }\n}\n\n// Whether the point (mask units, bounded) is inside the shape; a point outside the bounding box by more than its margin is not.\nfn shape_contains(shape: u32, x: B, y: B) -> bool {\n let kind = ai[shape]; let data = ai[shape + 1u]; let edges = ai[shape + 2u];\n let mx = margin(x); let my = margin(y);\n if (x.v + mx < af[data] || x.v - mx > af[data + 2u] || y.v + my < af[data + 1u] || y.v - my > af[data + 3u]) { return false; }\n if (kind == 1u) {\n let dx = bsub(x, bc(af[data + 4u])); let dy = bsub(y, bc(af[data + 5u]));\n let d2 = badd(bmul(dx, dx), bmul(dy, dy));\n return ge(d2, bc(af[data + 6u])) && le(d2, bc(af[data + 7u]));\n }\n var winding = 0;\n for (var k = 0u; k < edges; k++) {\n let o = data + 4u + 4u * k;\n let x0 = bc(af[o]); let y0 = bc(af[o + 1u]); let x1 = bc(af[o + 2u]); let y1 = bc(af[o + 3u]);\n if (ge(y, y0)) {\n if (gt(y1, y)) {\n let side = bsub(bmul(bsub(x1, x0), bsub(y, y0)), bmul(bsub(x, x0), bsub(y1, y0)));\n if (gt(side, bx(0.0))) { winding = winding + 1; }\n }\n } else if (le(y1, y)) {\n let side = bsub(bmul(bsub(x1, x0), bsub(y, y0)), bmul(bsub(x, x0), bsub(y1, y0)));\n if (lt(side, bx(0.0))) { winding = winding - 1; }\n }\n }\n return winding != 0;\n}\n\n// The material of a point: the topmost layer holding it, else the topmost element holding it, else the background.\nfn material(p: vec4<f32>) -> u32 {\n let ud = D(p.x, p.y); let vd = D(p.z, p.w);\n let u = bd(ud); let v = bd(vd);\n let layers = ai[1]; let hl = ai[3];\n for (var l = layers; l > 0u; l--) {\n let h = hl + ${CHART_RECORD_STRIDE.layer}u * (l - 1u);\n let f = ai[h + 6u];\n let u0 = bc(af[f]); let v0 = bc(af[f + 1u]); let u1 = bc(af[f + 2u]); let v1 = bc(af[f + 3u]);\n if (lt(u, u0) || gt(u, u1) || lt(v, v0) || gt(v, v1)) { continue; }\n let x = bmul(bsub(u, u0), bc(af[f + 4u])); let y = bmul(bsub(v1, v), bc(af[f + 5u]));\n let cellsX = ai[h]; let cellsY = ai[h + 1u];\n let i = u32(clamp(floor(x.v / af[f + 6u]), 0.0, f32(cellsX - 1u)));\n let j = u32(clamp(floor(y.v / af[f + 7u]), 0.0, f32(cellsY - 1u)));\n let cell = j * cellsX + i;\n let start = ai[ai[h + 2u] + cell]; let end = ai[ai[h + 2u] + cell + 1u];\n for (var e = start; e < end; e++) {\n let shape = ai[h + 4u] + ${CHART_RECORD_STRIDE.shape}u * ai[ai[h + 3u] + e];\n if (shape_contains(shape, x, y)) { return ai[h + 8u]; }\n }\n return ai[h + 7u];\n }\n let elements = ai[0]; let he = ai[2];\n for (var i = elements; i > 0u; i--) {\n let h = he + ${CHART_RECORD_STRIDE.element}u * (i - 1u);\n let kind = ai[h]; let offset = ai[h + 1u]; let ioff = ai[h + 2u]; let foff = ai[h + 3u]; let placement = ai[h + 4u];\n if (placement == ${CHART_PLACEMENT.frame}u) { return offset + classify(kind, ioff, foff + ${CHART_RECORD_STRIDE.placementFloats}u, ud, vd); }\n let du = d_sub(ud, D(af[foff], af[foff + 1u])); let dv = d_sub(vd, D(af[foff + 2u], af[foff + 3u]));\n let hw = bc(af[foff + 4u]); let hh = bc(af[foff + 5u]);\n let bu = bds(du, abs(ud.h) + abs(af[foff])); let bv = bds(dv, abs(vd.h) + abs(af[foff + 2u]));\n if (lt(bu, bneg(hw)) || gt(bu, hw) || lt(bv, bneg(hh)) || gt(bv, hh)) { continue; }\n if (placement == ${CHART_PLACEMENT.ellipse}u) {\n let a = bdiv(bu, hw); let b = bdiv(bv, hh);\n if (gt(badd(bmul(a, a), bmul(b, b)), bx(1.0))) { continue; }\n }\n let scale = D(af[foff + 6u], af[foff + 7u]);\n return offset + classify(kind, ioff, foff + ${CHART_RECORD_STRIDE.placementFloats}u, d_mul(du, scale), d_mul(dv, scale));\n }\n flagged = true;\n return 0u;\n}\n\n@compute @workgroup_size(${CHART_COMPOSITE_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 * ${CHART_COMPOSITE_WORKGROUP_SIZE}u;\n if (i >= params.count) { return; }\n flagged = false;\n let m = material(points[i]);\n if (params.wavelength_mode != 0u) {\n values[i] = table[m * params.columns + selectors[i]];\n } else {\n for (var c = 0u; c < params.columns; c++) { values[i * params.columns + c] = table[m * params.columns + c]; }\n }\n flags[i] = select(0u, 1u, flagged);\n}\n`;\n}\n/** The composite kernel with the Dekker product path (unchanged since #477). */\nexport const CHART_COMPOSITE_WGSL = chartCompositeWgsl(\"dekker\");\n\n/**\n * Probe of the double-float32 building blocks on the compiled device, both product paths: for each input (x, y, z, w) it writes\n * TwoSum(x, y), the Dekker and the fma TwoProduct(x, y) low parts, the double-word sum (x, y) + (z, w) low part, and both paths'\n * double-word products (x, y)·(z, w), hi and lo. The host (`classifyChartCompositeProbe`) compares them with the exact values and\n * with the round-to-nearest evaluation in the written order, names the components that differ, classifies the device and picks\n * the product path the composite kernel is compiled with. The functions are the kernel's own (`COMMON_TEMPLATE`).\n */\nexport const CHART_COMPOSITE_PROBE_WGSL = /* wgsl */ `${commonWgsl(\"dekker\", \"probe_zero.x\")}\n@group(0) @binding(0) var<storage, read> input: array<vec4<f32>>;\n@group(0) @binding(1) var<storage, read_write> output: array<vec4<f32>>;\n// x: 0, set by the host; the rounding barrier's word.\n@group(0) @binding(2) var<uniform> probe_zero: vec4<u32>;\n@compute @workgroup_size(1)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let i = gid.x;\n if (i >= arrayLength(&input)) { return; }\n let p = input[i];\n let s = two_sum(p.x, p.y);\n let m = two_prod_dekker(p.x, p.y);\n let a = d_add(D(p.x, p.y), D(p.z, p.w));\n let mf = two_prod_fma(p.x, p.y);\n let q = d_mul_dekker(D(p.x, p.y), D(p.z, p.w));\n let qf = d_mul_fma(D(p.x, p.y), D(p.z, p.w));\n output[2u * i] = vec4<f32>(s.l, m.l, a.l, mf.l);\n output[2u * i + 1u] = vec4<f32>(q.h, q.l, qf.h, qf.l);\n}\n`;\n/** Floats the probe writes per input (two vec4). */\nexport const CHART_COMPOSITE_PROBE_OUTPUTS = 8;\nexport const CHART_COMPOSITE_PROBE_LABEL = \"photivra-chart-composite-probe\";\n/** Pipeline label of the composite kernel per product path (the pipeline caches key on it). */\nexport function chartCompositeLabel(path: ChartCompositeProductPath): string { return path === \"dekker\" ? \"photivra-chart-composite\" : \"photivra-chart-composite-fma\"; }\nexport const CHART_COMPOSITE_LABEL = chartCompositeLabel(\"dekker\");\n"]}
@@ -20,17 +20,17 @@
20
20
  */
21
21
  export const CHART_WORKGROUP_SIZE = 256;
22
22
  /** Preset codes of `Params.preset`, in `CHART_SCENE_PRESETS` order. */
23
- export const CHART_PRESET_CODE = Object.freeze({ uniform: 0, "slanted-edge": 1, "siemens-star": 2, "step-wedge": 3, "grey-ramp": 4, checkerboard: 5,
23
+ export const CHART_PRESET_CODE = /* @__PURE__ */ Object.freeze({ uniform: 0, "slanted-edge": 1, "siemens-star": 2, "step-wedge": 3, "grey-ramp": 4, checkerboard: 5,
24
24
  "point-grid": 6, patches: 7 });
25
25
  export const CHART_RADIANCE_WGSL = /* wgsl */ `
26
26
  const E: f32 = 1.1920928955078125e-7;
27
27
  const SAFETY: f32 = 4.0;
28
28
  const SLACK: f32 = 1e-14;
29
29
  struct Params {
30
- count: u32, columns: u32, preset: u32, na: u32,
31
- nb: u32, nc: u32, wavelength_mode: u32, pad0: u32,
32
- f0: f32, f1: f32, f2: f32, f3: f32,
33
- f4: f32, f5: f32, f6: f32, f7: f32,
30
+ count: u32, columns: u32, preset: u32, na: u32,
31
+ nb: u32, nc: u32, wavelength_mode: u32, pad0: u32,
32
+ f0: f32, f1: f32, f2: f32, f3: f32,
33
+ f4: f32, f5: f32, f6: f32, f7: f32,
34
34
  };
35
35
  @group(0) @binding(0) var<uniform> params: Params;
36
36
  @group(0) @binding(1) var<storage, read> points: array<vec2<f32>>;
@@ -42,130 +42,130 @@ struct Params {
42
42
 
43
43
  var<private> flagged: bool;
44
44
  fn check(distance: f32, bound: f32) { if (!(abs(distance) > SAFETY * bound)) { flagged = true; } }
45
- // Flags t when it lies within the bound of an integer n with lo <= n <= hi.
45
+
46
46
  fn check_integer(t: f32, bound: f32, lo: f32, hi: f32) {
47
- let n = round(t);
48
- if (SAFETY * bound >= 0.5) { flagged = true; return; }
49
- if (n >= lo && n <= hi) { check(t - n, bound); }
47
+ let n = round(t);
48
+ if (SAFETY * bound >= 0.5) { flagged = true; return; }
49
+ if (n >= lo && n <= hi) { check(t - n, bound); }
50
50
  }
51
- // x = (a + b) * scale, with the bound of its distance from the float64 reference value.
51
+
52
52
  fn scaled_sum_bound(a: f32, b: f32, s: f32, scale: f32, x: f32, constant_rounding: f32) -> f32 {
53
- return E * ((abs(a) + abs(b) + abs(s)) * abs(scale) + (1.0 + constant_rounding) * abs(x)) + SLACK * (abs(x) + 1.0);
53
+ return E * ((abs(a) + abs(b) + abs(s)) * abs(scale) + (1.0 + constant_rounding) * abs(x)) + SLACK * (abs(x) + 1.0);
54
54
  }
55
55
  fn cross_d(u: f32, v: f32, k: u32) -> f32 { return v * aux[2u * k] - u * aux[2u * k + 1u]; }
56
56
  fn cross_bound(u: f32, v: f32, k: u32, d: f32) -> f32 {
57
- return E * (3.0 * abs(v * aux[2u * k]) + 3.0 * abs(u * aux[2u * k + 1u]) + abs(d)) + SLACK * (abs(u) + abs(v));
57
+ return E * (3.0 * abs(v * aux[2u * k]) + 3.0 * abs(u * aux[2u * k + 1u]) + abs(d)) + SLACK * (abs(u) + abs(v));
58
58
  }
59
59
  fn material(p: vec2<f32>) -> u32 {
60
- let u = p.x;
61
- let v = p.y;
62
- switch (params.preset) {
63
- case 0u: { return 0u; }
64
- case 1u: {
65
- // Light where u·cos t − v·sin t ≥ 0.
66
- let a = u * params.f0;
67
- let b = v * params.f1;
68
- let d = a - b;
69
- check(d, E * (3.0 * abs(a) + 3.0 * abs(b) + abs(d)) + SLACK * (abs(u) + abs(v)));
70
- return select(0u, 1u, d >= 0.0);
71
- }
72
- case 2u: {
73
- let uu = u * u;
74
- let vv = v * v;
75
- let q = uu + vv;
76
- check(q - params.f0, E * (4.0 * q + 2.0 * params.f0) + SLACK * (q + params.f0));
77
- if (q > params.f0) { return 2u; }
78
- // Boundaries b_k = π(k/cycles − 1), k = 0 … 2·cycles. In the half-plane v ≥ 0 the angle lies in [0, π],
79
- // between b_cycles and b_2cycles; for v < 0 in [−π, 0), between b_0 and b_cycles. Within one half-plane,
80
- // sign(cross(k)) = sign(θ − b_k), so a binary search on the cross-product signs finds the sector. A point
81
- // within the bound of a boundary (the u-axis and the ±π cut included) is caught by the two final checks.
82
- var low = select(0u, params.na, v >= 0.0);
83
- var high = low + params.na;
84
- loop {
85
- if (high - low <= 1u) { break; }
86
- let middle = (low + high) >> 1u;
87
- if (cross_d(u, v, middle) >= 0.0) { low = middle; } else { high = middle; }
88
- }
89
- let k = low;
90
- let lo = cross_d(u, v, k);
91
- let hi = cross_d(u, v, k + 1u);
92
- if (lo < 0.0 || hi >= 0.0) { flagged = true; }
93
- check(lo, cross_bound(u, v, k, lo));
94
- check(hi, cross_bound(u, v, k + 1u, hi));
95
- return select(0u, 1u, k % 2u == 0u);
96
- }
97
- case 3u, 4u: {
98
- let s = u + 1.0;
99
- let t = s * params.f0;
100
- check_integer(t, scaled_sum_bound(u, 1.0, s, params.f0, t, 0.0), 1.0, f32(params.na - 1u));
101
- return u32(clamp(floor(t), 0.0, f32(params.na - 1u)));
102
- }
103
- case 5u: {
104
- let s = u + 1.0;
105
- let x = s * params.f0;
106
- let w = params.f1 - v;
107
- let y = w * params.f0;
108
- check_integer(x, scaled_sum_bound(u, 1.0, s, params.f0, x, 0.0), -3.0e38, 3.0e38);
109
- check_integer(y, scaled_sum_bound(params.f1, v, w, params.f0, y, 0.0), -3.0e38, 3.0e38);
110
- let i = i32(floor(x));
111
- let j = i32(floor(y));
112
- return select(0u, 1u, (i + j) % 2 == 0);
113
- }
114
- case 6u: {
115
- let s = u + 1.0;
116
- let x = s * params.f0;
117
- let w = params.f1 - v;
118
- let y = w * params.f2;
119
- check_integer(x, scaled_sum_bound(u, 1.0, s, params.f0, x, 0.0), 1.0, f32(params.na - 1u));
120
- check_integer(y, scaled_sum_bound(params.f1, v, w, params.f2, y, 1.0), 1.0, f32(params.nb - 1u));
121
- let i = u32(clamp(floor(x), 0.0, f32(params.na - 1u)));
122
- let j = u32(clamp(floor(y), 0.0, f32(params.nb - 1u)));
123
- let c = j * params.na + i;
124
- let dx = u - aux[2u * c];
125
- let dy = v - aux[2u * c + 1u];
126
- let ex = E * (abs(u) + abs(aux[2u * c]) + abs(dx));
127
- let ey = E * (abs(v) + abs(aux[2u * c + 1u]) + abs(dy));
128
- let d2 = dx * dx + dy * dy;
129
- check(d2 - params.f3, 2.0 * (abs(dx) * ex + abs(dy) * ey) + ex * ex + ey * ey + E * (2.0 * d2 + params.f3) + SLACK * (d2 + params.f3));
130
- return select(0u, 1u, d2 <= params.f3);
131
- }
132
- default: {
133
- let s = u + 1.0;
134
- let fx = s * params.f0;
135
- let w = params.f1 - v;
136
- let fy = w * params.f2;
137
- let bx = scaled_sum_bound(u, 1.0, s, params.f0, fx, 0.0);
138
- let by = scaled_sum_bound(params.f1, v, w, params.f2, fy, 1.0);
139
- check_integer(fx, bx, -3.0e38, 3.0e38);
140
- check_integer(fy, by, -3.0e38, 3.0e38);
141
- let background = params.nc;
142
- let ifx = floor(fx);
143
- let ify = floor(fy);
144
- if (ifx < 0.0 || ify < 0.0 || ifx >= f32(params.na) || ify >= f32(params.nb)) { return background; }
145
- let index = u32(ify) * params.na + u32(ifx);
146
- let x = fx - ifx;
147
- let y = fy - ify;
148
- check(x - params.f3, bx + E * params.f3);
149
- check(x - params.f4, bx + E * params.f4);
150
- check(y - params.f3, by + E * params.f3);
151
- check(y - params.f4, by + E * params.f4);
152
- if (index >= params.nc || x < params.f3 || x > params.f4 || y < params.f3 || y > params.f4) { return background; }
153
- return index;
154
- }
155
- }
60
+ let u = p.x;
61
+ let v = p.y;
62
+ switch (params.preset) {
63
+ case 0u: { return 0u; }
64
+ case 1u: {
65
+
66
+ let a = u * params.f0;
67
+ let b = v * params.f1;
68
+ let d = a - b;
69
+ check(d, E * (3.0 * abs(a) + 3.0 * abs(b) + abs(d)) + SLACK * (abs(u) + abs(v)));
70
+ return select(0u, 1u, d >= 0.0);
71
+ }
72
+ case 2u: {
73
+ let uu = u * u;
74
+ let vv = v * v;
75
+ let q = uu + vv;
76
+ check(q - params.f0, E * (4.0 * q + 2.0 * params.f0) + SLACK * (q + params.f0));
77
+ if (q > params.f0) { return 2u; }
78
+
79
+
80
+
81
+
82
+ var low = select(0u, params.na, v >= 0.0);
83
+ var high = low + params.na;
84
+ loop {
85
+ if (high - low <= 1u) { break; }
86
+ let middle = (low + high) >> 1u;
87
+ if (cross_d(u, v, middle) >= 0.0) { low = middle; } else { high = middle; }
88
+ }
89
+ let k = low;
90
+ let lo = cross_d(u, v, k);
91
+ let hi = cross_d(u, v, k + 1u);
92
+ if (lo < 0.0 || hi >= 0.0) { flagged = true; }
93
+ check(lo, cross_bound(u, v, k, lo));
94
+ check(hi, cross_bound(u, v, k + 1u, hi));
95
+ return select(0u, 1u, k % 2u == 0u);
96
+ }
97
+ case 3u, 4u: {
98
+ let s = u + 1.0;
99
+ let t = s * params.f0;
100
+ check_integer(t, scaled_sum_bound(u, 1.0, s, params.f0, t, 0.0), 1.0, f32(params.na - 1u));
101
+ return u32(clamp(floor(t), 0.0, f32(params.na - 1u)));
102
+ }
103
+ case 5u: {
104
+ let s = u + 1.0;
105
+ let x = s * params.f0;
106
+ let w = params.f1 - v;
107
+ let y = w * params.f0;
108
+ check_integer(x, scaled_sum_bound(u, 1.0, s, params.f0, x, 0.0), -3.0e38, 3.0e38);
109
+ check_integer(y, scaled_sum_bound(params.f1, v, w, params.f0, y, 0.0), -3.0e38, 3.0e38);
110
+ let i = i32(floor(x));
111
+ let j = i32(floor(y));
112
+ return select(0u, 1u, (i + j) % 2 == 0);
113
+ }
114
+ case 6u: {
115
+ let s = u + 1.0;
116
+ let x = s * params.f0;
117
+ let w = params.f1 - v;
118
+ let y = w * params.f2;
119
+ check_integer(x, scaled_sum_bound(u, 1.0, s, params.f0, x, 0.0), 1.0, f32(params.na - 1u));
120
+ check_integer(y, scaled_sum_bound(params.f1, v, w, params.f2, y, 1.0), 1.0, f32(params.nb - 1u));
121
+ let i = u32(clamp(floor(x), 0.0, f32(params.na - 1u)));
122
+ let j = u32(clamp(floor(y), 0.0, f32(params.nb - 1u)));
123
+ let c = j * params.na + i;
124
+ let dx = u - aux[2u * c];
125
+ let dy = v - aux[2u * c + 1u];
126
+ let ex = E * (abs(u) + abs(aux[2u * c]) + abs(dx));
127
+ let ey = E * (abs(v) + abs(aux[2u * c + 1u]) + abs(dy));
128
+ let d2 = dx * dx + dy * dy;
129
+ check(d2 - params.f3, 2.0 * (abs(dx) * ex + abs(dy) * ey) + ex * ex + ey * ey + E * (2.0 * d2 + params.f3) + SLACK * (d2 + params.f3));
130
+ return select(0u, 1u, d2 <= params.f3);
131
+ }
132
+ default: {
133
+ let s = u + 1.0;
134
+ let fx = s * params.f0;
135
+ let w = params.f1 - v;
136
+ let fy = w * params.f2;
137
+ let bx = scaled_sum_bound(u, 1.0, s, params.f0, fx, 0.0);
138
+ let by = scaled_sum_bound(params.f1, v, w, params.f2, fy, 1.0);
139
+ check_integer(fx, bx, -3.0e38, 3.0e38);
140
+ check_integer(fy, by, -3.0e38, 3.0e38);
141
+ let background = params.nc;
142
+ let ifx = floor(fx);
143
+ let ify = floor(fy);
144
+ if (ifx < 0.0 || ify < 0.0 || ifx >= f32(params.na) || ify >= f32(params.nb)) { return background; }
145
+ let index = u32(ify) * params.na + u32(ifx);
146
+ let x = fx - ifx;
147
+ let y = fy - ify;
148
+ check(x - params.f3, bx + E * params.f3);
149
+ check(x - params.f4, bx + E * params.f4);
150
+ check(y - params.f3, by + E * params.f3);
151
+ check(y - params.f4, by + E * params.f4);
152
+ if (index >= params.nc || x < params.f3 || x > params.f4 || y < params.f3 || y > params.f4) { return background; }
153
+ return index;
154
+ }
155
+ }
156
156
  }
157
157
  @compute @workgroup_size(${CHART_WORKGROUP_SIZE})
158
158
  fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) groups: vec3<u32>) {
159
159
  let i = gid.x + gid.y * groups.x * ${CHART_WORKGROUP_SIZE}u;
160
- if (i >= params.count) { return; }
161
- flagged = false;
162
- let m = material(points[i]);
163
- if (params.wavelength_mode != 0u) {
164
- values[i] = table[m * params.columns + selectors[i]];
165
- } else {
166
- for (var c = 0u; c < params.columns; c++) { values[i * params.columns + c] = table[m * params.columns + c]; }
167
- }
168
- flags[i] = select(0u, 1u, flagged);
160
+ if (i >= params.count) { return; }
161
+ flagged = false;
162
+ let m = material(points[i]);
163
+ if (params.wavelength_mode != 0u) {
164
+ values[i] = table[m * params.columns + selectors[i]];
165
+ } else {
166
+ for (var c = 0u; c < params.columns; c++) { values[i * params.columns + c] = table[m * params.columns + c]; }
167
+ }
168
+ flags[i] = select(0u, 1u, flagged);
169
169
  }
170
170
  `;
171
171
  //# sourceMappingURL=wgsl-chart-kernel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"wgsl-chart-kernel.js","sourceRoot":"","sources":["../../src/gpu/wgsl-chart-kernel.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AACxC,uEAAuE;AACvE,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC;IACjJ,YAAY,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAW,CAAC,CAAC;AAE1C,MAAM,CAAC,MAAM,mBAAmB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAoInB,oBAAoB;;uCAER,oBAAoB;;;;;;;;;;;CAW1D,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * WGSL kernel for the `webgpu-f32` chart-radiance stage (#277), one invocation per query point. It\n * evaluates the chart's material classification (`resolveChartScene().materialAt`) in float32 and looks up\n * the material's value in a table the host computed in float64 with the reference's own operations.\n *\n * Every classification decision (half-plane, disk, sector, cell and gap boundaries) is evaluated with an\n * a-priori bound on how far the float32 decision value can be from the float64 reference value: the\n * point's float32 rounding, each float32 operation (E = 2^-23: one WGSL rounding of + − ×, in either\n * direction, W3C WGSL \"Floating Point Accuracy\"), the float32 rounding of the constants and the\n * reference's own float64 rounding. A point whose decision value lies within four times its bound is\n * flagged, and the host recomputes it with the reference. So every unflagged point has exactly the\n * reference's material, and its value is the reference value rounded once to float32.\n *\n * Siemens-star sectors are decided only by the signs of cross products with the sector boundary\n * directions (host float64 cos/sin, rounded once to float32): the sign of v chooses the half-plane, a\n * binary search over that half-plane's boundaries finds the sector, and the two enclosing cross products\n * carry the bound. The kernel calls no transcendental function, so neither the WGSL `atan2` bound nor the\n * realm's float64 `Math.atan2` enters. See docs/WEBGPU.md, \"Scene radiance on the GPU\".\n */\n\nexport const CHART_WORKGROUP_SIZE = 256;\n/** Preset codes of `Params.preset`, in `CHART_SCENE_PRESETS` order. */\nexport const CHART_PRESET_CODE = Object.freeze({ uniform: 0, \"slanted-edge\": 1, \"siemens-star\": 2, \"step-wedge\": 3, \"grey-ramp\": 4, checkerboard: 5,\n \"point-grid\": 6, patches: 7 } as const);\n\nexport const CHART_RADIANCE_WGSL = /* wgsl */ `\nconst E: f32 = 1.1920928955078125e-7;\nconst SAFETY: f32 = 4.0;\nconst SLACK: f32 = 1e-14;\nstruct Params {\n count: u32, columns: u32, preset: u32, na: u32,\n nb: u32, nc: u32, wavelength_mode: u32, pad0: u32,\n f0: f32, f1: f32, f2: f32, f3: f32,\n f4: f32, f5: f32, f6: f32, f7: f32,\n};\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> points: array<vec2<f32>>;\n@group(0) @binding(2) var<storage, read> table: array<f32>;\n@group(0) @binding(3) var<storage, read> aux: array<f32>;\n@group(0) @binding(4) var<storage, read> selectors: array<u32>;\n@group(0) @binding(5) var<storage, read_write> values: array<f32>;\n@group(0) @binding(6) var<storage, read_write> flags: array<u32>;\n\nvar<private> flagged: bool;\nfn check(distance: f32, bound: f32) { if (!(abs(distance) > SAFETY * bound)) { flagged = true; } }\n// Flags t when it lies within the bound of an integer n with lo <= n <= hi.\nfn check_integer(t: f32, bound: f32, lo: f32, hi: f32) {\n let n = round(t);\n if (SAFETY * bound >= 0.5) { flagged = true; return; }\n if (n >= lo && n <= hi) { check(t - n, bound); }\n}\n// x = (a + b) * scale, with the bound of its distance from the float64 reference value.\nfn scaled_sum_bound(a: f32, b: f32, s: f32, scale: f32, x: f32, constant_rounding: f32) -> f32 {\n return E * ((abs(a) + abs(b) + abs(s)) * abs(scale) + (1.0 + constant_rounding) * abs(x)) + SLACK * (abs(x) + 1.0);\n}\nfn cross_d(u: f32, v: f32, k: u32) -> f32 { return v * aux[2u * k] - u * aux[2u * k + 1u]; }\nfn cross_bound(u: f32, v: f32, k: u32, d: f32) -> f32 {\n return E * (3.0 * abs(v * aux[2u * k]) + 3.0 * abs(u * aux[2u * k + 1u]) + abs(d)) + SLACK * (abs(u) + abs(v));\n}\nfn material(p: vec2<f32>) -> u32 {\n let u = p.x;\n let v = p.y;\n switch (params.preset) {\n case 0u: { return 0u; }\n case 1u: {\n // Light where u·cos t − v·sin t ≥ 0.\n let a = u * params.f0;\n let b = v * params.f1;\n let d = a - b;\n check(d, E * (3.0 * abs(a) + 3.0 * abs(b) + abs(d)) + SLACK * (abs(u) + abs(v)));\n return select(0u, 1u, d >= 0.0);\n }\n case 2u: {\n let uu = u * u;\n let vv = v * v;\n let q = uu + vv;\n check(q - params.f0, E * (4.0 * q + 2.0 * params.f0) + SLACK * (q + params.f0));\n if (q > params.f0) { return 2u; }\n // Boundaries b_k = π(k/cycles − 1), k = 0 … 2·cycles. In the half-plane v ≥ 0 the angle lies in [0, π],\n // between b_cycles and b_2cycles; for v < 0 in [−π, 0), between b_0 and b_cycles. Within one half-plane,\n // sign(cross(k)) = sign(θ − b_k), so a binary search on the cross-product signs finds the sector. A point\n // within the bound of a boundary (the u-axis and the ±π cut included) is caught by the two final checks.\n var low = select(0u, params.na, v >= 0.0);\n var high = low + params.na;\n loop {\n if (high - low <= 1u) { break; }\n let middle = (low + high) >> 1u;\n if (cross_d(u, v, middle) >= 0.0) { low = middle; } else { high = middle; }\n }\n let k = low;\n let lo = cross_d(u, v, k);\n let hi = cross_d(u, v, k + 1u);\n if (lo < 0.0 || hi >= 0.0) { flagged = true; }\n check(lo, cross_bound(u, v, k, lo));\n check(hi, cross_bound(u, v, k + 1u, hi));\n return select(0u, 1u, k % 2u == 0u);\n }\n case 3u, 4u: {\n let s = u + 1.0;\n let t = s * params.f0;\n check_integer(t, scaled_sum_bound(u, 1.0, s, params.f0, t, 0.0), 1.0, f32(params.na - 1u));\n return u32(clamp(floor(t), 0.0, f32(params.na - 1u)));\n }\n case 5u: {\n let s = u + 1.0;\n let x = s * params.f0;\n let w = params.f1 - v;\n let y = w * params.f0;\n check_integer(x, scaled_sum_bound(u, 1.0, s, params.f0, x, 0.0), -3.0e38, 3.0e38);\n check_integer(y, scaled_sum_bound(params.f1, v, w, params.f0, y, 0.0), -3.0e38, 3.0e38);\n let i = i32(floor(x));\n let j = i32(floor(y));\n return select(0u, 1u, (i + j) % 2 == 0);\n }\n case 6u: {\n let s = u + 1.0;\n let x = s * params.f0;\n let w = params.f1 - v;\n let y = w * params.f2;\n check_integer(x, scaled_sum_bound(u, 1.0, s, params.f0, x, 0.0), 1.0, f32(params.na - 1u));\n check_integer(y, scaled_sum_bound(params.f1, v, w, params.f2, y, 1.0), 1.0, f32(params.nb - 1u));\n let i = u32(clamp(floor(x), 0.0, f32(params.na - 1u)));\n let j = u32(clamp(floor(y), 0.0, f32(params.nb - 1u)));\n let c = j * params.na + i;\n let dx = u - aux[2u * c];\n let dy = v - aux[2u * c + 1u];\n let ex = E * (abs(u) + abs(aux[2u * c]) + abs(dx));\n let ey = E * (abs(v) + abs(aux[2u * c + 1u]) + abs(dy));\n let d2 = dx * dx + dy * dy;\n check(d2 - params.f3, 2.0 * (abs(dx) * ex + abs(dy) * ey) + ex * ex + ey * ey + E * (2.0 * d2 + params.f3) + SLACK * (d2 + params.f3));\n return select(0u, 1u, d2 <= params.f3);\n }\n default: {\n let s = u + 1.0;\n let fx = s * params.f0;\n let w = params.f1 - v;\n let fy = w * params.f2;\n let bx = scaled_sum_bound(u, 1.0, s, params.f0, fx, 0.0);\n let by = scaled_sum_bound(params.f1, v, w, params.f2, fy, 1.0);\n check_integer(fx, bx, -3.0e38, 3.0e38);\n check_integer(fy, by, -3.0e38, 3.0e38);\n let background = params.nc;\n let ifx = floor(fx);\n let ify = floor(fy);\n if (ifx < 0.0 || ify < 0.0 || ifx >= f32(params.na) || ify >= f32(params.nb)) { return background; }\n let index = u32(ify) * params.na + u32(ifx);\n let x = fx - ifx;\n let y = fy - ify;\n check(x - params.f3, bx + E * params.f3);\n check(x - params.f4, bx + E * params.f4);\n check(y - params.f3, by + E * params.f3);\n check(y - params.f4, by + E * params.f4);\n if (index >= params.nc || x < params.f3 || x > params.f4 || y < params.f3 || y > params.f4) { return background; }\n return index;\n }\n }\n}\n@compute @workgroup_size(${CHART_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 * ${CHART_WORKGROUP_SIZE}u;\n if (i >= params.count) { return; }\n flagged = false;\n let m = material(points[i]);\n if (params.wavelength_mode != 0u) {\n values[i] = table[m * params.columns + selectors[i]];\n } else {\n for (var c = 0u; c < params.columns; c++) { values[i * params.columns + c] = table[m * params.columns + c]; }\n }\n flags[i] = select(0u, 1u, flagged);\n}\n`;\n"]}
1
+ {"version":3,"file":"wgsl-chart-kernel.js","sourceRoot":"","sources":["../../src/gpu/wgsl-chart-kernel.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AACxC,uEAAuE;AACvE,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,cAAc,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC;IACjK,YAAY,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAW,CAAC,CAAC;AAE1C,MAAM,CAAC,MAAM,mBAAmB,GAAG,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAoInB,oBAAoB;;uCAER,oBAAoB;;;;;;;;;;;CAW1D,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * WGSL kernel for the `webgpu-f32` chart-radiance stage (#277), one invocation per query point. It\n * evaluates the chart's material classification (`resolveChartScene().materialAt`) in float32 and looks up\n * the material's value in a table the host computed in float64 with the reference's own operations.\n *\n * Every classification decision (half-plane, disk, sector, cell and gap boundaries) is evaluated with an\n * a-priori bound on how far the float32 decision value can be from the float64 reference value: the\n * point's float32 rounding, each float32 operation (E = 2^-23: one WGSL rounding of + − ×, in either\n * direction, W3C WGSL \"Floating Point Accuracy\"), the float32 rounding of the constants and the\n * reference's own float64 rounding. A point whose decision value lies within four times its bound is\n * flagged, and the host recomputes it with the reference. So every unflagged point has exactly the\n * reference's material, and its value is the reference value rounded once to float32.\n *\n * Siemens-star sectors are decided only by the signs of cross products with the sector boundary\n * directions (host float64 cos/sin, rounded once to float32): the sign of v chooses the half-plane, a\n * binary search over that half-plane's boundaries finds the sector, and the two enclosing cross products\n * carry the bound. The kernel calls no transcendental function, so neither the WGSL `atan2` bound nor the\n * realm's float64 `Math.atan2` enters. See docs/WEBGPU.md, \"Scene radiance on the GPU\".\n */\n\nexport const CHART_WORKGROUP_SIZE = 256;\n/** Preset codes of `Params.preset`, in `CHART_SCENE_PRESETS` order. */\nexport const CHART_PRESET_CODE = /* @__PURE__ */ Object.freeze({ uniform: 0, \"slanted-edge\": 1, \"siemens-star\": 2, \"step-wedge\": 3, \"grey-ramp\": 4, checkerboard: 5,\n \"point-grid\": 6, patches: 7 } as const);\n\nexport const CHART_RADIANCE_WGSL = /* wgsl */ `\nconst E: f32 = 1.1920928955078125e-7;\nconst SAFETY: f32 = 4.0;\nconst SLACK: f32 = 1e-14;\nstruct Params {\n count: u32, columns: u32, preset: u32, na: u32,\n nb: u32, nc: u32, wavelength_mode: u32, pad0: u32,\n f0: f32, f1: f32, f2: f32, f3: f32,\n f4: f32, f5: f32, f6: f32, f7: f32,\n};\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> points: array<vec2<f32>>;\n@group(0) @binding(2) var<storage, read> table: array<f32>;\n@group(0) @binding(3) var<storage, read> aux: array<f32>;\n@group(0) @binding(4) var<storage, read> selectors: array<u32>;\n@group(0) @binding(5) var<storage, read_write> values: array<f32>;\n@group(0) @binding(6) var<storage, read_write> flags: array<u32>;\n\nvar<private> flagged: bool;\nfn check(distance: f32, bound: f32) { if (!(abs(distance) > SAFETY * bound)) { flagged = true; } }\n// Flags t when it lies within the bound of an integer n with lo <= n <= hi.\nfn check_integer(t: f32, bound: f32, lo: f32, hi: f32) {\n let n = round(t);\n if (SAFETY * bound >= 0.5) { flagged = true; return; }\n if (n >= lo && n <= hi) { check(t - n, bound); }\n}\n// x = (a + b) * scale, with the bound of its distance from the float64 reference value.\nfn scaled_sum_bound(a: f32, b: f32, s: f32, scale: f32, x: f32, constant_rounding: f32) -> f32 {\n return E * ((abs(a) + abs(b) + abs(s)) * abs(scale) + (1.0 + constant_rounding) * abs(x)) + SLACK * (abs(x) + 1.0);\n}\nfn cross_d(u: f32, v: f32, k: u32) -> f32 { return v * aux[2u * k] - u * aux[2u * k + 1u]; }\nfn cross_bound(u: f32, v: f32, k: u32, d: f32) -> f32 {\n return E * (3.0 * abs(v * aux[2u * k]) + 3.0 * abs(u * aux[2u * k + 1u]) + abs(d)) + SLACK * (abs(u) + abs(v));\n}\nfn material(p: vec2<f32>) -> u32 {\n let u = p.x;\n let v = p.y;\n switch (params.preset) {\n case 0u: { return 0u; }\n case 1u: {\n // Light where u·cos t − v·sin t ≥ 0.\n let a = u * params.f0;\n let b = v * params.f1;\n let d = a - b;\n check(d, E * (3.0 * abs(a) + 3.0 * abs(b) + abs(d)) + SLACK * (abs(u) + abs(v)));\n return select(0u, 1u, d >= 0.0);\n }\n case 2u: {\n let uu = u * u;\n let vv = v * v;\n let q = uu + vv;\n check(q - params.f0, E * (4.0 * q + 2.0 * params.f0) + SLACK * (q + params.f0));\n if (q > params.f0) { return 2u; }\n // Boundaries b_k = π(k/cycles − 1), k = 0 … 2·cycles. In the half-plane v ≥ 0 the angle lies in [0, π],\n // between b_cycles and b_2cycles; for v < 0 in [−π, 0), between b_0 and b_cycles. Within one half-plane,\n // sign(cross(k)) = sign(θ − b_k), so a binary search on the cross-product signs finds the sector. A point\n // within the bound of a boundary (the u-axis and the ±π cut included) is caught by the two final checks.\n var low = select(0u, params.na, v >= 0.0);\n var high = low + params.na;\n loop {\n if (high - low <= 1u) { break; }\n let middle = (low + high) >> 1u;\n if (cross_d(u, v, middle) >= 0.0) { low = middle; } else { high = middle; }\n }\n let k = low;\n let lo = cross_d(u, v, k);\n let hi = cross_d(u, v, k + 1u);\n if (lo < 0.0 || hi >= 0.0) { flagged = true; }\n check(lo, cross_bound(u, v, k, lo));\n check(hi, cross_bound(u, v, k + 1u, hi));\n return select(0u, 1u, k % 2u == 0u);\n }\n case 3u, 4u: {\n let s = u + 1.0;\n let t = s * params.f0;\n check_integer(t, scaled_sum_bound(u, 1.0, s, params.f0, t, 0.0), 1.0, f32(params.na - 1u));\n return u32(clamp(floor(t), 0.0, f32(params.na - 1u)));\n }\n case 5u: {\n let s = u + 1.0;\n let x = s * params.f0;\n let w = params.f1 - v;\n let y = w * params.f0;\n check_integer(x, scaled_sum_bound(u, 1.0, s, params.f0, x, 0.0), -3.0e38, 3.0e38);\n check_integer(y, scaled_sum_bound(params.f1, v, w, params.f0, y, 0.0), -3.0e38, 3.0e38);\n let i = i32(floor(x));\n let j = i32(floor(y));\n return select(0u, 1u, (i + j) % 2 == 0);\n }\n case 6u: {\n let s = u + 1.0;\n let x = s * params.f0;\n let w = params.f1 - v;\n let y = w * params.f2;\n check_integer(x, scaled_sum_bound(u, 1.0, s, params.f0, x, 0.0), 1.0, f32(params.na - 1u));\n check_integer(y, scaled_sum_bound(params.f1, v, w, params.f2, y, 1.0), 1.0, f32(params.nb - 1u));\n let i = u32(clamp(floor(x), 0.0, f32(params.na - 1u)));\n let j = u32(clamp(floor(y), 0.0, f32(params.nb - 1u)));\n let c = j * params.na + i;\n let dx = u - aux[2u * c];\n let dy = v - aux[2u * c + 1u];\n let ex = E * (abs(u) + abs(aux[2u * c]) + abs(dx));\n let ey = E * (abs(v) + abs(aux[2u * c + 1u]) + abs(dy));\n let d2 = dx * dx + dy * dy;\n check(d2 - params.f3, 2.0 * (abs(dx) * ex + abs(dy) * ey) + ex * ex + ey * ey + E * (2.0 * d2 + params.f3) + SLACK * (d2 + params.f3));\n return select(0u, 1u, d2 <= params.f3);\n }\n default: {\n let s = u + 1.0;\n let fx = s * params.f0;\n let w = params.f1 - v;\n let fy = w * params.f2;\n let bx = scaled_sum_bound(u, 1.0, s, params.f0, fx, 0.0);\n let by = scaled_sum_bound(params.f1, v, w, params.f2, fy, 1.0);\n check_integer(fx, bx, -3.0e38, 3.0e38);\n check_integer(fy, by, -3.0e38, 3.0e38);\n let background = params.nc;\n let ifx = floor(fx);\n let ify = floor(fy);\n if (ifx < 0.0 || ify < 0.0 || ifx >= f32(params.na) || ify >= f32(params.nb)) { return background; }\n let index = u32(ify) * params.na + u32(ifx);\n let x = fx - ifx;\n let y = fy - ify;\n check(x - params.f3, bx + E * params.f3);\n check(x - params.f4, bx + E * params.f4);\n check(y - params.f3, by + E * params.f3);\n check(y - params.f4, by + E * params.f4);\n if (index >= params.nc || x < params.f3 || x > params.f4 || y < params.f3 || y > params.f4) { return background; }\n return index;\n }\n }\n}\n@compute @workgroup_size(${CHART_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 * ${CHART_WORKGROUP_SIZE}u;\n if (i >= params.count) { return; }\n flagged = false;\n let m = material(points[i]);\n if (params.wavelength_mode != 0u) {\n values[i] = table[m * params.columns + selectors[i]];\n } else {\n for (var c = 0u; c < params.columns; c++) { values[i * params.columns + c] = table[m * params.columns + c]; }\n }\n flags[i] = select(0u, 1u, flagged);\n}\n`;\n"]}
@@ -41,9 +41,21 @@ export declare const DISPLAY_VIEW_QUALITY_FLAG: Readonly<{
41
41
  thresholdTie: 8;
42
42
  /** Informational: the pixel's expected value includes the low-signal correction. */
43
43
  corrected: 16;
44
+ /**
45
+ * #457: the reference takes AHD's choice on the noise-free mosaic (one candidate, or their average on a tie), which the kernel
46
+ * does not compute; it renders the average. The pixel is flagged (recompute) when Σ_j a_j·½|h(H) − h(V)| of some component
47
+ * exceeds `DISPLAY_VIEW_QUALITY_CHOICE_SIGMA` times the pixel's noise standard deviation.
48
+ */
49
+ ahdChoice: 32;
44
50
  /** The bits that require the host's recomputation. */
45
- recompute: 15;
51
+ recompute: 47;
46
52
  }>;
53
+ /**
54
+ * #457: the largest move, in units of the display pixel's own noise σ, that AHD's choice between its candidates may make to a pixel
55
+ * the kernel keeps (it renders the candidates' average and its bound carries the move); a larger one is recomputed on the host. A
56
+ * design tolerance, not a measurement: 2^-10 σ is a tenth of the 0.01 σ to which the float32 tests hold the GPU to the reference.
57
+ */
58
+ export declare const DISPLAY_VIEW_QUALITY_CHOICE_SIGMA: number;
47
59
  /** Words of the grid-key bitmap: one bit per grid key (AHD_KEY_LEVELS³ keys). */
48
60
  export declare const DISPLAY_VIEW_QUALITY_BITMAP_WORDS: number;
49
61
  /**