@photivra/engine 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +156 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +19 -3
  4. package/README.md +6 -6
  5. package/SECURITY.md +1 -1
  6. package/dist/core/portable-math.d.ts +6 -0
  7. package/dist/core/portable-math.js +39 -0
  8. package/dist/core/portable-math.js.map +1 -1
  9. package/dist/core/version.d.ts +1 -1
  10. package/dist/core/version.js +1 -1
  11. package/dist/core/version.js.map +1 -1
  12. package/dist/facade/chart-material-layers.d.ts +115 -0
  13. package/dist/facade/chart-material-layers.js +224 -0
  14. package/dist/facade/chart-material-layers.js.map +1 -0
  15. package/dist/facade/chart-radiance-batch.d.ts +8 -2
  16. package/dist/facade/chart-radiance-batch.js +11 -4
  17. package/dist/facade/chart-radiance-batch.js.map +1 -1
  18. package/dist/facade/chart-scenes.d.ts +134 -0
  19. package/dist/facade/chart-scenes.js +32 -22
  20. package/dist/facade/chart-scenes.js.map +1 -1
  21. package/dist/facade/diffraction-provider.d.ts +8 -2
  22. package/dist/facade/diffraction-provider.js +71 -2
  23. package/dist/facade/diffraction-provider.js.map +1 -1
  24. package/dist/facade/display-view-gpu-session.d.ts +18 -0
  25. package/dist/facade/display-view-gpu-session.js +280 -0
  26. package/dist/facade/display-view-gpu-session.js.map +1 -0
  27. package/dist/facade/display-view-gpu.d.ts +146 -0
  28. package/dist/facade/display-view-gpu.js +59 -0
  29. package/dist/facade/display-view-gpu.js.map +1 -0
  30. package/dist/facade/display-view.d.ts +48 -1
  31. package/dist/facade/display-view.js +7 -7
  32. package/dist/facade/display-view.js.map +1 -1
  33. package/dist/facade/expected-signal-setup.d.ts +5 -0
  34. package/dist/facade/expected-signal-setup.js +16 -0
  35. package/dist/facade/expected-signal-setup.js.map +1 -1
  36. package/dist/facade/expected-signal.d.ts +48 -3
  37. package/dist/facade/expected-signal.js +8 -0
  38. package/dist/facade/expected-signal.js.map +1 -1
  39. package/dist/facade/scene-radiance.d.ts +4 -2
  40. package/dist/facade/scene-radiance.js +7 -4
  41. package/dist/facade/scene-radiance.js.map +1 -1
  42. package/dist/facade/simulate-capture.d.ts +108 -8
  43. package/dist/facade/simulate-capture.js +226 -33
  44. package/dist/facade/simulate-capture.js.map +1 -1
  45. package/dist/facade/simulate-display-view.d.ts +31 -3
  46. package/dist/facade/simulate-display-view.js +73 -13
  47. package/dist/facade/simulate-display-view.js.map +1 -1
  48. package/dist/gpu/chart-composite-gpu.d.ts +119 -0
  49. package/dist/gpu/chart-composite-gpu.js +572 -0
  50. package/dist/gpu/chart-composite-gpu.js.map +1 -0
  51. package/dist/gpu/chart-radiance-gpu.d.ts +6 -6
  52. package/dist/gpu/chart-radiance-gpu.js +9 -11
  53. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  54. package/dist/gpu/display-view-chart-material-module.d.ts +10 -0
  55. package/dist/gpu/display-view-chart-material-module.js +58 -0
  56. package/dist/gpu/display-view-chart-material-module.js.map +1 -0
  57. package/dist/gpu/display-view-composite-material-module.d.ts +68 -0
  58. package/dist/gpu/display-view-composite-material-module.js +248 -0
  59. package/dist/gpu/display-view-composite-material-module.js.map +1 -0
  60. package/dist/gpu/display-view-quality-gpu.d.ts +185 -0
  61. package/dist/gpu/display-view-quality-gpu.js +1000 -0
  62. package/dist/gpu/display-view-quality-gpu.js.map +1 -0
  63. package/dist/gpu/display-view-speed-gpu.d.ts +156 -0
  64. package/dist/gpu/display-view-speed-gpu.js +524 -0
  65. package/dist/gpu/display-view-speed-gpu.js.map +1 -0
  66. package/dist/gpu/image-kernel-accelerator.d.ts +23 -4
  67. package/dist/gpu/image-kernel-accelerator.js +58 -10
  68. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  69. package/dist/gpu/image-kernel-execution.d.ts +55 -3
  70. package/dist/gpu/image-kernel-execution.js.map +1 -1
  71. package/dist/gpu/sensor-stage-gpu.d.ts +11 -0
  72. package/dist/gpu/sensor-stage-gpu.js +42 -13
  73. package/dist/gpu/sensor-stage-gpu.js.map +1 -1
  74. package/dist/gpu/webgpu-runtime.d.ts +4 -2
  75. package/dist/gpu/webgpu-runtime.js +13 -4
  76. package/dist/gpu/webgpu-runtime.js.map +1 -1
  77. package/dist/gpu/webgpu-switch.d.ts +4 -0
  78. package/dist/gpu/webgpu-switch.js +29 -0
  79. package/dist/gpu/webgpu-switch.js.map +1 -0
  80. package/dist/gpu/wgsl-chart-composite-kernel.d.ts +52 -0
  81. package/dist/gpu/wgsl-chart-composite-kernel.js +515 -0
  82. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -0
  83. package/dist/gpu/wgsl-chart-kernel.d.ts +11 -9
  84. package/dist/gpu/wgsl-chart-kernel.js +33 -27
  85. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  86. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +95 -0
  87. package/dist/gpu/wgsl-display-view-quality-kernel.js +769 -0
  88. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -0
  89. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +75 -0
  90. package/dist/gpu/wgsl-display-view-speed-kernel.js +386 -0
  91. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -0
  92. package/dist/gpu/wgsl-sensor-kernel.d.ts +28 -9
  93. package/dist/gpu/wgsl-sensor-kernel.js +94 -50
  94. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  95. package/dist/index.d.ts +5 -2
  96. package/dist/index.js +2 -0
  97. package/dist/index.js.map +1 -1
  98. package/dist/optics/defocus-disc-kernel.d.ts +61 -0
  99. package/dist/optics/defocus-disc-kernel.js +191 -0
  100. package/dist/optics/defocus-disc-kernel.js.map +1 -0
  101. package/docs/API_REFERENCE.md +47 -1
  102. package/docs/QUICK_START.md +150 -5
  103. package/docs/USAGE.md +81 -3
  104. package/docs/api/core-version.md +1 -1
  105. package/docs/api/exports.json +127 -2
  106. package/docs/api/facade-chart-material-layers.md +137 -0
  107. package/docs/api/facade-chart-radiance-batch.md +7 -2
  108. package/docs/api/facade-display-view-gpu.md +94 -0
  109. package/docs/api/facade-scene-radiance.md +3 -2
  110. package/docs/api/facade-simulate-capture.md +95 -11
  111. package/docs/api/facade-simulate-display-view.md +17 -2
  112. package/docs/api/gpu-image-kernel-accelerator.md +10 -3
  113. package/docs/api/gpu-image-kernel-execution.md +71 -4
  114. package/docs/api/gpu-webgpu-switch.md +23 -0
  115. package/package.json +3 -1
