@photivra/engine 2.12.0 → 2.14.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 (148) hide show
  1. package/CHANGELOG.md +144 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -7
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +3 -0
  6. package/dist/camera/lens-projection.js +2 -1
  7. package/dist/camera/lens-projection.js.map +1 -1
  8. package/dist/capture/dng-import.js +1 -1
  9. package/dist/capture/dng-import.js.map +1 -1
  10. package/dist/capture/export-jpeg.d.ts +1 -1
  11. package/dist/capture/export-jpeg.js +6 -4
  12. package/dist/capture/export-jpeg.js.map +1 -1
  13. package/dist/capture/host-yield-cadence.js +1 -1
  14. package/dist/capture/host-yield-cadence.js.map +1 -1
  15. package/dist/capture/native-environment-optical-stage.js +1 -1
  16. package/dist/capture/native-environment-optical-stage.js.map +1 -1
  17. package/dist/capture/native-raw.js +1 -1
  18. package/dist/capture/native-raw.js.map +1 -1
  19. package/dist/capture/packed-sensor-raw.js +3 -3
  20. package/dist/capture/packed-sensor-raw.js.map +1 -1
  21. package/dist/capture/sensor-raw-producer.js +1 -1
  22. package/dist/capture/sensor-raw-producer.js.map +1 -1
  23. package/dist/color/capture-color.js +3 -3
  24. package/dist/color/capture-color.js.map +1 -1
  25. package/dist/color/chromatic-adaptation.js +2 -2
  26. package/dist/color/chromatic-adaptation.js.map +1 -1
  27. package/dist/color/color-matching-functions.js +1 -1
  28. package/dist/color/color-matching-functions.js.map +1 -1
  29. package/dist/color/colorimetry-math.js +2 -2
  30. package/dist/color/colorimetry-math.js.map +1 -1
  31. package/dist/color/illuminants.js +3 -3
  32. package/dist/color/illuminants.js.map +1 -1
  33. package/dist/color/rgb-color-spaces.js +1 -1
  34. package/dist/color/rgb-color-spaces.js.map +1 -1
  35. package/dist/color/sensor-color-development.js +1 -1
  36. package/dist/color/sensor-color-development.js.map +1 -1
  37. package/dist/core/record-validation.js +3 -3
  38. package/dist/core/record-validation.js.map +1 -1
  39. package/dist/core/version.d.ts +1 -1
  40. package/dist/core/version.js +1 -1
  41. package/dist/core/version.js.map +1 -1
  42. package/dist/equipment/generic-prime-lenses.js +3 -3
  43. package/dist/equipment/generic-prime-lenses.js.map +1 -1
  44. package/dist/equipment/generic-tier-assets.d.ts +23 -1
  45. package/dist/equipment/generic-tier-assets.js +58 -30
  46. package/dist/equipment/generic-tier-assets.js.map +1 -1
  47. package/dist/equipment/generic-tier-presets.d.ts +6 -3
  48. package/dist/equipment/generic-tier-presets.js +26 -13
  49. package/dist/equipment/generic-tier-presets.js.map +1 -1
  50. package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
  51. package/dist/exposure/auto-iso-slowest-shutter.js +217 -0
  52. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -0
  53. package/dist/exposure/exposure-mode-resolver.d.ts +57 -1
  54. package/dist/exposure/exposure-mode-resolver.js +238 -33
  55. package/dist/exposure/exposure-mode-resolver.js.map +1 -1
  56. package/dist/exposure/flash-ttl.js +1 -1
  57. package/dist/exposure/flash-ttl.js.map +1 -1
  58. package/dist/exposure/flash.js +1 -1
  59. package/dist/exposure/flash.js.map +1 -1
  60. package/dist/facade/chart-material-layers.js +2 -2
  61. package/dist/facade/chart-material-layers.js.map +1 -1
  62. package/dist/facade/chart-radiance-batch.js +1 -1
  63. package/dist/facade/chart-radiance-batch.js.map +1 -1
  64. package/dist/facade/chart-scenes.d.ts +45 -8
  65. package/dist/facade/chart-scenes.js +101 -44
  66. package/dist/facade/chart-scenes.js.map +1 -1
  67. package/dist/facade/color-patches.d.ts +129 -0
  68. package/dist/facade/color-patches.js +209 -0
  69. package/dist/facade/color-patches.js.map +1 -0
  70. package/dist/facade/diffraction-provider.js +12 -13
  71. package/dist/facade/diffraction-provider.js.map +1 -1
  72. package/dist/facade/display-view-gpu.js +1 -1
  73. package/dist/facade/display-view-gpu.js.map +1 -1
  74. package/dist/facade/display-view.d.ts +27 -0
  75. package/dist/facade/display-view.js +234 -65
  76. package/dist/facade/display-view.js.map +1 -1
  77. package/dist/facade/scene-radiance.js +2 -2
  78. package/dist/facade/scene-radiance.js.map +1 -1
  79. package/dist/facade/simulate-capture.d.ts +139 -12
  80. package/dist/facade/simulate-capture.js +209 -36
  81. package/dist/facade/simulate-capture.js.map +1 -1
  82. package/dist/facade/simulate-display-view.js +2 -2
  83. package/dist/facade/simulate-display-view.js.map +1 -1
  84. package/dist/gpu/chart-composite-gpu.js +2 -2
  85. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  86. package/dist/gpu/display-view-composite-material-module.js +1 -1
  87. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  88. package/dist/gpu/display-view-quality-gpu.d.ts +2 -1
  89. package/dist/gpu/display-view-quality-gpu.js +24 -14
  90. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  91. package/dist/gpu/image-kernel-execution.js +2 -2
  92. package/dist/gpu/image-kernel-execution.js.map +1 -1
  93. package/dist/gpu/temporal-sum-gpu.js +23 -23
  94. package/dist/gpu/webgpu-runtime.js +1 -1
  95. package/dist/gpu/webgpu-runtime.js.map +1 -1
  96. package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
  97. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
  98. package/dist/gpu/wgsl-chart-kernel.js +117 -117
  99. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  100. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +13 -1
  101. package/dist/gpu/wgsl-display-view-quality-kernel.js +522 -488
  102. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  103. package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
  104. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  105. package/dist/gpu/wgsl-kernels.js +203 -203
  106. package/dist/gpu/wgsl-kernels.js.map +1 -1
  107. package/dist/gpu/wgsl-sensor-kernel.js +302 -302
  108. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  109. package/dist/index.d.ts +8 -5
  110. package/dist/index.js +6 -3
  111. package/dist/index.js.map +1 -1
  112. package/dist/motion/temporal-exposure-sum.js +1 -1
  113. package/dist/motion/temporal-exposure-sum.js.map +1 -1
  114. package/dist/optics/diffraction-psf-profile.js +4 -4
  115. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  116. package/dist/optics/front-of-lens-filter.js +1 -1
  117. package/dist/optics/front-of-lens-filter.js.map +1 -1
  118. package/dist/optics/irradiance-convolution.d.ts +10 -0
  119. package/dist/optics/irradiance-convolution.js +49 -14
  120. package/dist/optics/irradiance-convolution.js.map +1 -1
  121. package/dist/output/native-capture-sdr.js +1 -1
  122. package/dist/output/native-capture-sdr.js.map +1 -1
  123. package/dist/output/png-encoder.js +2 -2
  124. package/dist/output/png-encoder.js.map +1 -1
  125. package/dist/output/print-jpeg.js +13 -10
  126. package/dist/output/print-jpeg.js.map +1 -1
  127. package/dist/output/text-metadata.d.ts +22 -2
  128. package/dist/output/text-metadata.js +58 -9
  129. package/dist/output/text-metadata.js.map +1 -1
  130. package/dist/sensor/fixed-pattern-noise.js +1 -1
  131. package/dist/sensor/fixed-pattern-noise.js.map +1 -1
  132. package/dist/sensor/readout-exposure-linkage.js +1 -1
  133. package/dist/sensor/readout-exposure-linkage.js.map +1 -1
  134. package/docs/API_REFERENCE.md +49 -1
  135. package/docs/QUICK_START.md +104 -9
  136. package/docs/USAGE.md +241 -2
  137. package/docs/api/core-version.md +1 -1
  138. package/docs/api/equipment-generic-tier-assets.md +41 -1
  139. package/docs/api/equipment-generic-tier-presets.md +11 -3
  140. package/docs/api/exports.json +172 -2
  141. package/docs/api/exposure-auto-iso-slowest-shutter.md +139 -0
  142. package/docs/api/exposure-exposure-mode-resolver.md +68 -0
  143. package/docs/api/facade-chart-scenes.md +36 -5
  144. package/docs/api/facade-color-patches.md +150 -0
  145. package/docs/api/facade-simulate-capture.md +140 -9
  146. package/docs/api/output-text-metadata.md +34 -0
  147. package/llms.txt +1 -0
  148. package/package.json +2 -2
