@photivra/engine 2.21.0 → 2.23.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 (172) hide show
  1. package/CHANGELOG.md +150 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +11 -0
  6. package/dist/capture/c2pa-embed.d.ts +116 -0
  7. package/dist/capture/c2pa-embed.js +493 -0
  8. package/dist/capture/c2pa-embed.js.map +1 -0
  9. package/dist/capture/dng-import.d.ts +73 -8
  10. package/dist/capture/dng-import.js +315 -80
  11. package/dist/capture/dng-import.js.map +1 -1
  12. package/dist/capture/dng-opcodes.d.ts +96 -0
  13. package/dist/capture/dng-opcodes.js +391 -0
  14. package/dist/capture/dng-opcodes.js.map +1 -0
  15. package/dist/capture/dng-profile-maps.d.ts +99 -0
  16. package/dist/capture/dng-profile-maps.js +381 -0
  17. package/dist/capture/dng-profile-maps.js.map +1 -0
  18. package/dist/capture/dng-stage-image.d.ts +23 -0
  19. package/dist/capture/dng-stage-image.js +65 -0
  20. package/dist/capture/dng-stage-image.js.map +1 -0
  21. package/dist/capture/jpeg-xl-tiles.d.ts +22 -0
  22. package/dist/capture/jpeg-xl-tiles.js +151 -0
  23. package/dist/capture/jpeg-xl-tiles.js.map +1 -0
  24. package/dist/capture/lossy-jpeg.d.ts +17 -0
  25. package/dist/capture/lossy-jpeg.js +369 -0
  26. package/dist/capture/lossy-jpeg.js.map +1 -0
  27. package/dist/capture/native-raw-development.d.ts +2 -1
  28. package/dist/capture/native-raw-development.js.map +1 -1
  29. package/dist/color/neighbor-highlight-reconstruction.d.ts +76 -0
  30. package/dist/color/neighbor-highlight-reconstruction.js +257 -0
  31. package/dist/color/neighbor-highlight-reconstruction.js.map +1 -0
  32. package/dist/color/sensor-color-development.d.ts +5 -0
  33. package/dist/color/sensor-color-development.js +7 -1
  34. package/dist/color/sensor-color-development.js.map +1 -1
  35. package/dist/color/spectral-spikiness.d.ts +11 -0
  36. package/dist/color/spectral-spikiness.js +23 -0
  37. package/dist/color/spectral-spikiness.js.map +1 -0
  38. package/dist/core/execution-error.d.ts +21 -0
  39. package/dist/core/execution-error.js +12 -0
  40. package/dist/core/execution-error.js.map +1 -0
  41. package/dist/core/version.d.ts +1 -1
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/version.js.map +1 -1
  44. package/dist/facade/catalogue-lens-signal.d.ts +26 -1
  45. package/dist/facade/catalogue-lens-signal.js +25 -7
  46. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  47. package/dist/facade/diffraction-provider.d.ts +1 -1
  48. package/dist/facade/diffraction-provider.js +4 -4
  49. package/dist/facade/diffraction-provider.js.map +1 -1
  50. package/dist/facade/display-view-gpu-session.js +38 -6
  51. package/dist/facade/display-view-gpu-session.js.map +1 -1
  52. package/dist/facade/display-view-gpu.d.ts +13 -5
  53. package/dist/facade/display-view-gpu.js.map +1 -1
  54. package/dist/facade/display-view-normal.d.ts +15 -0
  55. package/dist/facade/display-view-normal.js +44 -0
  56. package/dist/facade/display-view-normal.js.map +1 -0
  57. package/dist/facade/display-view-rendering.d.ts +102 -0
  58. package/dist/facade/display-view-rendering.js +633 -0
  59. package/dist/facade/display-view-rendering.js.map +1 -0
  60. package/dist/facade/display-view.d.ts +37 -65
  61. package/dist/facade/display-view.js +147 -476
  62. package/dist/facade/display-view.js.map +1 -1
  63. package/dist/facade/expected-signal-setup.js +2 -2
  64. package/dist/facade/expected-signal-setup.js.map +1 -1
  65. package/dist/facade/fast-methods.d.ts +1 -1
  66. package/dist/facade/fast-methods.js +37 -8
  67. package/dist/facade/fast-methods.js.map +1 -1
  68. package/dist/facade/generic-lens-capture.d.ts +1 -1
  69. package/dist/facade/generic-lens-capture.js +1 -1
  70. package/dist/facade/generic-lens-capture.js.map +1 -1
  71. package/dist/facade/image-plane.js +41 -10
  72. package/dist/facade/image-plane.js.map +1 -1
  73. package/dist/facade/on-demand-kits.d.ts +14 -0
  74. package/dist/facade/on-demand-kits.js +36 -0
  75. package/dist/facade/on-demand-kits.js.map +1 -0
  76. package/dist/facade/opt-in-kit.d.ts +9 -0
  77. package/dist/facade/opt-in-kit.js +11 -0
  78. package/dist/facade/opt-in-kit.js.map +1 -0
  79. package/dist/facade/quality-kit.d.ts +21 -0
  80. package/dist/facade/quality-kit.js +23 -0
  81. package/dist/facade/quality-kit.js.map +1 -0
  82. package/dist/facade/quality-paths.d.ts +22 -0
  83. package/dist/facade/quality-paths.js +44 -0
  84. package/dist/facade/quality-paths.js.map +1 -0
  85. package/dist/facade/scene-radiance.d.ts +18 -2
  86. package/dist/facade/scene-radiance.js +6 -4
  87. package/dist/facade/scene-radiance.js.map +1 -1
  88. package/dist/facade/simulate-capture.d.ts +32 -13
  89. package/dist/facade/simulate-capture.js +162 -69
  90. package/dist/facade/simulate-capture.js.map +1 -1
  91. package/dist/facade/simulate-display-view.d.ts +10 -12
  92. package/dist/facade/simulate-display-view.js +57 -48
  93. package/dist/facade/simulate-display-view.js.map +1 -1
  94. package/dist/gpu/display-view-gpu-renderer.d.ts +43 -0
  95. package/dist/gpu/display-view-gpu-renderer.js +134 -0
  96. package/dist/gpu/display-view-gpu-renderer.js.map +1 -0
  97. package/dist/gpu/display-view-image-material-module.d.ts +3 -2
  98. package/dist/gpu/display-view-image-material-module.js +25 -3
  99. package/dist/gpu/display-view-image-material-module.js.map +1 -1
  100. package/dist/gpu/display-view-image-quality.d.ts +43 -1
  101. package/dist/gpu/display-view-image-quality.js +256 -11
  102. package/dist/gpu/display-view-image-quality.js.map +1 -1
  103. package/dist/gpu/display-view-quality-gpu.d.ts +13 -64
  104. package/dist/gpu/display-view-quality-gpu.js +55 -240
  105. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  106. package/dist/gpu/display-view-speed-gpu.d.ts +16 -16
  107. package/dist/gpu/display-view-speed-gpu.js +62 -19
  108. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  109. package/dist/gpu/image-kernel-accelerator.js +3 -0
  110. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  111. package/dist/gpu/image-kernel-execution.d.ts +2 -13
  112. package/dist/gpu/image-kernel-execution.js +2 -10
  113. package/dist/gpu/image-kernel-execution.js.map +1 -1
  114. package/dist/gpu/wgsl-display-view-coupled-stub.d.ts +13 -0
  115. package/dist/gpu/wgsl-display-view-coupled-stub.js +29 -0
  116. package/dist/gpu/wgsl-display-view-coupled-stub.js.map +1 -0
  117. package/dist/gpu/wgsl-display-view-coupled.d.ts +91 -0
  118. package/dist/gpu/wgsl-display-view-coupled.js +365 -0
  119. package/dist/gpu/wgsl-display-view-coupled.js.map +1 -0
  120. package/dist/gpu/wgsl-display-view-neighbors.d.ts +46 -0
  121. package/dist/gpu/wgsl-display-view-neighbors.js +114 -0
  122. package/dist/gpu/wgsl-display-view-neighbors.js.map +1 -0
  123. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +6 -9
  124. package/dist/gpu/wgsl-display-view-quality-kernel.js +113 -113
  125. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  126. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +1 -41
  127. package/dist/gpu/wgsl-display-view-speed-kernel.js +73 -10
  128. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  129. package/dist/index.d.ts +11 -3
  130. package/dist/index.js +9 -1
  131. package/dist/index.js.map +1 -1
  132. package/dist/output/camera-channel-response.d.ts +50 -0
  133. package/dist/output/camera-channel-response.js +69 -0
  134. package/dist/output/camera-channel-response.js.map +1 -0
  135. package/dist/output/native-capture-sdr.js +5 -2
  136. package/dist/output/native-capture-sdr.js.map +1 -1
  137. package/dist/output/sdr-rendering.d.ts +18 -1
  138. package/dist/output/sdr-rendering.js +39 -2
  139. package/dist/output/sdr-rendering.js.map +1 -1
  140. package/dist/sensor/native-sensor-report.d.ts +10 -0
  141. package/dist/sensor/native-sensor-report.js +16 -0
  142. package/dist/sensor/native-sensor-report.js.map +1 -0
  143. package/dist/sensor/sensor-characteristics.d.ts +81 -13
  144. package/dist/sensor/sensor-characteristics.js +10 -6
  145. package/dist/sensor/sensor-characteristics.js.map +1 -1
  146. package/docs/API_REFERENCE.md +117 -4
  147. package/docs/QUICK_START.md +37 -8
  148. package/docs/USAGE.md +105 -22
  149. package/docs/api/capture-c2pa-embed.md +177 -0
  150. package/docs/api/capture-dng-import.md +86 -10
  151. package/docs/api/capture-dng-opcodes.md +93 -0
  152. package/docs/api/capture-dng-profile-maps.md +141 -0
  153. package/docs/api/capture-dng-stage-image.md +43 -0
  154. package/docs/api/capture-jpeg-xl-tiles.md +33 -0
  155. package/docs/api/capture-lossy-jpeg.md +41 -0
  156. package/docs/api/capture-native-raw-development.md +2 -1
  157. package/docs/api/core-execution-error.md +43 -0
  158. package/docs/api/core-version.md +1 -1
  159. package/docs/api/exports.json +255 -5
  160. package/docs/api/facade-display-view-gpu.md +5 -2
  161. package/docs/api/facade-display-view.md +11 -15
  162. package/docs/api/facade-fast-methods.md +38 -9
  163. package/docs/api/facade-quality-paths.md +24 -0
  164. package/docs/api/facade-scene-radiance.md +1 -1
  165. package/docs/api/facade-simulate-capture.md +24 -7
  166. package/docs/api/facade-simulate-display-view.md +10 -12
  167. package/docs/api/gpu-image-kernel-execution.md +0 -36
  168. package/docs/api/output-camera-channel-response.md +55 -0
  169. package/docs/api/output-sdr-rendering.md +6 -1
  170. package/docs/api/sensor-sensor-characteristics.md +54 -2
  171. package/llms.txt +1 -0
  172. package/package.json +6 -5