@@ -5,15 +5,17 @@
5
5
  *
6
6
  * Every classification decision (half-plane, disk, sector, cell and gap boundaries) is evaluated with an
7
7
  * a-priori bound on how far the float32 decision value can be from the float64 reference value: the
8
- * point's float32 rounding, each float32 operation (u = 2^-24, correctly rounded + − ×), the float32
9
- * rounding of the constants and the reference's own float64 rounding. A point whose decision value lies
10
- * within four times its bound is flagged, and the host recomputes it with the reference. So every
11
- * unflagged point has exactly the reference's material, and its value is the reference value rounded
12
- * once to float32.
8
+ * point's float32 rounding, each float32 operation (E = 2^-23: one WGSL rounding of + − ×, in either
9
+ * direction, W3C WGSL "Floating Point Accuracy"), the float32 rounding of the constants and the
10
+ * reference's own float64 rounding. A point whose decision value lies within four times its bound is
11
+ * flagged, and the host recomputes it with the reference. So every unflagged point has exactly the
12
+ * reference's material, and its value is the reference value rounded once to float32.
13
13
  *
14
- * Siemens-star sectors are found from a rough `atan2` (any accuracy) and then decided exactly by the
15
- * signs of cross products with the sector boundary directions, so WGSL's 4096-ULP `atan2` bound does not
16
- * enter. See docs/WEBGPU.md, "Scene radiance on the GPU".
14
+ * Siemens-star sectors are decided only by the signs of cross products with the sector boundary
15
+ * directions (host float64 cos/sin, rounded once to float32): the sign of v chooses the half-plane, a
16
+ * binary search over that half-plane's boundaries finds the sector, and the two enclosing cross products
17
+ * carry the bound. The kernel calls no transcendental function, so neither the WGSL `atan2` bound nor the
18
+ * realm's float64 `Math.atan2` enters. See docs/WEBGPU.md, "Scene radiance on the GPU".
17
19
  */
