@photivra/engine 2.9.0 → 2.11.1

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 (147) hide show
  1. package/CHANGELOG.md +223 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +21 -3
  4. package/README.md +6 -6
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY.md +2 -0
  7. package/dist/capture/native-environment-lean.js +89 -5
  8. package/dist/capture/native-environment-lean.js.map +1 -1
  9. package/dist/capture/native-environment-tabulated.d.ts +37 -0
  10. package/dist/capture/native-environment-tabulated.js +42 -6
  11. package/dist/capture/native-environment-tabulated.js.map +1 -1
  12. package/dist/core/portable-math.d.ts +19 -0
  13. package/dist/core/portable-math.js +82 -0
  14. package/dist/core/portable-math.js.map +1 -1
  15. package/dist/core/version.d.ts +1 -1
  16. package/dist/core/version.js +1 -1
  17. package/dist/core/version.js.map +1 -1
  18. package/dist/facade/channel-response.d.ts +15 -0
  19. package/dist/facade/channel-response.js +33 -0
  20. package/dist/facade/channel-response.js.map +1 -0
  21. package/dist/facade/chart-material-layers.d.ts +115 -0
  22. package/dist/facade/chart-material-layers.js +224 -0
  23. package/dist/facade/chart-material-layers.js.map +1 -0
  24. package/dist/facade/chart-radiance-batch.d.ts +8 -2
  25. package/dist/facade/chart-radiance-batch.js +11 -4
  26. package/dist/facade/chart-radiance-batch.js.map +1 -1
  27. package/dist/facade/chart-scenes.d.ts +134 -0
  28. package/dist/facade/chart-scenes.js +32 -22
  29. package/dist/facade/chart-scenes.js.map +1 -1
  30. package/dist/facade/diffraction-provider.d.ts +8 -2
  31. package/dist/facade/diffraction-provider.js +120 -16
  32. package/dist/facade/diffraction-provider.js.map +1 -1
  33. package/dist/facade/display-view-gpu-session.d.ts +18 -0
  34. package/dist/facade/display-view-gpu-session.js +280 -0
  35. package/dist/facade/display-view-gpu-session.js.map +1 -0
  36. package/dist/facade/display-view-gpu.d.ts +146 -0
  37. package/dist/facade/display-view-gpu.js +59 -0
  38. package/dist/facade/display-view-gpu.js.map +1 -0
  39. package/dist/facade/display-view.d.ts +123 -1
  40. package/dist/facade/display-view.js +478 -116
  41. package/dist/facade/display-view.js.map +1 -1
  42. package/dist/facade/expected-signal-setup.d.ts +5 -0
  43. package/dist/facade/expected-signal-setup.js +18 -1
  44. package/dist/facade/expected-signal-setup.js.map +1 -1
  45. package/dist/facade/expected-signal.d.ts +80 -6
  46. package/dist/facade/expected-signal.js +200 -24
  47. package/dist/facade/expected-signal.js.map +1 -1
  48. package/dist/facade/scene-radiance.d.ts +4 -2
  49. package/dist/facade/scene-radiance.js +7 -4
  50. package/dist/facade/scene-radiance.js.map +1 -1
  51. package/dist/facade/simulate-capture.d.ts +81 -7
  52. package/dist/facade/simulate-capture.js +276 -53
  53. package/dist/facade/simulate-capture.js.map +1 -1
  54. package/dist/facade/simulate-display-view.d.ts +54 -3
  55. package/dist/facade/simulate-display-view.js +120 -27
  56. package/dist/facade/simulate-display-view.js.map +1 -1
  57. package/dist/gpu/chart-composite-gpu.d.ts +119 -0
  58. package/dist/gpu/chart-composite-gpu.js +572 -0
  59. package/dist/gpu/chart-composite-gpu.js.map +1 -0
  60. package/dist/gpu/chart-radiance-gpu.d.ts +6 -6
  61. package/dist/gpu/chart-radiance-gpu.js +9 -11
  62. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  63. package/dist/gpu/display-view-chart-material-module.d.ts +10 -0
  64. package/dist/gpu/display-view-chart-material-module.js +58 -0
  65. package/dist/gpu/display-view-chart-material-module.js.map +1 -0
  66. package/dist/gpu/display-view-composite-material-module.d.ts +68 -0
  67. package/dist/gpu/display-view-composite-material-module.js +248 -0
  68. package/dist/gpu/display-view-composite-material-module.js.map +1 -0
  69. package/dist/gpu/display-view-quality-gpu.d.ts +185 -0
  70. package/dist/gpu/display-view-quality-gpu.js +1000 -0
  71. package/dist/gpu/display-view-quality-gpu.js.map +1 -0
  72. package/dist/gpu/display-view-speed-gpu.d.ts +156 -0
  73. package/dist/gpu/display-view-speed-gpu.js +524 -0
  74. package/dist/gpu/display-view-speed-gpu.js.map +1 -0
  75. package/dist/gpu/image-kernel-accelerator.d.ts +23 -4
  76. package/dist/gpu/image-kernel-accelerator.js +58 -10
  77. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  78. package/dist/gpu/image-kernel-execution.d.ts +60 -3
  79. package/dist/gpu/image-kernel-execution.js +2 -2
  80. package/dist/gpu/image-kernel-execution.js.map +1 -1
  81. package/dist/gpu/sensor-stage-gpu.d.ts +11 -0
  82. package/dist/gpu/sensor-stage-gpu.js +42 -13
  83. package/dist/gpu/sensor-stage-gpu.js.map +1 -1
  84. package/dist/gpu/webgpu-runtime.d.ts +4 -2
  85. package/dist/gpu/webgpu-runtime.js +13 -4
  86. package/dist/gpu/webgpu-runtime.js.map +1 -1
  87. package/dist/gpu/webgpu-switch.d.ts +4 -0
  88. package/dist/gpu/webgpu-switch.js +29 -0
  89. package/dist/gpu/webgpu-switch.js.map +1 -0
  90. package/dist/gpu/wgsl-chart-composite-kernel.d.ts +52 -0
  91. package/dist/gpu/wgsl-chart-composite-kernel.js +515 -0
  92. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -0
  93. package/dist/gpu/wgsl-chart-kernel.d.ts +11 -9
  94. package/dist/gpu/wgsl-chart-kernel.js +33 -27
  95. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  96. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +95 -0
  97. package/dist/gpu/wgsl-display-view-quality-kernel.js +769 -0
  98. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -0
  99. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +75 -0
  100. package/dist/gpu/wgsl-display-view-speed-kernel.js +386 -0
  101. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -0
  102. package/dist/gpu/wgsl-sensor-kernel.d.ts +28 -9
  103. package/dist/gpu/wgsl-sensor-kernel.js +94 -50
  104. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  105. package/dist/index.d.ts +6 -3
  106. package/dist/index.js +4 -2
  107. package/dist/index.js.map +1 -1
  108. package/dist/optics/defocus-disc-kernel.d.ts +61 -0
  109. package/dist/optics/defocus-disc-kernel.js +191 -0
  110. package/dist/optics/defocus-disc-kernel.js.map +1 -0
  111. package/dist/optics/irradiance-convolution.d.ts +11 -0
  112. package/dist/optics/irradiance-convolution.js +64 -24
  113. package/dist/optics/irradiance-convolution.js.map +1 -1
  114. package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
  115. package/dist/optics/scene-to-sensor-irradiance.js +58 -24
  116. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  117. package/dist/optics/sensor-aperture-rays.js +2 -1
  118. package/dist/optics/sensor-aperture-rays.js.map +1 -1
  119. package/dist/optics/sensor-environment-query.js +4 -3
  120. package/dist/optics/sensor-environment-query.js.map +1 -1
  121. package/dist/optics/site-diffraction-kernel.d.ts +16 -0
  122. package/dist/optics/site-diffraction-kernel.js +60 -8
  123. package/dist/optics/site-diffraction-kernel.js.map +1 -1
  124. package/dist/processing/demosaic.js +114 -37
  125. package/dist/processing/demosaic.js.map +1 -1
  126. package/dist/schema/scene-radiance.d.ts +8 -0
  127. package/dist/schema/scene-radiance.js +28 -17
  128. package/dist/schema/scene-radiance.js.map +1 -1
  129. package/dist/sensor/environment-photo-signal.d.ts +21 -0
  130. package/dist/sensor/environment-photo-signal.js +68 -2
  131. package/dist/sensor/environment-photo-signal.js.map +1 -1
  132. package/docs/API_REFERENCE.md +60 -1
  133. package/docs/QUICK_START.md +120 -11
  134. package/docs/USAGE.md +82 -2
  135. package/docs/api/core-version.md +1 -1
  136. package/docs/api/exports.json +157 -2
  137. package/docs/api/facade-chart-material-layers.md +137 -0
  138. package/docs/api/facade-chart-radiance-batch.md +7 -2
  139. package/docs/api/facade-display-view-gpu.md +94 -0
  140. package/docs/api/facade-display-view.md +50 -0
  141. package/docs/api/facade-scene-radiance.md +3 -2
  142. package/docs/api/facade-simulate-capture.md +81 -12
  143. package/docs/api/facade-simulate-display-view.md +39 -2
  144. package/docs/api/gpu-image-kernel-accelerator.md +10 -3
  145. package/docs/api/gpu-image-kernel-execution.md +76 -4
  146. package/docs/api/gpu-webgpu-switch.md +23 -0
  147. package/package.json +3 -1
