@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.
Files changed (125) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -7
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +2 -0
  6. package/dist/camera/lens-projection.js +2 -1
  7. package/dist/camera/lens-projection.js.map +1 -1
  8. package/dist/capture/dng-import.js +1 -1
  9. package/dist/capture/dng-import.js.map +1 -1
  10. package/dist/capture/host-yield-cadence.js +1 -1
  11. package/dist/capture/host-yield-cadence.js.map +1 -1
  12. package/dist/capture/native-environment-optical-stage.js +1 -1
  13. package/dist/capture/native-environment-optical-stage.js.map +1 -1
  14. package/dist/capture/native-raw.js +1 -1
  15. package/dist/capture/native-raw.js.map +1 -1
  16. package/dist/capture/packed-sensor-raw.js +3 -3
  17. package/dist/capture/packed-sensor-raw.js.map +1 -1
  18. package/dist/capture/sensor-raw-producer.js +1 -1
  19. package/dist/capture/sensor-raw-producer.js.map +1 -1
  20. package/dist/color/capture-color.js +3 -3
  21. package/dist/color/capture-color.js.map +1 -1
  22. package/dist/color/chromatic-adaptation.js +2 -2
  23. package/dist/color/chromatic-adaptation.js.map +1 -1
  24. package/dist/color/color-matching-functions.js +1 -1
  25. package/dist/color/color-matching-functions.js.map +1 -1
  26. package/dist/color/colorimetry-math.js +2 -2
  27. package/dist/color/colorimetry-math.js.map +1 -1
  28. package/dist/color/illuminants.js +3 -3
  29. package/dist/color/illuminants.js.map +1 -1
  30. package/dist/color/rgb-color-spaces.js +1 -1
  31. package/dist/color/rgb-color-spaces.js.map +1 -1
  32. package/dist/color/sensor-color-development.js +1 -1
  33. package/dist/color/sensor-color-development.js.map +1 -1
  34. package/dist/core/record-validation.js +3 -3
  35. package/dist/core/record-validation.js.map +1 -1
  36. package/dist/core/version.d.ts +1 -1
  37. package/dist/core/version.js +1 -1
  38. package/dist/core/version.js.map +1 -1
  39. package/dist/equipment/generic-prime-lenses.js +3 -3
  40. package/dist/equipment/generic-prime-lenses.js.map +1 -1
  41. package/dist/equipment/generic-tier-assets.d.ts +23 -1
  42. package/dist/equipment/generic-tier-assets.js +58 -30
  43. package/dist/equipment/generic-tier-assets.js.map +1 -1
  44. package/dist/equipment/generic-tier-presets.d.ts +6 -3
  45. package/dist/equipment/generic-tier-presets.js +26 -13
  46. package/dist/equipment/generic-tier-presets.js.map +1 -1
  47. package/dist/exposure/auto-iso-slowest-shutter.js +2 -1
  48. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -1
  49. package/dist/exposure/flash-ttl.js +1 -1
  50. package/dist/exposure/flash-ttl.js.map +1 -1
  51. package/dist/exposure/flash.js +1 -1
  52. package/dist/exposure/flash.js.map +1 -1
  53. package/dist/facade/chart-material-layers.js +2 -2
  54. package/dist/facade/chart-material-layers.js.map +1 -1
  55. package/dist/facade/chart-radiance-batch.js +1 -1
  56. package/dist/facade/chart-radiance-batch.js.map +1 -1
  57. package/dist/facade/chart-scenes.d.ts +45 -8
  58. package/dist/facade/chart-scenes.js +101 -44
  59. package/dist/facade/chart-scenes.js.map +1 -1
  60. package/dist/facade/color-patches.d.ts +129 -0
  61. package/dist/facade/color-patches.js +209 -0
  62. package/dist/facade/color-patches.js.map +1 -0
  63. package/dist/facade/display-view-gpu.js +1 -1
  64. package/dist/facade/display-view-gpu.js.map +1 -1
  65. package/dist/facade/scene-radiance.js +2 -2
  66. package/dist/facade/scene-radiance.js.map +1 -1
  67. package/dist/facade/simulate-capture.d.ts +47 -3
  68. package/dist/facade/simulate-capture.js +71 -22
  69. package/dist/facade/simulate-capture.js.map +1 -1
  70. package/dist/facade/simulate-display-view.js +2 -2
  71. package/dist/facade/simulate-display-view.js.map +1 -1
  72. package/dist/gpu/chart-composite-gpu.js +2 -2
  73. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  74. package/dist/gpu/display-view-composite-material-module.js +1 -1
  75. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  76. package/dist/gpu/image-kernel-execution.js +2 -2
  77. package/dist/gpu/image-kernel-execution.js.map +1 -1
  78. package/dist/gpu/temporal-sum-gpu.js +23 -23
  79. package/dist/gpu/webgpu-runtime.js +1 -1
  80. package/dist/gpu/webgpu-runtime.js.map +1 -1
  81. package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
  82. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
  83. package/dist/gpu/wgsl-chart-kernel.js +117 -117
  84. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  85. package/dist/gpu/wgsl-display-view-quality-kernel.js +505 -505
  86. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  87. package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
  88. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  89. package/dist/gpu/wgsl-kernels.js +203 -203
  90. package/dist/gpu/wgsl-kernels.js.map +1 -1
  91. package/dist/gpu/wgsl-sensor-kernel.js +302 -302
  92. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  93. package/dist/index.d.ts +5 -3
  94. package/dist/index.js +4 -2
  95. package/dist/index.js.map +1 -1
  96. package/dist/motion/temporal-exposure-sum.js +1 -1
  97. package/dist/motion/temporal-exposure-sum.js.map +1 -1
  98. package/dist/optics/diffraction-psf-profile.js +4 -4
  99. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  100. package/dist/optics/front-of-lens-filter.js +1 -1
  101. package/dist/optics/front-of-lens-filter.js.map +1 -1
  102. package/dist/optics/irradiance-convolution.js +1 -1
  103. package/dist/optics/irradiance-convolution.js.map +1 -1
  104. package/dist/output/native-capture-sdr.js +1 -1
  105. package/dist/output/native-capture-sdr.js.map +1 -1
  106. package/dist/output/png-encoder.js +2 -2
  107. package/dist/output/png-encoder.js.map +1 -1
  108. package/dist/output/print-jpeg.js +1 -1
  109. package/dist/output/print-jpeg.js.map +1 -1
  110. package/dist/sensor/fixed-pattern-noise.js +1 -1
  111. package/dist/sensor/fixed-pattern-noise.js.map +1 -1
  112. package/dist/sensor/readout-exposure-linkage.js +1 -1
  113. package/dist/sensor/readout-exposure-linkage.js.map +1 -1
  114. package/docs/API_REFERENCE.md +29 -1
  115. package/docs/QUICK_START.md +40 -5
  116. package/docs/USAGE.md +35 -1
  117. package/docs/api/core-version.md +1 -1
  118. package/docs/api/equipment-generic-tier-assets.md +41 -1
  119. package/docs/api/equipment-generic-tier-presets.md +11 -3
  120. package/docs/api/exports.json +107 -2
  121. package/docs/api/facade-chart-scenes.md +36 -5
  122. package/docs/api/facade-color-patches.md +150 -0
  123. package/docs/api/facade-simulate-capture.md +52 -2
  124. package/llms.txt +1 -0
  125. package/package.json +2 -2
