@photivra/engine 2.19.0 → 2.21.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 (242) hide show
  1. package/CHANGELOG.md +289 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +7 -5
  4. package/README.md +7 -6
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY.md +21 -10
  7. package/dist/capture/dng-lens-correction-opcodes.d.ts +32 -0
  8. package/dist/capture/dng-lens-correction-opcodes.js +119 -0
  9. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -0
  10. package/dist/capture/exposure-build-up-stage.d.ts +16 -0
  11. package/dist/capture/exposure-build-up-stage.js +4 -0
  12. package/dist/capture/exposure-build-up-stage.js.map +1 -0
  13. package/dist/capture/native-environment-lean.d.ts +2 -1
  14. package/dist/capture/native-environment-lean.js +42 -78
  15. package/dist/capture/native-environment-lean.js.map +1 -1
  16. package/dist/capture/native-environment-tabulated.js +4 -3
  17. package/dist/capture/native-environment-tabulated.js.map +1 -1
  18. package/dist/capture/native-raw-dng.d.ts +6 -0
  19. package/dist/capture/native-raw-dng.js +16 -2
  20. package/dist/capture/native-raw-dng.js.map +1 -1
  21. package/dist/capture/native-raw.d.ts +3 -2
  22. package/dist/capture/native-raw.js +5 -4
  23. package/dist/capture/native-raw.js.map +1 -1
  24. package/dist/capture/sensor-raw-producer.d.ts +4 -1
  25. package/dist/capture/sensor-raw-producer.js +15 -8
  26. package/dist/capture/sensor-raw-producer.js.map +1 -1
  27. package/dist/core/shared-helpers.d.ts +2 -0
  28. package/dist/core/shared-helpers.js +4 -0
  29. package/dist/core/shared-helpers.js.map +1 -1
  30. package/dist/core/version.d.ts +1 -1
  31. package/dist/core/version.js +1 -1
  32. package/dist/core/version.js.map +1 -1
  33. package/dist/equipment/generic-lens-catalogue.d.ts +11 -4
  34. package/dist/equipment/generic-lens-catalogue.js +49 -7
  35. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  36. package/dist/equipment/generic-tier-assets.d.ts +32 -0
  37. package/dist/equipment/generic-tier-assets.js +18 -14
  38. package/dist/equipment/generic-tier-assets.js.map +1 -1
  39. package/dist/equipment/generic-tier-presets.js +2 -3
  40. package/dist/equipment/generic-tier-presets.js.map +1 -1
  41. package/dist/equipment/sensor-profile-body.d.ts +78 -0
  42. package/dist/equipment/sensor-profile-body.js +77 -0
  43. package/dist/equipment/sensor-profile-body.js.map +1 -0
  44. package/dist/equipment/sensor-profiles.d.ts +8 -0
  45. package/dist/equipment/sensor-profiles.js +82 -0
  46. package/dist/equipment/sensor-profiles.js.map +1 -0
  47. package/dist/facade/catalogue-lens-signal.d.ts +2 -2
  48. package/dist/facade/catalogue-lens-signal.js +58 -71
  49. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  50. package/dist/facade/chart-radiance-batch.js +2 -1
  51. package/dist/facade/chart-radiance-batch.js.map +1 -1
  52. package/dist/facade/defocus-image-space-provider.d.ts +7 -0
  53. package/dist/facade/defocus-image-space-provider.js +64 -76
  54. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  55. package/dist/facade/display-view-gpu-session.js +2 -0
  56. package/dist/facade/display-view-gpu-session.js.map +1 -1
  57. package/dist/facade/display-view-gpu.d.ts +13 -5
  58. package/dist/facade/display-view-gpu.js +4 -4
  59. package/dist/facade/display-view-gpu.js.map +1 -1
  60. package/dist/facade/display-view-lens-signal.d.ts +1 -1
  61. package/dist/facade/display-view-lens-signal.js +30 -26
  62. package/dist/facade/display-view-lens-signal.js.map +1 -1
  63. package/dist/facade/display-view.d.ts +3 -1
  64. package/dist/facade/display-view.js +6 -24
  65. package/dist/facade/display-view.js.map +1 -1
  66. package/dist/facade/expected-signal-setup.d.ts +3 -1
  67. package/dist/facade/expected-signal-setup.js +1 -1
  68. package/dist/facade/expected-signal-setup.js.map +1 -1
  69. package/dist/facade/expected-signal.d.ts +47 -14
  70. package/dist/facade/expected-signal.js +117 -112
  71. package/dist/facade/expected-signal.js.map +1 -1
  72. package/dist/facade/exposure-build-up.d.ts +70 -0
  73. package/dist/facade/exposure-build-up.js +164 -0
  74. package/dist/facade/exposure-build-up.js.map +1 -0
  75. package/dist/facade/fast-methods.d.ts +50 -0
  76. package/dist/facade/fast-methods.js +58 -0
  77. package/dist/facade/fast-methods.js.map +1 -0
  78. package/dist/facade/fast-resolution.d.ts +12 -0
  79. package/dist/facade/fast-resolution.js +18 -0
  80. package/dist/facade/fast-resolution.js.map +1 -0
  81. package/dist/facade/generic-lens-capture.d.ts +5 -4
  82. package/dist/facade/generic-lens-capture.js +5 -4
  83. package/dist/facade/generic-lens-capture.js.map +1 -1
  84. package/dist/facade/image-plane-enable.d.ts +7 -0
  85. package/dist/facade/image-plane-enable.js +17 -0
  86. package/dist/facade/image-plane-enable.js.map +1 -0
  87. package/dist/facade/image-plane-lattice-data.d.ts +6 -0
  88. package/dist/facade/image-plane-lattice-data.js +10 -0
  89. package/dist/facade/image-plane-lattice-data.js.map +1 -0
  90. package/dist/facade/image-plane-support.d.ts +33 -0
  91. package/dist/facade/image-plane-support.js +11 -0
  92. package/dist/facade/image-plane-support.js.map +1 -0
  93. package/dist/facade/image-plane.d.ts +36 -0
  94. package/dist/facade/image-plane.js +353 -0
  95. package/dist/facade/image-plane.js.map +1 -0
  96. package/dist/facade/mixed-illumination.d.ts +108 -0
  97. package/dist/facade/mixed-illumination.js +174 -0
  98. package/dist/facade/mixed-illumination.js.map +1 -0
  99. package/dist/facade/representative-light-sources.d.ts +72 -0
  100. package/dist/facade/representative-light-sources.js +124 -0
  101. package/dist/facade/representative-light-sources.js.map +1 -0
  102. package/dist/facade/scene-radiance.d.ts +129 -4
  103. package/dist/facade/scene-radiance.js +48 -13
  104. package/dist/facade/scene-radiance.js.map +1 -1
  105. package/dist/facade/simulate-capture.d.ts +88 -19
  106. package/dist/facade/simulate-capture.js +374 -145
  107. package/dist/facade/simulate-capture.js.map +1 -1
  108. package/dist/facade/simulate-display-view.d.ts +3 -2
  109. package/dist/facade/simulate-display-view.js +36 -18
  110. package/dist/facade/simulate-display-view.js.map +1 -1
  111. package/dist/gpu/chart-composite-gpu.d.ts +13 -1
  112. package/dist/gpu/chart-composite-gpu.js +48 -9
  113. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  114. package/dist/gpu/chart-radiance-gpu.js +4 -30
  115. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  116. package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
  117. package/dist/gpu/display-view-composite-material-module.js +39 -13
  118. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  119. package/dist/gpu/display-view-image-material-module.d.ts +86 -0
  120. package/dist/gpu/display-view-image-material-module.js +339 -0
  121. package/dist/gpu/display-view-image-material-module.js.map +1 -0
  122. package/dist/gpu/display-view-image-quality.d.ts +45 -0
  123. package/dist/gpu/display-view-image-quality.js +380 -0
  124. package/dist/gpu/display-view-image-quality.js.map +1 -0
  125. package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
  126. package/dist/gpu/display-view-quality-gpu.js +136 -205
  127. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  128. package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
  129. package/dist/gpu/display-view-speed-gpu.js +131 -93
  130. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  131. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
  132. package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
  133. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  134. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
  135. package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
  136. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  137. package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
  138. package/dist/gpu/wgsl-exact-sum.js +42 -0
  139. package/dist/gpu/wgsl-exact-sum.js.map +1 -0
  140. package/dist/index.d.ts +18 -3
  141. package/dist/index.js +12 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/motion/subject-motion.d.ts +66 -0
  144. package/dist/motion/subject-motion.js +52 -0
  145. package/dist/motion/subject-motion.js.map +1 -0
  146. package/dist/optics/aperture-diameter.d.ts +47 -0
  147. package/dist/optics/aperture-diameter.js +50 -0
  148. package/dist/optics/aperture-diameter.js.map +1 -0
  149. package/dist/optics/detail-limit.d.ts +182 -0
  150. package/dist/optics/detail-limit.js +336 -0
  151. package/dist/optics/detail-limit.js.map +1 -0
  152. package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
  153. package/dist/optics/generic-lens-lateral-color.js +77 -0
  154. package/dist/optics/generic-lens-lateral-color.js.map +1 -0
  155. package/dist/optics/generic-lens-psf.d.ts +18 -2
  156. package/dist/optics/generic-lens-psf.js +79 -48
  157. package/dist/optics/generic-lens-psf.js.map +1 -1
  158. package/dist/optics/lens-psf-profile.d.ts +7 -0
  159. package/dist/optics/lens-psf-profile.js +2 -1
  160. package/dist/optics/lens-psf-profile.js.map +1 -1
  161. package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
  162. package/dist/optics/scene-to-sensor-irradiance.js +7 -0
  163. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  164. package/dist/output/crop-to-match-focal-length.d.ts +62 -0
  165. package/dist/output/crop-to-match-focal-length.js +63 -0
  166. package/dist/output/crop-to-match-focal-length.js.map +1 -0
  167. package/dist/output/developed-lens-corrections.d.ts +19 -5
  168. package/dist/output/developed-lens-corrections.js +30 -11
  169. package/dist/output/developed-lens-corrections.js.map +1 -1
  170. package/dist/output/generic-lens-corrections.d.ts +41 -3
  171. package/dist/output/generic-lens-corrections.js +57 -9
  172. package/dist/output/generic-lens-corrections.js.map +1 -1
  173. package/dist/output/lens-corrections.d.ts +12 -2
  174. package/dist/output/lens-corrections.js +22 -7
  175. package/dist/output/lens-corrections.js.map +1 -1
  176. package/dist/output/print-contrast-reference.js +1 -1
  177. package/dist/output/print-contrast-reference.js.map +1 -1
  178. package/dist/output/print-plan.d.ts +41 -5
  179. package/dist/output/print-plan.js +59 -14
  180. package/dist/output/print-plan.js.map +1 -1
  181. package/dist/output/print-size-blur-noise.d.ts +110 -0
  182. package/dist/output/print-size-blur-noise.js +138 -0
  183. package/dist/output/print-size-blur-noise.js.map +1 -0
  184. package/dist/sensor/binomial-sample.d.ts +3 -0
  185. package/dist/sensor/binomial-sample.js +86 -0
  186. package/dist/sensor/binomial-sample.js.map +1 -0
  187. package/dist/sensor/eqe-electron-rate.js +1 -7
  188. package/dist/sensor/eqe-electron-rate.js.map +1 -1
  189. package/dist/sensor/exposure-window.d.ts +2 -0
  190. package/dist/sensor/exposure-window.js +2 -2
  191. package/dist/sensor/exposure-window.js.map +1 -1
  192. package/dist/sensor/readout-timing.js +2 -23
  193. package/dist/sensor/readout-timing.js.map +1 -1
  194. package/dist/sensor/response-application-compatibility.js +1 -6
  195. package/dist/sensor/response-application-compatibility.js.map +1 -1
  196. package/dist/sensor/responsivity-photocurrent.js +1 -7
  197. package/dist/sensor/responsivity-photocurrent.js.map +1 -1
  198. package/dist/sensor/sensor-characteristics.d.ts +147 -0
  199. package/dist/sensor/sensor-characteristics.js +126 -0
  200. package/dist/sensor/sensor-characteristics.js.map +1 -0
  201. package/dist/sensor/spatial-sample-reduction.js +1 -5
  202. package/dist/sensor/spatial-sample-reduction.js.map +1 -1
  203. package/dist/sensor/spectral-quadrature.js +1 -6
  204. package/dist/sensor/spectral-quadrature.js.map +1 -1
  205. package/dist/sensor/temporal-photo-signal.d.ts +3 -0
  206. package/dist/sensor/temporal-photo-signal.js +5 -2
  207. package/dist/sensor/temporal-photo-signal.js.map +1 -1
  208. package/dist/stabilization/system.d.ts +9 -0
  209. package/dist/stabilization/system.js +5 -6
  210. package/dist/stabilization/system.js.map +1 -1
  211. package/docs/AGENT_GUIDE.md +5 -0
  212. package/docs/API_REFERENCE.md +173 -2
  213. package/docs/QUICK_START.md +213 -6
  214. package/docs/USAGE.md +157 -3
  215. package/docs/api/capture-native-raw-dng.md +6 -0
  216. package/docs/api/core-version.md +1 -1
  217. package/docs/api/equipment-generic-lens-catalogue.md +17 -7
  218. package/docs/api/equipment-sensor-profile-body.md +64 -0
  219. package/docs/api/equipment-sensor-profiles.md +86 -0
  220. package/docs/api/exports.json +357 -2
  221. package/docs/api/facade-display-view-gpu.md +5 -2
  222. package/docs/api/facade-exposure-build-up.md +105 -0
  223. package/docs/api/facade-fast-methods.md +99 -0
  224. package/docs/api/facade-image-plane-enable.md +16 -0
  225. package/docs/api/facade-mixed-illumination.md +67 -0
  226. package/docs/api/facade-representative-light-sources.md +131 -0
  227. package/docs/api/facade-scene-radiance.md +54 -3
  228. package/docs/api/facade-simulate-capture.md +60 -15
  229. package/docs/api/facade-simulate-display-view.md +3 -2
  230. package/docs/api/motion-subject-motion.md +23 -0
  231. package/docs/api/optics-aperture-diameter.md +51 -0
  232. package/docs/api/optics-detail-limit.md +203 -0
  233. package/docs/api/optics-generic-lens-lateral-color.md +79 -0
  234. package/docs/api/output-crop-to-match-focal-length.md +67 -0
  235. package/docs/api/output-developed-lens-corrections.md +10 -1
  236. package/docs/api/output-generic-lens-corrections.md +40 -2
  237. package/docs/api/output-lens-corrections.md +11 -1
  238. package/docs/api/output-print-plan.md +35 -1
  239. package/docs/api/output-print-size-blur-noise.md +115 -0
  240. package/docs/api/sensor-sensor-characteristics.md +105 -0
  241. package/llms.txt +6 -0
  242. package/package.json +1 -1