18
20
  export declare const CHART_WORKGROUP_SIZE = 256;
19
21
  /** Preset codes of `Params.preset`, in `CHART_SCENE_PRESETS` order. */
@@ -27,4 +29,4 @@ export declare const CHART_PRESET_CODE: Readonly<{
27
29
  readonly "point-grid": 6;
28
30
  readonly patches: 7;
29
31
  }>;
30
- export declare const CHART_RADIANCE_WGSL = "\nconst U: f32 = 5.9604644775390625e-8;\nconst SAFETY: f32 = 4.0;\nconst SLACK: f32 = 1e-14;\nconst PI: f32 = 3.14159265358979;\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 U * ((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 U * (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\u00B7cos t \u2212 v\u00B7sin t \u2265 0.\n let a = u * params.f0;\n let b = v * params.f1;\n let d = a - b;\n check(d, U * (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, U * (4.0 * q + 2.0 * params.f0) + SLACK * (q + params.f0));\n if (q > params.f0) { return 2u; }\n let sectors = 2u * params.na;\n var k = u32(clamp(floor((atan2(v, u) + PI) * f32(params.na) / PI), 0.0, f32(sectors - 1u)));\n for (var step = 0u; step < 4u; step++) {\n if (cross_d(u, v, k) < 0.0 && k > 0u) { k = k - 1u; continue; }\n if (cross_d(u, v, k + 1u) >= 0.0 && k + 1u < sectors) { k = k + 1u; continue; }\n break;\n }\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 = U * (abs(u) + abs(aux[2u * c]) + abs(dx));\n let ey = U * (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 + U * (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 + U * params.f3);\n check(x - params.f4, bx + U * params.f4);\n check(y - params.f3, by + U * params.f3);\n check(y - params.f4, by + U * 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(256)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) groups: vec3<u32>) {\n let i = gid.x + gid.y * groups.x * 256u;\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";
32
+ export declare const CHART_RADIANCE_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\u00B7cos t \u2212 v\u00B7sin t \u2265 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 = \u03C0(k/cycles \u2212 1), k = 0 \u2026 2\u00B7cycles. In the half-plane v \u2265 0 the angle lies in [0, \u03C0],\n // between b_cycles and b_2cycles; for v < 0 in [\u2212\u03C0, 0), between b_0 and b_cycles. Within one half-plane,\n // sign(cross(k)) = sign(\u03B8 \u2212 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 \u00B1\u03C0 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(256)\nfn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) groups: vec3<u32>) {\n let i = gid.x + gid.y * groups.x * 256u;\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";
@@ -6,25 +6,26 @@
6
6
  *
7
7
  * Every classification decision (half-plane, disk, sector, cell and gap boundaries) is evaluated with an
8
8
  * a-priori bound on how far the float32 decision value can be from the float64 reference value: the