@@ -32,32 +32,17 @@
32
32
  * added as independent per-pixel variance, weighted by Σ_j (w_j/A)², as is the output quantization (Δ²/12).
33
33
  * One correlated normal draw per display pixel (the sum of hundreds of independent site noises) gives the noise.
34
34
  */
35
- import { createDeterministicUniformStream, createPortableStandardNormalPairs, deriveNativeSiteStreamSeedUint32, fmix32, samplePortableStandardNormal } from "../sensor/stochastic-seed.js";
35
+ import { createDeterministicUniformStream, deriveNativeSiteStreamSeedUint32, samplePortableStandardNormal } from "../sensor/stochastic-seed.js";
36
36
  import { encodeSrgbComponentPortable } from "../color/srgb-transfer.js";
37
- import { createPoissonSampler } from "../sensor/raw-readout.js";
38
37
  import { InvalidConfigurationError } from "../core/configuration-error.js";
39
- import { portableCbrt, portableCosTwoPi, portableExp, portableExp2, portableLog2, portablePow } from "../core/portable-math.js";
38
+ import { portableCbrt, portablePow } from "../core/portable-math.js";
40
39
  /** Stream IDs of the display view's two seeded streams, distinct from the native charge (0) and read-noise (1) streams. */
41
40
  export const DISPLAY_VIEW_JITTER_STREAM_ID = 32;
42
41
  export const DISPLAY_VIEW_NOISE_STREAM_ID = 33;
43
- export const DISPLAY_VIEW_AHD_STREAM_ID = 34;
44
42
  export { createDisplayViewExpectedSignal } from "./expected-signal.js";
43
+ import { CLIP_REACH_SIGMA, GL, bigPhi, phi } from "./display-view-normal.js";
45
44
  // ---------------------------------------------------------------------------------------------------------------
46
45
  // Normal-distribution helpers.
