@photivra/engine 2.20.0 → 2.22.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 (275) hide show
  1. package/CHANGELOG.md +295 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +18 -0
  6. package/dist/capture/c2pa-embed.d.ts +116 -0
  7. package/dist/capture/c2pa-embed.js +493 -0
  8. package/dist/capture/c2pa-embed.js.map +1 -0
  9. package/dist/capture/dng-import.d.ts +73 -8
  10. package/dist/capture/dng-import.js +315 -80
  11. package/dist/capture/dng-import.js.map +1 -1
  12. package/dist/capture/dng-lens-correction-opcodes.d.ts +2 -2
  13. package/dist/capture/dng-lens-correction-opcodes.js +2 -2
  14. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -1
  15. package/dist/capture/dng-opcodes.d.ts +96 -0
  16. package/dist/capture/dng-opcodes.js +391 -0
  17. package/dist/capture/dng-opcodes.js.map +1 -0
  18. package/dist/capture/dng-profile-maps.d.ts +99 -0
  19. package/dist/capture/dng-profile-maps.js +381 -0
  20. package/dist/capture/dng-profile-maps.js.map +1 -0
  21. package/dist/capture/dng-stage-image.d.ts +23 -0
  22. package/dist/capture/dng-stage-image.js +65 -0
  23. package/dist/capture/dng-stage-image.js.map +1 -0
  24. package/dist/capture/exposure-build-up-stage.d.ts +16 -0
  25. package/dist/capture/exposure-build-up-stage.js +4 -0
  26. package/dist/capture/exposure-build-up-stage.js.map +1 -0
  27. package/dist/capture/jpeg-xl-tiles.d.ts +22 -0
  28. package/dist/capture/jpeg-xl-tiles.js +151 -0
  29. package/dist/capture/jpeg-xl-tiles.js.map +1 -0
  30. package/dist/capture/lossy-jpeg.d.ts +17 -0
  31. package/dist/capture/lossy-jpeg.js +369 -0
  32. package/dist/capture/lossy-jpeg.js.map +1 -0
  33. package/dist/capture/native-environment-lean.d.ts +2 -1
  34. package/dist/capture/native-environment-lean.js +41 -73
  35. package/dist/capture/native-environment-lean.js.map +1 -1
  36. package/dist/capture/native-environment-tabulated.js +4 -3
  37. package/dist/capture/native-environment-tabulated.js.map +1 -1
  38. package/dist/capture/native-raw-development.d.ts +2 -1
  39. package/dist/capture/native-raw-development.js.map +1 -1
  40. package/dist/capture/native-raw.d.ts +3 -2
  41. package/dist/capture/native-raw.js +5 -4
  42. package/dist/capture/native-raw.js.map +1 -1
  43. package/dist/capture/sensor-raw-producer.d.ts +4 -1
  44. package/dist/capture/sensor-raw-producer.js +12 -3
  45. package/dist/capture/sensor-raw-producer.js.map +1 -1
  46. package/dist/color/neighbor-highlight-reconstruction.d.ts +66 -0
  47. package/dist/color/neighbor-highlight-reconstruction.js +234 -0
  48. package/dist/color/neighbor-highlight-reconstruction.js.map +1 -0
  49. package/dist/color/sensor-color-development.d.ts +5 -0
  50. package/dist/color/sensor-color-development.js +7 -1
  51. package/dist/color/sensor-color-development.js.map +1 -1
  52. package/dist/color/spectral-spikiness.d.ts +11 -0
  53. package/dist/color/spectral-spikiness.js +23 -0
  54. package/dist/color/spectral-spikiness.js.map +1 -0
  55. package/dist/core/execution-error.d.ts +21 -0
  56. package/dist/core/execution-error.js +12 -0
  57. package/dist/core/execution-error.js.map +1 -0
  58. package/dist/core/version.d.ts +1 -1
  59. package/dist/core/version.js +1 -1
  60. package/dist/core/version.js.map +1 -1
  61. package/dist/equipment/generic-lens-catalogue.d.ts +3 -2
  62. package/dist/equipment/generic-lens-catalogue.js +18 -5
  63. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  64. package/dist/equipment/generic-tier-assets.d.ts +32 -0
  65. package/dist/equipment/generic-tier-assets.js +16 -11
  66. package/dist/equipment/generic-tier-assets.js.map +1 -1
  67. package/dist/equipment/sensor-profile-body.d.ts +78 -0
  68. package/dist/equipment/sensor-profile-body.js +77 -0
  69. package/dist/equipment/sensor-profile-body.js.map +1 -0
  70. package/dist/equipment/sensor-profiles.d.ts +8 -0
  71. package/dist/equipment/sensor-profiles.js +82 -0
  72. package/dist/equipment/sensor-profiles.js.map +1 -0
  73. package/dist/facade/catalogue-lens-signal.d.ts +28 -3
  74. package/dist/facade/catalogue-lens-signal.js +83 -78
  75. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  76. package/dist/facade/chart-radiance-batch.js +2 -1
  77. package/dist/facade/chart-radiance-batch.js.map +1 -1
  78. package/dist/facade/defocus-image-space-provider.d.ts +7 -0
  79. package/dist/facade/defocus-image-space-provider.js +64 -76
  80. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  81. package/dist/facade/diffraction-provider.d.ts +1 -1
  82. package/dist/facade/diffraction-provider.js +4 -4
  83. package/dist/facade/diffraction-provider.js.map +1 -1
  84. package/dist/facade/display-view-gpu-session.js +39 -5
  85. package/dist/facade/display-view-gpu-session.js.map +1 -1
  86. package/dist/facade/display-view-gpu.d.ts +19 -6
  87. package/dist/facade/display-view-gpu.js.map +1 -1
  88. package/dist/facade/display-view-lens-signal.js +25 -23
  89. package/dist/facade/display-view-lens-signal.js.map +1 -1
  90. package/dist/facade/display-view-normal.d.ts +15 -0
  91. package/dist/facade/display-view-normal.js +44 -0
  92. package/dist/facade/display-view-normal.js.map +1 -0
  93. package/dist/facade/display-view-rendering.d.ts +97 -0
  94. package/dist/facade/display-view-rendering.js +598 -0
  95. package/dist/facade/display-view-rendering.js.map +1 -0
  96. package/dist/facade/display-view.d.ts +37 -65
  97. package/dist/facade/display-view.js +152 -499
  98. package/dist/facade/display-view.js.map +1 -1
  99. package/dist/facade/expected-signal-setup.d.ts +3 -1
  100. package/dist/facade/expected-signal-setup.js +3 -3
  101. package/dist/facade/expected-signal-setup.js.map +1 -1
  102. package/dist/facade/expected-signal.d.ts +47 -14
  103. package/dist/facade/expected-signal.js +117 -112
  104. package/dist/facade/expected-signal.js.map +1 -1
  105. package/dist/facade/exposure-build-up.d.ts +70 -0
  106. package/dist/facade/exposure-build-up.js +164 -0
  107. package/dist/facade/exposure-build-up.js.map +1 -0
  108. package/dist/facade/fast-methods.d.ts +1 -1
  109. package/dist/facade/fast-methods.js +46 -9
  110. package/dist/facade/fast-methods.js.map +1 -1
  111. package/dist/facade/generic-lens-capture.d.ts +3 -3
  112. package/dist/facade/generic-lens-capture.js +3 -3
  113. package/dist/facade/generic-lens-capture.js.map +1 -1
  114. package/dist/facade/image-plane-enable.d.ts +7 -0
  115. package/dist/facade/image-plane-enable.js +17 -0
  116. package/dist/facade/image-plane-enable.js.map +1 -0
  117. package/dist/facade/image-plane-lattice-data.d.ts +6 -0
  118. package/dist/facade/image-plane-lattice-data.js +10 -0
  119. package/dist/facade/image-plane-lattice-data.js.map +1 -0
  120. package/dist/facade/image-plane-support.d.ts +33 -0
  121. package/dist/facade/image-plane-support.js +11 -0
  122. package/dist/facade/image-plane-support.js.map +1 -0
  123. package/dist/facade/image-plane.d.ts +36 -0
  124. package/dist/facade/image-plane.js +384 -0
  125. package/dist/facade/image-plane.js.map +1 -0
  126. package/dist/facade/mixed-illumination.d.ts +108 -0
  127. package/dist/facade/mixed-illumination.js +174 -0
  128. package/dist/facade/mixed-illumination.js.map +1 -0
  129. package/dist/facade/on-demand-kits.d.ts +14 -0
  130. package/dist/facade/on-demand-kits.js +36 -0
  131. package/dist/facade/on-demand-kits.js.map +1 -0
  132. package/dist/facade/opt-in-kit.d.ts +9 -0
  133. package/dist/facade/opt-in-kit.js +11 -0
  134. package/dist/facade/opt-in-kit.js.map +1 -0
  135. package/dist/facade/quality-kit.d.ts +21 -0
  136. package/dist/facade/quality-kit.js +23 -0
  137. package/dist/facade/quality-kit.js.map +1 -0
  138. package/dist/facade/quality-paths.d.ts +22 -0
  139. package/dist/facade/quality-paths.js +44 -0
  140. package/dist/facade/quality-paths.js.map +1 -0
  141. package/dist/facade/representative-light-sources.d.ts +72 -0
  142. package/dist/facade/representative-light-sources.js +124 -0
  143. package/dist/facade/representative-light-sources.js.map +1 -0
  144. package/dist/facade/scene-radiance.d.ts +145 -4
  145. package/dist/facade/scene-radiance.js +50 -13
  146. package/dist/facade/scene-radiance.js.map +1 -1
  147. package/dist/facade/simulate-capture.d.ts +68 -20
  148. package/dist/facade/simulate-capture.js +399 -146
  149. package/dist/facade/simulate-capture.js.map +1 -1
  150. package/dist/facade/simulate-display-view.d.ts +10 -12
  151. package/dist/facade/simulate-display-view.js +70 -53
  152. package/dist/facade/simulate-display-view.js.map +1 -1
  153. package/dist/gpu/chart-composite-gpu.d.ts +13 -1
  154. package/dist/gpu/chart-composite-gpu.js +48 -9
  155. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  156. package/dist/gpu/chart-radiance-gpu.js +4 -30
  157. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  158. package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
  159. package/dist/gpu/display-view-composite-material-module.js +39 -13
  160. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  161. package/dist/gpu/display-view-gpu-renderer.d.ts +43 -0
  162. package/dist/gpu/display-view-gpu-renderer.js +134 -0
  163. package/dist/gpu/display-view-gpu-renderer.js.map +1 -0
  164. package/dist/gpu/display-view-image-material-module.d.ts +87 -0
  165. package/dist/gpu/display-view-image-material-module.js +361 -0
  166. package/dist/gpu/display-view-image-material-module.js.map +1 -0
  167. package/dist/gpu/display-view-image-quality.d.ts +87 -0
  168. package/dist/gpu/display-view-image-quality.js +625 -0
  169. package/dist/gpu/display-view-image-quality.js.map +1 -0
  170. package/dist/gpu/display-view-quality-gpu.d.ts +80 -65
  171. package/dist/gpu/display-view-quality-gpu.js +187 -445
  172. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  173. package/dist/gpu/display-view-speed-gpu.d.ts +67 -19
  174. package/dist/gpu/display-view-speed-gpu.js +181 -105
  175. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  176. package/dist/gpu/image-kernel-accelerator.js +3 -0
  177. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  178. package/dist/gpu/image-kernel-execution.d.ts +2 -13
  179. package/dist/gpu/image-kernel-execution.js +2 -10
  180. package/dist/gpu/image-kernel-execution.js.map +1 -1
  181. package/dist/gpu/wgsl-display-view-coupled-stub.d.ts +8 -0
  182. package/dist/gpu/wgsl-display-view-coupled-stub.js +21 -0
  183. package/dist/gpu/wgsl-display-view-coupled-stub.js.map +1 -0
  184. package/dist/gpu/wgsl-display-view-coupled.d.ts +88 -0
  185. package/dist/gpu/wgsl-display-view-coupled.js +353 -0
  186. package/dist/gpu/wgsl-display-view-coupled.js.map +1 -0
  187. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +33 -24
  188. package/dist/gpu/wgsl-display-view-quality-kernel.js +166 -146
  189. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  190. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +19 -44
  191. package/dist/gpu/wgsl-display-view-speed-kernel.js +116 -18
  192. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  193. package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
  194. package/dist/gpu/wgsl-exact-sum.js +42 -0
  195. package/dist/gpu/wgsl-exact-sum.js.map +1 -0
  196. package/dist/index.d.ts +22 -6
  197. package/dist/index.js +16 -2
  198. package/dist/index.js.map +1 -1
  199. package/dist/motion/subject-motion.d.ts +66 -0
  200. package/dist/motion/subject-motion.js +52 -0
  201. package/dist/motion/subject-motion.js.map +1 -0
  202. package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
  203. package/dist/optics/generic-lens-lateral-color.js +77 -0
  204. package/dist/optics/generic-lens-lateral-color.js.map +1 -0
  205. package/dist/optics/generic-lens-psf.js +54 -46
  206. package/dist/optics/generic-lens-psf.js.map +1 -1
  207. package/dist/optics/lens-psf-profile.d.ts +7 -0
  208. package/dist/optics/lens-psf-profile.js +2 -1
  209. package/dist/optics/lens-psf-profile.js.map +1 -1
  210. package/dist/output/camera-channel-response.d.ts +50 -0
  211. package/dist/output/camera-channel-response.js +69 -0
  212. package/dist/output/camera-channel-response.js.map +1 -0
  213. package/dist/output/developed-lens-corrections.d.ts +19 -5
  214. package/dist/output/developed-lens-corrections.js +28 -9
  215. package/dist/output/developed-lens-corrections.js.map +1 -1
  216. package/dist/output/generic-lens-corrections.d.ts +41 -5
  217. package/dist/output/generic-lens-corrections.js +52 -8
  218. package/dist/output/generic-lens-corrections.js.map +1 -1
  219. package/dist/output/native-capture-sdr.js +5 -2
  220. package/dist/output/native-capture-sdr.js.map +1 -1
  221. package/dist/output/print-plan.d.ts +41 -5
  222. package/dist/output/print-plan.js +59 -14
  223. package/dist/output/print-plan.js.map +1 -1
  224. package/dist/output/sdr-rendering.d.ts +18 -1
  225. package/dist/output/sdr-rendering.js +39 -2
  226. package/dist/output/sdr-rendering.js.map +1 -1
  227. package/dist/sensor/binomial-sample.d.ts +3 -0
  228. package/dist/sensor/binomial-sample.js +86 -0
  229. package/dist/sensor/binomial-sample.js.map +1 -0
  230. package/dist/sensor/native-sensor-report.d.ts +10 -0
  231. package/dist/sensor/native-sensor-report.js +16 -0
  232. package/dist/sensor/native-sensor-report.js.map +1 -0
  233. package/dist/sensor/sensor-characteristics.d.ts +91 -13
  234. package/dist/sensor/sensor-characteristics.js +30 -15
  235. package/dist/sensor/sensor-characteristics.js.map +1 -1
  236. package/docs/AGENT_GUIDE.md +1 -0
  237. package/docs/API_REFERENCE.md +214 -4
  238. package/docs/QUICK_START.md +162 -8
  239. package/docs/USAGE.md +195 -22
  240. package/docs/api/capture-c2pa-embed.md +177 -0
  241. package/docs/api/capture-dng-import.md +86 -10
  242. package/docs/api/capture-dng-opcodes.md +93 -0
  243. package/docs/api/capture-dng-profile-maps.md +141 -0
  244. package/docs/api/capture-dng-stage-image.md +43 -0
  245. package/docs/api/capture-jpeg-xl-tiles.md +33 -0
  246. package/docs/api/capture-lossy-jpeg.md +41 -0
  247. package/docs/api/capture-native-raw-development.md +2 -1
  248. package/docs/api/core-execution-error.md +43 -0
  249. package/docs/api/core-version.md +1 -1
  250. package/docs/api/equipment-generic-lens-catalogue.md +10 -5
  251. package/docs/api/equipment-sensor-profile-body.md +64 -0
  252. package/docs/api/equipment-sensor-profiles.md +86 -0
  253. package/docs/api/exports.json +460 -5
  254. package/docs/api/facade-display-view-gpu.md +5 -2
  255. package/docs/api/facade-display-view.md +11 -15
  256. package/docs/api/facade-exposure-build-up.md +105 -0
  257. package/docs/api/facade-fast-methods.md +47 -10
  258. package/docs/api/facade-image-plane-enable.md +16 -0
  259. package/docs/api/facade-mixed-illumination.md +67 -0
  260. package/docs/api/facade-quality-paths.md +24 -0
  261. package/docs/api/facade-representative-light-sources.md +131 -0
  262. package/docs/api/facade-scene-radiance.md +54 -3
  263. package/docs/api/facade-simulate-capture.md +48 -11
  264. package/docs/api/facade-simulate-display-view.md +10 -12
  265. package/docs/api/gpu-image-kernel-execution.md +0 -36
  266. package/docs/api/motion-subject-motion.md +23 -0
  267. package/docs/api/optics-generic-lens-lateral-color.md +79 -0
  268. package/docs/api/output-camera-channel-response.md +55 -0
  269. package/docs/api/output-developed-lens-corrections.md +10 -1
  270. package/docs/api/output-generic-lens-corrections.md +40 -4
  271. package/docs/api/output-print-plan.md +35 -1
  272. package/docs/api/output-sdr-rendering.md +6 -1
  273. package/docs/api/sensor-sensor-characteristics.md +54 -2
  274. package/llms.txt +3 -0
  275. package/package.json +6 -5
