@photivra/engine 2.7.0 → 2.8.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 (42) hide show
  1. package/CHANGELOG.md +86 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/dist/capture/native-environment-lean.d.ts +35 -5
  6. package/dist/capture/native-environment-lean.js +175 -16
  7. package/dist/capture/native-environment-lean.js.map +1 -1
  8. package/dist/capture/native-environment-raw.d.ts +9 -1
  9. package/dist/capture/native-environment-raw.js +14 -9
  10. package/dist/capture/native-environment-raw.js.map +1 -1
  11. package/dist/capture/native-environment-tabulated.d.ts +58 -0
  12. package/dist/capture/native-environment-tabulated.js +91 -0
  13. package/dist/capture/native-environment-tabulated.js.map +1 -0
  14. package/dist/core/internal-radiance-evaluator.d.ts +28 -0
  15. package/dist/core/internal-radiance-evaluator.js +30 -0
  16. package/dist/core/internal-radiance-evaluator.js.map +1 -1
  17. package/dist/core/version.d.ts +1 -1
  18. package/dist/core/version.js +1 -1
  19. package/dist/core/version.js.map +1 -1
  20. package/dist/facade/diffraction-provider.js +30 -2
  21. package/dist/facade/diffraction-provider.js.map +1 -1
  22. package/dist/facade/display-view.d.ts +14 -83
  23. package/dist/facade/display-view.js +331 -846
  24. package/dist/facade/display-view.js.map +1 -1
  25. package/dist/facade/expected-signal-setup.d.ts +65 -0
  26. package/dist/facade/expected-signal-setup.js +56 -0
  27. package/dist/facade/expected-signal-setup.js.map +1 -0
  28. package/dist/facade/expected-signal.d.ts +83 -0
  29. package/dist/facade/expected-signal.js +571 -0
  30. package/dist/facade/expected-signal.js.map +1 -0
  31. package/dist/facade/scene-radiance.d.ts +6 -0
  32. package/dist/facade/scene-radiance.js +31 -10
  33. package/dist/facade/scene-radiance.js.map +1 -1
  34. package/dist/facade/simulate-capture.js +65 -16
  35. package/dist/facade/simulate-capture.js.map +1 -1
  36. package/dist/facade/simulate-display-view.js +26 -49
  37. package/dist/facade/simulate-display-view.js.map +1 -1
  38. package/docs/API_REFERENCE.md +1 -1
  39. package/docs/QUICK_START.md +1 -1
  40. package/docs/api/core-version.md +1 -1
  41. package/docs/api/exports.json +2 -2
  42. package/package.json +1 -1