47
- /** Standard normal density. */
48
- function phi(z) { return 0.3989422804014327 * portableExp(-0.5 * z * z); }
49
- /**
50
- * Standard normal distribution function, Φ(z) = 1 − Q(z), with Q from the rational approximation 26.2.17 of
51
- * Abramowitz & Stegun, Handbook of Mathematical Functions (NBS AMS 55, 1964; free, US government work):
52
- * absolute error below 7.5e-8.
53
- */
54
- function bigPhi(z) {
55
- const x = Math.abs(z), t = 1 / (1 + 0.2316419 * x);
56
- const q = phi(x) * t * (0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
57
- return z >= 0 ? 1 - q : q;
58
- }
59
- /** Beyond this many standard deviations from a clip level the clip changes the moments by less than 1e-15 of σ². */
60
- const CLIP_REACH_SIGMA = 8.5;
61
46
  /**
62
47
  * Internal (exported for tests). Moments of min(Y, top) for Y ~ N(m, v): mean, variance and the slope d E/d m = P(Y < top). Exact for a normal Y:
63
48
  * E[min(Y,t)] = m − s·(φ(z) − z·(1 − Φ(z))), z = (t − m)/s.
@@ -122,29 +107,6 @@ export function unitClip(m, v, out, skewness = 0) {
122
107
  out[1] = Math.max(0, second - mean * mean);
123
108
  out[2] = inside;
124
109
  }
125
- /** Gauss–Legendre nodes and weights on [−1, 1], 16 points (computed once by Newton iteration on P_16 from cos(π(i + ¾)/(n + ½))). */
126
- const GL = (() => {
127
- const n = 16, x = new Float64Array(n), w = new Float64Array(n);
128
- for (let i = 0; i < n; i++) {
129
- let z = portableCosTwoPi((i + 0.75) / (2 * n + 1)), dp = 0;
130
- for (let it = 0; it < 100; it++) {
131
- let p0 = 1, p1 = z;
132
- for (let k = 2; k <= n; k++) {
133
- const p2 = ((2 * k - 1) * z * p1 - (k - 1) * p0) / k;
134
- p0 = p1;
135
- p1 = p2;
136
- }
137
- dp = n * (z * p1 - p0) / (z * z - 1);
138
- const dz = p1 / dp;
139
- z -= dz;
140
- if (Math.abs(dz) < 1e-16)
141
- break;
142
- }
143
- x[i] = z;
144
- w[i] = 2 / ((1 - z * z) * dp * dp);
145
- }
146
- return { x, w };
147
- })();
148
110
  /** Internal (exported for tests). Positive Reinhard per channel then the clip to [0, 1]: h(x) = x/(1 + x) for x > 0, else 0 (its slope h' = 1/(1 + x)²). */
149
111
  export function reinhardMoments(m, v, out) {
150
112
  if (!(v > 0)) {
@@ -275,44 +237,11 @@ export function footprintWeights(x, y, area, out) {
275
237
  out[3] = x.ww * y.ww / a2;
276
238
  }
277
239
  /** AHD's dependence reach in sites (directional green ±2, colour differences ±1, homogeneity 4-neighbours and 3 × 3 sum). */
278
- const AHD_REACH = 5, AHD_SIDE = 2 * AHD_REACH + 1;
279
- /** Grid of flat-neighbourhood states for the AHD correction: steps per octave of the normalized channel mean, octaves below white (exported for the GPU display view, #479). */
280
- export const AHD_KEY_STEPS = 8, AHD_KEY_OCTAVES = 12;
281
- /** CFA role (0 red, 1 green, 2 blue) of each patch site (dy + 5)·11 + dx + 5, per phase (phaseY·2 + phaseX) of the centre. */
282
- const AHD_ROLES = [0, 1, 2, 3].map((phase) => {
283
- const roles = new Uint8Array(AHD_SIDE * AHD_SIDE), px = phase & 1, py = phase >> 1;
284
- for (let dy = -AHD_REACH; dy <= AHD_REACH; dy++)
285
- for (let dx = -AHD_REACH; dx <= AHD_REACH; dx++) {
286
- const xx = (px + dx) & 1, yy = (py + dy) & 1;
287
- roles[(dy + AHD_REACH) * AHD_SIDE + dx + AHD_REACH] = yy === 0 ? (xx === 0 ? 0 : 1) : (xx === 0 ? 1 : 2);
288
- }
289
- return roles;
290
- });
291
- /**
292
- * 1 for the patch sites the AHD steps read (directional greens within 3 sites read 2 further along their axis, the candidates
293
- * within 2 read 1 further): |dy| ≤ 3 or |dx| ≤ 3. The 16 sites with |dx| ≥ 4 and |dy| ≥ 4 are never read.
294
- */
295
- const AHD_READ = Uint8Array.from({ length: AHD_SIDE * AHD_SIDE }, (_, p) => {
296
- const dx = (p % AHD_SIDE) - AHD_REACH, dy = Math.floor(p / AHD_SIDE) - AHD_REACH;
297
- return Math.abs(dx) <= 3 || Math.abs(dy) <= 3 ? 1 : 0;
298
- });
240
+ const AHD_REACH = 5;
299
241
  /** x², as one correctly rounded product (`**` is implementation-approximated in ECMA-262). */
300
242
  function square(x) { return x * x; }
301
243
  /** CIELAB's f(t) constants (CIE 15:2004): linear below (6/29)³ with slope (29/3)³/116. */
302
244
  const LAB_EPSILON = 216 / 24389, LAB_KAPPA = 24389 / 27;
303
- /** A 32-bit hash of a correction key (MurmurHash3 finalizer of the three quantized means). */
304
- function fmixKey(a, b, c) { return fmix32((Math.imul(a + 1, 0x9e3779b1) ^ Math.imul(b + 1, 0x85ebca77) ^ Math.imul(c + 1, 0xc2b2ae3d)) >>> 0); }
305
- /** Lowest grid level, 2^-AHD_KEY_OCTAVES of white (exact). */
306
- export const AHD_KEY_FLOOR = 1 / 4096;
307
- /** Grid levels per channel (indices 0 to AHD_KEY_OCTAVES·AHD_KEY_STEPS). */
308
- export const AHD_KEY_LEVELS = AHD_KEY_OCTAVES * AHD_KEY_STEPS + 1;
309
- /** Correction states kept per setup, and setups kept (least recently used first out); about 50 bytes per state. */
310
- const AHD_STATE_LIMIT = 1 << 16, AHD_SETUP_LIMIT = 4;
311
- const ahdStateCache = new Map();
312
- /** Every input a correction state depends on besides its indices, as exact decimal text (JavaScript numbers round-trip). */
313
- function correctionKey(seed, correction, perElectron, constantVariance, gains, clipLevel, matrix, toneCurve) {
314
- return [seed, correction.drawsPerState, ...perElectron, ...constantVariance, ...correction.labMatrix.flat(), ...gains, clipLevel, ...matrix.flat(), toneCurve].join(",");
315
- }
316
245
  /**
317
246
  * Internal (exported for tests, #442): the window offset of each slot, slot (dy + reach)·(2·reach + 1) + dx + reach, as
318
247
  * two integer tables: dx[slot] = (slot mod side) − reach and dy[slot] = ⌊slot / side⌋ − reach. The render loop reads them
@@ -349,68 +278,39 @@ export function flatDisplayViewCandidateTables(slots, weights, dense, list) {
349
278
  }
350
279
  return { count, denseAt, listAt, slotsAt, weightsAt };
351
280
  }
352
- /** Internal (exported for tests): the correction states kept across calls, and clearing them. */
353
- export function displayViewCorrectionStateCount() { let n = 0; for (const states of ahdStateCache.values())
354
- n += states.index.size; return n; }
355
- export function clearDisplayViewCorrectionStates() { ahdStateCache.clear(); }
356
- /** Number of grid keys: AHD_KEY_LEVELS³ (a grid key is (i0·AHD_KEY_LEVELS + i1)·AHD_KEY_LEVELS + i2). */
357
- const AHD_GRID_KEY_COUNT = AHD_KEY_LEVELS * AHD_KEY_LEVELS * AHD_KEY_LEVELS;
358
- /** Internal (#447): the correction key of a setup, or null without the AHD correction (the key `renderDisplayView` looks its states up with). */
359
- export function displayViewCorrectionKey(setup) {
360
- if (!setup.ahdCorrection)
361
- return null;
362
- 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);
363
- }
364
281
  /** True for a typed array of the named kind from any realm (a structured clone arrives as one of the receiving realm). */
365
282
  function isTypedArray(value, tag) {
366
283
  return ArrayBuffer.isView(value) && Object.prototype.toString.call(value) === "[object " + tag + "]";
367
284
  }
368
- /** A grid-key list, checked: an Int32Array of keys in [0, AHD_KEY_LEVELS³). */
369
- function checkGridKeys(gridKeys, caller) {
285
+ /** Internal (deprecated API): checks that `gridKeys` is an Int32Array of grid keys, as 2.10 to 2.21 required. */
286
+ export function checkDeprecatedGridKeys(gridKeys, caller) {
370
287
  if (!isTypedArray(gridKeys, "Int32Array"))
371
288
  throw new InvalidConfigurationError(caller + ": gridKeys must be an Int32Array.");
372
289
  const keys = gridKeys;
290
+ // 97³ grid keys, as 2.10 to 2.21 counted them (12 octaves of 8 steps, 97 levels per channel).
373
291
  for (let i = 0; i < keys.length; i++)
374
- if (!(keys[i] >= 0 && keys[i] < AHD_GRID_KEY_COUNT))
375
- throw new InvalidConfigurationError(caller + ": gridKeys[" + i + "] (" + keys[i] + ") is not a grid key, an integer in [0, " + AHD_GRID_KEY_COUNT + ").");
292
+ if (!(keys[i] >= 0 && keys[i] < 97 * 97 * 97))
293
+ throw new InvalidConfigurationError(caller + ": gridKeys[" + i + "] (" + keys[i] + ") is not a grid key, an integer in [0, " + 97 * 97 * 97 + ").");
376
294
  return keys;
377
295
  }
378
296
  /**
379
- * 2.10 (#447): the AHD low-signal correction states this JavaScript realm holds (a Web Worker, a page), one entry per cached
380
- * setup, least recently used first, each state's grid key and three values copied out. Another realm adds them with
381
- * {@link bindDisplayViewCorrectionStates}; a render there then reads them instead of computing them, with the same bytes.
297
+ * 2.10 (#447), deprecated in 2.22.0: returned the AHD low-signal correction states this realm held. The display view keeps no
298
+ * correction states since 2.22.0 (the correction was removed; docs/DISPLAY_VIEW.md, "AHD at low signal"), so this returns an
299
+ * empty array. Kept until 3.0.0.
300
+ * @deprecated Since 2.22.0: returns `[]`; there is nothing to move between realms. Removed in 3.0.0.
382
301
  */
383
- export function serializeDisplayViewCorrectionStates() {
384
- const out = [];
385
- for (const [key, states] of ahdStateCache) {
386
- const gridKeys = new Int32Array(states.index.size), values = new Float64Array(3 * states.index.size);
387
- let i = 0;
388
- for (const [gridKey, n] of states.index) {
389
- gridKeys[i] = gridKey;
390
- values[3 * i] = states.values[3 * n];
391
- values[3 * i + 1] = states.values[3 * n + 1];
392
- values[3 * i + 2] = states.values[3 * n + 2];
393
- i++;
394
- }
395
- out.push({ key, gridKeys, values });
396
- }
397
- return out;
398
- }
302
+ export function serializeDisplayViewCorrectionStates() { return []; }
399
303
  /**
400
- * 2.10 (#447): adds states another realm computed (an export, or `computeCorrectionStates` results) to this realm's cache and
401
- * returns the number of states added. For each entry, the grid keys its setup does not hold yet are appended; known ones are
402
- * left as they are (a state's values follow from its key, so they are equal). Every entry is checked before the cache is
403
- * touched: `key` a non-empty string, `gridKeys` an Int32Array of grid keys, `values` a Float64Array of 3 × `gridKeys.length`
404
- * finite numbers; otherwise it fails with an InvalidConfigurationError and the cache is unchanged. The cache's bounds apply as
405
- * in a render: an unknown key creates a setup and can evict the least recently used of the four kept, and a setup that
406
- * reaches 65 536 states is cleared before the next one is added. Import only keys a prepared view reported
407
- * (`correctionKey()`), so states a render here will read.
304
+ * 2.10 (#447), deprecated in 2.22.0: added states another realm computed to this realm's cache. The display view keeps no
305
+ * correction states since 2.22.0, so this checks `entries` as before (an array of `{ key, gridKeys, values }` with a non-empty
306
+ * string key, an Int32Array of grid keys and a Float64Array of 3 finite values per key; otherwise an
307
+ * InvalidConfigurationError) and returns 0: nothing is stored and no render reads them. Kept until 3.0.0.
308
+ * @deprecated Since 2.22.0: a checked no-op that returns 0. Removed in 3.0.0.
408
309
  */
409
310
  export function bindDisplayViewCorrectionStates(entries) {
410
311
  const caller = "bindDisplayViewCorrectionStates";
411
312
  if (!Array.isArray(entries))
412
313
  throw new InvalidConfigurationError(caller + ": entries must be an array of { key, gridKeys, values }.");
413
- const checked = [];
414
314
  for (let e = 0; e < entries.length; e++) {
415
315
  const entry = entries[e], at = caller + ": entries[" + e + "]";
416
316
  if (typeof entry !== "object" || entry === null || Array.isArray(entry))
@@ -420,8 +320,8 @@ export function bindDisplayViewCorrectionStates(entries) {
420
320
  throw new InvalidConfigurationError(at + " has unknown field " + JSON.stringify(field) + ".");
421
321
  const { key, gridKeys, values } = entry;
422
322
  if (typeof key !== "string" || key.length === 0)
423
- throw new InvalidConfigurationError(at + ".key must be a non-empty string (a prepared view's correctionKey()).");
424
- const keys = checkGridKeys(gridKeys, at);
323
+ throw new InvalidConfigurationError(at + ".key must be a non-empty string.");
324
+ const keys = checkDeprecatedGridKeys(gridKeys, at);
425
325
  if (!isTypedArray(values, "Float64Array"))
426
326
  throw new InvalidConfigurationError(at + ".values must be a Float64Array.");
427
327
  const v = values;
@@ -430,78 +330,16 @@ export function bindDisplayViewCorrectionStates(entries) {
430
330
  for (let i = 0; i < v.length; i++)
431
331
  if (!Number.isFinite(v[i]))
432
332
  throw new InvalidConfigurationError(at + ".values[" + i + "] (" + v[i] + ") is not finite.");
433
- checked.push({ key, gridKeys: keys, values: v });
434
- }
435
- let appended = 0;
436
- for (const entry of checked) {
437
- const states = correctionStates(entry.key);
438
- for (let i = 0; i < entry.gridKeys.length; i++) {
439
- const gridKey = entry.gridKeys[i];
440
- if (states.index.has(gridKey))
441
- continue;
442
- // As gridCorrection stores a state.
443
- if (states.index.size >= AHD_STATE_LIMIT)
444
- states.index.clear();
445
- const n = states.index.size;
446
- if (states.values.length < 3 * (n + 1)) {
447
- const grown = new Float64Array(2 * states.values.length);
448
- grown.set(states.values);
449
- states.values = grown;
450
- }
451
- states.values[3 * n] = entry.values[3 * i];
452
- states.values[3 * n + 1] = entry.values[3 * i + 1];
453
- states.values[3 * n + 2] = entry.values[3 * i + 2];
454
- states.index.set(gridKey, n);
455
- appended++;
456
- }
457
333
  }
458
- return appended;
459
- }
460
- /**
461
- * Internal (#447, `PreparedDisplayView.computeCorrectionStates`): computes the states of the given grid keys for a setup (each
462
- * 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
463
- * three values in the order given. Yields to the host and checks for abort between states, as a render does.
464
- */
465
- export async function computeDisplayViewCorrectionStates(setup, gridKeys) {
466
- const keys = checkGridKeys(gridKeys, "computeCorrectionStates");
467
- setup.checkAbort();
468
- const key = displayViewCorrectionKey(setup), states = correctionStates(key), gridCorrection = createGridCorrection(setup, states);
469
- const outKeys = Int32Array.from(keys), values = new Float64Array(3 * keys.length);
470
- let lastYield = Date.now();
471
- for (let i = 0; i < keys.length; i++) {
472
- 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);
473
- const at = gridCorrection(i0, i1, i2);
474
- values[3 * i] = states.values[at];
475
- values[3 * i + 1] = states.values[at + 1];
476
- values[3 * i + 2] = states.values[at + 2];
477
- if (Date.now() - lastYield >= 8) {
478
- await setup.yieldControl();
479
- setup.checkAbort();
480
- lastYield = Date.now();
481
- }
482
- }
483
- return { key, gridKeys: outKeys, values };
484
- }
485
- /** The state map of one setup (moved to the most recently used end). */
486
- function correctionStates(key) {
487
- let states = ahdStateCache.get(key);
488
- if (states)
489
- ahdStateCache.delete(key);
490
- else {
491
- states = { index: new Map(), values: new Float64Array(3 * 256) };
492
- if (ahdStateCache.size >= AHD_SETUP_LIMIT)
493
- ahdStateCache.delete(ahdStateCache.keys().next().value);
494
- }
495
- ahdStateCache.set(key, states);
496
- return states;
334
+ return 0;
497
335
  }
498
336
  /**
499
337
  * Recorded terms of a one-material sample: [0, 3) the channel site variances csv; [3, 21) per candidate d and channel c the
500
338
  * linear slopes a (3 + (3d + c)·3); [21, 27) per candidate the residual terms; [27, 30) the expected-value sums; [30, 33) the
501
- * AHD choice terms; 33 a 1 when the AHD correction applies and [34, 37) its values; 37 the real pixel's saturation probability
502
- * (#533: the probability that at least one site its demosaic reads is clipped at the white level).
339
+ * AHD choice terms; 33 the real pixel's saturation probability (#533: the probability that at least one site its demosaic
340
+ * reads is clipped at the white level).
503
341
  */
504
- const MEMO_SIZE = 38;
342
+ const MEMO_SIZE = 34;
505
343
  /** Most real pixels a display pixel's footprint may have for the per-real-pixel memo (#392); larger footprints rarely repeat a real pixel. */
506
344
  const PIXEL_MEMO_LIMIT = 1 << 16;
507
345
  /**
@@ -515,83 +353,6 @@ export const EXACT_AREA_MEAN_MAX_PIXELS = 25;
515
353
  const SITE_RING_LIMIT = 1 << 18;
516
354
  /** Pilot points per axis: about one per two sites of the footprint plus its reach, 4 to 16. */
517
355
  function pilotCount(span) { return Math.max(4, Math.min(16, Math.ceil(span / 2))); }
518
- /**
519
- * createAhdDemosaicTask's steps at the centre pixel of an 11 × 11 patch (patch (dy + 5)·11 + dx + 5, normalized site values):
520
- * `choose(phase)` fills the two candidates within 2 sites (`horizontal`, `vertical`: (dy + 2)·5 + dx + 2, three channels) and
521
- * returns the centre's direction, 0 horizontal, 1 vertical, 2 equal homogeneity (both averaged). Greens are read by flat
522
- * index (dy + 3)·7 + dx + 3; every value is formed with the same operations in the same order as the steps themselves,
523
- * except that CIELAB's cube root is `portableCbrt` (within 1 ulp of Math.cbrt) and squares are products, so the result has
524
- * the same bits in every JavaScript engine. (The task keeps its intermediate planes in float32; these are binary64.)
525
- */
526
- export function createAhdCentreSteps(labMatrix, patch) {
527
- const lab = labMatrix, labWhite = Float64Array.from(lab, (row) => row[0] + row[1] + row[2]);
528
- 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];
529
- const lw0 = labWhite[0], lw1 = labWhite[1], lw2 = labWhite[2];
530
- const gh = new Float64Array(49), gv = new Float64Array(49), cand = [new Float64Array(75), new Float64Array(75)], labs = [new Float64Array(75), new Float64Array(75)];
531
- function choose(phase) {
532
- const roles = AHD_ROLES[phase];
533
- // 1. Directional greens within 3 sites.
534
- for (let dy = -3; dy <= 3; dy++)
535
- for (let dx = -3; dx <= 3; dx++) {
536
- const i = (dy + 3) * 7 + dx + 3, p = (dy + AHD_REACH) * AHD_SIDE + dx + AHD_REACH, c = patch[p];
537
- if (roles[p] === 1) {
538
- gh[i] = c;
539
- gv[i] = c;
540
- continue;
541
- }
542
- gh[i] = (patch[p - 1] + patch[p + 1]) / 2 + (2 * c - patch[p - 2] - patch[p + 2]) / 4;
543
- gv[i] = (patch[p - AHD_SIDE] + patch[p + AHD_SIDE]) / 2 + (2 * c - patch[p - 2 * AHD_SIDE] - patch[p + 2 * AHD_SIDE]) / 4;
544
- }
545
- // 2. Candidates within 2 sites; 3. their CIELAB. The four corners (±2, ±2) are not read by step 4 and are skipped.
546
- for (let d = 0; d < 2; d++) {
547
- const g = d === 0 ? gh : gv, o = cand[d], ol = labs[d];
548
- for (let dy = -2; dy <= 2; dy++)
549
- for (let dx = -2; dx <= 2; dx++) {
550
- if ((dx === -2 || dx === 2) && (dy === -2 || dy === 2))
551
- continue;
552
- const i = (dy + 2) * 5 + dx + 2, p = (dy + AHD_REACH) * AHD_SIDE + dx + AHD_REACH, q = (dy + 3) * 7 + dx + 3, own = roles[p], gc = g[q];
553
- o[3 * i + 1] = gc;
554
- if (own === 1) {
555
- const hRole = roles[p + 1], vRole = 2 - hRole;
556
- o[3 * i + hRole] = gc + ((patch[p - 1] - g[q - 1]) + (patch[p + 1] - g[q + 1])) / 2;
557
- o[3 * i + vRole] = gc + ((patch[p - AHD_SIDE] - g[q - 7]) + (patch[p + AHD_SIDE] - g[q + 7])) / 2;
558
- }
559
- else {
560
- o[3 * i + own] = patch[p];
561
- o[3 * i + 2 - own] = gc + ((patch[p - AHD_SIDE - 1] - g[q - 8]) + (patch[p - AHD_SIDE + 1] - g[q - 6]) + (patch[p + AHD_SIDE - 1] - g[q + 6]) +
562
- (patch[p + AHD_SIDE + 1] - g[q + 8])) / 4;
563
- }
564
- const R = o[3 * i], G = o[3 * i + 1], Bc = o[3 * i + 2];
565
- let tx = (l00 * R + l01 * G + l02 * Bc) / lw0, ty = (l10 * R + l11 * G + l12 * Bc) / lw1, tz = (l20 * R + l21 * G + l22 * Bc) / lw2;
566
- tx = tx > LAB_EPSILON ? portableCbrt(tx) : (LAB_KAPPA * tx + 16) / 116;
567
- ty = ty > LAB_EPSILON ? portableCbrt(ty) : (LAB_KAPPA * ty + 16) / 116;
568
- tz = tz > LAB_EPSILON ? portableCbrt(tz) : (LAB_KAPPA * tz + 16) / 116;
569
- ol[3 * i] = 116 * ty - 16;
570
- ol[3 * i + 1] = 500 * (tx - ty);
571
- ol[3 * i + 2] = 200 * (ty - tz);
572
- }
573
- }
574
- // 4. Homogeneity within 1 site, with the adaptive tolerances (neighbours left, right, up, down); 5. the 3 × 3 sum selects
575
- // the candidate.
576
- const h = labs[0], v = labs[1];
577
- let sh = 0, sv = 0;
578
- for (let dy = -1; dy <= 1; dy++)
579
- for (let dx = -1; dx <= 1; dx++) {
580
- const i = 3 * ((dy + 2) * 5 + dx + 2), j0 = i - 3, j1 = i + 3, j2 = i - 15, j3 = i + 15;
581
- const hL0 = Math.abs(h[i] - h[j0]), hL1 = Math.abs(h[i] - h[j1]), hL2 = Math.abs(h[i] - h[j2]), hL3 = Math.abs(h[i] - h[j3]);
582
- const vL0 = Math.abs(v[i] - v[j0]), vL1 = Math.abs(v[i] - v[j1]), vL2 = Math.abs(v[i] - v[j2]), vL3 = Math.abs(v[i] - v[j3]);
583
- const hC0 = square(h[i + 1] - h[j0 + 1]) + square(h[i + 2] - h[j0 + 2]), hC1 = square(h[i + 1] - h[j1 + 1]) + square(h[i + 2] - h[j1 + 2]);
584
- const hC2 = square(h[i + 1] - h[j2 + 1]) + square(h[i + 2] - h[j2 + 2]), hC3 = square(h[i + 1] - h[j3 + 1]) + square(h[i + 2] - h[j3 + 2]);
585
- const vC0 = square(v[i + 1] - v[j0 + 1]) + square(v[i + 2] - v[j0 + 2]), vC1 = square(v[i + 1] - v[j1 + 1]) + square(v[i + 2] - v[j1 + 2]);
586
- const vC2 = square(v[i + 1] - v[j2 + 1]) + square(v[i + 2] - v[j2 + 2]), vC3 = square(v[i + 1] - v[j3 + 1]) + square(v[i + 2] - v[j3 + 2]);
587
- const epsL = Math.min(Math.max(hL0, hL1), Math.max(vL2, vL3)), epsC = Math.min(Math.max(hC0, hC1), Math.max(vC2, vC3));
588
- sh += (hL0 <= epsL && hC0 <= epsC ? 1 : 0) + (hL1 <= epsL && hC1 <= epsC ? 1 : 0) + (hL2 <= epsL && hC2 <= epsC ? 1 : 0) + (hL3 <= epsL && hC3 <= epsC ? 1 : 0);
589
- sv += (vL0 <= epsL && vC0 <= epsC ? 1 : 0) + (vL1 <= epsL && vC1 <= epsC ? 1 : 0) + (vL2 <= epsL && vC2 <= epsC ? 1 : 0) + (vL3 <= epsL && vC3 <= epsC ? 1 : 0);
590
- }
591
- return sh > sv ? 0 : sv > sh ? 1 : 2;
592
- }
593
- return { horizontal: cand[0], vertical: cand[1], choose };
594
- }
595
356
  /**
596
357
  * #457: AHD's direction at real pixels of the noise-free mosaic, as `createAhdDemosaicTask` computes it on the whole raster
597
358
  * (every neighbour read at its reflect-101 position, so a position's greens, candidates, CIELAB and homogeneity are those of
@@ -714,79 +475,6 @@ export function createAhdDirectionPlane(labMatrix, nativeWidth, nativeHeight, co
714
475
  }
715
476
  };
716
477
  }
717
- /**
718
- * The state computer of one setup's AHD correction (#447: shared by the render and `computeDisplayViewCorrectionStates`):
719
- * gridCorrection(i0, i1, i2) returns the offset of the grid state's three values in `states.values`, computing and storing
720
- * the state on first use. The code and its operations are those the render ran in place before.
721
- */
722
- function createGridCorrection(setup, states) {
723
- const { seed, ahdCorrection } = setup;
724
- const k = Float64Array.from(setup.readout, (r) => r.perElectron), c0 = Float64Array.from(setup.readout, (r) => r.constantVariance);
725
- 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];
726
- const g0 = setup.gains[0], g1 = setup.gains[1], g2 = setup.gains[2], L = setup.clipLevel, reinhard = setup.toneCurve === "positive-reinhard-per-channel";
727
- const correction = new Float64Array(3), patch = new Float64Array(AHD_SIDE * AHD_SIDE);
728
- const readStd = Float64Array.from(c0, (v) => Math.sqrt(v));
729
- /** The offset of a grid state's three values in states.values (computed and stored on first use). */
730
- function gridCorrection(i0, i1, i2) {
731
- const key = (i0 * AHD_KEY_LEVELS + i1) * AHD_KEY_LEVELS + i2, cached = states.index.get(key);
732
- if (cached !== undefined)
733
- return 3 * cached;
734
- const electrons = [i0, i1, i2].map((i, c) => portableExp2(i / AHD_KEY_STEPS - AHD_KEY_OCTAVES) / k[c]);
735
- const random = createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, fmixKey(i0, i1, i2), DISPLAY_VIEW_AHD_STREAM_ID));
736
- const sum = new Float64Array(3), draws = ahdCorrection.drawsPerState, poisson = electrons.map((e) => createPoissonSampler(e, true));
737
- const normal = createPortableStandardNormalPairs(random);
738
- for (let draw = 0; draw < draws; draw++) {
739
- const phase = draw & 3, roles = AHD_ROLES[phase];
740
- for (let p = 0; p < AHD_SIDE * AHD_SIDE; p++) {
741
- // The 16 sites the AHD steps never read are not drawn.
742
- if (AHD_READ[p] === 0)
743
- continue;
744
- const c = roles[p], value = k[c] * poisson[c](random) + readStd[c] * normal();
745
- patch[p] = value < 1 ? value : 1;
746
- }
747
- ahdDifference(phase, correction);
748
- for (let comp = 0; comp < 3; comp++)
749
- sum[comp] = sum[comp] + correction[comp] / draws;
750
- }
751
- if (states.index.size >= AHD_STATE_LIMIT)
752
- states.index.clear();
753
- const n = states.index.size;
754
- if (states.values.length < 3 * (n + 1)) {
755
- const grown = new Float64Array(2 * states.values.length);
756
- grown.set(states.values);
757
- states.values = grown;
758
- }
759
- states.values.set(sum, 3 * n);
760
- states.index.set(key, n);
761
- return 3 * n;
762
- }
763
- const steps = createAhdCentreSteps(ahdCorrection.labMatrix, patch);
764
- const rgb = new Float64Array(3), rendered = new Float64Array(3), linear = new Float64Array(3);
765
- /** Develops one deterministic camera pixel and renders it: WB, highlight clip, colour matrix, tone curve, clip to [0, 1]. */
766
- function renderPixel(r, gr, b, out) {
767
- const u0 = Math.min(L, g0 * r), u1 = Math.min(L, g1 * gr), u2 = Math.min(L, g2 * b);
768
- for (let comp = 0; comp < 3; comp++) {
769
- let v = comp === 0 ? m00 * u0 + m01 * u1 + m02 * u2 : comp === 1 ? m10 * u0 + m11 * u1 + m12 * u2 : m20 * u0 + m21 * u1 + m22 * u2;
770
- if (reinhard)
771
- v = v > 0 ? v / (1 + v) : v;
772
- out[comp] = v < 0 ? 0 : v > 1 ? 1 : v;
773
- }
774
- }
775
- /** h(AHD) − ½(h(H) + h(V)) at the centre of the drawn neighbourhood (`createAhdCentreSteps`). */
776
- function ahdDifference(phase, out) {
777
- const dir = steps.choose(phase), a = steps.horizontal, b = steps.vertical, centre = 12;
778
- for (let c = 0; c < 3; c++)
779
- rgb[c] = dir === 0 ? a[3 * centre + c] : dir === 1 ? b[3 * centre + c] : (a[3 * centre + c] + b[3 * centre + c]) / 2;
780
- renderPixel(rgb[0], rgb[1], rgb[2], rendered);
781
- renderPixel(a[3 * centre], a[3 * centre + 1], a[3 * centre + 2], linear);
782
- for (let c = 0; c < 3; c++)
783
- out[c] = rendered[c] - linear[c] / 2;
784
- renderPixel(b[3 * centre], b[3 * centre + 1], b[3 * centre + 2], linear);
785
- for (let c = 0; c < 3; c++)
786
- out[c] = out[c] - linear[c] / 2;
787
- }
788
- return gridCorrection;
789
- }
790
478
  /** Renders the display view (docs/DISPLAY_VIEW.md). */