@@ -3,9 +3,7 @@
3
3
  * Host side of the display view's Quality path on the GPU (#479): the support check, the float64 tables (the lattice groups
4
4
  * and their combined diffraction kernels per material and channel, as the reference forms them; the bilinear and AHD
5
5
  * candidate kernels; the footprint geometry), the three passes per band of display rows (lattice materials, site moments,
6
- * display pixels; `wgsl-display-view-quality-kernel.ts`), the low-signal correction states (computed on the host with the
7
- * reference's own `computeDisplayViewCorrectionStates`, #447, uploaded as a sorted table and grown from the grid keys the
8
- * kernel records), and the reference's own float64 finishing per display pixel (`finishDisplayViewPixel` of the Speed host:
6
+ * display pixels; `wgsl-display-view-quality-kernel.ts`), and the reference's own float64 finishing per display pixel (`finishDisplayViewPixel` of the Speed host:
9
7
  * the quantization term, the seeded noise draw, the Cholesky factor, the clipped centre, the sRGB encode and the rounding),
10
8
  * so the noise draw is bit-identical to the CPU's.
11
9
  *
@@ -17,26 +15,26 @@
17
15
  *
18
16
  * The float64 reference is `renderDisplayView` (facade/display-view.ts) on the exact-area-mean path with