9
- * point's float32 rounding, each float32 operation (u = 2^-24, correctly rounded + − ×), the float32
10
- * rounding of the constants and the reference's own float64 rounding. A point whose decision value lies
11
- * within four times its bound is flagged, and the host recomputes it with the reference. So every
12
- * unflagged point has exactly the reference's material, and its value is the reference value rounded
13
- * once to float32.
9
+ * point's float32 rounding, each float32 operation (E = 2^-23: one WGSL rounding of + − ×, in either
10
+ * direction, W3C WGSL "Floating Point Accuracy"), the float32 rounding of the constants and the
11
+ * reference's own float64 rounding. A point whose decision value lies within four times its bound is
12
+ * flagged, and the host recomputes it with the reference. So every unflagged point has exactly the
13
+ * reference's material, and its value is the reference value rounded once to float32.
14
14
  *
15
- * Siemens-star sectors are found from a rough `atan2` (any accuracy) and then decided exactly by the
16
- * signs of cross products with the sector boundary directions, so WGSL's 4096-ULP `atan2` bound does not
17
- * enter. See docs/WEBGPU.md, "Scene radiance on the GPU".
15
+ * Siemens-star sectors are decided only by the signs of cross products with the sector boundary
16
+ * directions (host float64 cos/sin, rounded once to float32): the sign of v chooses the half-plane, a
17
+ * binary search over that half-plane's boundaries finds the sector, and the two enclosing cross products
18
+ * carry the bound. The kernel calls no transcendental function, so neither the WGSL `atan2` bound nor the
19
+ * realm's float64 `Math.atan2` enters. See docs/WEBGPU.md, "Scene radiance on the GPU".
18
20
  */
19
21
  export const CHART_WORKGROUP_SIZE = 256;
20
22
  /** Preset codes of `Params.preset`, in `CHART_SCENE_PRESETS` order. */
21
23
  export const CHART_PRESET_CODE = Object.freeze({ uniform: 0, "slanted-edge": 1, "siemens-star": 2, "step-wedge": 3, "grey-ramp": 4, checkerboard: 5,
22
24
  "point-grid": 6, patches: 7 });