@@ -499,73 +499,19 @@ const SITE_RING_LIMIT = 1 << 18;
499
499
  /** Pilot points per axis: about one per two sites of the footprint plus its reach, 4 to 16. */
500
500
  function pilotCount(span) { return Math.max(4, Math.min(16, Math.ceil(span / 2))); }
501
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.
502
+ * createAhdDemosaicTask's steps at the centre pixel of an 11 × 11 patch (patch (dy + 5)·11 + dx + 5, normalized site values):
503
+ * `choose(phase)` fills the two candidates within 2 sites (`horizontal`, `vertical`: (dy + 2)·5 + dx + 2, three channels) and
504
+ * returns the centre's direction, 0 horizontal, 1 vertical, 2 equal homogeneity (both averaged). Greens are read by flat
505
+ * index (dy + 3)·7 + dx + 3; every value is formed with the same operations in the same order as the steps themselves,
506
+ * except that CIELAB's cube root is `portableCbrt` (within 1 ulp of Math.cbrt) and squares are products, so the result has
507
+ * the same bits in every JavaScript engine. (The task keeps its intermediate planes in float32; these are binary64.)
505
508
  */
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);
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];
510
- const g0 = setup.gains[0], g1 = setup.gains[1], g2 = setup.gains[2], L = setup.clipLevel, reinhard = setup.toneCurve === "positive-reinhard-per-channel";
511
- const correction = new Float64Array(3), patch = new Float64Array(AHD_SIDE * AHD_SIDE);
512
- const readStd = Float64Array.from(c0, (v) => Math.sqrt(v));
513
- /** The offset of a grid state's three values in states.values (computed and stored on first use). */
514
- function gridCorrection(i0, i1, i2) {
515
- const key = (i0 * AHD_KEY_LEVELS + i1) * AHD_KEY_LEVELS + i2, cached = states.index.get(key);
516
- if (cached !== undefined)
517
- return 3 * cached;
518
- const electrons = [i0, i1, i2].map((i, c) => portableExp2(i / AHD_KEY_STEPS - AHD_KEY_OCTAVES) / k[c]);
519
- const random = createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, fmixKey(i0, i1, i2), DISPLAY_VIEW_AHD_STREAM_ID));
520
- const sum = new Float64Array(3), draws = ahdCorrection.drawsPerState, poisson = electrons.map((e) => createPoissonSampler(e, true));
521
- const normal = createPortableStandardNormalPairs(random);
522
- for (let draw = 0; draw < draws; draw++) {
523
- const phase = draw & 3, roles = AHD_ROLES[phase];
524
- for (let p = 0; p < AHD_SIDE * AHD_SIDE; p++) {
525
- // The 16 sites the AHD steps never read are not drawn.
526
- if (AHD_READ[p] === 0)
527
- continue;
528
- const c = roles[p], value = k[c] * poisson[c](random) + readStd[c] * normal();
529
- patch[p] = value < 1 ? value : 1;
530
- }
531
- ahdDifference(phase, correction);
532
- for (let comp = 0; comp < 3; comp++)
533
- sum[comp] = sum[comp] + correction[comp] / draws;
534
- }
535
- if (states.index.size >= AHD_STATE_LIMIT)
536
- states.index.clear();
537
- const n = states.index.size;
538
- if (states.values.length < 3 * (n + 1)) {
539
- const grown = new Float64Array(2 * states.values.length);
540
- grown.set(states.values);
541
- states.values = grown;
542
- }
543
- states.values.set(sum, 3 * n);
544
- states.index.set(key, n);
545
- return 3 * n;
546
- }
547
- const lab = ahdCorrection.labMatrix, labWhite = Float64Array.from(lab, (row) => row[0] + row[1] + row[2]);
509
+ export function createAhdCentreSteps(labMatrix, patch) {
510
+ const lab = labMatrix, labWhite = Float64Array.from(lab, (row) => row[0] + row[1] + row[2]);
548
511
  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];
