@photivra/engine 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +156 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +19 -3
  4. package/README.md +6 -6
  5. package/SECURITY.md +1 -1
  6. package/dist/core/portable-math.d.ts +6 -0
  7. package/dist/core/portable-math.js +39 -0
  8. package/dist/core/portable-math.js.map +1 -1
  9. package/dist/core/version.d.ts +1 -1
  10. package/dist/core/version.js +1 -1
  11. package/dist/core/version.js.map +1 -1
  12. package/dist/facade/chart-material-layers.d.ts +115 -0
  13. package/dist/facade/chart-material-layers.js +224 -0
  14. package/dist/facade/chart-material-layers.js.map +1 -0
  15. package/dist/facade/chart-radiance-batch.d.ts +8 -2
  16. package/dist/facade/chart-radiance-batch.js +11 -4
  17. package/dist/facade/chart-radiance-batch.js.map +1 -1
  18. package/dist/facade/chart-scenes.d.ts +134 -0
  19. package/dist/facade/chart-scenes.js +32 -22
  20. package/dist/facade/chart-scenes.js.map +1 -1
  21. package/dist/facade/diffraction-provider.d.ts +8 -2
  22. package/dist/facade/diffraction-provider.js +71 -2
  23. package/dist/facade/diffraction-provider.js.map +1 -1
  24. package/dist/facade/display-view-gpu-session.d.ts +18 -0
  25. package/dist/facade/display-view-gpu-session.js +280 -0
  26. package/dist/facade/display-view-gpu-session.js.map +1 -0
  27. package/dist/facade/display-view-gpu.d.ts +146 -0
  28. package/dist/facade/display-view-gpu.js +59 -0
  29. package/dist/facade/display-view-gpu.js.map +1 -0
  30. package/dist/facade/display-view.d.ts +48 -1
  31. package/dist/facade/display-view.js +7 -7
  32. package/dist/facade/display-view.js.map +1 -1
  33. package/dist/facade/expected-signal-setup.d.ts +5 -0
  34. package/dist/facade/expected-signal-setup.js +16 -0
  35. package/dist/facade/expected-signal-setup.js.map +1 -1
  36. package/dist/facade/expected-signal.d.ts +48 -3
  37. package/dist/facade/expected-signal.js +8 -0
  38. package/dist/facade/expected-signal.js.map +1 -1
  39. package/dist/facade/scene-radiance.d.ts +4 -2
  40. package/dist/facade/scene-radiance.js +7 -4
  41. package/dist/facade/scene-radiance.js.map +1 -1
  42. package/dist/facade/simulate-capture.d.ts +108 -8
  43. package/dist/facade/simulate-capture.js +226 -33
  44. package/dist/facade/simulate-capture.js.map +1 -1
  45. package/dist/facade/simulate-display-view.d.ts +31 -3
  46. package/dist/facade/simulate-display-view.js +73 -13
  47. package/dist/facade/simulate-display-view.js.map +1 -1
  48. package/dist/gpu/chart-composite-gpu.d.ts +119 -0
  49. package/dist/gpu/chart-composite-gpu.js +572 -0
  50. package/dist/gpu/chart-composite-gpu.js.map +1 -0
  51. package/dist/gpu/chart-radiance-gpu.d.ts +6 -6
  52. package/dist/gpu/chart-radiance-gpu.js +9 -11
  53. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  54. package/dist/gpu/display-view-chart-material-module.d.ts +10 -0
  55. package/dist/gpu/display-view-chart-material-module.js +58 -0
  56. package/dist/gpu/display-view-chart-material-module.js.map +1 -0
  57. package/dist/gpu/display-view-composite-material-module.d.ts +68 -0
  58. package/dist/gpu/display-view-composite-material-module.js +248 -0
  59. package/dist/gpu/display-view-composite-material-module.js.map +1 -0
  60. package/dist/gpu/display-view-quality-gpu.d.ts +185 -0
  61. package/dist/gpu/display-view-quality-gpu.js +1000 -0
  62. package/dist/gpu/display-view-quality-gpu.js.map +1 -0
  63. package/dist/gpu/display-view-speed-gpu.d.ts +156 -0
  64. package/dist/gpu/display-view-speed-gpu.js +524 -0
  65. package/dist/gpu/display-view-speed-gpu.js.map +1 -0
  66. package/dist/gpu/image-kernel-accelerator.d.ts +23 -4
  67. package/dist/gpu/image-kernel-accelerator.js +58 -10
  68. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  69. package/dist/gpu/image-kernel-execution.d.ts +55 -3
  70. package/dist/gpu/image-kernel-execution.js.map +1 -1
  71. package/dist/gpu/sensor-stage-gpu.d.ts +11 -0
  72. package/dist/gpu/sensor-stage-gpu.js +42 -13
  73. package/dist/gpu/sensor-stage-gpu.js.map +1 -1
  74. package/dist/gpu/webgpu-runtime.d.ts +4 -2
  75. package/dist/gpu/webgpu-runtime.js +13 -4
  76. package/dist/gpu/webgpu-runtime.js.map +1 -1
  77. package/dist/gpu/webgpu-switch.d.ts +4 -0
  78. package/dist/gpu/webgpu-switch.js +29 -0
  79. package/dist/gpu/webgpu-switch.js.map +1 -0
  80. package/dist/gpu/wgsl-chart-composite-kernel.d.ts +52 -0
  81. package/dist/gpu/wgsl-chart-composite-kernel.js +515 -0
  82. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -0
  83. package/dist/gpu/wgsl-chart-kernel.d.ts +11 -9
  84. package/dist/gpu/wgsl-chart-kernel.js +33 -27
  85. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  86. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +95 -0
  87. package/dist/gpu/wgsl-display-view-quality-kernel.js +769 -0
  88. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -0
  89. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +75 -0
  90. package/dist/gpu/wgsl-display-view-speed-kernel.js +386 -0
  91. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -0
  92. package/dist/gpu/wgsl-sensor-kernel.d.ts +28 -9
  93. package/dist/gpu/wgsl-sensor-kernel.js +94 -50
  94. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  95. package/dist/index.d.ts +5 -2
  96. package/dist/index.js +2 -0
  97. package/dist/index.js.map +1 -1
  98. package/dist/optics/defocus-disc-kernel.d.ts +61 -0
  99. package/dist/optics/defocus-disc-kernel.js +191 -0
  100. package/dist/optics/defocus-disc-kernel.js.map +1 -0
  101. package/docs/API_REFERENCE.md +47 -1
  102. package/docs/QUICK_START.md +150 -5
  103. package/docs/USAGE.md +81 -3
  104. package/docs/api/core-version.md +1 -1
  105. package/docs/api/exports.json +127 -2
  106. package/docs/api/facade-chart-material-layers.md +137 -0
  107. package/docs/api/facade-chart-radiance-batch.md +7 -2
  108. package/docs/api/facade-display-view-gpu.md +94 -0
  109. package/docs/api/facade-scene-radiance.md +3 -2
  110. package/docs/api/facade-simulate-capture.md +95 -11
  111. package/docs/api/facade-simulate-display-view.md +17 -2
  112. package/docs/api/gpu-image-kernel-accelerator.md +10 -3
  113. package/docs/api/gpu-image-kernel-execution.md +71 -4
  114. package/docs/api/gpu-webgpu-switch.md +23 -0
  115. package/package.json +3 -1
