@photivra/engine 2.13.0 → 2.14.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 +68 -1
- package/CITATION.cff +1 -1
- package/README.md +7 -7
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +2 -0
- package/dist/camera/lens-projection.js +2 -1
- package/dist/camera/lens-projection.js.map +1 -1
- package/dist/capture/dng-import.js +1 -1
- package/dist/capture/dng-import.js.map +1 -1
- package/dist/capture/host-yield-cadence.js +1 -1
- package/dist/capture/host-yield-cadence.js.map +1 -1
- package/dist/capture/native-environment-optical-stage.js +1 -1
- package/dist/capture/native-environment-optical-stage.js.map +1 -1
- package/dist/capture/native-raw.js +1 -1
- package/dist/capture/native-raw.js.map +1 -1
- package/dist/capture/packed-sensor-raw.js +3 -3
- package/dist/capture/packed-sensor-raw.js.map +1 -1
- package/dist/capture/sensor-raw-producer.js +1 -1
- package/dist/capture/sensor-raw-producer.js.map +1 -1
- package/dist/color/capture-color.js +3 -3
- package/dist/color/capture-color.js.map +1 -1
- package/dist/color/chromatic-adaptation.js +2 -2
- package/dist/color/chromatic-adaptation.js.map +1 -1
- package/dist/color/color-matching-functions.js +1 -1
- package/dist/color/color-matching-functions.js.map +1 -1
- package/dist/color/colorimetry-math.js +2 -2
- package/dist/color/colorimetry-math.js.map +1 -1
- package/dist/color/illuminants.js +3 -3
- package/dist/color/illuminants.js.map +1 -1
- package/dist/color/rgb-color-spaces.js +1 -1
- package/dist/color/rgb-color-spaces.js.map +1 -1
- package/dist/color/sensor-color-development.js +1 -1
- package/dist/color/sensor-color-development.js.map +1 -1
- package/dist/core/record-validation.js +3 -3
- package/dist/core/record-validation.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/equipment/generic-prime-lenses.js +3 -3
- package/dist/equipment/generic-prime-lenses.js.map +1 -1
- package/dist/equipment/generic-tier-assets.d.ts +23 -1
- package/dist/equipment/generic-tier-assets.js +58 -30
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/generic-tier-presets.d.ts +6 -3
- package/dist/equipment/generic-tier-presets.js +26 -13
- package/dist/equipment/generic-tier-presets.js.map +1 -1
- package/dist/exposure/auto-iso-slowest-shutter.js +2 -1
- package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -1
- package/dist/exposure/flash-ttl.js +1 -1
- package/dist/exposure/flash-ttl.js.map +1 -1
- package/dist/exposure/flash.js +1 -1
- package/dist/exposure/flash.js.map +1 -1
- package/dist/facade/chart-material-layers.js +2 -2
- package/dist/facade/chart-material-layers.js.map +1 -1
- package/dist/facade/chart-radiance-batch.js +1 -1
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/chart-scenes.d.ts +45 -8
- package/dist/facade/chart-scenes.js +101 -44
- package/dist/facade/chart-scenes.js.map +1 -1
- package/dist/facade/color-patches.d.ts +129 -0
- package/dist/facade/color-patches.js +209 -0
- package/dist/facade/color-patches.js.map +1 -0
- package/dist/facade/display-view-gpu.js +1 -1
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/scene-radiance.js +2 -2
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +47 -3
- package/dist/facade/simulate-capture.js +71 -22
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +2 -2
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.js +2 -2
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/display-view-composite-material-module.js +1 -1
- package/dist/gpu/display-view-composite-material-module.js.map +1 -1
- package/dist/gpu/image-kernel-execution.js +2 -2
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/gpu/temporal-sum-gpu.js +23 -23
- package/dist/gpu/webgpu-runtime.js +1 -1
- package/dist/gpu/webgpu-runtime.js.map +1 -1
- package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
- package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
- package/dist/gpu/wgsl-chart-kernel.js +117 -117
- package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.js +505 -505
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/gpu/wgsl-kernels.js +203 -203
- package/dist/gpu/wgsl-kernels.js.map +1 -1
- package/dist/gpu/wgsl-sensor-kernel.js +302 -302
- package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/motion/temporal-exposure-sum.js +1 -1
- package/dist/motion/temporal-exposure-sum.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.js +4 -4
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/optics/front-of-lens-filter.js +1 -1
- package/dist/optics/front-of-lens-filter.js.map +1 -1
- package/dist/optics/irradiance-convolution.js +1 -1
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/output/native-capture-sdr.js +1 -1
- package/dist/output/native-capture-sdr.js.map +1 -1
- package/dist/output/png-encoder.js +2 -2
- package/dist/output/png-encoder.js.map +1 -1
- package/dist/output/print-jpeg.js +1 -1
- package/dist/output/print-jpeg.js.map +1 -1
- package/dist/sensor/fixed-pattern-noise.js +1 -1
- package/dist/sensor/fixed-pattern-noise.js.map +1 -1
- package/dist/sensor/readout-exposure-linkage.js +1 -1
- package/dist/sensor/readout-exposure-linkage.js.map +1 -1
- package/docs/API_REFERENCE.md +29 -1
- package/docs/QUICK_START.md +40 -5
- package/docs/USAGE.md +35 -1
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-tier-assets.md +41 -1
- package/docs/api/equipment-generic-tier-presets.md +11 -3
- package/docs/api/exports.json +107 -2
- package/docs/api/facade-chart-scenes.md +36 -5
- package/docs/api/facade-color-patches.md +150 -0
- package/docs/api/facade-simulate-capture.md +52 -2
- package/llms.txt +1 -0
- package/package.json +2 -2
package/dist/gpu/wgsl-kernels.js
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
* largest per-sample linear bound of a luminance tone curve (a maximum, not a sum: non-negative float32
|
|
14
14
|
* bit patterns order like their values). `luminanceDomain` counts samples outside the float32 domain.
|
|
15
15
|
*/
|
|
16
|
-
export const SDR_COUNTER = Object.freeze({ negative: 0, above: 1, changed: 2, low: 3, high: 4, outOfGamut: 5, uncertain: 6, gamutUncertain: 7,
|
|
16
|
+
export const SDR_COUNTER = /* @__PURE__ */ Object.freeze({ negative: 0, above: 1, changed: 2, low: 3, high: 4, outOfGamut: 5, uncertain: 6, gamutUncertain: 7,
|
|
17
17
|
saturatedPixels: 8, whiteBalanceClipped: 9, reconstructedPixels: 10, linearBoundBits: 11, luminanceDomain: 12 });
|
|
18
18
|
export const SDR_COUNTER_SLOTS = 16;
|
|
19
19
|
/** Tone modes of the luminance kernels (`Params.tone_mode`); 0 is a per-channel curve. */
|
|
20
|
-
export const SDR_TONE_MODE = Object.freeze({ perChannel: 0, extendedReinhard: 1, nakaRushton: 2 });
|
|
20
|
+
export const SDR_TONE_MODE = /* @__PURE__ */ Object.freeze({ perChannel: 0, extendedReinhard: 1, nakaRushton: 2 });
|
|
21
21
|
export const LINEAR_WORKGROUP_SIZE = 256;
|
|
22
22
|
/** RAW_HIGHLIGHT_RECONSTRUCTION_START as a WGSL literal (rounded once to float32 by the compiler). */
|
|
23
23
|
const RAW_HIGHLIGHT_START_WGSL = "0.8";
|
|
@@ -31,43 +31,43 @@ const SDR_FUNCTIONS = /* wgsl */ `
|
|
|
31
31
|
var<workgroup> local_counters: array<atomic<u32>, ${SDR_COUNTER_SLOTS}>;
|
|
32
32
|
fn count(slot: u32) { atomicAdd(&local_counters[slot], 1u); }
|
|
33
33
|
fn encode_srgb(x: f32) -> f32 {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
if (x == 1.0) { return 1.0; }
|
|
35
|
+
if (x <= 0.0031308) { return x * 12.92; }
|
|
36
|
+
return 1.055 * pow(x, 1.0 / 2.4) - 0.055;
|
|
37
37
|
}
|
|
38
38
|
fn render_sample(e: f32, source_nonzero: bool, band0: f32, band1: f32, reinhard: bool, reject: bool, maximum: f32) -> u32 {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
|
|
40
|
+
var gamut_uncertain = (e == 0.0 && source_nonzero) || (!reinhard && abs(e - 1.0) <= band1);
|
|
41
|
+
if (band0 > 0.0 && abs(e) <= band0) { gamut_uncertain = true; }
|
|
42
|
+
var uncertain = gamut_uncertain || abs(e - 1.0) <= band1;
|
|
43
|
+
if (reinhard && e > 0.0 && e < 9.5367431640625e-7) { uncertain = true; }
|
|
44
44
|
if (uncertain) { count(${SDR_COUNTER.uncertain}u); }
|
|
45
45
|
if (gamut_uncertain) { count(${SDR_COUNTER.gamutUncertain}u); }
|
|
46
46
|
if (e < 0.0) { count(${SDR_COUNTER.negative}u); }
|
|
47
47
|
if (e > 1.0) { count(${SDR_COUNTER.above}u); }
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
var mapped = e;
|
|
49
|
+
if (reinhard && e > 0.0) { mapped = e / (1.0 + e); }
|
|
50
50
|
if (mapped != e) { count(${SDR_COUNTER.changed}u); }
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
var linear = mapped;
|
|
52
|
+
if (mapped < 0.0 || mapped > 1.0) {
|
|
53
53
|
if (reject) { count(${SDR_COUNTER.outOfGamut}u); }
|
|
54
54
|
else if (mapped < 0.0) { count(${SDR_COUNTER.low}u); linear = 0.0; }
|
|
55
55
|
else { count(${SDR_COUNTER.high}u); linear = 1.0; }
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
}
|
|
57
|
+
let code = floor(encode_srgb(linear) * maximum + 0.5);
|
|
58
|
+
return u32(clamp(code, 0.0, maximum));
|
|
59
59
|
}
|
|
60
60
|
fn clear_counters(local_index: u32) {
|
|
61
61
|
if (local_index < ${SDR_COUNTER_SLOTS}u) { atomicStore(&local_counters[local_index], 0u); }
|
|
62
|
-
|
|
62
|
+
workgroupBarrier();
|
|
63
63
|
}
|
|
64
64
|
fn flush_counters(local_index: u32) {
|
|
65
|
-
|
|
65
|
+
workgroupBarrier();
|
|
66
66
|
if (local_index < ${SDR_COUNTER_SLOTS}u) {
|
|
67
|
-
|
|
67
|
+
let value = atomicLoad(&local_counters[local_index]);
|
|
68
68
|
if (local_index == ${SDR_COUNTER.linearBoundBits}u) { atomicMax(&counters[local_index], value); }
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
else if (value > 0u) { atomicAdd(&counters[local_index], value); }
|
|
70
|
+
}
|
|
71
71
|
}
|
|
72
72
|
`;
|
|
73
73
|
/**
|
|
@@ -84,88 +84,88 @@ const LUMINANCE_FUNCTIONS = /* wgsl */ `
|
|
|
84
84
|
const LU: f32 = 5.9604644775390625e-8;
|
|
85
85
|
const ULP1: f32 = 1.1920928955078125e-7;
|
|
86
86
|
fn count_max(slot: u32, value: f32) { atomicMax(&local_counters[slot], bitcast<u32>(max(value, 0.0))); }
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
fn tone_h(l: f32) -> vec3<f32> {
|
|
89
89
|
if (params.tone_mode == ${SDR_TONE_MODE.extendedReinhard}u) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
90
|
+
return vec3<f32>((1.0 + l * params.tone.x) / (1.0 + l), 14.0 * LU, 0.0);
|
|
91
|
+
}
|
|
92
|
+
let lg = log2(params.tone.x / l);
|
|
93
|
+
let n = params.tone.y;
|
|
94
|
+
let z = n * lg;
|
|
95
|
+
|
|
96
|
+
let dz = n * (4.76837158203125e-7 + 3.0 * ULP1 * abs(lg) + 9.0 * LU) + 2.0 * LU * abs(z);
|
|
97
|
+
|
|
98
|
+
let rho = 0.7 * dz * (1.0 + dz) + (3.0 + 2.0 * abs(z)) * ULP1 + 9.094947e-13 * (1.0 + abs(z));
|
|
99
|
+
let eta = 2.0 * rho + 16.0 * LU;
|
|
100
|
+
if (z >= 0.0) {
|
|
101
|
+
let p = exp2(-z);
|
|
102
|
+
var abs_h = 0.0;
|
|
103
|
+
|
|
104
|
+
if (p < 7.52316384526264e-37) { abs_h = params.tone.z * 3.009265538105056e-36 / l; }
|
|
105
|
+
return vec3<f32>(params.tone.z * p / (l * (1.0 + p)), eta, abs_h);
|
|
106
|
+
}
|
|
107
|
+
let p = exp2(z);
|
|
108
|
+
return vec3<f32>(params.tone.z / (l * (1.0 + p)), eta, 0.0);
|
|
109
109
|
}
|
|
110
110
|
struct Toned { m: vec3<f32>, delta: vec3<f32>, changed_uncertain: bool };
|
|
111
111
|
fn luminance_tone(e: vec3<f32>, eps: vec3<f32>) -> Toned {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
112
|
+
var t: Toned;
|
|
113
|
+
let ae = abs(e);
|
|
114
|
+
let ea = ae + eps;
|
|
115
|
+
let s = (params.lum.x * ae.x + params.lum.y * ae.y) + params.lum.z * ae.z;
|
|
116
|
+
let l = (params.lum.x * e.x + params.lum.y * e.y) + params.lum.z * e.z;
|
|
117
|
+
let lambda = (((params.lum.x * eps.x + params.lum.y * eps.y) + params.lum.z * eps.z) + 6.0 * LU * s) * 1.001;
|
|
118
|
+
if (l < -2.0 * lambda || (l == 0.0 && lambda == 0.0)) {
|
|
119
|
+
|
|
120
|
+
t.m = e; t.delta = eps; t.changed_uncertain = false;
|
|
121
|
+
return t;
|
|
122
|
+
}
|
|
123
|
+
if (l > 2.0 * lambda) {
|
|
124
|
+
let r = tone_h(l);
|
|
125
|
+
let q = lambda / l;
|
|
126
|
+
let f = exp2(params.lum.w * log2((1.0 + q) / (1.0 - q))) / (1.0 - q) * 1.01;
|
|
127
|
+
let fq = params.lum.w * f * q;
|
|
128
|
+
let hh = r.x * (1.0 + 2.0 * r.y) + r.z;
|
|
129
|
+
t.m = e * r.x;
|
|
130
|
+
t.delta = (eps * hh * (1.0 + fq) + ea * (hh * (r.y + fq) + r.z) + 2.0 * LU * abs(t.m)) * 1.001;
|
|
131
|
+
t.changed_uncertain = abs(r.x - 1.0) <= 2.0 * (r.y + fq) * max(1.0, r.x) + r.z + 4.0 * LU;
|
|
132
|
+
return t;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
var hsup = max(1.0, params.tone.x);
|
|
136
136
|
if (params.tone_mode == ${SDR_TONE_MODE.nakaRushton}u) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
137
|
+
hsup = params.tone.z * exp2((params.tone.y - 1.0) * log2(3.0 * lambda)) / params.tone.w * 1.01;
|
|
138
|
+
}
|
|
139
|
+
var m = e;
|
|
140
|
+
if (l > 0.0) { m = e * tone_h(l).x; }
|
|
141
|
+
t.m = m;
|
|
142
|
+
t.delta = (2.0 * ea * (1.0 + hsup * (1.0 + 1e-3))) * 1.001;
|
|
143
|
+
t.changed_uncertain = true;
|
|
144
|
+
return t;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
147
|
fn render_toned(e: f32, source_nonzero: bool, band0: f32, band1: f32, m: f32, delta: f32, changed_uncertain: bool, reject: bool, maximum: f32) -> u32 {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
var uncertain = (e == 0.0 && source_nonzero) || abs(e - 1.0) <= band1 || (band0 > 0.0 && abs(e) <= band0) || (changed_uncertain && e != 0.0);
|
|
149
|
+
|
|
150
|
+
let gamut_uncertain = (m - delta < 0.0 && m + delta >= 0.0) || (m + delta > 1.0 && m - delta <= 1.0);
|
|
151
151
|
if (gamut_uncertain) { uncertain = true; count(${SDR_COUNTER.gamutUncertain}u); }
|
|
152
152
|
if (uncertain) { count(${SDR_COUNTER.uncertain}u); }
|
|
153
153
|
if (e < 0.0) { count(${SDR_COUNTER.negative}u); }
|
|
154
154
|
if (e > 1.0) { count(${SDR_COUNTER.above}u); }
|
|
155
155
|
if (m != e) { count(${SDR_COUNTER.changed}u); }
|
|
156
156
|
if (!(abs(m) + delta <= 1.2676506002282294e30)) { count(${SDR_COUNTER.luminanceDomain}u); }
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
var linear = m;
|
|
158
|
+
var bound = min(delta, 1.0);
|
|
159
|
+
if (m < 0.0 || m > 1.0) {
|
|
160
160
|
if (reject) { count(${SDR_COUNTER.outOfGamut}u); }
|
|
161
161
|
else if (m < 0.0) { count(${SDR_COUNTER.low}u); linear = 0.0; }
|
|
162
162
|
else { count(${SDR_COUNTER.high}u); linear = 1.0; }
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!reject && (m - delta >= 1.0 || m + delta <= 0.0)) { bound = 0.0; }
|
|
166
166
|
count_max(${SDR_COUNTER.linearBoundBits}u, bound);
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
let code = floor(encode_srgb(linear) * maximum + 0.5);
|
|
168
|
+
return u32(clamp(code, 0.0, maximum));
|
|
169
169
|
}
|
|
170
170
|
`;
|
|
171
171
|
/** Pointwise SDR over a packed float32 band; one invocation per sample. */
|
|
@@ -178,20 +178,20 @@ struct Params { count: u32, reinhard: u32, reject: u32, pad0: u32, scale: f32, m
|
|
|
178
178
|
${SDR_FUNCTIONS}
|
|
179
179
|
@compute @workgroup_size(${LINEAR_WORKGROUP_SIZE})
|
|
180
180
|
fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(local_invocation_index) local_index: u32,
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
@builtin(num_workgroups) groups: vec3<u32>) {
|
|
182
|
+
clear_counters(local_index);
|
|
183
183
|
let i = gid.x + gid.y * groups.x * ${LINEAR_WORKGROUP_SIZE}u;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
if (i < params.count) {
|
|
185
|
+
let s = source[i];
|
|
186
|
+
codes[i] = render_sample(s * params.scale, s != 0.0, 0.0, params.band1, params.reinhard != 0u, params.reject != 0u, params.maximum);
|
|
187
|
+
}
|
|
188
|
+
flush_counters(local_index);
|
|
189
189
|
}
|
|
190
190
|
`;
|
|
191
191
|
/** Pointwise SDR with a luminance tone curve over a packed float32 band; one invocation per pixel. */
|
|
192
192
|
export const SDR_LUMINANCE_WGSL = /* wgsl */ `
|
|
193
193
|
struct Params { pixels: u32, reject: u32, tone_mode: u32, pad0: u32, scale: f32, maximum: f32, band1: f32, pad1: f32,
|
|
194
|
-
|
|
194
|
+
tone: vec4<f32>, lum: vec4<f32> };
|
|
195
195
|
@group(0) @binding(0) var<uniform> params: Params;
|
|
196
196
|
@group(0) @binding(1) var<storage, read> source: array<f32>;
|
|
197
197
|
@group(0) @binding(2) var<storage, read_write> codes: array<u32>;
|
|
@@ -200,24 +200,24 @@ ${SDR_FUNCTIONS}
|
|
|
200
200
|
${LUMINANCE_FUNCTIONS}
|
|
201
201
|
@compute @workgroup_size(${LINEAR_WORKGROUP_SIZE})
|
|
202
202
|
fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(local_invocation_index) local_index: u32,
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
@builtin(num_workgroups) groups: vec3<u32>) {
|
|
204
|
+
clear_counters(local_index);
|
|
205
205
|
let i = gid.x + gid.y * groups.x * ${LINEAR_WORKGROUP_SIZE}u;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
206
|
+
if (i < params.pixels) {
|
|
207
|
+
let s = vec3<f32>(source[i * 3u], source[i * 3u + 1u], source[i * 3u + 2u]);
|
|
208
|
+
let e = s * params.scale;
|
|
209
|
+
|
|
210
|
+
var eps = 4.0 * LU * abs(e);
|
|
211
|
+
for (var j = 0u; j < 3u; j++) {
|
|
212
|
+
if (s[j] != 0.0 && abs(e[j]) < 7.888609052210118e-31) { eps[j] = eps[j] + 2.350988701644575e-38 * max(1.0, params.scale); }
|
|
213
|
+
}
|
|
214
|
+
let t = luminance_tone(e, eps);
|
|
215
|
+
let reject = params.reject != 0u;
|
|
216
|
+
for (var j = 0u; j < 3u; j++) {
|
|
217
|
+
codes[i * 3u + j] = render_toned(e[j], s[j] != 0.0, 0.0, params.band1, t.m[j], t.delta[j], t.changed_uncertain, reject, params.maximum);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
flush_counters(local_index);
|
|
221
221
|
}
|
|
222
222
|
`;
|
|
223
223
|
/**
|
|
@@ -230,17 +230,17 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(local_invocation
|
|
|
230
230
|
*/
|
|
231
231
|
export const RAW_DEVELOPMENT_WGSL = /* wgsl */ `
|
|
232
232
|
struct Params {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
233
|
+
width: u32, count: u32, row0: u32, reinhard: u32,
|
|
234
|
+
reject: u32, repeat_w: u32, repeat_h: u32, region_w: u32,
|
|
235
|
+
origin_x: i32, origin_y: i32, axx: i32, axy: i32,
|
|
236
|
+
ayx: i32, ayy: i32, region_x: i32, region_y: i32,
|
|
237
|
+
scale: f32, maximum: f32, band0: f32, band1: f32,
|
|
238
|
+
clip_level: f32, reconstruct: u32, band_u: f32, band_s: f32,
|
|
239
|
+
gains: vec4<f32>,
|
|
240
|
+
m0: vec4<f32>, m1: vec4<f32>, m2: vec4<f32>,
|
|
241
|
+
x0: vec4<f32>, x1: vec4<f32>, x2: vec4<f32>,
|
|
242
|
+
tone_mode: u32, pad0: u32, pad1: u32, pad2: u32,
|
|
243
|
+
tone: vec4<f32>, lum: vec4<f32>,
|
|
244
244
|
};
|
|
245
245
|
struct Tap { ox: i32, oy: i32, w: f32, pad: f32 };
|
|
246
246
|
@group(0) @binding(0) var<uniform> params: Params;
|
|
@@ -253,86 +253,86 @@ ${SDR_FUNCTIONS}
|
|
|
253
253
|
${LUMINANCE_FUNCTIONS}
|
|
254
254
|
var<private> saturated: bool;
|
|
255
255
|
fn channel(phase: u32, c: u32, nx: i32, ny: i32) -> f32 {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
256
|
+
let range = phases[phase * 3u + c];
|
|
257
|
+
var value = 0.0;
|
|
258
|
+
for (var k = 0u; k < range.y; k++) {
|
|
259
|
+
let t = taps[range.x + k];
|
|
260
|
+
let sx = nx + t.ox - params.region_x;
|
|
261
|
+
let sy = ny + t.oy - params.region_y;
|
|
262
|
+
let site = normalized[u32(sy) * params.region_w + u32(sx)];
|
|
263
|
+
if (site == 1.0) { saturated = true; }
|
|
264
|
+
value = value + site * t.w;
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
267
|
}
|
|
268
268
|
fn row3(m: vec4<f32>, v: vec3<f32>) -> f32 { return ((0.0 + m.x * v.x) + m.y * v.y) + m.z * v.z; }
|
|
269
269
|
fn max3(v: vec3<f32>) -> f32 {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
if (v.x > v.y) { if (v.x > v.z) { return v.x; } return v.z; }
|
|
271
|
+
if (v.y > v.z) { return v.y; }
|
|
272
|
+
return v.z;
|
|
273
273
|
}
|
|
274
274
|
@compute @workgroup_size(${LINEAR_WORKGROUP_SIZE})
|
|
275
275
|
fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(local_invocation_index) local_index: u32,
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
@builtin(num_workgroups) groups: vec3<u32>) {
|
|
277
|
+
clear_counters(local_index);
|
|
278
278
|
let i = gid.x + gid.y * groups.x * ${LINEAR_WORKGROUP_SIZE}u;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
279
|
+
if (i < params.count) {
|
|
280
|
+
let col = i32(i % params.width);
|
|
281
|
+
let row = i32(params.row0 + i / params.width);
|
|
282
|
+
let nx = params.origin_x + params.axx * col + params.axy * row;
|
|
283
|
+
let ny = params.origin_y + params.ayx * col + params.ayy * row;
|
|
284
|
+
let phase = (u32(ny) % params.repeat_h) * params.repeat_w + u32(nx) % params.repeat_w;
|
|
285
|
+
saturated = false;
|
|
286
|
+
let c = vec3<f32>(channel(phase, 0u, nx, ny), channel(phase, 1u, nx, ny), channel(phase, 2u, nx, ny));
|
|
287
287
|
if (saturated) { count(${SDR_COUNTER.saturatedPixels}u); }
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
let u = vec3<f32>(c.x * params.gains.x, c.y * params.gains.y, c.z * params.gains.z);
|
|
289
|
+
let clip = params.clip_level;
|
|
290
|
+
let k = vec3<f32>(select(u.x, clip, u.x > clip), select(u.y, clip, u.y > clip), select(u.z, clip, u.z > clip));
|
|
291
|
+
var b = k;
|
|
292
|
+
if (params.reconstruct == 0u) {
|
|
293
|
+
for (var j = 0u; j < 3u; j++) {
|
|
294
294
|
if (u[j] > clip) { count(${SDR_COUNTER.whiteBalanceClipped}u); }
|
|
295
295
|
if (abs(u[j] - clip) <= params.band_u) { count(${SDR_COUNTER.uncertain}u); }
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
let s = max3(c);
|
|
299
299
|
if (abs(s - ${RAW_HIGHLIGHT_START_WGSL}) <= params.band_s) { count(${SDR_COUNTER.uncertain}u); }
|
|
300
300
|
if (s > ${RAW_HIGHLIGHT_START_WGSL}) {
|
|
301
|
-
|
|
301
|
+
var t = 1.0;
|
|
302
302
|
if (s < 1.0) { t = (s - ${RAW_HIGHLIGHT_START_WGSL}) / (1.0 - ${RAW_HIGHLIGHT_START_WGSL}); }
|
|
303
|
-
|
|
304
|
-
|
|
303
|
+
let n = max3(k);
|
|
304
|
+
b = (1.0 - t) * u + t * vec3<f32>(n, n, n);
|
|
305
305
|
count(${SDR_COUNTER.reconstructedPixels}u);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
306
|
+
} else {
|
|
307
|
+
b = u;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
let xyz = vec3<f32>(row3(params.m0, b), row3(params.m1, b), row3(params.m2, b));
|
|
311
|
+
let rgb = vec3<f32>(row3(params.x0, xyz), row3(params.x1, xyz), row3(params.x2, xyz));
|
|
312
|
+
let reinhard = params.reinhard != 0u;
|
|
313
|
+
let reject = params.reject != 0u;
|
|
314
314
|
if (params.tone_mode == ${SDR_TONE_MODE.perChannel}u) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
315
|
+
for (var j = 0u; j < 3u; j++) {
|
|
316
|
+
codes[i * 3u + j] = render_sample(rgb[j] * params.scale, false, params.band0, params.band1, reinhard, reject, params.maximum);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
|
|
320
|
+
let e = rgb * params.scale;
|
|
321
|
+
let t = luminance_tone(e, vec3<f32>(params.band0, params.band0, params.band0));
|
|
322
|
+
for (var j = 0u; j < 3u; j++) {
|
|
323
|
+
codes[i * 3u + j] = render_toned(e[j], false, params.band0, params.band1, t.m[j], t.delta[j], t.changed_uncertain, reject, params.maximum);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
flush_counters(local_index);
|
|
328
328
|
}
|
|
329
329
|
`;
|
|
330
330
|
/** Source-indexed isoplanatic-patch convolution of one output-row band; one invocation per output site. */
|
|
331
331
|
export const IRRADIANCE_CONVOLUTION_WGSL = /* wgsl */ `
|
|
332
332
|
struct Params {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
333
|
+
width: u32, height: u32, radius: u32, size: u32,
|
|
334
|
+
patch_w: u32, patch_h: u32, cols: u32, rows: u32,
|
|
335
|
+
out_row0: u32, out_rows: u32, in_row0: u32, taps: u32,
|
|
336
336
|
};
|
|
337
337
|
@group(0) @binding(0) var<uniform> params: Params;
|
|
338
338
|
@group(0) @binding(1) var<storage, read> source: array<f32>;
|
|
@@ -340,26 +340,26 @@ struct Params {
|
|
|
340
340
|
@group(0) @binding(3) var<storage, read_write> result: array<f32>;
|
|
341
341
|
@compute @workgroup_size(${CONVOLUTION_WORKGROUP_EDGE}, ${CONVOLUTION_WORKGROUP_EDGE})
|
|
342
342
|
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
343
|
+
let x = gid.x;
|
|
344
|
+
let local_y = gid.y;
|
|
345
|
+
if (x >= params.width || local_y >= params.out_rows) { return; }
|
|
346
|
+
let y = i32(params.out_row0 + local_y);
|
|
347
|
+
let r = i32(params.radius);
|
|
348
|
+
var sum = 0.0;
|
|
349
|
+
for (var v = -r; v <= r; v++) {
|
|
350
|
+
let sy = y - v;
|
|
351
|
+
if (sy < 0 || sy >= i32(params.height)) { continue; }
|
|
352
|
+
let patch_row = min(u32(sy) / params.patch_h, params.rows - 1u) * params.cols;
|
|
353
|
+
let kernel_row = (v + r) * i32(params.size) + r;
|
|
354
|
+
let source_row = (u32(sy) - params.in_row0) * params.width;
|
|
355
|
+
for (var u = -r; u <= r; u++) {
|
|
356
|
+
let sx = i32(x) - u;
|
|
357
|
+
if (sx < 0 || sx >= i32(params.width)) { continue; }
|
|
358
|
+
let patch_index = patch_row + min(u32(sx) / params.patch_w, params.cols - 1u);
|
|
359
|
+
sum = sum + source[source_row + u32(sx)] * kernels[patch_index * params.taps + u32(kernel_row + u)];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
result[local_y * params.width + x] = sum;
|
|
363
363
|
}
|
|
364
364
|
`;
|
|
365
365
|
//# sourceMappingURL=wgsl-kernels.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wgsl-kernels.js","sourceRoot":"","sources":["../../src/gpu/wgsl-kernels.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * The project's own WGSL (WebGPU shader language) compute kernels for the `webgpu-f32` image-kernel\n * backend. Each kernel evaluates the same equations, in the same order, as its float64 CPU reference:\n * - SDR: `calculateSdrRendering` / native capture SDR (exposure → tone → gamut → sRGB → nearest-ties-up);\n * - RAW development: `createNativeRawDevelopmentTask` (CFA kernel sums → WB gains → camera→XYZ →\n * XYZ→linear sRGB → SDR);\n * - irradiance convolution: `convolveIrradianceRows` (source-indexed isoplanatic-patch gather).\n * The host packers in image-kernel-accelerator.ts use matching uniform layouts.\n */\n\n/**\n * Counter slots shared by the SDR-producing kernels. `linearBoundBits` holds the float32 bits of the\n * largest per-sample linear bound of a luminance tone curve (a maximum, not a sum: non-negative float32\n * bit patterns order like their values). `luminanceDomain` counts samples outside the float32 domain.\n */\nexport const SDR_COUNTER = Object.freeze({ negative: 0, above: 1, changed: 2, low: 3, high: 4, outOfGamut: 5, uncertain: 6, gamutUncertain: 7,\n saturatedPixels: 8, whiteBalanceClipped: 9, reconstructedPixels: 10, linearBoundBits: 11, luminanceDomain: 12 });\nexport const SDR_COUNTER_SLOTS = 16;\n/** Tone modes of the luminance kernels (`Params.tone_mode`); 0 is a per-channel curve. */\nexport const SDR_TONE_MODE = Object.freeze({ perChannel: 0, extendedReinhard: 1, nakaRushton: 2 });\nexport const LINEAR_WORKGROUP_SIZE = 256;\n/** RAW_HIGHLIGHT_RECONSTRUCTION_START as a WGSL literal (rounded once to float32 by the compiler). */\nconst RAW_HIGHLIGHT_START_WGSL = \"0.8\";\nexport const CONVOLUTION_WORKGROUP_EDGE = 16;\n\n/**\n * Shared SDR sample renderer. `band0`/`band1` are absolute classification bands around 0 and 1 inside\n * which the float32 decision may differ from float64; such samples increment the uncertainty counter.\n * A zero result from a non-zero source (underflow) and tiny positive Reinhard inputs are also uncertain.\n */\nconst SDR_FUNCTIONS = /* wgsl */ `\nvar<workgroup> local_counters: array<atomic<u32>, ${SDR_COUNTER_SLOTS}>;\nfn count(slot: u32) { atomicAdd(&local_counters[slot], 1u); }\nfn encode_srgb(x: f32) -> f32 {\n if (x == 1.0) { return 1.0; }\n if (x <= 0.0031308) { return x * 12.92; }\n return 1.055 * pow(x, 1.0 / 2.4) - 0.055;\n}\nfn render_sample(e: f32, source_nonzero: bool, band0: f32, band1: f32, reinhard: bool, reject: bool, maximum: f32) -> u32 {\n // Gamut decisions (mapped < 0, mapped > 1) depend only on the sign band and, for identity tone, the band at 1.\n var gamut_uncertain = (e == 0.0 && source_nonzero) || (!reinhard && abs(e - 1.0) <= band1);\n if (band0 > 0.0 && abs(e) <= band0) { gamut_uncertain = true; }\n var uncertain = gamut_uncertain || abs(e - 1.0) <= band1;\n if (reinhard && e > 0.0 && e < 9.5367431640625e-7) { uncertain = true; }\n if (uncertain) { count(${SDR_COUNTER.uncertain}u); }\n if (gamut_uncertain) { count(${SDR_COUNTER.gamutUncertain}u); }\n if (e < 0.0) { count(${SDR_COUNTER.negative}u); }\n if (e > 1.0) { count(${SDR_COUNTER.above}u); }\n var mapped = e;\n if (reinhard && e > 0.0) { mapped = e / (1.0 + e); }\n if (mapped != e) { count(${SDR_COUNTER.changed}u); }\n var linear = mapped;\n if (mapped < 0.0 || mapped > 1.0) {\n if (reject) { count(${SDR_COUNTER.outOfGamut}u); }\n else if (mapped < 0.0) { count(${SDR_COUNTER.low}u); linear = 0.0; }\n else { count(${SDR_COUNTER.high}u); linear = 1.0; }\n }\n let code = floor(encode_srgb(linear) * maximum + 0.5);\n return u32(clamp(code, 0.0, maximum));\n}\nfn clear_counters(local_index: u32) {\n if (local_index < ${SDR_COUNTER_SLOTS}u) { atomicStore(&local_counters[local_index], 0u); }\n workgroupBarrier();\n}\nfn flush_counters(local_index: u32) {\n workgroupBarrier();\n if (local_index < ${SDR_COUNTER_SLOTS}u) {\n let value = atomicLoad(&local_counters[local_index]);\n if (local_index == ${SDR_COUNTER.linearBoundBits}u) { atomicMax(&counters[local_index], value); }\n else if (value > 0u) { atomicAdd(&counters[local_index], value); }\n }\n}\n`;\n\n/**\n * Luminance tone curves (`prepareSdrPixelTone`): every component of a pixel is scaled by h(L) = f(L)/L,\n * L = kr·r + kg·g + kb·b, when L > 0. The kernel evaluates h directly (algebraically equal to f(L)/L):\n * - extended Reinhard: h = (1 + L/Lw²)/(1 + L);\n * - Naka–Rushton: h = (1/g(Lw))/(L·(1 + (σ/L)ⁿ)), with P = (σ/L)ⁿ = exp2(n·log2(σ/L)) and, for P ≥ 1,\n * the equal form (1/g(Lw))·P⁻¹/(L·(1 + P⁻¹)) so nothing overflows.\n * Per sample it also evaluates an a-priori bound on |m_gpu − m_reference| from the inputs' bounds ε\n * (see docs/WEBGPU.md, \"Luminance tone curves\"). The bound itself is evaluated in float32 and inflated.\n * Uniform fields used: tone_mode, tone = (Reinhard: 1/Lw²; Naka–Rushton: σ, n, 1/g(Lw), σⁿ), lum = (kr, kg, kb, T).\n */\nconst LUMINANCE_FUNCTIONS = /* wgsl */ `\nconst LU: f32 = 5.9604644775390625e-8;\nconst ULP1: f32 = 1.1920928955078125e-7;\nfn count_max(slot: u32, value: f32) { atomicMax(&local_counters[slot], bitcast<u32>(max(value, 0.0))); }\n// (h, eta, abs_h): |h_gpu - h(l)| <= eta*h(l) + abs_h for the exact h at this float32 l > 0.\nfn tone_h(l: f32) -> vec3<f32> {\n if (params.tone_mode == ${SDR_TONE_MODE.extendedReinhard}u) {\n return vec3<f32>((1.0 + l * params.tone.x) / (1.0 + l), 14.0 * LU, 0.0);\n }\n let lg = log2(params.tone.x / l);\n let n = params.tone.y;\n let z = n * lg;\n // log2: 2^-21 absolute or 3 ULP; the sigma/l division and sigma's rounding; the product's rounding.\n let dz = n * (4.76837158203125e-7 + 3.0 * ULP1 * abs(lg) + 9.0 * LU) + 2.0 * LU * abs(z);\n // exp2: 3 + 2|z| ULP; plus slack for the float64 reference's pow.\n let rho = 0.7 * dz * (1.0 + dz) + (3.0 + 2.0 * abs(z)) * ULP1 + 9.094947e-13 * (1.0 + abs(z));\n let eta = 2.0 * rho + 16.0 * LU;\n if (z >= 0.0) {\n let p = exp2(-z);\n var abs_h = 0.0;\n // Below 2^-120 the power may have flushed to zero: bound h absolutely instead.\n if (p < 7.52316384526264e-37) { abs_h = params.tone.z * 3.009265538105056e-36 / l; }\n return vec3<f32>(params.tone.z * p / (l * (1.0 + p)), eta, abs_h);\n }\n let p = exp2(z);\n return vec3<f32>(params.tone.z / (l * (1.0 + p)), eta, 0.0);\n}\nstruct Toned { m: vec3<f32>, delta: vec3<f32>, changed_uncertain: bool };\nfn luminance_tone(e: vec3<f32>, eps: vec3<f32>) -> Toned {\n var t: Toned;\n let ae = abs(e);\n let ea = ae + eps;\n let s = (params.lum.x * ae.x + params.lum.y * ae.y) + params.lum.z * ae.z;\n let l = (params.lum.x * e.x + params.lum.y * e.y) + params.lum.z * e.z;\n let lambda = (((params.lum.x * eps.x + params.lum.y * eps.y) + params.lum.z * eps.z) + 6.0 * LU * s) * 1.001;\n if (l < -2.0 * lambda || (l == 0.0 && lambda == 0.0)) {\n // L < 0 for the reference too: both keep the exposed values.\n t.m = e; t.delta = eps; t.changed_uncertain = false;\n return t;\n }\n if (l > 2.0 * lambda) {\n let r = tone_h(l);\n let q = lambda / l;\n let f = exp2(params.lum.w * log2((1.0 + q) / (1.0 - q))) / (1.0 - q) * 1.01;\n let fq = params.lum.w * f * q;\n let hh = r.x * (1.0 + 2.0 * r.y) + r.z;\n t.m = e * r.x;\n t.delta = (eps * hh * (1.0 + fq) + ea * (hh * (r.y + fq) + r.z) + 2.0 * LU * abs(t.m)) * 1.001;\n t.changed_uncertain = abs(r.x - 1.0) <= 2.0 * (r.y + fq) * max(1.0, r.x) + r.z + 4.0 * LU;\n return t;\n }\n // |L| is inside its classification band: either branch may run, so bound both.\n var hsup = max(1.0, params.tone.x);\n if (params.tone_mode == ${SDR_TONE_MODE.nakaRushton}u) {\n hsup = params.tone.z * exp2((params.tone.y - 1.0) * log2(3.0 * lambda)) / params.tone.w * 1.01;\n }\n var m = e;\n if (l > 0.0) { m = e * tone_h(l).x; }\n t.m = m;\n t.delta = (2.0 * ea * (1.0 + hsup * (1.0 + 1e-3))) * 1.001;\n t.changed_uncertain = true;\n return t;\n}\n// Classification, gamut handling and encoding of one luminance-toned sample (e exposed, m toned).\nfn render_toned(e: f32, source_nonzero: bool, band0: f32, band1: f32, m: f32, delta: f32, changed_uncertain: bool, reject: bool, maximum: f32) -> u32 {\n var uncertain = (e == 0.0 && source_nonzero) || abs(e - 1.0) <= band1 || (band0 > 0.0 && abs(e) <= band0) || (changed_uncertain && e != 0.0);\n // The reference value lies in [m - delta, m + delta]: uncertain only if that interval straddles a decision.\n let gamut_uncertain = (m - delta < 0.0 && m + delta >= 0.0) || (m + delta > 1.0 && m - delta <= 1.0);\n if (gamut_uncertain) { uncertain = true; count(${SDR_COUNTER.gamutUncertain}u); }\n if (uncertain) { count(${SDR_COUNTER.uncertain}u); }\n if (e < 0.0) { count(${SDR_COUNTER.negative}u); }\n if (e > 1.0) { count(${SDR_COUNTER.above}u); }\n if (m != e) { count(${SDR_COUNTER.changed}u); }\n if (!(abs(m) + delta <= 1.2676506002282294e30)) { count(${SDR_COUNTER.luminanceDomain}u); }\n var linear = m;\n var bound = min(delta, 1.0);\n if (m < 0.0 || m > 1.0) {\n if (reject) { count(${SDR_COUNTER.outOfGamut}u); }\n else if (m < 0.0) { count(${SDR_COUNTER.low}u); linear = 0.0; }\n else { count(${SDR_COUNTER.high}u); linear = 1.0; }\n }\n // Clipping is 1-Lipschitz; when both values clip to the same end the difference is zero.\n if (!reject && (m - delta >= 1.0 || m + delta <= 0.0)) { bound = 0.0; }\n count_max(${SDR_COUNTER.linearBoundBits}u, bound);\n let code = floor(encode_srgb(linear) * maximum + 0.5);\n return u32(clamp(code, 0.0, maximum));\n}\n`;\n\n/** Pointwise SDR over a packed float32 band; one invocation per sample. */\nexport const SDR_WGSL = /* wgsl */ `\nstruct Params { count: u32, reinhard: u32, reject: u32, pad0: u32, scale: f32, maximum: f32, band1: f32, pad1: f32 };\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> source: array<f32>;\n@group(0) @binding(2) var<storage, read_write> codes: array<u32>;\n@group(0) @binding(3) var<storage, read_write> counters: array<atomic<u32>, ${SDR_COUNTER_SLOTS}>;\n${SDR_FUNCTIONS}\n@compute @workgroup_size(${LINEAR_WORKGROUP_SIZE})\nfn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(local_invocation_index) local_index: u32,\n @builtin(num_workgroups) groups: vec3<u32>) {\n clear_counters(local_index);\n let i = gid.x + gid.y * groups.x * ${LINEAR_WORKGROUP_SIZE}u;\n if (i < params.count) {\n let s = source[i];\n codes[i] = render_sample(s * params.scale, s != 0.0, 0.0, params.band1, params.reinhard != 0u, params.reject != 0u, params.maximum);\n }\n flush_counters(local_index);\n}\n`;\n\n/** Pointwise SDR with a luminance tone curve over a packed float32 band; one invocation per pixel. */\nexport const SDR_LUMINANCE_WGSL = /* wgsl */ `\nstruct Params { pixels: u32, reject: u32, tone_mode: u32, pad0: u32, scale: f32, maximum: f32, band1: f32, pad1: f32,\n tone: vec4<f32>, lum: vec4<f32> };\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> source: array<f32>;\n@group(0) @binding(2) var<storage, read_write> codes: array<u32>;\n@group(0) @binding(3) var<storage, read_write> counters: array<atomic<u32>, ${SDR_COUNTER_SLOTS}>;\n${SDR_FUNCTIONS}\n${LUMINANCE_FUNCTIONS}\n@compute @workgroup_size(${LINEAR_WORKGROUP_SIZE})\nfn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(local_invocation_index) local_index: u32,\n @builtin(num_workgroups) groups: vec3<u32>) {\n clear_counters(local_index);\n let i = gid.x + gid.y * groups.x * ${LINEAR_WORKGROUP_SIZE}u;\n if (i < params.pixels) {\n let s = vec3<f32>(source[i * 3u], source[i * 3u + 1u], source[i * 3u + 2u]);\n let e = s * params.scale;\n // Source, scale and product roundings (plus the reference's own), and an absolute term where a non-zero source may underflow.\n var eps = 4.0 * LU * abs(e);\n for (var j = 0u; j < 3u; j++) {\n if (s[j] != 0.0 && abs(e[j]) < 7.888609052210118e-31) { eps[j] = eps[j] + 2.350988701644575e-38 * max(1.0, params.scale); }\n }\n let t = luminance_tone(e, eps);\n let reject = params.reject != 0u;\n for (var j = 0u; j < 3u; j++) {\n codes[i * 3u + j] = render_toned(e[j], s[j] != 0.0, 0.0, params.band1, t.m[j], t.delta[j], t.changed_uncertain, reject, params.maximum);\n }\n }\n flush_counters(local_index);\n}\n`;\n\n/**\n * Packed native RAW development of one band of output rows; one invocation per output pixel.\n * `normalized` holds the host's float64 site values, rounded once to float32. A saturated site\n * (flagged, or at or above its phase white level) is exactly 1. Any other site is\n * (code − black)/(white − black) ≤ 1 − 1/65535. So a saturated contributing site is recognised exactly.\n * Highlight handling matches `prepareSensorColorDevelopment` (clip at the smallest applied gain, or the\n * declared neutral blend).\n */\nexport const RAW_DEVELOPMENT_WGSL = /* wgsl */ `\nstruct Params {\n width: u32, count: u32, row0: u32, reinhard: u32,\n reject: u32, repeat_w: u32, repeat_h: u32, region_w: u32,\n origin_x: i32, origin_y: i32, axx: i32, axy: i32,\n ayx: i32, ayy: i32, region_x: i32, region_y: i32,\n scale: f32, maximum: f32, band0: f32, band1: f32,\n clip_level: f32, reconstruct: u32, band_u: f32, band_s: f32,\n gains: vec4<f32>,\n m0: vec4<f32>, m1: vec4<f32>, m2: vec4<f32>,\n x0: vec4<f32>, x1: vec4<f32>, x2: vec4<f32>,\n tone_mode: u32, pad0: u32, pad1: u32, pad2: u32,\n tone: vec4<f32>, lum: vec4<f32>,\n};\nstruct Tap { ox: i32, oy: i32, w: f32, pad: f32 };\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> normalized: array<f32>;\n@group(0) @binding(2) var<storage, read> taps: array<Tap>;\n@group(0) @binding(3) var<storage, read> phases: array<vec2<u32>>;\n@group(0) @binding(4) var<storage, read_write> codes: array<u32>;\n@group(0) @binding(5) var<storage, read_write> counters: array<atomic<u32>, ${SDR_COUNTER_SLOTS}>;\n${SDR_FUNCTIONS}\n${LUMINANCE_FUNCTIONS}\nvar<private> saturated: bool;\nfn channel(phase: u32, c: u32, nx: i32, ny: i32) -> f32 {\n let range = phases[phase * 3u + c];\n var value = 0.0;\n for (var k = 0u; k < range.y; k++) {\n let t = taps[range.x + k];\n let sx = nx + t.ox - params.region_x;\n let sy = ny + t.oy - params.region_y;\n let site = normalized[u32(sy) * params.region_w + u32(sx)];\n if (site == 1.0) { saturated = true; }\n value = value + site * t.w;\n }\n return value;\n}\nfn row3(m: vec4<f32>, v: vec3<f32>) -> f32 { return ((0.0 + m.x * v.x) + m.y * v.y) + m.z * v.z; }\nfn max3(v: vec3<f32>) -> f32 {\n if (v.x > v.y) { if (v.x > v.z) { return v.x; } return v.z; }\n if (v.y > v.z) { return v.y; }\n return v.z;\n}\n@compute @workgroup_size(${LINEAR_WORKGROUP_SIZE})\nfn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(local_invocation_index) local_index: u32,\n @builtin(num_workgroups) groups: vec3<u32>) {\n clear_counters(local_index);\n let i = gid.x + gid.y * groups.x * ${LINEAR_WORKGROUP_SIZE}u;\n if (i < params.count) {\n let col = i32(i % params.width);\n let row = i32(params.row0 + i / params.width);\n let nx = params.origin_x + params.axx * col + params.axy * row;\n let ny = params.origin_y + params.ayx * col + params.ayy * row;\n let phase = (u32(ny) % params.repeat_h) * params.repeat_w + u32(nx) % params.repeat_w;\n saturated = false;\n let c = vec3<f32>(channel(phase, 0u, nx, ny), channel(phase, 1u, nx, ny), channel(phase, 2u, nx, ny));\n if (saturated) { count(${SDR_COUNTER.saturatedPixels}u); }\n let u = vec3<f32>(c.x * params.gains.x, c.y * params.gains.y, c.z * params.gains.z);\n let clip = params.clip_level;\n let k = vec3<f32>(select(u.x, clip, u.x > clip), select(u.y, clip, u.y > clip), select(u.z, clip, u.z > clip));\n var b = k;\n if (params.reconstruct == 0u) {\n for (var j = 0u; j < 3u; j++) {\n if (u[j] > clip) { count(${SDR_COUNTER.whiteBalanceClipped}u); }\n if (abs(u[j] - clip) <= params.band_u) { count(${SDR_COUNTER.uncertain}u); }\n }\n } else {\n let s = max3(c);\n if (abs(s - ${RAW_HIGHLIGHT_START_WGSL}) <= params.band_s) { count(${SDR_COUNTER.uncertain}u); }\n if (s > ${RAW_HIGHLIGHT_START_WGSL}) {\n var t = 1.0;\n if (s < 1.0) { t = (s - ${RAW_HIGHLIGHT_START_WGSL}) / (1.0 - ${RAW_HIGHLIGHT_START_WGSL}); }\n let n = max3(k);\n b = (1.0 - t) * u + t * vec3<f32>(n, n, n);\n count(${SDR_COUNTER.reconstructedPixels}u);\n } else {\n b = u;\n }\n }\n let xyz = vec3<f32>(row3(params.m0, b), row3(params.m1, b), row3(params.m2, b));\n let rgb = vec3<f32>(row3(params.x0, xyz), row3(params.x1, xyz), row3(params.x2, xyz));\n let reinhard = params.reinhard != 0u;\n let reject = params.reject != 0u;\n if (params.tone_mode == ${SDR_TONE_MODE.perChannel}u) {\n for (var j = 0u; j < 3u; j++) {\n codes[i * 3u + j] = render_sample(rgb[j] * params.scale, false, params.band0, params.band1, reinhard, reject, params.maximum);\n }\n } else {\n // band0 bounds each exposed value against the reference (the development chain's a-priori bound).\n let e = rgb * params.scale;\n let t = luminance_tone(e, vec3<f32>(params.band0, params.band0, params.band0));\n for (var j = 0u; j < 3u; j++) {\n codes[i * 3u + j] = render_toned(e[j], false, params.band0, params.band1, t.m[j], t.delta[j], t.changed_uncertain, reject, params.maximum);\n }\n }\n }\n flush_counters(local_index);\n}\n`;\n\n/** Source-indexed isoplanatic-patch convolution of one output-row band; one invocation per output site. */\nexport const IRRADIANCE_CONVOLUTION_WGSL = /* wgsl */ `\nstruct Params {\n width: u32, height: u32, radius: u32, size: u32,\n patch_w: u32, patch_h: u32, cols: u32, rows: u32,\n out_row0: u32, out_rows: u32, in_row0: u32, taps: u32,\n};\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> source: array<f32>;\n@group(0) @binding(2) var<storage, read> kernels: array<f32>;\n@group(0) @binding(3) var<storage, read_write> result: array<f32>;\n@compute @workgroup_size(${CONVOLUTION_WORKGROUP_EDGE}, ${CONVOLUTION_WORKGROUP_EDGE})\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let x = gid.x;\n let local_y = gid.y;\n if (x >= params.width || local_y >= params.out_rows) { return; }\n let y = i32(params.out_row0 + local_y);\n let r = i32(params.radius);\n var sum = 0.0;\n for (var v = -r; v <= r; v++) {\n let sy = y - v;\n if (sy < 0 || sy >= i32(params.height)) { continue; }\n let patch_row = min(u32(sy) / params.patch_h, params.rows - 1u) * params.cols;\n let kernel_row = (v + r) * i32(params.size) + r;\n let source_row = (u32(sy) - params.in_row0) * params.width;\n for (var u = -r; u <= r; u++) {\n let sx = i32(x) - u;\n if (sx < 0 || sx >= i32(params.width)) { continue; }\n let patch_index = patch_row + min(u32(sx) / params.patch_w, params.cols - 1u);\n sum = sum + source[source_row + u32(sx)] * kernels[patch_index * params.taps + u32(kernel_row + u)];\n }\n }\n result[local_y * params.width + x] = sum;\n}\n`;\n"]}
|
|
1
|
+
{"version":3,"file":"wgsl-kernels.js","sourceRoot":"","sources":["../../src/gpu/wgsl-kernels.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * The project's own WGSL (WebGPU shader language) compute kernels for the `webgpu-f32` image-kernel\n * backend. Each kernel evaluates the same equations, in the same order, as its float64 CPU reference:\n * - SDR: `calculateSdrRendering` / native capture SDR (exposure → tone → gamut → sRGB → nearest-ties-up);\n * - RAW development: `createNativeRawDevelopmentTask` (CFA kernel sums → WB gains → camera→XYZ →\n * XYZ→linear sRGB → SDR);\n * - irradiance convolution: `convolveIrradianceRows` (source-indexed isoplanatic-patch gather).\n * The host packers in image-kernel-accelerator.ts use matching uniform layouts.\n */\n\n/**\n * Counter slots shared by the SDR-producing kernels. `linearBoundBits` holds the float32 bits of the\n * largest per-sample linear bound of a luminance tone curve (a maximum, not a sum: non-negative float32\n * bit patterns order like their values). `luminanceDomain` counts samples outside the float32 domain.\n */\nexport const SDR_COUNTER = /* @__PURE__ */ Object.freeze({ negative: 0, above: 1, changed: 2, low: 3, high: 4, outOfGamut: 5, uncertain: 6, gamutUncertain: 7,\n saturatedPixels: 8, whiteBalanceClipped: 9, reconstructedPixels: 10, linearBoundBits: 11, luminanceDomain: 12 });\nexport const SDR_COUNTER_SLOTS = 16;\n/** Tone modes of the luminance kernels (`Params.tone_mode`); 0 is a per-channel curve. */\nexport const SDR_TONE_MODE = /* @__PURE__ */ Object.freeze({ perChannel: 0, extendedReinhard: 1, nakaRushton: 2 });\nexport const LINEAR_WORKGROUP_SIZE = 256;\n/** RAW_HIGHLIGHT_RECONSTRUCTION_START as a WGSL literal (rounded once to float32 by the compiler). */\nconst RAW_HIGHLIGHT_START_WGSL = \"0.8\";\nexport const CONVOLUTION_WORKGROUP_EDGE = 16;\n\n/**\n * Shared SDR sample renderer. `band0`/`band1` are absolute classification bands around 0 and 1 inside\n * which the float32 decision may differ from float64; such samples increment the uncertainty counter.\n * A zero result from a non-zero source (underflow) and tiny positive Reinhard inputs are also uncertain.\n */\nconst SDR_FUNCTIONS = /* wgsl */ `\nvar<workgroup> local_counters: array<atomic<u32>, ${SDR_COUNTER_SLOTS}>;\nfn count(slot: u32) { atomicAdd(&local_counters[slot], 1u); }\nfn encode_srgb(x: f32) -> f32 {\n if (x == 1.0) { return 1.0; }\n if (x <= 0.0031308) { return x * 12.92; }\n return 1.055 * pow(x, 1.0 / 2.4) - 0.055;\n}\nfn render_sample(e: f32, source_nonzero: bool, band0: f32, band1: f32, reinhard: bool, reject: bool, maximum: f32) -> u32 {\n // Gamut decisions (mapped < 0, mapped > 1) depend only on the sign band and, for identity tone, the band at 1.\n var gamut_uncertain = (e == 0.0 && source_nonzero) || (!reinhard && abs(e - 1.0) <= band1);\n if (band0 > 0.0 && abs(e) <= band0) { gamut_uncertain = true; }\n var uncertain = gamut_uncertain || abs(e - 1.0) <= band1;\n if (reinhard && e > 0.0 && e < 9.5367431640625e-7) { uncertain = true; }\n if (uncertain) { count(${SDR_COUNTER.uncertain}u); }\n if (gamut_uncertain) { count(${SDR_COUNTER.gamutUncertain}u); }\n if (e < 0.0) { count(${SDR_COUNTER.negative}u); }\n if (e > 1.0) { count(${SDR_COUNTER.above}u); }\n var mapped = e;\n if (reinhard && e > 0.0) { mapped = e / (1.0 + e); }\n if (mapped != e) { count(${SDR_COUNTER.changed}u); }\n var linear = mapped;\n if (mapped < 0.0 || mapped > 1.0) {\n if (reject) { count(${SDR_COUNTER.outOfGamut}u); }\n else if (mapped < 0.0) { count(${SDR_COUNTER.low}u); linear = 0.0; }\n else { count(${SDR_COUNTER.high}u); linear = 1.0; }\n }\n let code = floor(encode_srgb(linear) * maximum + 0.5);\n return u32(clamp(code, 0.0, maximum));\n}\nfn clear_counters(local_index: u32) {\n if (local_index < ${SDR_COUNTER_SLOTS}u) { atomicStore(&local_counters[local_index], 0u); }\n workgroupBarrier();\n}\nfn flush_counters(local_index: u32) {\n workgroupBarrier();\n if (local_index < ${SDR_COUNTER_SLOTS}u) {\n let value = atomicLoad(&local_counters[local_index]);\n if (local_index == ${SDR_COUNTER.linearBoundBits}u) { atomicMax(&counters[local_index], value); }\n else if (value > 0u) { atomicAdd(&counters[local_index], value); }\n }\n}\n`;\n\n/**\n * Luminance tone curves (`prepareSdrPixelTone`): every component of a pixel is scaled by h(L) = f(L)/L,\n * L = kr·r + kg·g + kb·b, when L > 0. The kernel evaluates h directly (algebraically equal to f(L)/L):\n * - extended Reinhard: h = (1 + L/Lw²)/(1 + L);\n * - Naka–Rushton: h = (1/g(Lw))/(L·(1 + (σ/L)ⁿ)), with P = (σ/L)ⁿ = exp2(n·log2(σ/L)) and, for P ≥ 1,\n * the equal form (1/g(Lw))·P⁻¹/(L·(1 + P⁻¹)) so nothing overflows.\n * Per sample it also evaluates an a-priori bound on |m_gpu − m_reference| from the inputs' bounds ε\n * (see docs/WEBGPU.md, \"Luminance tone curves\"). The bound itself is evaluated in float32 and inflated.\n * Uniform fields used: tone_mode, tone = (Reinhard: 1/Lw²; Naka–Rushton: σ, n, 1/g(Lw), σⁿ), lum = (kr, kg, kb, T).\n */\nconst LUMINANCE_FUNCTIONS = /* wgsl */ `\nconst LU: f32 = 5.9604644775390625e-8;\nconst ULP1: f32 = 1.1920928955078125e-7;\nfn count_max(slot: u32, value: f32) { atomicMax(&local_counters[slot], bitcast<u32>(max(value, 0.0))); }\n// (h, eta, abs_h): |h_gpu - h(l)| <= eta*h(l) + abs_h for the exact h at this float32 l > 0.\nfn tone_h(l: f32) -> vec3<f32> {\n if (params.tone_mode == ${SDR_TONE_MODE.extendedReinhard}u) {\n return vec3<f32>((1.0 + l * params.tone.x) / (1.0 + l), 14.0 * LU, 0.0);\n }\n let lg = log2(params.tone.x / l);\n let n = params.tone.y;\n let z = n * lg;\n // log2: 2^-21 absolute or 3 ULP; the sigma/l division and sigma's rounding; the product's rounding.\n let dz = n * (4.76837158203125e-7 + 3.0 * ULP1 * abs(lg) + 9.0 * LU) + 2.0 * LU * abs(z);\n // exp2: 3 + 2|z| ULP; plus slack for the float64 reference's pow.\n let rho = 0.7 * dz * (1.0 + dz) + (3.0 + 2.0 * abs(z)) * ULP1 + 9.094947e-13 * (1.0 + abs(z));\n let eta = 2.0 * rho + 16.0 * LU;\n if (z >= 0.0) {\n let p = exp2(-z);\n var abs_h = 0.0;\n // Below 2^-120 the power may have flushed to zero: bound h absolutely instead.\n if (p < 7.52316384526264e-37) { abs_h = params.tone.z * 3.009265538105056e-36 / l; }\n return vec3<f32>(params.tone.z * p / (l * (1.0 + p)), eta, abs_h);\n }\n let p = exp2(z);\n return vec3<f32>(params.tone.z / (l * (1.0 + p)), eta, 0.0);\n}\nstruct Toned { m: vec3<f32>, delta: vec3<f32>, changed_uncertain: bool };\nfn luminance_tone(e: vec3<f32>, eps: vec3<f32>) -> Toned {\n var t: Toned;\n let ae = abs(e);\n let ea = ae + eps;\n let s = (params.lum.x * ae.x + params.lum.y * ae.y) + params.lum.z * ae.z;\n let l = (params.lum.x * e.x + params.lum.y * e.y) + params.lum.z * e.z;\n let lambda = (((params.lum.x * eps.x + params.lum.y * eps.y) + params.lum.z * eps.z) + 6.0 * LU * s) * 1.001;\n if (l < -2.0 * lambda || (l == 0.0 && lambda == 0.0)) {\n // L < 0 for the reference too: both keep the exposed values.\n t.m = e; t.delta = eps; t.changed_uncertain = false;\n return t;\n }\n if (l > 2.0 * lambda) {\n let r = tone_h(l);\n let q = lambda / l;\n let f = exp2(params.lum.w * log2((1.0 + q) / (1.0 - q))) / (1.0 - q) * 1.01;\n let fq = params.lum.w * f * q;\n let hh = r.x * (1.0 + 2.0 * r.y) + r.z;\n t.m = e * r.x;\n t.delta = (eps * hh * (1.0 + fq) + ea * (hh * (r.y + fq) + r.z) + 2.0 * LU * abs(t.m)) * 1.001;\n t.changed_uncertain = abs(r.x - 1.0) <= 2.0 * (r.y + fq) * max(1.0, r.x) + r.z + 4.0 * LU;\n return t;\n }\n // |L| is inside its classification band: either branch may run, so bound both.\n var hsup = max(1.0, params.tone.x);\n if (params.tone_mode == ${SDR_TONE_MODE.nakaRushton}u) {\n hsup = params.tone.z * exp2((params.tone.y - 1.0) * log2(3.0 * lambda)) / params.tone.w * 1.01;\n }\n var m = e;\n if (l > 0.0) { m = e * tone_h(l).x; }\n t.m = m;\n t.delta = (2.0 * ea * (1.0 + hsup * (1.0 + 1e-3))) * 1.001;\n t.changed_uncertain = true;\n return t;\n}\n// Classification, gamut handling and encoding of one luminance-toned sample (e exposed, m toned).\nfn render_toned(e: f32, source_nonzero: bool, band0: f32, band1: f32, m: f32, delta: f32, changed_uncertain: bool, reject: bool, maximum: f32) -> u32 {\n var uncertain = (e == 0.0 && source_nonzero) || abs(e - 1.0) <= band1 || (band0 > 0.0 && abs(e) <= band0) || (changed_uncertain && e != 0.0);\n // The reference value lies in [m - delta, m + delta]: uncertain only if that interval straddles a decision.\n let gamut_uncertain = (m - delta < 0.0 && m + delta >= 0.0) || (m + delta > 1.0 && m - delta <= 1.0);\n if (gamut_uncertain) { uncertain = true; count(${SDR_COUNTER.gamutUncertain}u); }\n if (uncertain) { count(${SDR_COUNTER.uncertain}u); }\n if (e < 0.0) { count(${SDR_COUNTER.negative}u); }\n if (e > 1.0) { count(${SDR_COUNTER.above}u); }\n if (m != e) { count(${SDR_COUNTER.changed}u); }\n if (!(abs(m) + delta <= 1.2676506002282294e30)) { count(${SDR_COUNTER.luminanceDomain}u); }\n var linear = m;\n var bound = min(delta, 1.0);\n if (m < 0.0 || m > 1.0) {\n if (reject) { count(${SDR_COUNTER.outOfGamut}u); }\n else if (m < 0.0) { count(${SDR_COUNTER.low}u); linear = 0.0; }\n else { count(${SDR_COUNTER.high}u); linear = 1.0; }\n }\n // Clipping is 1-Lipschitz; when both values clip to the same end the difference is zero.\n if (!reject && (m - delta >= 1.0 || m + delta <= 0.0)) { bound = 0.0; }\n count_max(${SDR_COUNTER.linearBoundBits}u, bound);\n let code = floor(encode_srgb(linear) * maximum + 0.5);\n return u32(clamp(code, 0.0, maximum));\n}\n`;\n\n/** Pointwise SDR over a packed float32 band; one invocation per sample. */\nexport const SDR_WGSL = /* wgsl */ `\nstruct Params { count: u32, reinhard: u32, reject: u32, pad0: u32, scale: f32, maximum: f32, band1: f32, pad1: f32 };\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> source: array<f32>;\n@group(0) @binding(2) var<storage, read_write> codes: array<u32>;\n@group(0) @binding(3) var<storage, read_write> counters: array<atomic<u32>, ${SDR_COUNTER_SLOTS}>;\n${SDR_FUNCTIONS}\n@compute @workgroup_size(${LINEAR_WORKGROUP_SIZE})\nfn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(local_invocation_index) local_index: u32,\n @builtin(num_workgroups) groups: vec3<u32>) {\n clear_counters(local_index);\n let i = gid.x + gid.y * groups.x * ${LINEAR_WORKGROUP_SIZE}u;\n if (i < params.count) {\n let s = source[i];\n codes[i] = render_sample(s * params.scale, s != 0.0, 0.0, params.band1, params.reinhard != 0u, params.reject != 0u, params.maximum);\n }\n flush_counters(local_index);\n}\n`;\n\n/** Pointwise SDR with a luminance tone curve over a packed float32 band; one invocation per pixel. */\nexport const SDR_LUMINANCE_WGSL = /* wgsl */ `\nstruct Params { pixels: u32, reject: u32, tone_mode: u32, pad0: u32, scale: f32, maximum: f32, band1: f32, pad1: f32,\n tone: vec4<f32>, lum: vec4<f32> };\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> source: array<f32>;\n@group(0) @binding(2) var<storage, read_write> codes: array<u32>;\n@group(0) @binding(3) var<storage, read_write> counters: array<atomic<u32>, ${SDR_COUNTER_SLOTS}>;\n${SDR_FUNCTIONS}\n${LUMINANCE_FUNCTIONS}\n@compute @workgroup_size(${LINEAR_WORKGROUP_SIZE})\nfn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(local_invocation_index) local_index: u32,\n @builtin(num_workgroups) groups: vec3<u32>) {\n clear_counters(local_index);\n let i = gid.x + gid.y * groups.x * ${LINEAR_WORKGROUP_SIZE}u;\n if (i < params.pixels) {\n let s = vec3<f32>(source[i * 3u], source[i * 3u + 1u], source[i * 3u + 2u]);\n let e = s * params.scale;\n // Source, scale and product roundings (plus the reference's own), and an absolute term where a non-zero source may underflow.\n var eps = 4.0 * LU * abs(e);\n for (var j = 0u; j < 3u; j++) {\n if (s[j] != 0.0 && abs(e[j]) < 7.888609052210118e-31) { eps[j] = eps[j] + 2.350988701644575e-38 * max(1.0, params.scale); }\n }\n let t = luminance_tone(e, eps);\n let reject = params.reject != 0u;\n for (var j = 0u; j < 3u; j++) {\n codes[i * 3u + j] = render_toned(e[j], s[j] != 0.0, 0.0, params.band1, t.m[j], t.delta[j], t.changed_uncertain, reject, params.maximum);\n }\n }\n flush_counters(local_index);\n}\n`;\n\n/**\n * Packed native RAW development of one band of output rows; one invocation per output pixel.\n * `normalized` holds the host's float64 site values, rounded once to float32. A saturated site\n * (flagged, or at or above its phase white level) is exactly 1. Any other site is\n * (code − black)/(white − black) ≤ 1 − 1/65535. So a saturated contributing site is recognised exactly.\n * Highlight handling matches `prepareSensorColorDevelopment` (clip at the smallest applied gain, or the\n * declared neutral blend).\n */\nexport const RAW_DEVELOPMENT_WGSL = /* wgsl */ `\nstruct Params {\n width: u32, count: u32, row0: u32, reinhard: u32,\n reject: u32, repeat_w: u32, repeat_h: u32, region_w: u32,\n origin_x: i32, origin_y: i32, axx: i32, axy: i32,\n ayx: i32, ayy: i32, region_x: i32, region_y: i32,\n scale: f32, maximum: f32, band0: f32, band1: f32,\n clip_level: f32, reconstruct: u32, band_u: f32, band_s: f32,\n gains: vec4<f32>,\n m0: vec4<f32>, m1: vec4<f32>, m2: vec4<f32>,\n x0: vec4<f32>, x1: vec4<f32>, x2: vec4<f32>,\n tone_mode: u32, pad0: u32, pad1: u32, pad2: u32,\n tone: vec4<f32>, lum: vec4<f32>,\n};\nstruct Tap { ox: i32, oy: i32, w: f32, pad: f32 };\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> normalized: array<f32>;\n@group(0) @binding(2) var<storage, read> taps: array<Tap>;\n@group(0) @binding(3) var<storage, read> phases: array<vec2<u32>>;\n@group(0) @binding(4) var<storage, read_write> codes: array<u32>;\n@group(0) @binding(5) var<storage, read_write> counters: array<atomic<u32>, ${SDR_COUNTER_SLOTS}>;\n${SDR_FUNCTIONS}\n${LUMINANCE_FUNCTIONS}\nvar<private> saturated: bool;\nfn channel(phase: u32, c: u32, nx: i32, ny: i32) -> f32 {\n let range = phases[phase * 3u + c];\n var value = 0.0;\n for (var k = 0u; k < range.y; k++) {\n let t = taps[range.x + k];\n let sx = nx + t.ox - params.region_x;\n let sy = ny + t.oy - params.region_y;\n let site = normalized[u32(sy) * params.region_w + u32(sx)];\n if (site == 1.0) { saturated = true; }\n value = value + site * t.w;\n }\n return value;\n}\nfn row3(m: vec4<f32>, v: vec3<f32>) -> f32 { return ((0.0 + m.x * v.x) + m.y * v.y) + m.z * v.z; }\nfn max3(v: vec3<f32>) -> f32 {\n if (v.x > v.y) { if (v.x > v.z) { return v.x; } return v.z; }\n if (v.y > v.z) { return v.y; }\n return v.z;\n}\n@compute @workgroup_size(${LINEAR_WORKGROUP_SIZE})\nfn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(local_invocation_index) local_index: u32,\n @builtin(num_workgroups) groups: vec3<u32>) {\n clear_counters(local_index);\n let i = gid.x + gid.y * groups.x * ${LINEAR_WORKGROUP_SIZE}u;\n if (i < params.count) {\n let col = i32(i % params.width);\n let row = i32(params.row0 + i / params.width);\n let nx = params.origin_x + params.axx * col + params.axy * row;\n let ny = params.origin_y + params.ayx * col + params.ayy * row;\n let phase = (u32(ny) % params.repeat_h) * params.repeat_w + u32(nx) % params.repeat_w;\n saturated = false;\n let c = vec3<f32>(channel(phase, 0u, nx, ny), channel(phase, 1u, nx, ny), channel(phase, 2u, nx, ny));\n if (saturated) { count(${SDR_COUNTER.saturatedPixels}u); }\n let u = vec3<f32>(c.x * params.gains.x, c.y * params.gains.y, c.z * params.gains.z);\n let clip = params.clip_level;\n let k = vec3<f32>(select(u.x, clip, u.x > clip), select(u.y, clip, u.y > clip), select(u.z, clip, u.z > clip));\n var b = k;\n if (params.reconstruct == 0u) {\n for (var j = 0u; j < 3u; j++) {\n if (u[j] > clip) { count(${SDR_COUNTER.whiteBalanceClipped}u); }\n if (abs(u[j] - clip) <= params.band_u) { count(${SDR_COUNTER.uncertain}u); }\n }\n } else {\n let s = max3(c);\n if (abs(s - ${RAW_HIGHLIGHT_START_WGSL}) <= params.band_s) { count(${SDR_COUNTER.uncertain}u); }\n if (s > ${RAW_HIGHLIGHT_START_WGSL}) {\n var t = 1.0;\n if (s < 1.0) { t = (s - ${RAW_HIGHLIGHT_START_WGSL}) / (1.0 - ${RAW_HIGHLIGHT_START_WGSL}); }\n let n = max3(k);\n b = (1.0 - t) * u + t * vec3<f32>(n, n, n);\n count(${SDR_COUNTER.reconstructedPixels}u);\n } else {\n b = u;\n }\n }\n let xyz = vec3<f32>(row3(params.m0, b), row3(params.m1, b), row3(params.m2, b));\n let rgb = vec3<f32>(row3(params.x0, xyz), row3(params.x1, xyz), row3(params.x2, xyz));\n let reinhard = params.reinhard != 0u;\n let reject = params.reject != 0u;\n if (params.tone_mode == ${SDR_TONE_MODE.perChannel}u) {\n for (var j = 0u; j < 3u; j++) {\n codes[i * 3u + j] = render_sample(rgb[j] * params.scale, false, params.band0, params.band1, reinhard, reject, params.maximum);\n }\n } else {\n // band0 bounds each exposed value against the reference (the development chain's a-priori bound).\n let e = rgb * params.scale;\n let t = luminance_tone(e, vec3<f32>(params.band0, params.band0, params.band0));\n for (var j = 0u; j < 3u; j++) {\n codes[i * 3u + j] = render_toned(e[j], false, params.band0, params.band1, t.m[j], t.delta[j], t.changed_uncertain, reject, params.maximum);\n }\n }\n }\n flush_counters(local_index);\n}\n`;\n\n/** Source-indexed isoplanatic-patch convolution of one output-row band; one invocation per output site. */\nexport const IRRADIANCE_CONVOLUTION_WGSL = /* wgsl */ `\nstruct Params {\n width: u32, height: u32, radius: u32, size: u32,\n patch_w: u32, patch_h: u32, cols: u32, rows: u32,\n out_row0: u32, out_rows: u32, in_row0: u32, taps: u32,\n};\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> source: array<f32>;\n@group(0) @binding(2) var<storage, read> kernels: array<f32>;\n@group(0) @binding(3) var<storage, read_write> result: array<f32>;\n@compute @workgroup_size(${CONVOLUTION_WORKGROUP_EDGE}, ${CONVOLUTION_WORKGROUP_EDGE})\nfn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n let x = gid.x;\n let local_y = gid.y;\n if (x >= params.width || local_y >= params.out_rows) { return; }\n let y = i32(params.out_row0 + local_y);\n let r = i32(params.radius);\n var sum = 0.0;\n for (var v = -r; v <= r; v++) {\n let sy = y - v;\n if (sy < 0 || sy >= i32(params.height)) { continue; }\n let patch_row = min(u32(sy) / params.patch_h, params.rows - 1u) * params.cols;\n let kernel_row = (v + r) * i32(params.size) + r;\n let source_row = (u32(sy) - params.in_row0) * params.width;\n for (var u = -r; u <= r; u++) {\n let sx = i32(x) - u;\n if (sx < 0 || sx >= i32(params.width)) { continue; }\n let patch_index = patch_row + min(u32(sx) / params.patch_w, params.cols - 1u);\n sum = sum + source[source_row + u32(sx)] * kernels[patch_index * params.taps + u32(kernel_row + u)];\n }\n }\n result[local_y * params.width + x] = sum;\n}\n`;\n"]}
|