19
17
  * `createDisplayViewExpectedSignal` (facade/expected-signal.ts), chart materials, with or without diffraction kernels,
20
- * the bilinear or the AHD candidates and the low-signal correction. Internal: not exported from the package.
18
+ * the bilinear or the AHD candidates. Internal: not exported from the package.
21
19
  *
22
- * Flagged display pixels (an uncertain lattice material among the sites read, a missing correction state after the
23
- * states were computed, a correction-threshold decision within its bound, AHD's choice on the noise-free mosaic able to move
20
+ * #594: this module and its WGSL load on demand (`prepareDisplayViewQualityGpu` in `display-view-gpu-renderer.ts`, a dynamic
21
+ * import), at the first render that takes the Quality passes; the renderer and the Speed path never import it statically.
22
+ *
23
+ * Flagged display pixels (an uncertain lattice material among the sites read, AHD's choice on the noise-free mosaic able to move
24
24
  * the pixel by more than DISPLAY_VIEW_QUALITY_CHOICE_SIGMA of its noise (#457), a non-finite output) are recomputed with the
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 { axisSums, 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
- import { createDisplayViewMaterialModules } from "./display-view-composite-material-module.js";
32
- import { setDisplayViewGpuRendererFactory } from "../facade/display-view-gpu.js";
33
30
  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";
31
+ import { CONFORMANCE_FRAME, LARGE_DEFOCUS_REASON, recomputeRuns, footprintShape, conformanceChannelAt, conformanceChart, conformanceResponses, conformanceSetup, conformanceVerdict, withConformanceAbort, axisRecord, finishDisplayViewPixel, gaussLegendre16, queryGeometry } from "./display-view-speed-gpu.js";
35
32
  import { createHostYieldGate } from "../capture/host-yield-cadence.js";
36
33
  import { createDefocusDiscSiteKernel } from "../optics/defocus-disc-kernel.js";
37
34
  import { ImageKernelExecutionError, WEBGPU_F32_UNIT_ROUNDOFF, webGpuExecution } from "./image-kernel-execution.js";
38
- 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";
35
+ import { BUFFER_USAGE, GpuResourceScope, linearDispatch, runComputePass, throwIfAborted as checkAbort } from "./webgpu-runtime.js";
36
+ import { DISPLAY_VIEW_QUALITY_AUX, 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, assembleDisplayViewQualityPixelsWgsl } from "./wgsl-display-view-quality-kernel.js";
37
+ import { SDR_LUMINANCE_WEIGHTS } from "../output/sdr-rendering.js";
40
38
  /** One WGSL rounding of + − × in either direction (W3C WGSL "Floating Point Accuracy"; #471, #487): twice the binary32 unit roundoff. */
41
39
  const E = 2 * WEBGPU_F32_UNIT_ROUNDOFF;
42
40
  /** Every reported bound is doubled: the kernel's running bounds are first order, and their own float32 arithmetic rounds. */
@@ -62,7 +60,7 @@ export const DISPLAY_VIEW_QUALITY_MAXIMUM_KERNEL_RADIUS = 96;
62
60
  * several invocations per site is needed.
63
61
  */
64
62
  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). */
63
+ /** The smallest positive normal float32: values below it may be flushed to zero by a device (WGSL allows it). */
66
64
  const FLOAT32_MINIMUM_NORMAL = 2 ** -126;
67
65
  /** Candidate table entries per phase: the (2·reach + 1)² window. */
68
66
  const WINDOW_SLOTS = (2 * MAXIMUM_REACH + 1) ** 2;
@@ -76,37 +74,6 @@ const QUANTIZATION_SLOPE = 6.04, QUANTIZATION_JOIN_JUMP = 2.1e-4, QUANTIZATION_J
76
74
  /** Bytes per display pixel in the output buffers (moments plus the flag word). */
77
75
  const OUTPUT_BYTES_PER_PIXEL = 4 * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS + 4 * DISPLAY_VIEW_QUALITY_FLAG_WORDS;
78
76
  const SITE_BYTES = 4 * DISPLAY_VIEW_QUALITY_SITE_FLOATS;
79
- /**
80
- * The low-signal correction states a renderer holds for one correction key, as the GPU table reads them: grid keys ascending
81
- * with three float32 values each (the host keeps the float64 values; the table is their single rounding).
82
- */
83
- export class CorrectionStateStore {
84
- #known = new Map();
85
- #keys = new Int32Array(0);
86
- #values = new Float32Array(0);
87
- #dirty = false;
88
- get size() { return this.#known.size; }
89
- has(key) { return this.#known.has(key); }
90
- add(gridKeys, values) {
91
- for (let i = 0; i < gridKeys.length; i++) {
92
- if (this.#known.has(gridKeys[i]))
93
- continue;
94
- this.#known.set(gridKeys[i], [values[3 * i], values[3 * i + 1], values[3 * i + 2]]);
95
- this.#dirty = true;
96
- }
97
- }
98
- /** The sorted table (rebuilt when states were added since the last call). */
99
- table() {
100
- if (this.#dirty) {
101
- const keys = Int32Array.from(this.#known.keys()).sort(), values = new Float32Array(3 * keys.length);
102
- keys.forEach((key, i) => { const v = this.#known.get(key); values[3 * i] = v[0]; values[3 * i + 1] = v[1]; values[3 * i + 2] = v[2]; });
103
- this.#keys = keys;
104
- this.#values = values;
105
- this.#dirty = false;
106
- }
107
- return { keys: this.#keys, values: this.#values };
108
- }
109
- }
110
77
  /**
111
78
  * Waits until the queue has finished the work submitted so far (`GPUQueue.onSubmittedWorkDone`), so a pass that reads nothing
112
79
  * back is timed with its own execution, not the next pass's wait. The passes depend on each other in order anyway.
@@ -116,17 +83,6 @@ async function queueIdle(runtime, signal) {
116
83
  if (typeof queue.onSubmittedWorkDone === "function")
117
84
  await runtime.guard(queue.onSubmittedWorkDone(), signal);
118
85
  }
119
- /** The footprint shape of a view: real pixels per display pixel along each axis, and whether the exact area mean applies. */
120
- export function footprintShape(setup) {
121
- const sx = setup.frameWidth / setup.viewWidth, sy = setup.frameHeight / setup.viewHeight, area = sx * sy;
122
- const exact = area <= Math.min(EXACT_AREA_MEAN_MAX_PIXELS, setup.samplesPerAxis * setup.samplesPerAxis);
123
- let maximumColumns = 0, maximumRows = 0;
124
- for (let i = 0; i < setup.region.width; i++)
125
- maximumColumns = Math.max(maximumColumns, overlaps((setup.region.x + i) * sx, (setup.region.x + i + 1) * sx, setup.frameWidth).w.length);
126
- for (let j = 0; j < setup.region.height; j++)
127
- maximumRows = Math.max(maximumRows, overlaps((setup.region.y + j) * sy, (setup.region.y + j + 1) * sy, setup.frameHeight).w.length);
128
- return { sx, sy, exact, maximumColumns, maximumRows };
129
- }
130
86
  /** The demosaic candidates of a setup (the bilinear kernels when it has none) and their reach in sites. */
131
87
  function candidatesOf(setup) {
132
88
  const candidates = setup.candidates ?? setup.demosaic.map((phase) => [phase]), count = candidates[0]?.length ?? 0;
@@ -148,22 +104,26 @@ function exactLatticeConstant(c, span) {
148
104
  /**
149
105
  * Why the Quality kernels do not cover a request, or null when they do: chart materials, the exact area mean (footprints of
150
106
  * at most 25 real pixels and 6 per axis), the identity or positive Reinhard tone curve, demosaic candidates within 3 sites
151
- * (1 or 2 per phase), at most 64 lattice groups, lattice constants exact in float32, no collect pass, and a raster at least
107
+ * (1 or 2 per phase), at most 64 lattice groups, lattice constants exact in float32, and a raster at least
152
108
  * 2·reach + 2 sites on each axis (so a reflected border site lies in the band's window).
153
109
  */
154
110
  export function displayViewQualityGpuUnsupportedReason(setup, signalInput) {
155
111
  if (signalInput.largeKernel)
156
112
  return LARGE_DEFOCUS_REASON;
157
- if (signalInput.scene.kind !== "materials")
113
+ if (signalInput.scene.kind !== "materials" && !signalInput.scene.image)
158
114
  return "radiance scenes (caller functions) are not on the GPU";
159
115
  if (!signalInput.kernels && !signalInput.queryPoint)
160
116
  return "the expected signal has no query points";
161
- if (setup.ahdCollect)
162
- return "a collect pass of the correction states runs on the CPU";
163
- if (setup.toneCurve !== "identity" && setup.toneCurve !== "positive-reinhard-per-channel")
117
+ if (!["identity", "positive-reinhard-per-channel", "extended-reinhard-luminance", "naka-rushton-luminance"].includes(setup.toneCurve))
164
118
  return "unsupported tone curve " + String(setup.toneCurve);
119
+ if (setup.rendering && !setup.coupled)
120
+ return "the coupled rendering steps were not prepared";
121
+ if (setup.rendering?.highlightMode === "reconstruct-neighbors")
122
+ return "highlightMode reconstruct-neighbors (#602; its regions' ratios are found on the host) is not on the GPU in 2.22.0";
165
123
  if (setup.region.width < 1 || setup.region.height < 1)
166
124
  return "an empty region";
125
+ if (signalInput.wavelengthsNm.length < 1 || signalInput.wavelengthsNm.length > DISPLAY_VIEW_QUALITY_MAXIMUM_NODES)
126
+ return "the site passes integrate 1 to " + DISPLAY_VIEW_QUALITY_MAXIMUM_NODES + " spectral nodes per site";
167
127
  if (signalInput.kernels && (signalInput.kernels.radius < 0 || signalInput.kernels.radius > DISPLAY_VIEW_QUALITY_MAXIMUM_KERNEL_RADIUS))
168
128
  return "a site kernel radius above " + DISPLAY_VIEW_QUALITY_MAXIMUM_KERNEL_RADIUS + " (the diffraction and defocus caps)";
169
129
  const shape = footprintShape(setup);
@@ -209,9 +169,9 @@ export function displayViewQualityGpuUnsupportedReason(setup, signalInput) {
209
169
  }
210
170
  return null;
211
171
  }
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;
172
+ function signalGather(signalInput) {
173
+ const tables = displayViewSignalTables(signalInput), A = signalInput.electronsPerRadiance, N = signalInput.wavelengthsNm.length, spectra = tables.spectra;
174
+ const W = signalInput.nativeWidth, H = signalInput.nativeHeight, R = signalInput.kernels ? signalInput.kernels.radius : 0, materials = spectra.length;
215
175
  // Taps in order: per group, with site kernels the dense rectangle row-major (tap (dx, dy) reads lattice offset (minIx − R + dx,
216
176
  // minIy − R + dy)), without them one tap per member. Plain loops: a radius-96 kernel has up to 150 544 taps.
217
177
  const tapGroup = [], tapDp = [], tapDq = [], tapIndex = [];
@@ -239,7 +199,6 @@ function signalGather(signalInput, factored) {
239
199
  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
200
  cw: signalInput.kernels ? group.cw : 0, ch: signalInput.kernels ? group.ch : 0 });
241
201
  });
242
- const direct = (m, c) => { const si = tables.spectralIndex[m]; return si < 0 ? tables.reflectance[m] * tables.neutralDirect[c] : tables.spectralDirect[c][si]; };
243
202
  const T = tapGroup.length, taps = new Uint32Array(T * DISPLAY_VIEW_QUALITY_TAP_WORDS), spread = new Float32Array(3 * T);
244
203
  for (let t = 0; t < T; t++) {
245
204
  const g = tapGroup[t], group = groups[g];
@@ -247,91 +206,64 @@ function signalGather(signalInput, factored) {
247
206
  taps[t * DISPLAY_VIEW_QUALITY_TAP_WORDS + 1] = tapDp[t] - group.dpMin;
248
207
  taps[t * DISPLAY_VIEW_QUALITY_TAP_WORDS + 2] = tapDq[t] - group.dqMin;
249
208
  }
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
- }
209
+ // Every value finite and non-negative, every spectrum value normal (a device may flush the others) and every sum in range.
210
+ const domain = () => { throw new ImageKernelExecutionError("unsupported-float32-domain", "A site's spectral terms are outside the float32 domain."); };
211
+ // The spectra and their envelope per node.
212
+ const base = 3 * T * N, table = new Float32Array(base + materials * N), low = new Float64Array(N).fill(Infinity), high = new Float64Array(N).fill(-Infinity);
213
+ let smallestL = Infinity;
214
+ spectra.forEach((L, m) => {
215
+ for (let n = 0; n < N; n++) {
216
+ const l = L[n];
217
+ if (!(l >= 0 && l <= FLOAT32_SAFE_MAGNITUDE))
218
+ domain();
219
+ table[base + m * N + n] = l;
220
+ low[n] = Math.min(low[n], l);
221
+ high[n] = Math.max(high[n], l);
222
+ if (l > 0)
223
+ smallestL = Math.min(smallestL, l);
277
224
  }
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
- }
301
- }
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);
225
+ });
226
+ if (smallestL < FLOAT32_MINIMUM_NORMAL)
227
+ domain();
228
+ let largestSum = 0, tinyMass = (T + 2) * N * FLOAT32_MINIMUM_NORMAL;
308
229
  for (let t = 0; t < T; t++) {
309
230
  const source = tables.groups[tapGroup[t]], index = tapIndex[t];
231
+ let tiny = 0;
310
232
  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);
233
+ let range = 0, mass = 0, tinyHere = 0;
234
+ for (let n = 0; n < N; n++) {
235
+ const value = signalInput.kernels ? source.nodeKernels[c][index * N + n] : source.members[index].weight * A[c][n];
236
+ if (!(value >= 0 && value <= FLOAT32_SAFE_MAGNITUDE))
237
+ domain();
238
+ table[(c * T + t) * N + n] = value;
239
+ if (materials > 0) {
240
+ const top = value * high[n];
241
+ range += value * (high[n] - low[n]);
242
+ mass += top;
243
+ // A kernel value or product below the normal range may be flushed: at most the product with the largest spectrum.
244
+ if (value > 0 && (value < FLOAT32_MINIMUM_NORMAL || top < 2 * FLOAT32_MINIMUM_NORMAL))
245
+ tinyHere += top;
246
+ }
320
247
  }
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));
248
+ // The spread rounded up (the float32 roundings of the operands, the products and the sum).
249
+ spread[c * T + t] = Math.fround(range * (1 + (N + 4) * E));
250
+ largestSum = Math.max(largestSum, mass);
251
+ tiny = Math.max(tiny, tinyHere);
323
252
  }
253
+ tinyMass += tiny;
324
254
  }
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 };
255
+ if (!(largestSum * T <= FLOAT32_SAFE_MAGNITUDE))
256
+ domain();
257
+ return { groups, taps, tapCount: T, nodes: N, table, spread, materials, tinyMass };
329
258
  }