@@ -31,6 +31,11 @@ export interface ExpectedSignalSetupInput {
31
31
  psf: Psf;
32
32
  profileWavelengths: readonly number[];
33
33
  } | undefined;
34
+ /** Defocus (#492): the geometric disc on the site lattice, (2R + 1)² row-major weights; absent or null in focus. */
35
+ defocus?: {
36
+ radius: number;
37
+ weights: Float64Array;
38
+ } | null | undefined;
34
39
  /** Material scenes: the materials (neutral reflectances, or radiance per wavelength), the material lookup and the white radiance. */
35
40
  materials?: {
36
41
  list: readonly ({
@@ -3,10 +3,20 @@
3
3
  * Internal (#394, not exported from the package): the input of the tabulated expected signal (`createDisplayViewExpectedSignal`)
4
4
  * for a resolved `simulateCapture` request, shared by the display view and the capture's tabulated RAW path so both build it
5
5
  * the same way: query offsets of each site, the environment's query points, the diffraction kernels and the scene tables.
6
+ *
7
+ * Defocus (#492, `lens.defocus`): the geometric disc D on the site lattice (`createDefocusDiscSiteKernel`) enters as a site
8
+ * kernel, so every query stays on the lattice. Without diffraction every spectral node takes D itself (a wavelength-independent
9
+ * kernel). With diffraction node λ takes K_λ ∗ D, the full 2-D convolution of the diffraction kernel with the disc (radius
10
+ * R_diff + R_defocus): a labelled approximation, since the defocused wave's PSF is not the in-focus Airy pattern convolved
11
+ * with a geometric disc. The convolution is linear, so Σ_t (K_λ ∗ D)(t)·L(p − t) is the diffracted radiance blurred by D, the
12
+ * order the capture's object-space convolutions apply them in. The kernels are built as the capture builds them
13
+ * (`siteDiffractionKernel`, then `convolveSiteKernels`), so the two have the same bits.
6
14
  */
7
15
  import { InvalidConfigurationError } from "../core/configuration-error.js";
8
16
  import { siteDiffractionKernel } from "../optics/site-diffraction-kernel.js";
17
+ import { convolveSiteKernels } from "../optics/defocus-disc-kernel.js";
9
18
  import { resolveProjectionDistanceMm } from "../motion/camera-rotation.js";
19
+ import { hypot3 } from "../core/portable-math.js";
10
20
  /** The query offsets of each site and the expected-signal input (see the module comment). */
11
21
  export function buildExpectedSignalInput(input) {
12
22
  const { boxSamples, pitch, wavelengths, imagingArea, subjectDistanceM, metresPerFrameUnit } = input;
@@ -25,6 +35,13 @@ export function buildExpectedSignalInput(input) {
25
35
  return siteDiffractionKernel(diffraction.psf, wavelengthNm, pitch, radius, input.caller + ": diffraction kernels must sample the site pitch exactly.");
26
36
  }) };
27
37
  }
38
+ // Defocus (#492): the disc alone at every node (one shared array), or each node's diffraction kernel convolved with it.
39
+ const defocus = input.defocus;
40
+ if (defocus) {
41
+ const diffracted = kernels;
42
+ kernels = diffracted ? { radius: diffracted.radius + defocus.radius, perNode: diffracted.perNode.map((k) => convolveSiteKernels(k, diffracted.radius, defocus.weights, defocus.radius)) } :
43
+ { radius: defocus.radius, perNode: wavelengths.map(() => defocus.weights) };
44
+ }
28
45
  // Chart presets and (2.7) caller material scenes take the material path: each material tabulated once per node.
29
46
  const materials = input.materials;
30
47
  const scene = materials ? { kind: "materials", materialAt: materials.materialAt,
@@ -42,7 +59,7 @@ export function buildExpectedSignalInput(input) {
42
59
  const centerX = -imagingArea.widthMm / 2 + (pitch / 2 + x * pitch + 0) / 1000, centerY = -imagingArea.heightMm / 2 + (pitch / 2 + y * pitch + 0) / 1000;
43
60
  const sourceX = (centerX - siteWidthMm / 2 + node.xPhase * siteWidthMm) - node.aaX, sourceY = (centerY - siteWidthMm / 2 + node.yPhase * siteWidthMm) - node.aaY;
44
61
  const rx = projectionDistance * (sourceX / projectionDistance), ry = projectionDistance * (-sourceY / projectionDistance);
45
- const length = Math.hypot(rx, ry, projectionDistance), t = subjectDistanceM / (projectionDistance / length);
62
+ const length = hypot3(rx, ry, projectionDistance), t = subjectDistanceM / (projectionDistance / length);
46
63
  out[0] = rx / length * t / metresPerFrameUnit;
47
64
  out[1] = ry / length * t / metresPerFrameUnit;
48
65
  };
@@ -1 +1 @@
1
- {"version":3,"file":"expected-signal-setup.js","sourceRoot":"","sources":["../../src/facade/expected-signal-setup.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (#394, not exported from the package): the input of the tabulated expected signal (`createDisplayViewExpectedSignal`)\n * for a resolved `simulateCapture` request, shared by the display view and the capture's tabulated RAW path so both build it\n * the same way: query offsets of each site, the environment's query points, the diffraction kernels and the scene tables.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport type { resolveLensPsfConvolutionKernels } from \"../optics/lens-psf-kernels.js\";\nimport { siteDiffractionKernel } from \"../optics/site-diffraction-kernel.js\";\nimport { resolveProjectionDistanceMm } from \"../motion/camera-rotation.js\";\nimport type { DisplayViewExpectedSignalInput, DisplayViewScene } from \"./display-view.js\";\n\ntype Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0][\"psf\"];\n\nexport interface ExpectedSignalSetupInput {\n nativeWidth: number;\n nativeHeight: number;\n /** Frame output width (plane coordinates are 2 / width frame units per site). */\n width: number;\n wavelengths: readonly number[];\n /** Per CFA channel (red, green, blue): expected electrons per unit spectral radiance at each node. */\n perNode: readonly [Float64Array, Float64Array, Float64Array];\n /** Pixel-aperture nodes in sites from the site centre (`apertureNodeOffsets(boxSamples)`) and the box sample count. */\n apertureNodes: readonly { x: number; y: number }[];\n boxSamples: number;\n antiAliasing?: { components: readonly { offsetMicrometers: { x: number; y: number }; normalizedWeight: number }[] } | undefined;\n pitch: number;\n /** Object-space diffraction: the kernel radius, the PSF and its profile wavelengths. */\n diffraction?: { radius: number; psf: Psf; profileWavelengths: readonly number[] } | undefined;\n /** Material scenes: the materials (neutral reflectances, or radiance per wavelength), the material lookup and the white radiance. */\n materials?: { list: readonly ({ neutral: number } | { radiance: (nm: number) => number })[]; materialAt(u: number, v: number): number; whiteRadiance(nm: number): number } | undefined;\n radianceAt(u: number, v: number, nm: number): number;\n imagingArea: { widthMm: number; heightMm: number };\n focalLengthMm: number;\n focusDistanceM: number;\n subjectDistanceM: number;\n metresPerFrameUnit: number;\n /** Prefix of the error messages (\"simulateDisplayView\", \"simulateCapture\"). */\n caller: string;\n}\n\n/** The query offsets of each site and the expected-signal input (see the module comment). */\nexport function buildExpectedSignalInput(input: ExpectedSignalSetupInput): { offsets: { x: number; y: number; weight: number }[]; signalInput: DisplayViewExpectedSignalInput } {\n const { boxSamples, pitch, wavelengths, imagingArea, subjectDistanceM, metresPerFrameUnit } = input;\n // Query offsets of each site: pixel-aperture nodes minus anti-aliasing split offsets (sites, +x right, +y down), as the environment forms them.\n const spots = input.antiAliasing ? input.antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 }, normalizedWeight: 1 }];\n const offsets = input.apertureNodes.flatMap((node) => spots.map((c) => ({ x: node.x - c.offsetMicrometers.x / pitch, y: node.y - c.offsetMicrometers.y / pitch,\n weight: c.normalizedWeight / (boxSamples * boxSamples) })));\n // Diffraction: the same per-node site kernels as the object-space convolution (createDiffractedPlanarProvider).\n let kernels: { radius: number; perNode: Float64Array[] } | undefined;\n const diffraction = input.diffraction;\n if (diffraction) {\n const radius = diffraction.radius;\n kernels = { radius, perNode: wavelengths.map((wavelengthNm) => {\n if (!diffraction.profileWavelengths.includes(wavelengthNm)) throw new InvalidConfigurationError(input.caller + \": a spectral node has no diffraction kernel.\");\n return siteDiffractionKernel(diffraction.psf, wavelengthNm, pitch, radius, input.caller + \": diffraction kernels must sample the site pitch exactly.\");\n }) };\n }\n // Chart presets and (2.7) caller material scenes take the material path: each material tabulated once per node.\n const materials = input.materials;\n const scene: DisplayViewScene = materials ? { kind: \"materials\", materialAt: materials.materialAt,\n materials: materials.list.map((m) => (\"neutral\" in m ? { neutral: m.neutral } : { radiance: Float64Array.from(wavelengths, (nm) => m.radiance(nm)) })),\n whiteRadiance: Float64Array.from(wavelengths, (nm) => materials.whiteRadiance(nm)) } :\n { kind: \"radiance\", radianceAt: input.radianceAt };\n const channelIndex = (x: number, y: number): number => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2));\n // Without diffraction the environment's own query arithmetic (native-environment-lean.ts `geometry`, then the rectilinear\n // still-camera ray and the planar provider's plane point), so boundary points resolve as in the capture.\n const projectionDistance = resolveProjectionDistanceMm(input.focalLengthMm, input.focusDistanceM).projectionDistanceMm, siteWidthMm = pitch / 1000;\n const nodePhases = input.apertureNodes.flatMap((_, index) => spots.map((c) => ({ xPhase: (index % boxSamples + 0.5) / boxSamples, yPhase: (Math.floor(index / boxSamples) + 0.5) / boxSamples,\n aaX: c.offsetMicrometers.x / 1000, aaY: c.offsetMicrometers.y / 1000 })));\n const queryPoint = (x: number, y: number, o: number, out: Float64Array): void => {\n const node = nodePhases[o]!;\n const centerX = -imagingArea.widthMm / 2 + (pitch / 2 + x * pitch + 0) / 1000, centerY = -imagingArea.heightMm / 2 + (pitch / 2 + y * pitch + 0) / 1000;\n const sourceX = (centerX - siteWidthMm / 2 + node.xPhase * siteWidthMm) - node.aaX, sourceY = (centerY - siteWidthMm / 2 + node.yPhase * siteWidthMm) - node.aaY;\n const rx = projectionDistance * (sourceX / projectionDistance), ry = projectionDistance * (-sourceY / projectionDistance);\n const length = Math.hypot(rx, ry, projectionDistance), t = subjectDistanceM / (projectionDistance / length);\n out[0] = rx / length * t / metresPerFrameUnit; out[1] = ry / length * t / metresPerFrameUnit;\n };\n return { offsets, signalInput: { nativeWidth: input.nativeWidth, nativeHeight: input.nativeHeight, outputWidth: input.width, wavelengthsNm: wavelengths,\n electronsPerRadiance: input.perNode, offsets, ...(kernels ? { kernels } : { queryPoint }), scene, channelAt: channelIndex } };\n}\n"]}
1
+ {"version":3,"file":"expected-signal-setup.js","sourceRoot":"","sources":["../../src/facade/expected-signal-setup.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (#394, not exported from the package): the input of the tabulated expected signal (`createDisplayViewExpectedSignal`)\n * for a resolved `simulateCapture` request, shared by the display view and the capture's tabulated RAW path so both build it\n * the same way: query offsets of each site, the environment's query points, the diffraction kernels and the scene tables.\n *\n * Defocus (#492, `lens.defocus`): the geometric disc D on the site lattice (`createDefocusDiscSiteKernel`) enters as a site\n * kernel, so every query stays on the lattice. Without diffraction every spectral node takes D itself (a wavelength-independent\n * kernel). With diffraction node λ takes K_λ ∗ D, the full 2-D convolution of the diffraction kernel with the disc (radius\n * R_diff + R_defocus): a labelled approximation, since the defocused wave's PSF is not the in-focus Airy pattern convolved\n * with a geometric disc. The convolution is linear, so Σ_t (K_λ ∗ D)(t)·L(p − t) is the diffracted radiance blurred by D, the\n * order the capture's object-space convolutions apply them in. The kernels are built as the capture builds them\n * (`siteDiffractionKernel`, then `convolveSiteKernels`), so the two have the same bits.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport type { resolveLensPsfConvolutionKernels } from \"../optics/lens-psf-kernels.js\";\nimport { siteDiffractionKernel } from \"../optics/site-diffraction-kernel.js\";\nimport { convolveSiteKernels } from \"../optics/defocus-disc-kernel.js\";\nimport { resolveProjectionDistanceMm } from \"../motion/camera-rotation.js\";\nimport { hypot3 } from \"../core/portable-math.js\";\nimport type { DisplayViewExpectedSignalInput, DisplayViewScene } from \"./display-view.js\";\n\ntype Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0][\"psf\"];\n\nexport interface ExpectedSignalSetupInput {\n nativeWidth: number;\n nativeHeight: number;\n /** Frame output width (plane coordinates are 2 / width frame units per site). */\n width: number;\n wavelengths: readonly number[];\n /** Per CFA channel (red, green, blue): expected electrons per unit spectral radiance at each node. */\n perNode: readonly [Float64Array, Float64Array, Float64Array];\n /** Pixel-aperture nodes in sites from the site centre (`apertureNodeOffsets(boxSamples)`) and the box sample count. */\n apertureNodes: readonly { x: number; y: number }[];\n boxSamples: number;\n antiAliasing?: { components: readonly { offsetMicrometers: { x: number; y: number }; normalizedWeight: number }[] } | undefined;\n pitch: number;\n /** Object-space diffraction: the kernel radius, the PSF and its profile wavelengths. */\n diffraction?: { radius: number; psf: Psf; profileWavelengths: readonly number[] } | undefined;\n /** Defocus (#492): the geometric disc on the site lattice, (2R + 1)² row-major weights; absent or null in focus. */\n defocus?: { radius: number; weights: Float64Array } | null | undefined;\n /** Material scenes: the materials (neutral reflectances, or radiance per wavelength), the material lookup and the white radiance. */\n materials?: { list: readonly ({ neutral: number } | { radiance: (nm: number) => number })[]; materialAt(u: number, v: number): number; whiteRadiance(nm: number): number } | undefined;\n radianceAt(u: number, v: number, nm: number): number;\n imagingArea: { widthMm: number; heightMm: number };\n focalLengthMm: number;\n focusDistanceM: number;\n subjectDistanceM: number;\n metresPerFrameUnit: number;\n /** Prefix of the error messages (\"simulateDisplayView\", \"simulateCapture\"). */\n caller: string;\n}\n\n/** The query offsets of each site and the expected-signal input (see the module comment). */\nexport function buildExpectedSignalInput(input: ExpectedSignalSetupInput): { offsets: { x: number; y: number; weight: number }[]; signalInput: DisplayViewExpectedSignalInput } {\n const { boxSamples, pitch, wavelengths, imagingArea, subjectDistanceM, metresPerFrameUnit } = input;\n // Query offsets of each site: pixel-aperture nodes minus anti-aliasing split offsets (sites, +x right, +y down), as the environment forms them.\n const spots = input.antiAliasing ? input.antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 }, normalizedWeight: 1 }];\n const offsets = input.apertureNodes.flatMap((node) => spots.map((c) => ({ x: node.x - c.offsetMicrometers.x / pitch, y: node.y - c.offsetMicrometers.y / pitch,\n weight: c.normalizedWeight / (boxSamples * boxSamples) })));\n // Diffraction: the same per-node site kernels as the object-space convolution (createDiffractedPlanarProvider).\n let kernels: { radius: number; perNode: Float64Array[] } | undefined;\n const diffraction = input.diffraction;\n if (diffraction) {\n const radius = diffraction.radius;\n kernels = { radius, perNode: wavelengths.map((wavelengthNm) => {\n if (!diffraction.profileWavelengths.includes(wavelengthNm)) throw new InvalidConfigurationError(input.caller + \": a spectral node has no diffraction kernel.\");\n return siteDiffractionKernel(diffraction.psf, wavelengthNm, pitch, radius, input.caller + \": diffraction kernels must sample the site pitch exactly.\");\n }) };\n }\n // Defocus (#492): the disc alone at every node (one shared array), or each node's diffraction kernel convolved with it.\n const defocus = input.defocus;\n if (defocus) {\n const diffracted = kernels;\n kernels = diffracted ? { radius: diffracted.radius + defocus.radius, perNode: diffracted.perNode.map((k) => convolveSiteKernels(k, diffracted.radius, defocus.weights, defocus.radius)) } :\n { radius: defocus.radius, perNode: wavelengths.map(() => defocus.weights) };\n }\n // Chart presets and (2.7) caller material scenes take the material path: each material tabulated once per node.\n const materials = input.materials;\n const scene: DisplayViewScene = materials ? { kind: \"materials\", materialAt: materials.materialAt,\n materials: materials.list.map((m) => (\"neutral\" in m ? { neutral: m.neutral } : { radiance: Float64Array.from(wavelengths, (nm) => m.radiance(nm)) })),\n whiteRadiance: Float64Array.from(wavelengths, (nm) => materials.whiteRadiance(nm)) } :\n { kind: \"radiance\", radianceAt: input.radianceAt };\n const channelIndex = (x: number, y: number): number => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2));\n // Without diffraction the environment's own query arithmetic (native-environment-lean.ts `geometry`, then the rectilinear\n // still-camera ray and the planar provider's plane point), so boundary points resolve as in the capture.\n const projectionDistance = resolveProjectionDistanceMm(input.focalLengthMm, input.focusDistanceM).projectionDistanceMm, siteWidthMm = pitch / 1000;\n const nodePhases = input.apertureNodes.flatMap((_, index) => spots.map((c) => ({ xPhase: (index % boxSamples + 0.5) / boxSamples, yPhase: (Math.floor(index / boxSamples) + 0.5) / boxSamples,\n aaX: c.offsetMicrometers.x / 1000, aaY: c.offsetMicrometers.y / 1000 })));\n const queryPoint = (x: number, y: number, o: number, out: Float64Array): void => {\n const node = nodePhases[o]!;\n const centerX = -imagingArea.widthMm / 2 + (pitch / 2 + x * pitch + 0) / 1000, centerY = -imagingArea.heightMm / 2 + (pitch / 2 + y * pitch + 0) / 1000;\n const sourceX = (centerX - siteWidthMm / 2 + node.xPhase * siteWidthMm) - node.aaX, sourceY = (centerY - siteWidthMm / 2 + node.yPhase * siteWidthMm) - node.aaY;\n const rx = projectionDistance * (sourceX / projectionDistance), ry = projectionDistance * (-sourceY / projectionDistance);\n const length = hypot3(rx, ry, projectionDistance), t = subjectDistanceM / (projectionDistance / length);\n out[0] = rx / length * t / metresPerFrameUnit; out[1] = ry / length * t / metresPerFrameUnit;\n };\n return { offsets, signalInput: { nativeWidth: input.nativeWidth, nativeHeight: input.nativeHeight, outputWidth: input.width, wavelengthsNm: wavelengths,\n electronsPerRadiance: input.perNode, offsets, ...(kernels ? { kernels } : { queryPoint }), scene, channelAt: channelIndex } };\n}\n\n"]}
@@ -29,7 +29,11 @@ export interface DisplayViewExpectedSignalInput {
29
29
  wavelengthsNm: readonly number[];
30
30
  electronsPerRadiance: readonly [Float64Array, Float64Array, Float64Array];
31
31
  offsets: readonly DisplayViewQueryOffset[];
32
- /** Diffraction: per-node kernels on the site lattice, (2R + 1)² taps in row-major order, out(x) = Σ in(x − t)·K(t). */
32
+ /**
33
+ * Diffraction and (#492) defocus: per-node kernels on the site lattice, (2R + 1)² taps in row-major order,
34
+ * out(x) = Σ in(x − t)·K(t). With defocus each node's kernel is its diffraction kernel convolved with the disc, or the
35
+ * disc itself without diffraction (expected-signal-setup.ts).
36
+ */
33
37
  kernels?: {
34
38
  radius: number;
35
39
  perNode: readonly Float64Array[];
@@ -38,9 +42,10 @@ export interface DisplayViewExpectedSignalInput {
38
42
  /** CFA channel index (0 red, 1 green, 2 blue) of native site (x, y). */
39
43
  channelAt(x: number, y: number): number;
40
44
  /**
41
- * Without diffraction: the plane point (frame units) that the environment queries for offset `o` of native site (x, y),
45
+ * Without kernels: the plane point (frame units) that the environment queries for offset `o` of native site (x, y),
42
46
  * computed with the environment's own arithmetic, so points that fall exactly on a chart boundary resolve to the same
43
- * material as in the capture. With diffraction the facade's object-space convolution uses the lattice formula, as here.
47
+ * material as in the capture. With kernels (diffraction or defocus) the facade's object-space convolution uses the
48
+ * lattice formula, as here.
44
49
  */
45
50
  queryPoint?(x: number, y: number, offset: number, out: Float64Array): void;
46
51
  }
@@ -58,18 +63,87 @@ export interface DisplayViewExpectedSignal {
58
63
  /** Scene lookups performed so far (material or radiance queries), for diagnostics. */
59
64
  readonly lookups: number;
60
65
  /**
61
- * The chart material that every kernel tap of the last `site` call read, when they all read one material (its value is
62
- * then a function of that material and the site's CFA channel only); −1 when unknown or mixed.
66
+ * The chart material that every kernel tap (without diffraction: every query point) of the last `site` call read, when
67
+ * they all read one material (its value is then a function of that material and the site's CFA channel only); −1 when
68
+ * unknown or mixed.
63
69
  */
64
70
  readonly uniformMaterial: number;
65
- /** Whether `uniformMaterial` is tracked in the current window (otherwise it is not meaningful). */
71
+ /**
72
+ * Whether `uniformMaterial` is tracked in the current window (otherwise it is not meaningful): chart materials in a
73
+ * filled window or, without diffraction, in a window the direct ring serves (#444); never for radiance scenes.
74
+ */
66
75
  readonly tracksMaterial: boolean;
67
76
  /**
68
77
  * Whether the current window caches its sites: a second `site` call for a site inside the window then returns the stored
69
78
  * value without any work or lookup (#392).
70
79
  */
71
80
  readonly cachesSites: boolean;
81
+ /**
82
+ * Whether `markReads` is available in the current window: a filled window (chart materials with diffraction kernels),
83
+ * whose lookup count comes from the sites it computed. False otherwise: for windows without kernels (whose sites the
84
+ * direct ring already serves cheaply) and for the lazy window caches (radiance scenes with diffraction), whose count
85
+ * depends on the lattice points each computed site reads.
86
+ */
87
+ readonly marksReads: boolean;
88
+ /**
89
+ * Records `count` sites as read in the current window without computing them: site n at (xs[n], ys[n]) with value values[n]
90
+ * (as `site` returned it for that site) and uniform material materials[n] (as `uniformMaterial` reported it). Sites outside
91
+ * the window and sites the window already holds are skipped. For the window's lookup count and for later `site` calls this
92
+ * is the same as reading each site with `site`. Only valid while `marksReads` is true; it must do for a site the
93
+ * bookkeeping `site` does for a site the window's cache does not hold yet.
94
+ */
95
+ markReads(xs: Int32Array, ys: Int32Array, values: Float64Array, materials: Int32Array, count: number): void;
72
96
  }
97
+ /**
98
+ * The tables of `createDisplayViewExpectedSignal` (see there) that are a function of the input's numbers alone: the materials,
99
+ * the lattice groups' geometry and combined kernels, and the per-material sums. Written only while they are built, never after:
100
+ * they are shared by every render of an input and, for chart materials, by every input of the same content (#449). Everything a
101
+ * render changes (each group's window cache and its origin and size, the bands, rings and counts) lives in the render.
102
+ */
103
+ export interface SignalTables {
104
+ readonly reflectance: Float64Array;
105
+ readonly spectralIndex: Int32Array;
106
+ readonly spectral: readonly Float64Array[];
107
+ readonly groups: readonly GroupTables[];
108
+ readonly neutralDirect: readonly number[];
109
+ readonly spectralDirect: readonly (readonly number[])[];
110
+ readonly uniformSite: readonly Float64Array[];
111
+ }
112
+ /** A lattice group's fixed part: its fractional shift, members, index bounds, rectangle and combined kernels. */
113
+ export interface GroupTables {
114
+ fx: number;
115
+ fy: number;
116
+ members: {
117
+ ix: number;
118
+ iy: number;
119
+ weight: number;
120
+ }[];
121
+ minIx: number;
122
+ maxIx: number;
123
+ minIy: number;
124
+ maxIy: number;
125
+ cw: number;
126
+ ch: number;
127
+ neutral: Float64Array[];
128
+ spectral: Float64Array[][];
129
+ perNode: Float64Array[][];
130
+ }
131
+ /** Chart-material tables kept by content across input objects (#449), least recently used first. */
132
+ export declare const EXPECTED_SIGNAL_TABLE_CACHE_SIZE = 4;
133
+ /** Internal test hook: the kept table entries, and the tables built and found by content so far. */
134
+ export declare function expectedSignalTableCacheState(): {
135
+ size: number;
136
+ builds: number;
137
+ hits: number;
138
+ };
139
+ /** Internal test hook: forgets the tables kept by content (inputs already prepared keep theirs). */
140
+ export declare function clearExpectedSignalTableCache(): void;
141
+ /**
142
+ * Internal (#479): the read-only tables of an input (the ones `createDisplayViewExpectedSignal` reads), for the GPU display view,
143
+ * which uploads the combined kernels and per-material sums the reference forms in float64. Prepared and cached as a render
144
+ * prepares them, so a GPU render of an input and a CPU render read the same doubles.
145
+ */
146
+ export declare function displayViewSignalTables(input: DisplayViewExpectedSignalInput): SignalTables;
73
147
  /**
74
148
  * Expected photo electrons per site: μ_c(i, j) = Σ_o w_o Σ_λ A_c(λ) Σ_t K_λ(t)·L(p_o(i, j) − t, λ), where p_o is the
75
149
  * query point of offset o and K the diffraction kernel (a single unit tap without diffraction). This is the sum the
@@ -16,7 +16,93 @@ const BAND_LIMIT = 1 << 22;
16
16
  */
17
17
  const LATTICE_RING_ROWS = 128, LATTICE_RING_MAXIMUM_BAND_ROWS = 64, LATTICE_RING_MARGIN = 64;
18
18
  const PREPARED = new WeakMap();
19
+ /** Chart-material tables kept by content across input objects (#449), least recently used first. */
20
+ export const EXPECTED_SIGNAL_TABLE_CACHE_SIZE = 4;
21
+ const tableCache = [];
22
+ let tableBuilds = 0, tableHits = 0;
23
+ /** Internal test hook: the kept table entries, and the tables built and found by content so far. */
24
+ export function expectedSignalTableCacheState() { return { size: tableCache.length, builds: tableBuilds, hits: tableHits }; }
25
+ /** Internal test hook: forgets the tables kept by content (inputs already prepared keep theirs). */
26
+ export function clearExpectedSignalTableCache() { tableCache.length = 0; }
27
+ /**
28
+ * Every number the tables read, with the lengths that delimit them, as the bits of doubles (so -0, NaN and every digit are kept
29
+ * apart): the spectral nodes, the three channel responses, the offsets, the kernel radius and taps, each material's reflectance or
30
+ * radiance, and the white radiance. Undefined for radiance scenes, whose tables are not kept by content.
31
+ */
32
+ function tableSignature(input) {
33
+ const scene = input.scene;
34
+ if (scene.kind !== "materials")
35
+ return undefined;
36
+ const values = [];
37
+ const push = (a) => { values.push(a.length); for (let i = 0; i < a.length; i++)
38
+ values.push(a[i]); };
39
+ push(input.wavelengthsNm);
40
+ values.push(input.electronsPerRadiance.length);
41
+ for (const a of input.electronsPerRadiance)
42
+ push(a);
43
+ values.push(input.offsets.length);
44
+ for (const o of input.offsets)
45
+ values.push(o.x, o.y, o.weight);
46
+ if (input.kernels) {
47
+ values.push(1, input.kernels.radius, input.kernels.perNode.length);
48
+ for (const k of input.kernels.perNode)
49
+ push(k);
50
+ }
51
+ else
52
+ values.push(0);
53
+ values.push(scene.materials.length);
54
+ for (const m of scene.materials)
55
+ if ("neutral" in m)
56
+ values.push(0, m.neutral);
57
+ else {
58
+ values.push(1);
59
+ push(m.radiance);
60
+ }
61
+ push(scene.whiteRadiance);
62
+ return new Uint32Array(Float64Array.from(values).buffer);
63
+ }
64
+ function sameBits(a, b) {
65
+ if (a.length !== b.length)
66
+ return false;
67
+ for (let i = 0; i < a.length; i++)
68
+ if (a[i] !== b[i])
69
+ return false;
70
+ return true;
71
+ }
19
72
  function prepareSignal(input) {
73
+ const signature = tableSignature(input);
74
+ let tables;
75
+ if (signature) {
76
+ const at = tableCache.findIndex((entry) => sameBits(entry.signature, signature));
77
+ if (at >= 0) {
78
+ const [entry] = tableCache.splice(at, 1);
79
+ tableCache.push(entry);
80
+ tables = entry.tables;
81
+ tableHits++;
82
+ }
83
+ }
84
+ if (!tables) {
85
+ tables = buildTables(input);
86
+ tableBuilds++;
87
+ if (signature) {
88
+ tableCache.push({ signature, tables });
89
+ while (tableCache.length > EXPECTED_SIGNAL_TABLE_CACHE_SIZE)
90
+ tableCache.shift();
91
+ }
92
+ }
93
+ const prepared = { tables, rings: tables.groups.map(() => undefined) };
94
+ PREPARED.set(input, prepared);
95
+ return prepared;
96
+ }
97
+ /**
98
+ * Internal (#479): the read-only tables of an input (the ones `createDisplayViewExpectedSignal` reads), for the GPU display view,
99
+ * which uploads the combined kernels and per-material sums the reference forms in float64. Prepared and cached as a render
100
+ * prepares them, so a GPU render of an input and a CPU render read the same doubles.
101
+ */
102
+ export function displayViewSignalTables(input) {
103
+ return (PREPARED.get(input) ?? prepareSignal(input)).tables;
104
+ }
105
+ function buildTables(input) {
20
106
  const nodes = input.wavelengthsNm.length, R = input.kernels ? input.kernels.radius : 0, size = 2 * R + 1;
21
107
  const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets;
22
108
  const kernelAt = (node, t) => (input.kernels ? input.kernels.perNode[node][t] : 1);
@@ -26,11 +112,12 @@ function prepareSignal(input) {
26
112
  if (scene.kind === "materials") {
27
113
  reflectance = new Float64Array(scene.materials.length);
28
114
  spectralIndex = new Int32Array(scene.materials.length).fill(-1);
115
+ // A copy of each spectral radiance: the tables may outlive the input and serve other inputs of the same content.
29
116
  scene.materials.forEach((m, index) => { if ("neutral" in m)
30
117
  reflectance[index] = m.neutral;
31
118
  else {
32
119
  spectralIndex[index] = spectral.length;
33
- spectral.push(m.radiance);
120
+ spectral.push(Float64Array.from(m.radiance));
34
121
  } });
35
122
  }
36
123
  // Offsets grouped by fractional shift: offsets that differ by whole sites share one lattice.
@@ -40,7 +127,7 @@ function prepareSignal(input) {
40
127
  const fx = frac(o.x), fy = frac(o.y), ix = Math.round(o.x - fx), iy = Math.round(o.y - fy);
41
128
  let group = groups.find((q) => Math.abs(q.fx - fx) <= OFFSET_TOLERANCE && Math.abs(q.fy - fy) <= OFFSET_TOLERANCE);
42
129
  if (!group) {
43
- group = { fx, fy, members: [], minIx: ix, maxIx: ix, minIy: iy, maxIy: iy, cw: 0, ch: 0, neutral: [], spectral: [], perNode: [], cache: new Int32Array(0), cx0: 0, cy0: 0, cpw: 0, cph: 0 };
130
+ group = { fx, fy, members: [], minIx: ix, maxIx: ix, minIy: iy, maxIy: iy, cw: 0, ch: 0, neutral: [], spectral: [], perNode: [] };
44
131
  groups.push(group);
45
132
  }
46
133
  group.members.push({ ix, iy, weight: o.weight });
@@ -93,9 +180,10 @@ function prepareSignal(input) {
93
180
  }
94
181
  return sum;
95
182
  })) : [];
96
- const prepared = { reflectance, spectralIndex, spectral, groups, neutralDirect, spectralDirect, uniformSite, rings: groups.map(() => undefined) };
97
- PREPARED.set(input, prepared);
98
- return prepared;
183
+ // Frozen records: a render copies each group before it gives it a window (see createDisplayViewExpectedSignal).
184
+ for (const group of groups)
185
+ Object.freeze(group);
186
+ return Object.freeze({ reflectance, spectralIndex, spectral, groups, neutralDirect, spectralDirect, uniformSite });
99
187
  }
100
188
  /**
101
189
  * Expected photo electrons per site: μ_c(i, j) = Σ_o w_o Σ_λ A_c(λ) Σ_t K_λ(t)·L(p_o(i, j) − t, λ), where p_o is the
@@ -120,9 +208,10 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
120
208
  const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets, channelAt = input.channelAt;
121
209
  // The request's tables (materials, lattice groups, combined kernels, per-material sums) are the same for every render of a
122
210
  // prepared view, which passes the same input object: computed once per input. Each render gets its own groups' window caches.
211
+ // Chart-material tables are also shared by inputs of the same content (#449): each render gets its own groups' window state.
123
212
  const prepared = PREPARED.get(input) ?? prepareSignal(input);
124
- const { reflectance, spectralIndex, spectral, neutralDirect, spectralDirect, uniformSite } = prepared;
125
- const groups = prepared.groups.map((group) => ({ ...group, cache: new Int32Array(0), cx0: 0, cy0: 0, cpw: 0, cph: 0 }));
213
+ const { reflectance, spectralIndex, spectral, neutralDirect, spectralDirect, uniformSite } = prepared.tables;
214
+ const groups = prepared.tables.groups.map((group) => ({ ...group, cache: new Int32Array(0), cx0: 0, cy0: 0, cpw: 0, cph: 0 }));
126
215
  let lookups = 0;
127
216
  const point = new Float64Array(2);
128
217
  const materialSignalDirect = (m, c) => {
@@ -131,15 +220,21 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
131
220
  throw new InvalidConfigurationError("Unknown chart material.");
132
221
  return si < 0 ? reflectance[m] * neutralDirect[c] : spectralDirect[c][si];
133
222
  };
134
- /** Without diffraction: one site from the environment's own query points. */
223
+ /**
224
+ * Without diffraction: one site from the environment's own query points. Also sets lastMaterial: the chart material every
225
+ * query point read, or −1 when they read more than one (or the scene is a radiance scene). A site whose points all read
226
+ * material m sums the same operands in the same order as every other such site of its channel, so it holds the same value.
227
+ */
135
228
  function directSite(x, y) {
136
229
  const c = channelAt(x, y);
137
- let sum = 0;
230
+ let sum = 0, uniform = -1;
138
231
  for (let o = 0; o < offsets.length; o++) {
139
232
  input.queryPoint(x, y, o, point);
140
233
  let value;
141
234
  if (scene.kind === "materials") {
142
- value = materialSignalDirect(scene.materialAt(point[0], point[1]), c);
235
+ const m = scene.materialAt(point[0], point[1]);
236
+ uniform = o === 0 || uniform === m ? m : -2;
237
+ value = materialSignalDirect(m, c);
143
238
  lookups++;
144
239
  }
145
240
  else {
@@ -150,6 +245,7 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
150
245
  }
151
246
  sum += offsets[o].weight * value;
152
247
  }
248
+ lastMaterial = uniform < 0 ? -1 : uniform;
153
249
  return sum;
154
250
  }
155
251
  // Lattice point (p, q) of a group: the facade's lattice formula (createDiffractedPlanarProvider).
@@ -226,7 +322,8 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
226
322
  changeLeft: new Int32Array(0), changeUp: new Int32Array(0), v: new Float64Array(0) }));
227
323
  // Lattice rows of material indices per group, kept across windows (filled windows only): rows LATTICE_RING_ROWS apart share a
228
324
  // slot; columns from −LATTICE_RING_MARGIN. Int16 when every index fits (about 3 MB for four groups of a 6000-site row). A
229
- // lattice point's material depends only on the input, so the rings are kept with its prepared tables, across renders.
325
+ // lattice point's material depends only on the input, so the rings are kept per input object, across its renders (not with
326
+ // the tables kept by content: they depend on `materialAt` and the raster).
230
327
  const ringWide = scene.kind === "materials" && scene.materials.length <= 32767;
231
328
  const rings = prepared.rings;
232
329
  function latticeRing(g) {
@@ -412,8 +509,9 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
412
509
  return v;
413
510
  }
414
511
  // Without diffraction (#392): site values persist across windows the same way, in their own ring (a site's value does not
415
- // depend on the window). A ring hit counts the lookups directSite would have made, so `lookups` is unchanged.
416
- let directRows = 0, directOn = false, directRing = new Float64Array(0), directRowId = new Int32Array(0);
512
+ // depend on the window). A ring hit counts the lookups directSite would have made, so `lookups` is unchanged. The site's
513
+ // material (directSite's lastMaterial) is kept beside its value, so the display view's one-material memo applies (#444).
514
+ let directRows = 0, directOn = false, directRing = new Float64Array(0), directRowId = new Int32Array(0), directRingMaterial = new Int32Array(0);
417
515
  const directLookups = offsets.length * (scene.kind === "materials" ? 1 : nodes);
418
516
  function directRead(x, y) {
419
517
  const slot = y & (directRows - 1), q = slot * W + x;
@@ -421,6 +519,7 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
421
519
  const v = directRing[q];
422
520
  if (v === v) {
423
521
  lookups += directLookups;
522
+ lastMaterial = directRingMaterial[q];
424
523
  return v;
425
524
  }
426
525
  }
@@ -430,14 +529,17 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
430
529
  }
431
530
  const v = directSite(x, y);
432
531
  directRing[q] = v;
532
+ directRingMaterial[q] = lastMaterial;
433
533
  return v;
434
534
  }
