@photivra/engine 2.10.0 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +139 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +21 -3
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/core/portable-math.d.ts +6 -0
- package/dist/core/portable-math.js +39 -0
- package/dist/core/portable-math.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/chart-material-layers.d.ts +115 -0
- package/dist/facade/chart-material-layers.js +224 -0
- package/dist/facade/chart-material-layers.js.map +1 -0
- package/dist/facade/chart-radiance-batch.d.ts +8 -2
- package/dist/facade/chart-radiance-batch.js +11 -4
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/chart-scenes.d.ts +134 -0
- package/dist/facade/chart-scenes.js +32 -22
- package/dist/facade/chart-scenes.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +8 -2
- package/dist/facade/diffraction-provider.js +71 -2
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.d.ts +18 -0
- package/dist/facade/display-view-gpu-session.js +280 -0
- package/dist/facade/display-view-gpu-session.js.map +1 -0
- package/dist/facade/display-view-gpu.d.ts +146 -0
- package/dist/facade/display-view-gpu.js +59 -0
- package/dist/facade/display-view-gpu.js.map +1 -0
- package/dist/facade/display-view.d.ts +48 -1
- package/dist/facade/display-view.js +7 -7
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +5 -0
- package/dist/facade/expected-signal-setup.js +16 -0
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +48 -3
- package/dist/facade/expected-signal.js +8 -0
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/scene-radiance.d.ts +4 -2
- package/dist/facade/scene-radiance.js +7 -4
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +32 -5
- package/dist/facade/simulate-capture.js +148 -24
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +31 -3
- package/dist/facade/simulate-display-view.js +72 -12
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.d.ts +119 -0
- package/dist/gpu/chart-composite-gpu.js +572 -0
- package/dist/gpu/chart-composite-gpu.js.map +1 -0
- package/dist/gpu/chart-radiance-gpu.d.ts +6 -6
- package/dist/gpu/chart-radiance-gpu.js +9 -11
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-chart-material-module.d.ts +10 -0
- package/dist/gpu/display-view-chart-material-module.js +58 -0
- package/dist/gpu/display-view-chart-material-module.js.map +1 -0
- package/dist/gpu/display-view-composite-material-module.d.ts +68 -0
- package/dist/gpu/display-view-composite-material-module.js +248 -0
- package/dist/gpu/display-view-composite-material-module.js.map +1 -0
- package/dist/gpu/display-view-quality-gpu.d.ts +185 -0
- package/dist/gpu/display-view-quality-gpu.js +1000 -0
- package/dist/gpu/display-view-quality-gpu.js.map +1 -0
- package/dist/gpu/display-view-speed-gpu.d.ts +156 -0
- package/dist/gpu/display-view-speed-gpu.js +524 -0
- package/dist/gpu/display-view-speed-gpu.js.map +1 -0
- package/dist/gpu/image-kernel-accelerator.d.ts +23 -4
- package/dist/gpu/image-kernel-accelerator.js +58 -10
- package/dist/gpu/image-kernel-accelerator.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +55 -3
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/gpu/sensor-stage-gpu.d.ts +11 -0
- package/dist/gpu/sensor-stage-gpu.js +42 -13
- package/dist/gpu/sensor-stage-gpu.js.map +1 -1
- package/dist/gpu/webgpu-runtime.d.ts +4 -2
- package/dist/gpu/webgpu-runtime.js +13 -4
- package/dist/gpu/webgpu-runtime.js.map +1 -1
- package/dist/gpu/webgpu-switch.d.ts +4 -0
- package/dist/gpu/webgpu-switch.js +29 -0
- package/dist/gpu/webgpu-switch.js.map +1 -0
- package/dist/gpu/wgsl-chart-composite-kernel.d.ts +52 -0
- package/dist/gpu/wgsl-chart-composite-kernel.js +515 -0
- package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -0
- package/dist/gpu/wgsl-chart-kernel.d.ts +11 -9
- package/dist/gpu/wgsl-chart-kernel.js +33 -27
- package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +95 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.js +769 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +75 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.js +386 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -0
- package/dist/gpu/wgsl-sensor-kernel.d.ts +28 -9
- package/dist/gpu/wgsl-sensor-kernel.js +94 -50
- package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/optics/defocus-disc-kernel.d.ts +61 -0
- package/dist/optics/defocus-disc-kernel.js +191 -0
- package/dist/optics/defocus-disc-kernel.js.map +1 -0
- package/docs/API_REFERENCE.md +45 -1
- package/docs/QUICK_START.md +102 -3
- package/docs/USAGE.md +52 -2
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +117 -2
- package/docs/api/facade-chart-material-layers.md +137 -0
- package/docs/api/facade-chart-radiance-batch.md +7 -2
- package/docs/api/facade-display-view-gpu.md +94 -0
- package/docs/api/facade-scene-radiance.md +3 -2
- package/docs/api/facade-simulate-capture.md +23 -6
- package/docs/api/facade-simulate-display-view.md +17 -2
- package/docs/api/gpu-image-kernel-accelerator.md +10 -3
- package/docs/api/gpu-image-kernel-execution.md +71 -4
- package/docs/api/gpu-webgpu-switch.md +23 -0
- package/package.json +3 -1
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Host side of the display view's Quality path on the GPU (#479): the support check, the float64 tables (the lattice groups
|
|
4
|
+
* and their combined diffraction kernels per material and channel, as the reference forms them; the bilinear and AHD
|
|
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:
|
|
9
|
+
* the quantization term, the seeded noise draw, the Cholesky factor, the clipped centre, the sRGB encode and the rounding),
|
|
10
|
+
* so the noise draw is bit-identical to the CPU's.
|
|
11
|
+
*
|
|
12
|
+
* Defocus (#492 step 2c): out of focus the expected signal carries the defocus disc (or each node's diffraction kernel
|
|
13
|
+
* convolved with it) as site kernels, up to radius 96. The site pass then reads a factored table, sums in blocks, and reads a
|
|
14
|
+
* lattice whose uncertain points the host resolved with the reference's own material lookup; a band's site pass is split over
|
|
15
|
+
* dispatches of at most `DISPLAY_VIEW_QUALITY_SITE_TAPS_PER_DISPATCH` taps. A defocused Speed request runs these passes with
|
|
16
|
+
* its bilinear demosaic (docs/WEBGPU.md, "Defocus on the GPU").
|
|
17
|
+
*
|
|
18
|
+
* The float64 reference is `renderDisplayView` (facade/display-view.ts) on the exact-area-mean path with
|
|
19
|
+
* `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.
|
|
21
|
+
*
|
|
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, a non-finite output) are recomputed with the
|
|
24
|
+
* reference, so they are exact. Every stage's wall-clock time is reported (`stageMilliseconds`); on a software adapter it
|
|
25
|
+
* is not GPU performance.
|
|
26
|
+
*/
|
|
27
|
+
import { AHD_KEY_LEVELS, EXACT_AREA_MEAN_MAX_PIXELS, axisSums, computeDisplayViewCorrectionStates, createDisplayViewExpectedSignal, displayViewCorrectionKey, overlaps, renderDisplayView, ahdCandidateKernels } from "../facade/display-view.js";
|
|
28
|
+
import { displayViewSignalTables } from "../facade/expected-signal.js";
|
|
29
|
+
import { resolvePortableChartScene } from "../facade/chart-scenes.js";
|
|
30
|
+
import { createDisplayViewMaterialModules } from "./display-view-composite-material-module.js";
|
|
31
|
+
import { setDisplayViewGpuRendererFactory } from "../facade/display-view-gpu.js";
|
|
32
|
+
import { chartPresetMaterialModule } from "./display-view-chart-material-module.js";
|
|
33
|
+
import { DISPLAY_VIEW_SPEED_CONFORMANCE_CASE, axisRecord, displayViewSpeedGpuSupports, displayViewSpeedGpuUnsupportedReason, finishDisplayViewPixel, gaussLegendre16, queryGeometry, renderDisplayViewSpeedOnGpu } from "./display-view-speed-gpu.js";
|
|
34
|
+
import { createHostYieldGate } from "../capture/host-yield-cadence.js";
|
|
35
|
+
import { createDefocusDiscSiteKernel } from "../optics/defocus-disc-kernel.js";
|
|
36
|
+
import { ImageKernelExecutionError, WEBGPU_F32_UNIT_ROUNDOFF, webGpuExecution } from "./image-kernel-execution.js";
|
|
37
|
+
import { BUFFER_USAGE, GpuResourceScope, abortedError, createComputePipeline, linearDispatch, runComputePass } from "./webgpu-runtime.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_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";
|
|
39
|
+
/** One WGSL rounding of + − × in either direction (W3C WGSL "Floating Point Accuracy"; #471, #487): twice the binary32 unit roundoff. */
|
|
40
|
+
const E = 2 * WEBGPU_F32_UNIT_ROUNDOFF;
|
|
41
|
+
/** Every reported bound is doubled: the kernel's running bounds are first order, and their own float32 arithmetic rounds. */
|
|
42
|
+
const MARGIN = 2;
|
|
43
|
+
/** The reference's own float64 rounding of its chains, relative. */
|
|
44
|
+
const CPU_RELATIVE = 2 ** -40;
|
|
45
|
+
/** Table values above this are outside the float32 domain. */
|
|
46
|
+
const FLOAT32_SAFE_MAGNITUDE = 2 ** 120;
|
|
47
|
+
/** Largest demosaic reach (AHD's candidates read 3 sites away) and lattice group count the tables hold. */
|
|
48
|
+
const MAXIMUM_REACH = 3, MAXIMUM_GROUPS = 64;
|
|
49
|
+
/**
|
|
50
|
+
* Largest site-kernel radius the gather takes: the diffraction cap (32 sites) plus the defocus disc's (64 sites), the largest
|
|
51
|
+
* combined radius the reference builds (#492; `buildExpectedSignalInput`).
|
|
52
|
+
*/
|
|
53
|
+
export const DISPLAY_VIEW_QUALITY_MAXIMUM_KERNEL_RADIUS = 96;
|
|
54
|
+
/**
|
|
55
|
+
* Default largest number of taps one site-pass dispatch gathers (sites × taps per site), so that a band of a large defocus
|
|
56
|
+
* kernel (up to 150 544 taps per site at radius 96) is split over several dispatches instead of one long one that an operating
|
|
57
|
+
* system's GPU watchdog could reset (Windows' default TDR delay is 2 s). A choice, not a measurement: 2³⁰ taps take 0.1 s at an
|
|
58
|
+
* assumed 10¹⁰ taps per second; at radius 94 it leaves about 7 100 sites (111 workgroups) per dispatch, fewer than a large GPU
|
|
59
|
+
* runs at once. In focus a band stays one dispatch (400 taps per site at f/8). The hardware run of #475 records the site pass's
|
|
60
|
+
* time and dispatch count (`stageMilliseconds.sites`, `siteDispatches`), which decide whether the budget or a gather split over
|
|
61
|
+
* several invocations per site is needed.
|
|
62
|
+
*/
|
|
63
|
+
export const DISPLAY_VIEW_QUALITY_SITE_TAPS_PER_DISPATCH = 2 ** 30;
|
|
64
|
+
/** The smallest positive normal float32: table values below it may be flushed to zero by a device (WGSL allows it). */
|
|
65
|
+
const FLOAT32_MINIMUM_NORMAL = 2 ** -126;
|
|
66
|
+
/** Candidate table entries per phase: the (2·reach + 1)² window. */
|
|
67
|
+
const WINDOW_SLOTS = (2 * MAXIMUM_REACH + 1) ** 2;
|
|
68
|
+
/**
|
|
69
|
+
* The host's output quantization term q(m) = s(e)²·Δ²/12 (`quantizationVariance`, s the sRGB EOTF's slope at the encoded mean e)
|
|
70
|
+
* is evaluated at the GPU's mean, so it moves with the mean's error: dq/dm = 2·s′(e)·Δ²/12 (de/dm = 1/s), with
|
|
71
|
+
* s′(e) = (2.4/1.055)·(1.4/1.055)·((e + 0.055)/1.055)^0.4 ≤ 3.02 on [0, 1] and 0 on the linear segment and outside [0, 1];
|
|
72
|
+
* q is non-decreasing in m, with a jump of (s₊² − s₋²)·Δ²/12 ≤ 2.1·10⁻⁴·Δ²/12 at the segment join (linear m = 0.0031308).
|
|
73
|
+
*/
|
|
74
|
+
const QUANTIZATION_SLOPE = 6.04, QUANTIZATION_JOIN_JUMP = 2.1e-4, QUANTIZATION_JOIN = 0.0031308;
|
|
75
|
+
/** Bytes per display pixel in the output buffers (moments plus the flag word). */
|
|
76
|
+
const OUTPUT_BYTES_PER_PIXEL = 4 * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS + 4;
|
|
77
|
+
const SITE_BYTES = 4 * DISPLAY_VIEW_QUALITY_SITE_FLOATS;
|
|
78
|
+
/**
|
|
79
|
+
* The low-signal correction states a renderer holds for one correction key, as the GPU table reads them: grid keys ascending
|
|
80
|
+
* with three float32 values each (the host keeps the float64 values; the table is their single rounding).
|
|
81
|
+
*/
|
|
82
|
+
export class CorrectionStateStore {
|
|
83
|
+
#known = new Map();
|
|
84
|
+
#keys = new Int32Array(0);
|
|
85
|
+
#values = new Float32Array(0);
|
|
86
|
+
#dirty = false;
|
|
87
|
+
get size() { return this.#known.size; }
|
|
88
|
+
has(key) { return this.#known.has(key); }
|
|
89
|
+
add(gridKeys, values) {
|
|
90
|
+
for (let i = 0; i < gridKeys.length; i++) {
|
|
91
|
+
if (this.#known.has(gridKeys[i]))
|
|
92
|
+
continue;
|
|
93
|
+
this.#known.set(gridKeys[i], [values[3 * i], values[3 * i + 1], values[3 * i + 2]]);
|
|
94
|
+
this.#dirty = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** The sorted table (rebuilt when states were added since the last call). */
|
|
98
|
+
table() {
|
|
99
|
+
if (this.#dirty) {
|
|
100
|
+
const keys = Int32Array.from(this.#known.keys()).sort(), values = new Float32Array(3 * keys.length);
|
|
101
|
+
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]; });
|
|
102
|
+
this.#keys = keys;
|
|
103
|
+
this.#values = values;
|
|
104
|
+
this.#dirty = false;
|
|
105
|
+
}
|
|
106
|
+
return { keys: this.#keys, values: this.#values };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function checkAbort(signal) {
|
|
110
|
+
if (signal.aborted)
|
|
111
|
+
throw abortedError();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Waits until the queue has finished the work submitted so far (`GPUQueue.onSubmittedWorkDone`), so a pass that reads nothing
|
|
115
|
+
* back is timed with its own execution, not the next pass's wait. The passes depend on each other in order anyway.
|
|
116
|
+
*/
|
|
117
|
+
async function queueIdle(runtime, signal) {
|
|
118
|
+
const queue = runtime.device.queue;
|
|
119
|
+
if (typeof queue.onSubmittedWorkDone === "function")
|
|
120
|
+
await runtime.guard(queue.onSubmittedWorkDone(), signal);
|
|
121
|
+
}
|
|
122
|
+
/** The footprint shape of a view: real pixels per display pixel along each axis, and whether the exact area mean applies. */
|
|
123
|
+
function footprintShape(setup) {
|
|
124
|
+
const sx = setup.frameWidth / setup.viewWidth, sy = setup.frameHeight / setup.viewHeight, area = sx * sy;
|
|
125
|
+
const exact = area <= Math.min(EXACT_AREA_MEAN_MAX_PIXELS, setup.samplesPerAxis * setup.samplesPerAxis);
|
|
126
|
+
let maximumColumns = 0, maximumRows = 0;
|
|
127
|
+
for (let i = 0; i < setup.region.width; i++)
|
|
128
|
+
maximumColumns = Math.max(maximumColumns, overlaps((setup.region.x + i) * sx, (setup.region.x + i + 1) * sx, setup.frameWidth).w.length);
|
|
129
|
+
for (let j = 0; j < setup.region.height; j++)
|
|
130
|
+
maximumRows = Math.max(maximumRows, overlaps((setup.region.y + j) * sy, (setup.region.y + j + 1) * sy, setup.frameHeight).w.length);
|
|
131
|
+
return { sx, sy, exact, maximumColumns, maximumRows };
|
|
132
|
+
}
|
|
133
|
+
/** The demosaic candidates of a setup (the bilinear kernels when it has none) and their reach in sites. */
|
|
134
|
+
function candidatesOf(setup) {
|
|
135
|
+
const candidates = setup.candidates ?? setup.demosaic.map((phase) => [phase]), count = candidates[0]?.length ?? 0;
|
|
136
|
+
const reach = Math.max(1, ...candidates.flat(3).map((t) => Math.max(Math.abs(t.dx), Math.abs(t.dy))));
|
|
137
|
+
return { candidates, count, reach };
|
|
138
|
+
}
|
|
139
|
+
/** Fractional bits of a float64 (the smallest b with c·2^b an integer), or Infinity beyond 60. */
|
|
140
|
+
function fractionalBits(c) {
|
|
141
|
+
for (let b = 0; b <= 60; b++)
|
|
142
|
+
if (Number.isInteger(c * 2 ** b))
|
|
143
|
+
return b;
|
|
144
|
+
return Infinity;
|
|
145
|
+
}
|
|
146
|
+
/** Whether p + c is exact in float32 for every integer p with |p| ≤ `span` (c has b fractional bits and |p + c| < 2^(24 − b)). */
|
|
147
|
+
function exactLatticeConstant(c, span) {
|
|
148
|
+
const b = fractionalBits(c);
|
|
149
|
+
return Math.fround(c) === c && b <= 22 && span + Math.abs(c) + 1 < 2 ** (24 - b);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Why the Quality kernels do not cover a request, or null when they do: chart materials, the exact area mean (footprints of
|
|
153
|
+
* at most 25 real pixels and 6 per axis), the identity or positive Reinhard tone curve, demosaic candidates within 3 sites
|
|
154
|
+
* (1 or 2 per phase), at most 64 lattice groups, lattice constants exact in float32, no collect pass, and a raster at least
|
|
155
|
+
* 2·reach + 2 sites on each axis (so a reflected border site lies in the band's window).
|
|
156
|
+
*/
|
|
157
|
+
export function displayViewQualityGpuUnsupportedReason(setup, signalInput) {
|
|
158
|
+
if (signalInput.scene.kind !== "materials")
|
|
159
|
+
return "radiance scenes (caller functions) are not on the GPU";
|
|
160
|
+
if (!signalInput.kernels && !signalInput.queryPoint)
|
|
161
|
+
return "the expected signal has no query points";
|
|
162
|
+
if (setup.ahdCollect)
|
|
163
|
+
return "a collect pass of the correction states runs on the CPU";
|
|
164
|
+
if (setup.toneCurve !== "identity" && setup.toneCurve !== "positive-reinhard-per-channel")
|
|
165
|
+
return "unsupported tone curve " + String(setup.toneCurve);
|
|
166
|
+
if (setup.region.width < 1 || setup.region.height < 1)
|
|
167
|
+
return "an empty region";
|
|
168
|
+
if (signalInput.kernels && (signalInput.kernels.radius < 0 || signalInput.kernels.radius > DISPLAY_VIEW_QUALITY_MAXIMUM_KERNEL_RADIUS))
|
|
169
|
+
return "a site kernel radius above " + DISPLAY_VIEW_QUALITY_MAXIMUM_KERNEL_RADIUS + " (the diffraction and defocus caps)";
|
|
170
|
+
const shape = footprintShape(setup);
|
|
171
|
+
if (!shape.exact)
|
|
172
|
+
return "the stratified estimator (footprints above 25 real pixels) is not on the GPU";
|
|
173
|
+
if (shape.maximumColumns > DISPLAY_VIEW_QUALITY_MAXIMUM_PIXELS_PER_AXIS || shape.maximumRows > DISPLAY_VIEW_QUALITY_MAXIMUM_PIXELS_PER_AXIS)
|
|
174
|
+
return "a footprint touches more than " + DISPLAY_VIEW_QUALITY_MAXIMUM_PIXELS_PER_AXIS + " real pixels along an axis";
|
|
175
|
+
const { candidates, count, reach } = candidatesOf(setup);
|
|
176
|
+
if (candidates.length !== 4 || (count !== 1 && count !== 2) || candidates.some((p) => p.length !== count || p.some((d) => d.length !== 3)))
|
|
177
|
+
return "each CFA phase needs 1 or 2 demosaic candidates with a kernel per channel";
|
|
178
|
+
if (reach > MAXIMUM_REACH)
|
|
179
|
+
return "demosaic kernels reaching more than " + MAXIMUM_REACH + " sites";
|
|
180
|
+
// The kernel treats a channel whose sites are all exactly at white as exact: its weights must sum exactly in float32 (dyadic, denominators to 256, magnitudes below 2^10).
|
|
181
|
+
for (const t of candidates.flat(3))
|
|
182
|
+
if (!Number.isInteger(t.weight * 256) || Math.abs(t.weight) >= 1024)
|
|
183
|
+
return "demosaic weights that are not dyadic rationals (denominators to 256)";
|
|
184
|
+
for (let p = 0; p < 4; p++)
|
|
185
|
+
for (let c = 0; c < 3; c++) {
|
|
186
|
+
const taps = setup.demosaic[p]?.[c];
|
|
187
|
+
if (!taps || taps.length < 1 || taps.length > 4 || taps.some((t) => Math.abs(t.dx) > 1 || Math.abs(t.dy) > 1))
|
|
188
|
+
return "the site-variance kernels are not the bilinear kernels (at most 4 taps within one site)";
|
|
189
|
+
}
|
|
190
|
+
if (setup.nativeWidth < 2 * reach + 2 || setup.nativeHeight < 2 * reach + 2)
|
|
191
|
+
return "a raster smaller than 2·reach + 2 sites";
|
|
192
|
+
const tables = displayViewSignalTables(signalInput);
|
|
193
|
+
if (tables.groups.length < 1 || tables.groups.length > MAXIMUM_GROUPS)
|
|
194
|
+
return "the kernel holds 1 to " + MAXIMUM_GROUPS + " lattice groups";
|
|
195
|
+
const W = signalInput.nativeWidth, H = signalInput.nativeHeight, R = signalInput.kernels ? signalInput.kernels.radius : 0;
|
|
196
|
+
if (!signalInput.kernels) {
|
|
197
|
+
// Without diffraction the reference queries each offset's own point; the lattice pass forms it as (x + ix + c)·scale with the
|
|
198
|
+
// group's shift, which is the same number only when ix + f equals the offset exactly (the grouping tolerates 1e-9).
|
|
199
|
+
for (const group of tables.groups)
|
|
200
|
+
for (const member of group.members) {
|
|
201
|
+
const exact = signalInput.offsets.some((o) => o.x === member.ix + group.fx && o.y === member.iy + group.fy && o.weight === member.weight);
|
|
202
|
+
if (!exact)
|
|
203
|
+
return "a query offset that its lattice group does not reproduce exactly";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
for (const group of tables.groups) {
|
|
207
|
+
const spanX = Math.max(Math.abs(group.minIx), Math.abs(group.maxIx)) + R + W, spanY = Math.max(Math.abs(group.minIy), Math.abs(group.maxIy)) + R + H;
|
|
208
|
+
if (!exactLatticeConstant(0.5 + group.fx - W / 2, spanX) || !exactLatticeConstant(0.5 + group.fy - H / 2, spanY))
|
|
209
|
+
return "a query offset whose lattice constant is not exact in float32";
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function signalGather(signalInput, factored) {
|
|
214
|
+
const tables = displayViewSignalTables(signalInput), scene = signalInput.scene;
|
|
215
|
+
const W = signalInput.nativeWidth, H = signalInput.nativeHeight, R = signalInput.kernels ? signalInput.kernels.radius : 0, materials = scene.materials.length;
|
|
216
|
+
// Taps in order: per group, with site kernels the dense rectangle row-major (tap (dx, dy) reads lattice offset (minIx − R + dx,
|
|
217
|
+
// minIy − R + dy)), without them one tap per member. Plain loops: a radius-96 kernel has up to 150 544 taps.
|
|
218
|
+
const tapGroup = [], tapDp = [], tapDq = [], tapIndex = [];
|
|
219
|
+
const groups = [];
|
|
220
|
+
tables.groups.forEach((group, g) => {
|
|
221
|
+
const first = tapGroup.length;
|
|
222
|
+
let dpMin = Infinity, dpMax = -Infinity, dqMin = Infinity, dqMax = -Infinity;
|
|
223
|
+
const push = (dp, dq, index) => {
|
|
224
|
+
tapGroup.push(g);
|
|
225
|
+
tapDp.push(dp);
|
|
226
|
+
tapDq.push(dq);
|
|
227
|
+
tapIndex.push(index);
|
|
228
|
+
dpMin = Math.min(dpMin, dp);
|
|
229
|
+
dpMax = Math.max(dpMax, dp);
|
|
230
|
+
dqMin = Math.min(dqMin, dq);
|
|
231
|
+
dqMax = Math.max(dqMax, dq);
|
|
232
|
+
};
|
|
233
|
+
if (signalInput.kernels) {
|
|
234
|
+
for (let dy = 0; dy < group.ch; dy++)
|
|
235
|
+
for (let dx = 0; dx < group.cw; dx++)
|
|
236
|
+
push(group.minIx - R + dx, group.minIy - R + dy, dy * group.cw + dx);
|
|
237
|
+
}
|
|
238
|
+
else
|
|
239
|
+
group.members.forEach((member, index) => push(member.ix, member.iy, index));
|
|
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,
|
|
241
|
+
cw: signalInput.kernels ? group.cw : 0, ch: signalInput.kernels ? group.ch : 0 });
|
|
242
|
+
});
|
|
243
|
+
const direct = (m, c) => { const si = tables.spectralIndex[m]; return si < 0 ? tables.reflectance[m] * tables.neutralDirect[c] : tables.spectralDirect[c][si]; };
|
|
244
|
+
const T = tapGroup.length, taps = new Uint32Array(T * DISPLAY_VIEW_QUALITY_TAP_WORDS), spread = new Float32Array(3 * T);
|
|
245
|
+
for (let t = 0; t < T; t++) {
|
|
246
|
+
const g = tapGroup[t], group = groups[g];
|
|
247
|
+
taps[t * DISPLAY_VIEW_QUALITY_TAP_WORDS] = g;
|
|
248
|
+
taps[t * DISPLAY_VIEW_QUALITY_TAP_WORDS + 1] = tapDp[t] - group.dpMin;
|
|
249
|
+
taps[t * DISPLAY_VIEW_QUALITY_TAP_WORDS + 2] = tapDq[t] - group.dqMin;
|
|
250
|
+
}
|
|
251
|
+
const check = (value) => {
|
|
252
|
+
if (!(Math.abs(value) <= FLOAT32_SAFE_MAGNITUDE))
|
|
253
|
+
throw new ImageKernelExecutionError("unsupported-float32-domain", "A material's channel signal exceeds the float32 domain.");
|
|
254
|
+
return value;
|
|
255
|
+
};
|
|
256
|
+
const tiny = new Float64Array(T);
|
|
257
|
+
if (factored && signalInput.kernels) {
|
|
258
|
+
// #492: one neutral row (the white's combined kernel, which the reference scales by each neutral material's reflectance) and one
|
|
259
|
+
// row per spectral material; the spread of a tap over the materials from the neutral reflectances' range and the spectral rows.
|
|
260
|
+
const spectralCount = tables.spectral.length, rows = 1 + spectralCount, table = new Float32Array(rows * 3 * T), materialRows = new Uint32Array(2 * materials);
|
|
261
|
+
const reflectanceBits = new Float32Array(materialRows.buffer);
|
|
262
|
+
let neutralLow = Infinity, neutralHigh = -Infinity, neutralSmallest = Infinity;
|
|
263
|
+
for (let m = 0; m < materials; m++) {
|
|
264
|
+
const si = tables.spectralIndex[m];
|
|
265
|
+
if (si < 0) {
|
|
266
|
+
const r = check(tables.reflectance[m]);
|
|
267
|
+
materialRows[2 * m] = 0;
|
|
268
|
+
reflectanceBits[2 * m + 1] = r;
|
|
269
|
+
neutralLow = Math.min(neutralLow, r);
|
|
270
|
+
neutralHigh = Math.max(neutralHigh, r);
|
|
271
|
+
if (r > 0)
|
|
272
|
+
neutralSmallest = Math.min(neutralSmallest, r);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
materialRows[2 * m] = 1 + si;
|
|
276
|
+
reflectanceBits[2 * m + 1] = 1;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (let t = 0; t < T; t++) {
|
|
280
|
+
const source = tables.groups[tapGroup[t]], index = tapIndex[t];
|
|
281
|
+
for (let c = 0; c < 3; c++) {
|
|
282
|
+
let low = Infinity, high = -Infinity;
|
|
283
|
+
const white = check(source.neutral[c][index]);
|
|
284
|
+
table[c * T + t] = white;
|
|
285
|
+
if (neutralHigh >= neutralLow) {
|
|
286
|
+
low = neutralLow * white;
|
|
287
|
+
high = neutralHigh * white;
|
|
288
|
+
// A neutral term r·N below the float32 normal range may be flushed: at most min(r_max·N, 2⁻¹²⁶).
|
|
289
|
+
if (neutralSmallest * white < FLOAT32_MINIMUM_NORMAL && white > 0)
|
|
290
|
+
tiny[t] = Math.max(tiny[t], Math.min(neutralHigh * white, FLOAT32_MINIMUM_NORMAL));
|
|
291
|
+
}
|
|
292
|
+
for (let si = 0; si < spectralCount; si++) {
|
|
293
|
+
const value = check(source.spectral[c][si][index]);
|
|
294
|
+
table[((1 + si) * 3 + c) * T + t] = value;
|
|
295
|
+
low = Math.min(low, value);
|
|
296
|
+
high = Math.max(high, value);
|
|
297
|
+
if (value > 0 && value < FLOAT32_MINIMUM_NORMAL)
|
|
298
|
+
tiny[t] = Math.max(tiny[t], value);
|
|
299
|
+
}
|
|
300
|
+
spread[c * T + t] = Math.fround((high - low) * (1 + 4 * E));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
let tinyMass = 0;
|
|
304
|
+
for (let t = 0; t < T; t++)
|
|
305
|
+
tinyMass += tiny[t];
|
|
306
|
+
return { groups, taps, tapCount: T, table, materialRows, spread, materials, tinyMass };
|
|
307
|
+
}
|
|
308
|
+
const table = new Float32Array(materials * 3 * T);
|
|
309
|
+
for (let t = 0; t < T; t++) {
|
|
310
|
+
const source = tables.groups[tapGroup[t]], index = tapIndex[t];
|
|
311
|
+
for (let c = 0; c < 3; c++) {
|
|
312
|
+
let low = Infinity, high = -Infinity;
|
|
313
|
+
for (let m = 0; m < materials; m++) {
|
|
314
|
+
const si = tables.spectralIndex[m];
|
|
315
|
+
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));
|
|
316
|
+
table[(m * 3 + c) * T + t] = value;
|
|
317
|
+
low = Math.min(low, value);
|
|
318
|
+
high = Math.max(high, value);
|
|
319
|
+
if (value > 0 && value < FLOAT32_MINIMUM_NORMAL)
|
|
320
|
+
tiny[t] = Math.max(tiny[t], value);
|
|
321
|
+
}
|
|
322
|
+
// The spread rounded up (a float32 rounding of the two values and of the difference).
|
|
323
|
+
spread[c * T + t] = Math.fround((high - low) * (1 + 4 * E));
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
let tinyMass = 0;
|
|
327
|
+
for (let t = 0; t < T; t++)
|
|
328
|
+
tinyMass += tiny[t];
|
|
329
|
+
return { groups, taps, tapCount: T, table, materialRows: null, spread, materials, tinyMass };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* 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
|
|
333
|
+
* (#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
|
|
334
|
+
* 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
|
|
335
|
+
* non-negative terms (docs/WEBGPU.md).
|
|
336
|
+
*/
|
|
337
|
+
function siteSumRelativeBound(gather, blocked) {
|
|
338
|
+
if (!blocked)
|
|
339
|
+
return (gather.tapCount + 3) * E;
|
|
340
|
+
let cw = 0, ch = 0;
|
|
341
|
+
for (const g of gather.groups) {
|
|
342
|
+
cw = Math.max(cw, g.cw);
|
|
343
|
+
ch = Math.max(ch, g.ch);
|
|
344
|
+
}
|
|
345
|
+
return (cw + ch + gather.groups.length + 3) * E;
|
|
346
|
+
}
|
|
347
|
+
/** The aux table: bilinear kernels and 1/Σw², the Gauss–Legendre rule, and the dense candidate weights per phase. */
|
|
348
|
+
function auxTable(setup) {
|
|
349
|
+
const aux = new Float32Array(DISPLAY_VIEW_QUALITY_AUX.size), words = new Uint32Array(aux.buffer);
|
|
350
|
+
for (let p = 0; p < 4; p++)
|
|
351
|
+
for (let c = 0; c < 3; c++) {
|
|
352
|
+
const taps = setup.demosaic[p][c], base = DISPLAY_VIEW_QUALITY_AUX.bilinear + (p * 3 + c) * 16;
|
|
353
|
+
let w2 = 0;
|
|
354
|
+
taps.forEach((t, i) => { aux[base + 4 * i] = t.dx; aux[base + 4 * i + 1] = t.dy; aux[base + 4 * i + 2] = t.weight; w2 += t.weight * t.weight; });
|
|
355
|
+
aux[DISPLAY_VIEW_QUALITY_AUX.inverseW2 + p * 3 + c] = 1 / w2;
|
|
356
|
+
}
|
|
357
|
+
const gl = gaussLegendre16();
|
|
358
|
+
aux.set(gl.x, DISPLAY_VIEW_QUALITY_AUX.gaussLegendre);
|
|
359
|
+
aux.set(gl.w, DISPLAY_VIEW_QUALITY_AUX.gaussLegendre + 16);
|
|
360
|
+
const { candidates, count, reach } = candidatesOf(setup), side = 2 * reach + 1;
|
|
361
|
+
for (let p = 0; p < 4; p++) {
|
|
362
|
+
// Dense weights per slot and candidate, as the reference accumulates them (taps at one slot add).
|
|
363
|
+
const dense = new Float64Array(2 * 3 * WINDOW_SLOTS);
|
|
364
|
+
candidates[p].forEach((candidate, d) => candidate.forEach((list, c) => { for (const t of list) {
|
|
365
|
+
const slot = (t.dy + reach) * side + t.dx + reach;
|
|
366
|
+
{
|
|
367
|
+
const at = (d * 3 + c) * WINDOW_SLOTS + slot;
|
|
368
|
+
dense[at] = dense[at] + t.weight;
|
|
369
|
+
}
|
|
370
|
+
} }));
|
|
371
|
+
const base = DISPLAY_VIEW_QUALITY_AUX.candidates + p * DISPLAY_VIEW_QUALITY_AUX.candidatePhaseStride;
|
|
372
|
+
let n = 0;
|
|
373
|
+
for (let slot = 0; slot < side * side; slot++) {
|
|
374
|
+
let used = false;
|
|
375
|
+
for (let k = 0; k < 2 * 3 && !used; k++)
|
|
376
|
+
if (dense[k * WINDOW_SLOTS + slot] !== 0)
|
|
377
|
+
used = true;
|
|
378
|
+
if (!used)
|
|
379
|
+
continue;
|
|
380
|
+
const entry = base + 4 + n * DISPLAY_VIEW_QUALITY_CANDIDATE_FLOATS;
|
|
381
|
+
aux[entry] = (slot % side) - reach;
|
|
382
|
+
aux[entry + 1] = Math.floor(slot / side) - reach;
|
|
383
|
+
for (let k = 0; k < 6; k++)
|
|
384
|
+
aux[entry + 2 + k] = dense[k * WINDOW_SLOTS + slot];
|
|
385
|
+
n++;
|
|
386
|
+
}
|
|
387
|
+
if (n > DISPLAY_VIEW_QUALITY_CANDIDATE_ENTRIES)
|
|
388
|
+
throw new ImageKernelExecutionError("unsupported-on-backend", "The demosaic candidates read more slots than the table holds.");
|
|
389
|
+
words[base] = n;
|
|
390
|
+
}
|
|
391
|
+
void count;
|
|
392
|
+
return aux;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* #492: replaces every lattice point the material module flagged (bit 31) by the reference's material at that point: group g's
|
|
396
|
+
* point (p, q) is (p + 0.5 + f_x − W/2)·g, −(q + 0.5 + f_y − H/2)·g (with f = 0 written as the reference writes it), looked up
|
|
397
|
+
* with the expected signal's own `materialAt`. Returns the count of points resolved.
|
|
398
|
+
*/
|
|
399
|
+
function resolveUncertainLattice(lattice, groupWords, signalInput) {
|
|
400
|
+
const tables = displayViewSignalTables(signalInput), scene = signalInput.scene;
|
|
401
|
+
const W = signalInput.nativeWidth, H = signalInput.nativeHeight, g = 2 / signalInput.outputWidth, groupInts = new Int32Array(groupWords.buffer, groupWords.byteOffset, groupWords.length);
|
|
402
|
+
let resolved = 0;
|
|
403
|
+
tables.groups.forEach((group, k) => {
|
|
404
|
+
const base = k * DISPLAY_VIEW_QUALITY_GROUP_WORDS, lx0 = groupInts[base], ly0 = groupInts[base + 1], lw = groupWords[base + 2], lh = groupWords[base + 3], first = groupWords[base + 4];
|
|
405
|
+
for (let local = 0; local < lw * lh; local++) {
|
|
406
|
+
const raw = lattice[first + local];
|
|
407
|
+
if ((raw >>> 31) === 0)
|
|
408
|
+
continue;
|
|
409
|
+
const p = lx0 + (local % lw), q = ly0 + Math.floor(local / lw);
|
|
410
|
+
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;
|
|
411
|
+
const m = scene.materialAt(u, v);
|
|
412
|
+
if (!(Number.isInteger(m) && m >= 0 && m < scene.materials.length))
|
|
413
|
+
throw new ImageKernelExecutionError("unsupported-on-backend", "webgpu-f32 display view (Quality): a lattice point has no scene material; use cpu-reference.");
|
|
414
|
+
lattice[first + local] = m;
|
|
415
|
+
resolved++;
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
return resolved;
|
|
419
|
+
}
|
|
420
|
+
/** The uniform parameters shared by the three passes (per-pass words are set per dispatch). */
|
|
421
|
+
function constants(setup, signalInput, gather, reach, count, blocked) {
|
|
422
|
+
const params = new ArrayBuffer(4 * DISPLAY_VIEW_QUALITY_PARAM_WORDS), words = new Uint32Array(params), ints = new Int32Array(params), floats = new Float32Array(params);
|
|
423
|
+
words[2] = setup.region.width;
|
|
424
|
+
words[3] = setup.region.x;
|
|
425
|
+
words[4] = setup.region.y;
|
|
426
|
+
words[5] = setup.margin;
|
|
427
|
+
words[6] = setup.nativeWidth;
|
|
428
|
+
words[7] = setup.nativeHeight;
|
|
429
|
+
words[8] = setup.region.width;
|
|
430
|
+
words[9] = setup.toneCurve === "positive-reinhard-per-channel" ? 1 : 0;
|
|
431
|
+
words[10] = reach;
|
|
432
|
+
words[11] = count;
|
|
433
|
+
words[16] = setup.ahdCorrection ? 1 : 0;
|
|
434
|
+
words[18] = gather.tapCount;
|
|
435
|
+
words[19] = gather.groups.length;
|
|
436
|
+
for (let c = 0; c < 3; c++)
|
|
437
|
+
words[21 + c] = setup.gains[c] === setup.clipLevel ? 1 : 0;
|
|
438
|
+
for (let c = 0; c < 3; c++) {
|
|
439
|
+
floats[24 + c] = setup.readout[c].perElectron;
|
|
440
|
+
floats[28 + c] = setup.readout[c].darkElectrons;
|
|
441
|
+
floats[32 + c] = setup.readout[c].constantVariance;
|
|
442
|
+
floats[36 + c] = setup.gains[c];
|
|
443
|
+
}
|
|
444
|
+
floats[27] = 8.5 * 8.5;
|
|
445
|
+
floats[31] = setup.clipLevel;
|
|
446
|
+
floats[35] = setup.ahdCorrection ? setup.ahdCorrection.thresholdVariance : 0;
|
|
447
|
+
for (let r = 0; r < 3; r++)
|
|
448
|
+
for (let c = 0; c < 3; c++)
|
|
449
|
+
floats[40 + r * 3 + c] = setup.matrix[r][c];
|
|
450
|
+
if (signalInput.kernels) {
|
|
451
|
+
const g = 2 / signalInput.outputWidth;
|
|
452
|
+
floats[39] = g;
|
|
453
|
+
floats[49] = -g;
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
// Without diffraction the reference queries the environment's own points; the lattice model (x + c)·scale is checked against them (queryGeometry).
|
|
457
|
+
const geometry = queryGeometry(signalInput);
|
|
458
|
+
floats[39] = geometry.scaleU;
|
|
459
|
+
floats[49] = geometry.scaleV;
|
|
460
|
+
}
|
|
461
|
+
floats[50] = siteSumRelativeBound(gather, blocked);
|
|
462
|
+
// #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.
|
|
463
|
+
if (blocked && gather.tinyMass > 0)
|
|
464
|
+
floats[52] = Math.max(FLOAT32_MINIMUM_NORMAL, Math.fround(gather.tinyMass * (1 + 4 * E)));
|
|
465
|
+
return { params, words, ints, floats };
|
|
466
|
+
}
|
|
467
|
+
/** The reference's correction setup of a display-view setup with the AHD correction. */
|
|
468
|
+
function correctionSetup(setup) {
|
|
469
|
+
return { seed: setup.seed, readout: setup.readout, gains: setup.gains, clipLevel: setup.clipLevel, matrix: setup.matrix, toneCurve: setup.toneCurve, ahdCorrection: setup.ahdCorrection };
|
|
470
|
+
}
|
|
471
|
+
/** How far the host's quantization term Σ(w/A)²·q(m) can move with a mean bound Δm (see QUANTIZATION_SLOPE), doubled. */
|
|
472
|
+
function quantizationBound(moments, o, meanBound, pixels, step) {
|
|
473
|
+
let join = 0;
|
|
474
|
+
for (let c = 0; c < 3; c++) {
|
|
475
|
+
const m = moments[o + c];
|
|
476
|
+
if (m - meanBound <= QUANTIZATION_JOIN && m + meanBound >= QUANTIZATION_JOIN)
|
|
477
|
+
join = QUANTIZATION_JOIN_JUMP;
|
|
478
|
+
}
|
|
479
|
+
return MARGIN * pixels * (QUANTIZATION_SLOPE * meanBound + join) * step * step / 12;
|
|
480
|
+
}
|
|
481
|
+
/** Renders one region of a Quality display view on the GPU (see the module comment). */
|
|
482
|
+
export async function renderDisplayViewQualityOnGpu(runtime, bandBytes, input, abort, yieldControl, pipelines) {
|
|
483
|
+
const { setup, signalInput, material } = input;
|
|
484
|
+
const reason = displayViewQualityGpuUnsupportedReason(setup, signalInput);
|
|
485
|
+
if (reason)
|
|
486
|
+
throw new ImageKernelExecutionError("unsupported-on-backend", "webgpu-f32 display view (Quality): " + reason + "; use cpu-reference.");
|
|
487
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
488
|
+
const timings = { lattice: 0, sites: 0, pixels: 0, states: 0, finishing: 0, recompute: 0, pixelRedispatches: 0, siteDispatches: 0 };
|
|
489
|
+
const { region, margin } = setup, shape = footprintShape(setup), { reach, count } = candidatesOf(setup);
|
|
490
|
+
// #492: the defocus disc's kernels are summed in blocks (their dense rectangles), in focus the tap list runs as before.
|
|
491
|
+
const blocked = input.blockedSum ?? (setup.gpu?.defocusRadius ?? 0) > 0;
|
|
492
|
+
if (blocked && !signalInput.kernels)
|
|
493
|
+
throw new ImageKernelExecutionError("unsupported-on-backend", "webgpu-f32 display view (Quality): the blocked site sum needs site kernels; use cpu-reference.");
|
|
494
|
+
const tapBudget = input.maximumSiteTapsPerDispatch ?? DISPLAY_VIEW_QUALITY_SITE_TAPS_PER_DISPATCH;
|
|
495
|
+
const gather = signalGather(signalInput, blocked), aux = auxTable(setup), { params, words, ints, floats } = constants(setup, signalInput, gather, reach, count, blocked);
|
|
496
|
+
const stride = DISPLAY_VIEW_QUALITY_GEOMETRY_STRIDE, geometry = new Float32Array((region.width + region.height) * stride);
|
|
497
|
+
const columnSums = [], rowSums = [];
|
|
498
|
+
for (let i = 0; i < region.width; i++) {
|
|
499
|
+
axisRecord(geometry, i * stride, (region.x + i) * shape.sx, (region.x + i + 1) * shape.sx, setup.frameWidth, margin, shape.sx);
|
|
500
|
+
columnSums.push(axisSums((region.x + i) * shape.sx, (region.x + i + 1) * shape.sx, setup.frameWidth, margin));
|
|
501
|
+
}
|
|
502
|
+
for (let j = 0; j < region.height; j++) {
|
|
503
|
+
axisRecord(geometry, (region.width + j) * stride, (region.y + j) * shape.sy, (region.y + j + 1) * shape.sy, setup.frameHeight, margin, shape.sy);
|
|
504
|
+
rowSums.push(axisSums((region.y + j) * shape.sy, (region.y + j + 1) * shape.sy, setup.frameHeight, margin));
|
|
505
|
+
}
|
|
506
|
+
const area = shape.sx * shape.sy, pixelCount = region.width * region.height;
|
|
507
|
+
// The band's site window: the region's native columns plus the reach, clamped to the raster (border sites are reflected into it).
|
|
508
|
+
const siteX0 = Math.max(0, Math.floor(region.x * shape.sx) + margin - reach), siteX1 = Math.min(setup.nativeWidth - 1, Math.min(setup.frameWidth - 1, Math.ceil((region.x + region.width) * shape.sx) - 1) + margin + reach);
|
|
509
|
+
const siteW = siteX1 - siteX0 + 1;
|
|
510
|
+
const siteRows = (row0, row1) => ({ y0: Math.max(0, Math.floor((region.y + row0) * shape.sy) + margin - reach),
|
|
511
|
+
y1: Math.min(setup.nativeHeight - 1, Math.min(setup.frameHeight - 1, Math.ceil((region.y + row1) * shape.sy) - 1) + margin + reach) });
|
|
512
|
+
const bandBytesOf = (rows) => {
|
|
513
|
+
const { y0, y1 } = siteRows(0, rows), siteH = y1 - y0 + 1;
|
|
514
|
+
let lattice = 0;
|
|
515
|
+
for (const g of gather.groups)
|
|
516
|
+
lattice += (siteW + g.dpSpan) * (siteH + g.dqSpan) * 4;
|
|
517
|
+
return lattice + siteW * siteH * SITE_BYTES + rows * region.width * OUTPUT_BYTES_PER_PIXEL;
|
|
518
|
+
};
|
|
519
|
+
let rows = region.height;
|
|
520
|
+
while (rows > 1 && bandBytesOf(rows) > bandBytes)
|
|
521
|
+
rows = Math.ceil(rows / 2);
|
|
522
|
+
// The value table (materials × 3 channels × taps) is one binding: with a large defocus kernel and many materials it can pass the
|
|
523
|
+
// device's limit, which is then refused by name (never clipped).
|
|
524
|
+
if (gather.table.byteLength > runtime.maximumStorageBindingBytes)
|
|
525
|
+
throw new ImageKernelExecutionError("insufficient-limits", "The display view's kernel table (" +
|
|
526
|
+
(gather.materialRows ? gather.table.length / (3 * gather.tapCount) + " rows (the neutral white and each spectral material)" : gather.materials + " materials") + " × 3 channels × " +
|
|
527
|
+
gather.tapCount + " taps per site, " + gather.table.byteLength + " bytes) exceeds the device's storage binding limit of " + runtime.maximumStorageBindingBytes + " bytes.");
|
|
528
|
+
const largestBinding = Math.max(bandBytesOf(rows), geometry.byteLength, gather.table.byteLength, 4 * DISPLAY_VIEW_QUALITY_BITMAP_WORDS);
|
|
529
|
+
if (largestBinding > runtime.maximumStorageBindingBytes)
|
|
530
|
+
throw new ImageKernelExecutionError("insufficient-limits", "One display-view band exceeds the device's storage binding limit.");
|
|
531
|
+
const latticePipeline = await pipelines("photivra-display-view-quality-lattice:" + material.id, assembleDisplayViewQualityLatticeWgsl(material.wgsl));
|
|
532
|
+
const sitesPipeline = blocked ? await pipelines("photivra-display-view-quality-sites-blocked", DISPLAY_VIEW_QUALITY_SITES_BLOCKED_WGSL) :
|
|
533
|
+
await pipelines("photivra-display-view-quality-sites", DISPLAY_VIEW_QUALITY_SITES_WGSL);
|
|
534
|
+
const pixelsPipeline = await pipelines("photivra-display-view-quality-pixels", DISPLAY_VIEW_QUALITY_PIXELS_WGSL);
|
|
535
|
+
const moments = new Float32Array(pixelCount * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS), pixelFlags = new Uint32Array(pixelCount);
|
|
536
|
+
const states = input.states ?? new CorrectionStateStore(), correction = setup.ahdCorrection ? correctionSetup(setup) : null;
|
|
537
|
+
let statesComputed = 0, latticePointsResolved = 0;
|
|
538
|
+
const scope = new GpuResourceScope(runtime);
|
|
539
|
+
try {
|
|
540
|
+
const auxBuffer = scope.upload("photivra-display-view-aux", aux, BUFFER_USAGE.STORAGE);
|
|
541
|
+
const geometryBuffer = scope.upload("photivra-display-view-geometry", geometry, BUFFER_USAGE.STORAGE);
|
|
542
|
+
// The blocked pass reads no tap records (its layout has no binding 2).
|
|
543
|
+
const tapsBuffer = blocked ? null : scope.upload("photivra-display-view-taps", gather.taps, BUFFER_USAGE.STORAGE);
|
|
544
|
+
const tableBuffer = scope.upload("photivra-display-view-table", gather.table, BUFFER_USAGE.STORAGE);
|
|
545
|
+
const spreadBuffer = scope.upload("photivra-display-view-spread", gather.spread, BUFFER_USAGE.STORAGE);
|
|
546
|
+
const materialRowsBuffer = gather.materialRows ? scope.upload("photivra-display-view-material-rows", gather.materialRows, BUFFER_USAGE.STORAGE) : null;
|
|
547
|
+
// A module's binding is data uploaded per render or a buffer the module already holds on the device (#491: scene records).
|
|
548
|
+
const materialEntries = material.bindings.map((b) => ({ binding: b.binding, resource: { buffer: b.buffer ??
|
|
549
|
+
scope.upload("photivra-display-view-material-" + b.binding, b.data, b.kind === "uniform" ? BUFFER_USAGE.UNIFORM : BUFFER_USAGE.STORAGE) } }));
|
|
550
|
+
const materialGroup = runtime.device.createBindGroup({ layout: latticePipeline.getBindGroupLayout(1), entries: materialEntries });
|
|
551
|
+
const gate = createHostYieldGate();
|
|
552
|
+
for (let row0 = 0; row0 < region.height; row0 += rows) {
|
|
553
|
+
checkAbort(abort.signal);
|
|
554
|
+
const row1 = Math.min(region.height, row0 + rows), count = (row1 - row0) * region.width, first = row0 * region.width;
|
|
555
|
+
const { y0: siteY0, y1: siteY1 } = siteRows(row0, row1), siteH = siteY1 - siteY0 + 1;
|
|
556
|
+
// Lattice windows per group, and the group records.
|
|
557
|
+
const groupWords = new Uint32Array(gather.groups.length * DISPLAY_VIEW_QUALITY_GROUP_WORDS), groupInts = new Int32Array(groupWords.buffer), groupFloats = new Float32Array(groupWords.buffer);
|
|
558
|
+
let latticeTotal = 0;
|
|
559
|
+
gather.groups.forEach((g, index) => {
|
|
560
|
+
const base = index * DISPLAY_VIEW_QUALITY_GROUP_WORDS, lw = siteW + g.dpSpan, lh = siteH + g.dqSpan;
|
|
561
|
+
groupInts[base] = siteX0 + g.dpMin;
|
|
562
|
+
groupInts[base + 1] = siteY0 + g.dqMin;
|
|
563
|
+
groupWords[base + 2] = lw;
|
|
564
|
+
groupWords[base + 3] = lh;
|
|
565
|
+
groupWords[base + 4] = latticeTotal;
|
|
566
|
+
groupWords[base + 5] = g.firstTap;
|
|
567
|
+
groupFloats[base + 6] = g.cu;
|
|
568
|
+
groupFloats[base + 7] = g.cv;
|
|
569
|
+
latticeTotal += lw * lh;
|
|
570
|
+
});
|
|
571
|
+
words[1] = row0;
|
|
572
|
+
ints[12] = siteX0;
|
|
573
|
+
ints[13] = siteY0;
|
|
574
|
+
words[14] = siteW;
|
|
575
|
+
words[15] = siteH;
|
|
576
|
+
words[20] = latticeTotal;
|
|
577
|
+
const band = new GpuResourceScope(runtime);
|
|
578
|
+
try {
|
|
579
|
+
const groupBuffer = band.upload("photivra-display-view-groups", groupWords, BUFFER_USAGE.STORAGE);
|
|
580
|
+
let latticeBuffer = band.buffer("photivra-display-view-lattice", latticeTotal * 4, BUFFER_USAGE.STORAGE | (blocked ? BUFFER_USAGE.COPY_SRC : 0));
|
|
581
|
+
const siteBuffer = band.buffer("photivra-display-view-sites", siteW * siteH * SITE_BYTES, BUFFER_USAGE.STORAGE);
|
|
582
|
+
// Pass 1: lattice materials.
|
|
583
|
+
let t0 = now();
|
|
584
|
+
words[0] = latticeTotal;
|
|
585
|
+
const latticeParams = band.upload("photivra-display-view-params-lattice", new Uint8Array(params.slice(0)), BUFFER_USAGE.UNIFORM);
|
|
586
|
+
const latticeGroup = runtime.device.createBindGroup({ layout: latticePipeline.getBindGroupLayout(0), entries: [
|
|
587
|
+
{ binding: 0, resource: { buffer: latticeParams } }, { binding: 1, resource: { buffer: groupBuffer } }, { binding: 2, resource: { buffer: latticeBuffer } }
|
|
588
|
+
] });
|
|
589
|
+
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);
|
|
590
|
+
await queueIdle(runtime, abort.signal);
|
|
591
|
+
if (blocked) {
|
|
592
|
+
// #492: with a wide disc nearly every site reads some uncertain lattice point, and its spread over every material would flag
|
|
593
|
+
// it. The host resolves each uncertain point with the reference's own material lookup at the reference's lattice point
|
|
594
|
+
// (createDisplayViewExpectedSignal's latticeU/latticeV), so the sites read exactly the reference's materials.
|
|
595
|
+
const lattice = new Uint32Array(latticeData, 0, latticeTotal);
|
|
596
|
+
const resolved = resolveUncertainLattice(lattice, groupWords, signalInput);
|
|
597
|
+
if (resolved > 0)
|
|
598
|
+
latticeBuffer = band.upload("photivra-display-view-lattice-resolved", lattice, BUFFER_USAGE.STORAGE);
|
|
599
|
+
latticePointsResolved += resolved;
|
|
600
|
+
}
|
|
601
|
+
timings.lattice += now() - t0;
|
|
602
|
+
// Pass 2: site moments, in chunks of whole workgroups of sites whose taps stay within the budget per dispatch (#492).
|
|
603
|
+
t0 = now();
|
|
604
|
+
const siteTotal = siteW * siteH, perDispatch = Math.max(DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE, Math.floor(tapBudget / Math.max(1, gather.tapCount) / DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE) * DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE);
|
|
605
|
+
for (let siteFirst = 0; siteFirst < siteTotal; siteFirst += perDispatch) {
|
|
606
|
+
checkAbort(abort.signal);
|
|
607
|
+
words[0] = Math.min(perDispatch, siteTotal - siteFirst);
|
|
608
|
+
words[51] = siteFirst;
|
|
609
|
+
const chunk = new GpuResourceScope(runtime);
|
|
610
|
+
try {
|
|
611
|
+
const siteParams = chunk.upload("photivra-display-view-params-sites", new Uint8Array(params.slice(0)), BUFFER_USAGE.UNIFORM);
|
|
612
|
+
const siteGroup = runtime.device.createBindGroup({ layout: sitesPipeline.getBindGroupLayout(0), entries: [
|
|
613
|
+
{ binding: 0, resource: { buffer: siteParams } }, { binding: 1, resource: { buffer: groupBuffer } }, ...(tapsBuffer ? [{ binding: 2, resource: { buffer: tapsBuffer } }] : []),
|
|
614
|
+
{ binding: 3, resource: { buffer: tableBuffer } }, { binding: 4, resource: { buffer: latticeBuffer } }, { binding: 5, resource: { buffer: siteBuffer } }, { binding: 6, resource: { buffer: spreadBuffer } },
|
|
615
|
+
...(materialRowsBuffer ? [{ binding: 7, resource: { buffer: materialRowsBuffer } }] : [])
|
|
616
|
+
] });
|
|
617
|
+
await runComputePass(runtime, chunk, sitesPipeline, siteGroup, linearDispatch(words[0], DISPLAY_VIEW_QUALITY_WORKGROUP_SIZE, runtime.maximumWorkgroupsPerDimension), [], abort.signal);
|
|
618
|
+
await queueIdle(runtime, abort.signal);
|
|
619
|
+
}
|
|
620
|
+
finally {
|
|
621
|
+
chunk.release();
|
|
622
|
+
}
|
|
623
|
+
timings.siteDispatches++;
|
|
624
|
+
}
|
|
625
|
+
words[51] = 0;
|
|
626
|
+
timings.sites += now() - t0;
|
|
627
|
+
// Pass 3: display pixels, repeated once when a correction state was missing and has been computed.
|
|
628
|
+
words[0] = count;
|
|
629
|
+
const out = band.buffer("photivra-display-view-moments", count * 4 * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS, BUFFER_USAGE.STORAGE | BUFFER_USAGE.COPY_SRC);
|
|
630
|
+
const flagBuffer = band.buffer("photivra-display-view-flags", count * 4, BUFFER_USAGE.STORAGE | BUFFER_USAGE.COPY_SRC);
|
|
631
|
+
const bitmapBuffer = band.buffer("photivra-display-view-bitmap", 4 * DISPLAY_VIEW_QUALITY_BITMAP_WORDS, BUFFER_USAGE.STORAGE | BUFFER_USAGE.COPY_SRC);
|
|
632
|
+
for (let attempt = 0;; attempt++) {
|
|
633
|
+
checkAbort(abort.signal);
|
|
634
|
+
t0 = now();
|
|
635
|
+
const table = states.table(), stateKeys = table.keys.length > 0 ? table.keys : new Int32Array(1), stateValues = table.values.length > 0 ? table.values : new Float32Array(3);
|
|
636
|
+
words[17] = table.keys.length;
|
|
637
|
+
const pass = new GpuResourceScope(runtime);
|
|
638
|
+
let data, flagData, bitmapData;
|
|
639
|
+
try {
|
|
640
|
+
const pixelParams = pass.upload("photivra-display-view-params-pixels", new Uint8Array(params.slice(0)), BUFFER_USAGE.UNIFORM);
|
|
641
|
+
const keysBuffer = pass.upload("photivra-display-view-state-keys", stateKeys, BUFFER_USAGE.STORAGE);
|
|
642
|
+
const valuesBuffer = pass.upload("photivra-display-view-state-values", stateValues, BUFFER_USAGE.STORAGE);
|
|
643
|
+
const pixelGroup = runtime.device.createBindGroup({ layout: pixelsPipeline.getBindGroupLayout(0), entries: [
|
|
644
|
+
{ binding: 0, resource: { buffer: pixelParams } }, { binding: 1, resource: { buffer: auxBuffer } }, { binding: 2, resource: { buffer: geometryBuffer } },
|
|
645
|
+
{ binding: 3, resource: { buffer: siteBuffer } }, { binding: 4, resource: { buffer: keysBuffer } }, { binding: 5, resource: { buffer: valuesBuffer } },
|
|
646
|
+
{ binding: 6, resource: { buffer: bitmapBuffer } }, { binding: 7, resource: { buffer: out } }, { binding: 8, resource: { buffer: flagBuffer } }
|
|
647
|
+
] });
|
|
648
|
+
[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 }, { buffer: bitmapBuffer, bytes: 4 * DISPLAY_VIEW_QUALITY_BITMAP_WORDS }], abort.signal);
|
|
649
|
+
}
|
|
650
|
+
finally {
|
|
651
|
+
pass.release();
|
|
652
|
+
}
|
|
653
|
+
timings.pixels += now() - t0;
|
|
654
|
+
const bandFlags = new Uint32Array(flagData, 0, count);
|
|
655
|
+
let missing = false;
|
|
656
|
+
for (let i = 0; i < count && !missing; i++)
|
|
657
|
+
if ((bandFlags[i] & DISPLAY_VIEW_QUALITY_FLAG.stateMissing) !== 0)
|
|
658
|
+
missing = true;
|
|
659
|
+
if (missing && correction && attempt === 0) {
|
|
660
|
+
// 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.
|
|
661
|
+
t0 = now();
|
|
662
|
+
const bitmap = new Uint32Array(bitmapData, 0, DISPLAY_VIEW_QUALITY_BITMAP_WORDS), needed = [];
|
|
663
|
+
for (let w = 0; w < DISPLAY_VIEW_QUALITY_BITMAP_WORDS; w++) {
|
|
664
|
+
let bitsWord = bitmap[w];
|
|
665
|
+
while (bitsWord !== 0) {
|
|
666
|
+
const bit = 31 - Math.clz32(bitsWord);
|
|
667
|
+
bitsWord &= ~(1 << bit);
|
|
668
|
+
const key = w * 32 + bit;
|
|
669
|
+
if (key < AHD_KEY_LEVELS ** 3 && !states.has(key))
|
|
670
|
+
needed.push(key);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (needed.length > 0) {
|
|
674
|
+
const computed = await computeDisplayViewCorrectionStates({ ...correction, yieldControl: async () => { if (yieldControl)
|
|
675
|
+
await yieldControl(abort.signal); }, checkAbort: () => checkAbort(abort.signal) }, Int32Array.from(needed));
|
|
676
|
+
states.add(computed.gridKeys, computed.values);
|
|
677
|
+
statesComputed += needed.length;
|
|
678
|
+
}
|
|
679
|
+
timings.states += now() - t0;
|
|
680
|
+
timings.pixelRedispatches++;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
moments.set(new Float32Array(data, 0, count * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS), first * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS);
|
|
684
|
+
pixelFlags.set(bandFlags, first);
|
|
685
|
+
break;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
finally {
|
|
689
|
+
band.release();
|
|
690
|
+
}
|
|
691
|
+
if (yieldControl && gate.due()) {
|
|
692
|
+
await yieldControl(abort.signal);
|
|
693
|
+
gate.restart();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
finally {
|
|
698
|
+
scope.release();
|
|
699
|
+
}
|
|
700
|
+
checkAbort(abort.signal);
|
|
701
|
+
// The reference's finishing per display pixel (float64).
|
|
702
|
+
let t0 = now();
|
|
703
|
+
const maximum = setup.bitDepth === 8 ? 255 : 65535, step = 1 / maximum;
|
|
704
|
+
const samples = setup.bitDepth === 8 ? new Uint8Array(pixelCount * 3) : new Uint16Array(pixelCount * 3);
|
|
705
|
+
const expected = new Float64Array(pixelCount * 3), noiseStd = new Float64Array(pixelCount * 3);
|
|
706
|
+
const meanBound = new Float32Array(pixelCount), varianceBound = new Float32Array(pixelCount);
|
|
707
|
+
const flagged = [];
|
|
708
|
+
let largestMean = 0, largestVariance = 0, sampleCount = 0, siteCount = 0, corrected = 0;
|
|
709
|
+
const counts = new Int32Array(geometry.buffer);
|
|
710
|
+
for (let ry = 0, i = 0; ry < region.height; ry++) {
|
|
711
|
+
const rowWw = rowSums[ry].ww, ny = counts[(region.width + ry) * stride + 1];
|
|
712
|
+
for (let rx = 0; rx < region.width; rx++, i++) {
|
|
713
|
+
const pixels = columnSums[rx].ww * rowWw / (area * area), vy = region.y + ry, vx = region.x + rx, o = i * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS;
|
|
714
|
+
let flag = (pixelFlags[i] & DISPLAY_VIEW_QUALITY_FLAG.recompute) !== 0;
|
|
715
|
+
for (let k = 0; k < DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS && !flag; k++)
|
|
716
|
+
if (!Number.isFinite(moments[o + k]))
|
|
717
|
+
flag = true;
|
|
718
|
+
if (flag) {
|
|
719
|
+
flagged.push(i);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
finishDisplayViewPixel(moments.subarray(o, o + 9), pixels, setup.seed, vy * setup.viewWidth + vx, step, maximum, samples, expected, noiseStd, i * 3);
|
|
723
|
+
meanBound[i] = MARGIN * moments[o + 9];
|
|
724
|
+
varianceBound[i] = MARGIN * moments[o + 10] + quantizationBound(moments, o, meanBound[i], pixels, step);
|
|
725
|
+
largestMean = Math.max(largestMean, meanBound[i]);
|
|
726
|
+
largestVariance = Math.max(largestVariance, varianceBound[i]);
|
|
727
|
+
if ((pixelFlags[i] & DISPLAY_VIEW_QUALITY_FLAG.corrected) !== 0)
|
|
728
|
+
corrected++;
|
|
729
|
+
const nx = counts[rx * stride + 1];
|
|
730
|
+
sampleCount += nx * ny;
|
|
731
|
+
siteCount += (nx + 2 * reach) * (ny + 2 * reach);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
timings.finishing = now() - t0;
|
|
735
|
+
// Flagged display pixels: the reference's own pixel (a one-pixel region render is byte-identical to the whole view's pixel).
|
|
736
|
+
t0 = now();
|
|
737
|
+
const recomputed = [];
|
|
738
|
+
if (flagged.length > 0) {
|
|
739
|
+
if (input.recomputeFlagged === false) {
|
|
740
|
+
// Without recomputation a flagged pixel keeps the kernel's values where they are finite (its bound then does not hold).
|
|
741
|
+
for (const i of flagged) {
|
|
742
|
+
const o = i * DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS;
|
|
743
|
+
let finite = true;
|
|
744
|
+
for (let k = 0; k < DISPLAY_VIEW_QUALITY_OUTPUT_FLOATS && finite; k++)
|
|
745
|
+
if (!Number.isFinite(moments[o + k]))
|
|
746
|
+
finite = false;
|
|
747
|
+
if (!finite)
|
|
748
|
+
continue;
|
|
749
|
+
const rx = i % region.width, ry = (i - rx) / region.width, pixels = columnSums[rx].ww * rowSums[ry].ww / (area * area);
|
|
750
|
+
finishDisplayViewPixel(moments.subarray(o, o + 9), pixels, setup.seed, (region.y + ry) * setup.viewWidth + region.x + rx, step, maximum, samples, expected, noiseStd, i * 3);
|
|
751
|
+
meanBound[i] = MARGIN * moments[o + 9];
|
|
752
|
+
varianceBound[i] = MARGIN * moments[o + 10] + quantizationBound(moments, o, meanBound[i], pixels, step);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
const signal = createDisplayViewExpectedSignal(signalInput);
|
|
757
|
+
for (const i of flagged) {
|
|
758
|
+
checkAbort(abort.signal);
|
|
759
|
+
const rx = i % region.width, ry = (i - rx) / region.width;
|
|
760
|
+
const pixel = await renderDisplayView({ ...setup, signal, region: { x: region.x + rx, y: region.y + ry, width: 1, height: 1 } });
|
|
761
|
+
samples.set(pixel.samples, i * 3);
|
|
762
|
+
expected.set(pixel.expected, i * 3);
|
|
763
|
+
noiseStd.set(pixel.noiseStd, i * 3);
|
|
764
|
+
meanBound[i] = 0;
|
|
765
|
+
varianceBound[i] = 0;
|
|
766
|
+
corrected += pixel.ahdCorrectedPixelCount;
|
|
767
|
+
sampleCount += pixel.sampleCount;
|
|
768
|
+
siteCount += pixel.siteCount;
|
|
769
|
+
recomputed.push(i);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
timings.recompute = now() - t0;
|
|
774
|
+
const relative = MARGIN * CPU_RELATIVE;
|
|
775
|
+
const expectedValueBound = { kind: "relative-to-reference", relative, absolute: largestMean };
|
|
776
|
+
const uncertain = flagged.length - recomputed.length;
|
|
777
|
+
return { samples, expected, noiseStd, sampleCount, siteCount, detailPixelCount: pixelCount, uniformPixelCount: 0, ahdCorrectedPixelCount: corrected, estimation: "exact-area-mean",
|
|
778
|
+
execution: webGpuExecution(expectedValueBound, uncertain), expectedValueBound, varianceBound: { kind: "relative-to-reference", relative, absolute: largestVariance },
|
|
779
|
+
expectedValueAbsoluteBound: meanBound, varianceAbsoluteBound: varianceBound, flaggedPixelIndices: Uint32Array.from(flagged), hostRecomputedPixelIndices: Uint32Array.from(recomputed),
|
|
780
|
+
pixelFlags, statesComputed, statesHeld: states.size, tapsPerSite: gather.tapCount, blockedSum: blocked, siteSumRelativeBound: floats[50], latticePointsResolved, stageMilliseconds: timings };
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* The GPU setup the material modules read for a Quality render (#491's `createDisplayViewMaterialModules`). The composite module
|
|
784
|
+
* recovers the exact lattice point from the kernel's float32 point with the request's query offsets and the scale of its own
|
|
785
|
+
* query points. With diffraction kernels the reference has no query points: it evaluates each lattice group's points with the
|
|
786
|
+
* lattice formula (p + 0.5 + f − W/2)·g, −(q + 0.5 + f − H/2)·g (`createDisplayViewExpectedSignal`). The lattice pass forms
|
|
787
|
+
* exactly that point per group, so the module is given the groups as its offsets and that formula as the query points; the
|
|
788
|
+
* module's own checks (the model at the frame's corners within 2⁻⁴⁸, the offsets' separation, at most 16 offsets) then apply to
|
|
789
|
+
* them as to a Speed lattice. Without diffraction the request's own offsets and query points are the lattice already (each
|
|
790
|
+
* group member's point is checked to be its offset's, `displayViewQualityGpuUnsupportedReason`).
|
|
791
|
+
*/
|
|
792
|
+
export function qualityMaterialSetup(gpu) {
|
|
793
|
+
const input = gpu.signalInput;
|
|
794
|
+
if (!input.kernels)
|
|
795
|
+
return gpu;
|
|
796
|
+
const groups = displayViewSignalTables(input).groups, W = input.nativeWidth, H = input.nativeHeight, g = 2 / input.outputWidth;
|
|
797
|
+
const offsets = groups.map((group) => ({ x: group.fx, y: group.fy, weight: 1 / groups.length }));
|
|
798
|
+
const queryPoint = (x, y, o, out) => {
|
|
799
|
+
const group = groups[o];
|
|
800
|
+
out[0] = (group.fx === 0 ? (x + 0.5 - W / 2) : (x + 0.5 + group.fx - W / 2)) * g;
|
|
801
|
+
out[1] = -(group.fy === 0 ? (y + 0.5 - H / 2) : (y + 0.5 + group.fy - H / 2)) * g;
|
|
802
|
+
};
|
|
803
|
+
const { kernels: _kernels, ...rest } = input;
|
|
804
|
+
void _kernels;
|
|
805
|
+
return { ...gpu, signalInput: { ...rest, offsets, queryPoint } };
|
|
806
|
+
}
|
|
807
|
+
/** The material module of a Quality render's scene (see `qualityMaterialSetup`). */
|
|
808
|
+
export function qualityMaterialModule(modules, gpu, signal) {
|
|
809
|
+
return modules.moduleFor(qualityMaterialSetup(gpu), signal);
|
|
810
|
+
}
|
|
811
|
+
/** The pixel-aperture samples per axis and anti-aliasing spots of a request. */
|
|
812
|
+
function offsetCount(request) { return request.boxSamples * request.boxSamples * (request.antiAliasing ? 4 : 1); }
|
|
813
|
+
/**
|
|
814
|
+
* Why the Quality renderer does not support a prepared request (`DisplayViewGpuRenderer.supports`), or true: engine charts and
|
|
815
|
+
* declared `material-layers` scenes (the material modules of `createDisplayViewMaterialModules`, #491), the identity or Reinhard
|
|
816
|
+
* tone curve and at most 64 query offsets (lattice groups). `lens.defocus` out of focus is supported (#492 step 2c: the site pass
|
|
817
|
+
* gathers the disc, or each node's diffraction kernel convolved with it, up to the reference's own caps of radius 64 and 96);
|
|
818
|
+
* whether a view's footprints take the exact area mean, whether the device passes the composite probe, and whether the kernel
|
|
819
|
+
* table fits the device's storage binding limit, is known only per render.
|
|
820
|
+
*/
|
|
821
|
+
export function displayViewQualityGpuSupports(request) {
|
|
822
|
+
if (request.scene !== "chart" && request.scene !== "material-layers")
|
|
823
|
+
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)";
|
|
824
|
+
if (request.toneCurve !== "identity" && request.toneCurve !== "positive-reinhard-per-channel")
|
|
825
|
+
return "unsupported tone curve " + String(request.toneCurve);
|
|
826
|
+
if (offsetCount(request) > MAXIMUM_GROUPS)
|
|
827
|
+
return "more than " + MAXIMUM_GROUPS + " query offsets per site";
|
|
828
|
+
return true;
|
|
829
|
+
}
|
|
830
|
+
/** A normalized, symmetric site kernel of radius R for the self-check (a binned Gaussian of width `sigma` sites). */
|
|
831
|
+
function syntheticKernel(radius, sigma) {
|
|
832
|
+
const size = 2 * radius + 1, kernel = new Float64Array(size * size);
|
|
833
|
+
let sum = 0;
|
|
834
|
+
for (let v = -radius; v <= radius; v++)
|
|
835
|
+
for (let u = -radius; u <= radius; u++) {
|
|
836
|
+
const w = Math.exp(-0.5 * (u * u + v * v) / (sigma * sigma));
|
|
837
|
+
kernel[(v + radius) * size + u + radius] = w;
|
|
838
|
+
sum += w;
|
|
839
|
+
}
|
|
840
|
+
for (let t = 0; t < kernel.length; t++)
|
|
841
|
+
kernel[t] = kernel[t] / sum;
|
|
842
|
+
return kernel;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* The renderer's run-time self-checks (`DisplayViewGpuRenderer.conformanceCases`): a 38 × 26 checkerboard frame of 6 µm sites
|
|
846
|
+
* shown at 13 × 9 (2.9 real pixels per display pixel) with the 2 × 2 box and a four-spot filter (16 offsets, four lattice groups),
|
|
847
|
+
* the GPU against `renderDisplayView` within the a-priori bounds, every flagged pixel recomputed:
|
|
848
|
+
*
|
|
849
|
+
* - `display-view:quality:checkerboard-13x9-diffraction-ahd` (#479): synthetic radius-1 diffraction kernels at three spectral
|
|
850
|
+
* nodes, the AHD candidates and the low-signal correction (few electrons per site, so the correction applies; 64 draws per
|
|
851
|
+
* state keep the check short), the tap-list site pass;
|
|
852
|
+
* - `display-view:quality:checkerboard-13x9-defocus-blocked` (#492): the engine's own defocus disc of 6.3 sites (radius 3) at
|
|
853
|
+
* every node, the bilinear demosaic (a defocused Speed request's route), the blocked site sum, and a tap budget that splits
|
|
854
|
+
* each band's site pass over several dispatches.
|
|
855
|
+
*/
|
|
856
|
+
async function runQualityConformance(handle, signal, defocus) {
|
|
857
|
+
const runtime = handle;
|
|
858
|
+
const nativeWidth = 40, nativeHeight = 28, margin = 1, frameWidth = 38, frameHeight = 26, nodes = [450, 550, 650];
|
|
859
|
+
const chart = resolvePortableChartScene({ scene: { preset: "checkerboard", squaresAcross: 6 }, aspect: frameHeight / frameWidth });
|
|
860
|
+
const offsets = [];
|
|
861
|
+
for (const [nx, ny] of [[-0.25, -0.25], [0.25, -0.25], [-0.25, 0.25], [0.25, 0.25]])
|
|
862
|
+
for (const [ax, ay] of [[-0.5, -0.5], [0.5, -0.5], [-0.5, 0.5], [0.5, 0.5]])
|
|
863
|
+
offsets.push({ x: nx - ax, y: ny - ay, weight: 1 / 16 });
|
|
864
|
+
const disc = defocus ? createDefocusDiscSiteKernel(6.3) : null;
|
|
865
|
+
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)] };
|
|
866
|
+
const signalInput = { nativeWidth, nativeHeight, outputWidth: frameWidth, wavelengthsNm: nodes,
|
|
867
|
+
electronsPerRadiance: defocus ? [Float64Array.of(9000, 30000, 4000), Float64Array.of(6000, 42000, 9000), Float64Array.of(30000, 9000, 3000)] :
|
|
868
|
+
[Float64Array.of(900, 3000, 400), Float64Array.of(600, 4200, 900), Float64Array.of(3000, 900, 300)], offsets, kernels,
|
|
869
|
+
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) },
|
|
870
|
+
channelAt: (x, y) => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2)) };
|
|
871
|
+
const readout = { perElectron: defocus ? 1 / 40000 : 1 / 4000, darkElectrons: 0.07, constantVariance: 2.5e-7 };
|
|
872
|
+
const bilinear = bilinearKernels();
|
|
873
|
+
const setup = { frameWidth, frameHeight, margin, nativeWidth, nativeHeight, viewWidth: 13, viewHeight: 9, region: { x: 0, y: 0, width: 13, height: 9 },
|
|
874
|
+
samplesPerAxis: 16, reachSites: 5 + (disc ? disc.radius : 0), bitDepth: 8, seed: 479, signal: createDisplayViewExpectedSignal(signalInput), readout: [readout, readout, readout], demosaic: bilinear,
|
|
875
|
+
...(defocus ? {} : { candidates: [0, 1, 2, 3].map((p) => ahdCandidateKernels(p % 2, Math.floor(p / 2))),
|
|
876
|
+
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 } }),
|
|
877
|
+
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",
|
|
878
|
+
yieldControl: async () => undefined, checkAbort: () => { if (signal?.aborted)
|
|
879
|
+
throw abortedError(); } };
|
|
880
|
+
const reference = await renderDisplayView(setup);
|
|
881
|
+
const abort = new AbortController(), onAbort = () => abort.abort();
|
|
882
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
883
|
+
try {
|
|
884
|
+
if (signal?.aborted)
|
|
885
|
+
abort.abort();
|
|
886
|
+
const gather = defocus ? { blockedSum: true, maximumSiteTapsPerDispatch: 64 * 4 * (2 * 3 + 2) ** 2 } : {};
|
|
887
|
+
const gpu = await renderDisplayViewQualityOnGpu(runtime, 1 << 20, { setup, signalInput, material: chartPresetMaterialModule(chart), ...gather }, abort, undefined, (label, code) => createComputePipeline(runtime, label, code, abort.signal));
|
|
888
|
+
if (!defocus && (gpu.ahdCorrectedPixelCount === 0 || reference.ahdCorrectedPixelCount === 0))
|
|
889
|
+
return "the self-check did not exercise the low-signal correction.";
|
|
890
|
+
if (defocus && (!gpu.blockedSum || gpu.stageMilliseconds.siteDispatches < 2))
|
|
891
|
+
return "the self-check did not exercise the blocked site sum over several dispatches.";
|
|
892
|
+
for (let i = 0; i < reference.expected.length; i++) {
|
|
893
|
+
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];
|
|
894
|
+
const expectedLimit = gpu.expectedValueBound.relative * Math.abs(e0) + gpu.expectedValueAbsoluteBound[p], varianceLimit = gpu.varianceBound.relative * v0 + gpu.varianceAbsoluteBound[p];
|
|
895
|
+
if (!(Math.abs(e1 - e0) <= expectedLimit))
|
|
896
|
+
return "component " + i + ": expected value " + e1 + " differs from the reference " + e0 + " by more than the a-priori bound " + expectedLimit + ".";
|
|
897
|
+
if (!(Math.abs(v1 - v0) <= varianceLimit))
|
|
898
|
+
return "component " + i + ": variance " + v1 + " differs from the reference " + v0 + " by more than the a-priori bound " + varianceLimit + ".";
|
|
899
|
+
if (Math.abs(gpu.samples[i] - reference.samples[i]) > 1)
|
|
900
|
+
return "component " + i + ": sample " + gpu.samples[i] + " differs from the reference " + reference.samples[i] + " by more than one code.";
|
|
901
|
+
}
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
finally {
|
|
905
|
+
signal?.removeEventListener("abort", onAbort);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/** #479's self-check (see `runQualityConformance`). */
|
|
909
|
+
export const DISPLAY_VIEW_QUALITY_CONFORMANCE_CASE = { id: "display-view:quality:checkerboard-13x9-diffraction-ahd",
|
|
910
|
+
run: (handle, signal) => runQualityConformance(handle, signal, false) };
|
|
911
|
+
/** #492's self-check of the defocus disc's blocked site sum (see `runQualityConformance`). */
|
|
912
|
+
export const DISPLAY_VIEW_QUALITY_DEFOCUS_CONFORMANCE_CASE = { id: "display-view:quality:checkerboard-13x9-defocus-blocked",
|
|
913
|
+
run: (handle, signal) => runQualityConformance(handle, signal, true) };
|
|
914
|
+
/** The bilinear RGGB demosaic kernels per phase (phaseY·2 + phaseX) and channel, as `bilinearPhaseProfiles` gives them. */
|
|
915
|
+
function bilinearKernels() {
|
|
916
|
+
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]];
|
|
917
|
+
const average = (offsets) => offsets.map(([dx, dy]) => ({ dx: dx, dy: dy, weight: 1 / offsets.length })), self = [{ dx: 0, dy: 0, weight: 1 }];
|
|
918
|
+
return [[self, average(cross), average(diagonal)], [average(horizontal), self, average(vertical)], [average(vertical), self, average(horizontal)], [average(diagonal), average(cross), self]];
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* The display view's GPU renderer on one runtime (#481's `DisplayViewGpuRenderer`): the Speed kernel for requests it covers
|
|
922
|
+
* (no diffraction, no AHD, no defocus disc, at most 16 offsets) and the Quality kernels for the rest (#492: a defocused Speed
|
|
923
|
+
* request too, with its bilinear demosaic). One pipeline cache per device, one set of
|
|
924
|
+
* material modules (#491: chart presets, composite charts, the 2.3 elements and material-layers scenes; `dispose` releases their
|
|
925
|
+
* uploaded scene records), one correction-state store per correction key. `render` renders the part's region (`setup.gpu`
|
|
926
|
+
* carries the expected signal's input and the chart; a setup without it, a coarse view or a frame the kernels do not cover
|
|
927
|
+
* throws `unsupported-on-backend`), and throws `aborted` and `device-lost` as the accelerator's operations do.
|
|
928
|
+
*/
|
|
929
|
+
export function createDisplayViewGpuRenderer(runtime, options = {}) {
|
|
930
|
+
const pipelines = new Map(), stores = new Map();
|
|
931
|
+
const bandBytes = options.maximumBandBytes ?? Math.min(64 * 1024 * 1024, runtime.maximumStorageBindingBytes);
|
|
932
|
+
const source = (label, code) => {
|
|
933
|
+
let pending = pipelines.get(label);
|
|
934
|
+
if (!pending) {
|
|
935
|
+
pending = createComputePipeline(runtime, label, code, undefined);
|
|
936
|
+
pipelines.set(label, pending);
|
|
937
|
+
pending.catch(() => pipelines.delete(label));
|
|
938
|
+
}
|
|
939
|
+
return pending;
|
|
940
|
+
};
|
|
941
|
+
// #491: one set of material modules per device for both kernels (the preset module per chart; the composite module, its probe
|
|
942
|
+
// and its uploaded scene records), released by `dispose`.
|
|
943
|
+
const modules = createDisplayViewMaterialModules(runtime, source);
|
|
944
|
+
return {
|
|
945
|
+
id: "photivra-display-view",
|
|
946
|
+
supports(request) {
|
|
947
|
+
// #492: out of focus the expected signal carries the defocus disc as site kernels, which only the Quality site pass gathers;
|
|
948
|
+
// a Speed request then runs the Quality passes with its own bilinear demosaic (one candidate) and finishing.
|
|
949
|
+
if (request.defocus)
|
|
950
|
+
return displayViewQualityGpuSupports(request);
|
|
951
|
+
const speed = displayViewSpeedGpuSupports(request);
|
|
952
|
+
if (speed === true)
|
|
953
|
+
return true;
|
|
954
|
+
return displayViewQualityGpuSupports(request);
|
|
955
|
+
},
|
|
956
|
+
conformanceCases: [DISPLAY_VIEW_SPEED_CONFORMANCE_CASE, DISPLAY_VIEW_QUALITY_CONFORMANCE_CASE, DISPLAY_VIEW_QUALITY_DEFOCUS_CONFORMANCE_CASE],
|
|
957
|
+
async render(part) {
|
|
958
|
+
const { setup, signal } = part;
|
|
959
|
+
if (!setup.gpu)
|
|
960
|
+
throw new ImageKernelExecutionError("unsupported-on-backend", "The display-view setup carries no GPU inputs (setup.gpu); use cpu-reference.");
|
|
961
|
+
const abort = new AbortController(), onAbort = () => abort.abort();
|
|
962
|
+
if (signal?.aborted)
|
|
963
|
+
throw abortedError();
|
|
964
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
965
|
+
try {
|
|
966
|
+
const signalInput = setup.gpu.signalInput;
|
|
967
|
+
let result;
|
|
968
|
+
let material;
|
|
969
|
+
if (displayViewSpeedGpuUnsupportedReason(setup, signalInput) === null) {
|
|
970
|
+
material = await modules.moduleFor(setup.gpu, abort.signal);
|
|
971
|
+
result = await renderDisplayViewSpeedOnGpu(runtime, bandBytes, { setup, signalInput, material }, abort, options.yieldControl, source);
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
material = await qualityMaterialModule(modules, setup.gpu, abort.signal);
|
|
975
|
+
const key = displayViewCorrectionKey(setup);
|
|
976
|
+
let states = key === null ? undefined : stores.get(key);
|
|
977
|
+
if (key !== null && !states) {
|
|
978
|
+
states = new CorrectionStateStore();
|
|
979
|
+
stores.set(key, states);
|
|
980
|
+
}
|
|
981
|
+
result = await renderDisplayViewQualityOnGpu(runtime, bandBytes, { setup, signalInput, material, ...(states ? { states } : {}) }, abort, options.yieldControl, source);
|
|
982
|
+
}
|
|
983
|
+
return { samples: result.samples, expected: result.expected, noiseStd: result.noiseStd, sampleCount: result.sampleCount, siteCount: result.siteCount, detailPixelCount: result.detailPixelCount,
|
|
984
|
+
uniformPixelCount: result.uniformPixelCount, ahdCorrectedPixelCount: result.ahdCorrectedPixelCount, estimation: result.estimation, errorBound: result.expectedValueBound,
|
|
985
|
+
diagnosticUncertaintySampleCount: result.execution.diagnosticUncertaintySampleCount, ...(material.compositeProbe ? { compositeProbe: material.compositeProbe } : {}) };
|
|
986
|
+
}
|
|
987
|
+
finally {
|
|
988
|
+
signal?.removeEventListener("abort", onAbort);
|
|
989
|
+
}
|
|
990
|
+
},
|
|
991
|
+
dispose() { modules.release(); stores.clear(); }
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
/** The factory `prepareDisplayView` creates the renderer with (one per device): the Speed and Quality kernels behind one renderer. */
|
|
995
|
+
export const displayViewGpuRendererFactory = (handle) => createDisplayViewGpuRenderer(handle);
|
|
996
|
+
/** Registers the combined renderer as the display view's GPU renderer and returns the factory registered before. */
|
|
997
|
+
export function registerDisplayViewGpuRenderer() {
|
|
998
|
+
return setDisplayViewGpuRendererFactory(displayViewGpuRendererFactory);
|
|
999
|
+
}
|
|
1000
|
+
//# sourceMappingURL=display-view-quality-gpu.js.map
|