330
259
  /**
331
260
  * 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).
261
+ * (#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
262
+ * 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
263
+ * partial over the T taps (at most T − 1 inexact additions), the blocked pass into a row, group and total partial per node (at
264
+ * most cw − 1 + ch − 1 + G − 1). The N node partials and the dark electrons (host-rounded, E/2) are then summed in integer
265
+ * 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
266
+ * bounds (docs/WEBGPU.md).
335
267
  */
336
268
  function siteSumRelativeBound(gather, blocked) {
337
269
  if (!blocked)
@@ -406,8 +338,7 @@ function resolveUncertainLattice(lattice, groupWords, signalInput) {
406
338
  if ((raw >>> 31) === 0)
407
339
  continue;
408
340
  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);
341
+ const m = scene.materialAt(latticeCoordinate(p, group.fx, W, g), -latticeCoordinate(q, group.fy, H, g));
411
342
  if (!(Number.isInteger(m) && m >= 0 && m < scene.materials.length))
412
343
  throw new ImageKernelExecutionError("unsupported-on-backend", "webgpu-f32 display view (Quality): a lattice point has no scene material; use cpu-reference.");
413
344
  lattice[first + local] = m;
@@ -416,6 +347,11 @@ function resolveUncertainLattice(lattice, groupWords, signalInput) {
416
347
  });
417
348
  return resolved;
418
349
  }
350
+ /**
351
+ * One axis of the reference's lattice point (createDisplayViewExpectedSignal's latticeU / latticeV, written as it writes them):
352
+ * (i + 0.5 − size/2)·g with no shift, (i + 0.5 + f − size/2)·g otherwise; v is its negative.
353
+ */
354
+ export const latticeCoordinate = (i, f, size, g) => (f === 0 ? i + 0.5 - size / 2 : i + 0.5 + f - size / 2) * g;
419
355
  /** The uniform parameters shared by the three passes (per-pass words are set per dispatch). */
420
356
  function constants(setup, signalInput, gather, reach, count, blocked) {
421
357
  const params = new ArrayBuffer(4 * DISPLAY_VIEW_QUALITY_PARAM_WORDS), words = new Uint32Array(params), ints = new Int32Array(params), floats = new Float32Array(params);
@@ -429,7 +365,6 @@ function constants(setup, signalInput, gather, reach, count, blocked) {
429
365
  words[9] = setup.toneCurve === "positive-reinhard-per-channel" ? 1 : 0;
430
366
  words[10] = reach;
431
367
  words[11] = count;
432
- words[16] = setup.ahdCorrection ? 1 : 0;
433
368
  words[18] = gather.tapCount;
434
369
  words[19] = gather.groups.length;
435
370
  for (let c = 0; c < 3; c++)
@@ -442,7 +377,6 @@ function constants(setup, signalInput, gather, reach, count, blocked) {
442
377
  }
443
378
  floats[27] = 8.5 * 8.5;
444
379
  floats[31] = setup.clipLevel;
445
- floats[35] = setup.ahdCorrection ? setup.ahdCorrection.thresholdVariance : 0;
446
380
  for (let r = 0; r < 3; r++)
447
381
  for (let c = 0; c < 3; c++)
448
382
  floats[40 + r * 3 + c] = setup.matrix[r][c];
@@ -458,15 +392,12 @@ function constants(setup, signalInput, gather, reach, count, blocked) {
458
392
  floats[49] = geometry.scaleV;
459
393
  }
460
394
  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)));