@@ -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
- 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;
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
- // Gamut decisions (mapped < 0, mapped > 1) depend only on the sign band and, for identity tone, the band at 1.
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; }
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
- var mapped = e;
49
- if (reinhard && e > 0.0) { mapped = e / (1.0 + e); }
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
- var linear = mapped;
52
- if (mapped < 0.0 || mapped > 1.0) {
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
- let code = floor(encode_srgb(linear) * maximum + 0.5);
58
- return u32(clamp(code, 0.0, maximum));
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
- workgroupBarrier();
62
+ workgroupBarrier();
63
63
  }
64
64
  fn flush_counters(local_index: u32) {
65
- workgroupBarrier();
65
+ workgroupBarrier();
66
66
  if (local_index < ${SDR_COUNTER_SLOTS}u) {
67
- let value = atomicLoad(&local_counters[local_index]);
67
+ let value = atomicLoad(&local_counters[local_index]);
68
68
  if (local_index == ${SDR_COUNTER.linearBoundBits}u) { atomicMax(&counters[local_index], value); }
69
- else if (value > 0u) { atomicAdd(&counters[local_index], value); }
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
- // (h, eta, abs_h): |h_gpu - h(l)| <= eta*h(l) + abs_h for the exact h at this float32 l > 0.
87
+
88
88
  fn tone_h(l: f32) -> vec3<f32> {
89
89
  if (params.tone_mode == ${SDR_TONE_MODE.extendedReinhard}u) {
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
- // log2: 2^-21 absolute or 3 ULP; the sigma/l division and sigma's rounding; the product's rounding.
96
- let dz = n * (4.76837158203125e-7 + 3.0 * ULP1 * abs(lg) + 9.0 * LU) + 2.0 * LU * abs(z);
97
- // exp2: 3 + 2|z| ULP; plus slack for the float64 reference's pow.
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
- // Below 2^-120 the power may have flushed to zero: bound h absolutely instead.
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);
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
- 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
- // L < 0 for the reference too: both keep the exposed values.
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
- // |L| is inside its classification band: either branch may run, so bound both.
135
- var hsup = max(1.0, params.tone.x);
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
- 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
- // Classification, gamut handling and encoding of one luminance-toned sample (e exposed, m toned).
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
- var uncertain = (e == 0.0 && source_nonzero) || abs(e - 1.0) <= band1 || (band0 > 0.0 && abs(e) <= band0) || (changed_uncertain && e != 0.0);
149
- // The reference value lies in [m - delta, m + delta]: uncertain only if that interval straddles a decision.
150
- let gamut_uncertain = (m - delta < 0.0 && m + delta >= 0.0) || (m + delta > 1.0 && m - delta <= 1.0);
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
- var linear = m;
158
- var bound = min(delta, 1.0);
159
- if (m < 0.0 || m > 1.0) {
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
- // Clipping is 1-Lipschitz; when both values clip to the same end the difference is zero.
165
- if (!reject && (m - delta >= 1.0 || m + delta <= 0.0)) { bound = 0.0; }
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
- let code = floor(encode_srgb(linear) * maximum + 0.5);
168
- return u32(clamp(code, 0.0, maximum));
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
- @builtin(num_workgroups) groups: vec3<u32>) {
182
- clear_counters(local_index);
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
- 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);
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
- tone: vec4<f32>, lum: vec4<f32> };
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
- @builtin(num_workgroups) groups: vec3<u32>) {
204
- clear_counters(local_index);
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
- 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
- // Source, scale and product roundings (plus the reference's own), and an absolute term where a non-zero source may underflow.
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);
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
- 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>,
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
- 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;
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
- 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;
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
- @builtin(num_workgroups) groups: vec3<u32>) {
277
- clear_counters(local_index);
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
- 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));
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
- 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++) {
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
- } else {
298
- let s = max3(c);
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
- var t = 1.0;
301
+ var t = 1.0;
302
302
  if (s < 1.0) { t = (s - ${RAW_HIGHLIGHT_START_WGSL}) / (1.0 - ${RAW_HIGHLIGHT_START_WGSL}); }
303
- let n = max3(k);
304
- b = (1.0 - t) * u + t * vec3<f32>(n, n, n);
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
- } 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;
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
- 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
- // band0 bounds each exposed value against the reference (the development chain's a-priori bound).
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);
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
- 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,
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
- 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;
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"]}