@photivra/engine 2.9.0 → 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 +85 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +2 -0
- package/dist/capture/native-environment-lean.js +89 -5
- 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 +32 -3
- package/dist/facade/expected-signal.js +192 -24
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +51 -4
- package/dist/facade/simulate-capture.js +130 -31
- 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 -22
- 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 +2 -2
- package/dist/index.js +2 -2
- package/dist/index.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/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/docs/API_REFERENCE.md +16 -1
- package/docs/QUICK_START.md +18 -8
- package/docs/USAGE.md +30 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +42 -2
- package/docs/api/facade-display-view.md +50 -0
- package/docs/api/facade-simulate-capture.md +60 -8
- package/docs/api/facade-simulate-display-view.md +23 -1
- package/docs/api/gpu-image-kernel-execution.md +5 -0
- package/package.json +1 -1
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
import { createDeterministicUniformStream, createPortableStandardNormalPairs, deriveNativeSiteStreamSeedUint32, fmix32, samplePortableStandardNormal } from "../sensor/stochastic-seed.js";
|
|
36
36
|
import { encodeSrgbComponentPortable } from "../color/srgb-transfer.js";
|
|
37
37
|
import { createPoissonSampler } from "../sensor/raw-readout.js";
|
|
38
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
38
39
|
import { portableCbrt, portableCosTwoPi, portableExp, portableExp2, portableLog2, portablePow } from "../core/portable-math.js";
|
|
39
40
|
/** Stream IDs of the display view's two seeded streams, distinct from the native charge (0) and read-noise (1) streams. */
|
|
40
41
|
export const DISPLAY_VIEW_JITTER_STREAM_ID = 32;
|
|
@@ -296,10 +297,175 @@ const ahdStateCache = new Map();
|
|
|
296
297
|
function correctionKey(seed, correction, perElectron, constantVariance, gains, clipLevel, matrix, toneCurve) {
|
|
297
298
|
return [seed, correction.drawsPerState, ...perElectron, ...constantVariance, ...correction.labMatrix.flat(), ...gains, clipLevel, ...matrix.flat(), toneCurve].join(",");
|
|
298
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Internal (exported for tests, #442): the window offset of each slot, slot (dy + reach)·(2·reach + 1) + dx + reach, as
|
|
302
|
+
* two integer tables: dx[slot] = (slot mod side) − reach and dy[slot] = ⌊slot / side⌋ − reach. The render loop reads them
|
|
303
|
+
* instead of taking a modulo and a division per site read; they are the same integers.
|
|
304
|
+
*/
|
|
305
|
+
export function displayViewSlotOffsets(reach) {
|
|
306
|
+
const side = 2 * reach + 1, slotsCount = side * side;
|
|
307
|
+
return { dx: Int32Array.from({ length: slotsCount }, (_, slot) => (slot % side) - reach), dy: Int32Array.from({ length: slotsCount }, (_, slot) => ((slot / side) | 0) - reach) };
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Internal (exported for tests, #442): the nested per-phase tables [phase][candidate] (and [channel]) as flat arrays holding
|
|
311
|
+
* the same typed arrays. Plain arrays of typed arrays with one element kind keep the render loop's keyed loads on one hidden
|
|
312
|
+
* class each, where the nested arrays (arrays of arrays of typed arrays) made V8 deoptimize the render loop. Every phase must
|
|
313
|
+
* have the same number of candidates, 1 or 2 (the record layout and the choice variance assume it), each with 3 channels.
|
|
314
|
+
*/
|
|
315
|
+
export function flatDisplayViewCandidateTables(slots, weights, dense, list) {
|
|
316
|
+
const count = slots[0]?.length ?? 0;
|
|
317
|
+
if (slots.length !== 4 || (count !== 1 && count !== 2))
|
|
318
|
+
throw new InvalidConfigurationError("display view: each of the 4 CFA phases needs 1 or 2 demosaic candidates.");
|
|
319
|
+
const denseAt = [], listAt = [], slotsAt = [], weightsAt = [];
|
|
320
|
+
for (let p = 0; p < 4; p++) {
|
|
321
|
+
if (slots[p].length !== count || weights[p]?.length !== count || dense[p]?.length !== count || list[p]?.length !== count)
|
|
322
|
+
throw new InvalidConfigurationError("display view: every CFA phase needs the same number of demosaic candidates.");
|
|
323
|
+
for (let d = 0; d < count; d++) {
|
|
324
|
+
if (slots[p][d].length !== 3 || weights[p][d].length !== 3)
|
|
325
|
+
throw new InvalidConfigurationError("display view: each demosaic candidate needs a kernel per camera channel.");
|
|
326
|
+
denseAt[p * 2 + d] = dense[p][d];
|
|
327
|
+
listAt[p * 2 + d] = list[p][d];
|
|
328
|
+
for (let c = 0; c < 3; c++) {
|
|
329
|
+
slotsAt[(p * 2 + d) * 3 + c] = slots[p][d][c];
|
|
330
|
+
weightsAt[(p * 2 + d) * 3 + c] = weights[p][d][c];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return { count, denseAt, listAt, slotsAt, weightsAt };
|
|
335
|
+
}
|
|
299
336
|
/** Internal (exported for tests): the correction states kept across calls, and clearing them. */
|
|
300
337
|
export function displayViewCorrectionStateCount() { let n = 0; for (const states of ahdStateCache.values())
|
|
301
338
|
n += states.index.size; return n; }
|
|
302
339
|
export function clearDisplayViewCorrectionStates() { ahdStateCache.clear(); }
|
|
340
|
+
/** Number of grid keys: AHD_KEY_LEVELS³ (a grid key is (i0·AHD_KEY_LEVELS + i1)·AHD_KEY_LEVELS + i2). */
|
|
341
|
+
const AHD_GRID_KEY_COUNT = AHD_KEY_LEVELS * AHD_KEY_LEVELS * AHD_KEY_LEVELS;
|
|
342
|
+
/** Internal (#447): the correction key of a setup, or null without the AHD correction (the key `renderDisplayView` looks its states up with). */
|
|
343
|
+
export function displayViewCorrectionKey(setup) {
|
|
344
|
+
if (!setup.ahdCorrection)
|
|
345
|
+
return null;
|
|
346
|
+
return correctionKey(setup.seed, setup.ahdCorrection, Float64Array.from(setup.readout, (r) => r.perElectron), Float64Array.from(setup.readout, (r) => r.constantVariance), setup.gains, setup.clipLevel, setup.matrix, setup.toneCurve);
|
|
347
|
+
}
|
|
348
|
+
/** True for a typed array of the named kind from any realm (a structured clone arrives as one of the receiving realm). */
|
|
349
|
+
function isTypedArray(value, tag) {
|
|
350
|
+
return ArrayBuffer.isView(value) && Object.prototype.toString.call(value) === "[object " + tag + "]";
|
|
351
|
+
}
|
|
352
|
+
/** A grid-key list, checked: an Int32Array of keys in [0, AHD_KEY_LEVELS³). */
|
|
353
|
+
function checkGridKeys(gridKeys, caller) {
|
|
354
|
+
if (!isTypedArray(gridKeys, "Int32Array"))
|
|
355
|
+
throw new InvalidConfigurationError(caller + ": gridKeys must be an Int32Array.");
|
|
356
|
+
const keys = gridKeys;
|
|
357
|
+
for (let i = 0; i < keys.length; i++)
|
|
358
|
+
if (!(keys[i] >= 0 && keys[i] < AHD_GRID_KEY_COUNT))
|
|
359
|
+
throw new InvalidConfigurationError(caller + ": gridKeys[" + i + "] (" + keys[i] + ") is not a grid key, an integer in [0, " + AHD_GRID_KEY_COUNT + ").");
|
|
360
|
+
return keys;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* 2.10 (#447): the AHD low-signal correction states this JavaScript realm holds (a Web Worker, a page), one entry per cached
|
|
364
|
+
* setup, least recently used first, each state's grid key and three values copied out. Another realm adds them with
|
|
365
|
+
* {@link bindDisplayViewCorrectionStates}; a render there then reads them instead of computing them, with the same bytes.
|
|
366
|
+
*/
|
|
367
|
+
export function serializeDisplayViewCorrectionStates() {
|
|
368
|
+
const out = [];
|
|
369
|
+
for (const [key, states] of ahdStateCache) {
|
|
370
|
+
const gridKeys = new Int32Array(states.index.size), values = new Float64Array(3 * states.index.size);
|
|
371
|
+
let i = 0;
|
|
372
|
+
for (const [gridKey, n] of states.index) {
|
|
373
|
+
gridKeys[i] = gridKey;
|
|
374
|
+
values[3 * i] = states.values[3 * n];
|
|
375
|
+
values[3 * i + 1] = states.values[3 * n + 1];
|
|
376
|
+
values[3 * i + 2] = states.values[3 * n + 2];
|
|
377
|
+
i++;
|
|
378
|
+
}
|
|
379
|
+
out.push({ key, gridKeys, values });
|
|
380
|
+
}
|
|
381
|
+
return out;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* 2.10 (#447): adds states another realm computed (an export, or `computeCorrectionStates` results) to this realm's cache and
|
|
385
|
+
* returns the number of states added. For each entry, the grid keys its setup does not hold yet are appended; known ones are
|
|
386
|
+
* left as they are (a state's values follow from its key, so they are equal). Every entry is checked before the cache is
|
|
387
|
+
* touched: `key` a non-empty string, `gridKeys` an Int32Array of grid keys, `values` a Float64Array of 3 × `gridKeys.length`
|
|
388
|
+
* finite numbers; otherwise it fails with an InvalidConfigurationError and the cache is unchanged. The cache's bounds apply as
|
|
389
|
+
* in a render: an unknown key creates a setup and can evict the least recently used of the four kept, and a setup that
|
|
390
|
+
* reaches 65 536 states is cleared before the next one is added. Import only keys a prepared view reported
|
|
391
|
+
* (`correctionKey()`), so states a render here will read.
|
|
392
|
+
*/
|
|
393
|
+
export function bindDisplayViewCorrectionStates(entries) {
|
|
394
|
+
const caller = "bindDisplayViewCorrectionStates";
|
|
395
|
+
if (!Array.isArray(entries))
|
|
396
|
+
throw new InvalidConfigurationError(caller + ": entries must be an array of { key, gridKeys, values }.");
|
|
397
|
+
const checked = [];
|
|
398
|
+
for (let e = 0; e < entries.length; e++) {
|
|
399
|
+
const entry = entries[e], at = caller + ": entries[" + e + "]";
|
|
400
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry))
|
|
401
|
+
throw new InvalidConfigurationError(at + " must be an object { key, gridKeys, values }.");
|
|
402
|
+
for (const field of Object.keys(entry))
|
|
403
|
+
if (field !== "key" && field !== "gridKeys" && field !== "values")
|
|
404
|
+
throw new InvalidConfigurationError(at + " has unknown field " + JSON.stringify(field) + ".");
|
|
405
|
+
const { key, gridKeys, values } = entry;
|
|
406
|
+
if (typeof key !== "string" || key.length === 0)
|
|
407
|
+
throw new InvalidConfigurationError(at + ".key must be a non-empty string (a prepared view's correctionKey()).");
|
|
408
|
+
const keys = checkGridKeys(gridKeys, at);
|
|
409
|
+
if (!isTypedArray(values, "Float64Array"))
|
|
410
|
+
throw new InvalidConfigurationError(at + ".values must be a Float64Array.");
|
|
411
|
+
const v = values;
|
|
412
|
+
if (v.length !== 3 * keys.length)
|
|
413
|
+
throw new InvalidConfigurationError(at + ".values must hold 3 values per grid key (" + 3 * keys.length + "), not " + v.length + ".");
|
|
414
|
+
for (let i = 0; i < v.length; i++)
|
|
415
|
+
if (!Number.isFinite(v[i]))
|
|
416
|
+
throw new InvalidConfigurationError(at + ".values[" + i + "] (" + v[i] + ") is not finite.");
|
|
417
|
+
checked.push({ key, gridKeys: keys, values: v });
|
|
418
|
+
}
|
|
419
|
+
let appended = 0;
|
|
420
|
+
for (const entry of checked) {
|
|
421
|
+
const states = correctionStates(entry.key);
|
|
422
|
+
for (let i = 0; i < entry.gridKeys.length; i++) {
|
|
423
|
+
const gridKey = entry.gridKeys[i];
|
|
424
|
+
if (states.index.has(gridKey))
|
|
425
|
+
continue;
|
|
426
|
+
// As gridCorrection stores a state.
|
|
427
|
+
if (states.index.size >= AHD_STATE_LIMIT)
|
|
428
|
+
states.index.clear();
|
|
429
|
+
const n = states.index.size;
|
|
430
|
+
if (states.values.length < 3 * (n + 1)) {
|
|
431
|
+
const grown = new Float64Array(2 * states.values.length);
|
|
432
|
+
grown.set(states.values);
|
|
433
|
+
states.values = grown;
|
|
434
|
+
}
|
|
435
|
+
states.values[3 * n] = entry.values[3 * i];
|
|
436
|
+
states.values[3 * n + 1] = entry.values[3 * i + 1];
|
|
437
|
+
states.values[3 * n + 2] = entry.values[3 * i + 2];
|
|
438
|
+
states.index.set(gridKey, n);
|
|
439
|
+
appended++;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return appended;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Internal (#447, `PreparedDisplayView.computeCorrectionStates`): computes the states of the given grid keys for a setup (each
|
|
446
|
+
* as a render computes it, read from the cache where it is held) and stores them in this realm's cache; returns each key's
|
|
447
|
+
* three values in the order given. Yields to the host and checks for abort between states, as a render does.
|
|
448
|
+
*/
|
|
449
|
+
export async function computeDisplayViewCorrectionStates(setup, gridKeys) {
|
|
450
|
+
const keys = checkGridKeys(gridKeys, "computeCorrectionStates");
|
|
451
|
+
setup.checkAbort();
|
|
452
|
+
const key = displayViewCorrectionKey(setup), states = correctionStates(key), gridCorrection = createGridCorrection(setup, states);
|
|
453
|
+
const outKeys = Int32Array.from(keys), values = new Float64Array(3 * keys.length);
|
|
454
|
+
let lastYield = Date.now();
|
|
455
|
+
for (let i = 0; i < keys.length; i++) {
|
|
456
|
+
const gridKey = keys[i], i2 = gridKey % AHD_KEY_LEVELS, i1 = ((gridKey - i2) / AHD_KEY_LEVELS) % AHD_KEY_LEVELS, i0 = (gridKey - i1 * AHD_KEY_LEVELS - i2) / (AHD_KEY_LEVELS * AHD_KEY_LEVELS);
|
|
457
|
+
const at = gridCorrection(i0, i1, i2);
|
|
458
|
+
values[3 * i] = states.values[at];
|
|
459
|
+
values[3 * i + 1] = states.values[at + 1];
|
|
460
|
+
values[3 * i + 2] = states.values[at + 2];
|
|
461
|
+
if (Date.now() - lastYield >= 8) {
|
|
462
|
+
await setup.yieldControl();
|
|
463
|
+
setup.checkAbort();
|
|
464
|
+
lastYield = Date.now();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return { key, gridKeys: outKeys, values };
|
|
468
|
+
}
|
|
303
469
|
/** The state map of one setup (moved to the most recently used end). */
|
|
304
470
|
function correctionStates(key) {
|
|
305
471
|
let states = ahdStateCache.get(key);
|
|
@@ -328,77 +494,22 @@ const PIXEL_MEMO_LIMIT = 1 << 16;
|
|
|
328
494
|
* evaluated while the stratified estimator takes a quarter of its samples per axis where the pilot finds no detail.
|
|
329
495
|
*/
|
|
330
496
|
export const EXACT_AREA_MEAN_MAX_PIXELS = 25;
|
|
497
|
+
/** Most entries of the site ring (a native site's value, material and readout moments, kept across the display pixels of a render). */
|
|
498
|
+
const SITE_RING_LIMIT = 1 << 18;
|
|
331
499
|
/** Pilot points per axis: about one per two sites of the footprint plus its reach, 4 to 16. */
|
|
332
500
|
function pilotCount(span) { return Math.max(4, Math.min(16, Math.ceil(span / 2))); }
|
|
333
|
-
/**
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// grow with the footprint.
|
|
342
|
-
const exactView = area <= Math.min(EXACT_AREA_MEAN_MAX_PIXELS, fullK * fullK);
|
|
343
|
-
const columnOverlaps = exactView ? Array.from({ length: region.width }, (_, i) => overlaps((region.x + i) * sx, (region.x + i + 1) * sx, frameWidth)) : [];
|
|
344
|
-
// On the exact path a real pixel's recorded terms are shared by the 2–4 display pixels whose footprints touch it: they are
|
|
345
|
-
// kept for the region's native columns in a ring of native rows (a power of two, at least twice a footprint's height plus
|
|
346
|
-
// one), valid while a slot's stamp is its native row + 1.
|
|
347
|
-
const exactX0 = Math.floor(region.x * sx) + margin, exactWidth = Math.min(frameWidth - 1, Math.ceil((region.x + region.width) * sx) - 1) + margin - exactX0 + 1;
|
|
348
|
-
let exactRows = 1;
|
|
349
|
-
while (exactRows < 2 * (Math.ceil(sy) + 1))
|
|
350
|
-
exactRows *= 2;
|
|
351
|
-
const exactMemo = exactView && exactRows * exactWidth <= PIXEL_MEMO_LIMIT * 4;
|
|
352
|
-
const exactStamp = new Int32Array(exactMemo ? exactRows * exactWidth : 0), exactRecords = new Float64Array(exactMemo ? exactRows * exactWidth * MEMO_SIZE : 0);
|
|
353
|
-
const maximum = setup.bitDepth === 8 ? 255 : 65535, step = 1 / maximum;
|
|
354
|
-
const pixelCount = region.width * region.height;
|
|
355
|
-
const samples = setup.bitDepth === 8 ? new Uint8Array(pixelCount * 3) : new Uint16Array(pixelCount * 3);
|
|
356
|
-
const expected = new Float64Array(pixelCount * 3), noiseStd = new Float64Array(pixelCount * 3);
|
|
357
|
-
const columns = Array.from({ length: region.width }, (_, i) => axisSums((region.x + i) * sx, (region.x + i + 1) * sx, frameWidth, margin));
|
|
501
|
+
/**
|
|
502
|
+
* The state computer of one setup's AHD correction (#447: shared by the render and `computeDisplayViewCorrectionStates`):
|
|
503
|
+
* gridCorrection(i0, i1, i2) returns the offset of the grid state's three values in `states.values`, computing and storing
|
|
504
|
+
* the state on first use. The code and its operations are those the render ran in place before.
|
|
505
|
+
*/
|
|
506
|
+
function createGridCorrection(setup, states) {
|
|
507
|
+
const { seed, ahdCorrection } = setup;
|
|
508
|
+
const k = Float64Array.from(setup.readout, (r) => r.perElectron), c0 = Float64Array.from(setup.readout, (r) => r.constantVariance);
|
|
358
509
|
const M = setup.matrix, m00 = M[0][0], m01 = M[0][1], m02 = M[0][2], m10 = M[1][0], m11 = M[1][1], m12 = M[1][2], m20 = M[2][0], m21 = M[2][1], m22 = M[2][2];
|
|
359
510
|
const g0 = setup.gains[0], g1 = setup.gains[1], g2 = setup.gains[2], L = setup.clipLevel, reinhard = setup.toneCurve === "positive-reinhard-per-channel";
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
const candidates = setup.candidates ?? setup.demosaic.map((phase) => [phase]);
|
|
363
|
-
const reach = Math.max(1, ...candidates.flat(3).map((t) => Math.max(Math.abs(t.dx), Math.abs(t.dy)))), side = 2 * reach + 1, slotsCount = side * side;
|
|
364
|
-
const slotOf = (t) => (t.dy + reach) * side + t.dx + reach;
|
|
365
|
-
// Per phase: the bilinear kernels (for the site variance of each channel), the candidates, and the slots they read.
|
|
366
|
-
const phaseSlots = [], phaseWeights = [], phaseW2 = [], phaseList = [];
|
|
367
|
-
const candidateSlots = [], candidateWeights = [], candidateDense = [], candidateList = [];
|
|
368
|
-
for (let p = 0; p < 4; p++) {
|
|
369
|
-
const kernels = setup.demosaic[p];
|
|
370
|
-
phaseSlots[p] = kernels.map((list) => Int32Array.from(list, slotOf));
|
|
371
|
-
phaseWeights[p] = kernels.map((list) => Float64Array.from(list, (t) => t.weight));
|
|
372
|
-
phaseW2[p] = Float64Array.from(kernels, (list) => list.reduce((sum, t) => sum + t.weight * t.weight, 0));
|
|
373
|
-
candidateSlots[p] = candidates[p].map((candidate) => candidate.map((list) => Int32Array.from(list, slotOf)));
|
|
374
|
-
candidateDense[p] = candidates[p].map((candidate) => { const dense = new Float64Array(3 * slotsCount); candidate.forEach((list, c) => { for (const t of list) {
|
|
375
|
-
const i = c * slotsCount + slotOf(t);
|
|
376
|
-
dense[i] = dense[i] + t.weight;
|
|
377
|
-
} }); return dense; });
|
|
378
|
-
candidateWeights[p] = candidates[p].map((candidate) => candidate.map((list) => Float64Array.from(list, (t) => t.weight)));
|
|
379
|
-
const used = new Set([...phaseSlots[p].flatMap((a) => Array.from(a)), ...candidateSlots[p].flat().flatMap((a) => Array.from(a))]);
|
|
380
|
-
phaseList[p] = Int32Array.from([...used].sort((a, b) => a - b));
|
|
381
|
-
// The slots of phaseList (same order) where a candidate has a non-zero weight in some channel. A sum over phaseList adds
|
|
382
|
-
// ±0 at the other slots, which leaves a sum that starts at +0 unchanged, so the sums below skip them.
|
|
383
|
-
candidateList[p] = candidateDense[p].map((dense) => phaseList[p].filter((slot) => dense[slot] !== 0 || dense[slotsCount + slot] !== 0 || dense[2 * slotsCount + slot] !== 0));
|
|
384
|
-
}
|
|
385
|
-
const siteElectrons = new Float64Array(slotsCount), siteMean = new Float64Array(slotsCount), siteVar = new Float64Array(slotsCount), siteThird = new Float64Array(slotsCount), siteChannel = new Int32Array(slotsCount), clip = new Float64Array(3), B = new Float64Array(4), C = new Float64Array(9);
|
|
386
|
-
const cm = new Float64Array(3), cv = new Float64Array(3), csv = new Float64Array(3), bm = new Float64Array(3), bv = new Float64Array(3), bs = new Float64Array(3);
|
|
387
|
-
const hm = new Float64Array(3), hv = new Float64Array(3), hs = new Float64Array(3), an = new Float64Array(9);
|
|
388
|
-
const reach2 = CLIP_REACH_SIGMA * CLIP_REACH_SIGMA;
|
|
389
|
-
// AHD low-signal correction: an 11 × 11 noisy neighbourhood (AHD reads up to 5 sites away) and the local AHD steps.
|
|
390
|
-
const ahdCorrection = setup.ahdCorrection, windowReach = ahdCorrection ? Math.max(reach, AHD_REACH) : reach;
|
|
391
|
-
const correction = new Float64Array(3), patch = new Float64Array(AHD_SIDE * AHD_SIDE), cmAverage = new Float64Array(3);
|
|
392
|
-
let correctedPixels = 0;
|
|
393
|
-
const readStd = c0.map((v) => Math.sqrt(v));
|
|
394
|
-
// The correction of a flat neighbourhood whose sites of channel c have normalized mean μ_c, on a logarithmic grid of states
|
|
395
|
-
// (AHD_KEY_STEPS per octave from 2^-AHD_KEY_OCTAVES of white); each grid state is computed once with draws seeded by the state
|
|
396
|
-
// alone, so it is the same in every display pixel and region, and a pixel interpolates trilinearly between the 8 states
|
|
397
|
-
// around its own channel means.
|
|
398
|
-
// Grid states are shared across display pixels and across calls: a state depends only on its indices and on the inputs in
|
|
399
|
-
// `correctionKey` (the seed, the draws, the readout scale and noise, the homogeneity matrix and the development), so a cached
|
|
400
|
-
// state is the same value the draws would give again.
|
|
401
|
-
const states = ahdCorrection ? correctionStates(correctionKey(seed, ahdCorrection, k, c0, setup.gains, L, M, setup.toneCurve)) : { index: new Map(), values: new Float64Array(0) };
|
|
511
|
+
const correction = new Float64Array(3), patch = new Float64Array(AHD_SIDE * AHD_SIDE);
|
|
512
|
+
const readStd = Float64Array.from(c0, (v) => Math.sqrt(v));
|
|
402
513
|
/** The offset of a grid state's three values in states.values (computed and stored on first use). */
|
|
403
514
|
function gridCorrection(i0, i1, i2) {
|
|
404
515
|
const key = (i0 * AHD_KEY_LEVELS + i1) * AHD_KEY_LEVELS + i2, cached = states.index.get(key);
|
|
@@ -433,29 +544,7 @@ export async function renderDisplayView(setup) {
|
|
|
433
544
|
states.index.set(key, n);
|
|
434
545
|
return 3 * n;
|
|
435
546
|
}
|
|
436
|
-
const
|
|
437
|
-
const gridBase = new Float64Array(3), gridFrac = new Float64Array(3);
|
|
438
|
-
function flatCorrection(m0, m1, m2) {
|
|
439
|
-
for (let i = 0; i < 3; i++) {
|
|
440
|
-
const m = i === 0 ? m0 : i === 1 ? m1 : m2, g = (portableLog2(Math.min(1, Math.max(AHD_KEY_FLOOR, m))) + AHD_KEY_OCTAVES) * AHD_KEY_STEPS;
|
|
441
|
-
const base = Math.min(Math.floor(g), AHD_KEY_OCTAVES * AHD_KEY_STEPS - 1);
|
|
442
|
-
gridBase[i] = base;
|
|
443
|
-
gridFrac[i] = g - base;
|
|
444
|
-
}
|
|
445
|
-
interpolated.fill(0);
|
|
446
|
-
for (let corner = 0; corner < 8; corner++) {
|
|
447
|
-
let weight = 1;
|
|
448
|
-
for (let i = 0; i < 3; i++)
|
|
449
|
-
weight *= (corner >> i) & 1 ? gridFrac[i] : 1 - gridFrac[i];
|
|
450
|
-
if (weight === 0)
|
|
451
|
-
continue;
|
|
452
|
-
const at = gridCorrection(gridBase[0] + (corner & 1), gridBase[1] + ((corner >> 1) & 1), gridBase[2] + ((corner >> 2) & 1)), value = states.values;
|
|
453
|
-
for (let comp = 0; comp < 3; comp++)
|
|
454
|
-
interpolated[comp] = interpolated[comp] + weight * value[at + comp];
|
|
455
|
-
}
|
|
456
|
-
return interpolated;
|
|
457
|
-
}
|
|
458
|
-
const lab = ahdCorrection?.labMatrix ?? [[1, 0, 0], [0, 1, 0], [0, 0, 1]], labWhite = lab.map((row) => row[0] + row[1] + row[2]);
|
|
547
|
+
const lab = ahdCorrection.labMatrix, labWhite = Float64Array.from(lab, (row) => row[0] + row[1] + row[2]);
|
|
459
548
|
const l00 = lab[0][0], l01 = lab[0][1], l02 = lab[0][2], l10 = lab[1][0], l11 = lab[1][1], l12 = lab[1][2], l20 = lab[2][0], l21 = lab[2][1], l22 = lab[2][2];
|
|
460
549
|
const lw0 = labWhite[0], lw1 = labWhite[1], lw2 = labWhite[2];
|
|
461
550
|
const gh = new Float64Array(49), gv = new Float64Array(49), cand = [new Float64Array(75), new Float64Array(75)], labs = [new Float64Array(75), new Float64Array(75)];
|
|
@@ -547,9 +636,134 @@ export async function renderDisplayView(setup) {
|
|
|
547
636
|
for (let c = 0; c < 3; c++)
|
|
548
637
|
out[c] = out[c] - linear[c] / 2;
|
|
549
638
|
}
|
|
639
|
+
return gridCorrection;
|
|
640
|
+
}
|
|
641
|
+
/** Renders the display view (docs/DISPLAY_VIEW.md). */
|
|
642
|
+
export async function renderDisplayView(setup) {
|
|
643
|
+
const { frameWidth, frameHeight, margin, nativeWidth, nativeHeight, viewWidth, viewHeight, region, seed, reachSites } = setup;
|
|
644
|
+
const sx = frameWidth / viewWidth, sy = frameHeight / viewHeight, area = sx * sy, g = 2 / frameWidth;
|
|
645
|
+
const fullK = setup.samplesPerAxis, uniformK = Math.max(1, Math.round(fullK / 4));
|
|
646
|
+
// Exact area mean (#393): where a footprint holds at most EXACT_AREA_MEAN_MAX_PIXELS real pixels and at most K², every real
|
|
647
|
+
// pixel it touches is evaluated once with its exact overlap weight a_j = w_j / A, so the expected value is D_P = Σ_j a_j·E[L_j]
|
|
648
|
+
// with no sampling error at no more cost than sampling; coarser views keep the stratified estimator, whose cost does not
|
|
649
|
+
// grow with the footprint.
|
|
650
|
+
const exactView = area <= Math.min(EXACT_AREA_MEAN_MAX_PIXELS, fullK * fullK);
|
|
651
|
+
const columnOverlaps = exactView ? Array.from({ length: region.width }, (_, i) => overlaps((region.x + i) * sx, (region.x + i + 1) * sx, frameWidth)) : [];
|
|
652
|
+
// On the exact path a real pixel's recorded terms are shared by the 2–4 display pixels whose footprints touch it: they are
|
|
653
|
+
// kept for the region's native columns in a ring of native rows (a power of two, at least twice a footprint's height plus
|
|
654
|
+
// one), valid while a slot's stamp is its native row + 1.
|
|
655
|
+
const exactX0 = Math.floor(region.x * sx) + margin, exactWidth = Math.min(frameWidth - 1, Math.ceil((region.x + region.width) * sx) - 1) + margin - exactX0 + 1;
|
|
656
|
+
let exactRows = 1;
|
|
657
|
+
while (exactRows < 2 * (Math.ceil(sy) + 1))
|
|
658
|
+
exactRows *= 2;
|
|
659
|
+
const exactMemo = exactView && exactRows * exactWidth <= PIXEL_MEMO_LIMIT * 4;
|
|
660
|
+
const exactStamp = new Int32Array(exactMemo ? exactRows * exactWidth : 0), exactRecords = new Float64Array(exactMemo ? exactRows * exactWidth * MEMO_SIZE : 0);
|
|
661
|
+
const maximum = setup.bitDepth === 8 ? 255 : 65535, step = 1 / maximum;
|
|
662
|
+
const pixelCount = region.width * region.height;
|
|
663
|
+
const samples = setup.bitDepth === 8 ? new Uint8Array(pixelCount * 3) : new Uint16Array(pixelCount * 3);
|
|
664
|
+
const expected = new Float64Array(pixelCount * 3), noiseStd = new Float64Array(pixelCount * 3);
|
|
665
|
+
const columns = Array.from({ length: region.width }, (_, i) => axisSums((region.x + i) * sx, (region.x + i + 1) * sx, frameWidth, margin));
|
|
666
|
+
const M = setup.matrix, m00 = M[0][0], m01 = M[0][1], m02 = M[0][2], m10 = M[1][0], m11 = M[1][1], m12 = M[1][2], m20 = M[2][0], m21 = M[2][1], m22 = M[2][2];
|
|
667
|
+
const g0 = setup.gains[0], g1 = setup.gains[1], g2 = setup.gains[2], L = setup.clipLevel, reinhard = setup.toneCurve === "positive-reinhard-per-channel";
|
|
668
|
+
// Small per-render tables as Float64Arrays (#442): the same doubles as plain arrays, with one element kind for the keyed loads.
|
|
669
|
+
const k = Float64Array.from(setup.readout, (r) => r.perElectron), dark = Float64Array.from(setup.readout, (r) => r.darkElectrons), c0 = Float64Array.from(setup.readout, (r) => r.constantVariance);
|
|
670
|
+
// Site window: 3 × 3 for the bilinear kernels, 7 × 7 when AHD's candidates read up to 3 sites away. Slot (dy + r)·(2r + 1) + dx + r.
|
|
671
|
+
const candidates = setup.candidates ?? setup.demosaic.map((phase) => [phase]);
|
|
672
|
+
const reach = Math.max(1, ...candidates.flat(3).map((t) => Math.max(Math.abs(t.dx), Math.abs(t.dy)))), side = 2 * reach + 1, slotsCount = side * side;
|
|
673
|
+
const slotOf = (t) => (t.dy + reach) * side + t.dx + reach;
|
|
674
|
+
const { dx: slotDx, dy: slotDy } = displayViewSlotOffsets(reach);
|
|
675
|
+
// Per phase: the bilinear kernels (for the site variance of each channel), the candidates, and the slots they read.
|
|
676
|
+
const phaseSlots = [], phaseWeights = [], phaseW2 = [], phaseList = [];
|
|
677
|
+
const candidateSlots = [], candidateWeights = [], candidateDense = [], candidateList = [];
|
|
678
|
+
for (let p = 0; p < 4; p++) {
|
|
679
|
+
const kernels = setup.demosaic[p];
|
|
680
|
+
phaseSlots[p] = kernels.map((list) => Int32Array.from(list, slotOf));
|
|
681
|
+
phaseWeights[p] = kernels.map((list) => Float64Array.from(list, (t) => t.weight));
|
|
682
|
+
phaseW2[p] = Float64Array.from(kernels, (list) => list.reduce((sum, t) => sum + t.weight * t.weight, 0));
|
|
683
|
+
candidateSlots[p] = candidates[p].map((candidate) => candidate.map((list) => Int32Array.from(list, slotOf)));
|
|
684
|
+
candidateDense[p] = candidates[p].map((candidate) => { const dense = new Float64Array(3 * slotsCount); candidate.forEach((list, c) => { for (const t of list) {
|
|
685
|
+
const i = c * slotsCount + slotOf(t);
|
|
686
|
+
dense[i] = dense[i] + t.weight;
|
|
687
|
+
} }); return dense; });
|
|
688
|
+
candidateWeights[p] = candidates[p].map((candidate) => candidate.map((list) => Float64Array.from(list, (t) => t.weight)));
|
|
689
|
+
const used = new Set([...phaseSlots[p].flatMap((a) => Array.from(a)), ...candidateSlots[p].flat().flatMap((a) => Array.from(a))]);
|
|
690
|
+
phaseList[p] = Int32Array.from([...used].sort((a, b) => a - b));
|
|
691
|
+
// The slots of phaseList (same order) where a candidate has a non-zero weight in some channel. A sum over phaseList adds
|
|
692
|
+
// ±0 at the other slots, which leaves a sum that starts at +0 unchanged, so the sums below skip them.
|
|
693
|
+
candidateList[p] = candidateDense[p].map((dense) => phaseList[p].filter((slot) => dense[slot] !== 0 || dense[slotsCount + slot] !== 0 || dense[2 * slotsCount + slot] !== 0));
|
|
694
|
+
}
|
|
695
|
+
// The same tables flat (phase·2 + candidate, ·3 + channel), read by the sample loop below.
|
|
696
|
+
const { count: candidateCount, denseAt, listAt, slotsAt, weightsAt } = flatDisplayViewCandidateTables(candidateSlots, candidateWeights, candidateDense, candidateList);
|
|
697
|
+
const siteElectrons = new Float64Array(slotsCount), siteMean = new Float64Array(slotsCount), siteVar = new Float64Array(slotsCount), siteThird = new Float64Array(slotsCount), siteChannel = new Int32Array(slotsCount), clip = new Float64Array(3), B = new Float64Array(4), C = new Float64Array(9);
|
|
698
|
+
const cm = new Float64Array(3), cv = new Float64Array(3), csv = new Float64Array(3), bm = new Float64Array(3), bv = new Float64Array(3), bs = new Float64Array(3);
|
|
699
|
+
const hm = new Float64Array(3), hv = new Float64Array(3), hs = new Float64Array(3), an = new Float64Array(9);
|
|
700
|
+
const reach2 = CLIP_REACH_SIGMA * CLIP_REACH_SIGMA;
|
|
701
|
+
// Site ring (#441, exact path with filled windows only): a native site's expected electrons, chart material and readout moments depend on the
|
|
702
|
+
// site alone, so they are computed once per render and kept for the region's native columns (plus the reach) in a ring of
|
|
703
|
+
// native rows, a power of two holding two display rows' footprints and their reach. An entry is valid while its stamp is
|
|
704
|
+
// its row + 1 (negated when the window did not track materials, so a material read in one mode is never used in the other).
|
|
705
|
+
// The ~27 (AHD) or ~7 (bilinear) sites a real pixel reads are copied from the ring instead of being read through the
|
|
706
|
+
// signal and clipped again for each real pixel that shares them; the signal's window is still told which of its sites were
|
|
707
|
+
// read (markReads), so its lookup count is the count reading them would give.
|
|
708
|
+
const siteX0 = Math.max(0, Math.floor(region.x * sx) + margin - reach), siteX1 = Math.min(nativeWidth - 1, Math.ceil((region.x + region.width) * sx) - 1 + margin + reach), siteW = siteX1 - siteX0 + 1;
|
|
709
|
+
let siteRows = 1;
|
|
710
|
+
while (siteRows < 2 * Math.ceil(sy) + 2 + 2 * reach)
|
|
711
|
+
siteRows *= 2;
|
|
712
|
+
// Coarse (stratified) views read a few scattered real pixels per display pixel, where the ring measured slower; windows
|
|
713
|
+
// without diffraction kernels read their sites from the signal's own direct ring, cheaply enough that the ring measured
|
|
714
|
+
// 2–4 % slower there (Speed, #441). Both keep the per-read path below.
|
|
715
|
+
const siteMask = siteRows - 1, ringCapacity = siteRows * siteW, ringUsable = exactView && ringCapacity <= SITE_RING_LIMIT;
|
|
716
|
+
// Allocated at the first display pixel that uses the ring (a render whose windows never fill allocates nothing).
|
|
717
|
+
let ringStamp = new Int32Array(0), ringStatStamp = ringStamp, ringMat = ringStamp, siteSlotQ = ringStamp, siteSlotStamp = ringStamp, markX = ringStamp, markY = ringStamp, markM = ringStamp;
|
|
718
|
+
let ringV = new Float64Array(0), ringMean = ringV, ringVar = ringV, ringThird = ringV, markV = ringV;
|
|
719
|
+
// (No closure assigns them, so they stay locals of the render loop.)
|
|
720
|
+
// AHD low-signal correction: an 11 × 11 noisy neighbourhood (AHD reads up to 5 sites away) and the local AHD steps.
|
|
721
|
+
const ahdCorrection = setup.ahdCorrection, windowReach = ahdCorrection ? Math.max(reach, AHD_REACH) : reach;
|
|
722
|
+
const cmAverage = new Float64Array(3);
|
|
723
|
+
let correctedPixels = 0;
|
|
724
|
+
// The correction of a flat neighbourhood whose sites of channel c have normalized mean μ_c, on a logarithmic grid of states
|
|
725
|
+
// (AHD_KEY_STEPS per octave from 2^-AHD_KEY_OCTAVES of white); each grid state is computed once with draws seeded by the state
|
|
726
|
+
// alone, so it is the same in every display pixel and region, and a pixel interpolates trilinearly between the 8 states
|
|
727
|
+
// around its own channel means.
|
|
728
|
+
// Grid states are shared across display pixels and across calls: a state depends only on its indices and on the inputs in
|
|
729
|
+
// `correctionKey` (the seed, the draws, the readout scale and noise, the homogeneity matrix and the development), so a cached
|
|
730
|
+
// state is the same value the draws would give again.
|
|
731
|
+
// A collect pass (#447) records the grid keys its corrected samples read instead of computing or reading states, and
|
|
732
|
+
// leaves the cache untouched; its image is discarded.
|
|
733
|
+
const collect = setup.ahdCollect;
|
|
734
|
+
const states = ahdCorrection && !collect ? correctionStates(displayViewCorrectionKey(setup)) : { index: new Map(), values: new Float64Array(0) };
|
|
735
|
+
const gridCorrection = ahdCorrection && !collect ? createGridCorrection(setup, states) : () => 0;
|
|
736
|
+
const interpolated = new Float64Array(3);
|
|
737
|
+
const gridBase = new Float64Array(3), gridFrac = new Float64Array(3);
|
|
738
|
+
function flatCorrection(m0, m1, m2) {
|
|
739
|
+
for (let i = 0; i < 3; i++) {
|
|
740
|
+
const m = i === 0 ? m0 : i === 1 ? m1 : m2, g = (portableLog2(Math.min(1, Math.max(AHD_KEY_FLOOR, m))) + AHD_KEY_OCTAVES) * AHD_KEY_STEPS;
|
|
741
|
+
const base = Math.min(Math.floor(g), AHD_KEY_OCTAVES * AHD_KEY_STEPS - 1);
|
|
742
|
+
gridBase[i] = base;
|
|
743
|
+
gridFrac[i] = g - base;
|
|
744
|
+
}
|
|
745
|
+
interpolated.fill(0);
|
|
746
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
747
|
+
let weight = 1;
|
|
748
|
+
for (let i = 0; i < 3; i++)
|
|
749
|
+
weight *= (corner >> i) & 1 ? gridFrac[i] : 1 - gridFrac[i];
|
|
750
|
+
if (weight === 0)
|
|
751
|
+
continue;
|
|
752
|
+
const i0 = gridBase[0] + (corner & 1), i1 = gridBase[1] + ((corner >> 1) & 1), i2 = gridBase[2] + ((corner >> 2) & 1);
|
|
753
|
+
if (collect) {
|
|
754
|
+
collect.add((i0 * AHD_KEY_LEVELS + i1) * AHD_KEY_LEVELS + i2);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const at = gridCorrection(i0, i1, i2), value = states.values;
|
|
758
|
+
for (let comp = 0; comp < 3; comp++)
|
|
759
|
+
interpolated[comp] = interpolated[comp] + weight * value[at + comp];
|
|
760
|
+
}
|
|
761
|
+
return interpolated;
|
|
762
|
+
}
|
|
550
763
|
/** Normalized site value of a slot: normal with mean k·e and variance k²·e + constant, clipped at the white level (1). */
|
|
551
|
-
function siteStatistics(slot
|
|
552
|
-
|
|
764
|
+
function siteStatistics(slot) {
|
|
765
|
+
// The channel and electrons are read from the slot arrays: a double passed to a call that is not inlined is boxed.
|
|
766
|
+
const c = siteChannel[slot], e = siteElectrons[slot], kc = k[c], mean = kc * e, variance = kc * kc * e + c0[c], gap = 1 - mean;
|
|
553
767
|
// Third cumulant of the normalized value: Poisson electrons have κ₃ = e (read and rounding noise add none).
|
|
554
768
|
siteThird[slot] = kc * kc * kc * e;
|
|
555
769
|
if (gap > 0 && gap * gap > reach2 * variance) {
|
|
@@ -630,8 +844,27 @@ export async function renderDisplayView(setup) {
|
|
|
630
844
|
else
|
|
631
845
|
detail++;
|
|
632
846
|
const cols = exactView ? columnOverlaps[rx] : undefined, nx = cols ? cols.w.length : 0, sampleTotal = exactView ? nx * rowOverlaps.w.length : Q;
|
|
633
|
-
|
|
634
|
-
const
|
|
847
|
+
const wx0 = Math.floor(vx * sx) + margin - windowReach, wy0 = Math.floor(vy * sy) + margin - windowReach;
|
|
848
|
+
const wx1 = Math.ceil((vx + 1) * sx) + margin + windowReach, wy1 = Math.ceil((vy + 1) * sy) + margin + windowReach;
|
|
849
|
+
setup.signal.window(wx0, wy0, wx1, wy1);
|
|
850
|
+
// The jitter stream is only drawn from by the stratified estimator; the exact path reads no jitter.
|
|
851
|
+
const jitter = exactView ? undefined : createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, index, DISPLAY_VIEW_JITTER_STREAM_ID)), tracking = setup.signal.tracksMaterial;
|
|
852
|
+
const ringOn = ringUsable && setup.signal.marksReads, ringSign = tracking ? 1 : -1;
|
|
853
|
+
if (ringOn && ringStamp.length === 0) {
|
|
854
|
+
ringStamp = new Int32Array(ringCapacity);
|
|
855
|
+
ringStatStamp = new Int32Array(ringCapacity);
|
|
856
|
+
ringMat = new Int32Array(ringCapacity);
|
|
857
|
+
ringV = new Float64Array(ringCapacity);
|
|
858
|
+
ringMean = new Float64Array(ringCapacity);
|
|
859
|
+
ringVar = new Float64Array(ringCapacity);
|
|
860
|
+
ringThird = new Float64Array(ringCapacity);
|
|
861
|
+
siteSlotQ = new Int32Array(slotsCount);
|
|
862
|
+
siteSlotStamp = new Int32Array(slotsCount);
|
|
863
|
+
markX = new Int32Array(slotsCount);
|
|
864
|
+
markY = new Int32Array(slotsCount);
|
|
865
|
+
markV = new Float64Array(slotsCount);
|
|
866
|
+
markM = new Int32Array(slotsCount);
|
|
867
|
+
}
|
|
635
868
|
const mx0 = Math.floor(vx * sx) + margin, my0 = Math.floor(vy * sy) + margin;
|
|
636
869
|
const mw = Math.min(frameWidth - 1, Math.ceil((vx + 1) * sx)) + margin - mx0 + 1, mh = Math.min(frameHeight - 1, Math.ceil((vy + 1) * sy)) + margin - my0 + 1;
|
|
637
870
|
const pixelMemo = !exactView && setup.signal.cachesSites && mw * mh <= PIXEL_MEMO_LIMIT;
|
|
@@ -662,7 +895,7 @@ export async function renderDisplayView(setup) {
|
|
|
662
895
|
y = Math.min(frameHeight - 1, Math.floor(fy)) + margin;
|
|
663
896
|
}
|
|
664
897
|
const phase = (y & 1) * 2 + (x & 1), list = phaseList[phase];
|
|
665
|
-
let material = -2;
|
|
898
|
+
let material = -2, statistics = false;
|
|
666
899
|
const px = x - mx0, py = y - my0;
|
|
667
900
|
const inside = x >= reach && y >= reach && x + reach < nativeWidth && y + reach < nativeHeight;
|
|
668
901
|
const stamps = exactMemo ? exactStamp : pixelStamp, records = exactMemo ? exactRecords : pixelRecords, stamp = exactMemo ? y + 1 : pixelGeneration;
|
|
@@ -677,11 +910,95 @@ export async function renderDisplayView(setup) {
|
|
|
677
910
|
// The expected electrons of the sites the demosaic reads (sites outside the raster, at mirror borders, are read
|
|
678
911
|
// reflect-101); where the signal tracks it, whether they all read one chart material, with the statistics below only
|
|
679
912
|
// when the sample is not replayed; otherwise the statistics at once.
|
|
680
|
-
if (!memo)
|
|
913
|
+
if (!memo && ringOn) {
|
|
914
|
+
let marks = 0;
|
|
681
915
|
for (let li = 0; li < list.length; li++) {
|
|
682
916
|
const slot = list[li];
|
|
683
917
|
siteCount++;
|
|
684
|
-
let sxs = x +
|
|
918
|
+
let sxs = x + slotDx[slot], sys = y + slotDy[slot];
|
|
919
|
+
if (sxs < 0)
|
|
920
|
+
sxs = -sxs;
|
|
921
|
+
else if (sxs >= nativeWidth)
|
|
922
|
+
sxs = 2 * nativeWidth - 2 - sxs;
|
|
923
|
+
if (sys < 0)
|
|
924
|
+
sys = -sys;
|
|
925
|
+
else if (sys >= nativeHeight)
|
|
926
|
+
sys = 2 * nativeHeight - 2 - sys;
|
|
927
|
+
const c = (sys & 1) === 0 ? ((sxs & 1) === 0 ? 0 : 1) : ((sxs & 1) === 0 ? 1 : 2);
|
|
928
|
+
siteChannel[slot] = c;
|
|
929
|
+
// The site's ring entry, for a site inside the ring's columns and the signal's window (a site outside the window
|
|
930
|
+
// is read through the signal, which counts it as it counts any read outside its window).
|
|
931
|
+
const rowStamp = ringSign * (sys + 1), q = ringOn && sxs >= siteX0 && sxs <= siteX1 && sxs >= wx0 && sxs < wx1 && sys >= wy0 && sys < wy1 ? (sys & siteMask) * siteW + sxs - siteX0 : -1;
|
|
932
|
+
siteSlotQ[slot] = q;
|
|
933
|
+
siteSlotStamp[slot] = rowStamp;
|
|
934
|
+
let e;
|
|
935
|
+
if (q >= 0 && ringStamp[q] === rowStamp) {
|
|
936
|
+
// The value and material the signal returned for this site; the window is told it was read (markReads below).
|
|
937
|
+
const v = ringV[q], own = ringMat[q];
|
|
938
|
+
e = v + dark[c];
|
|
939
|
+
markX[marks] = sxs;
|
|
940
|
+
markY[marks] = sys;
|
|
941
|
+
markV[marks] = v;
|
|
942
|
+
markM[marks] = own;
|
|
943
|
+
marks++;
|
|
944
|
+
if (tracking) {
|
|
945
|
+
siteElectrons[slot] = e;
|
|
946
|
+
material = material === -2 || material === own ? own : -1;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
const v = setup.signal.site(sxs, sys), own = tracking ? setup.signal.uniformMaterial : -1;
|
|
952
|
+
e = v + dark[c];
|
|
953
|
+
if (q >= 0) {
|
|
954
|
+
ringStamp[q] = rowStamp;
|
|
955
|
+
ringV[q] = v;
|
|
956
|
+
ringMat[q] = own;
|
|
957
|
+
ringStatStamp[q] = 0;
|
|
958
|
+
}
|
|
959
|
+
if (tracking) {
|
|
960
|
+
siteElectrons[slot] = e;
|
|
961
|
+
material = material === -2 || material === own ? own : -1;
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (q >= 0 && ringStatStamp[q] === rowStamp) {
|
|
966
|
+
siteMean[slot] = ringMean[q];
|
|
967
|
+
siteVar[slot] = ringVar[q];
|
|
968
|
+
siteThird[slot] = ringThird[q];
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
// As siteStatistics, written out here for the untracked (Speed) path.
|
|
972
|
+
const kc = k[c], mean = kc * e, variance = kc * kc * e + c0[c], gap = 1 - mean, third = kc * kc * kc * e;
|
|
973
|
+
let sm, sv;
|
|
974
|
+
if (gap > 0 && gap * gap > reach2 * variance) {
|
|
975
|
+
sm = mean;
|
|
976
|
+
sv = variance;
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
upperClip(mean, variance, 1, clip);
|
|
980
|
+
sm = clip[0];
|
|
981
|
+
sv = clip[1];
|
|
982
|
+
}
|
|
983
|
+
siteMean[slot] = sm;
|
|
984
|
+
siteVar[slot] = sv;
|
|
985
|
+
siteThird[slot] = third;
|
|
986
|
+
if (q >= 0) {
|
|
987
|
+
ringStatStamp[q] = rowStamp;
|
|
988
|
+
ringMean[q] = sm;
|
|
989
|
+
ringVar[q] = sv;
|
|
990
|
+
ringThird[q] = third;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
if (marks > 0)
|
|
994
|
+
setup.signal.markReads(markX, markY, markV, markM, marks);
|
|
995
|
+
}
|
|
996
|
+
else if (!memo)
|
|
997
|
+
for (let li = 0; li < list.length; li++) {
|
|
998
|
+
// Without the ring (coarse views, windows without diffraction kernels or with lazy caches): each site through the signal.
|
|
999
|
+
const slot = list[li];
|
|
1000
|
+
siteCount++;
|
|
1001
|
+
let sxs = x + slotDx[slot], sys = y + slotDy[slot];
|
|
685
1002
|
if (sxs < 0)
|
|
686
1003
|
sxs = -sxs;
|
|
687
1004
|
else if (sxs >= nativeWidth)
|
|
@@ -693,11 +1010,18 @@ export async function renderDisplayView(setup) {
|
|
|
693
1010
|
const c = (sys & 1) === 0 ? ((sxs & 1) === 0 ? 0 : 1) : ((sxs & 1) === 0 ? 1 : 2);
|
|
694
1011
|
const e = setup.signal.site(sxs, sys) + dark[c];
|
|
695
1012
|
siteChannel[slot] = c;
|
|
696
|
-
if (tracking) {
|
|
1013
|
+
if (tracking && !statistics) {
|
|
697
1014
|
siteElectrons[slot] = e;
|
|
698
1015
|
const own = setup.signal.uniformMaterial;
|
|
699
1016
|
material = material === -2 || material === own ? own : -1;
|
|
700
|
-
|
|
1017
|
+
if (material >= 0)
|
|
1018
|
+
continue;
|
|
1019
|
+
// The first site of a second material: the sample is not replayed, so the statistics are taken now, for the earlier
|
|
1020
|
+
// sites and inline from this site on, instead of in a second pass (#444; each site's statistics depend on its own
|
|
1021
|
+
// slot only, so the order leaves every value's bits).
|
|
1022
|
+
statistics = true;
|
|
1023
|
+
for (let lj = 0; lj < li; lj++)
|
|
1024
|
+
siteStatistics(list[lj]);
|
|
701
1025
|
}
|
|
702
1026
|
// As siteStatistics, written out here for the untracked (Speed) path.
|
|
703
1027
|
const kc = k[c], mean = kc * e, variance = kc * kc * e + c0[c], gap = 1 - mean;
|
|
@@ -724,7 +1048,7 @@ export async function renderDisplayView(setup) {
|
|
|
724
1048
|
stamps[pk] = stamp;
|
|
725
1049
|
}
|
|
726
1050
|
}
|
|
727
|
-
const
|
|
1051
|
+
const count = candidateCount, share = 1 / count;
|
|
728
1052
|
if (memo) {
|
|
729
1053
|
for (let d = 0; d < count; d++) {
|
|
730
1054
|
for (let c = 0; c < 3; c++) {
|
|
@@ -758,11 +1082,48 @@ export async function renderDisplayView(setup) {
|
|
|
758
1082
|
}
|
|
759
1083
|
// The terms are written where they are kept: the real pixel's memo slot, or (for the first sample of a one-material key)
|
|
760
1084
|
// a scratch buffer that is then copied; no typed-array view per sample.
|
|
761
|
-
const rt = memoKey >= 0 ? recordScratch
|
|
762
|
-
|
|
1085
|
+
const rt = memoKey >= 0 ? recordScratch : pk >= 0 ? records : undefined, ro = memoKey < 0 && pk >= 0 ? pk * MEMO_SIZE : 0;
|
|
1086
|
+
// Every field of the record (MEMO_SIZE layout) that the propagation below does not write for this sample is zeroed, as a
|
|
1087
|
+
// fill of the whole record would leave it. Always written: [0, 3), [27, 30) and candidate 0's [3, 12) and [21, 24); with
|
|
1088
|
+
// two candidates also candidate 1's [12, 21) and [24, 27) and the choice terms [30, 33); [33, 37) only with the AHD
|
|
1089
|
+
// correction.
|
|
1090
|
+
if (rt) {
|
|
1091
|
+
if (count === 1) {
|
|
1092
|
+
for (let o = 12; o < 21; o++)
|
|
1093
|
+
rt[ro + o] = 0;
|
|
1094
|
+
rt[ro + 24] = 0;
|
|
1095
|
+
rt[ro + 25] = 0;
|
|
1096
|
+
rt[ro + 26] = 0;
|
|
1097
|
+
rt[ro + 30] = 0;
|
|
1098
|
+
rt[ro + 31] = 0;
|
|
1099
|
+
rt[ro + 32] = 0;
|
|
1100
|
+
}
|
|
1101
|
+
rt[ro + 33] = 0;
|
|
1102
|
+
rt[ro + 34] = 0;
|
|
1103
|
+
rt[ro + 35] = 0;
|
|
1104
|
+
rt[ro + 36] = 0;
|
|
1105
|
+
}
|
|
1106
|
+
if (tracking && !ringOn) {
|
|
1107
|
+
if (!statistics)
|
|
1108
|
+
for (let li = 0; li < list.length; li++)
|
|
1109
|
+
siteStatistics(list[li]);
|
|
1110
|
+
}
|
|
1111
|
+
else if (tracking)
|
|
763
1112
|
for (let li = 0; li < list.length; li++) {
|
|
764
|
-
const slot = list[li];
|
|
765
|
-
|
|
1113
|
+
const slot = list[li], q = siteSlotQ[slot], rowStamp = siteSlotStamp[slot];
|
|
1114
|
+
if (q >= 0 && ringStatStamp[q] === rowStamp) {
|
|
1115
|
+
siteMean[slot] = ringMean[q];
|
|
1116
|
+
siteVar[slot] = ringVar[q];
|
|
1117
|
+
siteThird[slot] = ringThird[q];
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
siteStatistics(slot);
|
|
1121
|
+
if (q >= 0) {
|
|
1122
|
+
ringStatStamp[q] = rowStamp;
|
|
1123
|
+
ringMean[q] = siteMean[slot];
|
|
1124
|
+
ringVar[q] = siteVar[slot];
|
|
1125
|
+
ringThird[q] = siteThird[slot];
|
|
1126
|
+
}
|
|
766
1127
|
}
|
|
767
1128
|
// Site variance of each channel through the bilinear kernels (for B_c), then the demosaic's own pixel statistics: the
|
|
768
1129
|
// mean and covariance of its camera channels, averaged over its candidates (AHD picks one per pixel).
|
|
@@ -780,7 +1141,7 @@ export async function renderDisplayView(setup) {
|
|
|
780
1141
|
let e0 = 0, e1 = 0, e2 = 0;
|
|
781
1142
|
for (let d = 0; d < count; d++) {
|
|
782
1143
|
for (let c = 0; c < 3; c++) {
|
|
783
|
-
const sl =
|
|
1144
|
+
const sl = slotsAt[(phase * 2 + d) * 3 + c], wt = weightsAt[(phase * 2 + d) * 3 + c];
|
|
784
1145
|
let mean = 0;
|
|
785
1146
|
for (let t = 0; t < sl.length; t++)
|
|
786
1147
|
mean += wt[t] * siteMean[sl[t]];
|
|
@@ -788,7 +1149,7 @@ export async function renderDisplayView(setup) {
|
|
|
788
1149
|
cmAverage[c] = (d === 0 ? 0 : cmAverage[c]) + mean * share;
|
|
789
1150
|
}
|
|
790
1151
|
// Covariance of channels i, j: Σ over the sites of w_i·w_j·Var(site), with the kernels as dense weights over the window.
|
|
791
|
-
const dense =
|
|
1152
|
+
const dense = denseAt[phase * 2 + d], list = listAt[phase * 2 + d];
|
|
792
1153
|
let v00 = 0, v01 = 0, v02 = 0, v11 = 0, v12 = 0, v22 = 0;
|
|
793
1154
|
for (let li = 0; li < list.length; li++) {
|
|
794
1155
|
const slot = list[li], v = siteVar[slot], w0 = dense[slot], w1 = dense[slotsCount + slot], w2 = dense[2 * slotsCount + slot];
|
|
@@ -844,7 +1205,7 @@ export async function renderDisplayView(setup) {
|
|
|
844
1205
|
}
|
|
845
1206
|
else {
|
|
846
1207
|
// The component is a linear combination of the sites: κ₃ = Σ (Σ_c M·Γ_c·w_c,site)³·κ₃(site).
|
|
847
|
-
const dense =
|
|
1208
|
+
const dense = denseAt[phase * 2 + d], list = listAt[phase * 2 + d];
|
|
848
1209
|
let third = 0;
|
|
849
1210
|
for (let li = 0; li < list.length; li++) {
|
|
850
1211
|
const slot = list[li], coefficient = r0m * bs[0] * dense[slot] + r1m * bs[1] * dense[slotsCount + slot] + r2m * bs[2] * dense[2 * slotsCount + slot];
|
|
@@ -897,7 +1258,8 @@ export async function renderDisplayView(setup) {
|
|
|
897
1258
|
rt[ro + 22 + d * 3] = t1;
|
|
898
1259
|
rt[ro + 23 + d * 3] = t2;
|
|
899
1260
|
}
|
|
900
|
-
|
|
1261
|
+
// Two-argument maxima (inlined; the same value as one four-argument Math.max, which V8 calls with boxed arguments).
|
|
1262
|
+
pixelVariance = Math.max(Math.max(pixelVariance, hv[0]), Math.max(hv[1], hv[2]));
|
|
901
1263
|
}
|
|
902
1264
|
s0 += wE * e0;
|
|
903
1265
|
s1 += wE * e1;
|
|
@@ -914,7 +1276,7 @@ export async function renderDisplayView(setup) {
|
|
|
914
1276
|
// AHD's per-pixel choice between the candidates adds independent variance: with a noise-driven choice each pixel
|
|
915
1277
|
// deviates from the candidates' average by ±½(H − V), variance ¼·Var(H − V) (linear in the sites, at the last
|
|
916
1278
|
// candidate's slopes).
|
|
917
|
-
const dh =
|
|
1279
|
+
const dh = denseAt[phase * 2], dv = denseAt[phase * 2 + 1], list = phaseList[phase];
|
|
918
1280
|
let x0 = 0, x1 = 0, x2 = 0;
|
|
919
1281
|
for (let li = 0; li < list.length; li++) {
|
|
920
1282
|
const slot = list[li], d0 = bs[0] * (dh[slot] - dv[slot]), d1 = bs[1] * (dh[slotsCount + slot] - dv[slotsCount + slot]), d2 = bs[2] * (dh[2 * slotsCount + slot] - dv[2 * slotsCount + slot]);
|