395
+ // 2.21.0: the spectral nodes the site passes integrate.
396
+ words[53] = gather.nodes;
397
+ // What a flushing device can drop (both site passes), rounded up and kept normal so the uniform is not flushed either.
398
+ floats[52] = Math.max(FLOAT32_MINIMUM_NORMAL, Math.fround(gather.tinyMass * (1 + 4 * E)));
464
399
  return { params, words, ints, floats };
465
400
  }
466
- /** The reference's correction setup of a display-view setup with the AHD correction. */
467
- function correctionSetup(setup) {
468
- return { seed: setup.seed, readout: setup.readout, gains: setup.gains, clipLevel: setup.clipLevel, matrix: setup.matrix, toneCurve: setup.toneCurve, ahdCorrection: setup.ahdCorrection };
469
- }
470
401
  /** How far the host's quantization term Σ(w/A)²·q(m) can move with a mean bound Δm (see QUANTIZATION_SLOPE), doubled. */
471
402
  function quantizationBound(moments, o, meanBound, pixels, step) {
472
403
  let join = 0;
@@ -479,19 +410,24 @@ function quantizationBound(moments, o, meanBound, pixels, step) {
479
410
  }
480
411
  /** Renders one region of a Quality display view on the GPU (see the module comment). */
481
412
  export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, abort, yieldControl, pipelines) {
482
- const { setup, signalInput, material } = input;
413
+ const { setup, signalInput, material, image } = input;
483
414
  const reason = displayViewQualityGpuUnsupportedReason(setup, signalInput);
484
415
  if (reason)
485
416
  throw new ImageKernelExecutionError("unsupported-on-backend", "webgpu-f32 display view (Quality): " + reason + "; use cpu-reference.");
486
417
  const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
487
- const timings = { lattice: 0, sites: 0, pixels: 0, states: 0, finishing: 0, recompute: 0, pixelRedispatches: 0, siteDispatches: 0 };
418
+ const timings = { lattice: 0, sites: 0, pixels: 0, finishing: 0, recompute: 0, siteDispatches: 0 };
488
419
  const { region, margin } = setup, shape = footprintShape(setup), { reach, count } = candidatesOf(setup);
489
420
  // #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)
421
+ const blocked = !!image || (input.blockedSum ?? (setup.gpu?.defocusRadius ?? 0) > 0);
422
+ if (blocked && !signalInput.kernels && !image)
492
423
  throw new ImageKernelExecutionError("unsupported-on-backend", "webgpu-f32 display view (Quality): the blocked site sum needs site kernels; use cpu-reference.");
493
424
  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);
425
+ const gather = image?.gather ?? signalGather(signalInput), aux = auxTable(setup), { params, words, ints, floats } = constants(setup, signalInput, gather, reach, count, blocked);
426
+ // #581: the print's site bound and its radiance values per lattice point (the site pass's node count in the last word).
427
+ if (image) {
428
+ floats[50] = image.siteGamma;
429
+ words[55] = image.pointFloats;
430
+ }
495
431
  const stride = DISPLAY_VIEW_QUALITY_GEOMETRY_STRIDE, geometry = new Float32Array((region.width + region.height) * stride);
496
432
  const columnSums = [], rowSums = [];
497
433
  for (let i = 0; i < region.width; i++) {
@@ -512,7 +448,7 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
512
448
  const { y0, y1 } = siteRows(0, rows), siteH = y1 - y0 + 1;
513
449
  let lattice = 0;
514
450
  for (const g of gather.groups)
515
- lattice += (siteW + g.dpSpan) * (siteH + g.dqSpan) * 4;
451
+ lattice += (siteW + g.dpSpan) * (siteH + g.dqSpan) * 4 * (image ? image.pointFloats + 1 : 1);
516
452
  return lattice + siteW * siteH * SITE_BYTES + rows * region.width * OUTPUT_BYTES_PER_PIXEL;
517
453
  };
518
454
  let rows = region.height;
@@ -521,32 +457,36 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
521
457
  // The value table (materials × 3 channels × taps) is one binding: with a large defocus kernel and many materials it can pass the
522
458
  // device's limit, which is then refused by name (never clipped).
523
459
  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.");
527
- const largestBinding = Math.max(bandBytesOf(rows), geometry.byteLength, gather.table.byteLength, 4 * DISPLAY_VIEW_QUALITY_BITMAP_WORDS);
460
+ throw new ImageKernelExecutionError("insufficient-limits", "The display view's kernel table (" + gather.table.byteLength +
461
+ " 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.");
462
+ const largestBinding = Math.max(bandBytesOf(rows), geometry.byteLength, gather.table.byteLength);
528
463
  if (largestBinding > runtime.maximumStorageBindingBytes)
529
464
  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) :
465
+ const latticePipeline = image ? await pipelines(...image.lattice) : await pipelines("photivra-display-view-quality-lattice:" + material.id, assembleDisplayViewQualityLatticeWgsl(material.wgsl));
466
+ const sitesPipeline = image ? await pipelines(...image.sites) : blocked ? await pipelines("photivra-display-view-quality-sites-blocked", DISPLAY_VIEW_QUALITY_SITES_BLOCKED_WGSL) :
532
467
  await pipelines("photivra-display-view-quality-sites", DISPLAY_VIEW_QUALITY_SITES_WGSL);
533
- const pixelsPipeline = await pipelines("photivra-display-view-quality-pixels", DISPLAY_VIEW_QUALITY_PIXELS_WGSL);
468
+ // #593, #601: the coupled steps' text and constants, loaded on demand for a view that asks for them.
469
+ const coupled = setup.rendering ? await import("./wgsl-display-view-coupled.js") : null;
470
+ const coupledConstants = coupled ? coupled.displayViewCoupledConstants(setup, SDR_LUMINANCE_WEIGHTS, gaussLegendre16()) : new Float32Array(1);
471
+ const pixelsPipeline = coupled ? await pipelines("photivra-display-view-quality-pixels+coupled", assembleDisplayViewQualityPixelsWgsl(coupled.DISPLAY_VIEW_COUPLED_WGSL))
472
+ : await pipelines("photivra-display-view-quality-pixels", DISPLAY_VIEW_QUALITY_PIXELS_WGSL);
534
473
  const moments = new Float32Array(pixelCount * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS), pixelFlags = new Uint32Array(pixelCount), masks = new Uint32Array(2 * pixelCount);
535
- const states = input.states ?? new CorrectionStateStore(), correction = setup.ahdCorrection ? correctionSetup(setup) : null;
536
- let statesComputed = 0, latticePointsResolved = 0;
474
+ let latticePointsResolved = 0;
537
475
  const scope = new GpuResourceScope(runtime);
538
476
  try {
539
- const auxBuffer = scope.upload("photivra-display-view-aux", aux, BUFFER_USAGE.STORAGE);
477
+ // #593, #601: the coupled steps' constants follow the aux table's own entries (one zero word without them).
478
+ const auxWithCoupled = new Float32Array(aux.length + coupledConstants.length);
479
+ auxWithCoupled.set(aux);
480
+ auxWithCoupled.set(coupledConstants, aux.length);
481
+ const auxBuffer = scope.upload("photivra-display-view-aux", auxWithCoupled, BUFFER_USAGE.STORAGE);
540
482
  const geometryBuffer = scope.upload("photivra-display-view-geometry", geometry, BUFFER_USAGE.STORAGE);
541
483
  // The blocked pass reads no tap records (its layout has no binding 2).
542
484
  const tapsBuffer = blocked ? null : scope.upload("photivra-display-view-taps", gather.taps, BUFFER_USAGE.STORAGE);
543
485
  const tableBuffer = scope.upload("photivra-display-view-table", gather.table, BUFFER_USAGE.STORAGE);
544
486
  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
487
  // 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 });
488
+ const materialGroup = image ? null : runtime.device.createBindGroup({ layout: latticePipeline.getBindGroupLayout(1), entries: material.bindings.map((b) => ({ binding: b.binding, resource: { buffer: b.buffer ??
489
+ scope.upload("photivra-display-view-material-" + b.binding, b.data, b.kind === "uniform" ? BUFFER_USAGE.UNIFORM : BUFFER_USAGE.STORAGE) } })) });
550
490
  const gate = createHostYieldGate();
551
491
  for (let row0 = 0; row0 < region.height; row0 += rows) {
552
492
  checkAbort(abort.signal);
@@ -576,26 +516,35 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
576
516
  const band = new GpuResourceScope(runtime);
577
517
  try {
578
518
  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
519
  const siteBuffer = band.buffer("photivra-display-view-sites", siteW * siteH * SITE_BYTES, BUFFER_USAGE.STORAGE);
581
- // Pass 1: lattice materials.
520
+ // Pass 1: lattice materials (#581: an image-plane print's radiance per node, with its flags, and their host resolution).
582
521
  let t0 = now();
583
522
  words[0] = latticeTotal;
584
523
  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;
524
+ let latticeBuffer, pointFlags = null;
525
+ if (image) {
526
+ const pass = await image.runLattice(band, latticePipeline, latticeParams, groupBuffer, groupWords, latticeTotal, abort.signal);
527
+ latticeBuffer = pass.values;
528
+ pointFlags = pass.flags;
529
+ latticePointsResolved += pass.resolved;
530
+ }
531
+ else {
532
+ latticeBuffer = band.buffer("photivra-display-view-lattice", latticeTotal * 4, BUFFER_USAGE.STORAGE | (blocked ? BUFFER_USAGE.COPY_SRC : 0));
533
+ const latticeGroup = runtime.device.createBindGroup({ layout: latticePipeline.getBindGroupLayout(0), entries: [
534
+ { binding: 0, resource: { buffer: latticeParams } }, { binding: 1, resource: { buffer: groupBuffer } }, { binding: 2, resource: { buffer: latticeBuffer } }
535
+ ] });
536
+ 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);
537
+ await queueIdle(runtime, abort.signal);
538
+ if (blocked) {
539
+ // #492: with a wide disc nearly every site reads some uncertain lattice point, and its spread over every material would flag
540
+ // it. The host resolves each uncertain point with the reference's own material lookup at the reference's lattice point
541
+ // (createDisplayViewExpectedSignal's latticeU/latticeV), so the sites read exactly the reference's materials.
542
+ const lattice = new Uint32Array(latticeData, 0, latticeTotal);
543
+ const resolved = resolveUncertainLattice(lattice, groupWords, signalInput);
544
+ if (resolved > 0)
545
+ latticeBuffer = band.upload("photivra-display-view-lattice-resolved", lattice, BUFFER_USAGE.STORAGE);
546
+ latticePointsResolved += resolved;
547
+ }
599
548
  }
600
549
  timings.lattice += now() - t0;
601
550
  // Pass 2: site moments, in chunks of whole workgroups of sites whose taps stay within the budget per dispatch (#492).
@@ -610,8 +559,7 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
610
559
  const siteParams = chunk.upload("photivra-display-view-params-sites", new Uint8Array(params.slice(0)), BUFFER_USAGE.UNIFORM);
611
560
  const siteGroup = runtime.device.createBindGroup({ layout: sitesPipeline.getBindGroupLayout(0), entries: [
612
561
  { 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 } }] : [])
562
+ { binding: 3, resource: { buffer: tableBuffer } }, { binding: 4, resource: { buffer: latticeBuffer } }, { binding: 5, resource: { buffer: siteBuffer } }, { binding: 6, resource: { buffer: pointFlags ?? spreadBuffer } },
615
563
  ] });
