@photivra/engine 2.8.1 → 2.10.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.
- package/CHANGELOG.md +144 -1
- package/CITATION.cff +1 -1
- package/README.md +8 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +2 -0
- package/dist/capture/export-jpeg.d.ts +19 -0
- package/dist/capture/export-jpeg.js +190 -108
- package/dist/capture/export-jpeg.js.map +1 -1
- package/dist/capture/native-environment-lean.js +106 -12
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-tabulated.d.ts +37 -0
- package/dist/capture/native-environment-tabulated.js +42 -6
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/core/portable-math.d.ts +13 -0
- package/dist/core/portable-math.js +43 -0
- package/dist/core/portable-math.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/channel-response.d.ts +15 -0
- package/dist/facade/channel-response.js +33 -0
- package/dist/facade/channel-response.js.map +1 -0
- package/dist/facade/diffraction-provider.js +50 -15
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +75 -0
- package/dist/facade/display-view.js +471 -109
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.js +2 -1
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +36 -4
- package/dist/facade/expected-signal.js +331 -43
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/scene-radiance.d.ts +4 -1
- package/dist/facade/scene-radiance.js +4 -4
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +77 -5
- package/dist/facade/simulate-capture.js +232 -40
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +25 -2
- package/dist/facade/simulate-display-view.js +55 -21
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +5 -0
- package/dist/gpu/image-kernel-execution.js +2 -2
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.d.ts +19 -17
- package/dist/optics/diffraction-psf-profile.js +49 -18
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/optics/irradiance-convolution.d.ts +11 -0
- package/dist/optics/irradiance-convolution.js +64 -24
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
- package/dist/optics/scene-to-sensor-irradiance.js +58 -24
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/optics/sensor-aperture-rays.js +2 -1
- package/dist/optics/sensor-aperture-rays.js.map +1 -1
- package/dist/optics/sensor-environment-query.js +4 -3
- package/dist/optics/sensor-environment-query.js.map +1 -1
- package/dist/optics/site-diffraction-kernel.d.ts +16 -0
- package/dist/optics/site-diffraction-kernel.js +60 -8
- package/dist/optics/site-diffraction-kernel.js.map +1 -1
- package/dist/output/print-jpeg.d.ts +28 -1
- package/dist/output/print-jpeg.js +79 -7
- package/dist/output/print-jpeg.js.map +1 -1
- package/dist/processing/demosaic.js +114 -37
- package/dist/processing/demosaic.js.map +1 -1
- package/dist/schema/scene-radiance.d.ts +8 -0
- package/dist/schema/scene-radiance.js +28 -17
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/sensor/environment-photo-signal.d.ts +21 -0
- package/dist/sensor/environment-photo-signal.js +68 -2
- package/dist/sensor/environment-photo-signal.js.map +1 -1
- package/dist/sensor/stochastic-seed.d.ts +12 -0
- package/dist/sensor/stochastic-seed.js +25 -0
- package/dist/sensor/stochastic-seed.js.map +1 -1
- package/docs/API_REFERENCE.md +23 -1
- package/docs/QUICK_START.md +77 -8
- package/docs/USAGE.md +30 -0
- package/docs/api/capture-export-jpeg.md +10 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +77 -2
- package/docs/api/facade-display-view.md +50 -0
- package/docs/api/facade-scene-radiance.md +2 -1
- package/docs/api/facade-simulate-capture.md +88 -8
- package/docs/api/facade-simulate-display-view.md +23 -1
- package/docs/api/gpu-image-kernel-execution.md +5 -0
- package/docs/api/optics-diffraction-psf-profile.md +1 -1
- package/docs/api/output-print-jpeg.md +41 -0
- package/package.json +1 -1
|
@@ -11,23 +11,92 @@ const CACHE_LIMIT = 1 << 22;
|
|
|
11
11
|
/** Largest band of looked-up lattice materials per group, in lattice points (a band that would be larger restarts at the window). */
|
|
12
12
|
const BAND_LIMIT = 1 << 22;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
14
|
+
* Lattice-row ring (refinement windows): rows kept per group, the tallest band that uses it, and the columns kept beyond each
|
|
15
|
+
* raster edge (kernel radius, demosaic reach and query offsets; columns beyond it are looked up directly).
|
|
22
16
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
const LATTICE_RING_ROWS = 128, LATTICE_RING_MAXIMUM_BAND_ROWS = 64, LATTICE_RING_MARGIN = 64;
|
|
18
|
+
const PREPARED = new WeakMap();
|
|
19
|
+
/** Chart-material tables kept by content across input objects (#449), least recently used first. */
|
|
20
|
+
export const EXPECTED_SIGNAL_TABLE_CACHE_SIZE = 4;
|
|
21
|
+
const tableCache = [];
|
|
22
|
+
let tableBuilds = 0, tableHits = 0;
|
|
23
|
+
/** Internal test hook: the kept table entries, and the tables built and found by content so far. */
|
|
24
|
+
export function expectedSignalTableCacheState() { return { size: tableCache.length, builds: tableBuilds, hits: tableHits }; }
|
|
25
|
+
/** Internal test hook: forgets the tables kept by content (inputs already prepared keep theirs). */
|
|
26
|
+
export function clearExpectedSignalTableCache() { tableCache.length = 0; }
|
|
27
|
+
/**
|
|
28
|
+
* Every number the tables read, with the lengths that delimit them, as the bits of doubles (so -0, NaN and every digit are kept
|
|
29
|
+
* apart): the spectral nodes, the three channel responses, the offsets, the kernel radius and taps, each material's reflectance or
|
|
30
|
+
* radiance, and the white radiance. Undefined for radiance scenes, whose tables are not kept by content.
|
|
31
|
+
*/
|
|
32
|
+
function tableSignature(input) {
|
|
33
|
+
const scene = input.scene;
|
|
34
|
+
if (scene.kind !== "materials")
|
|
35
|
+
return undefined;
|
|
36
|
+
const values = [];
|
|
37
|
+
const push = (a) => { values.push(a.length); for (let i = 0; i < a.length; i++)
|
|
38
|
+
values.push(a[i]); };
|
|
39
|
+
push(input.wavelengthsNm);
|
|
40
|
+
values.push(input.electronsPerRadiance.length);
|
|
41
|
+
for (const a of input.electronsPerRadiance)
|
|
42
|
+
push(a);
|
|
43
|
+
values.push(input.offsets.length);
|
|
44
|
+
for (const o of input.offsets)
|
|
45
|
+
values.push(o.x, o.y, o.weight);
|
|
46
|
+
if (input.kernels) {
|
|
47
|
+
values.push(1, input.kernels.radius, input.kernels.perNode.length);
|
|
48
|
+
for (const k of input.kernels.perNode)
|
|
49
|
+
push(k);
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
values.push(0);
|
|
53
|
+
values.push(scene.materials.length);
|
|
54
|
+
for (const m of scene.materials)
|
|
55
|
+
if ("neutral" in m)
|
|
56
|
+
values.push(0, m.neutral);
|
|
57
|
+
else {
|
|
58
|
+
values.push(1);
|
|
59
|
+
push(m.radiance);
|
|
60
|
+
}
|
|
61
|
+
push(scene.whiteRadiance);
|
|
62
|
+
return new Uint32Array(Float64Array.from(values).buffer);
|
|
63
|
+
}
|
|
64
|
+
function sameBits(a, b) {
|
|
65
|
+
if (a.length !== b.length)
|
|
66
|
+
return false;
|
|
67
|
+
for (let i = 0; i < a.length; i++)
|
|
68
|
+
if (a[i] !== b[i])
|
|
69
|
+
return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
function prepareSignal(input) {
|
|
73
|
+
const signature = tableSignature(input);
|
|
74
|
+
let tables;
|
|
75
|
+
if (signature) {
|
|
76
|
+
const at = tableCache.findIndex((entry) => sameBits(entry.signature, signature));
|
|
77
|
+
if (at >= 0) {
|
|
78
|
+
const [entry] = tableCache.splice(at, 1);
|
|
79
|
+
tableCache.push(entry);
|
|
80
|
+
tables = entry.tables;
|
|
81
|
+
tableHits++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!tables) {
|
|
85
|
+
tables = buildTables(input);
|
|
86
|
+
tableBuilds++;
|
|
87
|
+
if (signature) {
|
|
88
|
+
tableCache.push({ signature, tables });
|
|
89
|
+
while (tableCache.length > EXPECTED_SIGNAL_TABLE_CACHE_SIZE)
|
|
90
|
+
tableCache.shift();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const prepared = { tables, rings: tables.groups.map(() => undefined) };
|
|
94
|
+
PREPARED.set(input, prepared);
|
|
95
|
+
return prepared;
|
|
96
|
+
}
|
|
97
|
+
function buildTables(input) {
|
|
98
|
+
const nodes = input.wavelengthsNm.length, R = input.kernels ? input.kernels.radius : 0, size = 2 * R + 1;
|
|
99
|
+
const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets;
|
|
31
100
|
const kernelAt = (node, t) => (input.kernels ? input.kernels.perNode[node][t] : 1);
|
|
32
101
|
// Materials: neutral ones by reflectance, spectral ones by index.
|
|
33
102
|
let reflectance = new Float64Array(0), spectralIndex = new Int32Array(0);
|
|
@@ -35,11 +104,12 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
35
104
|
if (scene.kind === "materials") {
|
|
36
105
|
reflectance = new Float64Array(scene.materials.length);
|
|
37
106
|
spectralIndex = new Int32Array(scene.materials.length).fill(-1);
|
|
107
|
+
// A copy of each spectral radiance: the tables may outlive the input and serve other inputs of the same content.
|
|
38
108
|
scene.materials.forEach((m, index) => { if ("neutral" in m)
|
|
39
109
|
reflectance[index] = m.neutral;
|
|
40
110
|
else {
|
|
41
111
|
spectralIndex[index] = spectral.length;
|
|
42
|
-
spectral.push(m.radiance);
|
|
112
|
+
spectral.push(Float64Array.from(m.radiance));
|
|
43
113
|
} });
|
|
44
114
|
}
|
|
45
115
|
// Offsets grouped by fractional shift: offsets that differ by whole sites share one lattice.
|
|
@@ -49,7 +119,7 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
49
119
|
const fx = frac(o.x), fy = frac(o.y), ix = Math.round(o.x - fx), iy = Math.round(o.y - fy);
|
|
50
120
|
let group = groups.find((q) => Math.abs(q.fx - fx) <= OFFSET_TOLERANCE && Math.abs(q.fy - fy) <= OFFSET_TOLERANCE);
|
|
51
121
|
if (!group) {
|
|
52
|
-
group = { fx, fy, members: [], minIx: ix, maxIx: ix, minIy: iy, maxIy: iy, cw: 0, ch: 0, neutral: [], spectral: [], perNode: []
|
|
122
|
+
group = { fx, fy, members: [], minIx: ix, maxIx: ix, minIy: iy, maxIy: iy, cw: 0, ch: 0, neutral: [], spectral: [], perNode: [] };
|
|
53
123
|
groups.push(group);
|
|
54
124
|
}
|
|
55
125
|
group.members.push({ ix, iy, weight: o.weight });
|
|
@@ -92,6 +162,48 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
92
162
|
s += A[c][n] * scene.whiteRadiance[n]; return s; }) : [];
|
|
93
163
|
const spectralDirect = [0, 1, 2].map((c) => spectral.map((radiance) => { let s = 0; for (let n = 0; n < nodes; n++)
|
|
94
164
|
s += A[c][n] * radiance[n]; return s; }));
|
|
165
|
+
const uniformSite = scene.kind === "materials" && input.kernels ? [0, 1, 2].map((c) => Float64Array.from(scene.materials, (_, m) => {
|
|
166
|
+
const si = spectralIndex[m];
|
|
167
|
+
let sum = 0;
|
|
168
|
+
for (const group of groups) {
|
|
169
|
+
const nk = group.neutral[c], sk = si < 0 ? nk : group.spectral[c][si];
|
|
170
|
+
for (let t = 0; t < group.cw * group.ch; t++)
|
|
171
|
+
sum += si < 0 ? reflectance[m] * nk[t] : sk[t];
|
|
172
|
+
}
|
|
173
|
+
return sum;
|
|
174
|
+
})) : [];
|
|
175
|
+
// Frozen records: a render copies each group before it gives it a window (see createDisplayViewExpectedSignal).
|
|
176
|
+
for (const group of groups)
|
|
177
|
+
Object.freeze(group);
|
|
178
|
+
return Object.freeze({ reflectance, spectralIndex, spectral, groups, neutralDirect, spectralDirect, uniformSite });
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 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
|
|
182
|
+
* query point of offset o and K the diffraction kernel (a single unit tap without diffraction). This is the sum the
|
|
183
|
+
* facade's environment chain forms for a still, rectilinear, distortion-free planar scene, where the
|
|
184
|
+
* radiance-to-irradiance factor is the same at every site (docs/DISPLAY_VIEW.md, "Expected signal").
|
|
185
|
+
*
|
|
186
|
+
* With diffraction, offsets that share a fractional shift read one lattice, and their kernels are combined in advance:
|
|
187
|
+
* C_c(d) = Σ_members w·Σ_λ A_c(λ)·K_λ(t)·L(λ) over the taps t that reach lattice offset d. A neutral material's radiance
|
|
188
|
+
* is its reflectance times the white radiance, so all neutral materials share one combined kernel per channel.
|
|
189
|
+
*
|
|
190
|
+
* `spanColumns`: how many native columns the render's windows move across beyond one window's width (a filled band is never
|
|
191
|
+
* built wider). Only a speed hint: a window past it rebuilds its band, with the same values.
|
|
192
|
+
*/
|
|
193
|
+
export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
|
|
194
|
+
const W = input.nativeWidth, H = input.nativeHeight, g = 2 / input.outputWidth, nodes = input.wavelengthsNm.length;
|
|
195
|
+
const R = input.kernels ? input.kernels.radius : 0;
|
|
196
|
+
if (input.kernels && input.kernels.perNode.length !== nodes)
|
|
197
|
+
throw new InvalidConfigurationError("Display view kernels must match the spectral nodes.");
|
|
198
|
+
if (!input.kernels && !input.queryPoint)
|
|
199
|
+
throw new InvalidConfigurationError("Display view needs the environment query points without diffraction.");
|
|
200
|
+
const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets, channelAt = input.channelAt;
|
|
201
|
+
// The request's tables (materials, lattice groups, combined kernels, per-material sums) are the same for every render of a
|
|
202
|
+
// prepared view, which passes the same input object: computed once per input. Each render gets its own groups' window caches.
|
|
203
|
+
// Chart-material tables are also shared by inputs of the same content (#449): each render gets its own groups' window state.
|
|
204
|
+
const prepared = PREPARED.get(input) ?? prepareSignal(input);
|
|
205
|
+
const { reflectance, spectralIndex, spectral, neutralDirect, spectralDirect, uniformSite } = prepared.tables;
|
|
206
|
+
const groups = prepared.tables.groups.map((group) => ({ ...group, cache: new Int32Array(0), cx0: 0, cy0: 0, cpw: 0, cph: 0 }));
|
|
95
207
|
let lookups = 0;
|
|
96
208
|
const point = new Float64Array(2);
|
|
97
209
|
const materialSignalDirect = (m, c) => {
|
|
@@ -100,15 +212,21 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
100
212
|
throw new InvalidConfigurationError("Unknown chart material.");
|
|
101
213
|
return si < 0 ? reflectance[m] * neutralDirect[c] : spectralDirect[c][si];
|
|
102
214
|
};
|
|
103
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* Without diffraction: one site from the environment's own query points. Also sets lastMaterial: the chart material every
|
|
217
|
+
* query point read, or −1 when they read more than one (or the scene is a radiance scene). A site whose points all read
|
|
218
|
+
* material m sums the same operands in the same order as every other such site of its channel, so it holds the same value.
|
|
219
|
+
*/
|
|
104
220
|
function directSite(x, y) {
|
|
105
221
|
const c = channelAt(x, y);
|
|
106
|
-
let sum = 0;
|
|
222
|
+
let sum = 0, uniform = -1;
|
|
107
223
|
for (let o = 0; o < offsets.length; o++) {
|
|
108
224
|
input.queryPoint(x, y, o, point);
|
|
109
225
|
let value;
|
|
110
226
|
if (scene.kind === "materials") {
|
|
111
|
-
|
|
227
|
+
const m = scene.materialAt(point[0], point[1]);
|
|
228
|
+
uniform = o === 0 || uniform === m ? m : -2;
|
|
229
|
+
value = materialSignalDirect(m, c);
|
|
112
230
|
lookups++;
|
|
113
231
|
}
|
|
114
232
|
else {
|
|
@@ -119,6 +237,7 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
119
237
|
}
|
|
120
238
|
sum += offsets[o].weight * value;
|
|
121
239
|
}
|
|
240
|
+
lastMaterial = uniform < 0 ? -1 : uniform;
|
|
122
241
|
return sum;
|
|
123
242
|
}
|
|
124
243
|
// Lattice point (p, q) of a group: the facade's lattice formula (createDiffractedPlanarProvider).
|
|
@@ -190,24 +309,32 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
190
309
|
// kernelSite, so it is bit-identical. The scene lookup count stays the count kernelSite's lazy window cache makes (each
|
|
191
310
|
// cache point a computed site reads counts once per window, points outside the cache once per read): the rectangles of
|
|
192
311
|
// 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
312
|
const allNeutral = spectral.length === 0;
|
|
204
313
|
const bands = groups.map(() => ({ x0: 0, y0: 0, rows: 0, capacity: 0, filled: 0, material: new Int32Array(0), reflectance: new Float64Array(0),
|
|
205
314
|
changeLeft: new Int32Array(0), changeUp: new Int32Array(0), v: new Float64Array(0) }));
|
|
315
|
+
// Lattice rows of material indices per group, kept across windows (filled windows only): rows LATTICE_RING_ROWS apart share a
|
|
316
|
+
// slot; columns from −LATTICE_RING_MARGIN. Int16 when every index fits (about 3 MB for four groups of a 6000-site row). A
|
|
317
|
+
// lattice point's material depends only on the input, so the rings are kept per input object, across its renders (not with
|
|
318
|
+
// the tables kept by content: they depend on `materialAt` and the raster).
|
|
319
|
+
const ringWide = scene.kind === "materials" && scene.materials.length <= 32767;
|
|
320
|
+
const rings = prepared.rings;
|
|
321
|
+
function latticeRing(g) {
|
|
322
|
+
let ring = rings[g];
|
|
323
|
+
if (!ring) {
|
|
324
|
+
const width = W + 2 * LATTICE_RING_MARGIN, size = LATTICE_RING_ROWS * width;
|
|
325
|
+
ring = rings[g] = { width, material: ringWide ? new Int16Array(size) : new Int32Array(size), rowId: new Int32Array(LATTICE_RING_ROWS).fill(-2147483648) };
|
|
326
|
+
}
|
|
327
|
+
return ring;
|
|
328
|
+
}
|
|
206
329
|
/** 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
330
|
function coverBand(g, x0, x1, y0, rows) {
|
|
208
331
|
const group = groups[g], band = bands[g], materials = scene;
|
|
209
|
-
if (!(band.
|
|
210
|
-
|
|
332
|
+
if (!(y0 >= band.y0 && y0 + rows <= band.y0 + band.rows && x0 >= band.x0 && x1 - band.x0 <= band.capacity)) {
|
|
333
|
+
// A band with the ring is built twice as tall, so the next display rows' windows (a few lattice rows lower) reuse its
|
|
334
|
+
// filled columns; a lattice point's material and the change counts inside a rectangle do not depend on the band's origin.
|
|
335
|
+
if (rows <= LATTICE_RING_MAXIMUM_BAND_ROWS)
|
|
336
|
+
rows *= 2;
|
|
337
|
+
const capacity = Math.max(x1 - x0, Math.min(Math.floor(BAND_LIMIT / rows), W + 2 * (x1 - x0), spanColumns + x1 - x0));
|
|
211
338
|
const size = rows * capacity, tables = (rows + 1) * (capacity + 1);
|
|
212
339
|
band.x0 = x0;
|
|
213
340
|
band.y0 = y0;
|
|
@@ -234,10 +361,29 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
234
361
|
band.v[j] = latticeV(group, y0 + j);
|
|
235
362
|
}
|
|
236
363
|
const stride = band.capacity, S1 = stride + 1, material = band.material, left = band.changeLeft, up = band.changeUp;
|
|
364
|
+
rows = band.rows;
|
|
365
|
+
y0 = band.y0;
|
|
366
|
+
const ring = rows <= 2 * LATTICE_RING_MAXIMUM_BAND_ROWS ? latticeRing(g) : undefined;
|
|
237
367
|
for (let i = band.filled; i < x1 - band.x0; i++) {
|
|
238
|
-
const u = latticeU(group,
|
|
368
|
+
const p = band.x0 + i, u = latticeU(group, p), column = p + LATTICE_RING_MARGIN;
|
|
239
369
|
for (let j = 0; j < rows; j++) {
|
|
240
|
-
|
|
370
|
+
let m;
|
|
371
|
+
if (ring && column >= 0 && column < ring.width) {
|
|
372
|
+
// A lattice point's material is a function of the point alone, so one looked up for an earlier window (the rows a
|
|
373
|
+
// display row shares with the next) is the same material; the ring only spares the repeated lookup.
|
|
374
|
+
const q = y0 + j, slot = q & (LATTICE_RING_ROWS - 1), index = slot * ring.width + column;
|
|
375
|
+
if (ring.rowId[slot] !== q) {
|
|
376
|
+
ring.rowId[slot] = q;
|
|
377
|
+
ring.material.fill(-1, slot * ring.width, (slot + 1) * ring.width);
|
|
378
|
+
}
|
|
379
|
+
m = ring.material[index];
|
|
380
|
+
if (m < 0) {
|
|
381
|
+
m = materials.materialAt(u, band.v[j]);
|
|
382
|
+
ring.material[index] = m;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else
|
|
386
|
+
m = materials.materialAt(u, band.v[j]);
|
|
241
387
|
if (spectralIndex[m] === undefined)
|
|
242
388
|
throw new InvalidConfigurationError("Unknown chart material.");
|
|
243
389
|
material[j * stride + i] = m;
|
|
@@ -355,8 +501,9 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
355
501
|
return v;
|
|
356
502
|
}
|
|
357
503
|
// 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
|
-
|
|
504
|
+
// depend on the window). A ring hit counts the lookups directSite would have made, so `lookups` is unchanged. The site's
|
|
505
|
+
// material (directSite's lastMaterial) is kept beside its value, so the display view's one-material memo applies (#444).
|
|
506
|
+
let directRows = 0, directOn = false, directRing = new Float64Array(0), directRowId = new Int32Array(0), directRingMaterial = new Int32Array(0);
|
|
360
507
|
const directLookups = offsets.length * (scene.kind === "materials" ? 1 : nodes);
|
|
361
508
|
function directRead(x, y) {
|
|
362
509
|
const slot = y & (directRows - 1), q = slot * W + x;
|
|
@@ -364,6 +511,7 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
364
511
|
const v = directRing[q];
|
|
365
512
|
if (v === v) {
|
|
366
513
|
lookups += directLookups;
|
|
514
|
+
lastMaterial = directRingMaterial[q];
|
|
367
515
|
return v;
|
|
368
516
|
}
|
|
369
517
|
}
|
|
@@ -373,14 +521,17 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
373
521
|
}
|
|
374
522
|
const v = directSite(x, y);
|
|
375
523
|
directRing[q] = v;
|
|
524
|
+
directRingMaterial[q] = lastMaterial;
|
|
376
525
|
return v;
|
|
377
526
|
}
|
|
378
527
|
/** 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
528
|
function siteMiss(x, y, i, j, cached) {
|
|
380
529
|
if (directOn) {
|
|
381
530
|
const v = directRead(x, y);
|
|
382
|
-
if (cached)
|
|
531
|
+
if (cached) {
|
|
383
532
|
siteCache[j * sw + i] = v;
|
|
533
|
+
windowMaterial[j * sw + i] = lastMaterial;
|
|
534
|
+
}
|
|
384
535
|
return v;
|
|
385
536
|
}
|
|
386
537
|
if (ringOn) {
|
|
@@ -391,23 +542,114 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
391
542
|
const v = ringRead(x, y, i, j);
|
|
392
543
|
siteCache[j * sw + i] = v;
|
|
393
544
|
windowMaterial[j * sw + i] = lastMaterial;
|
|
545
|
+
markFilled(i, j);
|
|
394
546
|
return v;
|
|
395
547
|
}
|
|
396
548
|
if (filled && !cached)
|
|
397
549
|
outsideRead(i, j);
|
|
398
550
|
const v = filled ? filledSite(x, y, i, j) : input.kernels ? kernelSite(x, y) : directSite(x, y);
|
|
399
|
-
if (cached)
|
|
551
|
+
if (cached) {
|
|
400
552
|
siteCache[j * sw + i] = v;
|
|
553
|
+
if (filled)
|
|
554
|
+
markFilled(i, j);
|
|
555
|
+
}
|
|
401
556
|
return v;
|
|
402
557
|
}
|
|
403
558
|
/**
|
|
404
559
|
* Lattice points of the closing window that kernelSite's lazy cache would have looked up: per group, the window cache points
|
|
405
560
|
* covered by the rectangles of the computed sites (the site cache's filled entries and the outside sites), a box dilation.
|
|
561
|
+
* A group's count depends only on the computed sites and its geometry (cw, ch, cpw, cph), so a group shaped like an
|
|
562
|
+
* earlier one takes that group's count (the app's Quality groups all have one shape).
|
|
406
563
|
*/
|
|
564
|
+
const groupTotals = new Float64Array(groups.length);
|
|
565
|
+
function sameGeometry(g) {
|
|
566
|
+
const group = groups[g];
|
|
567
|
+
for (let h = 0; h < g; h++) {
|
|
568
|
+
const o = groups[h];
|
|
569
|
+
if (o.cw === group.cw && o.ch === group.ch && o.cpw === group.cpw && o.cph === group.cph)
|
|
570
|
+
return h;
|
|
571
|
+
}
|
|
572
|
+
return -1;
|
|
573
|
+
}
|
|
407
574
|
function windowLookups() {
|
|
408
575
|
let total = 0;
|
|
409
|
-
|
|
410
|
-
|
|
576
|
+
if (outsideSites.length === 0 && filledCount === 0)
|
|
577
|
+
return 0;
|
|
578
|
+
if (outsideSites.length === 0 && filledCount === sw * sh) {
|
|
579
|
+
// Every site computed (each once) and none outside: the marks at (i + cw − 1, j + ch − 1) cover cache columns
|
|
580
|
+
// 0 … sw + cw − 2 and rows 0 … sh + ch − 2, the whole cpw × cph cache of every group.
|
|
581
|
+
for (const group of groups)
|
|
582
|
+
total += group.cpw * group.cph;
|
|
583
|
+
return total;
|
|
584
|
+
}
|
|
585
|
+
if (outsideSites.length === 0 && sw <= 31) {
|
|
586
|
+
// Computed sites as a bit mask per window row. Cache point (ci, cj) is covered when a computed site (i, j) has
|
|
587
|
+
// i ≤ ci < i + cw and j ≤ cj < j + ch; a union of dilations is the dilation of the union, so per cache row the
|
|
588
|
+
// covered columns are the dilation of the OR of the masks of window rows cj − ch + 1 … cj. That OR is a prefix OR
|
|
589
|
+
// when the rows start at fy0 and a suffix OR when they end at fy1 (OR is associative and commutative).
|
|
590
|
+
if (prefixOr.length < sh) {
|
|
591
|
+
prefixOr = new Int32Array(sh);
|
|
592
|
+
suffixOr = new Int32Array(sh);
|
|
593
|
+
}
|
|
594
|
+
for (let j = fy0, m = 0; j <= fy1; j++) {
|
|
595
|
+
m |= rowMask[j];
|
|
596
|
+
prefixOr[j] = m;
|
|
597
|
+
}
|
|
598
|
+
for (let j = fy1, m = 0; j >= fy0; j--) {
|
|
599
|
+
m |= rowMask[j];
|
|
600
|
+
suffixOr[j] = m;
|
|
601
|
+
}
|
|
602
|
+
for (let g = 0; g < groups.length; g++) {
|
|
603
|
+
const same = sameGeometry(g);
|
|
604
|
+
if (same >= 0) {
|
|
605
|
+
total += groupTotals[same];
|
|
606
|
+
groupTotals[g] = groupTotals[same];
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const group = groups[g], cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, last = Math.min(cph - 1, fy1 + ch - 1), before = total;
|
|
610
|
+
let previous = -1, width = 0;
|
|
611
|
+
for (let cj = fy0; cj <= last; cj++) {
|
|
612
|
+
let m = 0;
|
|
613
|
+
const a = Math.max(fy0, cj - ch + 1), b = Math.min(fy1, cj);
|
|
614
|
+
if (a === fy0)
|
|
615
|
+
m = prefixOr[b];
|
|
616
|
+
else if (b === fy1)
|
|
617
|
+
m = suffixOr[a];
|
|
618
|
+
else
|
|
619
|
+
for (let j = a; j <= b; j++)
|
|
620
|
+
m |= rowMask[j];
|
|
621
|
+
if (m !== previous) {
|
|
622
|
+
previous = m;
|
|
623
|
+
width = 0;
|
|
624
|
+
let end = -1;
|
|
625
|
+
for (let i = 0; m >>> i !== 0; i++) {
|
|
626
|
+
if (((m >>> i) & 1) === 0)
|
|
627
|
+
continue;
|
|
628
|
+
const to = Math.min(i + cw - 1, cpw - 1);
|
|
629
|
+
if (i > end)
|
|
630
|
+
width += to - i + 1;
|
|
631
|
+
else if (to > end)
|
|
632
|
+
width += to - end;
|
|
633
|
+
if (to > end)
|
|
634
|
+
end = to;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
total += width;
|
|
638
|
+
}
|
|
639
|
+
groupTotals[g] = total - before;
|
|
640
|
+
}
|
|
641
|
+
return total;
|
|
642
|
+
}
|
|
643
|
+
for (let g = 0; g < groups.length; g++) {
|
|
644
|
+
// The marks are the computed sites shifted by (cw − 1, ch − 1), so the count depends on the group's geometry only.
|
|
645
|
+
const same = sameGeometry(g);
|
|
646
|
+
if (same >= 0) {
|
|
647
|
+
total += groupTotals[same];
|
|
648
|
+
groupTotals[g] = groupTotals[same];
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const group = groups[g], cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, ew = cpw + cw - 1, eh = cph + ch - 1, before = total;
|
|
652
|
+
groupTotals[g] = 0;
|
|
411
653
|
// Top-left marks in extended coordinates (window coordinates + (cw − 1, ch − 1)), and their bounding box.
|
|
412
654
|
if (marks.length < ew * eh) {
|
|
413
655
|
marks = new Uint8Array(ew * eh);
|
|
@@ -462,22 +704,63 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
462
704
|
total++;
|
|
463
705
|
}
|
|
464
706
|
}
|
|
707
|
+
groupTotals[g] = total - before;
|
|
465
708
|
}
|
|
466
709
|
return total;
|
|
467
710
|
}
|
|
468
711
|
let sx0 = 0, sy0 = 0, sw = 0, sh = 0, siteCache = new Float64Array(0);
|
|
712
|
+
// The computed entries of a filled window's site cache (windowLookups): their count, bounding box and a bit mask per row
|
|
713
|
+
// (columns below 31).
|
|
714
|
+
let filledCount = 0, fx0 = 0, fx1 = -1, fy0 = 0, fy1 = -1;
|
|
715
|
+
let rowMask = new Int32Array(0), prefixOr = new Int32Array(0), suffixOr = new Int32Array(0);
|
|
716
|
+
function markFilled(i, j) {
|
|
717
|
+
if (i < 31)
|
|
718
|
+
rowMask[j] = rowMask[j] | (1 << i);
|
|
719
|
+
if (filledCount++ === 0) {
|
|
720
|
+
fx0 = fx1 = i;
|
|
721
|
+
fy0 = fy1 = j;
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (i < fx0)
|
|
725
|
+
fx0 = i;
|
|
726
|
+
else if (i > fx1)
|
|
727
|
+
fx1 = i;
|
|
728
|
+
if (j < fy0)
|
|
729
|
+
fy0 = j;
|
|
730
|
+
else if (j > fy1)
|
|
731
|
+
fy1 = j;
|
|
732
|
+
}
|
|
469
733
|
const pilotNode = input.wavelengthsNm[Math.floor(nodes / 2)];
|
|
470
734
|
return {
|
|
471
735
|
get lookups() { return lookups + (filled ? windowLookups() : 0); },
|
|
472
736
|
get uniformMaterial() { return lastMaterial; },
|
|
473
|
-
|
|
737
|
+
// Chart materials are tracked on the filled path and, without diffraction, on the direct ring (radiance scenes never).
|
|
738
|
+
get tracksMaterial() { return ringOn || (directOn && scene.kind === "materials"); },
|
|
474
739
|
get cachesSites() { return sw > 0; },
|
|
740
|
+
get marksReads() { return filled; },
|
|
741
|
+
markReads(xs, ys, values, materials, count) {
|
|
742
|
+
for (let n = 0; n < count; n++) {
|
|
743
|
+
const i = xs[n] - sx0, j = ys[n] - sy0;
|
|
744
|
+
if (!(i >= 0 && j >= 0 && i < sw && j < sh))
|
|
745
|
+
continue;
|
|
746
|
+
const w = j * sw + i;
|
|
747
|
+
if (siteCache[w] === siteCache[w])
|
|
748
|
+
continue;
|
|
749
|
+
// As siteMiss for a site inside a filled window: the computed site is marked (windowLookups counts from the marks)
|
|
750
|
+
// and its material kept.
|
|
751
|
+
siteCache[w] = values[n];
|
|
752
|
+
windowMaterial[w] = materials[n];
|
|
753
|
+
markFilled(i, j);
|
|
754
|
+
}
|
|
755
|
+
},
|
|
475
756
|
window(x0, y0, x1, y1) {
|
|
476
757
|
if (filled)
|
|
477
758
|
lookups += windowLookups();
|
|
478
759
|
filled = false;
|
|
479
760
|
ringOn = false;
|
|
480
761
|
outsideSites = [];
|
|
762
|
+
filledCount = 0;
|
|
763
|
+
rowMask.fill(0, 0, sh);
|
|
481
764
|
sx0 = x0;
|
|
482
765
|
sy0 = y0;
|
|
483
766
|
sw = Math.max(0, x1 - x0);
|
|
@@ -488,6 +771,8 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
488
771
|
}
|
|
489
772
|
if (siteCache.length < sw * sh)
|
|
490
773
|
siteCache = new Float64Array(sw * sh);
|
|
774
|
+
if (rowMask.length < sh)
|
|
775
|
+
rowMask = new Int32Array(sh);
|
|
491
776
|
siteCache.fill(NaN, 0, sw * sh);
|
|
492
777
|
if (!input.kernels) {
|
|
493
778
|
// A ring of at least twice the window's rows keeps the rows of the previous display row's windows.
|
|
@@ -499,9 +784,12 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
499
784
|
directRows = rows;
|
|
500
785
|
directRing = new Float64Array(rows * W).fill(NaN);
|
|
501
786
|
directRowId = new Int32Array(rows).fill(-1);
|
|
787
|
+
directRingMaterial = new Int32Array(rows * W);
|
|
502
788
|
}
|
|
503
789
|
}
|
|
504
790
|
directOn = sh > 0 && directRows >= 2 * sh;
|
|
791
|
+
if (directOn && windowMaterial.length < sw * sh)
|
|
792
|
+
windowMaterial = new Int32Array(sw * sh);
|
|
505
793
|
return;
|
|
506
794
|
}
|
|
507
795
|
for (const group of groups) {
|
|
@@ -555,7 +843,7 @@ export function createDisplayViewExpectedSignal(input) {
|
|
|
555
843
|
if (cached) {
|
|
556
844
|
const w = j * sw + i, v = siteCache[w];
|
|
557
845
|
if (v === v) {
|
|
558
|
-
if (ringOn)
|
|
846
|
+
if (ringOn || directOn)
|
|
559
847
|
lastMaterial = windowMaterial[w];
|
|
560
848
|
return v;
|
|
561
849
|
}
|