435
535
  /** A site not in the window's site cache: from a ring, a filled window without the ring, the lazy window caches, or uncached. */
436
536
  function siteMiss(x, y, i, j, cached) {
437
537
  if (directOn) {
438
538
  const v = directRead(x, y);
439
- if (cached)
539
+ if (cached) {
440
540
  siteCache[j * sw + i] = v;
541
+ windowMaterial[j * sw + i] = lastMaterial;
542
+ }
441
543
  return v;
442
544
  }
443
545
  if (ringOn) {
@@ -464,22 +566,66 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
464
566
  /**
465
567
  * Lattice points of the closing window that kernelSite's lazy cache would have looked up: per group, the window cache points
466
568
  * covered by the rectangles of the computed sites (the site cache's filled entries and the outside sites), a box dilation.
569
+ * A group's count depends only on the computed sites and its geometry (cw, ch, cpw, cph), so a group shaped like an
570
+ * earlier one takes that group's count (the app's Quality groups all have one shape).
467
571
  */
572
+ const groupTotals = new Float64Array(groups.length);
573
+ function sameGeometry(g) {
574
+ const group = groups[g];
575
+ for (let h = 0; h < g; h++) {
576
+ const o = groups[h];
577
+ if (o.cw === group.cw && o.ch === group.ch && o.cpw === group.cpw && o.cph === group.cph)
578
+ return h;
579
+ }
580
+ return -1;
581
+ }
468
582
  function windowLookups() {
469
583
  let total = 0;
470
584
  if (outsideSites.length === 0 && filledCount === 0)
471
585
  return 0;
586
+ if (outsideSites.length === 0 && filledCount === sw * sh) {
587
+ // Every site computed (each once) and none outside: the marks at (i + cw − 1, j + ch − 1) cover cache columns
588
+ // 0 … sw + cw − 2 and rows 0 … sh + ch − 2, the whole cpw × cph cache of every group.
589
+ for (const group of groups)
590
+ total += group.cpw * group.cph;
591
+ return total;
592
+ }
472
593
  if (outsideSites.length === 0 && sw <= 31) {
473
594
  // Computed sites as a bit mask per window row. Cache point (ci, cj) is covered when a computed site (i, j) has
474
595
  // i ≤ ci < i + cw and j ≤ cj < j + ch; a union of dilations is the dilation of the union, so per cache row the
475
- // covered columns are the dilation of the OR of the masks of window rows cj − ch + 1 … cj.
476
- for (const group of groups) {
477
- const cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, last = Math.min(cph - 1, fy1 + ch - 1);
596
+ // covered columns are the dilation of the OR of the masks of window rows cj − ch + 1 … cj. That OR is a prefix OR
597
+ // when the rows start at fy0 and a suffix OR when they end at fy1 (OR is associative and commutative).
598
+ if (prefixOr.length < sh) {
599
+ prefixOr = new Int32Array(sh);
600
+ suffixOr = new Int32Array(sh);
601
+ }
602
+ for (let j = fy0, m = 0; j <= fy1; j++) {
603
+ m |= rowMask[j];
604
+ prefixOr[j] = m;
605
+ }
606
+ for (let j = fy1, m = 0; j >= fy0; j--) {
607
+ m |= rowMask[j];
608
+ suffixOr[j] = m;
609
+ }
610
+ for (let g = 0; g < groups.length; g++) {
611
+ const same = sameGeometry(g);
612
+ if (same >= 0) {
613
+ total += groupTotals[same];
614
+ groupTotals[g] = groupTotals[same];
615
+ continue;
616
+ }
617
+ const group = groups[g], cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, last = Math.min(cph - 1, fy1 + ch - 1), before = total;
478
618
  let previous = -1, width = 0;
479
619
  for (let cj = fy0; cj <= last; cj++) {
480
620
  let m = 0;
481
- for (let j = Math.max(fy0, cj - ch + 1), top = Math.min(fy1, cj); j <= top; j++)
482
- m |= rowMask[j];
621
+ const a = Math.max(fy0, cj - ch + 1), b = Math.min(fy1, cj);
622
+ if (a === fy0)
623
+ m = prefixOr[b];
624
+ else if (b === fy1)
625
+ m = suffixOr[a];
626
+ else
627
+ for (let j = a; j <= b; j++)
628
+ m |= rowMask[j];
483
629
  if (m !== previous) {
484
630
  previous = m;
485
631
  width = 0;
@@ -498,11 +644,20 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
498
644
  }
499
645
  total += width;
500
646
  }
647
+ groupTotals[g] = total - before;
501
648
  }
502
649
  return total;
503
650
  }
504
- for (const group of groups) {
505
- const cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, ew = cpw + cw - 1, eh = cph + ch - 1;
651
+ for (let g = 0; g < groups.length; g++) {
652
+ // The marks are the computed sites shifted by (cw − 1, ch − 1), so the count depends on the group's geometry only.
653
+ const same = sameGeometry(g);
654
+ if (same >= 0) {
655
+ total += groupTotals[same];
656
+ groupTotals[g] = groupTotals[same];
657
+ continue;
658
+ }
659
+ const group = groups[g], cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, ew = cpw + cw - 1, eh = cph + ch - 1, before = total;
660
+ groupTotals[g] = 0;
506
661
  // Top-left marks in extended coordinates (window coordinates + (cw − 1, ch − 1)), and their bounding box.
507
662
  if (marks.length < ew * eh) {
508
663
  marks = new Uint8Array(ew * eh);
@@ -557,6 +712,7 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
557
712
  total++;
558
713
  }
559
714
  }
715
+ groupTotals[g] = total - before;
560
716
  }
561
717
  return total;
562
718
  }
@@ -564,7 +720,7 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
564
720
  // The computed entries of a filled window's site cache (windowLookups): their count, bounding box and a bit mask per row
565
721
  // (columns below 31).
566
722
  let filledCount = 0, fx0 = 0, fx1 = -1, fy0 = 0, fy1 = -1;
567
- let rowMask = new Int32Array(0);
723
+ let rowMask = new Int32Array(0), prefixOr = new Int32Array(0), suffixOr = new Int32Array(0);
568
724
  function markFilled(i, j) {
569
725
  if (i < 31)
570
726
  rowMask[j] = rowMask[j] | (1 << i);
@@ -586,8 +742,25 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
586
742
  return {
587
743
  get lookups() { return lookups + (filled ? windowLookups() : 0); },
588
744
  get uniformMaterial() { return lastMaterial; },
589
- get tracksMaterial() { return ringOn; },
745
+ // Chart materials are tracked on the filled path and, without diffraction, on the direct ring (radiance scenes never).
746
+ get tracksMaterial() { return ringOn || (directOn && scene.kind === "materials"); },
590
747
  get cachesSites() { return sw > 0; },
748
+ get marksReads() { return filled; },
749
+ markReads(xs, ys, values, materials, count) {
750
+ for (let n = 0; n < count; n++) {
751
+ const i = xs[n] - sx0, j = ys[n] - sy0;
752
+ if (!(i >= 0 && j >= 0 && i < sw && j < sh))
753
+ continue;
754
+ const w = j * sw + i;
755
+ if (siteCache[w] === siteCache[w])
756
+ continue;
757
+ // As siteMiss for a site inside a filled window: the computed site is marked (windowLookups counts from the marks)
758
+ // and its material kept.
759
+ siteCache[w] = values[n];
760
+ windowMaterial[w] = materials[n];
761
+ markFilled(i, j);
762
+ }
763
+ },
591
764
  window(x0, y0, x1, y1) {
592
765
  if (filled)
593
766
  lookups += windowLookups();
@@ -619,9 +792,12 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
619
792
  directRows = rows;
620
793
  directRing = new Float64Array(rows * W).fill(NaN);
621
794
  directRowId = new Int32Array(rows).fill(-1);
795
+ directRingMaterial = new Int32Array(rows * W);
622
796
  }
623
797
  }
624
798
  directOn = sh > 0 && directRows >= 2 * sh;
799
+ if (directOn && windowMaterial.length < sw * sh)
800
+ windowMaterial = new Int32Array(sw * sh);
625
801
  return;
626
802
  }
627
803
  for (const group of groups) {
@@ -675,7 +851,7 @@ export function createDisplayViewExpectedSignal(input, spanColumns = Infinity) {
675
851
  if (cached) {
676
852
  const w = j * sw + i, v = siteCache[w];
677
853
  if (v === v) {
678
- if (ringOn)
854
+ if (ringOn || directOn)
679
855
  lastMaterial = windowMaterial[w];
680
856
  return v;
681
857
  }