616
564
  await runComputePass(runtime, chunk, sitesPipeline, siteGroup, linearDispatch(words[0], DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE, runtime.maximumWorkgroupsPerDimension), [], abort.signal);
617
565
  await queueIdle(runtime, abort.signal);
@@ -623,71 +571,32 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
623
571
  }
624
572
  words[51] = 0;
625
573
  timings.sites += now() - t0;
626
- // Pass 3: display pixels, repeated once when a correction state was missing and has been computed.
574
+ // Pass 3: display pixels.
627
575
  words[0] = count;
628
576
  const out = band.buffer("photivra-display-view-moments", count * 4 * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS, BUFFER_USAGE.STORAGE | BUFFER_USAGE.COPY_SRC);
629
577
  const flagBuffer = band.buffer("photivra-display-view-flags", count * 4 * DISPLAY_VIEW_QUALITY_FLAG_WORDS, BUFFER_USAGE.STORAGE | BUFFER_USAGE.COPY_SRC);
630
- const bitmapBuffer = band.buffer("photivra-display-view-bitmap", 4 * DISPLAY_VIEW_QUALITY_BITMAP_WORDS, BUFFER_USAGE.STORAGE | BUFFER_USAGE.COPY_SRC);
631
- for (let attempt = 0;; attempt++) {
632
- checkAbort(abort.signal);
633
- t0 = now();
634
- const table = states.table(), stateKeys = table.keys.length > 0 ? table.keys : new Int32Array(1), stateValues = table.values.length > 0 ? table.values : new Float32Array(3);
635
- words[17] = table.keys.length;
636
- const pass = new GpuResourceScope(runtime);
637
- let data, flagData, bitmapData;
638
- try {
639
- const pixelParams = pass.upload("photivra-display-view-params-pixels", new Uint8Array(params.slice(0)), BUFFER_USAGE.UNIFORM);
640
- const keysBuffer = pass.upload("photivra-display-view-state-keys", stateKeys, BUFFER_USAGE.STORAGE);
641
- const valuesBuffer = pass.upload("photivra-display-view-state-values", stateValues, BUFFER_USAGE.STORAGE);
642
- const pixelGroup = runtime.device.createBindGroup({ layout: pixelsPipeline.getBindGroupLayout(0), entries: [
643
- { binding: 0, resource: { buffer: pixelParams } }, { binding: 1, resource: { buffer: auxBuffer } }, { binding: 2, resource: { buffer: geometryBuffer } },
644
- { binding: 3, resource: { buffer: siteBuffer } }, { binding: 4, resource: { buffer: keysBuffer } }, { binding: 5, resource: { buffer: valuesBuffer } },
645
- { binding: 6, resource: { buffer: bitmapBuffer } }, { binding: 7, resource: { buffer: out } }, { binding: 8, resource: { buffer: flagBuffer } }
646
- ] });
647
- [data, flagData, bitmapData] = await runComputePass(runtime, pass, pixelsPipeline, pixelGroup, linearDispatch(count, DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE, runtime.maximumWorkgroupsPerDimension), [{ buffer: out, bytes: count * 4 * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS }, { buffer: flagBuffer, bytes: count * 4 * DISPLAY_VIEW_QUALITY_FLAG_WORDS }, { buffer: bitmapBuffer, bytes: 4 * DISPLAY_VIEW_QUALITY_BITMAP_WORDS }], abort.signal);
648
- }
649
- finally {
650
- pass.release();
651
- }
652
- timings.pixels += now() - t0;
653
- const flagWords = new Uint32Array(flagData, 0, count * DISPLAY_VIEW_QUALITY_FLAG_WORDS), bandFlags = new Uint32Array(count);
654
- for (let i = 0; i < count; i++)
655
- bandFlags[i] = flagWords[DISPLAY_VIEW_QUALITY_FLAG_WORDS * i];
656
- let missing = false;
657
- for (let i = 0; i < count && !missing; i++)
658
- if ((bandFlags[i] & DISPLAY_VIEW_QUALITY_FLAG.stateMissing) !== 0)
659
- missing = true;
660
- if (missing && correction && attempt === 0) {
661
- // The grid keys the band needs that the store lacks: compute them with the reference (the realm's cache serves known ones), then dispatch again.
662
- t0 = now();
663
- const bitmap = new Uint32Array(bitmapData, 0, DISPLAY_VIEW_QUALITY_BITMAP_WORDS), needed = [];
664
- for (let w = 0; w < DISPLAY_VIEW_QUALITY_BITMAP_WORDS; w++) {
665
- let bitsWord = bitmap[w];
666
- while (bitsWord !== 0) {
667
- const bit = 31 - Math.clz32(bitsWord);
668
- bitsWord &= ~(1 << bit);
669
- const key = w * 32 + bit;
670
- if (key < AHD_KEY_LEVELS ** 3 && !states.has(key))
671
- needed.push(key);
672
- }
673
- }
674
- if (needed.length > 0) {
675
- const computed = await computeDisplayViewCorrectionStates({ ...correction, yieldControl: async () => { if (yieldControl)
676
- await yieldControl(abort.signal); }, checkAbort: () => checkAbort(abort.signal) }, Int32Array.from(needed));
677
- states.add(computed.gridKeys, computed.values);
678
- statesComputed += needed.length;
679
- }
680
- timings.states += now() - t0;
681
- timings.pixelRedispatches++;
682
- continue;
683
- }
684
- moments.set(new Float32Array(data, 0, count * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS), first * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS);
685
- pixelFlags.set(bandFlags, first);
686
- for (let i = 0; i < count; i++) {
687
- masks[2 * (first + i)] = flagWords[DISPLAY_VIEW_QUALITY_FLAG_WORDS * i + 1];
688
- masks[2 * (first + i) + 1] = flagWords[DISPLAY_VIEW_QUALITY_FLAG_WORDS * i + 2];
689
- }
690
- break;
578
+ checkAbort(abort.signal);
579
+ t0 = now();
580
+ const pass = new GpuResourceScope(runtime);
581
+ let data, flagData;
582
+ try {
583
+ const pixelParams = pass.upload("photivra-display-view-params-pixels", new Uint8Array(params.slice(0)), BUFFER_USAGE.UNIFORM);
584
+ const pixelGroup = runtime.device.createBindGroup({ layout: pixelsPipeline.getBindGroupLayout(0), entries: [
585
+ { binding: 0, resource: { buffer: pixelParams } }, { binding: 1, resource: { buffer: auxBuffer } }, { binding: 2, resource: { buffer: geometryBuffer } },
586
+ { binding: 3, resource: { buffer: siteBuffer } }, { binding: 4, resource: { buffer: out } }, { binding: 5, resource: { buffer: flagBuffer } }
587
+ ] });
588
+ [data, flagData] = await runComputePass(runtime, pass, pixelsPipeline, pixelGroup, linearDispatch(count, DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE, runtime.maximumWorkgroupsPerDimension), [{ buffer: out, bytes: count * 4 * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS }, { buffer: flagBuffer, bytes: count * 4 * DISPLAY_VIEW_QUALITY_FLAG_WORDS }], abort.signal);
589
+ }
590
+ finally {
591
+ pass.release();
592
+ }
593
+ timings.pixels += now() - t0;
594
+ const flagWords = new Uint32Array(flagData, 0, count * DISPLAY_VIEW_QUALITY_FLAG_WORDS);
595
+ moments.set(new Float32Array(data, 0, count * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS), first * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS);
596
+ for (let i = 0; i < count; i++) {
597
+ pixelFlags[first + i] = flagWords[DISPLAY_VIEW_QUALITY_FLAG_WORDS * i];
598
+ masks[2 * (first + i)] = flagWords[DISPLAY_VIEW_QUALITY_FLAG_WORDS * i + 1];
599
+ masks[2 * (first + i) + 1] = flagWords[DISPLAY_VIEW_QUALITY_FLAG_WORDS * i + 2];
691
600
  }
692
601
  }