@@ -0,0 +1,571 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * The tabulated expected signal (2.6 display view; #394 also the capture's tabulated RAW path): expected photo electrons of
4
+ * real native sites for a still, rectilinear, distortion-free planar scene. Kept apart from display-view.ts so a bundle that
5
+ * only captures does not contain the display view or the portable functions it uses (issue #380).
6
+ */
7
+ import { InvalidConfigurationError } from "../core/configuration-error.js";
8
+ const OFFSET_TOLERANCE = 1e-9;
9
+ /** Largest cache window, in sites or lattice points; larger windows are evaluated without caching. */
10
+ const CACHE_LIMIT = 1 << 22;
11
+ /** Largest band of looked-up lattice materials per group, in lattice points (a band that would be larger restarts at the window). */
12
+ const BAND_LIMIT = 1 << 22;
13
+ /**
14
+ * Expected photo electrons per site: μ_c(i, j) = Σ_o w_o Σ_λ A_c(λ) Σ_t K_λ(t)·L(p_o(i, j) − t, λ), where p_o is the
15
+ * query point of offset o and K the diffraction kernel (a single unit tap without diffraction). This is the sum the
16
+ * facade's environment chain forms for a still, rectilinear, distortion-free planar scene, where the
17
+ * radiance-to-irradiance factor is the same at every site (docs/DISPLAY_VIEW.md, "Expected signal").
18
+ *
19
+ * With diffraction, offsets that share a fractional shift read one lattice, and their kernels are combined in advance:
20
+ * C_c(d) = Σ_members w·Σ_λ A_c(λ)·K_λ(t)·L(λ) over the taps t that reach lattice offset d. A neutral material's radiance
21
+ * is its reflectance times the white radiance, so all neutral materials share one combined kernel per channel.
22
+ */
23
+ export function createDisplayViewExpectedSignal(input) {
24
+ const W = input.nativeWidth, H = input.nativeHeight, g = 2 / input.outputWidth, nodes = input.wavelengthsNm.length;
25
+ const R = input.kernels ? input.kernels.radius : 0, size = 2 * R + 1;
26
+ if (input.kernels && input.kernels.perNode.length !== nodes)
27
+ throw new InvalidConfigurationError("Display view kernels must match the spectral nodes.");
28
+ if (!input.kernels && !input.queryPoint)
29
+ throw new InvalidConfigurationError("Display view needs the environment query points without diffraction.");
30
+ const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets, channelAt = input.channelAt;
31
+ const kernelAt = (node, t) => (input.kernels ? input.kernels.perNode[node][t] : 1);
32
+ // Materials: neutral ones by reflectance, spectral ones by index.
33
+ let reflectance = new Float64Array(0), spectralIndex = new Int32Array(0);
34
+ const spectral = [];
35
+ if (scene.kind === "materials") {
36
+ reflectance = new Float64Array(scene.materials.length);
37
+ spectralIndex = new Int32Array(scene.materials.length).fill(-1);
38
+ scene.materials.forEach((m, index) => { if ("neutral" in m)
39
+ reflectance[index] = m.neutral;
40
+ else {
41
+ spectralIndex[index] = spectral.length;
42
+ spectral.push(m.radiance);
43
+ } });
44
+ }
45
+ // Offsets grouped by fractional shift: offsets that differ by whole sites share one lattice.
46
+ const frac = (v) => { const f = v - Math.floor(v); return f > 1 - OFFSET_TOLERANCE || f < OFFSET_TOLERANCE ? 0 : f; };
47
+ const groups = [];
48
+ for (const o of offsets) {
49
+ const fx = frac(o.x), fy = frac(o.y), ix = Math.round(o.x - fx), iy = Math.round(o.y - fy);
50
+ let group = groups.find((q) => Math.abs(q.fx - fx) <= OFFSET_TOLERANCE && Math.abs(q.fy - fy) <= OFFSET_TOLERANCE);
51
+ if (!group) {
52
+ group = { fx, fy, members: [], minIx: ix, maxIx: ix, minIy: iy, maxIy: iy, cw: 0, ch: 0, neutral: [], spectral: [], perNode: [], cache: new Int32Array(0), cx0: 0, cy0: 0, cpw: 0, cph: 0 };
53
+ groups.push(group);
54
+ }
55
+ group.members.push({ ix, iy, weight: o.weight });
56
+ group.minIx = Math.min(group.minIx, ix);
57
+ group.maxIx = Math.max(group.maxIx, ix);
58
+ group.minIy = Math.min(group.minIy, iy);
59
+ group.maxIy = Math.max(group.maxIy, iy);
60
+ }
61
+ if (input.kernels)
62
+ for (const group of groups) {
63
+ // Combined kernel over lattice offsets d = (ix − tu, iy − tv), stored at (d − min + R).
64
+ const cw = group.maxIx - group.minIx + size, ch = group.maxIy - group.minIy + size;
65
+ group.cw = cw;
66
+ group.ch = ch;
67
+ const combine = (value) => [0, 1, 2].map((c) => {
68
+ const out = new Float64Array(cw * ch);
69
+ for (const member of group.members)
70
+ for (let tv = -R, t = 0; tv <= R; tv++)
71
+ for (let tu = -R; tu <= R; tu++, t++) {
72
+ let sum = 0;
73
+ for (let n = 0; n < nodes; n++)
74
+ sum += value(c, n, t);
75
+ const index = (member.iy - tv - group.minIy + R) * cw + (member.ix - tu - group.minIx + R);
76
+ out[index] = out[index] + member.weight * sum;
77
+ }
78
+ return out;
79
+ });
80
+ if (scene.kind === "materials") {
81
+ group.neutral = combine((c, n, t) => A[c][n] * kernelAt(n, t) * scene.whiteRadiance[n]);
82
+ const perMaterial = spectral.map((radiance) => combine((c, n, t) => A[c][n] * kernelAt(n, t) * radiance[n]));
83
+ group.spectral = [0, 1, 2].map((c) => perMaterial.map((tables) => tables[c]));
84
+ }
85
+ else {
86
+ // Radiance scenes: combined kernels per node, applied to the cached radiance at each node.
87
+ group.perNode = Array.from({ length: nodes }, (_, n) => combine((c, node, t) => (node === n ? A[c][n] * kernelAt(n, t) : 0)));
88
+ }
89
+ }
90
+ // Neutral tap-0 signal per channel and spectral per material, for the direct (no-kernel) path.
91
+ const neutralDirect = scene.kind === "materials" ? [0, 1, 2].map((c) => { let s = 0; for (let n = 0; n < nodes; n++)
92
+ s += A[c][n] * scene.whiteRadiance[n]; return s; }) : [];
93
+ const spectralDirect = [0, 1, 2].map((c) => spectral.map((radiance) => { let s = 0; for (let n = 0; n < nodes; n++)
94
+ s += A[c][n] * radiance[n]; return s; }));
95
+ let lookups = 0;
96
+ const point = new Float64Array(2);
97
+ const materialSignalDirect = (m, c) => {
98
+ const si = spectralIndex[m];
99
+ if (si === undefined)
100
+ throw new InvalidConfigurationError("Unknown chart material.");
101
+ return si < 0 ? reflectance[m] * neutralDirect[c] : spectralDirect[c][si];
102
+ };
103
+ /** Without diffraction: one site from the environment's own query points. */
104
+ function directSite(x, y) {
105
+ const c = channelAt(x, y);
106
+ let sum = 0;
107
+ for (let o = 0; o < offsets.length; o++) {
108
+ input.queryPoint(x, y, o, point);
109
+ let value;
110
+ if (scene.kind === "materials") {
111
+ value = materialSignalDirect(scene.materialAt(point[0], point[1]), c);
112
+ lookups++;
113
+ }
114
+ else {
115
+ value = 0;
116
+ for (let n = 0; n < nodes; n++)
117
+ value += A[c][n] * scene.radianceAt(point[0], point[1], input.wavelengthsNm[n]);
118
+ lookups += nodes;
119
+ }
120
+ sum += offsets[o].weight * value;
121
+ }
122
+ return sum;
123
+ }
124
+ // Lattice point (p, q) of a group: the facade's lattice formula (createDiffractedPlanarProvider).
125
+ const latticeU = (group, p) => (group.fx === 0 ? (p + 0.5 - W / 2) * g : (p + 0.5 + group.fx - W / 2) * g);
126
+ const latticeV = (group, q) => (group.fy === 0 ? -(q + 0.5 - H / 2) * g : -(q + 0.5 + group.fy - H / 2) * g);
127
+ const radianceScratch = new Float64Array(nodes);
128
+ /** Material (or radiance at each node, into radianceScratch) of a group's lattice point, from the window cache when it holds it. */
129
+ function latticeMaterial(group, p, q) {
130
+ const i = p - group.cx0, j = q - group.cy0, cached = i >= 0 && j >= 0 && i < group.cpw && j < group.cph;
131
+ if (cached) {
132
+ const m = group.cache[j * group.cpw + i];
133
+ if (m >= 0)
134
+ return m;
135
+ }
136
+ const m = scene.materialAt(latticeU(group, p), latticeV(group, q));
137
+ lookups++;
138
+ if (cached)
139
+ group.cache[j * group.cpw + i] = m;
140
+ return m;
141
+ }
142
+ function latticeRadiance(group, p, q) {
143
+ const i = p - group.cx0, j = q - group.cy0, cached = i >= 0 && j >= 0 && i < group.cpw && j < group.cph, cache = group.cache;
144
+ if (cached && !Number.isNaN(cache[(j * group.cpw + i) * nodes])) {
145
+ for (let n = 0; n < nodes; n++)
146
+ radianceScratch[n] = cache[(j * group.cpw + i) * nodes + n];
147
+ return radianceScratch;
148
+ }
149
+ const u = latticeU(group, p), v = latticeV(group, q);
150
+ for (let n = 0; n < nodes; n++)
151
+ radianceScratch[n] = scene.radianceAt(u, v, input.wavelengthsNm[n]);
152
+ lookups += nodes;
153
+ if (cached)
154
+ for (let n = 0; n < nodes; n++)
155
+ cache[(j * group.cpw + i) * nodes + n] = radianceScratch[n];
156
+ return radianceScratch;
157
+ }
158
+ /** With diffraction: one site through the combined kernels on each group's lattice. */
159
+ function kernelSite(x, y) {
160
+ const c = channelAt(x, y);
161
+ let sum = 0;
162
+ for (const group of groups) {
163
+ const p0 = x + group.minIx - R, q0 = y + group.minIy - R, cw = group.cw, ch = group.ch;
164
+ if (scene.kind === "materials") {
165
+ const nk = group.neutral[c], sk = group.spectral[c];
166
+ for (let dy = 0; dy < ch; dy++)
167
+ for (let dx = 0; dx < cw; dx++) {
168
+ const m = latticeMaterial(group, p0 + dx, q0 + dy), si = spectralIndex[m];
169
+ if (si === undefined)
170
+ throw new InvalidConfigurationError("Unknown chart material.");
171
+ sum += si < 0 ? reflectance[m] * nk[dy * cw + dx] : sk[si][dy * cw + dx];
172
+ }
173
+ }
174
+ else {
175
+ for (let dy = 0; dy < ch; dy++)
176
+ for (let dx = 0; dx < cw; dx++) {
177
+ const radiance = latticeRadiance(group, p0 + dx, q0 + dy);
178
+ for (let n = 0; n < nodes; n++)
179
+ sum += group.perNode[n][c][dy * cw + dx] * radiance[n];
180
+ }
181
+ }
182
+ }
183
+ return sum;
184
+ }
185
+ // Filled windows (chart materials with diffraction, every lattice point of the window cacheable): each group keeps a band of
186
+ // looked-up lattice materials that covers the window's rows and grows to the right as the windows of a display row move
187
+ // right, with summed-area tables of material changes. A site whose kernel reads one material only (each group's rectangle
188
+ // uniform, the same material in all) takes the value of the same sum over that material, precomputed below in the same
189
+ // order; other sites run the same sum from the band. Either way each site's value is the same sequence of operations as
190
+ // kernelSite, so it is bit-identical. The scene lookup count stays the count kernelSite's lazy window cache makes (each
191
+ // cache point a computed site reads counts once per window, points outside the cache once per read): the rectangles of
192
+ // the sites computed in a window are counted when the window closes (windowLookups).
193
+ const uniformSite = scene.kind === "materials" && input.kernels ? [0, 1, 2].map((c) => Float64Array.from(scene.materials, (_, m) => {
194
+ const si = spectralIndex[m];
195
+ let sum = 0;
196
+ for (const group of groups) {
197
+ const nk = group.neutral[c], sk = si < 0 ? nk : group.spectral[c][si];
198
+ for (let t = 0; t < group.cw * group.ch; t++)
199
+ sum += si < 0 ? reflectance[m] * nk[t] : sk[t];
200
+ }
201
+ return sum;
202
+ })) : [];
203
+ const allNeutral = spectral.length === 0;
204
+ const bands = groups.map(() => ({ x0: 0, y0: 0, rows: 0, capacity: 0, filled: 0, material: new Int32Array(0), reflectance: new Float64Array(0),
205
+ changeLeft: new Int32Array(0), changeUp: new Int32Array(0), v: new Float64Array(0) }));
206
+ /** Makes the band of group g cover lattice columns [x0, x1) of rows [y0, y0 + rows), extending it to the right when it can. */
207
+ function coverBand(g, x0, x1, y0, rows) {
208
+ const group = groups[g], band = bands[g], materials = scene;
209
+ if (!(band.rows === rows && band.y0 === y0 && x0 >= band.x0 && x1 - band.x0 <= band.capacity)) {
210
+ const capacity = Math.max(x1 - x0, Math.min(Math.floor(BAND_LIMIT / rows), W + 2 * (x1 - x0)));
211
+ const size = rows * capacity, tables = (rows + 1) * (capacity + 1);
212
+ band.x0 = x0;
213
+ band.y0 = y0;
214
+ band.rows = rows;
215
+ band.capacity = capacity;
216
+ band.filled = 0;
217
+ if (band.material.length < size) {
218
+ band.material = new Int32Array(size);
219
+ band.reflectance = new Float64Array(allNeutral ? size : 0);
220
+ }
221
+ if (band.changeLeft.length < tables) {
222
+ band.changeLeft = new Int32Array(tables);
223
+ band.changeUp = new Int32Array(tables);
224
+ }
225
+ band.changeLeft.fill(0, 0, capacity + 1);
226
+ band.changeUp.fill(0, 0, capacity + 1);
227
+ for (let j = 0; j < rows; j++) {
228
+ band.changeLeft[(j + 1) * (capacity + 1)] = 0;
229
+ band.changeUp[(j + 1) * (capacity + 1)] = 0;
230
+ }
231
+ if (band.v.length < rows)
232
+ band.v = new Float64Array(rows);
233
+ for (let j = 0; j < rows; j++)
234
+ band.v[j] = latticeV(group, y0 + j);
235
+ }
236
+ const stride = band.capacity, S1 = stride + 1, material = band.material, left = band.changeLeft, up = band.changeUp;
237
+ for (let i = band.filled; i < x1 - band.x0; i++) {
238
+ const u = latticeU(group, band.x0 + i);
239
+ for (let j = 0; j < rows; j++) {
240
+ const m = materials.materialAt(u, band.v[j]);
241
+ if (spectralIndex[m] === undefined)
242
+ throw new InvalidConfigurationError("Unknown chart material.");
243
+ material[j * stride + i] = m;
244
+ if (allNeutral)
245
+ band.reflectance[j * stride + i] = reflectance[m];
246
+ const changedLeft = i > 0 && material[j * stride + i - 1] !== m ? 1 : 0, changedUp = j > 0 && material[(j - 1) * stride + i] !== m ? 1 : 0;
247
+ left[(j + 1) * S1 + i + 1] = left[j * S1 + i + 1] + left[(j + 1) * S1 + i] - left[j * S1 + i] + changedLeft;
248
+ up[(j + 1) * S1 + i + 1] = up[j * S1 + i + 1] + up[(j + 1) * S1 + i] - up[j * S1 + i] + changedUp;
249
+ }
250
+ }
251
+ band.filled = Math.max(band.filled, x1 - band.x0);
252
+ }
253
+ const rectSum = (sat, S1, i0, j0, i1, j1) => sat[j1 * S1 + i1] - sat[j0 * S1 + i1] - sat[j1 * S1 + i0] + sat[j0 * S1 + i0];
254
+ let filled = false;
255
+ /** Sites outside the site window computed in this window (x, y pairs in window coordinates); their rectangles count when the window closes. */
256
+ let outsideSites = [];
257
+ let marks = new Uint8Array(0), cover = new Uint8Array(0);
258
+ /** A site of a filled window: (i, j) are its coordinates in the site window, inside or outside it. */
259
+ function filledSite(x, y, i, j) {
260
+ const c = channelAt(x, y), inside = i >= 0 && j >= 0 && i < sw && j < sh;
261
+ if (inside) {
262
+ let m = -1;
263
+ for (let g = 0; g < groups.length; g++) {
264
+ const group = groups[g], band = bands[g], S1 = band.capacity + 1, bi = x + group.minIx - R - band.x0, bj = y + group.minIy - R - band.y0;
265
+ if (rectSum(band.changeLeft, S1, bi + 1, bj, bi + group.cw, bj + group.ch) !== 0 || rectSum(band.changeUp, S1, bi, bj + 1, bi + group.cw, bj + group.ch) !== 0) {
266
+ m = -2;
267
+ break;
268
+ }
269
+ const own = band.material[bj * band.capacity + bi];
270
+ if (m === -1)
271
+ m = own;
272
+ else if (own !== m) {
273
+ m = -2;
274
+ break;
275
+ }
276
+ }
277
+ if (m >= 0) {
278
+ lastMaterial = m;
279
+ return uniformSite[c][m];
280
+ }
281
+ }
282
+ lastMaterial = -1;
283
+ let sum = 0;
284
+ for (let g = 0; g < groups.length; g++) {
285
+ const group = groups[g], band = bands[g], cw = group.cw, ch = group.ch, nk = group.neutral[c], sk = group.spectral[c], stride = band.capacity;
286
+ const p0 = x + group.minIx - R, q0 = y + group.minIy - R, bi = p0 - band.x0, bj = q0 - band.y0;
287
+ if (inside && allNeutral) {
288
+ const refl = band.reflectance;
289
+ for (let dy = 0, t = 0; dy < ch; dy++) {
290
+ const row = (bj + dy) * stride + bi;
291
+ for (let dx = 0; dx < cw; dx++, t++)
292
+ sum += refl[row + dx] * nk[t];
293
+ }
294
+ }
295
+ else if (inside) {
296
+ const material = band.material;
297
+ for (let dy = 0, t = 0; dy < ch; dy++) {
298
+ const row = (bj + dy) * stride + bi;
299
+ for (let dx = 0; dx < cw; dx++, t++) {
300
+ const m = material[row + dx], si = spectralIndex[m];
301
+ sum += si < 0 ? reflectance[m] * nk[t] : sk[si][t];
302
+ }
303
+ }
304
+ }
305
+ else {
306
+ for (let dy = 0; dy < ch; dy++)
307
+ for (let dx = 0; dx < cw; dx++) {
308
+ const ci = i + dx, cj = j + dy;
309
+ let m;
310
+ if (ci >= 0 && cj >= 0 && ci < group.cpw && cj < group.cph)
311
+ m = band.material[(bj + dy) * stride + bi + dx];
312
+ else
313
+ m = scene.materialAt(latticeU(group, p0 + dx), latticeV(group, q0 + dy));
314
+ const si = spectralIndex[m];
315
+ if (si === undefined)
316
+ throw new InvalidConfigurationError("Unknown chart material.");
317
+ sum += si < 0 ? reflectance[m] * nk[dy * cw + dx] : sk[si][dy * cw + dx];
318
+ }
319
+ }
320
+ }
321
+ return sum;
322
+ }
323
+ /** Lattice points of an outside site's rectangles that lie outside the window caches (kernelSite looks these up on every read). */
324
+ function outsideLookups(i, j) {
325
+ let count = 0;
326
+ for (const group of groups) {
327
+ const w = Math.max(0, Math.min(i + group.cw, group.cpw) - Math.max(i, 0)), h = Math.max(0, Math.min(j + group.ch, group.cph) - Math.max(j, 0));
328
+ count += group.cw * group.ch - w * h;
329
+ }
330
+ return count;
331
+ }
332
+ // Site values of filled windows persist across windows (a site's value does not depend on the window): rows of native
333
+ // sites in a ring of a power-of-two number of rows, row y in slot y mod ringRows. The window's site cache still holds the
334
+ // sites read in the window, which is also what its lookup count needs.
335
+ let ringRows = 0, ringOn = false, ring = new Float64Array(0), ringMaterial = new Int32Array(0), ringRowId = new Int32Array(0), windowMaterial = new Int32Array(0), lastMaterial = -1;
336
+ /** A filled-window site read outside the site window: counted as kernelSite counts it. */
337
+ function outsideRead(i, j) { lookups += outsideLookups(i, j); outsideSites.push(i, j); }
338
+ /** A site from the ring; on a miss (re)claims the row's slot when another row holds it, computes the site and stores it. */
339
+ function ringRead(x, y, i, j) {
340
+ const slot = y & (ringRows - 1), q = slot * W + x;
341
+ if (ringRowId[slot] === y) {
342
+ const v = ring[q];
343
+ if (v === v) {
344
+ lastMaterial = ringMaterial[q];
345
+ return v;
346
+ }
347
+ }
348
+ else {
349
+ ringRowId[slot] = y;
350
+ ring.fill(NaN, slot * W, (slot + 1) * W);
351
+ }
352
+ const v = filledSite(x, y, i, j);
353
+ ring[q] = v;
354
+ ringMaterial[q] = lastMaterial;
355
+ return v;
356
+ }
357
+ // Without diffraction (#392): site values persist across windows the same way, in their own ring (a site's value does not
358
+ // depend on the window). A ring hit counts the lookups directSite would have made, so `lookups` is unchanged.
359
+ let directRows = 0, directOn = false, directRing = new Float64Array(0), directRowId = new Int32Array(0);
360
+ const directLookups = offsets.length * (scene.kind === "materials" ? 1 : nodes);
361
+ function directRead(x, y) {
362
+ const slot = y & (directRows - 1), q = slot * W + x;
363
+ if (directRowId[slot] === y) {
364
+ const v = directRing[q];
365
+ if (v === v) {
366
+ lookups += directLookups;
367
+ return v;
368
+ }
369
+ }
370
+ else {
371
+ directRowId[slot] = y;
372
+ directRing.fill(NaN, slot * W, (slot + 1) * W);
373
+ }
374
+ const v = directSite(x, y);
375
+ directRing[q] = v;
376
+ return v;
377
+ }
378
+ /** A site not in the window's site cache: from a ring, a filled window without the ring, the lazy window caches, or uncached. */
379
+ function siteMiss(x, y, i, j, cached) {
380
+ if (directOn) {
381
+ const v = directRead(x, y);
382
+ if (cached)
383
+ siteCache[j * sw + i] = v;
384
+ return v;
385
+ }
386
+ if (ringOn) {
387
+ if (!cached) {
388
+ outsideRead(i, j);
389
+ return ringRead(x, y, i, j);
390
+ }
391
+ const v = ringRead(x, y, i, j);
392
+ siteCache[j * sw + i] = v;
393
+ windowMaterial[j * sw + i] = lastMaterial;
394
+ return v;
395
+ }
396
+ if (filled && !cached)
397
+ outsideRead(i, j);
398
+ const v = filled ? filledSite(x, y, i, j) : input.kernels ? kernelSite(x, y) : directSite(x, y);
399
+ if (cached)
400
+ siteCache[j * sw + i] = v;
401
+ return v;
402
+ }
403
+ /**
404
+ * Lattice points of the closing window that kernelSite's lazy cache would have looked up: per group, the window cache points
405
+ * covered by the rectangles of the computed sites (the site cache's filled entries and the outside sites), a box dilation.
406
+ */
407
+ function windowLookups() {
408
+ let total = 0;
409
+ for (const group of groups) {
410
+ const cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, ew = cpw + cw - 1, eh = cph + ch - 1;
411
+ // Top-left marks in extended coordinates (window coordinates + (cw − 1, ch − 1)), and their bounding box.
412
+ if (marks.length < ew * eh) {
413
+ marks = new Uint8Array(ew * eh);
414
+ }
415
+ let x0 = ew, x1 = -1, y0 = eh, y1 = -1;
416
+ const mark = (ei, ej) => {
417
+ if (ei < 0 || ej < 0 || ei >= ew || ej >= eh)
418
+ return;
419
+ marks[ej * ew + ei] = 1;
420
+ if (ei < x0)
421
+ x0 = ei;
422
+ if (ei > x1)
423
+ x1 = ei;
424
+ if (ej < y0)
425
+ y0 = ej;
426
+ if (ej > y1)
427
+ y1 = ej;
428
+ };
429
+ for (let j = 0; j < sh; j++)
430
+ for (let i = 0; i < sw; i++)
431
+ if (!Number.isNaN(siteCache[j * sw + i]))
432
+ mark(i + cw - 1, j + ch - 1);
433
+ for (let k = 0; k < outsideSites.length; k += 2)
434
+ mark(outsideSites[k] + cw - 1, outsideSites[k + 1] + ch - 1);
435
+ if (x1 < 0)
436
+ continue;
437
+ // Cache point (i, j) is covered when a mark lies in extended columns [i, i + cw) and rows [j, j + ch): a horizontal
438
+ // then a vertical running count over the marks' box (outside it nothing is covered).
439
+ const i0 = Math.max(0, x0 - cw + 1), i1 = Math.min(cpw - 1, x1), cwid = i1 - i0 + 1;
440
+ if (cover.length < cwid * (y1 - y0 + 1))
441
+ cover = new Uint8Array(cwid * (y1 - y0 + 1));
442
+ for (let r = y0; r <= y1; r++) {
443
+ let run = 0;
444
+ for (let e = i0; e <= i1 + cw - 1; e++) {
445
+ run += marks[r * ew + e];
446
+ if (e - cw >= i0)
447
+ run -= marks[r * ew + e - cw];
448
+ if (e >= i0 + cw - 1)
449
+ cover[(r - y0) * cwid + e - cw + 1 - i0] = run > 0 ? 1 : 0;
450
+ }
451
+ marks.fill(0, r * ew + x0, r * ew + x1 + 1);
452
+ }
453
+ for (let i = 0; i < cwid; i++) {
454
+ let run = 0;
455
+ for (let r = y0; r <= y1 + ch - 1; r++) {
456
+ if (r <= y1)
457
+ run += cover[(r - y0) * cwid + i];
458
+ if (r - ch >= y0 && r - ch <= y1)
459
+ run -= cover[(r - ch - y0) * cwid + i];
460
+ const j = r - ch + 1;
461
+ if (j >= 0 && j < cph && run > 0)
462
+ total++;
463
+ }
464
+ }
465
+ }
466
+ return total;
467
+ }
468
+ let sx0 = 0, sy0 = 0, sw = 0, sh = 0, siteCache = new Float64Array(0);
469
+ const pilotNode = input.wavelengthsNm[Math.floor(nodes / 2)];
470
+ return {
471
+ get lookups() { return lookups + (filled ? windowLookups() : 0); },
472
+ get uniformMaterial() { return lastMaterial; },
473
+ get tracksMaterial() { return ringOn; },
474
+ get cachesSites() { return sw > 0; },
475
+ window(x0, y0, x1, y1) {
476
+ if (filled)
477
+ lookups += windowLookups();
478
+ filled = false;
479
+ ringOn = false;
480
+ outsideSites = [];
481
+ sx0 = x0;
482
+ sy0 = y0;
483
+ sw = Math.max(0, x1 - x0);
484
+ sh = Math.max(0, y1 - y0);
485
+ if (sw * sh > CACHE_LIMIT) {
486
+ sw = 0;
487
+ sh = 0;
488
+ }
489
+ if (siteCache.length < sw * sh)
490
+ siteCache = new Float64Array(sw * sh);
491
+ siteCache.fill(NaN, 0, sw * sh);
492
+ if (!input.kernels) {
493
+ // A ring of at least twice the window's rows keeps the rows of the previous display row's windows.
494
+ if (sh > 0 && directRows < 2 * sh) {
495
+ let rows = 1;
496
+ while (rows < 2 * sh)
497
+ rows *= 2;
498
+ if (rows * W <= CACHE_LIMIT) {
499
+ directRows = rows;
500
+ directRing = new Float64Array(rows * W).fill(NaN);
501
+ directRowId = new Int32Array(rows).fill(-1);
502
+ }
503
+ }
504
+ directOn = sh > 0 && directRows >= 2 * sh;
505
+ return;
506
+ }
507
+ for (const group of groups) {
508
+ group.cx0 = x0 + group.minIx - R;
509
+ group.cy0 = y0 + group.minIy - R;
510
+ group.cpw = sw === 0 ? 0 : sw + group.cw - 1;
511
+ group.cph = sh === 0 ? 0 : sh + group.ch - 1;
512
+ if (group.cpw * group.cph * (scene.kind === "materials" ? 1 : nodes) > CACHE_LIMIT) {
513
+ group.cpw = 0;
514
+ group.cph = 0;
515
+ }
516
+ }
517
+ // Chart materials whose every group caches the whole window: the filled path (bands). Otherwise the lazy window caches.
518
+ if (scene.kind === "materials" && sw > 0 && groups.every((group) => group.cpw > 0)) {
519
+ groups.forEach((group, g) => coverBand(g, group.cx0, group.cx0 + group.cpw, group.cy0, group.cph));
520
+ filled = true;
521
+ if (windowMaterial.length < sw * sh)
522
+ windowMaterial = new Int32Array(sw * sh);
523
+ // A ring of at least twice the window's rows keeps the rows of the previous display row's windows.
524
+ if (ringRows < 2 * sh) {
525
+ let rows = 1;
526
+ while (rows < 2 * sh)
527
+ rows *= 2;
528
+ if (rows * W <= CACHE_LIMIT) {
529
+ ringRows = rows;
530
+ ring = new Float64Array(rows * W).fill(NaN);
531
+ ringMaterial = new Int32Array(rows * W);
532
+ ringRowId = new Int32Array(rows).fill(-1);
533
+ }
534
+ }
535
+ ringOn = ringRows >= 2 * sh;
536
+ return;
537
+ }
538
+ for (const group of groups) {
539
+ const n = group.cpw * group.cph * (scene.kind === "materials" ? 1 : nodes);
540
+ if (scene.kind === "materials") {
541
+ if (group.cache.length < n || !(group.cache instanceof Int32Array))
542
+ group.cache = new Int32Array(n);
543
+ group.cache.fill(-1, 0, n);
544
+ }
545
+ else {
546
+ if (group.cache.length < n || !(group.cache instanceof Float64Array))
547
+ group.cache = new Float64Array(n);
548
+ group.cache.fill(NaN, 0, n);
549
+ }
550
+ }
551
+ },
552
+ site(x, y) {
553
+ // The window's site cache first (with the ring: the sites read in this window, which its lookup count needs too).
554
+ const i = x - sx0, j = y - sy0, cached = i >= 0 && j >= 0 && i < sw && j < sh;
555
+ if (cached) {
556
+ const w = j * sw + i, v = siteCache[w];
557
+ if (v === v) {
558
+ if (ringOn)
559
+ lastMaterial = windowMaterial[w];
560
+ return v;
561
+ }
562
+ }
563
+ return siteMiss(x, y, i, j, cached);
564
+ },
565
+ pilot(u, v) {
566
+ lookups++;
567
+ return scene.kind === "materials" ? scene.materialAt(u, v) : scene.radianceAt(u, v, pilotNode);
568
+ }
569
+ };
570
+ }
571
+ //# sourceMappingURL=expected-signal.js.map