@@ -25,18 +25,17 @@
25
25
  * reference, so they are exact. Every stage's wall-clock time is reported (`stageMilliseconds`); on a software adapter it
26
26
  * is not GPU performance.
27
27
  */
28
- import { AHD_KEY_LEVELS, EXACT_AREA_MEAN_MAX_PIXELS, axisSums, computeDisplayViewCorrectionStates, createDisplayViewExpectedSignal, displayViewCorrectionKey, displayViewDecidedSaturation, overlaps, renderDisplayView, ahdCandidateKernels } from "../facade/display-view.js";
28
+ import { AHD_KEY_LEVELS, EXACT_AREA_MEAN_MAX_PIXELS, axisSums, computeDisplayViewCorrectionStates, displayViewCorrectionKey, displayViewDecidedSaturation, overlaps, renderDisplayView, ahdCandidateKernels } from "../facade/display-view.js";
29
29
  import { displayViewSignalTables } from "../facade/expected-signal.js";
30
- import { resolvePortableChartScene } from "../facade/chart-scenes.js";
31
30
  import { createDisplayViewMaterialModules } from "./display-view-composite-material-module.js";
32
31
  import { setDisplayViewGpuRendererFactory } from "../facade/display-view-gpu.js";
33
32
  import { chartPresetMaterialModule } from "./display-view-chart-material-module.js";