549
512
  const lw0 = labWhite[0], lw1 = labWhite[1], lw2 = labWhite[2];
550
513
  const gh = new Float64Array(49), gv = new Float64Array(49), cand = [new Float64Array(75), new Float64Array(75)], labs = [new Float64Array(75), new Float64Array(75)];
551
- const rgb = new Float64Array(3), rendered = new Float64Array(3), linear = new Float64Array(3);
552
- /** Develops one deterministic camera pixel and renders it: WB, highlight clip, colour matrix, tone curve, clip to [0, 1]. */
553
- function renderPixel(r, gr, b, out) {
554
- const u0 = Math.min(L, g0 * r), u1 = Math.min(L, g1 * gr), u2 = Math.min(L, g2 * b);
555
- for (let comp = 0; comp < 3; comp++) {
556
- let v = comp === 0 ? m00 * u0 + m01 * u1 + m02 * u2 : comp === 1 ? m10 * u0 + m11 * u1 + m12 * u2 : m20 * u0 + m21 * u1 + m22 * u2;
557
- if (reinhard)
558
- v = v > 0 ? v / (1 + v) : v;
559
- out[comp] = v < 0 ? 0 : v > 1 ? 1 : v;
560
- }
561
- }
562
- /**
563
- * h(AHD) − ½(h(H) + h(V)) at the centre of the drawn neighbourhood (createAhdDemosaicTask's steps, centre pixel only). The
564
- * patch and the greens are read by flat index (patch (dy + 5)·11 + dx + 5, greens (dy + 3)·7 + dx + 3); every value is formed
565
- * with the same operations in the same order as the steps themselves, except that CIELAB's cube root is `portableCbrt`
566
- * (within 1 ulp of Math.cbrt) and squares are products, so the result has the same bits in every JavaScript engine.
567
- */
568
- function ahdDifference(phase, out) {
514
+ function choose(phase) {
569
515
  const roles = AHD_ROLES[phase];
570
516
  // 1. Directional greens within 3 sites.
571
517
  for (let dy = -3; dy <= 3; dy++)
@@ -625,9 +571,195 @@ function createGridCorrection(setup, states) {
625
571
  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);
626
572
  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);
627
573
  }