693
602
  finally {
@@ -710,7 +619,7 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
710
619
  const expected = new Float64Array(pixelCount * 3), noiseStd = new Float64Array(pixelCount * 3), saturated = new Float64Array(pixelCount);
711
620
  const meanBound = new Float32Array(pixelCount), varianceBound = new Float32Array(pixelCount);
712
621
  const flagged = [];
713
- let largestMean = 0, largestVariance = 0, sampleCount = 0, siteCount = 0, corrected = 0;
622
+ let largestMean = 0, largestVariance = 0, sampleCount = 0, siteCount = 0;
714
623
  const counts = new Int32Array(geometry.buffer);
715
624
  // #533: each display pixel's saturated-pixel share from the kernel's mask (the reference's arithmetic, exact where it decided).
716
625
  const columnOverlaps = Array.from({ length: region.width }, (_, i) => overlaps((region.x + i) * shape.sx, (region.x + i + 1) * shape.sx, setup.frameWidth));
@@ -736,8 +645,6 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
736
645
  varianceBound[i] = MARGIN * moments[o + 10] + quantizationBound(moments, o, meanBound[i], pixels, step);
737
646
  largestMean = Math.max(largestMean, meanBound[i]);
738
647
  largestVariance = Math.max(largestVariance, varianceBound[i]);
739
- if ((pixelFlags[i] & DISPLAY_VIEW_QUALITY_FLAG.corrected) !== 0)
740
- corrected++;
741
648
  const nx = counts[rx * stride + 1];
742
649
  sampleCount += nx * ny;
743
650
  siteCount += (nx + 2 * reach) * (ny + 2 * reach);
@@ -754,22 +661,11 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
754
661
  for (let i = 0; i < pixelCount; i++)
755
662
  if (isFlagged[i] === 0 && (pixelFlags[i] & DISPLAY_VIEW_QUALITY_FLAG.highlightUndecided) !== 0)
756
663
  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
- }
664
+ if (input.recomputeFlagged !== false)
665
+ await recomputeRuns(setup, signalInput, undecided, abort, (first, run, part) => { for (let k = 0; k < run; k++) {
666
+ saturated[first + k] = part.saturated[k];
667
+ highlights.push(first + k);
668
+ } });
773
669
  if (flagged.length > 0) {
774
670
  if (input.recomputeFlagged === false) {
775
671
  // Without recomputation a flagged pixel keeps the kernel's values where they are finite (its bound then does not hold).
@@ -787,18 +683,9 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
787
683
  varianceBound[i] = MARGIN * moments[o + 10] + quantizationBound(moments, o, meanBound[i], pixels, step);
788
684
  }
789
685
  }
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 } });
686
+ else
687
+ await recomputeRuns(setup, signalInput, flagged, abort, (first, run, part) => {
688
+ // The run's counts are the sums of its pixels' counts.
802
689
  samples.set(part.samples, first * 3);
803
690
  expected.set(part.expected, first * 3);
804
691
  noiseStd.set(part.noiseStd, first * 3);
@@ -808,12 +695,9 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
808
695
  varianceBound[first + k] = 0;
809
696
  recomputed.push(first + k);
810
697
  }
811
- corrected += part.ahdCorrectedPixelCount;
812
698
  sampleCount += part.sampleCount;
813
699
  siteCount += part.siteCount;
814
- r += run;
815
- }
816
- }
700
+ });
817
701
  }
818
702
  timings.recompute = now() - t0;
819
703
  const relative = MARGIN * CPU_RELATIVE;
@@ -822,10 +706,10 @@ export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, a
822
706
  let saturatedPixelCount = 0;
823
707
  for (let i = 0; i < pixelCount; i++)
824
708
  saturatedPixelCount += saturated[i];
825
- return { samples, expected, noiseStd, sampleCount, siteCount, detailPixelCount: pixelCount, uniformPixelCount: 0, ahdCorrectedPixelCount: corrected, estimation: "exact-area-mean", saturated, saturatedPixelCount,
709
+ return { samples, expected, noiseStd, sampleCount, siteCount, detailPixelCount: pixelCount, uniformPixelCount: 0, estimation: "exact-area-mean", saturated, saturatedPixelCount,
826
710
  execution: webGpuExecution(expectedValueBound, uncertain), expectedValueBound, varianceBound: { kind: "relative-to-reference", relative, absolute: largestVariance },
827
711
  expectedValueAbsoluteBound: meanBound, varianceAbsoluteBound: varianceBound, flaggedPixelIndices: Uint32Array.from(flagged), hostRecomputedPixelIndices: Uint32Array.from(recomputed),
828
- pixelFlags, highlightRecomputedPixelIndices: Uint32Array.from(highlights), statesComputed, statesHeld: states.size, tapsPerSite: gather.tapCount, blockedSum: blocked, siteSumRelativeBound: floats[50], latticePointsResolved, stageMilliseconds: timings };
712
+ pixelFlags, highlightRecomputedPixelIndices: Uint32Array.from(highlights), tapsPerSite: gather.tapCount, blockedSum: blocked, siteSumRelativeBound: floats[50], latticePointsResolved, stageMilliseconds: timings };
829
713
  }
830
714
  /**
831
715
  * The GPU setup the material modules read for a Quality render (#491's `createDisplayViewMaterialModules`). The composite module
@@ -845,8 +729,8 @@ export function qualityMaterialSetup(gpu) {
845
729
  const offsets = groups.map((group) => ({ x: group.fx, y: group.fy, weight: 1 / groups.length }));
846
730
  const queryPoint = (x, y, o, out) => {
847
731
  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;
732
+ out[0] = latticeCoordinate(x, group.fx, W, g);
733
+ out[1] = -latticeCoordinate(y, group.fy, H, g);
850
734
  };
851
735
  const { kernels: _kernels, ...rest } = input;
852
736
  void _kernels;
@@ -856,25 +740,6 @@ export function qualityMaterialSetup(gpu) {
856
740
  export function qualityMaterialModule(modules, gpu, signal) {
857
741
  return modules.moduleFor(qualityMaterialSetup(gpu), signal);
858
742
  }
859
- /** The pixel-aperture samples per axis and anti-aliasing spots of a request. */
860
- export function offsetCount(request) { return request.boxSamples * request.boxSamples * (request.antiAliasing ? 4 : 1); }
861
- /**
862
- * Why the Quality renderer does not support a prepared request (`DisplayViewGpuRenderer.supports`), or true: engine charts and
863
- * declared `material-layers` scenes (the material modules of `createDisplayViewMaterialModules`, #491), the identity or Reinhard
864
- * tone curve and at most 64 query offsets (lattice groups). `lens.defocus` out of focus is supported (#492 step 2c: the site pass
865
- * gathers the disc, or each node's diffraction kernel convolved with it, up to the reference's own caps of radius 64 and 96);
866
- * whether a view's footprints take the exact area mean, whether the device passes the composite probe, and whether the kernel
867
- * table fits the device's storage binding limit, is known only per render.
868
- */
869
- 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;
877
- }
878
743
  /** A normalized, symmetric site kernel of radius R for the self-check (a binned Gaussian of width `sigma` sites). */
879
744
  function syntheticKernel(radius, sigma) {
880
745
  const size = 2 * radius + 1, kernel = new Float64Array(size * size);
@@ -895,66 +760,32 @@ function syntheticKernel(radius, sigma) {
895
760
  * the GPU against `renderDisplayView` within the a-priori bounds, every flagged pixel recomputed:
896
761
  *
897
762
  * - `display-view:quality:checkerboard-13x9-diffraction-ahd` (#479): synthetic radius-1 diffraction kernels at three spectral
898
- * nodes, the AHD candidates and the low-signal correction (few electrons per site, so the correction applies; 64 draws per
899
- * state keep the check short), the tap-list site pass;
763
+ * nodes, the AHD candidates at few electrons per site, the tap-list site pass;
900
764
  * - `display-view:quality:checkerboard-13x9-defocus-blocked` (#492): the engine's own defocus disc of 6.3 sites (radius 3) at
901
765
  * every node, the bilinear demosaic (a defocused Speed request's route), the blocked site sum, and a tap budget that splits
902
766
  * each band's site pass over several dispatches.
903
767
  */
904
768
  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 });
769
+ const runtime = handle, { nativeWidth, nativeHeight, frameWidth, nodes } = CONFORMANCE_FRAME, { chart, scene } = conformanceChart();
908
770
  const offsets = [];
909
771
  for (const [nx, ny] of [[-0.25, -0.25], [0.25, -0.25], [-0.25, 0.25], [0.25, 0.25]])
910
772
  for (const [ax, ay] of [[-0.5, -0.5], [0.5, -0.5], [-0.5, 0.5], [0.5, 0.5]])
911
773
  offsets.push({ x: nx - ax, y: ny - ay, weight: 1 / 16 });
912
774
  const disc = defocus ? createDefocusDiscSiteKernel(6.3) : null;
913
775
  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(); } };
776
+ const signalInput = { nativeWidth, nativeHeight, outputWidth: frameWidth, wavelengthsNm: nodes, electronsPerRadiance: conformanceResponses(defocus ? 1 : 0.1),
777
+ offsets, kernels, scene, channelAt: conformanceChannelAt };
778
+ 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))),
779
+ ahdChoice: { 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])) } });
928
780
  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();