23
25
  export const CHART_RADIANCE_WGSL = /* wgsl */ `
24
- const U: f32 = 5.9604644775390625e-8;
26
+ const E: f32 = 1.1920928955078125e-7;
25
27
  const SAFETY: f32 = 4.0;
26
28
  const SLACK: f32 = 1e-14;
27
- const PI: f32 = 3.14159265358979;
28
29
  struct Params {
29
30
  count: u32, columns: u32, preset: u32, na: u32,
30
31
  nb: u32, nc: u32, wavelength_mode: u32, pad0: u32,
@@ -49,11 +50,11 @@ fn check_integer(t: f32, bound: f32, lo: f32, hi: f32) {
49
50
  }
50
51
  // x = (a + b) * scale, with the bound of its distance from the float64 reference value.
51
52
  fn scaled_sum_bound(a: f32, b: f32, s: f32, scale: f32, x: f32, constant_rounding: f32) -> f32 {
52
- return U * ((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);
53
54
  }
54
55
  fn cross_d(u: f32, v: f32, k: u32) -> f32 { return v * aux[2u * k] - u * aux[2u * k + 1u]; }
55
56
  fn cross_bound(u: f32, v: f32, k: u32, d: f32) -> f32 {
56
- return U * (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));
57
58
  }
58
59
  fn material(p: vec2<f32>) -> u32 {
59
60
  let u = p.x;
@@ -65,22 +66,27 @@ fn material(p: vec2<f32>) -> u32 {
65
66
  let a = u * params.f0;
66
67
  let b = v * params.f1;
67
68
  let d = a - b;
68
- check(d, U * (3.0 * abs(a) + 3.0 * abs(b) + abs(d)) + SLACK * (abs(u) + abs(v)));
69
+ check(d, E * (3.0 * abs(a) + 3.0 * abs(b) + abs(d)) + SLACK * (abs(u) + abs(v)));
69
70
  return select(0u, 1u, d >= 0.0);
70
71
  }
71
72
  case 2u: {
72
73
  let uu = u * u;
73
74
  let vv = v * v;
74
75
  let q = uu + vv;
75
- check(q - params.f0, U * (4.0 * q + 2.0 * params.f0) + SLACK * (q + params.f0));
76
+ check(q - params.f0, E * (4.0 * q + 2.0 * params.f0) + SLACK * (q + params.f0));
76
77
  if (q > params.f0) { return 2u; }
77
- let sectors = 2u * params.na;
78
- var k = u32(clamp(floor((atan2(v, u) + PI) * f32(params.na) / PI), 0.0, f32(sectors - 1u)));
79
- for (var step = 0u; step < 4u; step++) {
80
- if (cross_d(u, v, k) < 0.0 && k > 0u) { k = k - 1u; continue; }
81
- if (cross_d(u, v, k + 1u) >= 0.0 && k + 1u < sectors) { k = k + 1u; continue; }
82
- break;
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; }
83
88
  }
89
+ let k = low;
84
90
  let lo = cross_d(u, v, k);
85
91
  let hi = cross_d(u, v, k + 1u);
86
92
  if (lo < 0.0 || hi >= 0.0) { flagged = true; }
@@ -117,10 +123,10 @@ fn material(p: vec2<f32>) -> u32 {
117
123
  let c = j * params.na + i;
118
124
  let dx = u - aux[2u * c];
119
125
  let dy = v - aux[2u * c + 1u];
120
- let ex = U * (abs(u) + abs(aux[2u * c]) + abs(dx));
121
- let ey = U * (abs(v) + abs(aux[2u * c + 1u]) + abs(dy));
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));
122
128
  let d2 = dx * dx + dy * dy;
123
- check(d2 - params.f3, 2.0 * (abs(dx) * ex + abs(dy) * ey) + ex * ex + ey * ey + U * (2.0 * d2 + params.f3) + SLACK * (d2 + params.f3));
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));
124
130
  return select(0u, 1u, d2 <= params.f3);
125
131
  }
126
132
  default: {
@@ -139,10 +145,10 @@ fn material(p: vec2<f32>) -> u32 {
139
145
  let index = u32(ify) * params.na + u32(ifx);
140
146
  let x = fx - ifx;
141
147
  let y = fy - ify;
142
- check(x - params.f3, bx + U * params.f3);
143
- check(x - params.f4, bx + U * params.f4);
144
- check(y - params.f3, by + U * params.f3);
145
- check(y - params.f4, by + U * params.f4);
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);
146
152
  if (index >= params.nc || x < params.f3 || x > params.f4 || y < params.f3 || y > params.f4) { return background; }
147
153
  return index;
148
154
  }
@@ -1 +1 @@
1
- {"version":3,"file":"wgsl-chart-kernel.js","sourceRoot":"","sources":["../../src/gpu/wgsl-chart-kernel.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;;;;;;GAgBG;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAgInB,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 (u = 2^-24, correctly rounded + − ×), the float32\n * rounding of the constants and the reference's own float64 rounding. A point whose decision value lies\n * within four times its bound is flagged, and the host recomputes it with the reference. So every\n * unflagged point has exactly the reference's material, and its value is the reference value rounded\n * once to float32.\n *\n * Siemens-star sectors are found from a rough `atan2` (any accuracy) and then decided exactly by the\n * signs of cross products with the sector boundary directions, so WGSL's 4096-ULP `atan2` bound does not\n * enter. 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 U: f32 = 5.9604644775390625e-8;\nconst SAFETY: f32 = 4.0;\nconst SLACK: f32 = 1e-14;\nconst PI: f32 = 3.14159265358979;\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 U * ((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 U * (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, U * (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, U * (4.0 * q + 2.0 * params.f0) + SLACK * (q + params.f0));\n if (q > params.f0) { return 2u; }\n let sectors = 2u * params.na;\n var k = u32(clamp(floor((atan2(v, u) + PI) * f32(params.na) / PI), 0.0, f32(sectors - 1u)));\n for (var step = 0u; step < 4u; step++) {\n if (cross_d(u, v, k) < 0.0 && k > 0u) { k = k - 1u; continue; }\n if (cross_d(u, v, k + 1u) >= 0.0 && k + 1u < sectors) { k = k + 1u; continue; }\n break;\n }\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 = U * (abs(u) + abs(aux[2u * c]) + abs(dx));\n let ey = U * (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 + U * (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 + U * params.f3);\n check(x - params.f4, bx + U * params.f4);\n check(y - params.f3, by + U * params.f3);\n check(y - params.f4, by + U * 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,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"]}
@@ -0,0 +1,95 @@
1
+ export declare const DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE = 64;
2
+ /** Largest footprint per axis (real pixels), as the Speed kernel. */
3
+ export declare const DISPLAY_VIEW_QUALITY_MAXIMUM_PIXELS_PER_AXIS = 6;
4
+ /** Uniform parameter words (see `Params`): 56 words, 224 bytes. */
5
+ export declare const DISPLAY_VIEW_QUALITY_PARAM_WORDS = 56;
6
+ /** Words per lattice-group record: lx0, ly0, lw, lh, lbase, first tap (the blocked site pass, #492), cu (f32), cv (f32). */
7
+ export declare const DISPLAY_VIEW_QUALITY_GROUP_WORDS = 8;
8
+ /** Words per tap record: group, dp offset, dq offset, pad. */
9
+ export declare const DISPLAY_VIEW_QUALITY_TAP_WORDS = 4;
10
+ /** Floats per native site of the site buffer: mean, variance, third cumulant, their three bounds, flag (u32 bits), electrons. */
11
+ export declare const DISPLAY_VIEW_QUALITY_SITE_FLOATS = 8;
12
+ /** Floats per display pixel in the output buffer: m0 m1 m2, v00 v11 v22, c01 c02 c12, mean bound, variance bound (absolute). */
13
+ export declare const DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS = 11;
14
+ /** Floats per column or row record of the geometry buffer (the Speed layout). */
15
+ export declare const DISPLAY_VIEW_QUALITY_GEOMETRY_STRIDE = 20;
16
+ /** Candidate table: entries per phase (the 7 × 7 window of AHD's reach 3) and floats per entry (dx, dy, dh0, dh1, dh2, dv0, dv1, dv2). */
17
+ export declare const DISPLAY_VIEW_QUALITY_CANDIDATE_ENTRIES = 49;
18
+ export declare const DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS = 8;
19
+ /** Layout of the aux table (float32 words). */
20
+ export declare const DISPLAY_VIEW_QUALITY_AUX: Readonly<{
21
+ /** Per phase p, channel c, tap t: dx, dy, weight, 0 (the bilinear kernels, 4 taps; unused taps have weight 0). */
22
+ bilinear: 0;
23
+ /** Per phase p, channel c: 1 / Σ_t w_t² of the bilinear kernel (host float64). */
24
+ inverseW2: 192;
25
+ /** 16 Gauss–Legendre nodes then 16 weights on [−1, 1]. */
26
+ gaussLegendre: 208;
27
+ /** Per phase: the entry count (as u32 bits) and 3 pad words, then the entries (dx, dy, dense weights of candidate 0 and 1 per channel). */
28
+ candidates: 240;
29
+ candidatePhaseStride: number;
30
+ size: number;
31
+ }>;
32
+ /** Pixel flag bits of the output flag word. */
33
+ export declare const DISPLAY_VIEW_QUALITY_FLAG: Readonly<{
34
+ /** A site the pixel read had an uncertain lattice material (recompute on the host). */
35
+ materialUncertain: 1;
36
+ /** A site the pixel read lies outside the band's site window (a host geometry error; recompute). */
37
+ siteOutsideWindow: 2;
38
+ /** The correction needed a grid state the table does not hold (recompute, or compute the state and dispatch again). */
39
+ stateMissing: 4;
40
+ /** A sample's pixel variance lies within its bound of the correction threshold (recompute). */
41
+ thresholdTie: 8;
42
+ /** Informational: the pixel's expected value includes the low-signal correction. */
43
+ corrected: 16;
44
+ /** The bits that require the host's recomputation. */
45
+ recompute: 15;
46
+ }>;
47
+ /** Words of the grid-key bitmap: one bit per grid key (AHD_KEY_LEVELS³ keys). */
48
+ export declare const DISPLAY_VIEW_QUALITY_BITMAP_WORDS: number;
49
+ /**
50
+ * A site whose uncertain lattice points (the material module's flag) could move its electrons by more than this fraction of
51
+ * their value is flagged for host recomputation; a smaller uncertainty enters the site's bound instead (the tap weights times
52
+ * the largest value difference between any two materials at that tap).
53
+ */
54
+ export declare const DISPLAY_VIEW_QUALITY_UNCERTAINTY_LIMIT: number;
55
+ /** Bound constant of a clipped-normal slope Φ(z) (in units of E): z's 8E through φ·|z| ≤ 0.24 plus the Abramowitz–Stegun 147E. */
56
+ export declare const DISPLAY_VIEW_QUALITY_SLOPE_U = 200;
57
+ /**
58
+ * Pass 1: the material of every lattice point of every group (the module's `material_at`, its uncertainty in bit 31; the output
59
+ * is `lattice_out`, since a material module may declare its own `lattice`, as #491's composite module does). The group
60
+ * records hold each group's lattice window (lx0, ly0, lw, lh), its first index in the lattice buffer and the point constants
61
+ * c_u = 0.5 + f_x − W/2 and c_v = 0.5 + f_y − H/2 (exact in float32, checked by the host).
62
+ */
63
+ export declare const DISPLAY_VIEW_QUALITY_LATTICE_WGSL: string;
64
+ /**
65
+ * Pass 2: the expected electrons, readout moments and white clip of every native site of the band's window, with their bounds.
66
+ * The site's electrons are Σ_t table[(m_t·3 + c)·T + t] + dark_c, every term non-negative and rounded once on the host, so the
67
+ * sum carries (T + 3)·E relative (`params.site_gamma`, the host's value). An uncertain lattice point adds the tap's value spread
68
+ * over the materials (`spread`, host float64) to the site's uncertainty, which enters its bounds, or flags the site when it
69
+ * exceeds UNCERTAINTY_LIMIT of the electrons. One dispatch covers the `params.count` sites from `params.site_first` (#492: the
70
+ * host splits a band's sites over several dispatches so that no dispatch gathers more than its tap budget).
71
+ */
72
+ export declare const DISPLAY_VIEW_QUALITY_SITES_WGSL: string;
73
+ /**
74
+ * Pass 2 with the defocus disc (#492): the same site electrons from a factored table, summed in blocks.
75
+ *
76
+ * - **Factored table.** A neutral material's value is its reflectance times the white's combined kernel (the reference forms it
77
+ * so: reflectance[m]·neutral_c(t)), so the table holds one neutral row per channel and one row per spectral material, and
78
+ * `materials` holds per material its row (0 for neutral, 1 + s for spectral material s) and its reflectance as float32 bits
79
+ * (1 for a spectral material, an exact product). A scene of thousands of neutral materials (#491's composite: 4 143) then
80
+ * needs 3·T values instead of 3·T per material.
81
+ * - **Blocked sum.** Every group's taps form a dense cw × ch rectangle (the combined kernel's window, row-major from the group's
82
+ * first tap, `groups[g + 5]`), so a site sums each row of a group into a row partial, the rows into a group partial and the
83
+ * groups into the total. No tap records are read: the tap's lattice offset within its group is its (dx, dy).
84
+ *
85
+ * Every term is non-negative and carries two host roundings (u each, u = E/2: the row value and the reflectance), one product
86
+ * (E) and at most cw + ch + G additions, plus the dark electrons' addition, so the electrons carry (cw_max + ch_max + G + 3)·E
87
+ * relative (`params.site_gamma`; recursive summation of the up to 4·(2·96 + 2)² = 150 544 taps of a combined radius of 96 would
88
+ * carry (T + 3)·E ≈ 1.8 % instead). The uncertainty, summed the same way, is scaled by 1 + site_gamma, and `params.site_tiny`
89
+ * (host float64, rounded up) bounds the terms below the float32 normal range, which a device may flush to zero.
90
+ */
91
+ export declare const DISPLAY_VIEW_QUALITY_SITES_BLOCKED_WGSL: string;
92
+ /** Pass 3: the display pixels (see the module comment). */
93
+ export declare const DISPLAY_VIEW_QUALITY_PIXELS_WGSL: string;
94
+ /** The lattice pass with a material module appended (`fn material_at(p: vec2<f32>) -> u32`, `var<private> material_uncertain: bool`). */
95
+ export declare function assembleDisplayViewQualityLatticeWgsl(materialModuleWgsl: string): string;