628
- const centre = 12, a = cand[0], b = cand[1];
574
+ return sh > sv ? 0 : sv > sh ? 1 : 2;
575
+ }
576
+ return { horizontal: cand[0], vertical: cand[1], choose };
577
+ }
578
+ /**
579
+ * #457: AHD's direction at real pixels of the noise-free mosaic, as `createAhdDemosaicTask` computes it on the whole raster
580
+ * (every neighbour read at its reflect-101 position, so a position's greens, candidates, CIELAB and homogeneity are those of
581
+ * the task's planes), with each per-position quantity computed once and kept in rings of native rows. `direction(x, y)` is 0
582
+ * horizontal, 1 vertical, 2 equal homogeneity; `site(x, y)` gives the normalized expected value of an in-raster site, clipped
583
+ * at white. The kept columns are [x0, x0 + columns), set by `origin(x0)` (which also forgets every kept value); they must hold
584
+ * every real pixel asked about and 5 sites around it, clipped to the raster. `rows` is a power of two at least the native rows
585
+ * the real pixels asked about between two `origin` calls span plus 10. The arithmetic is the task's, in binary64 (the task
586
+ * stores its planes in float32), with `portableCbrt` and products for squares, as `createAhdCentreSteps`.
587
+ */
588
+ export function createAhdDirectionPlane(labMatrix, nativeWidth, nativeHeight, columns, rows, site) {
589
+ const lab = labMatrix, labWhite = Float64Array.from(lab, (row) => row[0] + row[1] + row[2]);
590
+ 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];
591
+ const lw0 = labWhite[0], lw1 = labWhite[1], lw2 = labWhite[2];
592
+ const W = columns, mask = rows - 1, n = rows * W;
593
+ // Per position (slot (y & mask)·W + x − x0): the site value, the two greens, the two candidates' RGB and CIELAB and the two
594
+ // homogeneity counts, each valid while its stamp is epoch·2^24 + the position's row + 1 (exact in binary64; rows < 2^24).
595
+ const siteV = new Float64Array(n), siteS = new Float64Array(n), gH = new Float64Array(n), gV = new Float64Array(n), gS = new Float64Array(n);
596
+ const rgb = new Float64Array(6 * n), labs = new Float64Array(6 * n), cS = new Float64Array(n), hom = new Uint8Array(2 * n), hS = new Float64Array(n);
597
+ let x0 = 0, epoch = 0;
598
+ const rx = (x) => (x < 0 ? -x : x >= nativeWidth ? 2 * nativeWidth - 2 - x : x);
599
+ const ry = (y) => (y < 0 ? -y : y >= nativeHeight ? 2 * nativeHeight - 2 - y : y);
600
+ const role = (x, y) => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2));
601
+ /** The site at (x, y) read reflect-101 (`at` of the task). */
602
+ function at(x, y) {
603
+ const X = rx(x), Y = ry(y), q = (Y & mask) * W + X - x0;
604
+ if (siteS[q] !== epoch + Y + 1) {
605
+ siteV[q] = site(X, Y);
606
+ siteS[q] = epoch + Y + 1;
607
+ }
608
+ return siteV[q];
609
+ }
610
+ /** Fills the two greens of in-raster position (x, y) (step 1) and returns its slot. */
611
+ function greens(x, y) {
612
+ const q = (y & mask) * W + x - x0;
613
+ if (gS[q] === epoch + y + 1)
614
+ return q;
615
+ const c = at(x, y);
616
+ if (role(x, y) === 1) {
617
+ gH[q] = c;
618
+ gV[q] = c;
619
+ }
620
+ else {
621
+ gH[q] = (at(x - 1, y) + at(x + 1, y)) / 2 + (2 * c - at(x - 2, y) - at(x + 2, y)) / 4;
622
+ gV[q] = (at(x, y - 1) + at(x, y + 1)) / 2 + (2 * c - at(x, y - 2) - at(x, y + 2)) / 4;
623
+ }
624
+ gS[q] = epoch + y + 1;
625
+ return q;
626
+ }
627
+ /** s − g of direction d at the reflected position of (x, y). */
628
+ function difference(x, y, d) {
629
+ const X = rx(x), Y = ry(y), q = greens(X, Y);
630
+ return at(X, Y) - (d === 0 ? gH[q] : gV[q]);
631
+ }
632
+ /** Fills the two candidates and their CIELAB at in-raster position (x, y) (steps 2 and 3) and returns its slot. */
633
+ function candidates(x, y) {
634
+ const q = (y & mask) * W + x - x0;
635
+ if (cS[q] === epoch + y + 1)
636
+ return q;
637
+ greens(x, y);
638
+ const own = role(x, y);
639
+ for (let d = 0; d < 2; d++) {
640
+ const gi = d === 0 ? gH[q] : gV[q], o = 6 * q + 3 * d;
641
+ rgb[o + 1] = gi;
642
+ if (own === 1) {
643
+ const hRole = role(x + 1, y), vRole = 2 - hRole;
644
+ const a = difference(x - 1, y, d), b = difference(x + 1, y, d);
645
+ rgb[o + hRole] = gi + (a + b) / 2;
646
+ const cc = difference(x, y - 1, d), dd = difference(x, y + 1, d);
647
+ rgb[o + vRole] = gi + (cc + dd) / 2;
648
+ }
649
+ else {
650
+ rgb[o + own] = at(x, y);
651
+ rgb[o + 2 - own] = gi + (difference(x - 1, y - 1, d) + difference(x + 1, y - 1, d) + difference(x - 1, y + 1, d) + difference(x + 1, y + 1, d)) / 4;
652
+ }
653
+ const R = rgb[o], G = rgb[o + 1], Bc = rgb[o + 2];
654
+ 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;
655
+ tx = tx > LAB_EPSILON ? portableCbrt(tx) : (LAB_KAPPA * tx + 16) / 116;
656
+ ty = ty > LAB_EPSILON ? portableCbrt(ty) : (LAB_KAPPA * ty + 16) / 116;
657
+ tz = tz > LAB_EPSILON ? portableCbrt(tz) : (LAB_KAPPA * tz + 16) / 116;
658
+ labs[o] = 116 * ty - 16;
659
+ labs[o + 1] = 500 * (tx - ty);
660
+ labs[o + 2] = 200 * (ty - tz);
661
+ }
662
+ cS[q] = epoch + y + 1;
663
+ return q;
664
+ }
665
+ /** The two homogeneity counts of in-raster position (x, y) (step 4), at hom[2·slot] and hom[2·slot + 1]. */
666
+ function homogeneity(x, y) {
667
+ const q = (y & mask) * W + x - x0;
668
+ if (hS[q] === epoch + y + 1)
669
+ return q;
670
+ const i = 6 * candidates(x, y), jl = 6 * candidates(rx(x - 1), y), jr = 6 * candidates(rx(x + 1), y), ju = 6 * candidates(x, ry(y - 1)), jd = 6 * candidates(x, ry(y + 1));
671
+ const h = labs;
672
+ // Direction 0 at offsets +0, direction 1 at +3; neighbours left, right, up, down.
673
+ const hL0 = Math.abs(h[i] - h[jl]), hL1 = Math.abs(h[i] - h[jr]), hL2 = Math.abs(h[i] - h[ju]), hL3 = Math.abs(h[i] - h[jd]);
674
+ const vL0 = Math.abs(h[i + 3] - h[jl + 3]), vL1 = Math.abs(h[i + 3] - h[jr + 3]), vL2 = Math.abs(h[i + 3] - h[ju + 3]), vL3 = Math.abs(h[i + 3] - h[jd + 3]);
675
+ const hC0 = square(h[i + 1] - h[jl + 1]) + square(h[i + 2] - h[jl + 2]), hC1 = square(h[i + 1] - h[jr + 1]) + square(h[i + 2] - h[jr + 2]);
676
+ const hC2 = square(h[i + 1] - h[ju + 1]) + square(h[i + 2] - h[ju + 2]), hC3 = square(h[i + 1] - h[jd + 1]) + square(h[i + 2] - h[jd + 2]);
677
+ const vC0 = square(h[i + 4] - h[jl + 4]) + square(h[i + 5] - h[jl + 5]), vC1 = square(h[i + 4] - h[jr + 4]) + square(h[i + 5] - h[jr + 5]);
678
+ const vC2 = square(h[i + 4] - h[ju + 4]) + square(h[i + 5] - h[ju + 5]), vC3 = square(h[i + 4] - h[jd + 4]) + square(h[i + 5] - h[jd + 5]);
679
+ const epsL = Math.min(Math.max(hL0, hL1), Math.max(vL2, vL3)), epsC = Math.min(Math.max(hC0, hC1), Math.max(vC2, vC3));
680
+ hom[2 * q] = (hL0 <= epsL && hC0 <= epsC ? 1 : 0) + (hL1 <= epsL && hC1 <= epsC ? 1 : 0) + (hL2 <= epsL && hC2 <= epsC ? 1 : 0) + (hL3 <= epsL && hC3 <= epsC ? 1 : 0);
681
+ hom[2 * q + 1] = (vL0 <= epsL && vC0 <= epsC ? 1 : 0) + (vL1 <= epsL && vC1 <= epsC ? 1 : 0) + (vL2 <= epsL && vC2 <= epsC ? 1 : 0) + (vL3 <= epsL && vC3 <= epsC ? 1 : 0);
682
+ hS[q] = epoch + y + 1;
683
+ return q;
684
+ }
685
+ return {
686
+ origin(first) { x0 = first; epoch += 16777216; },
687
+ /** Step 5: the 3 × 3 sums of homogeneity (reflected) select the direction. */
688
+ direction(x, y) {
689
+ let sh = 0, sv = 0;
690
+ for (let dy = -1; dy <= 1; dy++)
691
+ for (let dx = -1; dx <= 1; dx++) {
692
+ const q = homogeneity(rx(x + dx), ry(y + dy));
693
+ sh += hom[2 * q];
694
+ sv += hom[2 * q + 1];
695
+ }
696
+ return sh > sv ? 0 : sv > sh ? 1 : 2;
697
+ }
698
+ };
699
+ }
700
+ /**
701
+ * The state computer of one setup's AHD correction (#447: shared by the render and `computeDisplayViewCorrectionStates`):
702
+ * gridCorrection(i0, i1, i2) returns the offset of the grid state's three values in `states.values`, computing and storing
703
+ * the state on first use. The code and its operations are those the render ran in place before.
704
+ */
705
+ function createGridCorrection(setup, states) {
706
+ const { seed, ahdCorrection } = setup;
707
+ const k = Float64Array.from(setup.readout, (r) => r.perElectron), c0 = Float64Array.from(setup.readout, (r) => r.constantVariance);
708
+ 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];
709
+ const g0 = setup.gains[0], g1 = setup.gains[1], g2 = setup.gains[2], L = setup.clipLevel, reinhard = setup.toneCurve === "positive-reinhard-per-channel";
710
+ const correction = new Float64Array(3), patch = new Float64Array(AHD_SIDE * AHD_SIDE);
711
+ const readStd = Float64Array.from(c0, (v) => Math.sqrt(v));
712
+ /** The offset of a grid state's three values in states.values (computed and stored on first use). */
713
+ function gridCorrection(i0, i1, i2) {
714
+ const key = (i0 * AHD_KEY_LEVELS + i1) * AHD_KEY_LEVELS + i2, cached = states.index.get(key);
715
+ if (cached !== undefined)
716
+ return 3 * cached;
717
+ const electrons = [i0, i1, i2].map((i, c) => portableExp2(i / AHD_KEY_STEPS - AHD_KEY_OCTAVES) / k[c]);
718
+ const random = createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, fmixKey(i0, i1, i2), DISPLAY_VIEW_AHD_STREAM_ID));
719
+ const sum = new Float64Array(3), draws = ahdCorrection.drawsPerState, poisson = electrons.map((e) => createPoissonSampler(e, true));
720
+ const normal = createPortableStandardNormalPairs(random);
721
+ for (let draw = 0; draw < draws; draw++) {
722
+ const phase = draw & 3, roles = AHD_ROLES[phase];
723
+ for (let p = 0; p < AHD_SIDE * AHD_SIDE; p++) {
724
+ // The 16 sites the AHD steps never read are not drawn.
725
+ if (AHD_READ[p] === 0)
726
+ continue;
727
+ const c = roles[p], value = k[c] * poisson[c](random) + readStd[c] * normal();
728
+ patch[p] = value < 1 ? value : 1;
729
+ }
730
+ ahdDifference(phase, correction);
731
+ for (let comp = 0; comp < 3; comp++)
732
+ sum[comp] = sum[comp] + correction[comp] / draws;
733
+ }
734
+ if (states.index.size >= AHD_STATE_LIMIT)
735
+ states.index.clear();
736
+ const n = states.index.size;
737
+ if (states.values.length < 3 * (n + 1)) {
738
+ const grown = new Float64Array(2 * states.values.length);
739
+ grown.set(states.values);
740
+ states.values = grown;
741
+ }
742
+ states.values.set(sum, 3 * n);
743
+ states.index.set(key, n);
744
+ return 3 * n;
745
+ }
746
+ const steps = createAhdCentreSteps(ahdCorrection.labMatrix, patch);
747
+ const rgb = new Float64Array(3), rendered = new Float64Array(3), linear = new Float64Array(3);
748
+ /** Develops one deterministic camera pixel and renders it: WB, highlight clip, colour matrix, tone curve, clip to [0, 1]. */
749
+ function renderPixel(r, gr, b, out) {
750
+ const u0 = Math.min(L, g0 * r), u1 = Math.min(L, g1 * gr), u2 = Math.min(L, g2 * b);
751
+ for (let comp = 0; comp < 3; comp++) {
752
+ let v = comp === 0 ? m00 * u0 + m01 * u1 + m02 * u2 : comp === 1 ? m10 * u0 + m11 * u1 + m12 * u2 : m20 * u0 + m21 * u1 + m22 * u2;
753
+ if (reinhard)
754
+ v = v > 0 ? v / (1 + v) : v;
755
+ out[comp] = v < 0 ? 0 : v > 1 ? 1 : v;
756
+ }
757
+ }
758
+ /** h(AHD) − ½(h(H) + h(V)) at the centre of the drawn neighbourhood (`createAhdCentreSteps`). */
759
+ function ahdDifference(phase, out) {
760
+ const dir = steps.choose(phase), a = steps.horizontal, b = steps.vertical, centre = 12;
629
761
  for (let c = 0; c < 3; c++)
630
- rgb[c] = sh > sv ? a[3 * centre + c] : sv > sh ? b[3 * centre + c] : (a[3 * centre + c] + b[3 * centre + c]) / 2;
762
+ rgb[c] = dir === 0 ? a[3 * centre + c] : dir === 1 ? b[3 * centre + c] : (a[3 * centre + c] + b[3 * centre + c]) / 2;
631
763
  renderPixel(rgb[0], rgb[1], rgb[2], rendered);
632
764
  renderPixel(a[3 * centre], a[3 * centre + 1], a[3 * centre + 2], linear);
633
765
  for (let c = 0; c < 3; c++)
@@ -720,6 +852,19 @@ export async function renderDisplayView(setup) {
720
852
  // AHD low-signal correction: an 11 × 11 noisy neighbourhood (AHD reads up to 5 sites away) and the local AHD steps.
721
853
  const ahdCorrection = setup.ahdCorrection, windowReach = ahdCorrection ? Math.max(reach, AHD_REACH) : reach;
722
854
  const cmAverage = new Float64Array(3);
855
+ // #457: AHD's choice on the noise-free mosaic. Per candidate the rendered mean and the camera-channel means of the sample.
856
+ const candidateRendered = new Float64Array(6), candidateChannels = new Float64Array(6);
857
+ /** The normalized expected value of an in-raster site, clipped at white (the noise-free mosaic AHD's choice reads). */
858
+ const noiseFreeSite = (x, y) => { const c = (y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2), v = k[c] * (setup.signal.site(x, y) + dark[c]); return v < 1 ? v : 1; };
859
+ // The exact path keeps the region's columns for the whole render; the stratified estimator keeps one display pixel's
860
+ // footprint at a time (its samples are scattered over it), so its plane is set to each display pixel's columns.
861
+ let choiceRows = 1;
862
+ while (choiceRows < Math.ceil(sy) + 2 + 2 * AHD_REACH)
863
+ choiceRows *= 2;
864
+ 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;
865
+ const choicePlane = ahdCorrection ? createAhdDirectionPlane(ahdCorrection.labMatrix, nativeWidth, nativeHeight, choiceColumns, exactView ? 2 * choiceRows : choiceRows, noiseFreeSite) : undefined;
866
+ if (choicePlane && exactView)
867
+ choicePlane.origin(choiceX0);
723
868
  let correctedPixels = 0;
724
869
  // The correction of a flat neighbourhood whose sites of channel c have normalized mean μ_c, on a logarithmic grid of states
725
870
  // (AHD_KEY_STEPS per octave from 2^-AHD_KEY_OCTAVES of white); each grid state is computed once with draws seeded by the state
@@ -866,6 +1011,8 @@ export async function renderDisplayView(setup) {
866
1011
  markM = new Int32Array(slotsCount);
867
1012
  }
868
1013
  const mx0 = Math.floor(vx * sx) + margin, my0 = Math.floor(vy * sy) + margin;
1014
+ if (choicePlane && !exactView)
1015
+ choicePlane.origin(Math.max(0, mx0 - AHD_REACH));
869
1016
  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;
870
1017
  const pixelMemo = !exactView && setup.signal.cachesSites && mw * mh <= PIXEL_MEMO_LIMIT;
871
1018
  if (pixelMemo) {
@@ -1220,6 +1367,12 @@ export async function renderDisplayView(setup) {
1220
1367
  e0 += share * hm[0];
1221
1368
  e1 += share * hm[1];
1222
1369
  e2 += share * hm[2];
1370
+ candidateRendered[3 * d] = hm[0];
1371
+ candidateRendered[3 * d + 1] = hm[1];
1372
+ candidateRendered[3 * d + 2] = hm[2];
1373
+ candidateChannels[3 * d] = cm[0];
1374
+ candidateChannels[3 * d + 1] = cm[1];
1375
+ candidateChannels[3 * d + 2] = cm[2];
1223
1376
  // Linear (Stein) part: a_c = J ∘ M[:, c]·Γ_c, accumulated with B_c·σ_c² (site variance of channel c); the pixel's own
1224
1377
  // linear variance (aᵀ·C·a, full channel covariance) for the residual.
1225
1378
  let l0 = 0, l1 = 0, l2 = 0;
@@ -1261,6 +1414,22 @@ export async function renderDisplayView(setup) {
1261
1414
  // Two-argument maxima (inlined; the same value as one four-argument Math.max, which V8 calls with boxed arguments).
1262
1415
  pixelVariance = Math.max(Math.max(pixelVariance, hv[0]), Math.max(hv[1], hv[2]));
1263
1416
  }
1417
+ // #457: AHD chooses one candidate per pixel by homogeneity. Where the scene decides that choice (structure finer than a
1418
+ // few sites, such as a Siemens star's centre), the expected value is the chosen candidate's, not the candidates' average:
1419
+ // the steps run on the noise-free mosaic (the expected normalized site values, clipped at white, mirrored at the raster
1420
+ // edges as the capture's AHD reads them), and a decided direction takes that candidate's rendered mean. A tie (every flat
1421
+ // neighbourhood, where the noise decides) keeps the average, and the low-signal correction below stays the noise's
1422
+ // bias of a flat neighbourhood. Skipped where the two candidates' channel means are equal (the choice cannot change the
1423
+ // value) and for one-material samples (equal up to rounding; their memo is keyed by material and phase, not by the
1424
+ // patch). The noise terms are unchanged.
1425
+ if (choicePlane && count === 2 && memoKey < 0 && (candidateChannels[0] !== candidateChannels[3] || candidateChannels[1] !== candidateChannels[4] || candidateChannels[2] !== candidateChannels[5])) {
1426
+ const direction = choicePlane.direction(x, y);
1427
+ if (direction !== 2) {
1428
+ e0 = candidateRendered[3 * direction];
1429
+ e1 = candidateRendered[3 * direction + 1];
1430
+ e2 = candidateRendered[3 * direction + 2];
1431
+ }
1432
+ }
1264
1433
  s0 += wE * e0;
1265
1434
  s1 += wE * e1;
1266
1435
  s2 += wE * e2;