781
+ return await withConformanceAbort(runtime, signal, async (abort, pipelines) => {
934
782
  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));
936
- if (!defocus && (gpu.ahdCorrectedPixelCount === 0 || reference.ahdCorrectedPixelCount === 0))
937
- return "the self-check did not exercise the low-signal correction.";
783
+ const gpu = await renderDisplayViewQualityOnGpu(runtime, 1 << 20, { setup, signalInput, material: chartPresetMaterialModule(chart), ...gather }, abort, undefined, pipelines);
938
784
  if (defocus && (!gpu.blockedSum || gpu.stageMilliseconds.siteDispatches < 2))
939
785
  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
- }
786
+ // #533: the verdict includes the saturated-pixel count (the light squares sit at the white clip).
787
+ return conformanceVerdict(reference, gpu);
788
+ });
958
789
  }
959
790
  /** #479's self-check (see `runQualityConformance`). */
960
791
  export const DISPLAY_VIEW_QUALITY_CONFORMANCE_CASE = { id: "display-view:quality:checkerboard-13x9-diffraction-ahd",
@@ -962,95 +793,6 @@ export const DISPLAY_VIEW_QUALITY_CONFORMANCE_CASE = { id: "display-view:quality
962
793
  /** #492's self-check of the defocus disc's blocked site sum (see `runQualityConformance`). */
963
794
  export const DISPLAY_VIEW_QUALITY_DEFOCUS_CONFORMANCE_CASE = { id: "display-view:quality:checkerboard-13x9-defocus-blocked",
964
795
  run: (handle, signal) => runQualityConformance(handle, signal, true) };
965
- /** The bilinear RGGB demosaic kernels per phase (phaseY·2 + phaseX) and channel, as `bilinearPhaseProfiles` gives them. */
966
- export function bilinearKernels() {
967
- const cross = [[1, 0], [-1, 0], [0, 1], [0, -1]], diagonal = [[1, 1], [1, -1], [-1, 1], [-1, -1]], horizontal = [[1, 0], [-1, 0]], vertical = [[0, 1], [0, -1]];
968
- const average = (offsets) => offsets.map(([dx, dy]) => ({ dx: dx, dy: dy, weight: 1 / offsets.length })), self = [{ dx: 0, dy: 0, weight: 1 }];
969
- return [[self, average(cross), average(diagonal)], [average(horizontal), self, average(vertical)], [average(vertical), self, average(horizontal)], [average(diagonal), average(cross), self]];
970
- }
971
- /**
972
- * The display view's GPU renderer on one runtime (#481's `DisplayViewGpuRenderer`): the Speed kernel for requests it covers
973
- * (no diffraction, no AHD, no defocus disc, at most 16 offsets) and the Quality kernels for the rest (#492: a defocused Speed
974
- * request too, with its bilinear demosaic). One pipeline cache per device, one set of
975
- * material modules (#491: chart presets, composite charts, the 2.3 elements and material-layers scenes; `dispose` releases their
976
- * uploaded scene records), one correction-state store per correction key. `render` renders the part's region (`setup.gpu`
977
- * carries the expected signal's input and the chart; a setup without it, a coarse view or a frame the kernels do not cover
978
- * throws `unsupported-on-backend`), and throws `aborted` and `device-lost` as the accelerator's operations do.
979
- */
980
- /** #564: the GPU refusal of a defocus kernel above the direct radius (the CPU convolves it by runs). */
981
- const LARGE_DEFOCUS_REASON = "a defocus kernel radius above 64 sites (convolved by runs on the CPU, #564) is not on the GPU";
982
- export function createDisplayViewGpuRenderer(runtime, options = {}) {
983
- const pipelines = new Map(), stores = new Map();
984
- const bandBytes = options.maximumBandBytes ?? Math.min(64 * 1024 * 1024, runtime.maximumStorageBindingBytes);
985
- const source = (label, code) => {
986
- let pending = pipelines.get(label);
987
- if (!pending) {
988
- pending = createComputePipeline(runtime, label, code, undefined);
989
- pipelines.set(label, pending);
990
- pending.catch(() => pipelines.delete(label));
991
- }
992
- return pending;
993
- };
994
- // #491: one set of material modules per device for both kernels (the preset module per chart; the composite module, its probe
995
- // and its uploaded scene records), released by `dispose`.
996
- const modules = createDisplayViewMaterialModules(runtime, source);
997
- return {
998
- id: "photivra-display-view",
999
- supports(request) {
1000
- if (request.largeDefocus)
1001
- return LARGE_DEFOCUS_REASON;
1002
- // #492: out of focus the expected signal carries the defocus disc as site kernels, which only the Quality site pass gathers;
1003
- // a Speed request then runs the Quality passes with its own bilinear demosaic (one candidate) and finishing.
1004
- if (request.defocus)
1005
- return displayViewQualityGpuSupports(request);
1006
- const speed = displayViewSpeedGpuSupports(request);
1007
- if (speed === true)
1008
- return true;
1009
- return displayViewQualityGpuSupports(request);
1010
- },
1011
- conformanceCases: [DISPLAY_VIEW_SPEED_CONFORMANCE_CASE, DISPLAY_VIEW_QUALITY_CONFORMANCE_CASE, DISPLAY_VIEW_QUALITY_DEFOCUS_CONFORMANCE_CASE],
1012
- async render(part) {
1013
- const { setup, signal } = part;
1014
- if (!setup.gpu)
1015
- throw new ImageKernelExecutionError("unsupported-on-backend", "The display-view setup carries no GPU inputs (setup.gpu); use cpu-reference.");
1016
- const abort = new AbortController(), onAbort = () => abort.abort();
1017
- if (signal?.aborted)
1018
- throw abortedError();
1019
- signal?.addEventListener("abort", onAbort, { once: true });
1020
- try {
1021
- const signalInput = setup.gpu.signalInput;
1022
- let result;
1023
- let material;
1024
- if (displayViewSpeedGpuUnsupportedReason(setup, signalInput) === null) {
1025
- material = await modules.moduleFor(setup.gpu, abort.signal);
1026
- result = await renderDisplayViewSpeedOnGpu(runtime, bandBytes, { setup, signalInput, material }, abort, options.yieldControl, source);
1027
- }
1028
- else {
1029
- material = await qualityMaterialModule(modules, setup.gpu, abort.signal);
1030
- const key = displayViewCorrectionKey(setup);
1031
- let states = key === null ? undefined : stores.get(key);
1032
- if (key !== null && !states) {
1033
- states = new CorrectionStateStore();
1034
- stores.set(key, states);
1035
- }
1036
- result = await renderDisplayViewQualityOnGpu(runtime, bandBytes, { setup, signalInput, material, ...(states ? { states } : {}) }, abort, options.yieldControl, source);
1037
- }
1038
- return { samples: result.samples, expected: result.expected, noiseStd: result.noiseStd, sampleCount: result.sampleCount, siteCount: result.siteCount, detailPixelCount: result.detailPixelCount,
1039
- uniformPixelCount: result.uniformPixelCount, ahdCorrectedPixelCount: result.ahdCorrectedPixelCount, estimation: result.estimation, saturated: result.saturated,
1040
- saturatedPixelCount: result.saturatedPixelCount, errorBound: result.expectedValueBound,
1041
- diagnosticUncertaintySampleCount: result.execution.diagnosticUncertaintySampleCount, ...(material.compositeProbe ? { compositeProbe: material.compositeProbe } : {}) };
1042
- }
1043
- finally {
1044
- signal?.removeEventListener("abort", onAbort);
1045
- }
1046
- },
1047
- dispose() { modules.release(); stores.clear(); }
1048
- };
1049
- }
1050
- /** The factory `prepareDisplayView` creates the renderer with (one per device): the Speed and Quality kernels behind one renderer. */
1051
- export const displayViewGpuRendererFactory = (handle) => createDisplayViewGpuRenderer(handle);
1052
- /** Registers the combined renderer as the display view's GPU renderer and returns the factory registered before. */
1053
- export function registerDisplayViewGpuRenderer() {
1054
- return setDisplayViewGpuRendererFactory(displayViewGpuRendererFactory);
1055
- }
796
+ /** #594: the Quality kernels' self-check cases, run before the first render that takes the Quality passes (`onDemandConformanceCases`). */
797
+ export const DISPLAY_VIEW_QUALITY_CONFORMANCE_CASES = Object.freeze([DISPLAY_VIEW_QUALITY_CONFORMANCE_CASE, DISPLAY_VIEW_QUALITY_DEFOCUS_CONFORMANCE_CASE]);
1056
798
  //# sourceMappingURL=display-view-quality-gpu.js.map