34
- import { DISPLAY_VIEW_SPEED_CONFORMANCE_CASE, axisRecord, displayViewSpeedGpuSupports, displayViewSpeedGpuUnsupportedReason, finishDisplayViewPixel, gaussLegendre16, queryGeometry, renderDisplayViewSpeedOnGpu } from "./display-view-speed-gpu.js";
33
+ import { DISPLAY_VIEW_SPEED_CONFORMANCE_CASE, CONFORMANCE_FRAME, recomputeRuns, sceneToneAndOffsetsReason, conformanceChannelAt, conformanceChart, conformanceResponses, conformanceSetup, conformanceVerdict, withConformanceAbort, axisRecord, displayViewSpeedGpuSupports, displayViewSpeedGpuUnsupportedReason, finishDisplayViewPixel, gaussLegendre16, queryGeometry, renderDisplayViewSpeedOnGpu } from "./display-view-speed-gpu.js";
35
34
  import { createHostYieldGate } from "../capture/host-yield-cadence.js";
36
35
  import { createDefocusDiscSiteKernel } from "../optics/defocus-disc-kernel.js";
37
36
  import { ImageKernelExecutionError, WEBGPU_F32_UNIT_ROUNDOFF, webGpuExecution } from "./image-kernel-execution.js";
38
37
  import { BUFFER_USAGE, GpuResourceScope, abortedError, createComputePipeline, linearDispatch, runComputePass, throwIfAborted as checkAbort } from "./webgpu-runtime.js";
39
- import { DISPLAY_VIEW_QUALITY_AUX, DISPLAY_VIEW_QUALITY_BITMAP_WORDS, DISPLAY_VIEW_QUALITY_CANDIDATE_ENTRIES, DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS, DISPLAY_VIEW_QUALITY_FLAG, DISPLAY_VIEW_QUALITY_FLAG_WORDS, DISPLAY_VIEW_QUALITY_GEOMETRY_STRIDE, DISPLAY_VIEW_QUALITY_GROUP_WORDS, DISPLAY_VIEW_QUALITY_MAXIMUM_PIXELS_PER_AXIS, DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS, DISPLAY_VIEW_QUALITY_PARAM_WORDS, DISPLAY_VIEW_QUALITY_PIXELS_WGSL, DISPLAY_VIEW_QUALITY_SITES_BLOCKED_WGSL, DISPLAY_VIEW_QUALITY_SITES_WGSL, DISPLAY_VIEW_QUALITY_SITE_FLOATS, DISPLAY_VIEW_QUALITY_TAP_WORDS, DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE, assembleDisplayViewQualityLatticeWgsl } from "./wgsl-display-view-quality-kernel.js";
38
+ import { DISPLAY_VIEW_QUALITY_AUX, DISPLAY_VIEW_QUALITY_BITMAP_WORDS, DISPLAY_VIEW_QUALITY_CANDIDATE_ENTRIES, DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS, DISPLAY_VIEW_QUALITY_FLAG, DISPLAY_VIEW_QUALITY_FLAG_WORDS, DISPLAY_VIEW_QUALITY_GEOMETRY_STRIDE, DISPLAY_VIEW_QUALITY_GROUP_WORDS, DISPLAY_VIEW_QUALITY_MAXIMUM_NODES, DISPLAY_VIEW_QUALITY_MAXIMUM_PIXELS_PER_AXIS, DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS, DISPLAY_VIEW_QUALITY_PARAM_WORDS, DISPLAY_VIEW_QUALITY_PIXELS_WGSL, DISPLAY_VIEW_QUALITY_SITES_BLOCKED_WGSL, DISPLAY_VIEW_QUALITY_SITES_WGSL, DISPLAY_VIEW_QUALITY_SITE_FLOATS, DISPLAY_VIEW_QUALITY_TAP_WORDS, DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE, assembleDisplayViewQualityLatticeWgsl } from "./wgsl-display-view-quality-kernel.js";
40
39
  /** One WGSL rounding of + − × in either direction (W3C WGSL "Floating Point Accuracy"; #471, #487): twice the binary32 unit roundoff. */
41
40
  const E = 2 * WEBGPU_F32_UNIT_ROUNDOFF;
42
41
  /** Every reported bound is doubled: the kernel's running bounds are first order, and their own float32 arithmetic rounds. */
@@ -62,7 +61,7 @@ export const DISPLAY_VIEW_QUALITY_MAXIMUM_KERNEL_RADIUS = 96;
62
61
  * several invocations per site is needed.
63
62
  */
64
63
  export const DISPLAY_VIEW_QUALITY_SITE_TAPS_PER_DISPATCH = 2 ** 30;
65
- /** The smallest positive normal float32: table values below it may be flushed to zero by a device (WGSL allows it). */
64
+ /** The smallest positive normal float32: values below it may be flushed to zero by a device (WGSL allows it). */
66
65
  const FLOAT32_MINIMUM_NORMAL = 2 ** -126;
67
66
  /** Candidate table entries per phase: the (2·reach + 1)² window. */
68
67
  const WINDOW_SLOTS = (2 * MAXIMUM_REACH + 1) ** 2;