@@ -0,0 +1,572 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Host side of the composite-chart radiance kernel (#477, wgsl-chart-composite-kernel.ts): packs a resolved
4
+ * chart's geometry (`chartGeometry`) and a declared scene's mask layers into the kernel's record arrays,
5
+ * keeps the packed records and their GPU buffers per scene so a prepared view uploads them once, probes the
6
+ * device's double-float32 arithmetic, dispatches in bands, and recomputes flagged points with the float64
7
+ * reference. Internal; the public entry point is `ImageKernelAccelerator.evaluateChartRadiance`.
8
+ */
9
+ import { createHostYieldGate } from "../capture/host-yield-cadence.js";
10
+ import { chartGeometry } from "../facade/chart-scenes.js";
11
+ import { ImageKernelExecutionError, webGpuExecution } from "./image-kernel-execution.js";
12
+ import { BUFFER_USAGE, GpuResourceScope, abortedError, linearDispatch, runComputePass } from "./webgpu-runtime.js";
13
+ import { CHART_COMPOSITE_PROBE_LABEL, CHART_COMPOSITE_PROBE_OUTPUTS, CHART_COMPOSITE_PROBE_WGSL, CHART_COMPOSITE_WORKGROUP_SIZE, CHART_ELEMENT_KIND, CHART_PLACEMENT, CHART_RECORD_STRIDE, chartCompositeLabel, chartCompositeWgsl } from "./wgsl-chart-composite-kernel.js";
14
+ const f = Math.fround;
15
+ const FLOAT32_SAFE_MAGNITUDE = 2 ** 120;
16
+ /** Frame-unit magnitude limit for GPU points (shared with the preset kernel). */
17
+ const MAXIMUM_COORDINATE = 2 ** 20;
18
+ /** The rough angle's allowance when the star's sector window is sized: the WGSL atan2 bound (4096 ULP, about 2^-10 rad) with a 4× margin. */
19
+ const STAR_ANGLE_ALLOWANCE = 2 ** -8;
20
+ export function dwSplit(x) { const hi = f(x), lo = f(x - hi); return [hi, lo]; }
21
+ export function twoSum(a, b) { const s = f(a + b), bb = f(s - a); return [s, f(f(a - f(s - bb)) + f(b - bb))]; }
22
+ export function quickTwoSum(a, b) { const s = f(a + b); return [s, f(b - f(s - a))]; }
23
+ function veltkamp(a) { const t = f(4097 * a), h = f(t - f(t - a)); return [h, f(a - h)]; }
24
+ export function twoProd(a, b) {
25
+ const p = f(a * b), [ah, al] = veltkamp(a), [bh, bl] = veltkamp(b);
26
+ const e = f(f(f(f(f(ah * bh) - p) + f(ah * bl)) + f(al * bh)) + f(al * bl));
27
+ return [p, e];
28
+ }
29
+ export function dwAdd(a, b) {
30
+ const s = twoSum(a[0], b[0]), t = twoSum(a[1], b[1]);
31
+ const c = f(s[1] + t[0]), v = quickTwoSum(s[0], c);
32
+ const w = f(v[1] + t[1]);
33
+ return quickTwoSum(v[0], w);
34
+ }
35
+ export function dwSub(a, b) { return dwAdd(a, [-b[0], -b[1]]); }
36
+ export function dwMul(a, b) {
37
+ const p = twoProd(a[0], b[0]), t = f(f(a[0] * b[1]) + f(a[1] * b[0])), l = f(p[1] + t);
38
+ return quickTwoSum(p[0], l);
39
+ }
40
+ /**
41
+ * fma on float32 operands with one rounding to nearest (ties to even), as a fused `fma` evaluates it: the product of two float32
42
+ * values is exact in float64 (48 significant bits), the float64 TwoSum gives the sum's exact error, and the one case where rounding
43
+ * the float64 sum to float32 differs from rounding the exact sum (the float64 sum lands on a float32 midpoint) is resolved by the error's sign.
44
+ */
45
+ export function fmaF32(a, b, c) {
46
+ const p = a * b, s = p + c, bb = s - p, e = (p - (s - bb)) + (c - bb), r = f(s);
47
+ if (e === 0 || r === s || !Number.isFinite(r))
48
+ return r;
49
+ const other = nextFloat32(r, s > r ? 1 : -1);
50
+ if (Math.abs(s - r) !== Math.abs(other - s))
51
+ return r;
52
+ return (e > 0) === (other > r) ? other : r;
53
+ }
54
+ const bits32 = new Float32Array(1), int32 = new Int32Array(bits32.buffer);
55
+ /** The float32 neighbour of a finite float32 x toward +∞ (direction 1) or −∞ (−1). */
56
+ function nextFloat32(x, direction) {
57
+ if (x === 0)
58
+ return direction * 2 ** -149;
59
+ bits32[0] = x;
60
+ int32[0] = int32[0] + ((x > 0) === (direction > 0) ? 1 : -1);
61
+ return bits32[0];
62
+ }
63
+ /** The fma TwoProduct: p = fl(a·b) and its exact error fma(a, b, −p) (exact for a fused fma; Joldes, Muller & Popescu 2017, Algorithm 2). */
64
+ export function twoProdFma(a, b) { const p = f(a * b); return [p, fmaF32(a, b, -p)]; }
65
+ /** The cross-term sum t = a.h·b.l + a.l·b.h of a double-word product: unfused (the default), or with one of the two products fused into the addition. */
66
+ export function dwCrossTerm(a, b, contraction = "none") {
67
+ if (contraction === "first")
68
+ return fmaF32(a[0], b[1], f(a[1] * b[0]));
69
+ if (contraction === "second")
70
+ return fmaF32(a[1], b[0], f(a[0] * b[1]));
71
+ return f(f(a[0] * b[1]) + f(a[1] * b[0]));
72
+ }
73
+ /**
74
+ * The fma path's double-word product (`d_mul_fma`): s = fma(a.h, b.h, t), lo = fl(fma(a.h, b.h, −s) + t), with t the cross-term
75
+ * sum. No rounded product is an operand of an addition, so a contraction cannot change it; see docs/WEBGPU.md for its error bound.
76
+ */
77
+ export function dwMulFma(a, b, t = dwCrossTerm(a, b)) {
78
+ const s = fmaF32(a[0], b[0], t);
79
+ return [s, f(fmaF32(a[0], b[0], -s) + t)];
80
+ }
81
+ /** The Dekker path's double-word product with a given cross-term sum (`dwMul` is it with the unfused one). */
82
+ export function dwMulDekker(a, b, t = dwCrossTerm(a, b)) {
83
+ const p = twoProd(a[0], b[0]), l = f(p[1] + t);
84
+ return quickTwoSum(p[0], l);
85
+ }
86
+ /**
87
+ * Probe inputs (x, y, z, w): pairs whose TwoSum and TwoProduct low parts are non-zero, double words to add and multiply, and sums and
88
+ * products that a directed (truncating or upward) rounding would place on the other neighbour than round-to-nearest: 1 + 3·2^-26
89
+ * rounds up to 1 + 2^-23 (nearest) but down to 1 (truncation); −1 − 3·2^-26 the mirror; 1.25 · (1 + 2^-23) likewise. The last four
90
+ * (2.11.0, the M5 Pro finding) are normalized double words whose products have non-zero TwoProduct errors on both operand pairs, so
91
+ * a contraction of the Dekker path's final Fast2Sum (which then adds the product's error twice) shows in the double-word product.
92
+ */
93
+ export const PROBE_INPUTS = Object.freeze([
94
+ [1, 2 ** -30, 1.5, 2 ** -29], [3.7, 1e-8, -2.25, 3e-9], [0.1, 0.7, 12345.678, -0.0001], [1e6, 1e-3, 1e-6, 1e-9], [-5.5, 2 ** -26, 7, -(2 ** -27)],
95
+ [0.3333333, 0.6666667, 1.0000001, 0.99999994], [1024.5, 1e-5, 0.0078125, 3e-10], [2.5e-3, 4.1e-11, 999.75, 2 ** -40],
96
+ [1, 3 * 2 ** -26, 1.25, 1 + 2 ** -23], [-1, -3 * 2 ** -26, -1.25, 1 + 2 ** -23], [1 + 2 ** -23, 1.25, 1, 5 * 2 ** -26], [0.75, 2 ** -25 + 2 ** -27, 3, 2 ** -24 + 2 ** -26],
97
+ [1.1, 2.2e-9, 1.3, -3.1e-9], [0.7071068, -1.2e-8, 0.7071067, 2.9e-8], [123.456, 3.3e-6, -0.0123, 4.4e-10], [3.14159274, -8.7422777e-8, 2.7182817, 8.2e-8]
98
+ ]);
99
+ /** The components of the probe in output order (see `CHART_COMPOSITE_PROBE_WGSL`). */
100
+ export const PROBE_COMPONENTS = Object.freeze(["two-sum", "two-product", "double-word-add", "double-word-multiply", "fma-two-product", "fma-double-word-multiply"]);
101
+ /**
102
+ * The probe's output per input on a device that evaluates as written, round to nearest, unfused, with a fused `fma`, in the shader's
103
+ * order: [TwoSum lo, Dekker TwoProduct lo, double-word sum lo, fma TwoProduct lo, Dekker product hi, lo, fma product hi, lo].
104
+ */
105
+ export function probeExpectation(input) {
106
+ const [x, y, z, w] = input.map(f);
107
+ const q = dwMulDekker([x, y], [z, w]), qf = dwMulFma([x, y], [z, w]);
108
+ return [twoSum(x, y)[1], twoProd(x, y)[1], dwAdd([x, y], [z, w])[1], twoProdFma(x, y)[1], q[0], q[1], qf[0], qf[1]];
109
+ }
110
+ const hex32 = (x) => { bits32[0] = x; return "0x" + (int32[0] >>> 0).toString(16).padStart(8, "0"); };
111
+ /**
112
+ * Classifies the probe's output (`CHART_COMPOSITE_PROBE_OUTPUTS` floats per input of `PROBE_INPUTS`). TwoSum, both TwoProducts and the
113
+ * double-word sum must equal their exact values bit for bit (each is error-free, or a fixed sequence of roundings to nearest); a
114
+ * double-word product must equal the written-order evaluation for the unfused cross-term sum t = a.h·b.l + a.l·b.h or for either
115
+ * product of t fused into its addition (a contraction WGSL permits, "Reassociation and Fusion", and harmless there: t is an ordinary
116
+ * bounded sum). Anything else is a mismatch, recorded with the float32 bits.
117
+ */
118
+ export function classifyChartCompositeProbe(got) {
119
+ const counts = Object.fromEntries(PROBE_COMPONENTS.map((c) => [c, 0]));
120
+ const inputs = Object.fromEntries(PROBE_COMPONENTS.map((c) => [c, []]));
121
+ const mismatches = [];
122
+ let nonFinite = false, sumZero = true;
123
+ const miss = (component, input, g, want) => {
124
+ counts[component]++;
125
+ inputs[component].push(input);
126
+ if (!Number.isFinite(g))
127
+ nonFinite = true;
128
+ if ((component === "two-sum" || component === "double-word-add") && g !== 0)
129
+ sumZero = false;
130
+ if (mismatches.length < 8)
131
+ mismatches.push({ component, input, got: hex32(g), want: hex32(want) });
132
+ };
133
+ PROBE_INPUTS.forEach((row, i) => {
134
+ const [x, y, z, w] = row.map(f), o = got.subarray(8 * i, 8 * i + 8), want = probeExpectation(row);
135
+ for (const [k, component] of [[0, "two-sum"], [1, "two-product"], [2, "double-word-add"]])
136
+ if (!Object.is(o[k], want[k]))
137
+ miss(component, i, o[k], want[k]);
138
+ const products = (mul, h, l, component) => {
139
+ const candidates = ["none", "first", "second"].map((c) => mul([x, y], [z, w], dwCrossTerm([x, y], [z, w], c)));
140
+ if (!candidates.some((c) => Object.is(c[0], h) && Object.is(c[1], l)))
141
+ miss(component, i, Object.is(candidates[0][0], h) ? l : h, Object.is(candidates[0][0], h) ? candidates[0][1] : candidates[0][0]);
142
+ };
143
+ products(dwMulDekker, o[4], o[5], "double-word-multiply");
144
+ if (!Object.is(o[3], want[3]))
145
+ miss("fma-two-product", i, o[3], want[3]);
146
+ products(dwMulFma, o[6], o[7], "fma-double-word-multiply");
147
+ });
148
+ mismatches.sort((a, b) => PROBE_COMPONENTS.indexOf(a.component) - PROBE_COMPONENTS.indexOf(b.component) || a.input - b.input);
149
+ const sums = counts["two-sum"] + counts["double-word-add"], dekker = counts["two-product"] + counts["double-word-multiply"];
150
+ const fma = counts["fma-two-product"] + counts["fma-double-word-multiply"];
151
+ const deviceClass = nonFinite ? "other" : sums > 0 ? (sumZero ? "reassociated" : "other") : dekker > 0 ? "contracted" : "exact";
152
+ const productPath = sums > 0 || nonFinite ? null : dekker === 0 ? "dekker" : fma === 0 ? "fma" : null;
153
+ return Object.freeze({ deviceClass, productPath, fmaFused: counts["fma-two-product"] === 0, inputs: PROBE_INPUTS.length, mismatchCounts: Object.freeze(counts),
154
+ mismatchedInputs: Object.freeze(Object.fromEntries(PROBE_COMPONENTS.map((c) => [c, Object.freeze(inputs[c])]))),
155
+ mismatches: Object.freeze(mismatches.map((m) => Object.freeze(m))) });
156
+ }
157
+ /** The components' names in a log line, with the probe output they are (s.l, m.l, a.l, q of the probe before 2.11.0; mf.l, qf of the fma path). */
158
+ const COMPONENT_NAMES = { "two-sum": "TwoSum (s.l)", "two-product": "TwoProduct (m.l)", "double-word-add": "double-word add (a.l)",
159
+ "double-word-multiply": "double-word multiply (q)", "fma-two-product": "fma TwoProduct (mf.l)", "fma-double-word-multiply": "fma double-word multiply (qf)" };
160
+ /**
161
+ * One log line for a probe record: the class, the chosen path, each differing component with its count and the probe input
162
+ * indices that differ, and the first difference with its float32 bits.
163
+ */
164
+ export function describeChartCompositeProbe(probe) {
165
+ const differing = PROBE_COMPONENTS.filter((c) => probe.mismatchCounts[c] > 0).map((c) => COMPONENT_NAMES[c] + " ×" + probe.mismatchCounts[c] + " [" + probe.mismatchedInputs[c].join(",") + "]");
166
+ const first = probe.mismatches[0];
167
+ return "class " + probe.deviceClass + ", product path " + (probe.productPath ?? "none") + ", fma " + (probe.fmaFused ? "fused" : "not fused") +
168
+ (differing.length ? "; differing of " + probe.inputs + " inputs: " + differing.join(", ") : "") +
169
+ (first ? "; first: " + COMPONENT_NAMES[first.component] + " input " + first.input + " got " + first.got + " want " + first.want : "");
170
+ }
171
+ /** The probe's refusal: `unsupported-on-backend` with the probe record (`probe`) for the execution diagnostics. */
172
+ export class ChartCompositeProbeError extends ImageKernelExecutionError {
173
+ probe;
174
+ constructor(probe) {
175
+ super("unsupported-on-backend", "This device does not evaluate the composite chart kernel's double-float32 arithmetic as written (" + describeChartCompositeProbe(probe) +
176
+ "); use cpu-reference for composite charts, 2.3 elements and material-layers scenes.");
177
+ this.probe = probe;
178
+ }
179
+ }
180
+ class Packer {
181
+ af = [];
182
+ ai = [];
183
+ floats(values) { const at = this.af.length; for (const x of values)
184
+ this.af.push(x); return at; }
185
+ ints(values) { const at = this.ai.length; for (const x of values)
186
+ this.ai.push(x); return at; }
187
+ reserveInts(count) { const at = this.ai.length; for (let i = 0; i < count; i++)
188
+ this.ai.push(0); return at; }
189
+ }
190
+ /** Sector boundary table of a Siemens star (bar or sine profile): directions, per-sector levels from the reference, bucket index and search window. */
191
+ export function starSectorTable(chart, g) {
192
+ const set = new Set(), normalize = (t) => { let x = t; while (x <= -Math.PI)
193
+ x += 2 * Math.PI; while (x > Math.PI)
194
+ x -= 2 * Math.PI; return x; };
195
+ if (g.profile === "bar") {
196
+ for (let k = 1; k <= 2 * g.cycles; k++)
197
+ set.add(normalize(Math.PI * (k / g.cycles - 1)));
198
+ }
199
+ else {
200
+ for (let L = 1; L < 1024; L++) {
201
+ const a = Math.asin(L / 512 - 1);
202
+ for (let m = 0; m < g.cycles; m++) {
203
+ set.add(normalize((a + 2 * Math.PI * m) / g.cycles - Math.PI));
204
+ set.add(normalize((Math.PI - a + 2 * Math.PI * m) / g.cycles - Math.PI));
205
+ }
206
+ }
207
+ }
208
+ const thetas = Float64Array.from(set).sort(), n = thetas.length, rho = g.r / 2;
209
+ const levels = new Uint32Array(n);
210
+ for (let k = 0; k < n; k++) {
211
+ const next = k + 1 < n ? thetas[k + 1] : thetas[0] + 2 * Math.PI, mid = (thetas[k] + next) / 2;
212
+ levels[k] = chart.materialAt(rho * Math.cos(mid), rho * Math.sin(mid));
213
+ }
214
+ const buckets = new Uint32Array(n);
215
+ for (let j = 0; j < n; j++) {
216
+ const start = -Math.PI + 2 * Math.PI * j / n;
217
+ let lo = 0, hi = n;
218
+ while (lo < hi) {
219
+ const mid = (lo + hi) >> 1;
220
+ if (thetas[mid] >= start)
221
+ hi = mid;
222
+ else
223
+ lo = mid + 1;
224
+ }
225
+ buckets[j] = lo;
226
+ }
227
+ // The window: the most boundaries any angular interval of the allowance (plus two buckets) can hold, capped so the window spans under a half turn.
228
+ const reach = STAR_ANGLE_ALLOWANCE + 4 * Math.PI / n;
229
+ let window = 1;
230
+ for (let k = 0; k < n; k++) {
231
+ let count = 0;
232
+ for (let i = 1; i <= n; i++) {
233
+ const t = k + i < n ? thetas[k + i] : thetas[(k + i) % n] + 2 * Math.PI;
234
+ if (t - thetas[k] <= 2 * reach)
235
+ count++;
236
+ else
237
+ break;
238
+ }
239
+ window = Math.max(window, count + 1);
240
+ }
241
+ window = Math.min(window, Math.max(0, Math.floor(n / 4) - 1));
242
+ return { thetas, levels, buckets, window };
243
+ }
244
+ function packElement(p, chart, offset, header, placement) {
245
+ const g = chartGeometry(chart);
246
+ const [cuH, cuL] = dwSplit(placement.cu), [cvH, cvL] = dwSplit(placement.cv), [sH, sL] = dwSplit(placement.scale);
247
+ const foff = p.floats([cuH, cuL, cvH, cvL, placement.hw, placement.hh, sH, sL, 0, 0]);
248
+ const kindName = g.kind === "composite" ? null : g.kind;
249
+ if (kindName === null)
250
+ throw new ImageKernelExecutionError("unsupported-on-backend", "A composite cannot be a composite element.");
251
+ const kind = CHART_ELEMENT_KIND[kindName];
252
+ let ioff = 0;
253
+ switch (g.kind) {
254
+ case "uniform": break;
255
+ case "slanted-edge":
256
+ p.floats([g.c, g.s]);
257
+ break;
258
+ case "siemens-star": {
259
+ const table = starSectorTable(chart, g), n = table.thetas.length;
260
+ const dirs = [];
261
+ for (let k = 0; k < n; k++) {
262
+ const t = table.thetas[k], c = dwSplit(Math.cos(t)), s = dwSplit(Math.sin(t));
263
+ dirs.push(c[0], c[1], s[0], s[1]);
264
+ }
265
+ p.floats([g.r * g.r]);
266
+ const dirsOff = p.floats(dirs), levelsOff = p.ints(Array.from(table.levels)), bucketsOff = p.ints(Array.from(table.buckets));
267
+ ioff = p.ints([n, dirsOff, levelsOff, bucketsOff, n, table.window, chart.materials.length - 1]);
268
+ break;
269
+ }
270
+ case "step-wedge":
271
+ ioff = p.ints([g.steps]);
272
+ break;
273
+ case "checkerboard":
274
+ p.floats([g.size, chart.aspect]);
275
+ break;
276
+ case "point-grid": {
277
+ p.floats([g.cellW, g.cellH, chart.aspect, g.radius * g.radius]);
278
+ const centers = p.floats(g.centers.flatMap((c) => [c.u, c.v]));
279
+ ioff = p.ints([g.columns, g.rows, centers]);
280
+ break;
281
+ }
282
+ case "patches":
283
+ p.floats([g.cellW, g.cellH, chart.aspect, g.gap / 2, 1 - g.gap / 2]);
284
+ ioff = p.ints([g.columns, g.rows, g.count, g.backgroundIndex]);
285
+ break;
286
+ case "dead-leaves": {
287
+ p.floats([g.u0, g.v0, g.cell]);
288
+ const cells = g.nx * g.ny, csr = p.reserveInts(cells + 1), entries = [];
289
+ for (let c = 0; c < cells; c++) {
290
+ p.ai[csr + c] = entries.length;
291
+ for (let e = g.head[c]; e >= 0; e = g.next[e])
292
+ entries.push(g.entryDisk[e]);
293
+ }
294
+ p.ai[csr + cells] = entries.length;
295
+ const entriesOff = p.ints(entries), disks = [];
296
+ for (let d = 0; d < g.diskCount; d++)
297
+ disks.push(g.X[d], g.Y[d], g.R2[d], g.level[d]);
298
+ const disksOff = p.floats(disks);
299
+ ioff = p.ints([g.nx, g.ny, csr, entriesOff, disksOff, g.mean]);
300
+ break;
301
+ }
302
+ case "bar-groups": {
303
+ const digits = [], rows = [];
304
+ for (const group of g.groups) {
305
+ const b = group.verticalBars, h = group.horizontalBars, label = group.label;
306
+ let digitIndex = 0;
307
+ if (label) {
308
+ digitIndex = digits.length;
309
+ digits.push(label.text.length, ...Array.from(label.text, (ch) => ch.charCodeAt(0) - 48));
310
+ }
311
+ rows.push(group.cell.u0, group.cell.u1, b.u0, b.v0, b.u1, b.v1, h.u0, h.v0, h.u1, h.v1, group.lineWidth, label ? 1 : 0, label?.u0 ?? 0, label?.v0 ?? 0, label?.height ?? 0, digitIndex);
312
+ }
313
+ const gf = p.floats(rows), digitsOff = p.ints(digits.length ? digits : [0]);
314
+ ioff = p.ints([g.groups.length, gf, digitsOff]);
315
+ break;
316
+ }
317
+ case "hyperbolic-wedge":
318
+ p.floats([chart.aspect, g.ph, g.lw0, g.lw1 - g.lw0, 2 * chart.aspect]);
319
+ ioff = p.ints([g.orientation === "horizontal" ? 0 : 1, g.lines]);
320
+ break;
321
+ case "zone-plate": {
322
+ const [kH, kL] = dwSplit(g.k);
323
+ p.floats([g.r * g.r, kH, kL]);
324
+ break;
325
+ }
326
+ case "slanted-squares":
327
+ p.floats([g.cellW, g.cellH, chart.aspect, g.c, g.s, g.half]);
328
+ ioff = p.ints([g.columns, g.rows]);
329
+ break;
330
+ case "line-grid":
331
+ p.floats([g.spacing, g.half]);
332
+ break;
333
+ case "fiducial":
334
+ p.floats([g.r * g.r, g.inner * g.inner]);
335
+ ioff = p.ints([g.inverted ? 1 : 0]);
336
+ break;
337
+ default: throw new ImageKernelExecutionError("unsupported-on-backend", "Unsupported chart element.");
338
+ }
339
+ p.ai[header] = kind;
340
+ p.ai[header + 1] = offset;
341
+ p.ai[header + 2] = ioff;
342
+ p.ai[header + 3] = foff;
343
+ p.ai[header + 4] = placement.mode;
344
+ }
345
+ function packLayer(p, layer, header) {
346
+ const r = layer.rectangle;
347
+ const foff = p.floats([r.u0, r.v0, r.u1, r.v1, layer.scaleX, layer.scaleY, layer.cellWidth, layer.cellHeight]);
348
+ const shapesOff = p.reserveInts(CHART_RECORD_STRIDE.shape * layer.shapes.length);
349
+ layer.shapes.forEach((shape, k) => {
350
+ const h = shapesOff + CHART_RECORD_STRIDE.shape * k;
351
+ if (shape.kind === "annulus") {
352
+ p.ai[h] = 1;
353
+ p.ai[h + 1] = p.floats([...shape.box, shape.centerX, shape.centerY, shape.inner2, shape.outer2]);
354
+ p.ai[h + 2] = 0;
355
+ }
356
+ else {
357
+ const data = p.floats([...shape.box]), edges = shape.edges.length / 4;
358
+ for (let e = 0; e < edges; e++)
359
+ p.floats([shape.edges[4 * e], shape.edges[4 * e + 1], shape.edges[4 * e + 2], shape.edges[4 * e + 3]]);
360
+ p.ai[h] = 0;
361
+ p.ai[h + 1] = data;
362
+ p.ai[h + 2] = edges;
363
+ }
364
+ });
365
+ const cells = layer.cellsX * layer.cellsY, cellStart = p.reserveInts(cells + 1), list = [];
366
+ for (let c = 0; c < cells; c++) {
367
+ p.ai[cellStart + c] = list.length;
368
+ list.push(...layer.cells[c]);
369
+ }
370
+ p.ai[cellStart + cells] = list.length;
371
+ const cellList = p.ints(list.length ? list : [0]);
372
+ const words = [layer.cellsX, layer.cellsY, cellStart, cellList, shapesOff, layer.shapes.length, foff, layer.backgroundMaterial, layer.inkMaterial];
373
+ words.forEach((w, i) => { p.ai[header + i] = w; });
374
+ }
375
+ /** Packs the plan's chart (a composite, an element preset, or the chart under layers) and its layers into the kernel's record arrays. */
376
+ export function packChartScene(plan) {
377
+ const p = new Packer(), chart = plan.chart, g = chartGeometry(chart), layers = plan.layers?.layers ?? [];
378
+ const elements = g.kind === "composite" ? g.elements : [];
379
+ const elementCount = elements.length + 1;
380
+ p.reserveInts(4);
381
+ const he = p.reserveInts(CHART_RECORD_STRIDE.element * elementCount), hl = p.reserveInts(CHART_RECORD_STRIDE.layer * layers.length);
382
+ const frame = { mode: CHART_PLACEMENT.frame, cu: 0, cv: 0, hw: 0, hh: 0, scale: 1 };
383
+ packElement(p, g.kind === "composite" ? g.background : chart, 0, he, frame);
384
+ elements.forEach((e, i) => packElement(p, e.chart, e.offset, he + CHART_RECORD_STRIDE.element * (i + 1), { mode: e.ellipse ? CHART_PLACEMENT.ellipse : CHART_PLACEMENT.rectangle, cu: e.cu, cv: e.cv, hw: e.hw, hh: e.hh, scale: e.scale }));
385
+ layers.forEach((layer, l) => packLayer(p, layer, hl + CHART_RECORD_STRIDE.layer * l));
386
+ p.ai[0] = elementCount;
387
+ p.ai[1] = layers.length;
388
+ p.ai[2] = he;
389
+ p.ai[3] = hl;
390
+ for (const x of p.af)
391
+ if (!(Math.abs(x) <= FLOAT32_SAFE_MAGNITUDE))
392
+ throw new ImageKernelExecutionError("unsupported-float32-domain", "A chart constant exceeds the float32 domain.");
393
+ for (const x of p.ai)
394
+ if (!(Number.isInteger(x) && x >= 0 && x <= 0xffffffff))
395
+ throw new ImageKernelExecutionError("unsupported-on-backend", "Chart records exceed the kernel's index range.");
396
+ return { af: Float32Array.from(p.af), ai: Uint32Array.from(p.ai), materials: plan.materials.length, elementCount, layerCount: layers.length };
397
+ }
398
+ /** Per-material values in float64 (the reference's operations), rounded once; a failing spectrum is NaN and its points are recomputed. */
399
+ export function chartMaterialTable(plan) {
400
+ const nm = plan.wavelengthsNanometers, stride = plan.mode === "radiance" ? nm.length : plan.columns, materials = plan.materials.length;
401
+ const table = new Float32Array(materials * stride);
402
+ for (let m = 0; m < materials; m++) {
403
+ for (let c = 0; c < stride; c++) {
404
+ let value;
405
+ try {
406
+ if (plan.mode === "radiance")
407
+ value = plan.materialRadiance(m, nm[c]);
408
+ else {
409
+ value = 0;
410
+ const row = plan.channelWeights[c];
411
+ for (let j = 0; j < nm.length; j++)
412
+ value += row[j] * plan.materialRadiance(m, nm[j]);
413
+ }
414
+ }
415
+ catch {
416
+ value = Number.NaN;
417
+ }
418
+ if (Math.abs(value) > FLOAT32_SAFE_MAGNITUDE)
419
+ throw new ImageKernelExecutionError("unsupported-float32-domain", "Chart values exceed the float32 domain.");
420
+ table[m * stride + c] = value;
421
+ }
422
+ }
423
+ return table;
424
+ }
425
+ /** The scene key of a batch input: the scene's data and the aspect (every record derives from them). */
426
+ export function chartSceneKey(scene, aspect) { return aspect + "|" + JSON.stringify(scene); }
427
+ /**
428
+ * The device's double-float32 probe (`CHART_COMPOSITE_PROBE_WGSL`, classified by `classifyChartCompositeProbe`): resolves with the probe
429
+ * record (its `productPath` is the variant the composite kernel is compiled with), or rejects with `ChartCompositeProbeError`
430
+ * (`unsupported-on-backend`, the record in `probe`) on a device that neither product path holds on. Run once per accelerator by
431
+ * `ChartSceneUploads.probe`, and once per display-view renderer by the composite material module (#491).
432
+ */
433
+ export async function probeChartCompositeDevice(runtime, pipelines, signal) {
434
+ const pipeline = await pipelines(CHART_COMPOSITE_PROBE_LABEL, CHART_COMPOSITE_PROBE_WGSL), scope = new GpuResourceScope(runtime);
435
+ const bytes = 4 * CHART_COMPOSITE_PROBE_OUTPUTS * PROBE_INPUTS.length;
436
+ let record;
437
+ try {
438
+ const input = scope.upload("photivra-chart-probe-in", Float32Array.from(PROBE_INPUTS.flat()), BUFFER_USAGE.STORAGE);
439
+ const output = scope.buffer("photivra-chart-probe-out", bytes, BUFFER_USAGE.STORAGE | BUFFER_USAGE.COPY_SRC);
440
+ // The rounding barrier's zero word (CHART_COMPOSITE_PROBE_WGSL `probe_zero`).
441
+ const zero = scope.upload("photivra-chart-probe-zero", new Uint32Array(4), BUFFER_USAGE.UNIFORM);
442
+ const bindGroup = runtime.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: input } }, { binding: 1, resource: { buffer: output } },
443
+ { binding: 2, resource: { buffer: zero } }] });
444
+ const [data] = await runComputePass(runtime, scope, pipeline, bindGroup, [PROBE_INPUTS.length, 1], [{ buffer: output, bytes }], signal);
445
+ record = classifyChartCompositeProbe(new Float32Array(data, 0, CHART_COMPOSITE_PROBE_OUTPUTS * PROBE_INPUTS.length));
446
+ }
447
+ finally {
448
+ scope.release();
449
+ }
450
+ if (record.productPath === null)
451
+ throw new ChartCompositeProbeError(record);
452
+ return record;
453
+ }
454
+ /** Per accelerator: the uploaded records of recent scenes (a prepared view's scene is uploaded once) and the device probe. */
455
+ export class ChartSceneUploads {
456
+ runtime;
457
+ keep;
458
+ #entries = [];
459
+ #probed = null;
460
+ constructor(runtime, keep = 4) {
461
+ this.runtime = runtime;
462
+ this.keep = keep;
463
+ }
464
+ /** The device's double-float32 probe, run once per accelerator (single flight; a refusal is retried on the next call): the record, or `ChartCompositeProbeError`. */
465
+ probe(pipelines, signal) {
466
+ this.#probed ??= probeChartCompositeDevice(this.runtime, pipelines, signal).catch((error) => { this.#probed = null; throw error; });
467
+ return this.#probed;
468
+ }
469
+ get(plan, scene, aspect) {
470
+ const key = chartSceneKey(scene, aspect), index = this.#entries.findIndex((e) => e.key === key);
471
+ if (index >= 0) {
472
+ const [entry] = this.#entries.splice(index, 1);
473
+ this.#entries.push(entry);
474
+ return entry;
475
+ }
476
+ const packed = packChartScene(plan);
477
+ if (Math.max(packed.af.byteLength, packed.ai.byteLength) > this.runtime.maximumStorageBindingBytes) {
478
+ throw new ImageKernelExecutionError("insufficient-limits", "Chart records exceed the device's storage binding limit.");
479
+ }
480
+ const scope = new GpuResourceScope(this.runtime);
481
+ const entry = { key, scope, af: scope.upload("photivra-chart-floats", packed.af, BUFFER_USAGE.STORAGE), ai: scope.upload("photivra-chart-records", packed.ai, BUFFER_USAGE.STORAGE), packed };
482
+ this.#entries.push(entry);
483
+ while (this.#entries.length > this.keep)
484
+ this.#entries.shift().scope.release();
485
+ return entry;
486
+ }
487
+ /** Scenes currently uploaded (for tests). */
488
+ get size() { return this.#entries.length; }
489
+ release() { for (const entry of this.#entries.splice(0))
490
+ entry.scope.release(); }
491
+ }
492
+ function checkAbort(signal) { if (signal.aborted)
493
+ throw abortedError(); }
494
+ /** Points as float32 pairs (hi, lo) per coordinate: the float64 point to 2^-48. */
495
+ export function splitPoints(points) {
496
+ const out = new Float32Array(points.length * 2);
497
+ for (let i = 0; i < points.length; i++) {
498
+ const hi = f(points[i]);
499
+ out[2 * i] = hi;
500
+ out[2 * i + 1] = f(points[i] - hi);
501
+ }
502
+ return out;
503
+ }
504
+ export async function evaluateChartCompositeOnGpu(runtime, bandBytes, plan, scene, points, indices, abort, yieldControl, pipelines, uploads) {
505
+ for (let i = 0; i < points.length; i++) {
506
+ if (Math.abs(points[i]) > MAXIMUM_COORDINATE)
507
+ throw new ImageKernelExecutionError("unsupported-float32-domain", "Chart points beyond 2^20 frame units are outside the float32 domain.");
508
+ }
509
+ const probe = await uploads.probe(pipelines, abort.signal), path = probe.productPath ?? "dekker";
510
+ const uploaded = uploads.get(plan, scene, plan.chart.aspect), table = chartMaterialTable(plan), radianceMode = plan.mode === "radiance";
511
+ const columns = plan.columns, stride = radianceMode ? plan.wavelengthsNanometers.length : columns, count = plan.queryCount;
512
+ const bytesPerQuery = 16 + 4 * columns + 4 + 4, rows = Math.max(1, Math.floor(bandBytes / bytesPerQuery));
513
+ if (Math.max(table.byteLength, Math.min(rows, count) * 4 * columns) > runtime.maximumStorageBindingBytes) {
514
+ throw new ImageKernelExecutionError("insufficient-limits", "Chart tables or bands exceed the device's storage binding limit.");
515
+ }
516
+ const params = new ArrayBuffer(16), words = new Uint32Array(params);
517
+ words[1] = stride;
518
+ words[2] = radianceMode ? 1 : 0;
519
+ words[3] = 0; // words[3]: the rounding barrier's zero (chartCompositeWgsl `params.zero`)
520
+ const pipeline = await pipelines(chartCompositeLabel(path), chartCompositeWgsl(path));
521
+ const values = new Float32Array(count * columns), recomputed = [], split = splitPoints(points);
522
+ const scope = new GpuResourceScope(runtime);
523
+ try {
524
+ const tableBuffer = scope.upload("photivra-chart-table", table, BUFFER_USAGE.STORAGE), gate = createHostYieldGate();
525
+ for (let first = 0; first < count; first += rows) {
526
+ checkAbort(abort.signal);
527
+ const n = Math.min(rows, count - first);
528
+ words[0] = n;
529
+ const band = new GpuResourceScope(runtime);
530
+ try {
531
+ const pointBuffer = band.upload("photivra-chart-points", split.subarray(4 * first, 4 * (first + n)), BUFFER_USAGE.STORAGE);
532
+ const selectors = band.upload("photivra-chart-selectors", indices ? Uint32Array.from(indices.subarray(first, first + n)) : new Uint32Array(1), BUFFER_USAGE.STORAGE);
533
+ const out = band.buffer("photivra-chart-values", n * 4 * columns, BUFFER_USAGE.STORAGE | BUFFER_USAGE.COPY_SRC);
534
+ const flagBuffer = band.buffer("photivra-chart-flags", n * 4, BUFFER_USAGE.STORAGE | BUFFER_USAGE.COPY_SRC);
535
+ const uniform = band.upload("photivra-chart-params", new Uint8Array(params), BUFFER_USAGE.UNIFORM);
536
+ const bindGroup = runtime.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [
537
+ { binding: 0, resource: { buffer: uniform } }, { binding: 1, resource: { buffer: pointBuffer } }, { binding: 2, resource: { buffer: tableBuffer } },
538
+ { binding: 3, resource: { buffer: uploaded.af } }, { binding: 4, resource: { buffer: uploaded.ai } }, { binding: 5, resource: { buffer: selectors } },
539
+ { binding: 6, resource: { buffer: out } }, { binding: 7, resource: { buffer: flagBuffer } }
540
+ ] });
541
+ const [valueData, flagData] = await runComputePass(runtime, band, pipeline, bindGroup, linearDispatch(n, CHART_COMPOSITE_WORKGROUP_SIZE, runtime.maximumWorkgroupsPerDimension), [{ buffer: out, bytes: n * 4 * columns }, { buffer: flagBuffer, bytes: n * 4 }], abort.signal);
542
+ values.set(new Float32Array(valueData, 0, n * columns), first * columns);
543
+ const bandFlags = new Uint32Array(flagData, 0, n);
544
+ for (let i = 0; i < n; i++) {
545
+ let recompute = bandFlags[i] !== 0;
546
+ for (let c = 0; c < columns && !recompute; c++)
547
+ if (!Number.isFinite(values[(first + i) * columns + c]))
548
+ recompute = true;
549
+ if (recompute)
550
+ recomputed.push(first + i);
551
+ }
552
+ }
553
+ finally {
554
+ band.release();
555
+ }
556
+ if (yieldControl && gate.due()) {
557
+ await yieldControl(abort.signal);
558
+ gate.restart();
559
+ }
560
+ }
561
+ }
562
+ finally {
563
+ scope.release();
564
+ }
565
+ checkAbort(abort.signal);
566
+ for (const q of recomputed)
567
+ for (let c = 0; c < columns; c++)
568
+ values[q * columns + c] = plan.queryValue(points, indices, q, c);
569
+ const execution = webGpuExecution({ kind: "relative-to-reference", relative: 2 ** -24, absolute: 2 ** -126 }, 0);
570
+ return { plan, values, columns, execution, hostRecomputedQueryIndices: Uint32Array.from(recomputed) };
571
+ }
572
+ //# sourceMappingURL=chart-composite-gpu.js.map