@photivra/engine 2.10.0 → 2.12.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.
- package/CHANGELOG.md +156 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +19 -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 +108 -8
- package/dist/facade/simulate-capture.js +226 -33
- 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 +73 -13
- 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 +5 -2
- 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 +47 -1
- package/docs/QUICK_START.md +150 -5
- package/docs/USAGE.md +81 -3
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +127 -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 +95 -11
- 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,524 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Host side of the display view's Speed path on the GPU (#478): the support check, the float64 tables (site table,
|
|
4
|
+
* overlaps over the scale, axis sums over the squared scale, 1/Σw²), banded dispatch of
|
|
5
|
+
* `DISPLAY_VIEW_SPEED_KERNEL_WGSL`, and the finishing the CPU reference does after its per-sample loop: the output
|
|
6
|
+
* quantization term, the seeded noise draw, the Cholesky factor, the clipped-normal centre, the sRGB encode and the
|
|
7
|
+
* rounding, all with the reference's own float64 functions. So the noise draw is bit-identical to the CPU's, and only
|
|
8
|
+
* the expected value and the covariance carry the float32 bound.
|
|
9
|
+
*
|
|
10
|
+
* The float64 reference is `renderDisplayView` (facade/display-view.ts) on the exact-area-mean path without
|
|
11
|
+
* diffraction, with the bilinear demosaic and chart materials. Internal: not exported from the package. The
|
|
12
|
+
* material lookup per query point is a module the caller supplies (`DisplayViewGpuMaterialModule`, the seam of
|
|
13
|
+
* #477); `display-view-chart-material-module.ts` provides one for the eight 2.0 chart presets and
|
|
14
|
+
* `display-view-composite-material-module.ts` (#491) one for composite charts, the 2.3 elements and `material-layers` scenes.
|
|
15
|
+
*/
|
|
16
|
+
import { DISPLAY_VIEW_NOISE_STREAM_ID, EXACT_AREA_MEAN_MAX_PIXELS, axisSums, clipCentre, createDisplayViewExpectedSignal, overlaps, quantizationVariance, renderDisplayView } from "../facade/display-view.js";
|
|
17
|
+
import { resolvePortableChartScene } from "../facade/chart-scenes.js";
|
|
18
|
+
import { setDisplayViewGpuRendererFactory } from "../facade/display-view-gpu.js";
|
|
19
|
+
import { createDisplayViewMaterialModules } from "./display-view-composite-material-module.js";
|
|
20
|
+
import { chartPresetMaterialModule } from "./display-view-chart-material-module.js";
|
|
21
|
+
import { encodeSrgbComponentPortable } from "../color/srgb-transfer.js";
|
|
22
|
+
import { createDeterministicUniformStream, deriveNativeSiteStreamSeedUint32, samplePortableStandardNormal } from "../sensor/stochastic-seed.js";
|
|
23
|
+
import { createHostYieldGate } from "../capture/host-yield-cadence.js";
|
|
24
|
+
import { ImageKernelExecutionError, WEBGPU_F32_UNIT_ROUNDOFF, webGpuExecution } from "./image-kernel-execution.js";
|
|
25
|
+
import { BUFFER_USAGE, GpuResourceScope, abortedError, createComputePipeline, linearDispatch, runComputePass } from "./webgpu-runtime.js";
|
|
26
|
+
import { DISPLAY_VIEW_AUX, DISPLAY_VIEW_GEOMETRY_STRIDE, DISPLAY_VIEW_GPU_MAXIMUM_OFFSETS, DISPLAY_VIEW_GPU_MAXIMUM_PIXELS_PER_AXIS, DISPLAY_VIEW_OUTPUT_FLOATS, DISPLAY_VIEW_PARAM_WORDS, DISPLAY_VIEW_REINHARD_MEAN_U, DISPLAY_VIEW_REINHARD_VARIANCE_U, DISPLAY_VIEW_WORKGROUP_SIZE, assembleDisplayViewSpeedWgsl } from "./wgsl-display-view-speed-kernel.js";
|
|
27
|
+
/** One WGSL rounding of + − × in either direction (W3C WGSL "Floating Point Accuracy"; #471, #487): twice the binary32 unit roundoff. */
|
|
28
|
+
const E = 2 * WEBGPU_F32_UNIT_ROUNDOFF;
|
|
29
|
+
/** Higham's γ_k with E in place of u. */
|
|
30
|
+
const gammaE = (k) => k * E / (1 - k * E);
|
|
31
|
+
/** Every reported bound is doubled, as the sensor stage doubles its margins. */
|
|
32
|
+
const MARGIN = 2;
|
|
33
|
+
/** Table values above this are outside the float32 domain. */
|
|
34
|
+
const FLOAT32_SAFE_MAGNITUDE = 2 ** 120;
|
|
35
|
+
/** Roundings of the expected value's multiplicative chain (docs/WEBGPU.md, "Display view (Speed)"): site 9, readout 2, demosaic 4, WB 2, matrix 4, weights 3, up to 35 additions, 5 spare. */
|
|
36
|
+
const MEAN_ROUNDINGS = 64;
|
|
37
|
+
/** Roundings of the variance chain: the squares double the readout, demosaic, WB and matrix counts. */
|
|
38
|
+
const VARIANCE_ROUNDINGS = 80;
|
|
39
|
+
/** The reference's own float64 rounding of its chains, relative. */
|
|
40
|
+
const CPU_RELATIVE = 2 ** -40;
|
|
41
|
+
/**
|
|
42
|
+
* #496: the host's output quantization term q(m) = s(e)²·Δ²/12 (`quantizationVariance`, s the sRGB EOTF's slope at the encoded
|
|
43
|
+
* mean e) 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
|
|
44
|
+
* 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];
|
|
45
|
+
* 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).
|
|
46
|
+
* The Quality kernel's constants (#479).
|
|
47
|
+
*/
|
|
48
|
+
const QUANTIZATION_SLOPE = 6.04, QUANTIZATION_JOIN_JUMP = 2.1e-4, QUANTIZATION_JOIN = 0.0031308;
|
|
49
|
+
/** Workgroups per band are limited by the output buffer: DISPLAY_VIEW_OUTPUT_FLOATS floats plus one flag word per display pixel. */
|
|
50
|
+
const BYTES_PER_PIXEL = 4 * DISPLAY_VIEW_OUTPUT_FLOATS + 4;
|
|
51
|
+
function checkAbort(signal) {
|
|
52
|
+
if (signal.aborted)
|
|
53
|
+
throw abortedError();
|
|
54
|
+
}
|
|
55
|
+
/** The footprint shape of a view: real pixels per display pixel along each axis, and whether the exact area mean applies. */
|
|
56
|
+
function footprintShape(setup) {
|
|
57
|
+
const sx = setup.frameWidth / setup.viewWidth, sy = setup.frameHeight / setup.viewHeight, area = sx * sy;
|
|
58
|
+
const exact = area <= Math.min(EXACT_AREA_MEAN_MAX_PIXELS, setup.samplesPerAxis * setup.samplesPerAxis);
|
|
59
|
+
let maximumColumns = 0, maximumRows = 0;
|
|
60
|
+
for (let i = 0; i < setup.region.width; i++)
|
|
61
|
+
maximumColumns = Math.max(maximumColumns, overlaps((setup.region.x + i) * sx, (setup.region.x + i + 1) * sx, setup.frameWidth).w.length);
|
|
62
|
+
for (let j = 0; j < setup.region.height; j++)
|
|
63
|
+
maximumRows = Math.max(maximumRows, overlaps((setup.region.y + j) * sy, (setup.region.y + j + 1) * sy, setup.frameHeight).w.length);
|
|
64
|
+
return { sx, sy, exact, maximumColumns, maximumRows };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Why the kernel does not cover a request, or null when it does. The kernel is the Speed path: the exact area mean
|
|
68
|
+
* (views where a footprint holds at most 25 real pixels and at most K², and here at most 6 per axis), no diffraction
|
|
69
|
+
* kernels, chart materials, the bilinear demosaic (no AHD candidates or correction), the identity or positive Reinhard
|
|
70
|
+
* tone curve, and at most 16 query offsets.
|
|
71
|
+
*/
|
|
72
|
+
export function displayViewSpeedGpuUnsupportedReason(setup, signalInput) {
|
|
73
|
+
if (signalInput.kernels)
|
|
74
|
+
return "diffraction kernels (the Quality path) are not on the GPU";
|
|
75
|
+
if (signalInput.scene.kind !== "materials")
|
|
76
|
+
return "radiance scenes (caller functions) are not on the GPU";
|
|
77
|
+
if (!signalInput.queryPoint)
|
|
78
|
+
return "the expected signal has no query points";
|
|
79
|
+
if (setup.candidates || setup.ahdCorrection || setup.ahdCollect)
|
|
80
|
+
return "AHD candidates and the low-signal correction are not on the GPU";
|
|
81
|
+
if (setup.toneCurve !== "identity" && setup.toneCurve !== "positive-reinhard-per-channel")
|
|
82
|
+
return "unsupported tone curve " + String(setup.toneCurve);
|
|
83
|
+
if (signalInput.offsets.length < 1 || signalInput.offsets.length > DISPLAY_VIEW_GPU_MAXIMUM_OFFSETS)
|
|
84
|
+
return "the kernel holds 1 to " + DISPLAY_VIEW_GPU_MAXIMUM_OFFSETS + " query offsets";
|
|
85
|
+
if (setup.region.width < 1 || setup.region.height < 1)
|
|
86
|
+
return "an empty region";
|
|
87
|
+
const shape = footprintShape(setup);
|
|
88
|
+
if (!shape.exact)
|
|
89
|
+
return "the stratified estimator (footprints above 25 real pixels) is not on the GPU";
|
|
90
|
+
if (shape.maximumColumns > DISPLAY_VIEW_GPU_MAXIMUM_PIXELS_PER_AXIS || shape.maximumRows > DISPLAY_VIEW_GPU_MAXIMUM_PIXELS_PER_AXIS)
|
|
91
|
+
return "a footprint touches more than " + DISPLAY_VIEW_GPU_MAXIMUM_PIXELS_PER_AXIS + " real pixels along an axis";
|
|
92
|
+
for (let p = 0; p < 4; p++)
|
|
93
|
+
for (let c = 0; c < 3; c++) {
|
|
94
|
+
const taps = setup.demosaic[p]?.[c];
|
|
95
|
+
if (!taps || taps.length < 1 || taps.length > 4 || taps.some((t) => Math.abs(t.dx) > 1 || Math.abs(t.dy) > 1))
|
|
96
|
+
return "the demosaic is not the bilinear kernels (at most 4 taps within one site)";
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/** The site table: Σ_λ A_c(λ)·L_m(λ) per material and channel in float64 (the reference's `materialSignalDirect`), rounded once to float32. */
|
|
101
|
+
function siteTable(signalInput) {
|
|
102
|
+
const scene = signalInput.scene, A = signalInput.electronsPerRadiance, nodes = signalInput.wavelengthsNm.length;
|
|
103
|
+
const neutral = [0, 1, 2].map((c) => { let s = 0; for (let n = 0; n < nodes; n++)
|
|
104
|
+
s += A[c][n] * scene.whiteRadiance[n]; return s; });
|
|
105
|
+
const table = new Float32Array(scene.materials.length * 3);
|
|
106
|
+
scene.materials.forEach((m, index) => {
|
|
107
|
+
for (let c = 0; c < 3; c++) {
|
|
108
|
+
let value;
|
|
109
|
+
if ("neutral" in m)
|
|
110
|
+
value = m.neutral * neutral[c];
|
|
111
|
+
else {
|
|
112
|
+
value = 0;
|
|
113
|
+
for (let n = 0; n < nodes; n++)
|
|
114
|
+
value += A[c][n] * m.radiance[n];
|
|
115
|
+
}
|
|
116
|
+
if (!(Math.abs(value) <= FLOAT32_SAFE_MAGNITUDE))
|
|
117
|
+
throw new ImageKernelExecutionError("unsupported-float32-domain", "A material's channel signal exceeds the float32 domain.");
|
|
118
|
+
table[index * 3 + c] = value;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return table;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The query-point scale and offsets: the reference's query point of offset o at site (x, y) is, up to its own float64
|
|
125
|
+
* rounding, (x + 0.5 + o_x − W/2)·g and −(y + 0.5 + o_y − H/2)·g with g = 2/outputWidth frame units per site. The scale
|
|
126
|
+
* is taken from the reference's own query points (two sites a frame apart, divided in float64), so a request whose
|
|
127
|
+
* geometry differs is caught by the check against the reference at four corners (within 2^-40 relative).
|
|
128
|
+
*/
|
|
129
|
+
export function queryGeometry(signalInput) {
|
|
130
|
+
const W = signalInput.nativeWidth, H = signalInput.nativeHeight, point = new Float64Array(2), query = signalInput.queryPoint;
|
|
131
|
+
const x1 = W - 1, y1 = H - 1;
|
|
132
|
+
query(0, 0, 0, point);
|
|
133
|
+
const u0 = point[0], v0 = point[1];
|
|
134
|
+
query(x1, 0, 0, point);
|
|
135
|
+
const u1 = point[0];
|
|
136
|
+
query(0, y1, 0, point);
|
|
137
|
+
const v1 = point[1];
|
|
138
|
+
const scaleU = (u1 - u0) / x1, scaleV = (v1 - v0) / y1;
|
|
139
|
+
const offsetU = new Float64Array(signalInput.offsets.length), offsetV = new Float64Array(signalInput.offsets.length);
|
|
140
|
+
for (let o = 0; o < signalInput.offsets.length; o++) {
|
|
141
|
+
const offset = signalInput.offsets[o];
|
|
142
|
+
offsetU[o] = 0.5 + offset.x - W / 2;
|
|
143
|
+
offsetV[o] = 0.5 + offset.y - H / 2;
|
|
144
|
+
// The model (x + c)·scale against the reference at the corners: a geometry the model does not describe is refused.
|
|
145
|
+
for (const [x, y] of [[0, 0], [x1, 0], [0, y1], [x1, y1]]) {
|
|
146
|
+
query(x, y, o, point);
|
|
147
|
+
const mu = (x + offsetU[o]) * scaleU, mv = (y + offsetV[o]) * scaleV;
|
|
148
|
+
if (!(Math.abs(mu - point[0]) <= 2 ** -40 * (Math.abs(point[0]) + 1) && Math.abs(mv - point[1]) <= 2 ** -40 * (Math.abs(point[1]) + 1)))
|
|
149
|
+
throw new ImageKernelExecutionError("unsupported-on-backend", "The request's query points are not a rectilinear site lattice; use cpu-reference.");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { scaleU, scaleV, offsetU, offsetV };
|
|
153
|
+
}
|
|
154
|
+
/** Gauss–Legendre nodes and weights on [−1, 1], 16 points, by the reference's own construction (display-view.ts). */
|
|
155
|
+
export function gaussLegendre16() {
|
|
156
|
+
const n = 16, x = new Float64Array(n), w = new Float64Array(n);
|
|
157
|
+
for (let i = 0; i < n; i++) {
|
|
158
|
+
let z = Math.cos(Math.PI * (i + 0.75) / (n + 0.5)), dp = 0;
|
|
159
|
+
for (let it = 0; it < 100; it++) {
|
|
160
|
+
let p0 = 1, p1 = z;
|
|
161
|
+
for (let k = 2; k <= n; k++) {
|
|
162
|
+
const p2 = ((2 * k - 1) * z * p1 - (k - 1) * p0) / k;
|
|
163
|
+
p0 = p1;
|
|
164
|
+
p1 = p2;
|
|
165
|
+
}
|
|
166
|
+
dp = n * (z * p1 - p0) / (z * z - 1);
|
|
167
|
+
const dz = p1 / dp;
|
|
168
|
+
z -= dz;
|
|
169
|
+
if (Math.abs(dz) < 1e-16)
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
x[i] = z;
|
|
173
|
+
w[i] = 2 / ((1 - z * z) * dp * dp);
|
|
174
|
+
}
|
|
175
|
+
return { x, w };
|
|
176
|
+
}
|
|
177
|
+
/** One axis record of the geometry buffer: first real pixel, count, overlaps over the scale, axis sums over the squared scale. */
|
|
178
|
+
export function axisRecord(geometry, at, start, end, limit, margin, scale) {
|
|
179
|
+
const { first, w } = overlaps(start, end, limit), sums = axisSums(start, end, limit, margin), scale2 = scale * scale;
|
|
180
|
+
const words = new Int32Array(geometry.buffer, geometry.byteOffset, geometry.length);
|
|
181
|
+
words[at] = first;
|
|
182
|
+
words[at + 1] = w.length;
|
|
183
|
+
for (let i = 0; i < w.length; i++)
|
|
184
|
+
geometry[at + 2 + i] = w[i] / scale;
|
|
185
|
+
const order = ["bEven", "bOdd", "ppEven", "ppOdd", "qqEven", "qqOdd", "pqEven", "pqOdd", "ww"];
|
|
186
|
+
order.forEach((key, i) => { geometry[at + 8 + i] = sums[key] / scale2; });
|
|
187
|
+
}
|
|
188
|
+
/** The uniform parameters and the aux table of a render. */
|
|
189
|
+
function constants(setup, signalInput) {
|
|
190
|
+
const params = new ArrayBuffer(4 * DISPLAY_VIEW_PARAM_WORDS), words = new Uint32Array(params), floats = new Float32Array(params);
|
|
191
|
+
words[2] = setup.region.width;
|
|
192
|
+
words[3] = setup.region.x;
|
|
193
|
+
words[4] = setup.region.y;
|
|
194
|
+
words[5] = setup.margin;
|
|
195
|
+
words[6] = setup.nativeWidth;
|
|
196
|
+
words[7] = setup.nativeHeight;
|
|
197
|
+
words[8] = setup.region.width;
|
|
198
|
+
words[9] = setup.toneCurve === "positive-reinhard-per-channel" ? 1 : 0;
|
|
199
|
+
words[10] = signalInput.offsets.length;
|
|
200
|
+
for (let c = 0; c < 3; c++) {
|
|
201
|
+
floats[12 + c] = setup.readout[c].perElectron;
|
|
202
|
+
floats[16 + c] = setup.readout[c].darkElectrons;
|
|
203
|
+
floats[20 + c] = setup.readout[c].constantVariance;
|
|
204
|
+
floats[24 + c] = setup.gains[c];
|
|
205
|
+
}
|
|
206
|
+
floats[15] = 8.5 * 8.5;
|
|
207
|
+
floats[19] = setup.clipLevel;
|
|
208
|
+
for (let r = 0; r < 3; r++)
|
|
209
|
+
for (let c = 0; c < 3; c++)
|
|
210
|
+
floats[28 + r * 3 + c] = setup.matrix[r][c];
|
|
211
|
+
const geometry = queryGeometry(signalInput);
|
|
212
|
+
floats[37] = geometry.scaleU;
|
|
213
|
+
floats[38] = geometry.scaleV;
|
|
214
|
+
const aux = new Float32Array(DISPLAY_VIEW_AUX.size);
|
|
215
|
+
for (let o = 0; o < signalInput.offsets.length; o++) {
|
|
216
|
+
aux[DISPLAY_VIEW_AUX.offsets + 4 * o] = geometry.offsetU[o];
|
|
217
|
+
aux[DISPLAY_VIEW_AUX.offsets + 4 * o + 1] = geometry.offsetV[o];
|
|
218
|
+
aux[DISPLAY_VIEW_AUX.offsets + 4 * o + 2] = signalInput.offsets[o].weight;
|
|
219
|
+
}
|
|
220
|
+
for (let p = 0; p < 4; p++)
|
|
221
|
+
for (let c = 0; c < 3; c++) {
|
|
222
|
+
const taps = setup.demosaic[p][c], base = DISPLAY_VIEW_AUX.taps + (p * 3 + c) * 16;
|
|
223
|
+
let w2 = 0;
|
|
224
|
+
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; });
|
|
225
|
+
aux[DISPLAY_VIEW_AUX.inverseW2 + p * 3 + c] = 1 / w2;
|
|
226
|
+
}
|
|
227
|
+
const gl = gaussLegendre16();
|
|
228
|
+
aux.set(gl.x, DISPLAY_VIEW_AUX.gaussLegendre);
|
|
229
|
+
aux.set(gl.w, DISPLAY_VIEW_AUX.gaussLegendre + 16);
|
|
230
|
+
return { params, aux };
|
|
231
|
+
}
|
|
232
|
+
/** The a-priori relative bounds of the expected value and the variance (docs/WEBGPU.md, "Display view (Speed)"), doubled. */
|
|
233
|
+
export function displayViewSpeedGpuRelativeBounds(toneCurve) {
|
|
234
|
+
const reinhard = toneCurve === "positive-reinhard-per-channel";
|
|
235
|
+
return { mean: MARGIN * (gammaE(MEAN_ROUNDINGS) + CPU_RELATIVE + (reinhard ? DISPLAY_VIEW_REINHARD_MEAN_U * E : 0)),
|
|
236
|
+
variance: MARGIN * (gammaE(VARIANCE_ROUNDINGS) + CPU_RELATIVE + (reinhard ? DISPLAY_VIEW_REINHARD_VARIANCE_U * E : 0)) };
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The reference's finishing of one display pixel from its linear-light moments (display-view.ts, after the sample loop):
|
|
240
|
+
* the output quantization term, the seeded noise draw (`sensor.noiseSeed`, the display pixel's view index, stream 33),
|
|
241
|
+
* the Cholesky factor, the clipped-normal centre, the sRGB encode and the rounding; the same functions, so the draw and
|
|
242
|
+
* every operation after the moments are the CPU's. `moments` holds m0 m1 m2 v00 v11 v22 c01 c02 c12 (the variances
|
|
243
|
+
* without the quantization term); `pixels` is Σ_j (w_j/A)².
|
|
244
|
+
*/
|
|
245
|
+
export function finishDisplayViewPixel(moments, pixels, seed, index, step, maximum, samples, expected, noiseStd, o) {
|
|
246
|
+
const mean0 = moments[0], mean1 = moments[1], mean2 = moments[2];
|
|
247
|
+
const q0 = quantizationVariance(mean0, step), q1 = quantizationVariance(mean1, step), q2 = quantizationVariance(mean2, step);
|
|
248
|
+
const v00 = moments[3] + pixels * q0, v11 = moments[4] + pixels * q1, v22 = moments[5] + pixels * q2, v01 = moments[6], v02 = moments[7], v12 = moments[8];
|
|
249
|
+
const noise = createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, index, DISPLAY_VIEW_NOISE_STREAM_ID));
|
|
250
|
+
const z0 = samplePortableStandardNormal(noise), z1 = samplePortableStandardNormal(noise), z2 = samplePortableStandardNormal(noise);
|
|
251
|
+
const l00 = Math.sqrt(Math.max(0, v00)), l10 = l00 > 0 ? v01 / l00 : 0, l20 = l00 > 0 ? v02 / l00 : 0;
|
|
252
|
+
const l11 = Math.sqrt(Math.max(0, v11 - l10 * l10)), l21 = l11 > 0 ? (v12 - l20 * l10) / l11 : 0;
|
|
253
|
+
const l22 = Math.sqrt(Math.max(0, v22 - l20 * l20 - l21 * l21));
|
|
254
|
+
const n0 = clipCentre(mean0, v00) + l00 * z0, n1 = clipCentre(mean1, v11) + l10 * z0 + l11 * z1, n2 = clipCentre(mean2, v22) + l20 * z0 + l21 * z1 + l22 * z2;
|
|
255
|
+
samples[o] = Math.round(encodeSrgbComponentPortable(n0 < 0 ? 0 : n0 > 1 ? 1 : n0) * maximum);
|
|
256
|
+
samples[o + 1] = Math.round(encodeSrgbComponentPortable(n1 < 0 ? 0 : n1 > 1 ? 1 : n1) * maximum);
|
|
257
|
+
samples[o + 2] = Math.round(encodeSrgbComponentPortable(n2 < 0 ? 0 : n2 > 1 ? 1 : n2) * maximum);
|
|
258
|
+
expected[o] = mean0;
|
|
259
|
+
expected[o + 1] = mean1;
|
|
260
|
+
expected[o + 2] = mean2;
|
|
261
|
+
noiseStd[o] = Math.sqrt(Math.max(0, v00));
|
|
262
|
+
noiseStd[o + 1] = Math.sqrt(Math.max(0, v11));
|
|
263
|
+
noiseStd[o + 2] = Math.sqrt(Math.max(0, v22));
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* #496: how far the host's quantization term Σ(w/A)²·q(m) of a display pixel can move when each component's mean carries the
|
|
267
|
+
* bound relative·|m| + absolute (see QUANTIZATION_SLOPE), the largest over the three components, doubled. Added to the pixel's
|
|
268
|
+
* absolute variance bound; it changes no computed value.
|
|
269
|
+
*/
|
|
270
|
+
export function displayViewSpeedQuantizationBound(moments, relative, absolute, pixels, step) {
|
|
271
|
+
let largest = 0;
|
|
272
|
+
for (let c = 0; c < 3; c++) {
|
|
273
|
+
const m = moments[c], bound = relative * Math.abs(m) + absolute;
|
|
274
|
+
largest = Math.max(largest, QUANTIZATION_SLOPE * bound + (m - bound <= QUANTIZATION_JOIN && m + bound >= QUANTIZATION_JOIN ? QUANTIZATION_JOIN_JUMP : 0));
|
|
275
|
+
}
|
|
276
|
+
return MARGIN * pixels * largest * step * step / 12;
|
|
277
|
+
}
|
|
278
|
+
/** Renders one region of a Speed display view on the GPU (see the module comment). */
|
|
279
|
+
export async function renderDisplayViewSpeedOnGpu(runtime, bandBytes, input, abort, yieldControl, pipelines) {
|
|
280
|
+
const { setup, signalInput, material } = input;
|
|
281
|
+
const reason = displayViewSpeedGpuUnsupportedReason(setup, signalInput);
|
|
282
|
+
if (reason)
|
|
283
|
+
throw new ImageKernelExecutionError("unsupported-on-backend", "webgpu-f32 display view (Speed): " + reason + "; use cpu-reference.");
|
|
284
|
+
const { region, margin } = setup, shape = footprintShape(setup), table = siteTable(signalInput), { params, aux } = constants(setup, signalInput);
|
|
285
|
+
const stride = DISPLAY_VIEW_GEOMETRY_STRIDE, geometry = new Float32Array((region.width + region.height) * stride);
|
|
286
|
+
const columnSums = [], rowSums = [];
|
|
287
|
+
for (let i = 0; i < region.width; i++) {
|
|
288
|
+
axisRecord(geometry, i * stride, (region.x + i) * shape.sx, (region.x + i + 1) * shape.sx, setup.frameWidth, margin, shape.sx);
|
|
289
|
+
columnSums.push(axisSums((region.x + i) * shape.sx, (region.x + i + 1) * shape.sx, setup.frameWidth, margin));
|
|
290
|
+
}
|
|
291
|
+
for (let j = 0; j < region.height; j++) {
|
|
292
|
+
axisRecord(geometry, (region.width + j) * stride, (region.y + j) * shape.sy, (region.y + j + 1) * shape.sy, setup.frameHeight, margin, shape.sy);
|
|
293
|
+
rowSums.push(axisSums((region.y + j) * shape.sy, (region.y + j + 1) * shape.sy, setup.frameHeight, margin));
|
|
294
|
+
}
|
|
295
|
+
const area = shape.sx * shape.sy, pixelCount = region.width * region.height;
|
|
296
|
+
const rows = Math.max(1, Math.min(region.height, Math.floor(bandBytes / (region.width * BYTES_PER_PIXEL))));
|
|
297
|
+
if (Math.max(rows * region.width * 4 * DISPLAY_VIEW_OUTPUT_FLOATS, geometry.byteLength, table.byteLength) > runtime.maximumStorageBindingBytes)
|
|
298
|
+
throw new ImageKernelExecutionError("insufficient-limits", "One display-view band exceeds the device's storage binding limit.");
|
|
299
|
+
const pipeline = await pipelines("photivra-display-view-speed:" + material.id, assembleDisplayViewSpeedWgsl(material.wgsl));
|
|
300
|
+
const moments = new Float32Array(pixelCount * DISPLAY_VIEW_OUTPUT_FLOATS), flagged = [];
|
|
301
|
+
const words = new Uint32Array(params);
|
|
302
|
+
const scope = new GpuResourceScope(runtime);
|
|
303
|
+
try {
|
|
304
|
+
const auxBuffer = scope.upload("photivra-display-view-aux", aux, BUFFER_USAGE.STORAGE);
|
|
305
|
+
const geometryBuffer = scope.upload("photivra-display-view-geometry", geometry, BUFFER_USAGE.STORAGE);
|
|
306
|
+
const tableBuffer = scope.upload("photivra-display-view-table", table, BUFFER_USAGE.STORAGE);
|
|
307
|
+
const materialEntries = material.bindings.map((b) => ({ binding: b.binding, resource: { buffer: b.buffer ?? scope.upload("photivra-display-view-material-" + b.binding, b.data, b.kind === "uniform" ? BUFFER_USAGE.UNIFORM : BUFFER_USAGE.STORAGE) } }));
|
|
308
|
+
const materialGroup = runtime.device.createBindGroup({ layout: pipeline.getBindGroupLayout(1), entries: materialEntries });
|
|
309
|
+
const gate = createHostYieldGate();
|
|
310
|
+
for (let row0 = 0; row0 < region.height; row0 += rows) {
|
|
311
|
+
checkAbort(abort.signal);
|
|
312
|
+
const row1 = Math.min(region.height, row0 + rows), count = (row1 - row0) * region.width, first = row0 * region.width;
|
|
313
|
+
words[0] = count;
|
|
314
|
+
words[1] = row0;
|
|
315
|
+
const band = new GpuResourceScope(runtime);
|
|
316
|
+
try {
|
|
317
|
+
const out = band.buffer("photivra-display-view-moments", count * 4 * DISPLAY_VIEW_OUTPUT_FLOATS, BUFFER_USAGE.STORAGE | BUFFER_USAGE.COPY_SRC);
|
|
318
|
+
const flagBuffer = band.buffer("photivra-display-view-flags", count * 4, BUFFER_USAGE.STORAGE | BUFFER_USAGE.COPY_SRC);
|
|
319
|
+
const uniform = band.upload("photivra-display-view-params", new Uint8Array(params), BUFFER_USAGE.UNIFORM);
|
|
320
|
+
const bindGroup = runtime.device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [
|
|
321
|
+
{ binding: 0, resource: { buffer: uniform } }, { binding: 1, resource: { buffer: auxBuffer } }, { binding: 2, resource: { buffer: geometryBuffer } },
|
|
322
|
+
{ binding: 3, resource: { buffer: tableBuffer } }, { binding: 4, resource: { buffer: out } }, { binding: 5, resource: { buffer: flagBuffer } }
|
|
323
|
+
] });
|
|
324
|
+
const [data, flagData] = await runComputePass(runtime, band, pipeline, [bindGroup, materialGroup], linearDispatch(count, DISPLAY_VIEW_WORKGROUP_SIZE, runtime.maximumWorkgroupsPerDimension), [{ buffer: out, bytes: count * 4 * DISPLAY_VIEW_OUTPUT_FLOATS }, { buffer: flagBuffer, bytes: count * 4 }], abort.signal);
|
|
325
|
+
moments.set(new Float32Array(data, 0, count * DISPLAY_VIEW_OUTPUT_FLOATS), first * DISPLAY_VIEW_OUTPUT_FLOATS);
|
|
326
|
+
const bandFlags = new Uint32Array(flagData, 0, count);
|
|
327
|
+
for (let i = 0; i < count; i++) {
|
|
328
|
+
let flag = bandFlags[i] !== 0;
|
|
329
|
+
for (let k = 0; k < DISPLAY_VIEW_OUTPUT_FLOATS && !flag; k++)
|
|
330
|
+
if (!Number.isFinite(moments[(first + i) * DISPLAY_VIEW_OUTPUT_FLOATS + k]))
|
|
331
|
+
flag = true;
|
|
332
|
+
if (flag)
|
|
333
|
+
flagged.push(first + i);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
band.release();
|
|
338
|
+
}
|
|
339
|
+
if (yieldControl && gate.due()) {
|
|
340
|
+
await yieldControl(abort.signal);
|
|
341
|
+
gate.restart();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
scope.release();
|
|
347
|
+
}
|
|
348
|
+
checkAbort(abort.signal);
|
|
349
|
+
// The reference's finishing per display pixel (float64).
|
|
350
|
+
const maximum = setup.bitDepth === 8 ? 255 : 65535, step = 1 / maximum;
|
|
351
|
+
const samples = setup.bitDepth === 8 ? new Uint8Array(pixelCount * 3) : new Uint16Array(pixelCount * 3);
|
|
352
|
+
const expected = new Float64Array(pixelCount * 3), noiseStd = new Float64Array(pixelCount * 3);
|
|
353
|
+
const meanBound = new Float32Array(pixelCount), varianceBound = new Float32Array(pixelCount);
|
|
354
|
+
let largestMean = 0, largestVariance = 0, sampleCount = 0, siteCount = 0;
|
|
355
|
+
const counts = new Int32Array(geometry.buffer), relative = displayViewSpeedGpuRelativeBounds(setup.toneCurve);
|
|
356
|
+
for (let ry = 0, i = 0; ry < region.height; ry++) {
|
|
357
|
+
const rowWw = rowSums[ry].ww, ny = counts[(region.width + ry) * stride + 1];
|
|
358
|
+
for (let rx = 0; rx < region.width; rx++, i++) {
|
|
359
|
+
const pixels = columnSums[rx].ww * rowWw / (area * area), vy = region.y + ry, vx = region.x + rx, o = i * DISPLAY_VIEW_OUTPUT_FLOATS;
|
|
360
|
+
finishDisplayViewPixel(moments.subarray(o, o + 9), pixels, setup.seed, vy * setup.viewWidth + vx, step, maximum, samples, expected, noiseStd, i * 3);
|
|
361
|
+
meanBound[i] = MARGIN * moments[o + 9];
|
|
362
|
+
varianceBound[i] = MARGIN * moments[o + 10] + displayViewSpeedQuantizationBound(moments.subarray(o, o + 3), relative.mean, meanBound[i], pixels, step);
|
|
363
|
+
largestMean = Math.max(largestMean, meanBound[i]);
|
|
364
|
+
largestVariance = Math.max(largestVariance, varianceBound[i]);
|
|
365
|
+
const nx = counts[rx * stride + 1];
|
|
366
|
+
sampleCount += nx * ny;
|
|
367
|
+
siteCount += (nx + 2) * (ny + 2);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Flagged display pixels: the reference's own pixel (a one-pixel region render is byte-identical to the whole view's pixel).
|
|
371
|
+
const recomputed = [];
|
|
372
|
+
if (input.recomputeFlagged !== false && flagged.length > 0) {
|
|
373
|
+
const signal = createDisplayViewExpectedSignal(signalInput);
|
|
374
|
+
for (const i of flagged) {
|
|
375
|
+
checkAbort(abort.signal);
|
|
376
|
+
const rx = i % region.width, ry = (i - rx) / region.width;
|
|
377
|
+
const pixel = await renderDisplayView({ ...setup, signal, region: { x: region.x + rx, y: region.y + ry, width: 1, height: 1 } });
|
|
378
|
+
samples.set(pixel.samples, i * 3);
|
|
379
|
+
expected.set(pixel.expected, i * 3);
|
|
380
|
+
noiseStd.set(pixel.noiseStd, i * 3);
|
|
381
|
+
meanBound[i] = 0;
|
|
382
|
+
varianceBound[i] = 0;
|
|
383
|
+
recomputed.push(i);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const expectedValueBound = { kind: "relative-to-reference", relative: relative.mean, absolute: largestMean };
|
|
387
|
+
const uncertain = flagged.length - recomputed.length;
|
|
388
|
+
return { samples, expected, noiseStd, sampleCount, siteCount, detailPixelCount: pixelCount, uniformPixelCount: 0, ahdCorrectedPixelCount: 0, estimation: "exact-area-mean",
|
|
389
|
+
execution: webGpuExecution(expectedValueBound, uncertain), expectedValueBound,
|
|
390
|
+
varianceBound: { kind: "relative-to-reference", relative: relative.variance, absolute: largestVariance },
|
|
391
|
+
expectedValueAbsoluteBound: meanBound, varianceAbsoluteBound: varianceBound,
|
|
392
|
+
flaggedPixelIndices: Uint32Array.from(flagged), hostRecomputedPixelIndices: Uint32Array.from(recomputed) };
|
|
393
|
+
}
|
|
394
|
+
/** The pixel-aperture samples per axis and anti-aliasing spots a request may have for the kernel's offset table. */
|
|
395
|
+
function offsetCount(request) { return request.boxSamples * request.boxSamples * (request.antiAliasing ? 4 : 1); }
|
|
396
|
+
/**
|
|
397
|
+
* Why the Speed renderer does not support a prepared request (`DisplayViewGpuRenderer.supports`), or true. It covers every
|
|
398
|
+
* engine chart (the 2.0 presets through `display-view-chart-material-module.ts`; composites, the 2.3 elements, the sine star
|
|
399
|
+
* and fixed-size charts through the composite module, #491) and declared `material-layers` scenes, never a caller scene
|
|
400
|
+
* function, without AHD or diffraction, with the identity or Reinhard tone curve and at most 16 query offsets; whether a
|
|
401
|
+
* view's footprints take the exact area mean, and whether the device passes the composite kernel's probe, is known only per
|
|
402
|
+
* render (`displayViewSpeedGpuUnsupportedReason`, `createDisplayViewMaterialModules`).
|
|
403
|
+
*/
|
|
404
|
+
export function displayViewSpeedGpuSupports(request) {
|
|
405
|
+
if (request.ahd)
|
|
406
|
+
return "AHD (the Quality demosaic) is not on the GPU";
|
|
407
|
+
if (request.diffraction)
|
|
408
|
+
return "diffraction (the Quality optics) is not on the GPU";
|
|
409
|
+
if (request.scene !== "chart" && request.scene !== "material-layers")
|
|
410
|
+
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)";
|
|
411
|
+
if (request.toneCurve !== "identity" && request.toneCurve !== "positive-reinhard-per-channel")
|
|
412
|
+
return "unsupported tone curve " + String(request.toneCurve);
|
|
413
|
+
if (offsetCount(request) > DISPLAY_VIEW_GPU_MAXIMUM_OFFSETS)
|
|
414
|
+
return "more than " + DISPLAY_VIEW_GPU_MAXIMUM_OFFSETS + " query offsets per site";
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
/** The bilinear RGGB demosaic kernels per phase (phaseY·2 + phaseX) and channel, as `bilinearPhaseProfiles` gives them, for the self-check. */
|
|
418
|
+
function bilinearKernels() {
|
|
419
|
+
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]];
|
|
420
|
+
const average = (offsets) => offsets.map(([dx, dy]) => ({ dx: dx, dy: dy, weight: 1 / offsets.length })), self = [{ dx: 0, dy: 0, weight: 1 }];
|
|
421
|
+
return [[self, average(cross), average(diagonal)], [average(horizontal), self, average(vertical)], [average(vertical), self, average(horizontal)], [average(diagonal), average(cross), self]];
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* The renderer's run-time self-check (`DisplayViewGpuRenderer.conformanceCases`): a 38 × 26 checkerboard frame of 6 µm sites
|
|
425
|
+
* shown at 13 × 9 (2.9 real pixels per display pixel) with the 2 × 2 box, light squares at the white clip, the GPU against
|
|
426
|
+
* `renderDisplayView` within the a-priori bounds, every flagged pixel recomputed.
|
|
427
|
+
*/
|
|
428
|
+
export const DISPLAY_VIEW_SPEED_CONFORMANCE_CASE = { id: "display-view:speed:checkerboard-13x9", async run(handle, signal) {
|
|
429
|
+
// The handle is the engine's own runtime (the contract keeps WebGPU types out of the published declarations).
|
|
430
|
+
const runtime = handle;
|
|
431
|
+
const nativeWidth = 40, nativeHeight = 28, margin = 1, frameWidth = 38, frameHeight = 26, g = 2 / frameWidth, nodes = [450, 550, 650];
|
|
432
|
+
const chart = resolvePortableChartScene({ scene: { preset: "checkerboard", squaresAcross: 6 }, aspect: frameHeight / frameWidth });
|
|
433
|
+
const offsets = [[-0.25, -0.25], [0.25, -0.25], [-0.25, 0.25], [0.25, 0.25]].map(([x, y]) => ({ x: x, y: y, weight: 0.25 }));
|
|
434
|
+
const signalInput = { nativeWidth, nativeHeight, outputWidth: frameWidth, wavelengthsNm: nodes,
|
|
435
|
+
electronsPerRadiance: [Float64Array.of(9000, 30000, 4000), Float64Array.of(6000, 42000, 9000), Float64Array.of(30000, 9000, 3000)], offsets,
|
|
436
|
+
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) },
|
|
437
|
+
channelAt: (x, y) => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2)),
|
|
438
|
+
queryPoint: (x, y, o, out) => { out[0] = (x + 0.5 + offsets[o].x - nativeWidth / 2) * g; out[1] = -(y + 0.5 + offsets[o].y - nativeHeight / 2) * g; } };
|
|
439
|
+
const readout = { perElectron: 1 / 60000, darkElectrons: 0.07, constantVariance: 1.5e-9 };
|
|
440
|
+
const setup = { frameWidth, frameHeight, margin, nativeWidth, nativeHeight, viewWidth: 13, viewHeight: 9, region: { x: 0, y: 0, width: 13, height: 9 },
|
|
441
|
+
samplesPerAxis: 16, reachSites: 2, bitDepth: 8, seed: 478, signal: createDisplayViewExpectedSignal(signalInput), readout: [readout, readout, readout], demosaic: bilinearKernels(),
|
|
442
|
+
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",
|
|
443
|
+
yieldControl: async () => undefined, checkAbort: () => { if (signal?.aborted)
|
|
444
|
+
throw abortedError(); } };
|
|
445
|
+
const reference = await renderDisplayView(setup);
|
|
446
|
+
const abort = new AbortController(), onAbort = () => abort.abort();
|
|
447
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
448
|
+
try {
|
|
449
|
+
if (signal?.aborted)
|
|
450
|
+
abort.abort();
|
|
451
|
+
const gpu = await renderDisplayViewSpeedOnGpu(runtime, 1 << 20, { setup, signalInput, material: chartPresetMaterialModule(chart) }, abort, undefined, (label, code) => createComputePipeline(runtime, label, code, abort.signal));
|
|
452
|
+
for (let i = 0; i < reference.expected.length; i++) {
|
|
453
|
+
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];
|
|
454
|
+
const expectedLimit = gpu.expectedValueBound.relative * Math.abs(e0) + gpu.expectedValueAbsoluteBound[p], varianceLimit = gpu.varianceBound.relative * v0 + gpu.varianceAbsoluteBound[p];
|
|
455
|
+
if (!(Math.abs(e1 - e0) <= expectedLimit))
|
|
456
|
+
return "component " + i + ": expected value " + e1 + " differs from the reference " + e0 + " by more than the a-priori bound " + expectedLimit + ".";
|
|
457
|
+
if (!(Math.abs(v1 - v0) <= varianceLimit))
|
|
458
|
+
return "component " + i + ": variance " + v1 + " differs from the reference " + v0 + " by more than the a-priori bound " + varianceLimit + ".";
|
|
459
|
+
if (Math.abs(gpu.samples[i] - reference.samples[i]) > 1)
|
|
460
|
+
return "component " + i + ": sample " + gpu.samples[i] + " differs from the reference " + reference.samples[i] + " by more than one code.";
|
|
461
|
+
}
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
finally {
|
|
465
|
+
signal?.removeEventListener("abort", onAbort);
|
|
466
|
+
}
|
|
467
|
+
} };
|
|
468
|
+
/**
|
|
469
|
+
* The display view's Speed renderer on one runtime (#481's `DisplayViewGpuRenderer`): one pipeline cache per device, the
|
|
470
|
+
* material modules of `createDisplayViewMaterialModules` (the preset module per resolved chart, the composite module with its
|
|
471
|
+
* scene records uploaded once per scene). `render` renders the part's region (`setup.gpu` carries the expected signal's input
|
|
472
|
+
* and the resolved scene; a setup without it, a caller scene, a coarse view, a frame the kernel does not cover or a device
|
|
473
|
+
* that fails the composite probe throws `unsupported-on-backend`), and throws `aborted` and `device-lost` as the accelerator's
|
|
474
|
+
* operations do. `dispose` releases the uploaded scene records.
|
|
475
|
+
*/
|
|
476
|
+
export function createDisplayViewSpeedGpuRenderer(runtime, options = {}) {
|
|
477
|
+
const pipelines = new Map();
|
|
478
|
+
const bandBytes = options.maximumBandBytes ?? Math.min(64 * 1024 * 1024, runtime.maximumStorageBindingBytes);
|
|
479
|
+
const source = (label, code) => {
|
|
480
|
+
let pending = pipelines.get(label);
|
|
481
|
+
if (!pending) {
|
|
482
|
+
pending = createComputePipeline(runtime, label, code, undefined);
|
|
483
|
+
pipelines.set(label, pending);
|
|
484
|
+
pending.catch(() => pipelines.delete(label));
|
|
485
|
+
}
|
|
486
|
+
return pending;
|
|
487
|
+
};
|
|
488
|
+
const modules = createDisplayViewMaterialModules(runtime, source);
|
|
489
|
+
return {
|
|
490
|
+
id: "photivra-display-view-speed",
|
|
491
|
+
supports: displayViewSpeedGpuSupports,
|
|
492
|
+
conformanceCases: [DISPLAY_VIEW_SPEED_CONFORMANCE_CASE],
|
|
493
|
+
async render(part) {
|
|
494
|
+
const { setup, signal } = part;
|
|
495
|
+
if (!setup.gpu)
|
|
496
|
+
throw new ImageKernelExecutionError("unsupported-on-backend", "The display-view setup carries no GPU inputs (setup.gpu); use cpu-reference.");
|
|
497
|
+
const abort = new AbortController(), onAbort = () => abort.abort();
|
|
498
|
+
if (signal?.aborted)
|
|
499
|
+
throw abortedError();
|
|
500
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
501
|
+
try {
|
|
502
|
+
const material = await modules.moduleFor(setup.gpu, abort.signal);
|
|
503
|
+
const result = await renderDisplayViewSpeedOnGpu(runtime, bandBytes, { setup, signalInput: setup.gpu.signalInput, material }, abort, options.yieldControl, source);
|
|
504
|
+
return { samples: result.samples, expected: result.expected, noiseStd: result.noiseStd, sampleCount: result.sampleCount, siteCount: result.siteCount, detailPixelCount: result.detailPixelCount,
|
|
505
|
+
uniformPixelCount: result.uniformPixelCount, ahdCorrectedPixelCount: result.ahdCorrectedPixelCount, estimation: result.estimation, errorBound: result.expectedValueBound,
|
|
506
|
+
diagnosticUncertaintySampleCount: result.execution.diagnosticUncertaintySampleCount, ...(material.compositeProbe ? { compositeProbe: material.compositeProbe } : {}) };
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
signal?.removeEventListener("abort", onAbort);
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
dispose() { modules.release(); }
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
/** The factory `prepareDisplayView` creates the renderer with (one per device). */
|
|
516
|
+
export const displayViewSpeedGpuRendererFactory = (handle) => createDisplayViewSpeedGpuRenderer(handle);
|
|
517
|
+
/**
|
|
518
|
+
* Registers the Speed renderer as the display view's GPU renderer (`setDisplayViewGpuRendererFactory`) and returns the factory
|
|
519
|
+
* registered before. Nothing registers it on import: the integration of #481 decides where the engine registers it.
|
|
520
|
+
*/
|
|
521
|
+
export function registerDisplayViewSpeedGpuRenderer() {
|
|
522
|
+
return setDisplayViewGpuRendererFactory(displayViewSpeedGpuRendererFactory);
|
|
523
|
+
}
|
|
524
|
+
//# sourceMappingURL=display-view-speed-gpu.js.map
|