@@ -154,7 +153,7 @@ function exactLatticeConstant(c, span) {
154
153
  export function displayViewQualityGpuUnsupportedReason(setup, signalInput) {
155
154
  if (signalInput.largeKernel)
156
155
  return LARGE_DEFOCUS_REASON;
157
- if (signalInput.scene.kind !== "materials")
156
+ if (signalInput.scene.kind !== "materials" && !signalInput.scene.image)
158
157
  return "radiance scenes (caller functions) are not on the GPU";
159
158
  if (!signalInput.kernels && !signalInput.queryPoint)
160
159
  return "the expected signal has no query points";
@@ -164,6 +163,8 @@ export function displayViewQualityGpuUnsupportedReason(setup, signalInput) {
164
163
  return "unsupported tone curve " + String(setup.toneCurve);
165
164
  if (setup.region.width < 1 || setup.region.height < 1)
166
165
  return "an empty region";
166
+ if (signalInput.wavelengthsNm.length < 1 || signalInput.wavelengthsNm.length > DISPLAY_VIEW_QUALITY_MAXIMUM_NODES)
167
+ return "the site passes integrate 1 to " + DISPLAY_VIEW_QUALITY_MAXIMUM_NODES + " spectral nodes per site";
167
168
  if (signalInput.kernels && (signalInput.kernels.radius < 0 || signalInput.kernels.radius > DISPLAY_VIEW_QUALITY_MAXIMUM_KERNEL_RADIUS))
168
169
  return "a site kernel radius above " + DISPLAY_VIEW_QUALITY_MAXIMUM_KERNEL_RADIUS + " (the diffraction and defocus caps)";
169
170
  const shape = footprintShape(setup);
@@ -209,9 +210,9 @@ export function displayViewQualityGpuUnsupportedReason(setup, signalInput) {
209
210
  }
210
211
  return null;
211
212
  }
212
- function signalGather(signalInput, factored) {
213
- const tables = displayViewSignalTables(signalInput), scene = signalInput.scene;
214
- const W = signalInput.nativeWidth, H = signalInput.nativeHeight, R = signalInput.kernels ? signalInput.kernels.radius : 0, materials = scene.materials.length;
213
+ function signalGather(signalInput) {
214
+ const tables = displayViewSignalTables(signalInput), A = signalInput.electronsPerRadiance, N = signalInput.wavelengthsNm.length, spectra = tables.spectra;
215
+ const W = signalInput.nativeWidth, H = signalInput.nativeHeight, R = signalInput.kernels ? signalInput.kernels.radius : 0, materials = spectra.length;
215
216
  // Taps in order: per group, with site kernels the dense rectangle row-major (tap (dx, dy) reads lattice offset (minIx − R + dx,
216
217
  // minIy − R + dy)), without them one tap per member. Plain loops: a radius-96 kernel has up to 150 544 taps.
217
218
  const tapGroup = [], tapDp = [], tapDq = [], tapIndex = [];
@@ -239,7 +240,6 @@ function signalGather(signalInput, factored) {
239
240
  groups.push({ dpMin, dqMin, dpSpan: dpMax - dpMin, dqSpan: dqMax - dqMin, cu: 0.5 + group.fx - W / 2, cv: 0.5 + group.fy - H / 2, firstTap: first,
240
241
  cw: signalInput.kernels ? group.cw : 0, ch: signalInput.kernels ? group.ch : 0 });
241
242
  });
242
- const direct = (m, c) => { const si = tables.spectralIndex[m]; return si < 0 ? tables.reflectance[m] * tables.neutralDirect[c] : tables.spectralDirect[c][si]; };
243
243
  const T = tapGroup.length, taps = new Uint32Array(T * DISPLAY_VIEW_QUALITY_TAP_WORDS), spread = new Float32Array(3 * T);
244
244
  for (let t = 0; t < T; t++) {
245
245
  const g = tapGroup[t], group = groups[g];
@@ -247,91 +247,64 @@ function signalGather(signalInput, factored) {
247
247
  taps[t * DISPLAY_VIEW_QUALITY_TAP_WORDS + 1] = tapDp[t] - group.dpMin;
248
248
  taps[t * DISPLAY_VIEW_QUALITY_TAP_WORDS + 2] = tapDq[t] - group.dqMin;
249
249
  }
250
- const check = (value) => {
251
- if (!(Math.abs(value) <= FLOAT32_SAFE_MAGNITUDE))
252
- throw new ImageKernelExecutionError("unsupported-float32-domain", "A material's channel signal exceeds the float32 domain.");
253
- return value;
254
- };
255
- const tiny = new Float64Array(T);
256
- if (factored && signalInput.kernels) {
257
- // #492: one neutral row (the white's combined kernel, which the reference scales by each neutral material's reflectance) and one
258
- // row per spectral material; the spread of a tap over the materials from the neutral reflectances' range and the spectral rows.
259
- const spectralCount = tables.spectral.length, rows = 1 + spectralCount, table = new Float32Array(rows * 3 * T), materialRows = new Uint32Array(2 * materials);
260
- const reflectanceBits = new Float32Array(materialRows.buffer);
261
- let neutralLow = Infinity, neutralHigh = -Infinity, neutralSmallest = Infinity;
262
- for (let m = 0; m < materials; m++) {
263
- const si = tables.spectralIndex[m];
264
- if (si < 0) {
265
- const r = check(tables.reflectance[m]);
266
- materialRows[2 * m] = 0;
267
- reflectanceBits[2 * m + 1] = r;
268
- neutralLow = Math.min(neutralLow, r);
269
- neutralHigh = Math.max(neutralHigh, r);
270
- if (r > 0)
271
- neutralSmallest = Math.min(neutralSmallest, r);
272
- }
273
- else {
274
- materialRows[2 * m] = 1 + si;
275
- reflectanceBits[2 * m + 1] = 1;
276
- }
277
- }
278
- for (let t = 0; t < T; t++) {
279
- const source = tables.groups[tapGroup[t]], index = tapIndex[t];
280
- for (let c = 0; c < 3; c++) {
281
- let low = Infinity, high = -Infinity;
282
- const white = check(source.neutral[c][index]);
283
- table[c * T + t] = white;
284
- if (neutralHigh >= neutralLow) {
285
- low = neutralLow * white;
286
- high = neutralHigh * white;
287
- // A neutral term r·N below the float32 normal range may be flushed: at most min(r_max·N, 2⁻¹²⁶).
288
- if (neutralSmallest * white < FLOAT32_MINIMUM_NORMAL && white > 0)
289
- tiny[t] = Math.max(tiny[t], Math.min(neutralHigh * white, FLOAT32_MINIMUM_NORMAL));
290
- }
291
- for (let si = 0; si < spectralCount; si++) {
292
- const value = check(source.spectral[c][si][index]);
293
- table[((1 + si) * 3 + c) * T + t] = value;
294
- low = Math.min(low, value);
295
- high = Math.max(high, value);
296
- if (value > 0 && value < FLOAT32_MINIMUM_NORMAL)
297
- tiny[t] = Math.max(tiny[t], value);
298
- }
299
- spread[c * T + t] = Math.fround((high - low) * (1 + 4 * E));
300
- }
250
+ // Every value finite and non-negative, every spectrum value normal (a device may flush the others) and every sum in range.
251
+ const domain = () => { throw new ImageKernelExecutionError("unsupported-float32-domain", "A site's spectral terms are outside the float32 domain."); };
252
+ // The spectra and their envelope per node.
253
+ const base = 3 * T * N, table = new Float32Array(base + materials * N), low = new Float64Array(N).fill(Infinity), high = new Float64Array(N).fill(-Infinity);
254
+ let smallestL = Infinity;
255
+ spectra.forEach((L, m) => {
256
+ for (let n = 0; n < N; n++) {
257
+ const l = L[n];
258
+ if (!(l >= 0 && l <= FLOAT32_SAFE_MAGNITUDE))
259
+ domain();
260
+ table[base + m * N + n] = l;
261
+ low[n] = Math.min(low[n], l);
262
+ high[n] = Math.max(high[n], l);
263
+ if (l > 0)
264
+ smallestL = Math.min(smallestL, l);
301
265
  }
302
- let tinyMass = 0;
303
- for (let t = 0; t < T; t++)
304
- tinyMass += tiny[t];
305
- return { groups, taps, tapCount: T, table, materialRows, spread, materials, tinyMass };
306
- }
307
- const table = new Float32Array(materials * 3 * T);
266
+ });
267
+ if (smallestL < FLOAT32_MINIMUM_NORMAL)
268
+ domain();
269
+ let largestSum = 0, tinyMass = (T + 2) * N * FLOAT32_MINIMUM_NORMAL;
308
270
  for (let t = 0; t < T; t++) {
309
271
  const source = tables.groups[tapGroup[t]], index = tapIndex[t];
272
+ let tiny = 0;
310
273
  for (let c = 0; c < 3; c++) {
311
- let low = Infinity, high = -Infinity;
312
- for (let m = 0; m < materials; m++) {
313
- const si = tables.spectralIndex[m];
314
- const value = check(signalInput.kernels ? (si < 0 ? tables.reflectance[m] * source.neutral[c][index] : source.spectral[c][si][index]) : source.members[index].weight * direct(m, c));
315
- table[(m * 3 + c) * T + t] = value;
316
- low = Math.min(low, value);
317
- high = Math.max(high, value);
318
- if (value > 0 && value < FLOAT32_MINIMUM_NORMAL)
319
- tiny[t] = Math.max(tiny[t], value);
274
+ let range = 0, mass = 0, tinyHere = 0;
275
+ for (let n = 0; n < N; n++) {
276
+ const value = signalInput.kernels ? source.nodeKernels[c][index * N + n] : source.members[index].weight * A[c][n];
277
+ if (!(value >= 0 && value <= FLOAT32_SAFE_MAGNITUDE))
278
+ domain();
279
+ table[(c * T + t) * N + n] = value;
280
+ if (materials > 0) {
281
+ const top = value * high[n];
282
+ range += value * (high[n] - low[n]);
283
+ mass += top;
284
+ // A kernel value or product below the normal range may be flushed: at most the product with the largest spectrum.
285
+ if (value > 0 && (value < FLOAT32_MINIMUM_NORMAL || top < 2 * FLOAT32_MINIMUM_NORMAL))
286
+ tinyHere += top;
287
+ }
320
288
  }
321
- // The spread rounded up (a float32 rounding of the two values and of the difference).
322
- spread[c * T + t] = Math.fround((high - low) * (1 + 4 * E));
289
+ // The spread rounded up (the float32 roundings of the operands, the products and the sum).
290
+ spread[c * T + t] = Math.fround(range * (1 + (N + 4) * E));
291
+ largestSum = Math.max(largestSum, mass);
292
+ tiny = Math.max(tiny, tinyHere);
323
293
  }
294
+ tinyMass += tiny;
324
295
  }
325
- let tinyMass = 0;
326
- for (let t = 0; t < T; t++)
327
- tinyMass += tiny[t];
328
- return { groups, taps, tapCount: T, table, materialRows: null, spread, materials, tinyMass };
296
+ if (!(largestSum * T <= FLOAT32_SAFE_MAGNITUDE))
297
+ domain();
298
+ return { groups, taps, tapCount: T, nodes: N, table, spread, materials, tinyMass };
329
299
  }
330
300
  /**
331
301
  * The relative bound of a site's electrons (`params.site_gamma`): (T + 3)·E for the tap-list sum (#479), and with the blocked sum
332
- * (#492) (cw_max + ch_max + G + 3)·E: a term carries two host roundings (u = E/2 each: its row value and the reflectance), the
333
- * product (E), at most cw additions in its row, ch in its group and G in the total, and the dark electrons' addition, all of
334
- * non-negative terms (docs/WEBGPU.md).
302
+ * (#492) (cw_max + ch_max + G + 3)·E, both unchanged by 2.21.0. Every term C_c(t, n)·L_m(λ_n) is non-negative and carries two host
303
+ * roundings (u = E/2 each: the kernel value and the spectrum) and its product (E); the tap-list pass adds it into its node's
304
+ * partial over the T taps (at most T − 1 inexact additions), the blocked pass into a row, group and total partial per node (at
305
+ * most cw − 1 + ch − 1 + G − 1). The N node partials and the dark electrons (host-rounded, E/2) are then summed in integer
306
+ * arithmetic and rounded once (E, `EXACT_SUM_WGSL`). So a site carries at most (T + 2.5)·E, or (cw + ch + G + 0.5)·E, inside the
307
+ * bounds (docs/WEBGPU.md).
335
308
  */
336
309
  function siteSumRelativeBound(gather, blocked) {
337
310
  if (!blocked)
@@ -406,8 +379,7 @@ function resolveUncertainLattice(lattice, groupWords, signalInput) {
406
379
  if ((raw >>> 31) === 0)
407
380
  continue;
408
381
  const p = lx0 + (local % lw), q = ly0 + Math.floor(local / lw);
409
- const u = group.fx === 0 ? (p + 0.5 - W / 2) * g : (p + 0.5 + group.fx - W / 2) * g, v = group.fy === 0 ? -(q + 0.5 - H / 2) * g : -(q + 0.5 + group.fy - H / 2) * g;
410
- const m = scene.materialAt(u, v);
382
+ const m = scene.materialAt(latticeCoordinate(p, group.fx, W, g), -latticeCoordinate(q, group.fy, H, g));
411
383
  if (!(Number.isInteger(m) && m >= 0 && m < scene.materials.length))
412
384
  throw new ImageKernelExecutionError("unsupported-on-backend", "webgpu-f32 display view (Quality): a lattice point has no scene material; use cpu-reference.");
413
385
  lattice[first + local] = m;
@@ -416,6 +388,11 @@ function resolveUncertainLattice(lattice, groupWords, signalInput) {
416
388
  });
417
389
  return resolved;
418
390
  }
391
+ /**
392
+ * One axis of the reference's lattice point (createDisplayViewExpectedSignal's latticeU / latticeV, written as it writes them):
393
+ * (i + 0.5 − size/2)·g with no shift, (i + 0.5 + f − size/2)·g otherwise; v is its negative.
394
+ */
395
+ export const latticeCoordinate = (i, f, size, g) => (f === 0 ? i + 0.5 - size / 2 : i + 0.5 + f - size / 2) * g;
419
396
  /** The uniform parameters shared by the three passes (per-pass words are set per dispatch). */
420
397
  function constants(setup, signalInput, gather, reach, count, blocked) {
421
398
  const params = new ArrayBuffer(4 * DISPLAY_VIEW_QUALITY_PARAM_WORDS), words = new Uint32Array(params), ints = new Int32Array(params), floats = new Float32Array(params);
@@ -458,9 +435,10 @@ function constants(setup, signalInput, gather, reach, count, blocked) {
458
435
  floats[49] = geometry.scaleV;
459
436
  }
460
437
  floats[50] = siteSumRelativeBound(gather, blocked);
461
- // #492: the table values a device may flush to zero (blocked sum only), rounded up and kept normal so the uniform is not flushed either.
462
- if (blocked && gather.tinyMass > 0)
463
- floats[52] = Math.max(FLOAT32_MINIMUM_NORMAL, Math.fround(gather.tinyMass * (1 + 4 * E)));
438
+ // 2.21.0: the spectral nodes the site passes integrate.
439
+ words[53] = gather.nodes;
440
+ // What a flushing device can drop (both site passes), rounded up and kept normal so the uniform is not flushed either.
441
+ floats[52] = Math.max(FLOAT32_MINIMUM_NORMAL, Math.fround(gather.tinyMass * (1 + 4 * E)));
464
442
  return { params, words, ints, floats };
465
443
  }
466
444
  /** The reference's correction setup of a display-view setup with the AHD correction. */
@@ -479,7 +457,7 @@ function quantizationBound(moments, o, meanBound, pixels, step) {
479
457
  }
480
458
  /** Renders one region of a Quality display view on the GPU (see the module comment). */
481
459
  export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, abort, yieldControl, pipelines) {
482
- const { setup, signalInput, material } = input;
460
+ const { setup, signalInput, material, image } = input;
483
461
  const reason = displayViewQualityGpuUnsupportedReason(setup, signalInput);
484
462
  if (reason)
485
463
  throw new ImageKernelExecutionError("unsupported-on-backend", "webgpu-f32 display view (Quality): " + reason + "; use cpu-reference.");
@@ -487,11 +465,16 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
487
465
  const timings = { lattice: 0, sites: 0, pixels: 0, states: 0, finishing: 0, recompute: 0, pixelRedispatches: 0, siteDispatches: 0 };
488
466
  const { region, margin } = setup, shape = footprintShape(setup), { reach, count } = candidatesOf(setup);
489
467
  // #492: the defocus disc's kernels are summed in blocks (their dense rectangles), in focus the tap list runs as before.
490
- const blocked = input.blockedSum ?? (setup.gpu?.defocusRadius ?? 0) > 0;
491
- if (blocked && !signalInput.kernels)
468
+ const blocked = !!image || (input.blockedSum ?? (setup.gpu?.defocusRadius ?? 0) > 0);
469
+ if (blocked && !signalInput.kernels && !image)
492
470
  throw new ImageKernelExecutionError("unsupported-on-backend", "webgpu-f32 display view (Quality): the blocked site sum needs site kernels; use cpu-reference.");
493
471
  const tapBudget = input.maximumSiteTapsPerDispatch ?? DISPLAY_VIEW_QUALITY_SITE_TAPS_PER_DISPATCH;
494
- const gather = signalGather(signalInput, blocked), aux = auxTable(setup), { params, words, ints, floats } = constants(setup, signalInput, gather, reach, count, blocked);
472
+ const gather = image?.gather ?? signalGather(signalInput), aux = auxTable(setup), { params, words, ints, floats } = constants(setup, signalInput, gather, reach, count, blocked);
473
+ // #581: the print's site bound and its radiance values per lattice point (the site pass's node count in the last word).
474
+ if (image) {
475
+ floats[50] = image.siteGamma;
476
+ words[55] = image.pointFloats;
477
+ }
495
478
  const stride = DISPLAY_VIEW_QUALITY_GEOMETRY_STRIDE, geometry = new Float32Array((region.width + region.height) * stride);
496
479
  const columnSums = [], rowSums = [];
497
480
  for (let i = 0; i < region.width; i++) {
@@ -512,7 +495,7 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
512
495
  const { y0, y1 } = siteRows(0, rows), siteH = y1 - y0 + 1;
513
496
  let lattice = 0;
514
497
  for (const g of gather.groups)
515
- lattice += (siteW + g.dpSpan) * (siteH + g.dqSpan) * 4;
498
+ lattice += (siteW + g.dpSpan) * (siteH + g.dqSpan) * 4 * (image ? image.pointFloats + 1 : 1);
516
499
  return lattice + siteW * siteH * SITE_BYTES + rows * region.width * OUTPUT_BYTES_PER_PIXEL;
517
500
  };
518
501
  let rows = region.height;
@@ -521,14 +504,13 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
521
504
  // The value table (materials × 3 channels × taps) is one binding: with a large defocus kernel and many materials it can pass the
522
505
  // device's limit, which is then refused by name (never clipped).
523
506
  if (gather.table.byteLength > runtime.maximumStorageBindingBytes)
524
- throw new ImageKernelExecutionError("insufficient-limits", "The display view's kernel table (" +
525
- (gather.materialRows ? gather.table.length / (3 * gather.tapCount) + " rows (the neutral white and each spectral material)" : gather.materials + " materials") + " × 3 channels × " +
526
- gather.tapCount + " taps per site, " + gather.table.byteLength + " bytes) exceeds the device's storage binding limit of " + runtime.maximumStorageBindingBytes + " bytes.");
507
+ throw new ImageKernelExecutionError("insufficient-limits", "The display view's kernel table (" + gather.table.byteLength +
508
+ " bytes: 3 channels × " + gather.tapCount + " taps × " + gather.nodes + " spectral nodes, and the material spectra) exceeds the device's storage binding limit of " + runtime.maximumStorageBindingBytes + " bytes.");
527
509
  const largestBinding = Math.max(bandBytesOf(rows), geometry.byteLength, gather.table.byteLength, 4 * DISPLAY_VIEW_QUALITY_BITMAP_WORDS);
528
510
  if (largestBinding > runtime.maximumStorageBindingBytes)
529
511
  throw new ImageKernelExecutionError("insufficient-limits", "One display-view band exceeds the device's storage binding limit.");
530
- const latticePipeline = await pipelines("photivra-display-view-quality-lattice:" + material.id, assembleDisplayViewQualityLatticeWgsl(material.wgsl));
531
- const sitesPipeline = blocked ? await pipelines("photivra-display-view-quality-sites-blocked", DISPLAY_VIEW_QUALITY_SITES_BLOCKED_WGSL) :
512
+ const latticePipeline = image ? await pipelines(...image.lattice) : await pipelines("photivra-display-view-quality-lattice:" + material.id, assembleDisplayViewQualityLatticeWgsl(material.wgsl));
513
+ const sitesPipeline = image ? await pipelines(...image.sites) : blocked ? await pipelines("photivra-display-view-quality-sites-blocked", DISPLAY_VIEW_QUALITY_SITES_BLOCKED_WGSL) :
532
514
  await pipelines("photivra-display-view-quality-sites", DISPLAY_VIEW_QUALITY_SITES_WGSL);
533
515
  const pixelsPipeline = await pipelines("photivra-display-view-quality-pixels", DISPLAY_VIEW_QUALITY_PIXELS_WGSL);
534
516
  const moments = new Float32Array(pixelCount * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS), pixelFlags = new Uint32Array(pixelCount), masks = new Uint32Array(2 * pixelCount);
@@ -542,11 +524,9 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
542
524
  const tapsBuffer = blocked ? null : scope.upload("photivra-display-view-taps", gather.taps, BUFFER_USAGE.STORAGE);
543
525
  const tableBuffer = scope.upload("photivra-display-view-table", gather.table, BUFFER_USAGE.STORAGE);
544
526
  const spreadBuffer = scope.upload("photivra-display-view-spread", gather.spread, BUFFER_USAGE.STORAGE);
545
- const materialRowsBuffer = gather.materialRows ? scope.upload("photivra-display-view-material-rows", gather.materialRows, BUFFER_USAGE.STORAGE) : null;
546
527
  // A module's binding is data uploaded per render or a buffer the module already holds on the device (#491: scene records).
547
- const materialEntries = material.bindings.map((b) => ({ binding: b.binding, resource: { buffer: b.buffer ??
548
- scope.upload("photivra-display-view-material-" + b.binding, b.data, b.kind === "uniform" ? BUFFER_USAGE.UNIFORM : BUFFER_USAGE.STORAGE) } }));
549
- const materialGroup = runtime.device.createBindGroup({ layout: latticePipeline.getBindGroupLayout(1), entries: materialEntries });
528
+ const materialGroup = image ? null : runtime.device.createBindGroup({ layout: latticePipeline.getBindGroupLayout(1), entries: material.bindings.map((b) => ({ binding: b.binding, resource: { buffer: b.buffer ??
529
+ scope.upload("photivra-display-view-material-" + b.binding, b.data, b.kind === "uniform" ? BUFFER_USAGE.UNIFORM : BUFFER_USAGE.STORAGE) } })) });
550
530
  const gate = createHostYieldGate();
551
531
  for (let row0 = 0; row0 < region.height; row0 += rows) {
552
532
  checkAbort(abort.signal);
@@ -576,26 +556,35 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
576
556
  const band = new GpuResourceScope(runtime);
577
557
  try {
578
558
  const groupBuffer = band.upload("photivra-display-view-groups", groupWords, BUFFER_USAGE.STORAGE);
579
- let latticeBuffer = band.buffer("photivra-display-view-lattice", latticeTotal * 4, BUFFER_USAGE.STORAGE | (blocked ? BUFFER_USAGE.COPY_SRC : 0));
580
559
  const siteBuffer = band.buffer("photivra-display-view-sites", siteW * siteH * SITE_BYTES, BUFFER_USAGE.STORAGE);
581
- // Pass 1: lattice materials.
560
+ // Pass 1: lattice materials (#581: an image-plane print's radiance per node, with its flags, and their host resolution).
582
561
  let t0 = now();
583
562
  words[0] = latticeTotal;
584
563
  const latticeParams = band.upload("photivra-display-view-params-lattice", new Uint8Array(params.slice(0)), BUFFER_USAGE.UNIFORM);
585
- const latticeGroup = runtime.device.createBindGroup({ layout: latticePipeline.getBindGroupLayout(0), entries: [
586
- { binding: 0, resource: { buffer: latticeParams } }, { binding: 1, resource: { buffer: groupBuffer } }, { binding: 2, resource: { buffer: latticeBuffer } }
587
- ] });
588
- const [latticeData] = await runComputePass(runtime, band, latticePipeline, [latticeGroup, materialGroup], linearDispatch(latticeTotal, DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE, runtime.maximumWorkgroupsPerDimension), blocked ? [{ buffer: latticeBuffer, bytes: latticeTotal * 4 }] : [], abort.signal);
589
- await queueIdle(runtime, abort.signal);
590
- if (blocked) {
591
- // #492: with a wide disc nearly every site reads some uncertain lattice point, and its spread over every material would flag
592
- // it. The host resolves each uncertain point with the reference's own material lookup at the reference's lattice point
593
- // (createDisplayViewExpectedSignal's latticeU/latticeV), so the sites read exactly the reference's materials.
594
- const lattice = new Uint32Array(latticeData, 0, latticeTotal);
595
- const resolved = resolveUncertainLattice(lattice, groupWords, signalInput);
596
- if (resolved > 0)
597
- latticeBuffer = band.upload("photivra-display-view-lattice-resolved", lattice, BUFFER_USAGE.STORAGE);
598
- latticePointsResolved += resolved;
564
+ let latticeBuffer, pointFlags = null;
565
+ if (image) {
566
+ const pass = await image.runLattice(band, latticePipeline, latticeParams, groupBuffer, groupWords, latticeTotal, abort.signal);
567
+ latticeBuffer = pass.values;
568
+ pointFlags = pass.flags;
569
+ latticePointsResolved += pass.resolved;
570
+ }
571
+ else {
572
+ latticeBuffer = band.buffer("photivra-display-view-lattice", latticeTotal * 4, BUFFER_USAGE.STORAGE | (blocked ? BUFFER_USAGE.COPY_SRC : 0));
573
+ const latticeGroup = runtime.device.createBindGroup({ layout: latticePipeline.getBindGroupLayout(0), entries: [
574
+ { binding: 0, resource: { buffer: latticeParams } }, { binding: 1, resource: { buffer: groupBuffer } }, { binding: 2, resource: { buffer: latticeBuffer } }
575
+ ] });
576
+ const [latticeData] = await runComputePass(runtime, band, latticePipeline, [latticeGroup, materialGroup], linearDispatch(latticeTotal, DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE, runtime.maximumWorkgroupsPerDimension), blocked ? [{ buffer: latticeBuffer, bytes: latticeTotal * 4 }] : [], abort.signal);
577
+ await queueIdle(runtime, abort.signal);
578
+ if (blocked) {
579
+ // #492: with a wide disc nearly every site reads some uncertain lattice point, and its spread over every material would flag
580
+ // it. The host resolves each uncertain point with the reference's own material lookup at the reference's lattice point
581
+ // (createDisplayViewExpectedSignal's latticeU/latticeV), so the sites read exactly the reference's materials.
582
+ const lattice = new Uint32Array(latticeData, 0, latticeTotal);
583
+ const resolved = resolveUncertainLattice(lattice, groupWords, signalInput);
584
+ if (resolved > 0)
585
+ latticeBuffer = band.upload("photivra-display-view-lattice-resolved", lattice, BUFFER_USAGE.STORAGE);
586
+ latticePointsResolved += resolved;
587
+ }
599
588
  }
600
589
  timings.lattice += now() - t0;
601
590
  // Pass 2: site moments, in chunks of whole workgroups of sites whose taps stay within the budget per dispatch (#492).
@@ -610,8 +599,7 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
610
599
  const siteParams = chunk.upload("photivra-display-view-params-sites", new Uint8Array(params.slice(0)), BUFFER_USAGE.UNIFORM);
611
600
  const siteGroup = runtime.device.createBindGroup({ layout: sitesPipeline.getBindGroupLayout(0), entries: [
612
601
  { binding: 0, resource: { buffer: siteParams } }, { binding: 1, resource: { buffer: groupBuffer } }, ...(tapsBuffer ? [{ binding: 2, resource: { buffer: tapsBuffer } }] : []),
613
- { binding: 3, resource: { buffer: tableBuffer } }, { binding: 4, resource: { buffer: latticeBuffer } }, { binding: 5, resource: { buffer: siteBuffer } }, { binding: 6, resource: { buffer: spreadBuffer } },
614
- ...(materialRowsBuffer ? [{ binding: 7, resource: { buffer: materialRowsBuffer } }] : [])
602
+ { binding: 3, resource: { buffer: tableBuffer } }, { binding: 4, resource: { buffer: latticeBuffer } }, { binding: 5, resource: { buffer: siteBuffer } }, { binding: 6, resource: { buffer: pointFlags ?? spreadBuffer } },
615
603
  ] });
616
604
  await runComputePass(runtime, chunk, sitesPipeline, siteGroup, linearDispatch(words[0], DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE, runtime.maximumWorkgroupsPerDimension), [], abort.signal);
617
605
  await queueIdle(runtime, abort.signal);
@@ -754,22 +742,11 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
754
742
  for (let i = 0; i < pixelCount; i++)
755
743
  if (isFlagged[i] === 0 && (pixelFlags[i] & DISPLAY_VIEW_QUALITY_FLAG.highlightUndecided) !== 0)
756
744
  undecided.push(i);
757
- if (undecided.length > 0 && input.recomputeFlagged !== false) {
758
- const signal = createDisplayViewExpectedSignal(signalInput);
759
- for (let r = 0; r < undecided.length;) {
760
- checkAbort(abort.signal);
761
- const first = undecided[r], rx = first % region.width, ry = (first - rx) / region.width;
762
- let run = 1;
763
- while (r + run < undecided.length && undecided[r + run] === first + run && rx + run < region.width)
764
- run++;
765
- const part = await renderDisplayView({ ...setup, signal, region: { x: region.x + rx, y: region.y + ry, width: run, height: 1 } });
766
- for (let k = 0; k < run; k++) {
767
- saturated[first + k] = part.saturated[k];
768
- highlights.push(first + k);
769
- }
770
- r += run;
771
- }
772
- }
745
+ if (input.recomputeFlagged !== false)
746
+ await recomputeRuns(setup, signalInput, undecided, abort, (first, run, part) => { for (let k = 0; k < run; k++) {
747
+ saturated[first + k] = part.saturated[k];
748
+ highlights.push(first + k);
749
+ } });
773
750
  if (flagged.length > 0) {
774
751
  if (input.recomputeFlagged === false) {
775
752
  // Without recomputation a flagged pixel keeps the kernel's values where they are finite (its bound then does not hold).
@@ -787,18 +764,9 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
787
764
  varianceBound[i] = MARGIN * moments[o + 10] + quantizationBound(moments, o, meanBound[i], pixels, step);
788
765
  }
789
766
  }
790
- else {
791
- const signal = createDisplayViewExpectedSignal(signalInput);
792
- // Runs of consecutive flagged pixels along a row are rendered as one region (#457: AHD's choice flags stretches near
793
- // edges); a region render is byte-identical to the same pixels of the whole view, so each pixel equals its one-pixel
794
- // render, and the run's counts are the sums of its pixels' counts.
795
- for (let r = 0; r < flagged.length;) {
796
- checkAbort(abort.signal);
797
- const first = flagged[r], rx = first % region.width, ry = (first - rx) / region.width;
798
- let run = 1;
799
- while (r + run < flagged.length && flagged[r + run] === first + run && rx + run < region.width)
800
- run++;
801
- const part = await renderDisplayView({ ...setup, signal, region: { x: region.x + rx, y: region.y + ry, width: run, height: 1 } });
767
+ else
768
+ await recomputeRuns(setup, signalInput, flagged, abort, (first, run, part) => {
769
+ // The run's counts are the sums of its pixels' counts.
802
770
  samples.set(part.samples, first * 3);
803
771
  expected.set(part.expected, first * 3);
804
772
  noiseStd.set(part.noiseStd, first * 3);
@@ -811,9 +779,7 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
811
779
  corrected += part.ahdCorrectedPixelCount;
812
780
  sampleCount += part.sampleCount;
813
781
  siteCount += part.siteCount;
814
- r += run;
815
- }
816
- }
782
+ });
817
783
  }
818
784
  timings.recompute = now() - t0;
819
785
  const relative = MARGIN * CPU_RELATIVE;
@@ -845,8 +811,8 @@ export function qualityMaterialSetup(gpu) {
845
811
  const offsets = groups.map((group) => ({ x: group.fx, y: group.fy, weight: 1 / groups.length }));
846
812
  const queryPoint = (x, y, o, out) => {
847
813
  const group = groups[o];
848
- out[0] = (group.fx === 0 ? (x + 0.5 - W / 2) : (x + 0.5 + group.fx - W / 2)) * g;
849
- out[1] = -(group.fy === 0 ? (y + 0.5 - H / 2) : (y + 0.5 + group.fy - H / 2)) * g;
814
+ out[0] = latticeCoordinate(x, group.fx, W, g);
815
+ out[1] = -latticeCoordinate(y, group.fy, H, g);
850
816
  };
851
817
  const { kernels: _kernels, ...rest } = input;
852
818
  void _kernels;
@@ -867,13 +833,7 @@ export function offsetCount(request) { return request.boxSamples * request.boxSa
867
833
  * table fits the device's storage binding limit, is known only per render.
868
834
  */
869
835
  export function displayViewQualityGpuSupports(request) {
870
- if (request.scene !== "chart" && request.scene !== "material-layers")
871
- return "the scene is a caller " + request.scene + "; the GPU material lookup covers engine charts and declared material-layers scenes (describe the scene as material-layers)";
872
- if (request.toneCurve !== "identity" && request.toneCurve !== "positive-reinhard-per-channel")
873
- return "unsupported tone curve " + String(request.toneCurve);
874
- if (offsetCount(request) > MAXIMUM_GROUPS)
875
- return "more than " + MAXIMUM_GROUPS + " query offsets per site";
876
- return true;
836
+ return sceneToneAndOffsetsReason(request, MAXIMUM_GROUPS);
877
837
  }
878
838
  /** A normalized, symmetric site kernel of radius R for the self-check (a binned Gaussian of width `sigma` sites). */
879
839
  function syntheticKernel(radius, sigma) {
@@ -902,59 +862,28 @@ function syntheticKernel(radius, sigma) {
902
862
  * each band's site pass over several dispatches.
903
863
  */
904
864
  async function runQualityConformance(handle, signal, defocus) {
905
- const runtime = handle;
906
- const nativeWidth = 40, nativeHeight = 28, margin = 1, frameWidth = 38, frameHeight = 26, nodes = [450, 550, 650];
907
- const chart = resolvePortableChartScene({ scene: { preset: "checkerboard", squaresAcross: 6 }, aspect: frameHeight / frameWidth });
865
+ const runtime = handle, { nativeWidth, nativeHeight, frameWidth, nodes } = CONFORMANCE_FRAME, { chart, scene } = conformanceChart();
908
866
  const offsets = [];
909
867
  for (const [nx, ny] of [[-0.25, -0.25], [0.25, -0.25], [-0.25, 0.25], [0.25, 0.25]])
910
868
  for (const [ax, ay] of [[-0.5, -0.5], [0.5, -0.5], [-0.5, 0.5], [0.5, 0.5]])
911
869
  offsets.push({ x: nx - ax, y: ny - ay, weight: 1 / 16 });
912
870
  const disc = defocus ? createDefocusDiscSiteKernel(6.3) : null;
913
871
  const kernels = disc ? { radius: disc.radius, perNode: nodes.map(() => disc.weights) } : { radius: 1, perNode: [syntheticKernel(1, 0.6), syntheticKernel(1, 0.7), syntheticKernel(1, 0.8)] };
914
- const signalInput = { nativeWidth, nativeHeight, outputWidth: frameWidth, wavelengthsNm: nodes,
915
- electronsPerRadiance: defocus ? [Float64Array.of(9000, 30000, 4000), Float64Array.of(6000, 42000, 9000), Float64Array.of(30000, 9000, 3000)] :
916
- [Float64Array.of(900, 3000, 400), Float64Array.of(600, 4200, 900), Float64Array.of(3000, 900, 300)], offsets, kernels,
917
- scene: { kind: "materials", materialAt: (u, v) => chart.materialAt(u, v), materials: chart.materials.map((m) => ({ neutral: m.kind === "neutral" ? m.reflectance : 0.5 })), whiteRadiance: Float64Array.of(0.5, 0.9, 0.7) },
918
- channelAt: (x, y) => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2)) };
919
- const readout = { perElectron: defocus ? 1 / 40000 : 1 / 4000, darkElectrons: 0.07, constantVariance: 2.5e-7 };
920
- const bilinear = bilinearKernels();
921
- const setup = { frameWidth, frameHeight, margin, nativeWidth, nativeHeight, viewWidth: 13, viewHeight: 9, region: { x: 0, y: 0, width: 13, height: 9 },
922
- samplesPerAxis: 16, reachSites: 5 + (disc ? disc.radius : 0), bitDepth: 8, seed: 479, signal: createDisplayViewExpectedSignal(signalInput), readout: [readout, readout, readout], demosaic: bilinear,
923
- ...(defocus ? {} : { candidates: [0, 1, 2, 3].map((p) => ahdCandidateKernels(p % 2, Math.floor(p / 2))),
924
- ahdCorrection: { labMatrix: [[0.4124, 0.3576, 0.1805], [0.2126, 0.7152, 0.0722], [0.0193, 0.1192, 0.9505]].map((row) => row.map((v, c) => v * [1.1, 1, 1.6][c])), drawsPerState: 64, thresholdVariance: 1e-4 } }),
925
- gains: [1.1, 1, 1.6], clipLevel: 1, matrix: [[3.08, -1.54, -0.54], [-0.92, 1.88, 0.045], [0.053, -0.2, 1.15]], toneCurve: "identity",
926
- yieldControl: async () => undefined, checkAbort: () => { if (signal?.aborted)
927
- throw abortedError(); } };
872
+ const signalInput = { nativeWidth, nativeHeight, outputWidth: frameWidth, wavelengthsNm: nodes, electronsPerRadiance: conformanceResponses(defocus ? 1 : 0.1),
873
+ offsets, kernels, scene, channelAt: conformanceChannelAt };
874
+ const setup = conformanceSetup(signalInput, { perElectron: defocus ? 1 / 40000 : 1 / 4000, darkElectrons: 0.07, constantVariance: 2.5e-7 }, 479, 5 + (disc ? disc.radius : 0), signal, defocus ? {} : { candidates: [0, 1, 2, 3].map((p) => ahdCandidateKernels(p % 2, Math.floor(p / 2))),
875
+ ahdCorrection: { labMatrix: [[0.4124, 0.3576, 0.1805], [0.2126, 0.7152, 0.0722], [0.0193, 0.1192, 0.9505]].map((row) => row.map((v, c) => v * [1.1, 1, 1.6][c])), drawsPerState: 64, thresholdVariance: 1e-4 } });
928
876
  const reference = await renderDisplayView(setup);
929
- const abort = new AbortController(), onAbort = () => abort.abort();
930
- signal?.addEventListener("abort", onAbort, { once: true });
931
- try {
932
- if (signal?.aborted)
933
- abort.abort();
877
+ return await withConformanceAbort(runtime, signal, async (abort, pipelines) => {
934
878
  const gather = defocus ? { blockedSum: true, maximumSiteTapsPerDispatch: 64 * 4 * (2 * 3 + 2) ** 2 } : {};
935
- const gpu = await renderDisplayViewQualityOnGpu(runtime, 1 << 20, { setup, signalInput, material: chartPresetMaterialModule(chart), ...gather }, abort, undefined, (label, code) => createComputePipeline(runtime, label, code, abort.signal));
879
+ const gpu = await renderDisplayViewQualityOnGpu(runtime, 1 << 20, { setup, signalInput, material: chartPresetMaterialModule(chart), ...gather }, abort, undefined, pipelines);
936
880
  if (!defocus && (gpu.ahdCorrectedPixelCount === 0 || reference.ahdCorrectedPixelCount === 0))
937
881
  return "the self-check did not exercise the low-signal correction.";
938
882
  if (defocus && (!gpu.blockedSum || gpu.stageMilliseconds.siteDispatches < 2))
939
883
  return "the self-check did not exercise the blocked site sum over several dispatches.";
940
- for (let i = 0; i < reference.expected.length; i++) {
941
- const p = Math.floor(i / 3), e0 = reference.expected[i], e1 = gpu.expected[i], v0 = reference.noiseStd[i] * reference.noiseStd[i], v1 = gpu.noiseStd[i] * gpu.noiseStd[i];
942
- const expectedLimit = gpu.expectedValueBound.relative * Math.abs(e0) + gpu.expectedValueAbsoluteBound[p], varianceLimit = gpu.varianceBound.relative * v0 + gpu.varianceAbsoluteBound[p];
943
- if (!(Math.abs(e1 - e0) <= expectedLimit))
944
- return "component " + i + ": expected value " + e1 + " differs from the reference " + e0 + " by more than the a-priori bound " + expectedLimit + ".";
945
- if (!(Math.abs(v1 - v0) <= varianceLimit))
946
- return "component " + i + ": variance " + v1 + " differs from the reference " + v0 + " by more than the a-priori bound " + varianceLimit + ".";
947
- if (Math.abs(gpu.samples[i] - reference.samples[i]) > 1)
948
- return "component " + i + ": sample " + gpu.samples[i] + " differs from the reference " + reference.samples[i] + " by more than one code.";
949
- }
950
- // #533: the saturated-pixel count is the reference's, bit for bit (the light squares sit at the white clip).
951
- if (gpu.saturatedPixelCount !== reference.saturatedPixelCount)
952
- return "the saturated-pixel count " + gpu.saturatedPixelCount + " differs from the reference's " + reference.saturatedPixelCount + ".";
953
- return true;
954
- }
955
- finally {
956
- signal?.removeEventListener("abort", onAbort);
957
- }
884
+ // #533: the verdict includes the saturated-pixel count (the light squares sit at the white clip).
885
+ return conformanceVerdict(reference, gpu);
886
+ });
958
887
  }
959
888
  /** #479's self-check (see `runQualityConformance`). */
960
889
  export const DISPLAY_VIEW_QUALITY_CONFORMANCE_CASE = { id: "display-view:quality:checkerboard-13x9-diffraction-ahd",
@@ -1026,14 +955,16 @@ export function createDisplayViewGpuRenderer(runtime, options = {}) {
1026
955
  result = await renderDisplayViewSpeedOnGpu(runtime, bandBytes, { setup, signalInput, material }, abort, options.yieldControl, source);
1027
956
  }
1028
957
  else {
1029
- material = await qualityMaterialModule(modules, setup.gpu, abort.signal);
958
+ // #581: an image-plane print brings its own passes 1 and 2.
959
+ const image = signalInput.scene.image ? await modules.imagePasses(setup.gpu, abort.signal) : undefined;
960
+ material = image ? { id: "image-plane", wgsl: "", bindings: [] } : await qualityMaterialModule(modules, setup.gpu, abort.signal);
1030
961
  const key = displayViewCorrectionKey(setup);
1031
962
  let states = key === null ? undefined : stores.get(key);
1032
963
  if (key !== null && !states) {
1033
964
  states = new CorrectionStateStore();
1034
965
  stores.set(key, states);
1035
966
  }
1036
- result = await renderDisplayViewQualityOnGpu(runtime, bandBytes, { setup, signalInput, material, ...(states ? { states } : {}) }, abort, options.yieldControl, source);
967
+ result = await renderDisplayViewQualityOnGpu(runtime, bandBytes, { setup, signalInput, material, ...(image ? { image } : {}), ...(states ? { states } : {}) }, abort, options.yieldControl, source);
1037
968
  }
1038
969
  return { samples: result.samples, expected: result.expected, noiseStd: result.noiseStd, sampleCount: result.sampleCount, siteCount: result.siteCount, detailPixelCount: result.detailPixelCount,
1039
970
  uniformPixelCount: result.uniformPixelCount, ahdCorrectedPixelCount: result.ahdCorrectedPixelCount, estimation: result.estimation, saturated: result.saturated,