791
479
  export async function renderDisplayView(setup) {
792
480
  const { frameWidth, frameHeight, margin, nativeWidth, nativeHeight, viewWidth, viewHeight, region, seed, reachSites } = setup;
@@ -849,6 +537,12 @@ export async function renderDisplayView(setup) {
849
537
  const siteElectrons = new Float64Array(slotsCount), siteClip = 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);
850
538
  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);
851
539
  const hm = new Float64Array(3), hv = new Float64Array(3), hs = new Float64Array(3), an = new Float64Array(9);
540
+ // #593, #601: the coupled rendering steps (display-view-rendering.ts); null keeps the per-component arithmetic below unchanged.
541
+ const coupled = setup.coupled ?? null;
542
+ const coupledA = new Float64Array(9), muY = new Float64Array(3), sY = new Float64Array(9), jY = new Float64Array(9);
543
+ let coupledUsed = false;
544
+ // #602: the neighbor reconstruction finds its clipped regions from this render's noise-free channels.
545
+ coupled?.bindView?.(setup);
852
546
  const reach2 = CLIP_REACH_SIGMA * CLIP_REACH_SIGMA;
853
547
  // Site ring (#441, exact path with filled windows only): a native site's expected electrons, chart material and readout moments depend on the
854
548
  // site alone, so they are computed once per render and kept for the region's native columns (plus the reach) in a ring of
@@ -869,9 +563,8 @@ export async function renderDisplayView(setup) {
869
563
  let ringStamp = new Int32Array(0), ringStatStamp = ringStamp, ringMat = ringStamp, siteSlotQ = ringStamp, siteSlotStamp = ringStamp, markX = ringStamp, markY = ringStamp, markM = ringStamp;
870
564
  let ringV = new Float64Array(0), ringMean = ringV, ringVar = ringV, ringThird = ringV, ringClip = ringV, markV = ringV;
871
565
  // (No closure assigns them, so they stay locals of the render loop.)
872
- // AHD low-signal correction: an 11 × 11 noisy neighbourhood (AHD reads up to 5 sites away) and the local AHD steps.
873
- const ahdCorrection = setup.ahdCorrection, windowReach = ahdCorrection ? Math.max(reach, AHD_REACH) : reach;
874
- const cmAverage = new Float64Array(3);
566
+ // With AHD the windows reach 5 sites around a footprint, the sites #457's choice on the noise-free mosaic reads.
567
+ const ahdChoice = setup.ahdChoice, windowReach = ahdChoice ? Math.max(reach, AHD_REACH) : reach;
875
568
  // #457: AHD's choice on the noise-free mosaic. Per candidate the rendered mean and the camera-channel means of the sample.
876
569
  const candidateRendered = new Float64Array(6), candidateChannels = new Float64Array(6);
877
570
  /** The normalized expected value of an in-raster site, clipped at white (the noise-free mosaic AHD's choice reads). */
@@ -882,49 +575,9 @@ export async function renderDisplayView(setup) {
882
575
  while (choiceRows < Math.ceil(sy) + 2 + 2 * AHD_REACH)
883
576
  choiceRows *= 2;
884
577
  const choiceX0 = Math.max(0, exactX0 - AHD_REACH), choiceColumns = exactView ? Math.min(nativeWidth - 1, exactX0 + exactWidth - 1 + AHD_REACH) - choiceX0 + 1 : Math.ceil(sx) + 2 + 2 * AHD_REACH;
885
- const choicePlane = ahdCorrection ? createAhdDirectionPlane(ahdCorrection.labMatrix, nativeWidth, nativeHeight, choiceColumns, exactView ? 2 * choiceRows : choiceRows, noiseFreeSite) : undefined;
578
+ const choicePlane = ahdChoice ? createAhdDirectionPlane(ahdChoice.labMatrix, nativeWidth, nativeHeight, choiceColumns, exactView ? 2 * choiceRows : choiceRows, noiseFreeSite) : undefined;
886
579
  if (choicePlane && exactView)
887
580
  choicePlane.origin(choiceX0);
888
- let correctedPixels = 0;
889
- // The correction of a flat neighbourhood whose sites of channel c have normalized mean μ_c, on a logarithmic grid of states
890
- // (AHD_KEY_STEPS per octave from 2^-AHD_KEY_OCTAVES of white); each grid state is computed once with draws seeded by the state
891
- // alone, so it is the same in every display pixel and region, and a pixel interpolates trilinearly between the 8 states
892
- // around its own channel means.
893
- // Grid states are shared across display pixels and across calls: a state depends only on its indices and on the inputs in
894
- // `correctionKey` (the seed, the draws, the readout scale and noise, the homogeneity matrix and the development), so a cached
895
- // state is the same value the draws would give again.
896
- // A collect pass (#447) records the grid keys its corrected samples read instead of computing or reading states, and
897
- // leaves the cache untouched; its image is discarded.
898
- const collect = setup.ahdCollect;
899
- const states = ahdCorrection && !collect ? correctionStates(displayViewCorrectionKey(setup)) : { index: new Map(), values: new Float64Array(0) };
900
- const gridCorrection = ahdCorrection && !collect ? createGridCorrection(setup, states) : () => 0;
901
- const interpolated = new Float64Array(3);
902
- const gridBase = new Float64Array(3), gridFrac = new Float64Array(3);
903
- function flatCorrection(m0, m1, m2) {
904
- for (let i = 0; i < 3; i++) {
905
- 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;
906
- const base = Math.min(Math.floor(g), AHD_KEY_OCTAVES * AHD_KEY_STEPS - 1);
907
- gridBase[i] = base;
908
- gridFrac[i] = g - base;
909
- }
910
- interpolated.fill(0);
911
- for (let corner = 0; corner < 8; corner++) {
912
- let weight = 1;
913
- for (let i = 0; i < 3; i++)
914
- weight *= (corner >> i) & 1 ? gridFrac[i] : 1 - gridFrac[i];
915
- if (weight === 0)
916
- continue;
917
- const i0 = gridBase[0] + (corner & 1), i1 = gridBase[1] + ((corner >> 1) & 1), i2 = gridBase[2] + ((corner >> 2) & 1);
918
- if (collect) {
919
- collect.add((i0 * AHD_KEY_LEVELS + i1) * AHD_KEY_LEVELS + i2);
920
- continue;
921
- }
922
- const at = gridCorrection(i0, i1, i2), value = states.values;
923
- for (let comp = 0; comp < 3; comp++)
924
- interpolated[comp] = interpolated[comp] + weight * value[at + comp];
925
- }
926
- return interpolated;
927
- }
928
581
  /** Normalized site value of a slot: normal with mean k·e and variance k²·e + constant, clipped at the white level (1). */
929
582
  function siteStatistics(slot) {
930
583
  // The channel and electrons are read from the slot arrays: a double passed to a call that is not inlined is boxed.
@@ -1045,7 +698,7 @@ export async function renderDisplayView(setup) {
1045
698
  }
1046
699
  pixelGeneration++;
1047
700
  }
1048
- let s0 = 0, s1 = 0, s2 = 0, c00 = 0, c01 = 0, c02 = 0, c11 = 0, c12 = 0, c22 = 0, r0 = 0, r1 = 0, r2 = 0, pixelVariance = 0, corrected = false, sat = 0;
701
+ let s0 = 0, s1 = 0, s2 = 0, c00 = 0, c01 = 0, c02 = 0, c11 = 0, c12 = 0, c22 = 0, r0 = 0, r1 = 0, r2 = 0, sat = 0;
1049
702
  for (let n = 0; n < sampleTotal; n++) {
1050
703
  // A sample's weights: 1 for the stratified estimator (its sums are divided by Q below); a_j for the expected value and
1051
704
  // linear covariance and a_j² for the independent residual on the exact path. Multiplying by 1 leaves every value's bits.
@@ -1238,36 +891,23 @@ export async function renderDisplayView(setup) {
1238
891
  r1 += wR * memo[mo + 31];
1239
892
  r2 += wR * memo[mo + 32];
1240
893
  }
1241
- if (memo[mo + 33] === 1) {
1242
- s0 += wE * memo[mo + 34];
1243
- s1 += wE * memo[mo + 35];
1244
- s2 += wE * memo[mo + 36];
1245
- corrected = true;
1246
- }
1247
- sat += wE * memo[mo + 37];
894
+ sat += wE * memo[mo + 33];
1248
895
  continue;
1249
896
  }
1250
897
  // The terms are written where they are kept: the real pixel's memo slot; no typed-array view per sample.
1251
898
  const rt = pk >= 0 ? records : undefined, ro = pk >= 0 ? pk * MEMO_SIZE : 0;
1252
899
  // Every field of the record (MEMO_SIZE layout) that the propagation below does not write for this sample is zeroed, as a
1253
900
  // fill of the whole record would leave it. Always written: [0, 3), [27, 30) and candidate 0's [3, 12) and [21, 24); with
1254
- // two candidates also candidate 1's [12, 21) and [24, 27) and the choice terms [30, 33); [33, 37) only with the AHD
1255
- // correction.
1256
- if (rt) {
1257
- if (count === 1) {
1258
- for (let o = 12; o < 21; o++)
1259
- rt[ro + o] = 0;
1260
- rt[ro + 24] = 0;
1261
- rt[ro + 25] = 0;
1262
- rt[ro + 26] = 0;
1263
- rt[ro + 30] = 0;
1264
- rt[ro + 31] = 0;
1265
- rt[ro + 32] = 0;
1266
- }
1267
- rt[ro + 33] = 0;
1268
- rt[ro + 34] = 0;
1269
- rt[ro + 35] = 0;
1270
- rt[ro + 36] = 0;
901
+ // two candidates also candidate 1's [12, 21) and [24, 27) and the choice terms [30, 33).
902
+ if (rt && count === 1) {
903
+ for (let o = 12; o < 21; o++)
904
+ rt[ro + o] = 0;
905
+ rt[ro + 24] = 0;
906
+ rt[ro + 25] = 0;
907
+ rt[ro + 26] = 0;
908
+ rt[ro + 30] = 0;
909
+ rt[ro + 31] = 0;
910
+ rt[ro + 32] = 0;
1271
911
  }
1272
912
  if (tracking && !ringOn) {
1273
913
  if (!statistics)
@@ -1314,7 +954,6 @@ export async function renderDisplayView(setup) {
1314
954
  for (let t = 0; t < sl.length; t++)
1315
955
  mean += wt[t] * siteMean[sl[t]];
1316
956
  cm[c] = mean;
1317
- cmAverage[c] = (d === 0 ? 0 : cmAverage[c]) + mean * share;
1318
957
  }
1319
958
  // Covariance of channels i, j: Σ over the sites of w_i·w_j·Var(site), with the kernels as dense weights over the window.
1320
959
  const dense = denseAt[phase * 2 + d], list = listAt[phase * 2 + d];
@@ -1337,53 +976,95 @@ export async function renderDisplayView(setup) {
1337
976
  C[6] = v02;
1338
977
  C[7] = v12;
1339
978
  C[8] = v22;
1340
- // White balance and the highlight clip at the smallest gain (per channel; covariances scaled by the expected slopes).
1341
- for (let c = 0; c < 3; c++) {
1342
- const mean = cm[c], variance = C[c * 4];
1343
- cv[c] = variance;
1344
- const gc = c === 0 ? g0 : c === 1 ? g1 : g2, um = gc * mean, uv = gc * gc * variance, gap = L - um;
1345
- if (gap > 0 && gap * gap > reach2 * uv) {
1346
- bm[c] = um;
1347
- bv[c] = uv;
1348
- bs[c] = gc;
1349
- }
1350
- else {
1351
- upperClip(um, uv, L, clip);
1352
- bm[c] = clip[0];
1353
- bv[c] = clip[1];
1354
- bs[c] = gc * clip[2];
979
+ // #593: where the reconstruct blend is within reach the whole chain is integrated (display-view-rendering.ts); out of
980
+ // its reach reconstruct is the unclipped white balance.
981
+ let chainRun = coupled !== null && coupled.reconstruct && coupled.reconstructInReach(cm, C);
982
+ coupledUsed = chainRun;
983
+ if (!chainRun) {
984
+ // White balance and the highlight clip at the smallest gain (per channel; covariances scaled by the expected slopes).
985
+ for (let c = 0; c < 3; c++) {
986
+ const mean = cm[c], variance = C[c * 4];
987
+ cv[c] = variance;
988
+ const gc = c === 0 ? g0 : c === 1 ? g1 : g2, um = gc * mean, uv = gc * gc * variance, gap = L - um;
989
+ if (coupled?.reconstruct || gap > 0 && gap * gap > reach2 * uv) {
990
+ bm[c] = um;
991
+ bv[c] = uv;
992
+ bs[c] = gc;
993
+ }
994
+ else {
995
+ upperClip(um, uv, L, clip);
996
+ bm[c] = clip[0];
997
+ bv[c] = clip[1];
998
+ bs[c] = gc * clip[2];
999
+ }
1355
1000
  }
1356
1001
  }
1357
- // Colour matrix, then the rendering of each component (tone curve, clip to [0, 1]).
1358
- for (let comp = 0; comp < 3; comp++) {
1359
- const r0m = comp === 0 ? m00 : comp === 1 ? m10 : m20, r1m = comp === 0 ? m01 : comp === 1 ? m11 : m21, r2m = comp === 0 ? m02 : comp === 1 ? m12 : m22;
1360
- const mean = r0m * bm[0] + r1m * bm[1] + r2m * bm[2];
1361
- const variance = r0m * r0m * bv[0] + r1m * r1m * bv[1] + r2m * r2m * bv[2] +
1362
- 2 * (r0m * r1m * bs[0] * bs[1] * C[1] + r0m * r2m * bs[0] * bs[2] * C[2] + r1m * r2m * bs[1] * bs[2] * C[5]);
1363
- if (!reinhard && mean > 0 && mean < 1 && mean * mean > reach2 * variance && (1 - mean) * (1 - mean) > reach2 * variance) {
1364
- hm[comp] = mean;
1365
- hv[comp] = variance;
1366
- hs[comp] = 1;
1367
- }
1368
- else if (reinhard) {
1369
- reinhardMoments(mean, variance, clip);
1370
- hm[comp] = clip[0];
1371
- hv[comp] = clip[1];
1372
- hs[comp] = clip[2];
1002
+ if (!chainRun && coupled?.luminanceTone) {
1003
+ // The developed y = M·b: mean and covariance (diagonal from the clipped variances, off-diagonal through the slopes).
1004
+ for (let r = 0; r < 3; r++)
1005
+ muY[r] = M[r][0] * bm[0] + M[r][1] * bm[1] + M[r][2] * bm[2];
1006
+ for (let r = 0; r < 3; r++)
1007
+ for (let q = 0; q < 3; q++) {
1008
+ let v = 0;
1009
+ for (let i = 0; i < 3; i++)
1010
+ for (let j = 0; j < 3; j++)
1011
+ v += M[r][i] * M[q][j] * (i === j ? bv[i] : bs[i] * bs[j] * C[i * 3 + j]);
1012
+ sY[r * 3 + q] = v;
1013
+ }
1014
+ if (coupled.tone(muY, sY, hm, hv, jY)) {
1015
+ for (let r = 0; r < 3; r++)
1016
+ for (let c = 0; c < 3; c++)
1017
+ coupledA[r * 3 + c] = (jY[r * 3] * M[0][c] + jY[r * 3 + 1] * M[1][c] + jY[r * 3 + 2] * M[2][c]) * bs[c];
1018
+ coupledUsed = true;
1373
1019
  }
1374
- else {
1375
- // The component is a linear combination of the sites: κ₃ = Σ (Σ_c M·Γ_c·w_c,site)³·κ₃(site).
1376
- const dense = denseAt[phase * 2 + d], list = listAt[phase * 2 + d];
1377
- let third = 0;
1378
- for (let li = 0; li < list.length; li++) {
1379
- const slot = list[li], coefficient = r0m * bs[0] * dense[slot] + r1m * bs[1] * dense[slotsCount + slot] + r2m * bs[2] * dense[2 * slotsCount + slot];
1380
- third += coefficient * coefficient * coefficient * siteThird[slot];
1020
+ else
1021
+ chainRun = true;
1022
+ }
1023
+ else if (!chainRun) {
1024
+ // Colour matrix, then the rendering of each component (tone curve, clip to [0, 1]).
1025
+ for (let comp = 0; comp < 3; comp++) {
1026
+ const r0m = comp === 0 ? m00 : comp === 1 ? m10 : m20, r1m = comp === 0 ? m01 : comp === 1 ? m11 : m21, r2m = comp === 0 ? m02 : comp === 1 ? m12 : m22;
1027
+ const mean = r0m * bm[0] + r1m * bm[1] + r2m * bm[2];
1028
+ const variance = r0m * r0m * bv[0] + r1m * r1m * bv[1] + r2m * r2m * bv[2] +
1029
+ 2 * (r0m * r1m * bs[0] * bs[1] * C[1] + r0m * r2m * bs[0] * bs[2] * C[2] + r1m * r2m * bs[1] * bs[2] * C[5]);
1030
+ if (!reinhard && mean > 0 && mean < 1 && mean * mean > reach2 * variance && (1 - mean) * (1 - mean) > reach2 * variance) {
1031
+ hm[comp] = mean;
1032
+ hv[comp] = variance;
1033
+ hs[comp] = 1;
1034
+ }
1035
+ else if (reinhard) {
1036
+ reinhardMoments(mean, variance, clip);
1037
+ hm[comp] = clip[0];
1038
+ hv[comp] = clip[1];
1039
+ hs[comp] = clip[2];
1040
+ }
1041
+ else {
1042
+ // The component is a linear combination of the sites: κ₃ = Σ (Σ_c M·Γ_c·w_c,site)³·κ₃(site).
1043
+ const dense = denseAt[phase * 2 + d], list = listAt[phase * 2 + d];
1044
+ let third = 0;
1045
+ for (let li = 0; li < list.length; li++) {
1046
+ const slot = list[li], coefficient = r0m * bs[0] * dense[slot] + r1m * bs[1] * dense[slotsCount + slot] + r2m * bs[2] * dense[2 * slotsCount + slot];
1047
+ third += coefficient * coefficient * coefficient * siteThird[slot];
1048
+ }
1049
+ unitClip(mean, variance, clip, variance > 0 ? third / (variance * Math.sqrt(variance)) : 0);
1050
+ hm[comp] = clip[0];
1051
+ hv[comp] = clip[1];
1052
+ hs[comp] = clip[2];
1381
1053
  }
1382
- unitClip(mean, variance, clip, variance > 0 ? third / (variance * Math.sqrt(variance)) : 0);
1383
- hm[comp] = clip[0];
1384
- hv[comp] = clip[1];
1385
- hs[comp] = clip[2];
1386
1054
  }
1055
+ // #601: a component that reaches 1 within reach is desaturated with the others: the chain is integrated.
1056
+ if (coupled?.desaturate && !reinhard)
1057
+ for (let comp = 0; comp < 3; comp++) {
1058
+ const mean = hm[comp];
1059
+ // The test is taken at 0.999 rather than 1, so a component clipped to 1 is decided in float32 too (the GPU kernels).
1060
+ if (mean > 0 && hm[comp] + CLIP_REACH_SIGMA * Math.sqrt(hv[comp]) >= 0.999)
1061
+ chainRun = true;
1062
+ }
1063
+ }
1064
+ if (chainRun) {
1065
+ coupled.at?.(x, y);
1066
+ coupled.chain(cm, C, hm, hv, coupledA);
1067
+ coupledUsed = true;
1387
1068
  }
1388
1069
  e0 += share * hm[0];
1389
1070
  e1 += share * hm[1];
@@ -1398,7 +1079,7 @@ export async function renderDisplayView(setup) {
1398
1079
  // linear variance (aᵀ·C·a, full channel covariance) for the residual.
1399
1080
  let l0 = 0, l1 = 0, l2 = 0;
1400
1081
  for (let c = 0; c < 3; c++) {
1401
- const a0 = hs[0] * (c === 0 ? m00 : c === 1 ? m01 : m02) * bs[c], a1 = hs[1] * (c === 0 ? m10 : c === 1 ? m11 : m12) * bs[c], a2 = hs[2] * (c === 0 ? m20 : c === 1 ? m21 : m22) * bs[c];
1082
+ const a0 = coupledUsed ? coupledA[c] : hs[0] * (c === 0 ? m00 : c === 1 ? m01 : m02) * bs[c], a1 = coupledUsed ? coupledA[3 + c] : hs[1] * (c === 0 ? m10 : c === 1 ? m11 : m12) * bs[c], a2 = coupledUsed ? coupledA[6 + c] : hs[2] * (c === 0 ? m20 : c === 1 ? m21 : m22) * bs[c];
1402
1083
  const wgt = wE * share * B[c] * csv[c];
1403
1084
  c00 += wgt * a0 * a0;
1404
1085
  c01 += wgt * a0 * a1;
@@ -1432,8 +1113,6 @@ export async function renderDisplayView(setup) {
1432
1113
  rt[ro + 22 + d * 3] = t1;
1433
1114
  rt[ro + 23 + d * 3] = t2;
1434
1115
  }
1435
- // Two-argument maxima (inlined; the same value as one four-argument Math.max, which V8 calls with boxed arguments).
1436
- pixelVariance = Math.max(Math.max(pixelVariance, hv[0]), Math.max(hv[1], hv[2]));
1437
1116
  }
1438
1117
  // #457: AHD chooses one candidate per pixel by homogeneity. Where the scene decides that choice (structure finer than a
1439
1118
  // few sites, such as a Siemens star's centre), the expected value is the chosen candidate's, not the candidates' average:
@@ -1481,7 +1160,7 @@ export async function renderDisplayView(setup) {
1481
1160
  const saturation = 1 - unclipped;
1482
1161
  sat += wE * saturation;
1483
1162
  if (rt)
1484
- rt[ro + 37] = saturation;
1163
+ rt[ro + 33] = saturation;
1485
1164
  if (count === 2) {
1486
1165
  // AHD's per-pixel choice between the candidates adds independent variance: with a noise-driven choice each pixel
1487
1166
  // deviates from the candidates' average by ±½(H − V), variance ¼·Var(H − V) (linear in the sites, at the last
@@ -1490,7 +1169,20 @@ export async function renderDisplayView(setup) {
1490
1169
  let x0 = 0, x1 = 0, x2 = 0;
1491
1170
  for (let li = 0; li < list.length; li++) {
1492
1171
  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]);
1493
- const v = siteVar[slot], a = hs[0] * (m00 * d0 + m01 * d1 + m02 * d2), b = hs[1] * (m10 * d0 + m11 * d1 + m12 * d2), c = hs[2] * (m20 * d0 + m21 * d1 + m22 * d2);
1172
+ const v = siteVar[slot];
1173
+ let a, b, c;
1174
+ if (coupledUsed) {
1175
+ // #593: the coupled slopes ∂out/∂c (camera channels) of the last candidate.
1176
+ const e0 = dh[slot] - dv[slot], e1 = dh[slotsCount + slot] - dv[slotsCount + slot], e2 = dh[2 * slotsCount + slot] - dv[2 * slotsCount + slot];
1177
+ a = coupledA[0] * e0 + coupledA[1] * e1 + coupledA[2] * e2;
1178
+ b = coupledA[3] * e0 + coupledA[4] * e1 + coupledA[5] * e2;
1179
+ c = coupledA[6] * e0 + coupledA[7] * e1 + coupledA[8] * e2;
1180
+ }
1181
+ else {
1182
+ a = hs[0] * (m00 * d0 + m01 * d1 + m02 * d2);
1183
+ b = hs[1] * (m10 * d0 + m11 * d1 + m12 * d2);
1184
+ c = hs[2] * (m20 * d0 + m21 * d1 + m22 * d2);
1185
+ }
1494
1186
  x0 += a * a * v;
1495
1187
  x1 += b * b * v;
1496
1188
  x2 += c * c * v;
@@ -1504,31 +1196,10 @@ export async function renderDisplayView(setup) {
1504
1196
  rt[ro + 32] = x2 / 4;
1505
1197
  }
1506
1198
  }
1507
- // AHD at low signal: its homogeneity choice reacts to the noise, which the linear candidates cannot express. A sample
1508
- // whose pixel noise exceeds the threshold adds the correction of a flat neighbourhood with its channel means, from the
1509
- // shared grid of states (flatCorrection): there AHD and the two linear candidates run on the same drawn neighbourhoods
1510
- // and the difference of the rendered results is averaged, a control variate whose expectation is the AHD bias
1511
- // (docs/DISPLAY_VIEW.md).
1512
- if (ahdCorrection && pixelVariance > ahdCorrection.thresholdVariance) {
1513
- const fix = flatCorrection(cmAverage[0], cmAverage[1], cmAverage[2]);
1514
- s0 += wE * fix[0];
1515
- s1 += wE * fix[1];
1516
- s2 += wE * fix[2];
1517
- corrected = true;
1518
- if (rt) {
1519
- rt[ro + 33] = 1;
1520
- rt[ro + 34] = fix[0];
1521
- rt[ro + 35] = fix[1];
1522
- rt[ro + 36] = fix[2];
1523
- }
1524
- }
1525
1199
  if (pk >= 0)
1526
1200
  stamps[pk] = stamp;
1527
- pixelVariance = 0;
1528
1201
  }
1529
1202
  sampleCount += sampleTotal;
1530
- if (corrected)
1531
- correctedPixels++;
1532
1203
  // Stratified: means over Q samples. Exact: the weights already sum to 1 (Σ a_j = 1) and the residual carries Σ a_j²·r_j.
1533
1204
  const Qd = exactView ? 1 : Q, mean0 = s0 / Qd, mean1 = s1 / Qd, mean2 = s2 / Qd, pixels = B[3];
1534
1205
  // #533: the expected saturated real pixels of the rectangle (A real pixels): A·Σ_j a_j·P_j, or A times the samples' mean.
@@ -1566,7 +1237,7 @@ export async function renderDisplayView(setup) {
1566
1237
  }
1567
1238
  }
1568
1239
  }
1569
- return { samples, expected, noiseStd, sampleCount, siteCount, detailPixelCount: detail, uniformPixelCount: uniform, ahdCorrectedPixelCount: correctedPixels,
1240
+ return { samples, expected, noiseStd, sampleCount, siteCount, detailPixelCount: detail, uniformPixelCount: uniform,
1570
1241
  estimation: exactView ? "exact-area-mean" : "stratified-monte-carlo", saturated, saturatedPixelCount: saturatedTotal };
1571
1242
  }
1572
1243
  /**