Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
coefficient_field.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with darktable. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19// Coefficient-field colour-line transport + HF-refit stage (CPU + OpenCL). (implementation; see
20// coefficient_field.h for the public API.)
21
22#include "system/openmp.h"
25#include "iop/highlights/blur.h"
26#include "iop/highlights/knee.h"
27#include "iop/highlights/pde.h" // _region_blur1_cl(): single-channel device gaussian for the guide // _knee_blur(): single-channel gaussian for the edge-aware guide
30#include <math.h>
31#include <string.h>
32#include <stdlib.h>
33
34// Variance-adaptive steering tensor: a continuous blend between the isophote
35// tensor (transport along level lines, correct where a HARD EDGE crosses the zone: the content
36// beyond the edge follows another colour-line) and the gradient tensor (radial transport,
37// correct on a clean halo: the model lives on the rim and must travel inward). The blend weight
38// m is the TREND-CORRECTED windowed variance of the steering plane: raw windowed variance minus
39// the part the local linear ramp explains -- a smooth halo ramp has variance but no residual,
40// a hard edge has residual variance no ramp can explain. m = v_res / (v_res + (k * mean)^2),
41// scale-free (k = relative std threshold). D = [m + (1-m) c2] t t^T + [m c2 + (1-m)] g g^T,
42// both weights in (0, 1], D SPD, so the Weickert stencil stays nonnegative (maximum principle).
43//
44// MATHS BRIDGE -- article "The algorithm" step 3, the E_transport steering tensor. Builds the D of
45// E_transport = Sum_p integral grad(p)^T D grad(p) dOmega, whose Euler-Lagrange div(D grad p)=0 is the
46// anisotropic fill relaxed below. Article eq:
47// D = [ m + (1-m) c2 ] t t^T + [ m c2 + (1-m) ] g g^T , c2 = exp(-|grad L_mean| / (4 <|grad L_mean|>))
48// m = v / (v + (k Lbar_mean)^2) , v = max( var_w(L_mean) - (4/3)|grad L_mean|^2 , 0 ) , k = 0.15
49// g = unit gradient (uphill) of the steering plane L_mean, t = unit isophote (level-line), m in [0,1] the
50// edge probability. m->0 (clean halo ramp) => D -> g g^T + c2 t t^T, transport radial inward along the
51// ramp; m->1 (hard edge in the zone) => D -> t t^T + c2 g g^T, transport along the boundary, not across it.
53static void _cf_adaptive_tensor(const float *const restrict luminance, float *const restrict tensor_xx,
54 float *const restrict tensor_xy, float *const restrict tensor_yy,
55 float *const restrict scratch_lin, float *const restrict scratch_quad,
56 const int region_w, const int region_h, const float k)
57{
58 const size_t region_pixels = (size_t)region_w * region_h;
59
60 // two 3x3 box passes on L (into scratch_lin) and on L^2 (into scratch_quad)
61 for(int pass = 0; pass < 2; pass++)
62 {
63 const float *const src_lin = (pass == 0) ? luminance : scratch_lin;
64 const float *const src_quad = (pass == 0) ? luminance : scratch_quad; // pass 0 squares on the fly
65
66 HL_PFOR(collapse(2))
67 for(int y = 0; y < region_h; y++)
68 for(int x = 0; x < region_w; x++)
69 {
70 double sum_lin = 0.0;
71 double sum_quad = 0.0;
72 int count = 0;
73 for(int offset_y = -1; offset_y <= 1; offset_y++)
74 for(int offset_x = -1; offset_x <= 1; offset_x++)
75 {
76 const int neighbour_y = CLAMP(y + offset_y, 0, region_h - 1);
77 const int neighbour_x = CLAMP(x + offset_x, 0, region_w - 1);
78 const float value_lin = src_lin[(size_t)neighbour_y * region_w + neighbour_x];
79 sum_lin += value_lin;
80 sum_quad += (pass == 0) ? (double)value_lin * value_lin
81 : src_quad[(size_t)neighbour_y * region_w + neighbour_x];
82 count++;
83 }
84 tensor_xx[(size_t)y * region_w + x] = (float)(sum_lin / count);
85 tensor_yy[(size_t)y * region_w + x] = (float)(sum_quad / count);
86 }
87
88 HL_PFOR()
89 for(size_t i = 0; i < region_pixels; i++)
90 {
91 scratch_lin[i] = tensor_xx[i];
92 scratch_quad[i] = tensor_yy[i];
93 }
94 }
95
96 // gradients of the blurred L + mean magnitude
97 double grad_sum = 0.0;
98 HL_PFOR(collapse(2) reduction(+ : grad_sum))
99 for(int y = 0; y < region_h; y++)
100 for(int x = 0; x < region_w; x++)
101 {
102 const int x_lo = MAX(x - 1, 0), x_hi = MIN(x + 1, region_w - 1);
103 const int y_lo = MAX(y - 1, 0), y_hi = MIN(y + 1, region_h - 1);
104 const float grad_x
105 = 0.5f * (scratch_lin[(size_t)y * region_w + x_hi] - scratch_lin[(size_t)y * region_w + x_lo]);
106 const float grad_y
107 = 0.5f * (scratch_lin[(size_t)y_hi * region_w + x] - scratch_lin[(size_t)y_lo * region_w + x]);
108 tensor_xx[(size_t)y * region_w + x] = grad_x;
109 tensor_xy[(size_t)y * region_w + x] = grad_y;
110 grad_sum += dt_fast_hypotf(grad_x, grad_y);
111 }
112 const float grad_mean
113 = fmaxf((float)(grad_sum / (double)region_pixels),
114 1e-9f); // <|grad L_mean|>, the regional mean magnitude (exposure-independent normaliser)
115
116 HL_PFOR()
117 for(size_t i = 0; i < region_pixels; i++)
118 {
119 const float grad_x = tensor_xx[i];
120 const float grad_y = tensor_xy[i];
121 const float grad_mag = dt_fast_hypotf(grad_x, grad_y);
122 const float nonzero = (grad_mag > 1e-12f) ? 1.f : 0.f;
123 const float inv_mag = nonzero / (grad_mag + (1.f - nonzero));
124 const float grad_unit_x = grad_x * inv_mag + (1.f - nonzero); // g = unit gradient direction (uphill)
125 const float grad_unit_y = grad_y * inv_mag;
126 const float isophote_x = -grad_unit_y, isophote_y = grad_unit_x; // t = unit isophote = g rotated 90deg
127 const float cross_damp = expf(-grad_mag / (4.f * grad_mean)); // c2 = exp(-|grad L_mean| / (4 <|grad
128 // L_mean|>)), edge-crossing damping
129
130 // trend-corrected windowed variance: two 3x3 box passes have spatial variance 4/3 per axis
131 const float variance = fmaxf(scratch_quad[i] - scratch_lin[i] * scratch_lin[i],
132 0.f); // var_w(L_mean) = E[L^2]-E[L]^2 (centred by construction of the box passes)
133 const float residual_var
134 = fmaxf(variance - (4.f / 3.f) * (grad_x * grad_x + grad_y * grad_y),
135 0.f); // v = max(var_w - (4/3)|grad L_mean|^2, 0): subtract the variance the local ramp explains
136 const float k_term
137 = sqf(k * fmaxf(scratch_lin[i], 1e-9f)); // (k * Lbar_mean)^2, the scale-free contrast threshold
138 const float edge_prob
139 = residual_var / (residual_var + k_term + 1e-18f); // m = v / (v + (k Lbar_mean)^2) in [0,1]
140
141 const float diffuse_tangent = edge_prob + (1.f - edge_prob) * cross_damp; // coeff of t t^T = m + (1-m) c2
142 const float diffuse_gradient = edge_prob * cross_damp + (1.f - edge_prob); // coeff of g g^T = m c2 + (1-m)
143
144 // D = diffuse_tangent * t t^T + diffuse_gradient * g g^T, stored as its symmetric xx/xy/yy entries
145 tensor_xx[i] = diffuse_tangent * isophote_x * isophote_x + diffuse_gradient * grad_unit_x * grad_unit_x;
146 tensor_xy[i] = diffuse_tangent * isophote_x * isophote_y + diffuse_gradient * grad_unit_x * grad_unit_y;
147 tensor_yy[i] = diffuse_tangent * isophote_y * isophote_y + diffuse_gradient * grad_unit_y * grad_unit_y;
148 }
149}
150
151// Coarse-to-fine harmonic fill of up to DT_HL_FILL_MAXP coefficient planes SHARING ONE anchor
152// mask: hole pixels relax toward their 4-neighbour average (Jacobi) with anchors pinned, each
153// pyramid level seeding the next finer one. Unconditionally stable by the maximum principle
154// (values stay within the anchors' range), unlike a float CG on the near-singular pure-harmonic
155// system, which diverges stochastically when the hole reaches the region border. Coefficients
156// are smooth, so the solve runs on a base grid downsampled by `base_ds` and is bilinearly
157// upsampled into the hole pixels.
158// With `steer` non-NULL (the coefficient planes), the relaxation is tensor-weighted instead
159// of uniform: per level, the variance-adaptive tensor is built from the downsampled steering
160// plane (_cf_adaptive_tensor) and the update becomes an 8-neighbour average with the Weickert
161// nonnegativity weights (_aniso_edge_w) -- all weights >= 0, so the fill stays a convex
162// combination of anchors (maximum principle intact). NULL steer = plain isotropic fill (the
163// rim-chrominance ratios, and any plane with no guide structure to follow).
164
165// One level's Jacobi relaxation of NP planes sharing one anchor mask, macro-generated so NP
166// is a compile-time literal: the plane guards fold away and the per-plane accumulators stay in
167// registers. (An inline function with a runtime plane count does NOT specialize -- GCC outlines
168// the OpenMP region and the count arrives through the shared-args struct, so the su[] array
169// spilled to the stack on every fma and the fused sweep measured 2.5x SLOWER than the
170// single-plane one. Literal NP recovers it.)
171//
172// Jacobi relaxation of the holes (anchors pinned): a flat 100-sweep budget per level.
173// Convergence is guaranteed by the pyramid depth of the caller, NOT by the sweep count --
174// boosting sweeps at the coarsest level instead was measured pathological (thousands of
175// parallel sweeps of microsecond work = pure scheduling overhead, seconds per fill on small
176// regions). One parallel region for the whole relaxation: launching a fresh team per sweep
177// was pure scheduling overhead on these small grids (the sweep's work is microseconds; 100
178// sweeps x levels x fills x regions reached tens of thousands of launches per image). Threads
179// ping-pong between u and tmp (no per-sweep memcpy); the even sweep count lands the final
180// solution in u. The omp-for barrier at the end of each sweep keeps Jacobi ordering.
181// All NP planes advance inside the same sweep: the weights are read once per cell.
182//
183// MATHS BRIDGE -- article "The algorithm" step 3, one Jacobi sweep of the E_transport solver
184// div(D grad p)=0 on the coefficient planes p in {a, b, d, R^2}. Discrete update rules (anchors are
185// Dirichlet boundary data, pinned = copied through unchanged):
186// steered (D != I): dst(i) = Sum_k w_ik * src(neighbour_k) / Sum_k w_ik over the 8-neighbour
187// Weickert nonnegativity stencil weights w_ik = _aniso_edge_w(D) >= 0, so the
188// update is a convex combination of neighbours -> maximum principle holds.
189// isotropic (D = I): dst(i) = 1/4 (north + south + west + east), the plain harmonic (Laplace) fill.
190// NOTE (C preprocessor): every comment inside this macro body MUST be a /* ... */ closed on its own
191// physical line before the trailing backslash -- a // comment would splice with the next line and
192// swallow the rest of the macro. That is why the annotations below use block-comment form.
193#define DEFINE_CF_FILL_RELAX(NP) \
194 __DT_CLONE_TARGETS__ \
195 static void _cf_fill_relax_##NP( \
196 float *const restrict field, float *const restrict tmp, const uint8_t *const restrict level_anchor, \
197 const float *const restrict edge_weights, const float *const restrict edge_weight_sum, const int coarse_w, \
198 const int coarse_h, const size_t cell_count, const int steered) \
199 { \
200 const int n_sweeps = 100; \
201 __OMP_PARALLEL__() \
202 for(int sweep = 0; sweep < n_sweeps; sweep++) \
203 { \
204 const float *const source = (sweep & 1) ? tmp : field; \
205 float *const dest = (sweep & 1) ? field : tmp; \
206 const float *const src0 = source; \
207 const float *const src1 = source + ((NP) > 1 ? cell_count : 0); \
208 const float *const src2 = source + ((NP) > 2 ? 2 * cell_count : 0); \
209 const float *const src3 = source + ((NP) > 3 ? 3 * cell_count : 0); \
210 float *const dst0 = dest; \
211 float *const dst1 = dest + ((NP) > 1 ? cell_count : 0); \
212 float *const dst2 = dest + ((NP) > 2 ? 2 * cell_count : 0); \
213 float *const dst3 = dest + ((NP) > 3 ? 3 * cell_count : 0); \
214 \
215 __OMP_FOR__(collapse(2)) \
216 for(int cell_y = 0; cell_y < coarse_h; cell_y++) \
217 for(int cell_x = 0; cell_x < coarse_w; cell_x++) \
218 { \
219 const size_t i = (size_t)cell_y * coarse_w + cell_x; \
220 \
221 /* anchor cell = Dirichlet boundary datum p|anchors = p_fit: copy it through unchanged */ \
222 if(level_anchor[i]) \
223 { \
224 dst0[i] = src0[i]; \
225 if((NP) > 1) dst1[i] = src1[i]; \
226 if((NP) > 2) dst2[i] = src2[i]; \
227 if((NP) > 3) dst3[i] = src3[i]; \
228 continue; \
229 } \
230 \
231 const size_t idx_north = (size_t)MAX(cell_y - 1, 0) * coarse_w + cell_x; \
232 const size_t idx_south = (size_t)MIN(cell_y + 1, coarse_h - 1) * coarse_w + cell_x; \
233 const size_t idx_west = (size_t)cell_y * coarse_w + MAX(cell_x - 1, 0); \
234 const size_t idx_east = (size_t)cell_y * coarse_w + MIN(cell_x + 1, coarse_w - 1); \
235 \
236 if(steered) \
237 { \
238 /* 8-neighbour Jacobi with the precomputed Weickert nonnegativity weights: every */ \
239 /* weight >= 0, so the update is a convex combination and the maximum principle */ \
240 /* is preserved. */ \
241 static const int neighbour_dy[8] = { 0, 0, -1, 1, -1, 1, 1, -1 }; \
242 static const int neighbour_dx[8] = { -1, 1, 0, 0, -1, 1, -1, 1 }; \
243 float accum0 = 0.f, accum1 = 0.f, accum2 = 0.f, accum3 = 0.f; \
244 for(int k = 0; k < 8; k++) \
245 { \
246 const int neighbour_y = CLAMP(cell_y + neighbour_dy[k], 0, coarse_h - 1); \
247 const int neighbour_x = CLAMP(cell_x + neighbour_dx[k], 0, coarse_w - 1); \
248 const size_t j = (size_t)neighbour_y * coarse_w + neighbour_x; \
249 const float weight = edge_weights[i * 8 + k]; \
250 accum0 += weight * src0[j]; \
251 if((NP) > 1) accum1 += weight * src1[j]; \
252 if((NP) > 2) accum2 += weight * src2[j]; \
253 if((NP) > 3) accum3 += weight * src3[j]; \
254 } \
255 /* dst = Sum_k w_ik src(nb_k) / Sum_k w_ik : the steered div(D grad p)=0 Jacobi update */ \
256 const float weight_sum = edge_weight_sum[i]; \
257 const int valid = (weight_sum > 1e-9f); \
258 dst0[i] = valid ? accum0 / weight_sum : src0[i]; \
259 if((NP) > 1) dst1[i] = valid ? accum1 / weight_sum : src1[i]; \
260 if((NP) > 2) dst2[i] = valid ? accum2 / weight_sum : src2[i]; \
261 if((NP) > 3) dst3[i] = valid ? accum3 / weight_sum : src3[i]; \
262 } \
263 /* D = I: plain 4-neighbour average, the discrete harmonic (Laplace) fill div(grad p)=0 */ \
264 else \
265 { \
266 dst0[i] = 0.25f * (src0[idx_north] + src0[idx_south] + src0[idx_west] + src0[idx_east]); \
267 if((NP) > 1) dst1[i] = 0.25f * (src1[idx_north] + src1[idx_south] + src1[idx_west] + src1[idx_east]); \
268 if((NP) > 2) dst2[i] = 0.25f * (src2[idx_north] + src2[idx_south] + src2[idx_west] + src2[idx_east]); \
269 if((NP) > 3) dst3[i] = 0.25f * (src3[idx_north] + src3[idx_south] + src3[idx_west] + src3[idx_east]); \
270 } \
271 } \
272 } \
273 }
274
279
280// MATHS BRIDGE -- article "The algorithm" step 3, the E_transport solver: the anchored, coarse-to-fine
281// anisotropic transport of the coefficient planes. Minimizes E_transport = Sum_p int grad(p)^T D grad(p)
282// with p|anchors = p_fit by relaxing div(D grad p)=0 to its fixed point. `hole` marks the cells to fill
283// (holes); its complement are the anchors (the gated colour-line fits, R^2 > 0.25, bounded slopes).
284// `steer` non-NULL feeds _cf_adaptive_tensor to build D (steered fill); NULL => D = I (plain harmonic
285// fill). Coefficients are smooth, so the whole relaxation runs on a base grid at pitch ~sigma/4
286// (article "Cell"), and Jacobi convergence comes from the PYRAMID DEPTH, not the fixed 100-sweep budget:
287// the coarsest level starts from a flat anchor mean and each finer level is bilinearly seeded from the
288// coarser solution, then corrected. Final result is bilinearly upsampled into the full-res hole pixels.
290static void _cf_harmonic_fill_n(float *const restrict *vals, const int n_planes_in,
291 const uint8_t *const restrict hole, const int region_w, const int region_h,
292 const int base_ds, const float *const restrict steer,
293 const dt_dev_pixelpipe_t *pipe)
294{
295 const int n_planes = CLAMP(n_planes_in, 1, DT_HL_FILL_MAXP);
296 const int downsample = CLAMP(base_ds, 1, 8);
297 const int base_w = (region_w + downsample - 1) / downsample;
298 const int base_h = (region_h + downsample - 1) / downsample;
299 const size_t cell_count = (size_t)base_w * base_h;
300
301 float *const restrict base_vals = dt_pixelpipe_cache_alloc_align_float(cell_count * n_planes, pipe);
302 uint8_t *const restrict base_anchor
303 = (uint8_t *)dt_pixelpipe_cache_alloc_align(sizeof(uint8_t) * cell_count, pipe);
304 // field, tmp, f (n_planes planes each, plane-major) + shared L
305 float *const restrict level_buffers
306 = dt_pixelpipe_cache_alloc_align_float(cell_count * 3 * (size_t)n_planes, pipe);
307 uint8_t *const restrict level_anchor
308 = (uint8_t *)dt_pixelpipe_cache_alloc_align(sizeof(uint8_t) * cell_count, pipe);
309 // aniso: base-grid steering plane + per-level {level_steer, tensor_xx, tensor_xy, tensor_yy, scratch,
310 // var-scratch}
311 // + per-cell edge weights (8, interleaved) + their sum, precomputed once per level
312 float *const restrict aniso_aux = steer ? dt_pixelpipe_cache_alloc_align_float(cell_count * 16, pipe) : NULL;
313 const int steered = (steer && aniso_aux) ? 1 : 0;
314
315 if(!base_vals || !base_anchor || !level_buffers || !level_anchor || (steer && !aniso_aux))
316 {
317 // fallback: fill the holes with the global anchor mean (never leave garbage coefficients)
318 for(int plane = 0; plane < n_planes; plane++)
319 {
320 float *const restrict plane_vals = vals[plane];
321 double anchor_sum = 0.0;
322 size_t anchor_count = 0;
323 for(size_t i = 0; i < (size_t)region_w * region_h; i++)
324 if(!hole[i])
325 {
326 anchor_sum += plane_vals[i];
327 anchor_count++;
328 }
329
330 const float anchor_mean = anchor_count ? (float)(anchor_sum / (double)anchor_count) : 0.f;
331 HL_PFOR()
332 for(size_t i = 0; i < (size_t)region_w * region_h; i++)
333 if(hole[i]) plane_vals[i] = anchor_mean;
334 }
335
338 dt_pixelpipe_cache_free_align(level_buffers);
339 dt_pixelpipe_cache_free_align(level_anchor);
341 return;
342 }
343
344 // aniso: steering plane on the base grid (plain cell mean; the tensor smooths later)
345 float *const restrict base_steer = steered ? aniso_aux + 5 * cell_count : NULL; // plane 6 = adaptive scratch
346 if(steered)
347 {
348 HL_PFOR(collapse(2))
349 for(int base_y = 0; base_y < base_h; base_y++)
350 for(int base_x = 0; base_x < base_w; base_x++)
351 {
352 double accum = 0.0;
353 int n_total = 0;
354 for(int y = base_y * downsample; y < MIN((base_y + 1) * downsample, region_h); y++)
355 for(int x = base_x * downsample; x < MIN((base_x + 1) * downsample, region_w); x++)
356 {
357 accum += steer[(size_t)y * region_w + x];
358 n_total++;
359 }
360 base_steer[(size_t)base_y * base_w + base_x] = (float)(accum / n_total);
361 }
362 }
363
364 // base grid: anchor-weighted mean per cell and per plane, anchor = cell majority (shared)
365 HL_PFOR(collapse(2))
366 for(int base_y = 0; base_y < base_h; base_y++)
367 for(int base_x = 0; base_x < base_w; base_x++)
368 {
369 double accum[DT_HL_FILL_MAXP] = { 0.0 };
370 int n_anchor = 0;
371 int n_total = 0;
372
373 for(int y = base_y * downsample; y < MIN((base_y + 1) * downsample, region_h); y++)
374 for(int x = base_x * downsample; x < MIN((base_x + 1) * downsample, region_w); x++)
375 {
376 const size_t i = (size_t)y * region_w + x;
377 n_total++;
378
379 if(!hole[i])
380 {
381 for(int plane = 0; plane < n_planes; plane++) accum[plane] += vals[plane][i];
382 n_anchor++;
383 }
384 }
385
386 const size_t cell_index = (size_t)base_y * base_w + base_x;
387 for(int plane = 0; plane < n_planes; plane++)
388 base_vals[plane * cell_count + cell_index] = n_anchor ? (float)(accum[plane] / n_anchor) : 0.f;
389 base_anchor[cell_index] = (2 * n_anchor > n_total);
390 }
391
392 // Pyramid depth (article step 3, "Convergence comes from the pyramid's depth"): the slowest Jacobi
393 // error mode on a hole N cells wide decays in O(N^2) sweeps, so the coarsest grid must be small.
394 // Halve until the LONG side is <= 8 cells. The coarsest level is seeded
395 // with a flat anchor mean, and Jacobi needs ~O(N^2) sweeps to relax a flat seed on a hole
396 // N cells wide -- so the coarsest grid must be small enough that the fixed per-level sweep
397 // budget genuinely converges it; every finer level then only corrects local interpolation
398 // error. (The previous short-side floor of 16 left elongated coarsest grids under-converged
399 // on deep holes: pk1synth -22% RMSE, occluded -13% once actually converged.)
400 int n_levels = 1;
401 while((MAX(base_w, base_h) >> n_levels) > 8 && n_levels < 12) n_levels++;
402
403 float *const restrict field = level_buffers + 0 * cell_count; // n_planes planes, stride cell_count
404 float *const restrict tmp = level_buffers + (size_t)n_planes * cell_count; // n_planes planes, stride cell_count
405 float *const restrict level_vals
406 = level_buffers + 2 * (size_t)n_planes * cell_count; // n_planes planes, stride cell_count
407
408 int prev_level_w = 0;
409 int prev_level_h = 0;
410
411 for(int level = n_levels - 1; level >= 0; level--)
412 {
413 const int step = 1 << level;
414 const int level_w = (base_w + step - 1) / step;
415 const int level_h = (base_h + step - 1) / step;
416
417 // downsample the base grid to this level (anchor-weighted mean + majority), into f/level_anchor
418 HL_PFOR(collapse(2))
419 for(int level_y = 0; level_y < level_h; level_y++)
420 for(int level_x = 0; level_x < level_w; level_x++)
421 {
422 double accum[DT_HL_FILL_MAXP] = { 0.0 };
423 int n_anchor = 0;
424 int n_total = 0;
425
426 for(int y = level_y * step; y < MIN((level_y + 1) * step, base_h); y++)
427 for(int x = level_x * step; x < MIN((level_x + 1) * step, base_w); x++)
428 {
429 const size_t i = (size_t)y * base_w + x;
430 n_total++;
431
432 if(base_anchor[i])
433 {
434 for(int plane = 0; plane < n_planes; plane++) accum[plane] += base_vals[plane * cell_count + i];
435 n_anchor++;
436 }
437 }
438
439 const size_t cell_index = (size_t)level_y * level_w + level_x;
440 for(int plane = 0; plane < n_planes; plane++)
441 level_vals[plane * cell_count + cell_index] = n_anchor ? (float)(accum[plane] / n_anchor) : 0.f;
442 level_anchor[cell_index] = (2 * n_anchor > n_total);
443 }
444
445 // aniso: level steering plane -> structure tensor (Weickert-stencil weights)
446 float *const restrict level_steer = steered ? aniso_aux + 0 * cell_count : NULL;
447 float *const restrict tensor_xx = steered ? aniso_aux + 1 * cell_count : NULL;
448 float *const restrict tensor_xy = steered ? aniso_aux + 2 * cell_count : NULL;
449 float *const restrict tensor_yy = steered ? aniso_aux + 3 * cell_count : NULL;
450
451 if(steered)
452 {
453 HL_PFOR(collapse(2))
454 for(int level_y = 0; level_y < level_h; level_y++)
455 for(int level_x = 0; level_x < level_w; level_x++)
456 {
457 double steer_sum = 0.0;
458 int n_total = 0;
459 for(int y = level_y * step; y < MIN((level_y + 1) * step, base_h); y++)
460 for(int x = level_x * step; x < MIN((level_x + 1) * step, base_w); x++)
461 {
462 steer_sum += base_steer[(size_t)y * base_w + x];
463 n_total++;
464 }
465 level_steer[(size_t)level_y * level_w + level_x] = (float)(steer_sum / n_total);
466 }
467
468 // build the steering tensor D at this pyramid level from the downsampled L_mean plane
469 _cf_adaptive_tensor(level_steer, tensor_xx, tensor_xy, tensor_yy, aniso_aux + 4 * cell_count,
470 aniso_aux + 6 * cell_count, level_w, level_h, DT_HL_CF_K);
471
472 // The Weickert edge weights are constant across every sweep of this level (the tensor is
473 // fixed): precompute the 8 weights per cell (interleaved) plus their sum once, so the
474 // Jacobi inner loop is a pure multiply-accumulate. Same values, same accumulation order
475 // as the previous inline computation -- the relaxation result is bit-identical.
476 float *const restrict edge_weights = aniso_aux + 7 * cell_count;
477 float *const restrict edge_weight_sum = aniso_aux + 15 * cell_count;
478 HL_PFOR(collapse(2))
479 for(int level_y = 0; level_y < level_h; level_y++)
480 for(int level_x = 0; level_x < level_w; level_x++)
481 {
482 static const int neighbour_dy[8] = { 0, 0, -1, 1, -1, 1, 1, -1 };
483 static const int neighbour_dx[8] = { -1, 1, 0, 0, -1, 1, -1, 1 };
484 const size_t i = (size_t)level_y * level_w + level_x;
485 float weight_sum = 0.f;
486 for(int k = 0; k < 8; k++)
487 {
488 const int neighbour_y = CLAMP(level_y + neighbour_dy[k], 0, level_h - 1);
489 const int neighbour_x = CLAMP(level_x + neighbour_dx[k], 0, level_w - 1);
490 const size_t cell_index = (size_t)neighbour_y * level_w + neighbour_x;
491 // w_ik: Weickert nonnegativity stencil weight for direction k, derived from D (>= 0)
492 const float weight
493 = _aniso_edge_w(tensor_xx, tensor_xy, tensor_yy, i, cell_index, neighbour_dx[k], neighbour_dy[k]);
494 edge_weights[i * 8 + k] = weight;
495 weight_sum += weight;
496 }
497 edge_weight_sum[i] = weight_sum;
498 }
499 }
500
501 if(level == n_levels - 1)
502 {
503 // coarsest: seed the holes with the level's anchor mean, per plane (the flat starting state,
504 // farthest from the solution; the pyramid depth guarantees Jacobi relaxes it within budget)
505 double anchor_sum[DT_HL_FILL_MAXP] = { 0.0 };
506 size_t anchor_count = 0;
507 for(size_t i = 0; i < (size_t)level_w * level_h; i++)
508 if(level_anchor[i])
509 {
510 for(int plane = 0; plane < n_planes; plane++) anchor_sum[plane] += level_vals[plane * cell_count + i];
511 anchor_count++;
512 }
513
514 float anchor_mean[DT_HL_FILL_MAXP];
515 for(int plane = 0; plane < n_planes; plane++)
516 anchor_mean[plane] = anchor_count ? (float)(anchor_sum[plane] / (double)anchor_count) : 0.f;
517 HL_PFOR()
518 for(size_t i = 0; i < (size_t)level_w * level_h; i++)
519 for(int plane = 0; plane < n_planes; plane++)
520 tmp[plane * cell_count + i] = level_anchor[i] ? level_vals[plane * cell_count + i] : anchor_mean[plane];
521 }
522 else
523 {
524 // seed the holes from the coarser solution (bilinear), anchors from this level's means
525 HL_PFOR(collapse(2))
526 for(int level_y = 0; level_y < level_h; level_y++)
527 for(int level_x = 0; level_x < level_w; level_x++)
528 {
529 const size_t i = (size_t)level_y * level_w + level_x;
530
531 if(level_anchor[i])
532 {
533 for(int plane = 0; plane < n_planes; plane++)
534 tmp[plane * cell_count + i] = level_vals[plane * cell_count + i];
535 continue;
536 }
537
538 const float grid_x = ((float)level_x + 0.5f) * 0.5f - 0.5f;
539 const float grid_y = ((float)level_y + 0.5f) * 0.5f - 0.5f;
540 const int x_lo = CLAMP((int)floorf(grid_x), 0, prev_level_w - 1);
541 const int y_lo = CLAMP((int)floorf(grid_y), 0, prev_level_h - 1);
542 const int x_hi = MIN(x_lo + 1, prev_level_w - 1);
543 const int y_hi = MIN(y_lo + 1, prev_level_h - 1);
544 const float frac_x = CLAMP(grid_x - x_lo, 0.f, 1.f);
545 const float frac_y = CLAMP(grid_y - y_lo, 0.f, 1.f);
546 for(int plane = 0; plane < n_planes; plane++)
547 {
548 const float *const plane_field = field + plane * cell_count;
549 const float interp_top = plane_field[(size_t)y_lo * prev_level_w + x_lo] * (1.f - frac_x)
550 + plane_field[(size_t)y_lo * prev_level_w + x_hi] * frac_x;
551 const float interp_bottom = plane_field[(size_t)y_hi * prev_level_w + x_lo] * (1.f - frac_x)
552 + plane_field[(size_t)y_hi * prev_level_w + x_hi] * frac_x;
553 tmp[plane * cell_count + i] = interp_top * (1.f - frac_y) + interp_bottom * frac_y;
554 }
555 }
556 }
557
558 for(int plane = 0; plane < n_planes; plane++)
559 memcpy(field + plane * cell_count, tmp + plane * cell_count, (size_t)level_w * level_h * sizeof(float));
560
561 // relaxation: iterate the div(D grad p)=0 Jacobi update to its fixed point on this level
562 // (specialized on the plane count, see DEFINE_CF_FILL_RELAX)
563 {
564 const float *const restrict edge_weights = steered ? aniso_aux + 7 * cell_count : NULL;
565 const float *const restrict edge_weight_sum = steered ? aniso_aux + 15 * cell_count : NULL;
566 switch(n_planes)
567 {
568 case 1:
569 _cf_fill_relax_1(field, tmp, level_anchor, edge_weights, edge_weight_sum, level_w, level_h, cell_count,
570 steered);
571 break;
572 case 2:
573 _cf_fill_relax_2(field, tmp, level_anchor, edge_weights, edge_weight_sum, level_w, level_h, cell_count,
574 steered);
575 break;
576 case 3:
577 _cf_fill_relax_3(field, tmp, level_anchor, edge_weights, edge_weight_sum, level_w, level_h, cell_count,
578 steered);
579 break;
580 default:
581 _cf_fill_relax_4(field, tmp, level_anchor, edge_weights, edge_weight_sum, level_w, level_h, cell_count,
582 steered);
583 break;
584 }
585 }
586
587 prev_level_w = level_w;
588 prev_level_h = level_h;
589 }
590
591 // upsample the base-grid coefficient solution into the full-res hole pixels by bilinear interp
592 // (anchors keep their exact fitted values -- the Dirichlet data is never overwritten)
593 HL_PFOR(collapse(2))
594 for(int y = 0; y < region_h; y++)
595 for(int x = 0; x < region_w; x++)
596 {
597 const size_t i = (size_t)y * region_w + x;
598
599 if(!hole[i]) continue;
600
601 const float grid_x = ((float)x + 0.5f) / downsample - 0.5f;
602 const float grid_y = ((float)y + 0.5f) / downsample - 0.5f;
603 const int x_lo = CLAMP((int)floorf(grid_x), 0, base_w - 1);
604 const int y_lo = CLAMP((int)floorf(grid_y), 0, base_h - 1);
605 const int x_hi = MIN(x_lo + 1, base_w - 1);
606 const int y_hi = MIN(y_lo + 1, base_h - 1);
607 const float frac_x = CLAMP(grid_x - x_lo, 0.f, 1.f);
608 const float frac_y = CLAMP(grid_y - y_lo, 0.f, 1.f);
609 for(int plane = 0; plane < n_planes; plane++)
610 {
611 const float *const plane_field = field + plane * cell_count;
612 const float interp_top = plane_field[(size_t)y_lo * base_w + x_lo] * (1.f - frac_x)
613 + plane_field[(size_t)y_lo * base_w + x_hi] * frac_x;
614 const float interp_bottom = plane_field[(size_t)y_hi * base_w + x_lo] * (1.f - frac_x)
615 + plane_field[(size_t)y_hi * base_w + x_hi] * frac_x;
616 vals[plane][i] = interp_top * (1.f - frac_y) + interp_bottom * frac_y;
617 }
618 }
619
622 dt_pixelpipe_cache_free_align(level_buffers);
623 dt_pixelpipe_cache_free_align(level_anchor);
625}
626
627void _cf_harmonic_fill(float *const restrict val, const uint8_t *const restrict hole, const int region_w,
628 const int region_h, const int base_ds, const float *const restrict steer,
629 const dt_dev_pixelpipe_t *pipe)
630{
631 float *plane_ptrs[1] = { val };
632 _cf_harmonic_fill_n((float *const restrict *)plane_ptrs, 1, hole, region_w, region_h, base_ds, steer, pipe);
633}
634
637{
638 const _hl_region_t *const region = ctx->region;
639 const dt_dev_pixelpipe_t *const pipe = ctx->pipe;
640 const int region_w = ctx->region_w;
641 const int region_h = ctx->region_h;
642 const size_t region_pixels = ctx->region_pixels;
643 float *const restrict estimate = ctx->estimate;
644 float *const restrict prev_scale = ctx->prev_scale;
645 float *const restrict valid = ctx->valid;
646 float *const restrict blur_in = ctx->blur_in;
647 float *const restrict plane1 = ctx->plane1;
648 float *const restrict plane2 = ctx->plane2;
649 float *const restrict plane3 = ctx->plane3;
650 float *const restrict valid_variance = ctx->valid_variance;
651 float *const restrict guide_score = ctx->guide_score;
652 float *const restrict clip_depth = ctx->clip_depth;
653 float *const restrict clip0 = ctx->clip0;
654 uint8_t *const restrict hole = ctx->hole;
655 float *const restrict solver_field = ctx->solver_field;
656 float *const restrict fill_planes = ctx->fill_planes;
657 float *const restrict dome_lum = ctx->dome_lum;
658 float *const restrict lum_accum = ctx->lum_accum;
659 float *const restrict reaction_weight = ctx->reaction_weight;
660 float *const restrict flat_target = ctx->flat_target;
661
662 const float cf_sigma
663 = CLAMP(region->radius / 6.f, 8.f, 64.f); // sigma = clip(r/6, 8, 64): +/-3 sigma window reaches the deepest
664 // pixel; floor/cap bound samples/cost
665 const float cf_fmin = 0.05f;
666
667 // Edge-aware transport of the fit windows (see _region_edge_blur): guide = the region's own
668 // luminance, so a silhouette stops the moments from crossing it; range scale is a fraction of the
669 // blown zone's plateau, so it follows exposure like every other threshold here.
670 float *const restrict cf_guide = ctx->cg_tmp1; // group-B scratch, free at this point
671 float *const restrict cf_step_x = ctx->cg_tmp2; // domain-transform warp rates
672 float *const restrict cf_step_y = ctx->cg_residual;
673
674 // region luminance + the blown zone's plateau level, for the occlusion-aware fills
675 HL_PFOR()
676 for(size_t i = 0; i < region_pixels; i++)
677 lum_accum[i] = estimate[i * 4 + 0] + estimate[i * 4 + 1] + estimate[i * 4 + 2];
678
679 double laccum = 0.0;
680 size_t lcnt = 0;
681 HL_PFOR(reduction(+ : laccum, lcnt))
682 for(size_t i = 0; i < region_pixels; i++)
683 if(valid[i * 4 + 0] < 0.5f || valid[i * 4 + 1] < 0.5f || valid[i * 4 + 2] < 0.5f)
684 {
685 laccum += lum_accum[i];
686 lcnt++;
687 }
688
689 const float cf_lref = lcnt ? (float)(laccum / (double)lcnt) : 0.f;
690
691 // Steering plane for the coefficient fills = the measured guide structure.
692 // Mean of the VALID channels where at least one survives (real data inside the
693 // partial-clip zone); the flat plateau mean elsewhere (all-clip core), where a flat
694 // steer degenerates the tensor to identity, i.e. back to the isotropic fill.
695 float *const restrict steer = dt_pixelpipe_cache_alloc_align_float(region_pixels, pipe);
696 if(steer)
697 {
698 HL_PFOR()
699 for(size_t i = 0; i < region_pixels; i++)
700 {
701 float accum = 0.f;
702 int n_valid = 0;
703 for(int c = 0; c < 3; c++)
704 if(valid[i * 4 + c] >= 0.5f)
705 {
706 accum += estimate[i * 4 + c];
707 n_valid++;
708 }
709 steer[i] = n_valid ? accum / n_valid : lum_accum[i] / 3.f;
710 }
711 }
712
713 // Per-channel means of the VALID values: the moment packs below are CENTERED on them.
714 // var = E[u^2] - E[u]^2 in float32 on a smooth plane cancels catastrophically (the mean
715 // squared dwarfs the variance, ~4 digits lost) and the fit's cov/var division amplifies
716 // the surviving noise -- measured as a device-dependent slope error growing with depth.
717 // Centering the packs makes the blurred moments carry the (co)variances directly ; the
718 // slopes and R^2 are shift-invariant, and the intercept is unshifted right after the fit.
719 double maccum[3] = { 0.0, 0.0, 0.0 };
720 size_t mcnt[3] = { 0, 0, 0 };
721 HL_PFOR(reduction(+ : maccum[:3], mcnt[:3]))
722 for(size_t i = 0; i < region_pixels; i++)
723 for(int c = 0; c < 3; c++)
724 if(valid[i * 4 + c] >= 0.5f)
725 {
726 maccum[c] += estimate[i * 4 + c];
727 mcnt[c]++;
728 }
729 const float channel_means[3]
730 = { mcnt[0] ? (float)(maccum[0] / mcnt[0]) : 0.f, mcnt[1] ? (float)(maccum[1] / mcnt[1]) : 0.f,
731 mcnt[2] ? (float)(maccum[2] / mcnt[2]) : 0.f };
732
733 // Soft luminance affinity for the FIT WINDOWS: pixels much darker than the blown zone's
734 // plateau contribute ~nothing to the windowed moments, so a window straddling a dark
735 // occluder and the sky fits the SKY's colour-line instead of a poisoned mixture. Content
736 // brighter than ~a third of the plateau keeps full weight, so unoccluded scenes are
737 // untouched by construction.
738 const float cf_binv = (cf_lref > 1e-9f) ? 1.f / (0.35f * cf_lref) : 0.f;
739
740 // The warp rate is 1 + (sigma_s/sigma_r)|grad guide|, so ANY gradient shrinks the window --
741 // including sensor noise and the sky's own smooth glow, which would cost reconstruction strength
742 // everywhere for nothing. Smooth the guide first: noise stops inflating distances while a
743 // silhouette, which is a step orders of magnitude larger, still stops the transport dead.
744 float *const restrict cf_guide_raw = ctx->cg_dir; // scratch, free until the CG solver runs
745 HL_PFOR()
746 for(size_t i = 0; i < region_pixels; i++) cf_guide_raw[i] = lum_accum[i];
747 float cf_guide_smooth = CF_EDGE_GUIDE_SIGMA;
748 { const char *o = getenv("HL_CF_GUIDE"); if(o) cf_guide_smooth = (float)atof(o); }
749 if(cf_guide_smooth > 0.f)
750 _knee_blur(cf_guide_raw, cf_guide, region_w, region_h, cf_guide_smooth);
751 else
752 memcpy(cf_guide, cf_guide_raw, region_pixels * sizeof(float));
753 float cf_sigma_r = CF_EDGE_RANGE * fmaxf(cf_lref, 1e-9f);
754 { const char *o = getenv("HL_CF_RANGE"); if(o) cf_sigma_r = (float)atof(o) * fmaxf(cf_lref, 1e-9f); }
755
756 // broad-anchor mask for the model-quality plane (bounded even where the fit degenerates)
757 uint8_t *const restrict hole2
758 = (uint8_t *)dt_pixelpipe_cache_alloc_align(sizeof(uint8_t) * region_pixels, ctx->pipe);
759
760 // bsc collects the DIFFUSED fit quality (R^2) per channel: the weight of the
761 // high-frequency damping and of the depth-gated self-dome blend below. It is spatially
762 // smooth (windowed moments + harmonic fill), so neither conaccumer introduces a hand-off.
763 HL_PFOR()
764 for(size_t i = 0; i < region_pixels; i++)
765 for(int c = 0; c < 4; c++) guide_score[i * 4 + c] = 0.f;
766
767 // The TEN blurred moment planes of the fit (article step 3: "solved from ten blurred moment
768 // planes ... through the 2x2 normal equations"). Each _region_blur below IS the windowed weighted
769 // sum Sum_y w(y) G_sigma(x-y) (.) : packing the per-pixel product then Gaussian-blurring gives the
770 // windowed moment at x. w(y) = [all three channels valid] * lum_weight (the soft occlusion weight).
771 // Moment 1 of 3 (blur -> prev_scale): the mass count n and the 3 centred means.
772 // joint windowed moments, weight = all three channels valid at the pixel, packed as
773 // prev = [n, wR, wG, wB], s1 = [wRR, wGG, wBB, wRG], s3 = [wRB, wGB, 0, 0]
774 HL_PFOR()
775 for(size_t i = 0; i < region_pixels; i++)
776 {
777 const float lum_weight = (cf_binv > 0.f) ? sqf(fminf(lum_accum[i] * cf_binv, 1.f)) : 1.f;
778 const float weight
779 = (valid[i * 4 + 0] >= 0.5f && valid[i * 4 + 1] >= 0.5f && valid[i * 4 + 2] >= 0.5f) ? lum_weight : 0.f;
780 blur_in[i * 4 + 0] = weight; // Sum w -> n (trusted mass)
781 blur_in[i * 4 + 1] = weight * (estimate[i * 4 + 0] - channel_means[0]); // Sum w*(R-Rbar) -> centred mean of R
782 blur_in[i * 4 + 2] = weight * (estimate[i * 4 + 1] - channel_means[1]); // Sum w*(G-Gbar) -> centred mean of G
783 blur_in[i * 4 + 3] = weight * (estimate[i * 4 + 2] - channel_means[2]); // Sum w*(B-Bbar) -> centred mean of B
784 }
785
786 _region_edge_blur(blur_in, prev_scale, cf_guide, cf_step_x, cf_step_y, region_w, region_h,
787 cf_sigma, cf_sigma_r);
788
789 HL_PFOR()
790 for(size_t i = 0; i < region_pixels; i++)
791 {
792 const float lum_weight = (cf_binv > 0.f) ? sqf(fminf(lum_accum[i] * cf_binv, 1.f)) : 1.f;
793 const float weight
794 = (valid[i * 4 + 0] >= 0.5f && valid[i * 4 + 1] >= 0.5f && valid[i * 4 + 2] >= 0.5f) ? lum_weight : 0.f;
795 // Moment 2 of 3 (blur -> plane1): four of the six centred second moments (products of x-xbar)
796 const float val_r
797 = estimate[i * 4 + 0] - channel_means[0]; // R - Rbar (centred, avoids the E[u^2]-E[u]^2 cancellation)
798 const float val_g = estimate[i * 4 + 1] - channel_means[1]; // G - Gbar
799 const float val_b = estimate[i * 4 + 2] - channel_means[2]; // B - Bbar
800 blur_in[i * 4 + 0] = weight * val_r * val_r; // -> E[(R-Rbar)^2] = Var(R)
801 blur_in[i * 4 + 1] = weight * val_g * val_g; // -> Var(G)
802 blur_in[i * 4 + 2] = weight * val_b * val_b; // -> Var(B)
803 blur_in[i * 4 + 3] = weight * val_r * val_g; // -> Cov(R,G)
804 }
805
806 _region_edge_blur(blur_in, plane1, cf_guide, cf_step_x, cf_step_y, region_w, region_h,
807 cf_sigma, cf_sigma_r);
808
809 HL_PFOR()
810 for(size_t i = 0; i < region_pixels; i++)
811 {
812 const float lum_weight = (cf_binv > 0.f) ? sqf(fminf(lum_accum[i] * cf_binv, 1.f)) : 1.f;
813 const float weight
814 = (valid[i * 4 + 0] >= 0.5f && valid[i * 4 + 1] >= 0.5f && valid[i * 4 + 2] >= 0.5f) ? lum_weight : 0.f;
815 // Moment 3 of 3 (blur -> plane3): the last two centred second moments + the unweighted mass
816 blur_in[i * 4 + 0] = weight * (estimate[i * 4 + 0] - channel_means[0])
817 * (estimate[i * 4 + 2] - channel_means[2]); // -> Cov(R,B)
818 blur_in[i * 4 + 1] = weight * (estimate[i * 4 + 1] - channel_means[1])
819 * (estimate[i * 4 + 2] - channel_means[2]); // -> Cov(G,B)
820 blur_in[i * 4 + 2] = (valid[i * 4 + 0] >= 0.5f && valid[i * 4 + 1] >= 0.5f && valid[i * 4 + 2] >= 0.5f)
821 ? 1.f
822 : 0.f; // UNWEIGHTED valid mass: anchors must exist at the rim
823 blur_in[i * 4 + 3] = 0.f;
824 }
825
826 _region_edge_blur(blur_in, plane3, cf_guide, cf_step_x, cf_step_y, region_w, region_h,
827 cf_sigma, cf_sigma_r);
828
829 // second-moment plane lookup: diag (c,c) -> s1 slot c; off-diag (a,b) -> slot 2+a+b,
830 // where slots 3 = RG (s1), 4 = RB (s3[0]), 5 = GB (s3[1])
831 // CF_M2(i, a, b) returns the (unnormalized) windowed sum Sum w*(u_a-ubar_a)(u_b-ubar_b) at pixel i,
832 // i.e. n*Cov(u_a,u_b) once divided by the mass n -- the raw material of the normal matrix Sigma.
833#define CF_M2(nb_index, coef_a, coef_b) \
834 (((coef_a) == (coef_b)) ? plane1[(nb_index) * 4 + (coef_a)] \
835 : ((2 + (coef_a) + (coef_b)) < 4 ? plane1[(nb_index) * 4 + 2 + (coef_a) + (coef_b)] \
836 : plane3[(nb_index) * 4 + (coef_a) + (coef_b) - 2]))
837
838 // The DEEP channel (most clipped pixels: its zone contains the multi-clip cores) is not
839 // evaluated here -- its diffused coefficients are STASHED and evaluated after the pair
840 // fallbacks have reconstructed the other clipped channels, so its joint model reads
841 // CONTINUOUS guides everywhere. Evaluating it against a guide that jumps from measured
842 // to clip-plateau at the guide's own clip contour printed that contour as an arc.
843 size_t nclip_c[3] = { 0, 0, 0 };
844 for(size_t i = 0; i < region_pixels; i++)
845 for(int c = 0; c < 3; c++)
846 if(valid[i * 4 + c] < 0.5f) nclip_c[c]++;
847
848 // cdeep = the channel with the most clipped pixels (its zone holds the multi-clip cores)
849 const int cdeep
850 = (nclip_c[0] >= nclip_c[1] && nclip_c[0] >= nclip_c[2]) ? 0 : ((nclip_c[1] >= nclip_c[2]) ? 1 : 2);
851 int deep_stashed = 0;
852
853 // ---- per channel: joint 2-guide coefficients, harmonic diffusion, evaluation ----
854 for(int c = 0; c < 3; c++)
855 {
856 // guide-pair selection: predict clipped channel v = c from its two OTHER channels u1=guide1, u2=guide2
857 const int guide1 = (c == 0) ? 1 : 0;
858 const int guide2 = (c == 2) ? 1 : 2;
859
860 size_t ntarget = 0;
861 HL_PFOR(reduction(+ : ntarget))
862 for(size_t i = 0; i < region_pixels; i++)
863 if(valid[i * 4 + c] < 0.5f && (valid[i * 4 + guide1] >= 0.5f || valid[i * 4 + guide2] >= 0.5f)) ntarget++;
864
865 if(ntarget == 0) continue;
866
867 // NOTE the ntarget gate accepts ONE surviving guide so the DEEP channel is fitted
868 // (and stashed) even when its zone has no strict two-guide pixel; the immediate
869 // evaluation below stays strict.
870
871 // coefficients (a, b, d) from the windowed moments at every pixel (garbage where the
872 // window held no trusted mass -- replaced by the diffusion); anchor = trusted window
873 // Solve the 2x2 normal equations of the weighted least squares at every pixel (article step 3):
874 // Sigma [a;b] = [Cov(u1,v); Cov(u2,v)], Sigma = [[Var u1, Cov(u1,u2)],[Cov(u1,u2), Var u2]]
875 // via Cramer's rule, with relative Tikhonov ridge lambda added to the diagonal.
876 HL_PFOR()
877 for(size_t i = 0; i < region_pixels; i++)
878 {
879 const float norm = fmaxf(prev_scale[i * 4 + 0], 1e-9f); // n = windowed trusted mass
880 const float inv_det = 1.f / norm; // 1/n, turns the summed moments into expectations
881 const float mean1 = prev_scale[i * 4 + 1 + guide1] * inv_det; // E[u1] (of the centred pack)
882 const float mean2 = prev_scale[i * 4 + 1 + guide2] * inv_det; // E[u2]
883 const float mean_target = prev_scale[i * 4 + 1 + c] * inv_det; // E[v]
884 const float var11
885 = fmaxf(CF_M2(i, guide1, guide1) * inv_det - mean1 * mean1, 0.f); // Var(u1) = E[u1^2]-E[u1]^2
886 const float var22 = fmaxf(CF_M2(i, guide2, guide2) * inv_det - mean2 * mean2, 0.f); // Var(u2)
887 const float var12 = CF_M2(i, guide1, guide2) * inv_det - mean1 * mean2; // Cov(u1,u2)
888 const float cov_tg1 = CF_M2(i, c, guide1) * inv_det - mean_target * mean1; // Cov(v,u1) = RHS_1
889 const float cov_tg2 = CF_M2(i, c, guide2) * inv_det - mean_target * mean2; // Cov(v,u2) = RHS_2
890
891 const float var_target
892 = fmaxf(CF_M2(i, c, c) * inv_det - mean_target * mean_target, 0.f); // Var(v), denom of R^2
893
894 // relative Tikhonov: scales with the signal, never eats a weak-but-real slope
895 const float lambda = 1e-3f * 0.5f * (var11 + var22) + 1e-12f; // ridge = 1e-3 * (Var u1 + Var u2)/2
896 const float determinant
897 = fmaxf((var11 + lambda) * (var22 + lambda) - var12 * var12, 1e-18f); // det Sigma (with ridge)
898 const float slope_a
899 = ((var22 + lambda) * cov_tg1 - var12 * cov_tg2) / determinant; // a = (Sigma^-1 RHS)_1 (Cramer)
900 const float slope_b
901 = ((var11 + lambda) * cov_tg2 - var12 * cov_tg1) / determinant; // b = (Sigma^-1 RHS)_2 (Cramer)
902 const float r_sq = CLAMP((slope_a * cov_tg1 + slope_b * cov_tg2) / (var_target + 1e-12f), 0.f,
903 1.f); // R^2 = (a Cov(v,u1)+b Cov(v,u2)) / Var(v) = explained/total
904
905 valid_variance[i * 4 + 0] = slope_a;
906 valid_variance[i * 4 + 1] = slope_b;
907 // intercept of the CENTERED fit, unshifted back to absolute values: d = E[v] - a E[u1] - b E[u2]
908 valid_variance[i * 4 + 2] = (mean_target + channel_means[c]) - slope_a * (mean1 + channel_means[guide1])
909 - slope_b * (mean2 + channel_means[guide2]);
910 valid_variance[i * 4 + 3] = r_sq;
911
912 // anchor = trusted window AND a sane fit: degenerate (near-zero-variance) windows
913 // produce exploding slopes that would poison the diffusion boundary
914 // anchors EXIST wherever enough valid pixels are in reach (continuity at the rim
915 // needs locally-exact fits there), and their weighted fits are bright-content-pure;
916 // windows that are MOSTLY dark (weighted mass a small fraction of the valid mass)
917 // describe unrelated content and must not anchor
918 const int mass_ok = (plane3[i * 4 + 2] > cf_fmin && prev_scale[i * 4 + 0] > 0.25f * plane3[i * 4 + 2]);
919 // anchor gate (article: R^2 > 0.25 with bounded slopes) -> the Dirichlet data for E_transport.
920 // hole = NOT an anchor (the cell to be filled by the transport); |a|,|b| < 64 rejects only
921 // degenerate near-zero-variance windows whose exploding slopes would poison the fill boundary.
922 hole[i] = !(mass_ok && valid[i * 4 + c] >= 0.5f && r_sq > 0.25f && fabsf(slope_a) < 64.f
923 && fabsf(slope_b) < 64.f);
924 if(hole2)
925 hole2[i] = !(mass_ok && valid[i * 4 + c] >= 0.5f); // broader (mass-only) anchor set for the R^2 plane
926 }
927
928 // harmonic diffusion of each coefficient field into the non-anchor area (stable
929 // coarse-to-fine Jacobi fill; base grid at ~sigma/4 since coefficients are smooth).
930 // a/b/d share the anchor mask, so they ride ONE fused fill (one mask pyramid, one
931 // tensor, one sweep pass); r2 may use its own broader mask and fills alone.
932 {
933 HL_PFOR()
934 for(size_t i = 0; i < region_pixels; i++)
935 {
936 fill_planes[i] = valid_variance[i * 4 + 0];
937 fill_planes[region_pixels + i] = valid_variance[i * 4 + 1];
938 fill_planes[2 * region_pixels + i] = valid_variance[i * 4 + 2];
939 solver_field[i] = valid_variance[i * 4 + 3];
940 }
941
942 // E_transport on p in {a, b, d}: anchored anisotropic fill, base grid pitch ~sigma/4 (article "Cell")
943 float *planes[3] = { fill_planes, fill_planes + region_pixels, fill_planes + 2 * region_pixels };
944 _cf_harmonic_fill_n((float *const restrict *)planes, 3, hole, region_w, region_h, (int)(cf_sigma / 4.f),
945 steer, pipe);
946 // the R^2 plane is diffused too (article: "R^2 is diffused alongside (a,b,d) as a fourth plane"),
947 // on the broader mass-only anchor set so it stays bounded even where the fit degenerates
948 _cf_harmonic_fill(solver_field, hole2 ? hole2 : hole, region_w, region_h, (int)(cf_sigma / 4.f), steer, pipe);
949
950 HL_PFOR()
951 for(size_t i = 0; i < region_pixels; i++)
952 {
953 valid_variance[i * 4 + 0] = fill_planes[i];
954 valid_variance[i * 4 + 1] = fill_planes[region_pixels + i];
955 valid_variance[i * 4 + 2] = fill_planes[2 * region_pixels + i];
956 valid_variance[i * 4 + 3] = solver_field[i];
957 }
958 }
959
960 // evaluate against the measured guides at every joint target pixel, keeping the
961 // diffused fit R^2 (in-sample R^2 is the honest quality signal: decorrelated content
962 // simply has no colour-line and scores 0.25..0.6 against ~0.9 for correlated content)
963 if(c == cdeep)
964 {
965 // stash the diffused fields (dbuf/tbuf/ldb/bsc slot 3 are free until the HF and
966 // dome stages); evaluated after the pair fallbacks below
967 HL_PFOR()
968 for(size_t i = 0; i < region_pixels; i++)
969 {
970 reaction_weight[i] = valid_variance[i * 4 + 0];
971 flat_target[i] = valid_variance[i * 4 + 1];
972 dome_lum[i] = valid_variance[i * 4 + 2];
973 guide_score[i * 4 + 3] = valid_variance[i * 4 + 3];
974 }
975 deep_stashed = 1;
976 continue;
977 }
978
979 // strict two-guide gate: extending this evaluation into the multi-clip band (with the
980 // clipped guide at its plateau) was tried and regressed the correlated synthetics --
981 // continuity there is the deep channel's deferred evaluation's job, and the non-deep
982 // channels' pair fits are locally anchored at their own fences anyway
983 HL_PFOR()
984 for(size_t i = 0; i < region_pixels; i++)
985 if(valid[i * 4 + c] < 0.5f && valid[i * 4 + guide1] >= 0.5f && valid[i * 4 + guide2] >= 0.5f)
986 {
987 // evaluation v_hat = a*u1 + b*u2 + d against the MEASURED guides (diffused a,b,d; true u1,u2)
988 estimate[i * 4 + c] = valid_variance[i * 4 + 0] * estimate[i * 4 + guide1]
989 + valid_variance[i * 4 + 1] * estimate[i * 4 + guide2] + valid_variance[i * 4 + 2];
990 guide_score[i * 4 + c]
991 = CLAMP(valid_variance[i * 4 + 3], 0.f, 1.f); // carry the diffused R^2 as the model quality
992 }
993 }
994
995#undef CF_M2
996
997 // ---- single-guide fallback for 2-clip pixels (target + one other channel clipped) ----
998 // Article step 3: "Pixels with a single surviving guide get the same treatment with a one-guide
999 // fit." Same fit+transport+evaluate, but the model collapses to v_hat = a*u + d (one guide u):
1000 // a = Cov(u,v)/Var(u) (1x1 normal equation), R^2 = Cov(u,v)^2 / (Var(u) Var(v)) = squared correlation.
1001 size_t n2clip = 0;
1002 HL_PFOR(reduction(+ : n2clip))
1003 for(size_t i = 0; i < region_pixels; i++)
1004 {
1005 const int n_valid = (valid[i * 4 + 0] >= 0.5f) + (valid[i * 4 + 1] >= 0.5f) + (valid[i * 4 + 2] >= 0.5f);
1006 if(n_valid == 1) n2clip++;
1007 }
1008
1009 if(n2clip > 0)
1010 for(int chan_a = 0; chan_a < 3; chan_a++)
1011 for(int chan_b = chan_a + 1; chan_b < 3; chan_b++)
1012 {
1013 // pair moments, weight = both channels of the pair valid, packed as
1014 // s2 = [n, wa, wb, waa], s3 = [wbb, wab, unweighted n, 0]
1015 HL_PFOR()
1016 for(size_t i = 0; i < region_pixels; i++)
1017 {
1018 const float lum_weight = (cf_binv > 0.f) ? sqf(fminf(lum_accum[i] * cf_binv, 1.f)) : 1.f;
1019 const float weight = (valid[i * 4 + chan_a] >= 0.5f && valid[i * 4 + chan_b] >= 0.5f) ? lum_weight : 0.f;
1020 const float var_a = estimate[i * 4 + chan_a] - channel_means[chan_a];
1021 const float var_b = estimate[i * 4 + chan_b] - channel_means[chan_b];
1022 blur_in[i * 4 + 0] = weight;
1023 blur_in[i * 4 + 1] = weight * var_a;
1024 blur_in[i * 4 + 2] = weight * var_b;
1025 blur_in[i * 4 + 3] = weight * var_a * var_a;
1026 }
1027
1028 _region_blur(blur_in, plane2, region_w, region_h, cf_sigma);
1029
1030 HL_PFOR()
1031 for(size_t i = 0; i < region_pixels; i++)
1032 {
1033 const float lum_weight = (cf_binv > 0.f) ? sqf(fminf(lum_accum[i] * cf_binv, 1.f)) : 1.f;
1034 const float weight = (valid[i * 4 + chan_a] >= 0.5f && valid[i * 4 + chan_b] >= 0.5f) ? lum_weight : 0.f;
1035 const float var_a = estimate[i * 4 + chan_a] - channel_means[chan_a];
1036 const float var_b = estimate[i * 4 + chan_b] - channel_means[chan_b];
1037 blur_in[i * 4 + 0] = weight * var_b * var_b;
1038 blur_in[i * 4 + 1] = weight * var_a * var_b;
1039 blur_in[i * 4 + 2] = (valid[i * 4 + chan_a] >= 0.5f && valid[i * 4 + chan_b] >= 0.5f) ? 1.f : 0.f;
1040 blur_in[i * 4 + 3] = 0.f;
1041 }
1042
1043 _region_blur(blur_in, plane3, region_w, region_h, cf_sigma);
1044
1045 // both orientations: predict a from b, then b from a
1046 for(int orient = 0; orient < 2; orient++)
1047 {
1048 const int target_chan = orient ? chan_b : chan_a; // target channel
1049 const int guide_chan = orient ? chan_a : chan_b; // guide channel
1050 const int other_chan = 3 - chan_a - chan_b; // the third channel, must be clipped at the target
1051
1052 size_t ntarget = 0;
1053 HL_PFOR(reduction(+ : ntarget))
1054 for(size_t i = 0; i < region_pixels; i++)
1055 if(valid[i * 4 + target_chan] < 0.5f && valid[i * 4 + guide_chan] >= 0.5f
1056 && valid[i * 4 + other_chan] < 0.5f)
1057 ntarget++;
1058
1059 if(ntarget == 0) continue;
1060
1061 HL_PFOR()
1062 for(size_t i = 0; i < region_pixels; i++)
1063 {
1064 const float norm = fmaxf(plane2[i * 4 + 0], 1e-9f);
1065 const float inv_det = 1.f / norm;
1066 const float pair_mean_target = plane2[i * 4 + (orient ? 2 : 1)] * inv_det;
1067 const float mean_guide = plane2[i * 4 + (orient ? 1 : 2)] * inv_det;
1068 const float var_guide
1069 = fmaxf((orient ? plane2[i * 4 + 3] : plane3[i * 4 + 0]) * inv_det - mean_guide * mean_guide,
1070 0.f); // Var(u) (guide)
1071 const float var_t = fmaxf((orient ? plane3[i * 4 + 0] : plane2[i * 4 + 3]) * inv_det
1072 - pair_mean_target * pair_mean_target,
1073 0.f); // Var(v) (target), denom of R^2
1074 const float covariance = plane3[i * 4 + 1] * inv_det - pair_mean_target * mean_guide; // Cov(u,v)
1075 const float slope_a
1076 = covariance / (var_guide * (1.f + 1e-3f) + 1e-12f); // a = Cov(u,v)/Var(u), 1e-3 relative ridge
1077 const float r_sq = CLAMP(covariance * covariance / (var_guide * var_t + 1e-18f), 0.f,
1078 1.f); // R^2 = Cov^2/(Var u Var v)
1079
1080 valid_variance[i * 4 + 0] = slope_a;
1081 // intercept of the CENTERED fit, unshifted back to absolute values: d = E[v] - a E[u]
1082 valid_variance[i * 4 + 1] = (pair_mean_target + channel_means[target_chan])
1083 - slope_a * (mean_guide + channel_means[guide_chan]);
1084 valid_variance[i * 4 + 2] = r_sq;
1085 const int mass_ok = (plane3[i * 4 + 2] > cf_fmin && plane2[i * 4 + 0] > 0.25f * plane3[i * 4 + 2]);
1086 hole[i] = !(mass_ok && valid[i * 4 + target_chan] >= 0.5f && r_sq > 0.25f && fabsf(slope_a) < 64.f);
1087 if(hole2) hole2[i] = !(mass_ok && valid[i * 4 + target_chan] >= 0.5f);
1088 }
1089
1090 // slope and intercept share the anchor mask -> one fused fill; r2 may use its
1091 // own broader mask and fills alone
1092 {
1093 HL_PFOR()
1094 for(size_t i = 0; i < region_pixels; i++)
1095 {
1096 fill_planes[i] = valid_variance[i * 4 + 0];
1097 fill_planes[region_pixels + i] = valid_variance[i * 4 + 1];
1098 solver_field[i] = valid_variance[i * 4 + 2];
1099 }
1100
1101 float *planes[2] = { fill_planes, fill_planes + region_pixels };
1102 _cf_harmonic_fill_n((float *const restrict *)planes, 2, hole, region_w, region_h,
1103 (int)(cf_sigma / 4.f), steer, pipe);
1104 _cf_harmonic_fill(solver_field, hole2 ? hole2 : hole, region_w, region_h, (int)(cf_sigma / 4.f), steer,
1105 pipe);
1106
1107 HL_PFOR()
1108 for(size_t i = 0; i < region_pixels; i++)
1109 {
1110 valid_variance[i * 4 + 0] = fill_planes[i];
1111 valid_variance[i * 4 + 1] = fill_planes[region_pixels + i];
1112 valid_variance[i * 4 + 2] = solver_field[i];
1113 }
1114 }
1115
1116 // FEATHERED hand-off: instead of switching hard to the pair model exactly where
1117 // the third channel clips (its contour prints the joint/pair disagreement as an
1118 // arc), blend by the blurred oc-clip mask -- ~0 far into the joint region, ~1
1119 // deep into the multi-clip band, ~0.5 at the contour where BOTH estimates are
1120 // continuous extrapolations. est currently holds the extended joint estimate.
1121 // (A sharper ramp was tried and regressed the outer-contour smoothness.)
1122 // hard write at the multi-clip pixels (the iter-3 semantics). For the deep
1123 // channel this is only the DEEP-CORE estimate: the deferred stashed-joint
1124 // evaluation below owns the fence and blends this back in by depth. For the
1125 // other channels the pair fit is locally anchored at their fence (both its
1126 // channels are measured in the adjacent band), so the hard write is already
1127 // continuous there. A feathered joint-ext blend over this write was tried and
1128 // regressed the correlated synthetics without helping the arc.
1129 HL_PFOR()
1130 for(size_t i = 0; i < region_pixels; i++)
1131 if(valid[i * 4 + target_chan] < 0.5f && valid[i * 4 + guide_chan] >= 0.5f
1132 && valid[i * 4 + other_chan] < 0.5f)
1133 {
1134 // evaluation v_hat = a*u + d against the measured guide (diffused a,d; true u)
1135 estimate[i * 4 + target_chan]
1136 = valid_variance[i * 4 + 0] * estimate[i * 4 + guide_chan] + valid_variance[i * 4 + 1];
1137 guide_score[i * 4 + target_chan] = CLAMP(valid_variance[i * 4 + 2], 0.f, 1.f);
1138 }
1139 }
1140 }
1141
1142 // ---- deep-channel evaluation from the stashed joint model ----
1143 // Runs after the pair fallbacks so a clipped guide reads as its RECONSTRUCTION (itself
1144 // continuous: the pair fit of a less-clipped channel is anchored in the adjacent band
1145 // where both its channels are measured). Smooth coefficient fields x continuous guides
1146 // = no estimator hand-off anywhere inside the deep channel's zone -- the arc the hard
1147 // joint <-> pair switch used to print at the second guide's clip contour cannot form.
1148 // DEPTH SPLIT: the chained evaluation is only NEEDED near the multi-clip fence; deep
1149 // inside the core the direct pair colour-line is the better estimator on correlated
1150 // content (one hop, no compounded reconstruction error). Blend pair over stashed-joint
1151 // by a smoothstep of the blurred multi-clip mask: ~0 at the fence (mask ~0.5 there),
1152 // ~1 deep inside. Smooth weight x smooth fields = still no printable level set.
1153 if(deep_stashed)
1154 {
1155 const int guide1 = (cdeep == 0) ? 1 : 0;
1156 const int guide2 = (cdeep == 2) ? 1 : 2;
1157
1158 HL_PFOR()
1159 for(size_t i = 0; i < region_pixels; i++)
1160 {
1161 blur_in[i * 4 + 0]
1162 = (valid[i * 4 + cdeep] < 0.5f && (valid[i * 4 + guide1] < 0.5f || valid[i * 4 + guide2] < 0.5f)) ? 1.f
1163 : 0.f;
1164 blur_in[i * 4 + 1] = blur_in[i * 4 + 2] = blur_in[i * 4 + 3] = 0.f;
1165 }
1166
1167 _region_blur(blur_in, plane2, region_w, region_h,
1168 cf_sigma); // s2 is free scratch here (pair moments are done)
1169
1170 HL_PFOR()
1171 for(size_t i = 0; i < region_pixels; i++)
1172 {
1173 const int anyvalid = (valid[i * 4 + 0] >= 0.5f) || (valid[i * 4 + 1] >= 0.5f) || (valid[i * 4 + 2] >= 0.5f);
1174 if(valid[i * 4 + cdeep] < 0.5f && anyvalid)
1175 {
1176 // deferred evaluation of the stashed deep-channel joint model: v_hat = a*u1 + b*u2 + d
1177 // (a=reaction_weight, b=flat_target, d=dome_lum are the stashed diffused coefficients)
1178 const float joint = reaction_weight[i] * estimate[i * 4 + guide1]
1179 + flat_target[i] * estimate[i * 4 + guide2] + dome_lum[i];
1180 // pair values exist only at multi-clip px (the pair loop's write gate)
1181 const int has_pair = (valid[i * 4 + guide1] < 0.5f || valid[i * 4 + guide2] < 0.5f);
1182 const float pair_conf = CLAMP(plane2[i * 4 + 0], 0.f, 1.f);
1183 const float smooth_t = CLAMP((pair_conf - 0.7f) / 0.25f, 0.f, 1.f);
1184 const float floor_width = has_pair ? smooth_t * smooth_t * (3.f - 2.f * smooth_t) : 0.f;
1185 estimate[i * 4 + cdeep] = floor_width * estimate[i * 4 + cdeep] + (1.f - floor_width) * joint;
1186 guide_score[i * 4 + cdeep] = floor_width * guide_score[i * 4 + cdeep]
1187 + (1.f - floor_width) * CLAMP(guide_score[i * 4 + 3], 0.f, 1.f);
1188 }
1189 }
1190 }
1191
1192 // MATHS BRIDGE -- Step 4 (HF refit), article §"Hybrid Laplacian-band guiding of the high
1193 // frequencies" / §"Rebuild the high frequencies": the estimate is split at sigma/4 into a low band
1194 // ubar (plane2 below) and a detail band u - ubar. The detail band gets its OWN windowed colour-line
1195 // with R^2-shrunk gains (on a zero-mean band shrinkage is the correct estimator: no magnitude to
1196 // lose, only noise to not print), and the HF is blended between this guided resynthesis
1197 // h_g = a(u_g1-ubar_g1)+b(u_g2-ubar_g2) and the R^2-damped transfer h_d = R^2 (u_c - ubar_c) by
1198 // quadratic min-energy odds w = e_d^2/(e_d^2 + e_g^2), e_{d,g} = blurred |HF_{d,g}| -- an edge
1199 // misfire spikes the guided HF energy e_g, so w -> 0 and the damped path wins exactly there (the
1200 // failure self-detects, no content discriminator needed). Note the band split blurs at sigma/4
1201 // (floored at 2 px) while the moments below blur at the fit's cf_sigma -- two deliberate scales.
1202 //
1203 // R^2-scaled HIGH-FREQUENCY damping: where the colour-line is weak, the guides' fine
1204 // texture is unrelated to the truth and must not be printed onto the reconstruction.
1205 // Continuous in the quality weight -- no estimator hand-off.
1206 memcpy(blur_in, estimate, region_pixels * 4 * sizeof(float));
1207 _region_blur(blur_in, plane2, region_w, region_h,
1208 fmaxf(cf_sigma / 4.f, 2.f)); // ubar = low band, Gaussian at sigma/4 (>= 2 px)
1209
1210 // ---- Laplacian-band guiding (see the DT_HL_HF_GUIDE macro comment) ----
1211 // detail-band moments, weight = all three channels valid; packed exactly like the
1212 // full-signal moments: prev = [n, hR, hG, hB], s1 = [hRR, hGG, hBB, hRG], s3 = [hRB, hGB]
1213 HL_PFOR()
1214 for(size_t i = 0; i < region_pixels; i++)
1215 {
1216 const float lum_weight = (cf_binv > 0.f) ? sqf(fminf(lum_accum[i] * cf_binv, 1.f)) : 1.f;
1217 const float weight
1218 = (valid[i * 4 + 0] >= 0.5f && valid[i * 4 + 1] >= 0.5f && valid[i * 4 + 2] >= 0.5f) ? lum_weight : 0.f;
1219 // detail band H = est - ubar (plane2), weighted; packed like the CF moments = [n, hR, hG, hB]
1220 blur_in[i * 4 + 0] = weight;
1221 blur_in[i * 4 + 1] = weight * (estimate[i * 4 + 0] - plane2[i * 4 + 0]);
1222 blur_in[i * 4 + 2] = weight * (estimate[i * 4 + 1] - plane2[i * 4 + 1]);
1223 blur_in[i * 4 + 3] = weight * (estimate[i * 4 + 2] - plane2[i * 4 + 2]);
1224 }
1225
1226 _region_blur(blur_in, prev_scale, region_w, region_h,
1227 cf_sigma); // windowed means of the detail band (blur at fit sigma)
1228
1229 HL_PFOR()
1230 for(size_t i = 0; i < region_pixels; i++)
1231 {
1232 const float weight = blur_in[i * 4 + 0];
1233 const float hf_r = estimate[i * 4 + 0] - plane2[i * 4 + 0];
1234 const float hf_g = estimate[i * 4 + 1] - plane2[i * 4 + 1];
1235 const float hf_b = estimate[i * 4 + 2] - plane2[i * 4 + 2];
1236 blur_in[i * 4 + 0] = weight * hf_r * hf_r;
1237 blur_in[i * 4 + 1] = weight * hf_g * hf_g;
1238 blur_in[i * 4 + 2] = weight * hf_b * hf_b;
1239 blur_in[i * 4 + 3] = weight * hf_r * hf_g;
1240 }
1241
1242 _region_blur(blur_in, plane1, region_w, region_h, cf_sigma);
1243
1244 HL_PFOR()
1245 for(size_t i = 0; i < region_pixels; i++)
1246 {
1247 const float lum_weight = (cf_binv > 0.f) ? sqf(fminf(lum_accum[i] * cf_binv, 1.f)) : 1.f;
1248 const float weight
1249 = (valid[i * 4 + 0] >= 0.5f && valid[i * 4 + 1] >= 0.5f && valid[i * 4 + 2] >= 0.5f) ? lum_weight : 0.f;
1250 blur_in[i * 4 + 0]
1251 = weight * (estimate[i * 4 + 0] - plane2[i * 4 + 0]) * (estimate[i * 4 + 2] - plane2[i * 4 + 2]);
1252 blur_in[i * 4 + 1]
1253 = weight * (estimate[i * 4 + 1] - plane2[i * 4 + 1]) * (estimate[i * 4 + 2] - plane2[i * 4 + 2]);
1254 blur_in[i * 4 + 2] = (valid[i * 4 + 0] >= 0.5f && valid[i * 4 + 1] >= 0.5f && valid[i * 4 + 2] >= 0.5f)
1255 ? 1.f
1256 : 0.f; // unweighted valid mass for the anchor gate
1257 blur_in[i * 4 + 3] = 0.f;
1258 }
1259
1260 _region_blur(blur_in, plane3, region_w, region_h, cf_sigma);
1261
1262 // HF_M2(i, a, b) returns the windowed sum Sum w * H_a * H_b at pixel i (H = detail band, already
1263 // zero-mean, so no centering needed unlike CF_M2), indexing the packed second-moment planes:
1264 // diag (a==b) in plane1[0..2], RG/RB in plane1[3]/plane3[0], GB in plane3[1] -- feeds Var/Cov below
1265#define HF_M2(nb_index, coef_a, coef_b) \
1266 (((coef_a) == (coef_b)) ? plane1[(nb_index) * 4 + (coef_a)] \
1267 : ((2 + (coef_a) + (coef_b)) < 4 ? plane1[(nb_index) * 4 + 2 + (coef_a) + (coef_b)] \
1268 : plane3[(nb_index) * 4 + (coef_a) + (coef_b) - 2]))
1269
1270 for(int c = 0; c < 3; c++)
1271 {
1272 const int guide1 = (c == 0) ? 1 : 0;
1273 const int guide2 = (c == 2) ? 1 : 2;
1274
1275 size_t ntarget = 0;
1276 HL_PFOR(reduction(+ : ntarget))
1277 for(size_t i = 0; i < region_pixels; i++)
1278 if(valid[i * 4 + c] < 0.5f && valid[i * 4 + guide1] >= 0.5f && valid[i * 4 + guide2] >= 0.5f) ntarget++;
1279
1280 if(ntarget == 0) continue;
1281
1282 // R^2-shrunk detail-band gains at every pixel; anchors = trusted mass + bounded slopes
1283 HL_PFOR()
1284 for(size_t i = 0; i < region_pixels; i++)
1285 {
1286 const float norm = fmaxf(prev_scale[i * 4 + 0], 1e-9f);
1287 const float inv_det = 1.f / norm;
1288 const float mean1 = prev_scale[i * 4 + 1 + guide1] * inv_det;
1289 const float mean2 = prev_scale[i * 4 + 1 + guide2] * inv_det;
1290 const float mean_target = prev_scale[i * 4 + 1 + c] * inv_det;
1291 // same 2x2 normal equations as the CF fit, but on the detail-band moments (article step 4):
1292 // solve Sigma [a;b] = [Cov(H_u1,H_c); Cov(H_u2,H_c)] by Cramer's rule. u1=guide1, u2=guide2, v=c.
1293 const float var11 = fmaxf(HF_M2(i, guide1, guide1) * inv_det - mean1 * mean1, 0.f); // Var(H_u1)
1294 const float var22 = fmaxf(HF_M2(i, guide2, guide2) * inv_det - mean2 * mean2, 0.f); // Var(H_u2)
1295 const float var12 = HF_M2(i, guide1, guide2) * inv_det - mean1 * mean2; // Cov(H_u1,H_u2)
1296 const float cov_tg1 = HF_M2(i, c, guide1) * inv_det - mean_target * mean1; // Cov(H_v,H_u1) = RHS_1
1297 const float cov_tg2 = HF_M2(i, c, guide2) * inv_det - mean_target * mean2; // Cov(H_v,H_u2) = RHS_2
1298 const float var_target
1299 = fmaxf(HF_M2(i, c, c) * inv_det - mean_target * mean_target, 0.f); // Var(H_v), denom of R^2
1300
1301 const float lambda = 1e-3f * 0.5f * (var11 + var22) + 1e-12f; // relative Tikhonov ridge
1302 const float determinant = fmaxf((var11 + lambda) * (var22 + lambda) - var12 * var12, 1e-18f); // det Sigma
1303 const float hf_a = ((var22 + lambda) * cov_tg1 - var12 * cov_tg2) / determinant; // a (Cramer)
1304 const float hf_b_slope = ((var11 + lambda) * cov_tg2 - var12 * cov_tg1) / determinant; // b (Cramer)
1305 const float hf_r2 = CLAMP((hf_a * cov_tg1 + hf_b_slope * cov_tg2) / (var_target + 1e-12f), 0.f, 1.f); // R^2
1306
1307 // R^2-shrunk gains g*R^2 (correct estimator on a zero-mean band): stashed for the diffusion below
1308 reaction_weight[i] = hf_a * hf_r2;
1309 flat_target[i] = hf_b_slope * hf_r2;
1310 hole[i] = !(plane3[i * 4 + 2] > cf_fmin && prev_scale[i * 4 + 0] > 0.25f * plane3[i * 4 + 2]
1311 && valid[i * 4 + c] >= 0.5f && fabsf(reaction_weight[i]) < 64.f && fabsf(flat_target[i]) < 64.f);
1312 }
1313
1314 {
1315 // the two HF gain planes share the anchor mask -> one fused fill
1316 float *planes[2] = { reaction_weight, flat_target };
1317 _cf_harmonic_fill_n((float *const restrict *)planes, 2, hole, region_w, region_h, (int)(cf_sigma / 4.f),
1318 steer, pipe);
1319 }
1320
1321 // both HF candidates + their local energies (blurred |.|), packed into varc via one blur
1322 HL_PFOR()
1323 for(size_t i = 0; i < region_pixels; i++)
1324 {
1325 // h_g = a(u_g1-ubar_g1) + b(u_g2-ubar_g2): guide-transferred detail (diffused shrunk gains)
1326 const float hf_guided = reaction_weight[i] * (estimate[i * 4 + guide1] - plane2[i * 4 + guide1])
1327 + flat_target[i] * (estimate[i * 4 + guide2] - plane2[i * 4 + guide2]);
1328 // h_d = R^2 (u_c - ubar_c): the channel's own detail damped by its fit quality
1329 const float hf_damped = CLAMP(guide_score[i * 4 + c], 0.f, 1.f) * (estimate[i * 4 + c] - plane2[i * 4 + c]);
1330 blur_in[i * 4 + 0] = fabsf(hf_guided); // |h_g| -> blurred to e_g
1331 blur_in[i * 4 + 1] = fabsf(hf_damped); // |h_d| -> blurred to e_d
1332 blur_in[i * 4 + 2] = 0.f;
1333 blur_in[i * 4 + 3] = 0.f;
1334 }
1335
1336 _region_blur(blur_in, valid_variance, region_w, region_h, fmaxf(cf_sigma / 4.f, 2.f));
1337
1338 // quadratic min-energy blend of the two HF sources, then resynthesize
1339 HL_PFOR()
1340 for(size_t i = 0; i < region_pixels; i++)
1341 if(valid[i * 4 + c] < 0.5f && valid[i * 4 + guide1] >= 0.5f && valid[i * 4 + guide2] >= 0.5f)
1342 {
1343 const float hf_guided = reaction_weight[i] * (estimate[i * 4 + guide1] - plane2[i * 4 + guide1])
1344 + flat_target[i] * (estimate[i * 4 + guide2] - plane2[i * 4 + guide2]);
1345 const float hf_damped
1346 = CLAMP(guide_score[i * 4 + c], 0.f, 1.f) * (estimate[i * 4 + c] - plane2[i * 4 + c]);
1347 const float energy_g = valid_variance[i * 4 + 0]; // e_g = blurred |h_g|
1348 const float energy_d = valid_variance[i * 4 + 1]; // e_d = blurred |h_d|
1349 // quadratic min-energy odds w = e_d^2/(e_d^2 + e_g^2): favours the LOWER-energy candidate,
1350 // so a guide misfire (spiked e_g) drives w -> 0 and the damped path wins there
1351 const float energy_weight = energy_d * energy_d / fmaxf(energy_d * energy_d + energy_g * energy_g, 1e-18f);
1352 // resynthesis: u_c = ubar_c + w*h_g + (1-w)*h_d
1353 estimate[i * 4 + c] = plane2[i * 4 + c] + energy_weight * hf_guided + (1.f - energy_weight) * hf_damped;
1354 }
1355 }
1356
1357#undef HF_M2
1358
1359 // pixels with a single surviving guide keep the damped treatment
1360 HL_PFOR()
1361 for(size_t i = 0; i < region_pixels; i++)
1362 {
1363 const int n_valid = (valid[i * 4 + 0] >= 0.5f) + (valid[i * 4 + 1] >= 0.5f) + (valid[i * 4 + 2] >= 0.5f);
1364 if(n_valid != 1) continue;
1365 for(int c = 0; c < 3; c++)
1366 if(valid[i * 4 + c] < 0.5f)
1367 {
1368 // no second guide -> no h_g: keep only the R^2-damped own detail u_c = ubar_c + R^2(u_c-ubar_c)
1369 const float hf_weight = CLAMP(guide_score[i * 4 + c], 0.f, 1.f);
1370 estimate[i * 4 + c] = plane2[i * 4 + c] + hf_weight * (estimate[i * 4 + c] - plane2[i * 4 + c]);
1371 }
1372 }
1373
1374 // Step 5 / Soft saturation floor (article §"The algorithm" step 5): a clipped channel is
1375 // physically at least its saturated reading c0, but the hard max(e, c0) prints the
1376 // floor-binding contour as an edge wherever a weak prediction oscillates around saturation;
1377 // round the transition over ~2% of c0 instead. out = 1/2 (e + c0 + sqrt((e-c0)^2 + (0.02 c0)^2)),
1378 // a smooth max: -> e for e >> c0, -> c0 for e << c0, softened over a width 0.02*c0.
1379 //
1380 // JOINT (chromaticity-preserving) variant, blended by the clip-asymmetry gate ctx->floor_gate
1381 // (see _hl_floor_gate in common.h): the independent per-channel floor imprints the CLIP LEVELS'
1382 // chromaticity on multi-clip pixels wherever the fit under-predicts -- with real-camera WB'd
1383 // clips that imprint is the inverse-WB magenta (measured on MAC25640's clipped neons: R snapped
1384 // to its floor, G left low -> G the lowest channel). The joint form lifts the whole clipped
1385 // subset by ONE scalar s = max_k(t_k/e_k) so the most-demanding floor is met with the fit's
1386 // chromaticity preserved; identical to per-channel for 1-clip pixels; lift capped at 8x with the
1387 // per-channel soft floor kept after as the degenerate-estimate safety net. At gate 0 (equal
1388 // clips, the approved unit-WB behavior) the per-channel path runs verbatim.
1389 const float floor_gate = ctx->floor_gate;
1390
1391 // Pass 1: what does the joint form buy this REGION as a whole? (see _hl_region_worth) The choice
1392 // is uniform across the zone because a spatial blend between the two floors prints magenta
1393 // wherever the per-channel side dominates.
1394 float region_worth = 1.f;
1395 ctx->region_worth = 1.f;
1396 if(floor_gate > 1e-6f)
1397 {
1398 float sp0 = 0.f, sp1 = 0.f, sp2 = 0.f, sj0 = 0.f, sj1 = 0.f, sj2 = 0.f;
1399 HL_PFOR(reduction(+ : sp0, sp1, sp2, sj0, sj1, sj2))
1400 for(size_t i = 0; i < region_pixels; i++)
1401 {
1402 const int n_clip
1403 = (valid[i * 4 + 0] < 0.5f) + (valid[i * 4 + 1] < 0.5f) + (valid[i * 4 + 2] < 0.5f);
1404 if(n_clip == 0) continue;
1405 float lift = 1.f;
1406 for(int c = 0; c < 3; c++)
1407 if(valid[i * 4 + c] < 0.5f)
1408 {
1409 const float e = fmaxf(estimate[i * 4 + c], 1e-6f);
1410 const float c0 = clip0[i * 4 + c];
1411 const float dd = e - c0, wd = 0.02f * fmaxf(c0, 1e-6f);
1412 const float tgt = c0 + 0.5f * (dd + sqrtf(dd * dd + wd * wd));
1413 lift = fmaxf(lift, fminf(tgt / e, 8.f));
1414 }
1415 float pc[3], jt[3];
1416 for(int c = 0; c < 3; c++)
1417 {
1418 pc[c] = jt[c] = estimate[i * 4 + c];
1419 if(valid[i * 4 + c] >= 0.5f) continue;
1420 const float c0 = clip0[i * 4 + c], wd = 0.02f * fmaxf(c0, 1e-6f);
1421 const float dd = estimate[i * 4 + c] - c0;
1422 pc[c] = c0 + 0.5f * (dd + sqrtf(dd * dd + wd * wd));
1423 const float lf = fmaxf(estimate[i * 4 + c], 1e-6f) * lift, dj = lf - c0;
1424 jt[c] = c0 + 0.5f * (dj + sqrtf(dj * dj + wd * wd));
1425 }
1426 sp0 += pc[0]; sp1 += pc[1]; sp2 += pc[2];
1427 sj0 += jt[0]; sj1 += jt[1]; sj2 += jt[2];
1428 }
1429 const float tp = fmaxf(sp0 + sp1 + sp2, 1e-9f), tj = fmaxf(sj0 + sj1 + sj2, 1e-9f);
1430 const float benefit = fabsf(sj0 / tj - sp0 / tp) + fabsf(sj1 / tj - sp1 / tp)
1431 + fabsf(sj2 / tj - sp2 / tp);
1432 region_worth = _hl_region_worth(benefit);
1433 ctx->region_worth = region_worth; // every floor site in this region reuses the same decision
1434 // %llu and a cast below, not %zu: dt_print() carries __attribute__((format(printf))),
1435 // which MinGW maps to ms_printf, and that does not implement the C99 length modifier.
1436 // Plain fprintf() gets gnu_printf via __USE_MINGW_ANSI_STDIO and does accept %zu, which
1437 // is why the ones in selftests.c are fine. Same reasoning as develop/pixelpipe_hb.c.
1438 if(getenv("HL_REGION_W"))
1439 dt_print(DT_DEBUG_ALWAYS, "[hl regionw] px=%llu benefit=%.6f region_worth=%.4f\n",
1440 (unsigned long long)region_pixels, benefit, region_worth);
1441 }
1442
1443 HL_PFOR()
1444 for(size_t i = 0; i < region_pixels; i++)
1445 {
1446 float lift = 1.f;
1447 if(floor_gate > 1e-6f)
1448 for(int c = 0; c < 3; c++)
1449 if(valid[i * 4 + c] < 0.5f)
1450 {
1451 const float e = fmaxf(estimate[i * 4 + c], 1e-6f);
1452 const float clip_floor_c = clip0[i * 4 + c];
1453 const float delta = e - clip_floor_c;
1454 const float weight = 0.02f * fmaxf(clip_floor_c, 1e-6f);
1455 const float target = clip_floor_c + 0.5f * (delta + sqrtf(delta * delta + weight * weight));
1456 lift = fmaxf(lift, fminf(target / e, 8.f));
1457 }
1458 // Both candidates, for every channel (valid channels are identical in each), so the decision
1459 // below can compare them as COLOURS instead of channel by channel.
1460 float per_chan_v[3], joint_v[3];
1461 for(int c = 0; c < 3; c++)
1462 {
1463 per_chan_v[c] = joint_v[c] = estimate[i * 4 + c];
1464 if(valid[i * 4 + c] >= 0.5f) continue;
1465 const float clip_floor_c = clip0[i * 4 + c]; // c0, the saturated reading
1466 const float weight = 0.02f * fmaxf(clip_floor_c, 1e-6f); // transition width = 2% of c0
1467 const float delta = estimate[i * 4 + c] - clip_floor_c; // e - c0
1468 // c0 + 1/2 ( (e-c0) + sqrt((e-c0)^2 + width^2) ): the rounded lower bound at c0
1469 per_chan_v[c] = clip_floor_c + 0.5f * (delta + sqrtf(delta * delta + weight * weight));
1470 const float lifted = fmaxf(estimate[i * 4 + c], 1e-6f) * lift;
1471 const float delta_joint = lifted - clip_floor_c;
1472 joint_v[c]
1473 = clip_floor_c + 0.5f * (delta_joint + sqrtf(delta_joint * delta_joint + weight * weight));
1474 }
1475 if(floor_gate <= 1e-6f)
1476 {
1477 for(int c = 0; c < 3; c++)
1478 if(valid[i * 4 + c] < 0.5f) estimate[i * 4 + c] = per_chan_v[c]; // bit-exact approved path
1479 continue;
1480 }
1481
1482 // WHAT THE JOINT FORM IS WORTH, PER PIXEL. The per-channel floor clamps each clipped channel at
1483 // its own c0: that SUPPRESSES the fit's noise (the output is pinned to a measured constant) and
1484 // meets the surround at exactly the level neighbouring valid pixels sit at. The joint form keeps
1485 // the fit -- and with it the fit's noise. Measured cost: +56% grain on MAC25640, +58% on
1486 // PK1_3540, i.e. the same penalty on both. The BENEFIT is what differs: on MAC the joint form
1487 // moves G/((R+B)/2) 0.300 -> 0.635 (2.1x, the magenta rescue it exists for); on PK1's blown sky
1488 // 0.755 -> 0.773 (2.3%) -- the same noise bought for nothing, which is what prints as a flat,
1489 // grainy core badly blended into its surround. So pay the noise only in proportion to the
1490 // chromaticity actually rescued: L1 distance between the two candidates' shares, ramped over one
1491 // just-noticeable hue step. Where the two agree the per-channel floor is kept verbatim, so this
1492 // cannot regress a scene the joint form was not changing anyway.
1493 float sum_p = 0.f, sum_j = 0.f;
1494 for(int c = 0; c < 3; c++)
1495 {
1496 sum_p += per_chan_v[c];
1497 sum_j += joint_v[c];
1498 }
1499 sum_p = fmaxf(sum_p, 1e-9f);
1500 sum_j = fmaxf(sum_j, 1e-9f);
1501 float chroma_gain = 0.f;
1502 for(int c = 0; c < 3; c++) chroma_gain += fabsf(joint_v[c] / sum_j - per_chan_v[c] / sum_p);
1503 // per-pixel gain still modulates WITHIN the region's uniform decision: where the two
1504 // candidates agree the per-channel floor is kept verbatim, which prints nothing either way.
1505 const float w = floor_gate * region_worth * _hl_floor_worth(chroma_gain);
1506 for(int c = 0; c < 3; c++)
1507 if(valid[i * 4 + c] < 0.5f)
1508 estimate[i * 4 + c] = per_chan_v[c] + w * (joint_v[c] - per_chan_v[c]);
1509 }
1510
1511 // Step 6 dome gate (article §"The algorithm" step 6): hand the dome-blend weight to the
1512 // self-dome block (it reads varc as Wc, uses We = Wc^2 as the keep weight). The two factors
1513 // answer two questions: dome fraction = (1 - S_{0.4}^{0.85}(R^2)) * exp(-(delta/1.5 sigma)^2)
1514 // R^2 (guide_score) -> "is the colour-line real here" via a smoothstep S (0 below 0.4,
1515 // 1 above 0.85): low R^2 = DOUBTFUL model -> lean on the dome.
1516 // delta (clip_depth) -> "is the dome trustworthy here" via a gaussian of depth/(1.5 sigma):
1517 // biharmonic extrapolation is excellent near the rim and degrades with distance, so the
1518 // hand-over decays over ~1.5 sigma of depth. Deep interiors always stay on the fit.
1519 // We store Wc = sqrt(keep) with keep = 1 - dome_fraction = 1 - (1 - S(R^2)) * gdep, so the
1520 // self-dome block's conf_weight = Wc^2 = keep is exactly the coefficient-field share.
1521 HL_PFOR()
1522 for(size_t i = 0; i < region_pixels; i++)
1523 for(int c = 0; c < 3; c++)
1524 {
1525 const float dome_t = CLAMP((guide_score[i * 4 + c] - 0.4f) / 0.45f, 0.f, 1.f); // ramp arg (0.4..0.85)
1526 const float we_r2 = dome_t * dome_t * (3.f - 2.f * dome_t); // S_{0.4}^{0.85}(R^2)
1527 const float smooth_t = clip_depth[i] / (1.5f * cf_sigma); // delta / (1.5 sigma)
1528 const float gdep = expf(-smooth_t * smooth_t); // exp(-(delta/1.5 sigma)^2)
1529 valid_variance[i * 4 + c] = sqrtf(CLAMP(1.f - (1.f - we_r2) * gdep, 0.f, 1.f)); // Wc = sqrt(keep)
1530 }
1531
1534}
1535
1536// ============================ OpenCL ============================
1537
1538#ifdef HAVE_OPENCL
1539// GPU counterpart of _cf_harmonic_fill_n: harmonic fill (repeatedly replace each hole pixel by
1540// the average of its four neighbours -- Jacobi iterations -- run coarse-to-fine on shrunken
1541// copies of the grid) of up to 3 planes SHARING ONE anchor mask, executed entirely on device
1542// buffers. The mask pyramid, the tensor and the edge weights depend only on (mask, steer,
1543// geometry), so the planes share one build and the fused Jacobi kernels advance all of them
1544// per launch, reading the weights once per cell.
1545// vals[p] (float, rw*rh) hold the planes to fill; despite its name, `hole` (uchar, rw*rh) must
1546// be the ANCHOR mask (1 = trusted pixel to keep, 0 = hole to fill) -- see the caller-contract
1547// note below. Fills the hole cells of every vals[p] in place. Mirrors _cf_harmonic_fill_n on
1548// the CPU: any change here must be mirrored there and re-validated with the HL_FILLCL_TEST
1549// self-test (_cf_harmonic_fill_cl_selftest).
1550//
1551// MATHS BRIDGE -- article "The algorithm" step 3, the E_transport solver on the GPU: the anchored,
1552// coarse-to-fine anisotropic transport that minimizes E_transport = Sum_p int grad(p)^T D grad(p),
1553// p|anchors = p_fit, by relaxing div(D grad p)=0 (steer != NULL builds D via the hl_cfa_* kernels;
1554// steer == NULL => D = I, the plain harmonic fill). Structure mirrors the CPU _cf_harmonic_fill_n:
1555// base grid at pitch ~sigma/4, pyramid halved until the long side <= 8 cells (convergence from depth,
1556// not sweep count), flat anchor-mean seed at the coarsest level, bilinear seed of each finer level,
1557// 100 Jacobi sweeps per level, then bilinear upsample into the full-res hole pixels.
1558static cl_int _cf_harmonic_fill_cl_n(const int devid, void *gd_void, cl_mem *vals, const int n_planes_in,
1559 cl_mem hole, const int region_w, const int region_h, const int base_ds,
1560 const int mask_is_hole, cl_mem steer)
1561{
1563 const int n_planes = CLAMP(n_planes_in, 1, DT_HL_FILL_CL_MAXP);
1564 const int downsample = CLAMP(base_ds, 1, 8);
1565 const int base_w = (region_w + downsample - 1) / downsample;
1566 const int base_h = (region_h + downsample - 1) / downsample;
1567 const size_t cell_count = (size_t)base_w * base_h;
1568 cl_int cl_err = DT_OPENCL_DEFAULT_ERROR;
1569 const int steered = (steer != NULL);
1570 const float steer_k = DT_HL_CF_K;
1571
1572 cl_mem base_vals[DT_HL_FILL_CL_MAXP] = { NULL };
1573 cl_mem level_vals[DT_HL_FILL_CL_MAXP] = { NULL };
1574 cl_mem level_solution[DT_HL_FILL_CL_MAXP] = { NULL };
1575 cl_mem level_scratch[DT_HL_FILL_CL_MAXP] = { NULL };
1576 cl_mem prev_level_solution[DT_HL_FILL_CL_MAXP] = { NULL };
1577 int alloc_ok = 1;
1578 for(int plane = 0; plane < n_planes; plane++)
1579 {
1580 base_vals[plane] = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1581 level_vals[plane] = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1582 level_solution[plane] = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1583 level_scratch[plane] = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1584 prev_level_solution[plane] = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1585 alloc_ok &= (base_vals[plane] && level_vals[plane] && level_solution[plane] && level_scratch[plane]
1586 && prev_level_solution[plane]);
1587 }
1588 cl_mem base_anchor_mask = dt_opencl_alloc_device_buffer(devid, cell_count);
1589 cl_mem level_anchor_mask = dt_opencl_alloc_device_buffer(devid, cell_count);
1590 // aniso steering planes, only needed when a steering plane was passed in (all sized to the
1591 // base grid). Allocate them in one guarded block and fold their null checks into alloc_ok,
1592 // so the abort decision is taken exactly once below.
1593 cl_mem base_steer = NULL;
1594 cl_mem level_steer = NULL;
1595 cl_mem steer_blur_lin = NULL;
1596 cl_mem steer_blur_quad = NULL;
1597 cl_mem steer_grad_x = NULL;
1598 cl_mem steer_grad_y = NULL;
1599 cl_mem steer_tensor_xx = NULL;
1600 cl_mem steer_tensor_xy = NULL;
1601 cl_mem steer_tensor_yy = NULL;
1602 cl_mem grad_partial_sums = NULL;
1603 cl_mem grad_mean_norm = NULL;
1604 cl_mem neighbour_weights = NULL;
1605 cl_mem neighbour_weights_sum = NULL;
1606 if(steered)
1607 {
1608 base_steer = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1609 level_steer = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1610 steer_blur_lin = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1611 steer_blur_quad = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1612 steer_grad_x = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1613 steer_grad_y = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1614 steer_tensor_xx = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1615 steer_tensor_xy = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1616 steer_tensor_yy = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1617 grad_partial_sums = dt_opencl_alloc_device_buffer(devid, sizeof(float) * 256);
1618 grad_mean_norm = dt_opencl_alloc_device_buffer(devid, sizeof(float));
1619 neighbour_weights = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count * 8);
1620 neighbour_weights_sum = dt_opencl_alloc_device_buffer(devid, sizeof(float) * cell_count);
1621 alloc_ok &= (base_steer && level_steer && steer_blur_lin && steer_blur_quad && steer_grad_x && steer_grad_y
1622 && steer_tensor_xx && steer_tensor_xy && steer_tensor_yy && grad_partial_sums && grad_mean_norm
1623 && neighbour_weights && neighbour_weights_sum)
1624 ? 1
1625 : 0;
1626 }
1627 if(!alloc_ok || !base_anchor_mask || !level_anchor_mask) goto out;
1628
1629 // base grid from full resolution, per plane (base_anchor_mask is identical every time). The caller's mask
1630 // may be in either convention (1 = trusted anchor, or 1 = hole with mask_is_hole set);
1631 // hl_fill_down normalizes iter, and every internal level mask below is in the ANCHOR
1632 // convention regardless.
1633 for(int plane = 0; plane < n_planes; plane++)
1634 {
1635 const int kernel = global_data->kernel_hl_fill_down;
1636 size_t size[3] = { ROUNDUPDWD(base_w, devid), ROUNDUPDHT(base_h, devid), 1 };
1637 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &vals[plane]);
1638 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &hole);
1639 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &base_vals[plane]);
1640 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &base_anchor_mask);
1641 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &region_w);
1642 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &region_h);
1643 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &base_w);
1644 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &base_h);
1645 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &downsample);
1646 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(int), &mask_is_hole);
1647 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
1648 if(cl_err != CL_SUCCESS) goto out;
1649 }
1650
1651 // aniso: steering plane on the base grid (plain block mean)
1652 if(steered)
1653 {
1654 const int kernel = global_data->kernel_hl_cfa_down;
1655 size_t size_level[3] = { ROUNDUPDWD(base_w, devid), ROUNDUPDHT(base_h, devid), 1 };
1656 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &steer);
1657 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &base_steer);
1658 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(int), &region_w);
1659 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &region_h);
1660 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &base_w);
1661 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &base_h);
1662 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &downsample);
1663 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size_level);
1664 if(cl_err != CL_SUCCESS) goto out;
1665 }
1666
1667 // pyramid depth: halve until the LONG side is <= 8 cells (same rationale as the CPU fill:
1668 // the coarsest flat seed must be trivially relaxable within the fixed sweep budget)
1669 int nlev = 1;
1670 while((MAX(base_w, base_h) >> nlev) > 8 && nlev < 12) nlev++;
1671
1672 // coarse-to-fine sweep: solve on the coarsest grid first, then use each solved level to seed
1673 // the next finer one (prev_level_w/prev_level_h remember the previous level's dimensions for the seed upsample)
1674 int prev_level_w = 0;
1675 int prev_level_h = 0;
1676 for(int level = nlev - 1; level >= 0; level--)
1677 {
1678 const int step = 1 << level;
1679 const int level_w = (base_w + step - 1) / step;
1680 const int level_h = (base_h + step - 1) / step;
1681 size_t size[3] = { ROUNDUPDWD(level_w, devid), ROUNDUPDHT(level_h, devid), 1 };
1682
1683 // level grid from the base grid, per plane (level_anchor_mask identical every time)
1684 for(int plane = 0; plane < n_planes; plane++)
1685 {
1686 const int kernel_down = global_data->kernel_hl_fill_down;
1687 dt_opencl_set_kernel_arg(devid, kernel_down, 0, sizeof(cl_mem), &base_vals[plane]);
1688 dt_opencl_set_kernel_arg(devid, kernel_down, 1, sizeof(cl_mem), &base_anchor_mask);
1689 dt_opencl_set_kernel_arg(devid, kernel_down, 2, sizeof(cl_mem), &level_vals[plane]);
1690 dt_opencl_set_kernel_arg(devid, kernel_down, 3, sizeof(cl_mem), &level_anchor_mask);
1691 dt_opencl_set_kernel_arg(devid, kernel_down, 4, sizeof(int), &base_w);
1692 dt_opencl_set_kernel_arg(devid, kernel_down, 5, sizeof(int), &base_h);
1693 dt_opencl_set_kernel_arg(devid, kernel_down, 6, sizeof(int), &level_w);
1694 dt_opencl_set_kernel_arg(devid, kernel_down, 7, sizeof(int), &level_h);
1695 dt_opencl_set_kernel_arg(devid, kernel_down, 8, sizeof(int), &step);
1696 const int level_anchor = 0; // internal level masks are always in the anchor convention
1697 dt_opencl_set_kernel_arg(devid, kernel_down, 9, sizeof(int), &level_anchor);
1698 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel_down, size);
1699 if(cl_err != CL_SUCCESS) goto out;
1700 }
1701
1702 if(level == nlev - 1)
1703 {
1704 // coarsest level: seed every hole cell with the mean of the anchor cells (single workgroup)
1705 for(int plane = 0; plane < n_planes; plane++)
1706 {
1707 const int kernel_seed = global_data->kernel_hl_fill_seed;
1708 const int n_cells = level_w * level_h;
1709 const int local_size = 256;
1710 size_t size_level[3] = { local_size, 1, 1 };
1711 size_t local[3] = { local_size, 1, 1 };
1712 dt_opencl_set_kernel_arg(devid, kernel_seed, 0, sizeof(cl_mem), &level_solution[plane]);
1713 dt_opencl_set_kernel_arg(devid, kernel_seed, 1, sizeof(cl_mem), &level_vals[plane]);
1714 dt_opencl_set_kernel_arg(devid, kernel_seed, 2, sizeof(cl_mem), &level_anchor_mask);
1715 dt_opencl_set_kernel_arg(devid, kernel_seed, 3, sizeof(int), &n_cells);
1716 dt_opencl_set_kernel_arg(devid, kernel_seed, 4, sizeof(float) * local_size, NULL);
1717 dt_opencl_set_kernel_arg(devid, kernel_seed, 5, sizeof(int) * local_size, NULL);
1718 cl_err = dt_opencl_enqueue_kernel_2d_with_local(devid, kernel_seed, size_level, local);
1719 if(cl_err != CL_SUCCESS) goto out;
1720 }
1721 }
1722 else
1723 {
1724 // finer levels: seed hole cells by upsampling the previous (coarser) level's solution
1725 for(int plane = 0; plane < n_planes; plane++)
1726 {
1727 const int kernel_seed_up = global_data->kernel_hl_fill_seed_up;
1728 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 0, sizeof(cl_mem), &level_solution[plane]);
1729 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 1, sizeof(cl_mem), &level_vals[plane]);
1730 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 2, sizeof(cl_mem), &level_anchor_mask);
1731 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 3, sizeof(cl_mem), &prev_level_solution[plane]);
1732 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 4, sizeof(int), &level_w);
1733 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 5, sizeof(int), &level_h);
1734 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 6, sizeof(int), &prev_level_w);
1735 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 7, sizeof(int), &prev_level_h);
1736 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel_seed_up, size);
1737 if(cl_err != CL_SUCCESS) goto out;
1738 }
1739 }
1740
1741 // aniso: build the E_transport steering tensor D at this level (article step 3 D equation):
1742 // level steering plane -> blurred L/L^2 -> gradients (+ mean-magnitude reduction)
1743 // -> Weickert tensor (hl_cfa_tensor = _cf_adaptive_tensor) -> precomputed edge weights.
1744 // Mirrors the CPU per-level build exactly;
1745 // the gnorm reduction is finished on device so the queue never drains mid-fill. Shared by
1746 // all n_planes planes -- fusing them is what amortizes this whole chain.
1747 if(steered)
1748 {
1749 const int n_cells = level_w * level_h;
1750 {
1751 const int kernel = global_data->kernel_hl_cfa_down;
1752 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &base_steer);
1753 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &level_steer);
1754 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(int), &base_w);
1755 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &base_h);
1756 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &level_w);
1757 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &level_h);
1758 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &step);
1759 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
1760 if(cl_err != CL_SUCCESS) goto out;
1761 }
1762 for(int pass = 0; pass < 2; pass++)
1763 {
1764 const int kernel = global_data->kernel_hl_cfa_box;
1765 cl_mem blur_in_lin = pass ? steer_grad_x : level_steer;
1766 cl_mem blur_in_quad = pass ? steer_grad_y : level_steer;
1767 cl_mem outL = pass ? steer_blur_lin : steer_grad_x;
1768 cl_mem outQ = pass ? steer_blur_quad : steer_grad_y;
1769 const int square = (pass == 0);
1770 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &blur_in_lin);
1771 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &blur_in_quad);
1772 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &outL);
1773 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &outQ);
1774 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &level_w);
1775 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &level_h);
1776 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &square);
1777 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
1778 if(cl_err != CL_SUCCESS) goto out;
1779 }
1780 {
1781 const int kernel = global_data->kernel_hl_cfa_grad;
1782 const int local_size = 64, n_groups = 256;
1783 size_t size_1d[3] = { (size_t)n_groups * local_size, 1, 1 };
1784 size_t local_size_1d[3] = { local_size, 1, 1 };
1785 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &steer_blur_lin);
1786 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &steer_grad_x);
1787 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &steer_grad_y);
1788 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &grad_partial_sums);
1789 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &level_w);
1790 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &level_h);
1791 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(float) * local_size, NULL);
1792 cl_err = dt_opencl_enqueue_kernel_2d_with_local(devid, kernel, size_1d, local_size_1d);
1793 if(cl_err != CL_SUCCESS) goto out;
1794 }
1795 {
1796 // finish the reduction on device (single work-item): no blocking readback
1797 const int kernel = global_data->kernel_hl_cfa_gnorm;
1798 const int ngroups = 256;
1799 size_t size_1d[3] = { 1, 1, 1 };
1800 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &grad_partial_sums);
1801 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &grad_mean_norm);
1802 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(int), &ngroups);
1803 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &n_cells);
1804 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size_1d);
1805 if(cl_err != CL_SUCCESS) goto out;
1806 }
1807 {
1808 const int kernel = global_data->kernel_hl_cfa_tensor;
1809 size_t size_1d[3] = { ROUNDUPDWD(n_cells, devid), 1, 1 };
1810 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &steer_grad_x);
1811 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &steer_grad_y);
1812 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &steer_blur_lin);
1813 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &steer_blur_quad);
1814 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(cl_mem), &steer_tensor_xx);
1815 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(cl_mem), &steer_tensor_xy);
1816 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(cl_mem), &steer_tensor_yy);
1817 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(cl_mem), &grad_mean_norm);
1818 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(float), &steer_k);
1819 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(int), &n_cells);
1820 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size_1d);
1821 if(cl_err != CL_SUCCESS) goto out;
1822 }
1823 // edge weights are constant across the level's sweeps: precompute once -- but only for
1824 // the small grids the block kernel serves (the large-grid launch loop reads the tensor
1825 // planes directly, see above)
1826 if(level_w * level_h <= 4096)
1827 {
1828 const int kernel = global_data->kernel_hl_cfa_weights;
1829 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &steer_tensor_xx);
1830 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &steer_tensor_xy);
1831 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &steer_tensor_yy);
1832 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &neighbour_weights);
1833 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(cl_mem), &neighbour_weights_sum);
1834 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &level_w);
1835 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &level_h);
1836 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
1837 if(cl_err != CL_SUCCESS) goto out;
1838 }
1839 }
1840
1841 // small grids: all 100 iterations inside ONE single-workgroup launch (bit-identical);
1842 // the 100-launch ping-pong was the region loop's dominant enqueue cost on small regions
1843 if(level_w * level_h <= 4096)
1844 {
1845 const int iters = 100; // flat budget; convergence comes from the pyramid depth
1846 size_t size_box[3] = { 256, 1, 1 };
1847 size_t local_box[3] = { 256, 1, 1 };
1848 if(steered)
1849 {
1850 // fused: all n_planes planes advance inside the single launch (dummy slots read plane 0)
1851 const int kernel_block = global_data->kernel_hl_cfa_jacobi_block;
1852 cl_mem solution1 = (n_planes > 1) ? level_solution[1] : level_solution[0];
1853 cl_mem solution2 = (n_planes > 2) ? level_solution[2] : level_solution[0];
1854 cl_mem scratch1 = (n_planes > 1) ? level_scratch[1] : level_scratch[0];
1855 cl_mem scratch2 = (n_planes > 2) ? level_scratch[2] : level_scratch[0];
1856 int arg_index = 0;
1857 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &level_solution[0]);
1858 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &solution1);
1859 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &solution2);
1860 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &level_scratch[0]);
1861 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &scratch1);
1862 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &scratch2);
1863 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &level_anchor_mask);
1864 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &neighbour_weights);
1865 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &neighbour_weights_sum);
1866 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(int), &level_w);
1867 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(int), &level_h);
1868 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(int), &iters);
1869 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(int), &n_planes);
1870 cl_err = dt_opencl_enqueue_kernel_2d_with_local(devid, kernel_block, size_box, local_box);
1871 if(cl_err != CL_SUCCESS) goto out;
1872 }
1873 else
1874 for(int plane = 0; plane < n_planes; plane++)
1875 {
1876 const int kernel_block = global_data->kernel_hl_fill_jacobi_block;
1877 int arg_index = 0;
1878 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &level_solution[plane]);
1879 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &level_scratch[plane]);
1880 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(cl_mem), &level_anchor_mask);
1881 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(int), &level_w);
1882 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(int), &level_h);
1883 dt_opencl_set_kernel_arg(devid, kernel_block, arg_index++, sizeof(int), &iters);
1884 cl_err = dt_opencl_enqueue_kernel_2d_with_local(devid, kernel_block, size_box, local_box);
1885 if(cl_err != CL_SUCCESS) goto out;
1886 }
1887 // 100 (even) internal swaps leave the solution in u, exactly like the launch loop:
1888 // rotate iter into prev for the next finer level's seed
1889 for(int plane = 0; plane < n_planes; plane++)
1890 {
1891 cl_mem swap_buf = prev_level_solution[plane];
1892 prev_level_solution[plane] = level_solution[plane];
1893 level_solution[plane] = swap_buf;
1894 }
1895 prev_level_w = level_w;
1896 prev_level_h = level_h;
1897 continue;
1898 }
1899
1900 // larger grids: Jacobi sweeps as separate launches, ping-ponging between the two buffers
1901 const int n_iter = 100; // flat budget; convergence comes from the pyramid depth
1902 cl_mem solution_planes[DT_HL_FILL_CL_MAXP], scratch_planes[DT_HL_FILL_CL_MAXP];
1903 for(int plane = 0; plane < n_planes; plane++)
1904 {
1905 solution_planes[plane] = level_solution[plane];
1906 scratch_planes[plane] = level_scratch[plane];
1907 }
1908 for(int iter = 0; iter < n_iter; iter++)
1909 {
1910 if(steered)
1911 {
1912 // fused sweep: one launch advances all n_planes planes (dummy slots read plane 0).
1913 // large grids keep the tensor form (see the kernel comment: cache reuse beats
1914 // precomputed weights there); only the small-grid block kernel uses neighbour_weights/neighbour_weights_sum
1915 const int kernel_jacobi = global_data->kernel_hl_cfa_jacobi;
1916 cl_mem solution1 = (n_planes > 1) ? solution_planes[1] : solution_planes[0];
1917 cl_mem solution2 = (n_planes > 2) ? solution_planes[2] : solution_planes[0];
1918 cl_mem scratch1 = (n_planes > 1) ? scratch_planes[1] : scratch_planes[0];
1919 cl_mem scratch2 = (n_planes > 2) ? scratch_planes[2] : scratch_planes[0];
1920 int arg_index = 0;
1921 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &solution_planes[0]);
1922 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &solution1);
1923 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &solution2);
1924 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &scratch_planes[0]);
1925 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &scratch1);
1926 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &scratch2);
1927 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &level_anchor_mask);
1928 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &steer_tensor_xx);
1929 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &steer_tensor_xy);
1930 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &steer_tensor_yy);
1931 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(int), &level_w);
1932 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(int), &level_h);
1933 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(int), &n_planes);
1934 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel_jacobi, size);
1935 if(cl_err != CL_SUCCESS) goto out;
1936 }
1937 else
1938 for(int plane = 0; plane < n_planes; plane++)
1939 {
1940 const int kernel_jacobi = global_data->kernel_hl_fill_jacobi;
1941 int arg_index = 0;
1942 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &solution_planes[plane]);
1943 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &scratch_planes[plane]);
1944 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(cl_mem), &level_anchor_mask);
1945 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(int), &level_w);
1946 dt_opencl_set_kernel_arg(devid, kernel_jacobi, arg_index++, sizeof(int), &level_h);
1947 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel_jacobi, size);
1948 if(cl_err != CL_SUCCESS) goto out;
1949 }
1950 for(int plane = 0; plane < n_planes; plane++)
1951 {
1952 cl_mem swap_buf = solution_planes[plane];
1953 solution_planes[plane] = scratch_planes[plane];
1954 scratch_planes[plane] = swap_buf;
1955 }
1956 }
1957 // solution of this level in `a`: stash into prev for the next finer seed, keeping u/v the
1958 // two scratches distinct from prev
1959 for(int plane = 0; plane < n_planes; plane++)
1960 {
1961 cl_mem swap_buf = prev_level_solution[plane];
1962 prev_level_solution[plane] = solution_planes[plane];
1963 solution_planes[plane] = swap_buf;
1964 level_solution[plane]
1965 = (prev_level_solution[plane] == level_solution[plane]) ? solution_planes[plane] : level_solution[plane];
1966 level_scratch[plane]
1967 = (prev_level_solution[plane] == level_scratch[plane]) ? solution_planes[plane] : level_scratch[plane];
1968 }
1969 prev_level_w = level_w;
1970 prev_level_h = level_h;
1971 }
1972
1973 // upsample prev (base-grid solution) into the full-res holes: kernel expects HOLE mask; our
1974 // `hole` buffer holds ANCHORS (caller contract), so hl_fill_up's test is inverted there.
1975 for(int plane = 0; plane < n_planes; plane++)
1976 {
1977 const int kernel_seed_up = global_data->kernel_hl_fill_up;
1978 size_t size_upsample[3] = { ROUNDUPDWD(region_w, devid), ROUNDUPDHT(region_h, devid), 1 };
1979 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 0, sizeof(cl_mem), &vals[plane]);
1980 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 1, sizeof(cl_mem), &hole);
1981 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 2, sizeof(cl_mem), &prev_level_solution[plane]);
1982 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 3, sizeof(int), &region_w);
1983 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 4, sizeof(int), &region_h);
1984 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 5, sizeof(int), &base_w);
1985 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 6, sizeof(int), &base_h);
1986 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 7, sizeof(int), &downsample);
1987 dt_opencl_set_kernel_arg(devid, kernel_seed_up, 8, sizeof(int), &mask_is_hole);
1988 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel_seed_up, size_upsample);
1989 if(cl_err != CL_SUCCESS) goto out;
1990 }
1991
1992out:
1993 for(int plane = 0; plane < DT_HL_FILL_CL_MAXP; plane++)
1994 {
1995 dt_opencl_release_mem_object(base_vals[plane]);
1996 dt_opencl_release_mem_object(level_vals[plane]);
1997 dt_opencl_release_mem_object(level_solution[plane]);
1998 dt_opencl_release_mem_object(level_scratch[plane]);
1999 dt_opencl_release_mem_object(prev_level_solution[plane]);
2000 }
2001 dt_opencl_release_mem_object(base_anchor_mask);
2002 dt_opencl_release_mem_object(level_anchor_mask);
2003 dt_opencl_release_mem_object(base_steer);
2004 dt_opencl_release_mem_object(level_steer);
2005 dt_opencl_release_mem_object(steer_blur_lin);
2006 dt_opencl_release_mem_object(steer_blur_quad);
2007 dt_opencl_release_mem_object(steer_grad_x);
2008 dt_opencl_release_mem_object(steer_grad_y);
2009 dt_opencl_release_mem_object(steer_tensor_xx);
2010 dt_opencl_release_mem_object(steer_tensor_xy);
2011 dt_opencl_release_mem_object(steer_tensor_yy);
2012 dt_opencl_release_mem_object(grad_partial_sums);
2013 dt_opencl_release_mem_object(grad_mean_norm);
2014 dt_opencl_release_mem_object(neighbour_weights);
2015 dt_opencl_release_mem_object(neighbour_weights_sum);
2016 return cl_err;
2017}
2018
2019cl_int _cf_harmonic_fill_cl(const int devid, void *gd_void, cl_mem val, cl_mem hole, const int region_w,
2020 const int region_h, const int base_ds, const int mask_is_hole, cl_mem steer)
2021{
2022 cl_mem val_planes[1] = { val };
2023 return _cf_harmonic_fill_cl_n(devid, gd_void, val_planes, 1, hole, region_w, region_h, base_ds, mask_is_hole,
2024 steer);
2025}
2026#endif // HAVE_OPENCL
2027
2028#ifdef HAVE_OPENCL
2029
2030cl_int _cf_joint_stage_cl(const int devid, void *gd_void, cl_mem estimate, cl_mem valid, cl_mem model_quality,
2031 cl_mem mom0, cl_mem mom1, cl_mem mom2, cl_mem steer,
2032 const float *const restrict channel_means, const int region_w, const int region_h,
2033 const float cf_sigma, const float cf_fmin, const int c, const int guide1,
2034 const int guide2)
2035{
2037 const size_t region_pixels = (size_t)region_w * region_h;
2038 cl_int cl_err = DT_OPENCL_DEFAULT_ERROR;
2039 size_t work_size[3] = { ROUNDUPDWD(region_w, devid), ROUNDUPDHT(region_h, devid), 1 };
2040
2041 cl_mem coeff_slope_a = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2042 cl_mem coeff_slope_b = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2043 cl_mem coeff_offset = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2044 cl_mem coeff_r2 = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2045 cl_mem anchor = dt_opencl_alloc_device_buffer(devid, region_pixels);
2046 cl_mem broad = dt_opencl_alloc_device_buffer(devid, region_pixels);
2047 if(!coeff_slope_a || !coeff_slope_b || !coeff_offset || !coeff_r2 || !anchor || !broad) goto out;
2048
2049 // per-pixel colour-line fit from the blurred moments; also writes the anchor masks
2050 {
2051 const int kernel = global_data->kernel_hl_cf_fit_joint;
2052 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &mom0);
2053 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &mom1);
2054 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &mom2);
2055 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &valid);
2056 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &region_w);
2057 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &region_h);
2058 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &c);
2059 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &guide1);
2060 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &guide2);
2061 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(float), &cf_fmin);
2062 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(cl_mem), &coeff_slope_a);
2063 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(cl_mem), &coeff_slope_b);
2064 dt_opencl_set_kernel_arg(devid, kernel, 12, sizeof(cl_mem), &coeff_offset);
2065 dt_opencl_set_kernel_arg(devid, kernel, 13, sizeof(cl_mem), &coeff_r2);
2066 dt_opencl_set_kernel_arg(devid, kernel, 14, sizeof(cl_mem), &anchor);
2067 dt_opencl_set_kernel_arg(devid, kernel, 15, sizeof(cl_mem), &broad);
2068 dt_opencl_set_kernel_arg(devid, kernel, 16, sizeof(float), &channel_means[0]);
2069 dt_opencl_set_kernel_arg(devid, kernel, 17, sizeof(float), &channel_means[1]);
2070 dt_opencl_set_kernel_arg(devid, kernel, 18, sizeof(float), &channel_means[2]);
2071 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_size);
2072 if(cl_err != CL_SUCCESS) goto out;
2073 }
2074
2075 // harmonic diffusion of the coefficient fields across the clipped zone (fit quality cr2 uses
2076 // the broader anchor mask)
2077 {
2078 const int base_ds = (int)(cf_sigma / 4.f);
2079 cl_mem coeff_planes[3]
2080 = { coeff_slope_a, coeff_slope_b, coeff_offset }; // shared anchor mask -> one fused fill
2081 cl_err
2082 = _cf_harmonic_fill_cl_n(devid, gd_void, coeff_planes, 3, anchor, region_w, region_h, base_ds, 0, steer);
2083 if(cl_err != CL_SUCCESS) goto out;
2084 cl_err = _cf_harmonic_fill_cl(devid, gd_void, coeff_r2, broad, region_w, region_h, base_ds, 0, steer);
2085 if(cl_err != CL_SUCCESS) goto out;
2086 }
2087
2088 // evaluate the diffused colour line against the measured guides; write est + fit score bsc
2089 {
2090 const int kernel = global_data->kernel_hl_cf_eval_joint;
2091 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &coeff_slope_a);
2092 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &coeff_slope_b);
2093 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &coeff_offset);
2094 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &coeff_r2);
2095 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(cl_mem), &valid);
2096 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(cl_mem), &estimate);
2097 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(cl_mem), &model_quality);
2098 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &region_w);
2099 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &region_h);
2100 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(int), &c);
2101 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(int), &guide1);
2102 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(int), &guide2);
2103 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_size);
2104 }
2105
2106out:
2107 dt_opencl_release_mem_object(coeff_slope_a);
2108 dt_opencl_release_mem_object(coeff_slope_b);
2109 dt_opencl_release_mem_object(coeff_offset);
2113 return cl_err;
2114}
2115
2116// Pair stage for one orientation: predict one clipped channel from a SINGLE guide channel
2117// (slope + intercept fitted from the windowed moments), diffuse the fitted coefficients across
2118// the clipped zone, then evaluate against the measured guide and write into est.
2119// `a`/`b` name the channel pair and `o` picks the orientation (which of the two is the target
2120// tc and which is the guide gc); oc is the remaining third channel.
2121// Runs unconditionally (no target-count guard: an empty target set writes nothing).
2122// Mirrors the pair coefficient-field stage inside _region_guided_filter (CPU): any change here
2123// must be mirrored there and re-validated with the HL_CFCL_TEST self-tests.
2124//
2125// MATHS BRIDGE -- article "The algorithm" step 3, the single-guide fallback: the model collapses to
2126// v_hat = a*u + d with a = Cov(u,v)/Var(u) and R^2 = Cov(u,v)^2/(Var(u) Var(v)) (hl_cf_fit_pair),
2127// transported by the E_transport fill and evaluated by hl_cf_eval_pair. Same fit/transport/evaluate
2128// skeleton as the joint stage, one guide instead of two.
2129static cl_int _cf_pair_stage_cl(const int devid, void *gd_void, cl_mem estimate, cl_mem valid,
2130 cl_mem model_quality, cl_mem moment_a, cl_mem moment_b, cl_mem steer,
2131 const float *const restrict channel_means, const int region_w, const int region_h,
2132 const float cf_sigma, const float cf_fmin, const int chan_a, const int chan_b,
2133 const int orientation)
2134{
2136 const size_t region_pixels = (size_t)region_w * region_h;
2137 cl_int cl_err = DT_OPENCL_DEFAULT_ERROR;
2138 size_t work_size[3] = { ROUNDUPDWD(region_w, devid), ROUNDUPDHT(region_h, devid), 1 };
2139 const int target_chan = orientation ? chan_b : chan_a;
2140 const int guide_chan = orientation ? chan_a : chan_b;
2141 const int other_chan = 3 - chan_a - chan_b;
2142
2143 cl_mem coeff_slope = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2144 cl_mem coeff_intercept = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2145 cl_mem coeff_r2 = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2146 cl_mem anchor = dt_opencl_alloc_device_buffer(devid, region_pixels);
2147 cl_mem broad = dt_opencl_alloc_device_buffer(devid, region_pixels);
2148 if(!coeff_slope || !coeff_intercept || !coeff_r2 || !anchor || !broad) goto out;
2149
2150 // per-pixel single-guide fit (slope cs, intercept ci, fit quality cr2) from the moments
2151 {
2152 const int kernel = global_data->kernel_hl_cf_fit_pair;
2153 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &moment_a);
2154 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &moment_b);
2155 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &valid);
2156 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &region_w);
2157 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &region_h);
2158 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &target_chan);
2159 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &orientation);
2160 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(float), &cf_fmin);
2161 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(cl_mem), &coeff_slope);
2162 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(cl_mem), &coeff_intercept);
2163 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(cl_mem), &coeff_r2);
2164 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(cl_mem), &anchor);
2165 dt_opencl_set_kernel_arg(devid, kernel, 12, sizeof(cl_mem), &broad);
2166 dt_opencl_set_kernel_arg(devid, kernel, 13, sizeof(float), &channel_means[target_chan]);
2167 dt_opencl_set_kernel_arg(devid, kernel, 14, sizeof(float), &channel_means[guide_chan]);
2168 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_size);
2169 if(cl_err != CL_SUCCESS) goto out;
2170 }
2171
2172 // harmonic diffusion of slope/intercept/fit-quality fields across the clipped zone
2173 {
2174 const int base_ds = (int)(cf_sigma / 4.f);
2175 cl_mem coeff_planes[2] = { coeff_slope, coeff_intercept }; // shared anchor mask -> one fused fill
2176 cl_err
2177 = _cf_harmonic_fill_cl_n(devid, gd_void, coeff_planes, 2, anchor, region_w, region_h, base_ds, 0, steer);
2178 if(cl_err != CL_SUCCESS) goto out;
2179 cl_err = _cf_harmonic_fill_cl(devid, gd_void, coeff_r2, broad, region_w, region_h, base_ds, 0, steer);
2180 if(cl_err != CL_SUCCESS) goto out;
2181 }
2182
2183 // evaluate the diffused fit against the measured guide; write est + fit score bsc
2184 {
2185 const int kernel = global_data->kernel_hl_cf_eval_pair;
2186 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &coeff_slope);
2187 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &coeff_intercept);
2188 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &coeff_r2);
2189 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &valid);
2190 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(cl_mem), &estimate);
2191 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(cl_mem), &model_quality);
2192 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &region_w);
2193 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &region_h);
2194 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &target_chan);
2195 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(int), &guide_chan);
2196 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(int), &other_chan);
2197 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_size);
2198 }
2199
2200out:
2201 dt_opencl_release_mem_object(coeff_slope);
2202 dt_opencl_release_mem_object(coeff_intercept);
2206 return cl_err;
2207}
2208
2209// Variant of the joint stage that fits and DIFFUSES the coefficient fields but defers the
2210// evaluation: the caller keeps the four returned buffers (slope a, slope b, offset d, fit
2211// quality r2) to evaluate later in the deep-channel cascade (deep-channel stash).
2212// Caller releases the returned cl_mem buffers. Mirrors the deferred joint fit inside
2213// _region_guided_filter (CPU): any change here must be mirrored there and re-validated with
2214// the HL_CFCL_TEST self-tests.
2215//
2216// MATHS BRIDGE -- article "The algorithm" step 3, the deep channel's ordering subtlety: fit (a,b,d)
2217// and transport them (E_transport) exactly as the joint stage, but DEFER v_hat = a*u1 + b*u2 + d so it
2218// is evaluated only after the other clipped channels are reconstructed -- then every guide it reads is
2219// a continuous surface (no clip-contour arc). Returns the four diffused planes (a, b, d, R^2) to stash.
2220static cl_int _cf_joint_fit_cl(const int devid, void *gd_void, cl_mem estimate, cl_mem valid, cl_mem mom0,
2221 cl_mem mom1, cl_mem mom2, cl_mem steer, const float *const restrict channel_means,
2222 const int region_w, const int region_h, const float cf_sigma, const float cf_fmin,
2223 const int c, const int guide1, const int guide2, cl_mem *ca_out, cl_mem *cb_out,
2224 cl_mem *cd_out, cl_mem *cr2_out)
2225{
2227 const size_t region_pixels = (size_t)region_w * region_h;
2228 cl_int cl_err = DT_OPENCL_DEFAULT_ERROR;
2229 size_t work_size[3] = { ROUNDUPDWD(region_w, devid), ROUNDUPDHT(region_h, devid), 1 };
2230
2231 cl_mem coeff_slope_a = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2232 cl_mem coeff_slope_b = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2233 cl_mem coeff_offset = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2234 cl_mem coeff_r2 = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2235 cl_mem anchor = dt_opencl_alloc_device_buffer(devid, region_pixels);
2236 cl_mem broad = dt_opencl_alloc_device_buffer(devid, region_pixels);
2237 *ca_out = *cb_out = *cd_out = *cr2_out = NULL;
2238 if(!coeff_slope_a || !coeff_slope_b || !coeff_offset || !coeff_r2 || !anchor || !broad) goto out;
2239
2240 {
2241 const int kernel = global_data->kernel_hl_cf_fit_joint;
2242 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &mom0);
2243 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &mom1);
2244 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &mom2);
2245 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &valid);
2246 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &region_w);
2247 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &region_h);
2248 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &c);
2249 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &guide1);
2250 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &guide2);
2251 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(float), &cf_fmin);
2252 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(cl_mem), &coeff_slope_a);
2253 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(cl_mem), &coeff_slope_b);
2254 dt_opencl_set_kernel_arg(devid, kernel, 12, sizeof(cl_mem), &coeff_offset);
2255 dt_opencl_set_kernel_arg(devid, kernel, 13, sizeof(cl_mem), &coeff_r2);
2256 dt_opencl_set_kernel_arg(devid, kernel, 14, sizeof(cl_mem), &anchor);
2257 dt_opencl_set_kernel_arg(devid, kernel, 15, sizeof(cl_mem), &broad);
2258 dt_opencl_set_kernel_arg(devid, kernel, 16, sizeof(float), &channel_means[0]);
2259 dt_opencl_set_kernel_arg(devid, kernel, 17, sizeof(float), &channel_means[1]);
2260 dt_opencl_set_kernel_arg(devid, kernel, 18, sizeof(float), &channel_means[2]);
2261 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_size);
2262 if(cl_err != CL_SUCCESS) goto out;
2263 }
2264
2265 {
2266 const int base_ds = (int)(cf_sigma / 4.f);
2267 cl_mem coeff_planes[3]
2268 = { coeff_slope_a, coeff_slope_b, coeff_offset }; // shared anchor mask -> one fused fill
2269 cl_err
2270 = _cf_harmonic_fill_cl_n(devid, gd_void, coeff_planes, 3, anchor, region_w, region_h, base_ds, 0, steer);
2271 if(cl_err != CL_SUCCESS) goto out;
2272 cl_err = _cf_harmonic_fill_cl(devid, gd_void, coeff_r2, broad, region_w, region_h, base_ds, 0, steer);
2273 if(cl_err != CL_SUCCESS) goto out;
2274 }
2275
2276 *ca_out = coeff_slope_a;
2277 *cb_out = coeff_slope_b;
2278 *cd_out = coeff_offset;
2279 *cr2_out = coeff_r2;
2280 coeff_slope_a = coeff_slope_b = coeff_offset = coeff_r2 = NULL;
2281 cl_err = CL_SUCCESS;
2282
2283out:
2284 dt_opencl_release_mem_object(coeff_slope_a);
2285 dt_opencl_release_mem_object(coeff_slope_b);
2286 dt_opencl_release_mem_object(coeff_offset);
2290 return cl_err;
2291}
2292
2293cl_int _cf_stage_cl(const int devid, void *gd_void, cl_mem estimate, cl_mem valid, cl_mem model_quality,
2294 cl_mem luminance, cl_mem steer, const float *const restrict channel_means,
2295 dt_gaussian_cl_t *gaussian, const int region_w, const int region_h, const float cf_sigma,
2296 const float cf_fmin, const float cf_binv, const int cdeep)
2297{
2298 cl_int cl_err = CL_SUCCESS;
2299 cl_mem deep_a = NULL, deep_b = NULL, deep_d = NULL, deep_r2 = NULL;
2301 size_t size[3] = { ROUNDUPDWD(region_w, devid), ROUNDUPDHT(region_h, devid), 1 };
2302
2303 // the windowed moments are shared: the joint weight (all-valid x frozen bw_) does not depend
2304 // on the target channel and the evals only write CLIPPED channels, so estimate never changes at chan_a
2305 // weighted pixel -- packed + blur ONCE and fit the three channels from the same fields (the
2306 // per-channel repack the CPU does is redundant on both sides; here the blurs dominate)
2307 cl_mem packed = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2308 cl_mem moment0 = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2309 cl_mem moment1 = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2310 cl_mem moment2 = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2311 cl_mem moments[3];
2312 moments[0] = moment0;
2313 moments[1] = moment1;
2314 moments[2] = moment2;
2315 // edge-aware transport of the windows (see _region_edge_blur): buffers for the recursive sweeps,
2316 // plus the guide, smoothed ONCE here rather than once per moment plane
2317 const size_t region_pixels_cl = (size_t)region_w * region_h;
2318 cl_mem dt_data = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels_cl * 4);
2319 cl_mem dt_step_x = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels_cl);
2320 cl_mem dt_step_y = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels_cl);
2321 cl_mem dt_guide = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels_cl);
2322 cl_mem dt_guide_img = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float));
2323 cl_mem dt_guide_blur = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float));
2324 if(!packed || !moment0 || !moment1 || !moment2 || !dt_data || !dt_step_x || !dt_step_y || !dt_guide
2325 || !dt_guide_img || !dt_guide_blur)
2326 {
2327 cl_err = DT_OPENCL_DEFAULT_ERROR;
2328 goto out;
2329 }
2330 {
2331 const size_t origin[] = { 0, 0, 0 };
2332 const size_t reg[] = { (size_t)region_w, (size_t)region_h, 1 };
2333 cl_err = dt_opencl_enqueue_copy_buffer_to_image(devid, luminance, dt_guide_img, 0, (size_t *)origin,
2334 (size_t *)reg);
2335 if(cl_err == CL_SUCCESS)
2336 cl_err = _region_blur1_cl(devid, dt_guide_img, dt_guide_blur, region_w, region_h, CF_EDGE_GUIDE_SIGMA);
2337 if(cl_err == CL_SUCCESS)
2338 cl_err = dt_opencl_enqueue_copy_image_to_buffer(devid, dt_guide_blur, dt_guide, (size_t *)origin,
2339 (size_t *)reg, 0);
2340 if(cl_err != CL_SUCCESS) goto out;
2341 }
2342 // cf_binv = 1/(0.35 * plateau) on the host side, so recover the plateau the same way the CPU
2343 // scales its range: both sides must key the transport on the same luminance reference
2344 const float cf_lref_cl = (cf_binv > 0.f) ? 1.f / (0.35f * cf_binv) : 0.f;
2345 const float cf_sigma_r_cl = CF_EDGE_RANGE * fmaxf(cf_lref_cl, 1e-9f);
2346 // three modes = the ten centred moment planes: mode 0 -> [n, wR, wG, wB], mode 1 -> [wRR, wGG, wBB, wRG],
2347 // mode 2 -> [wRB, wGB, unweighted-n, 0]; each packed product image is Gaussian-blurred to a windowed moment
2348 for(int mode = 0; mode < 3 && cl_err == CL_SUCCESS; mode++)
2349 {
2350 const int kernel = global_data->kernel_hl_cf_pack_joint;
2351 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &estimate);
2352 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &valid);
2353 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &luminance);
2354 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &packed);
2355 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &region_w);
2356 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &region_h);
2357 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(float), &cf_binv);
2358 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &mode);
2359 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(float), &channel_means[0]);
2360 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(float), &channel_means[1]);
2361 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(float), &channel_means[2]);
2362 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
2363 if(cl_err == CL_SUCCESS)
2364 cl_err = _region_edge_blur_cl(devid, gd_void, packed, moments[mode], dt_guide, dt_data, dt_step_x,
2365 dt_step_y, region_w, region_h, cf_sigma, cf_sigma_r_cl);
2366 }
2367 if(cl_err != CL_SUCCESS) goto out;
2368
2369 // joint fits: immediate strict eval for the non-deep channels, stash for the deep one
2370 for(int c = 0; c < 3; c++)
2371 {
2372 const int guide1 = (c == 0) ? 1 : 0;
2373 const int guide2 = (c == 2) ? 1 : 2;
2374 if(c == cdeep)
2375 {
2376 cl_err = _cf_joint_fit_cl(devid, gd_void, estimate, valid, moment0, moment1, moment2, steer, channel_means,
2377 region_w, region_h, cf_sigma, cf_fmin, c, guide1, guide2, &deep_a, &deep_b,
2378 &deep_d, &deep_r2);
2379 }
2380 else
2381 cl_err = _cf_joint_stage_cl(devid, gd_void, estimate, valid, model_quality, moment0, moment1, moment2, steer,
2382 channel_means, region_w, region_h, cf_sigma, cf_fmin, c, guide1, guide2);
2383 if(cl_err != CL_SUCCESS) goto out;
2384 }
2385
2386 // pair fallbacks: the pair weight (both-valid x frozen bw_) is orientation-independent and
2387 // estimate at weighted pixels never changes, so packed each pair's moments once for both orientations
2388 for(int chan_a = 0; chan_a < 3; chan_a++)
2389 for(int chan_b = chan_a + 1; chan_b < 3; chan_b++)
2390 {
2391 for(int mode = 0; mode < 2 && cl_err == CL_SUCCESS; mode++)
2392 {
2393 const int kernel = global_data->kernel_hl_cf_pack_pair;
2394 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &estimate);
2395 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &valid);
2396 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &luminance);
2397 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &packed);
2398 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &region_w);
2399 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &region_h);
2400 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(float), &cf_binv);
2401 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &chan_a);
2402 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &chan_b);
2403 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(int), &mode);
2404 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(float), &channel_means[chan_a]);
2405 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(float), &channel_means[chan_b]);
2406 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
2407 if(cl_err == CL_SUCCESS)
2408 cl_err = gaussian
2409 ? dt_gaussian_blur_cl(gaussian, packed, mode ? moment1 : moment0)
2410 : _region_blur_cl(devid, packed, mode ? moment1 : moment0, region_w, region_h, cf_sigma);
2411 }
2412 for(int orientation = 0; orientation < 2 && cl_err == CL_SUCCESS; orientation++)
2413 cl_err
2414 = _cf_pair_stage_cl(devid, gd_void, estimate, valid, model_quality, moment0, moment1, steer,
2415 channel_means, region_w, region_h, cf_sigma, cf_fmin, chan_a, chan_b, orientation);
2416 if(cl_err != CL_SUCCESS) goto out;
2417 }
2418
2419 // deferred deep evaluation: blur the deep-channel validity masks (feathered depth split),
2420 // then evaluate the stashed coefficient fields now that the guide channels are reconstructed
2421 if(deep_a)
2422 {
2423 const int guide1 = (cdeep == 0) ? 1 : 0;
2424 const int guide2 = (cdeep == 2) ? 1 : 2;
2425 cl_mem deep_packed = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2426 cl_mem mask_blurred = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2427 if(!deep_packed || !mask_blurred)
2428 {
2429 dt_opencl_release_mem_object(deep_packed);
2430 dt_opencl_release_mem_object(mask_blurred);
2431 cl_err = DT_OPENCL_DEFAULT_ERROR;
2432 goto out;
2433 }
2434
2435 const int kernel_mask = global_data->kernel_hl_cf_pack_deepmask;
2436 dt_opencl_set_kernel_arg(devid, kernel_mask, 0, sizeof(cl_mem), &valid);
2437 dt_opencl_set_kernel_arg(devid, kernel_mask, 1, sizeof(cl_mem), &deep_packed);
2438 dt_opencl_set_kernel_arg(devid, kernel_mask, 2, sizeof(int), &region_w);
2439 dt_opencl_set_kernel_arg(devid, kernel_mask, 3, sizeof(int), &region_h);
2440 dt_opencl_set_kernel_arg(devid, kernel_mask, 4, sizeof(int), &cdeep);
2441 dt_opencl_set_kernel_arg(devid, kernel_mask, 5, sizeof(int), &guide1);
2442 dt_opencl_set_kernel_arg(devid, kernel_mask, 6, sizeof(int), &guide2);
2443 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel_mask, size);
2444 if(cl_err == CL_SUCCESS)
2445 cl_err = gaussian ? dt_gaussian_blur_cl(gaussian, deep_packed, mask_blurred)
2446 : _region_blur_cl(devid, deep_packed, mask_blurred, region_w, region_h, cf_sigma);
2447
2448 if(cl_err == CL_SUCCESS)
2449 {
2450 const int kernel_eval = global_data->kernel_hl_cf_eval_deep;
2451 dt_opencl_set_kernel_arg(devid, kernel_eval, 0, sizeof(cl_mem), &deep_a);
2452 dt_opencl_set_kernel_arg(devid, kernel_eval, 1, sizeof(cl_mem), &deep_b);
2453 dt_opencl_set_kernel_arg(devid, kernel_eval, 2, sizeof(cl_mem), &deep_d);
2454 dt_opencl_set_kernel_arg(devid, kernel_eval, 3, sizeof(cl_mem), &deep_r2);
2455 dt_opencl_set_kernel_arg(devid, kernel_eval, 4, sizeof(cl_mem), &mask_blurred);
2456 dt_opencl_set_kernel_arg(devid, kernel_eval, 5, sizeof(cl_mem), &valid);
2457 dt_opencl_set_kernel_arg(devid, kernel_eval, 6, sizeof(cl_mem), &estimate);
2458 dt_opencl_set_kernel_arg(devid, kernel_eval, 7, sizeof(cl_mem), &model_quality);
2459 dt_opencl_set_kernel_arg(devid, kernel_eval, 8, sizeof(int), &region_w);
2460 dt_opencl_set_kernel_arg(devid, kernel_eval, 9, sizeof(int), &region_h);
2461 dt_opencl_set_kernel_arg(devid, kernel_eval, 10, sizeof(int), &cdeep);
2462 dt_opencl_set_kernel_arg(devid, kernel_eval, 11, sizeof(int), &guide1);
2463 dt_opencl_set_kernel_arg(devid, kernel_eval, 12, sizeof(int), &guide2);
2464 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel_eval, size);
2465 }
2466 dt_opencl_release_mem_object(deep_packed);
2467 dt_opencl_release_mem_object(mask_blurred);
2468 }
2469
2470out:
2475 dt_opencl_release_mem_object(dt_guide_img);
2476 dt_opencl_release_mem_object(dt_guide_blur);
2485 return cl_err;
2486}
2487
2488cl_int _hf_stage_cl(const int devid, void *gd_void, cl_mem estimate, cl_mem valid, cl_mem model_quality,
2489 cl_mem luminance, cl_mem steer, dt_gaussian_cl_t *gaussian, const int region_w,
2490 const int region_h, const float cf_sigma, const float cf_fmin, const float cf_binv)
2491{
2493 const size_t region_pixels = (size_t)region_w * region_h;
2494 cl_int cl_err = DT_OPENCL_DEFAULT_ERROR;
2495 size_t size[3] = { ROUNDUPDWD(region_w, devid), ROUNDUPDHT(region_h, devid), 1 };
2496 const float blur_sigma = fmaxf(cf_sigma / 4.f, 2.f);
2497
2498 cl_mem packed = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2499 cl_mem lowpass = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2500 cl_mem moment0 = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2501 cl_mem moment1 = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2502 cl_mem moment2 = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2503 cl_mem energy = dt_opencl_alloc_device(devid, size[0], size[1], sizeof(float) * 4);
2504 cl_mem moments[3];
2505 cl_mem gain_a = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2506 cl_mem gain_b = dt_opencl_alloc_device_buffer(devid, sizeof(float) * region_pixels);
2507 cl_mem anchor = dt_opencl_alloc_device_buffer(devid, region_pixels);
2508 moments[0] = moment0;
2509 moments[1] = moment1;
2510 moments[2] = moment2;
2511 if(!packed || !lowpass || !moment0 || !moment1 || !moment2 || !energy || !gain_a || !gain_b || !anchor) goto out;
2512
2513 // lowpass of estimate (computed ONCE, shared by every channel and the damped path)
2514 {
2515 const int kernel = global_data->kernel_hl_buf_to_img;
2516 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &estimate);
2517 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &packed);
2518 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(int), &region_w);
2519 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &region_h);
2520 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
2521 if(cl_err != CL_SUCCESS) goto out;
2522 cl_err = _region_blur_cl(devid, packed, lowpass, region_w, region_h, blur_sigma);
2523 if(cl_err != CL_SUCCESS) goto out;
2524 }
2525
2526 // windowed moments of the detail band (estimate minus lowpass), packed then blurred
2527 for(int mode = 0; mode < 3; mode++)
2528 {
2529 const int kernel = global_data->kernel_hl_hf_pack;
2530 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &estimate);
2531 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &valid);
2532 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &luminance);
2533 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &lowpass);
2534 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(cl_mem), &packed);
2535 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &region_w);
2536 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &region_h);
2537 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(float), &cf_binv);
2538 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &mode);
2539 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
2540 if(cl_err != CL_SUCCESS) goto out;
2541 cl_err = gaussian ? dt_gaussian_blur_cl(gaussian, packed, moments[mode])
2542 : _region_blur_cl(devid, packed, moments[mode], region_w, region_h, cf_sigma);
2543 if(cl_err != CL_SUCCESS) goto out;
2544 }
2545
2546 // per channel: fit detail-band gains, diffuse them, measure both candidates' energy, evaluate
2547 for(int c = 0; c < 3; c++)
2548 {
2549 const int guide1 = (c == 0) ? 1 : 0;
2550 const int guide2 = (c == 2) ? 1 : 2;
2551
2552 // fit the quality-shrunk detail-band gains (gain_a, gain_b) and the anchor mask
2553 {
2554 const int kernel = global_data->kernel_hl_hf_fit;
2555 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &moment0);
2556 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &moment1);
2557 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &moment2);
2558 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &valid);
2559 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &region_w);
2560 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &region_h);
2561 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &c);
2562 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &guide1);
2563 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &guide2);
2564 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(float), &cf_fmin);
2565 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(cl_mem), &gain_a);
2566 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(cl_mem), &gain_b);
2567 dt_opencl_set_kernel_arg(devid, kernel, 12, sizeof(cl_mem), &anchor);
2568 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
2569 if(cl_err != CL_SUCCESS) goto out;
2570 }
2571
2572 // harmonic diffusion of the two gain fields across the clipped zone
2573 const int base_ds = (int)(cf_sigma / 4.f);
2574 cl_mem gain_pair[2] = { gain_a, gain_b }; // shared anchor mask -> one fused fill
2575 cl_err = _cf_harmonic_fill_cl_n(devid, gd_void, gain_pair, 2, anchor, region_w, region_h, base_ds, 0, steer);
2576 if(cl_err != CL_SUCCESS) goto out;
2577
2578 // local energy of the guided vs damped detail candidates (blurred absolute values)
2579 {
2580 const int kernel = global_data->kernel_hl_hf_energy;
2581 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &estimate);
2582 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &valid);
2583 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &model_quality);
2584 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &lowpass);
2585 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(cl_mem), &gain_a);
2586 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(cl_mem), &gain_b);
2587 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(cl_mem), &packed);
2588 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &region_w);
2589 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &region_h);
2590 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(int), &c);
2591 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(int), &guide1);
2592 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(int), &guide2);
2593 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
2594 if(cl_err != CL_SUCCESS) goto out;
2595 cl_err = _region_blur_cl(devid, packed, energy, region_w, region_h, blur_sigma);
2596 if(cl_err != CL_SUCCESS) goto out;
2597 }
2598
2599 // minimum-energy blend of the two candidates at strict (both-guides-valid) targets
2600 {
2601 const int kernel = global_data->kernel_hl_hf_eval;
2602 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &estimate);
2603 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &valid);
2604 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &model_quality);
2605 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &lowpass);
2606 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(cl_mem), &energy);
2607 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(cl_mem), &gain_a);
2608 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(cl_mem), &gain_b);
2609 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &region_w);
2610 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &region_h);
2611 dt_opencl_set_kernel_arg(devid, kernel, 9, sizeof(int), &c);
2612 dt_opencl_set_kernel_arg(devid, kernel, 10, sizeof(int), &guide1);
2613 dt_opencl_set_kernel_arg(devid, kernel, 11, sizeof(int), &guide2);
2614 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
2615 if(cl_err != CL_SUCCESS) goto out;
2616 }
2617 }
2618
2619 // single-guide pixels: only the quality-damped detail (no guided resynthesis possible)
2620 {
2621 const int kernel = global_data->kernel_hl_hf_damp;
2622 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &estimate);
2623 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &valid);
2624 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &model_quality);
2625 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &lowpass);
2626 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &region_w);
2627 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &region_h);
2628 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, size);
2629 }
2630
2631out:
2641 return cl_err;
2642}
2643
2644#endif // HAVE_OPENCL
void _region_edge_blur(const float *const restrict in, float *const restrict out, const float *const restrict guide, float *const restrict step_x, float *const restrict step_y, const int region_w, const int region_h, const float sigma_s, const float sigma_r)
Definition blur.c:103
cl_int _region_edge_blur_cl(const int devid, void *gd_void, cl_mem in_image, cl_mem out_image, cl_mem guide, cl_mem data, cl_mem step_x, cl_mem step_y, const int region_w, const int region_h, const float sigma_s, const float sigma_r)
Definition blur.c:177
cl_int _region_blur_cl(const int devid, cl_mem in, cl_mem out, const int region_w, const int region_h, const float sigma)
Definition blur.c:74
static void _region_blur(const float *const restrict in, float *const restrict out, const int region_w, const int region_h, const float sigma)
Definition blur.h:43
static float _aniso_edge_w(const float *const restrict tensor_xx, const float *const restrict tensor_xy, const float *const restrict tensor_yy, const size_t i, const size_t j, const int offset_x, const int offset_y)
Definition chroma.h:55
static cl_int _cf_harmonic_fill_cl_n(const int devid, void *gd_void, cl_mem *vals, const int n_planes_in, cl_mem hole, const int region_w, const int region_h, const int base_ds, const int mask_is_hole, cl_mem steer)
cl_int _cf_stage_cl(const int devid, void *gd_void, cl_mem estimate, cl_mem valid, cl_mem model_quality, cl_mem luminance, cl_mem steer, const float *const restrict channel_means, dt_gaussian_cl_t *gaussian, const int region_w, const int region_h, const float cf_sigma, const float cf_fmin, const float cf_binv, const int cdeep)
static cl_int _cf_pair_stage_cl(const int devid, void *gd_void, cl_mem estimate, cl_mem valid, cl_mem model_quality, cl_mem moment_a, cl_mem moment_b, cl_mem steer, const float *const restrict channel_means, const int region_w, const int region_h, const float cf_sigma, const float cf_fmin, const int chan_a, const int chan_b, const int orientation)
cl_int _cf_harmonic_fill_cl(const int devid, void *gd_void, cl_mem val, cl_mem hole, const int region_w, const int region_h, const int base_ds, const int mask_is_hole, cl_mem steer)
#define HF_M2(nb_index, coef_a, coef_b)
static __DT_CLONE_TARGETS__ void _cf_fill_relax_3(float *const restrict field, float *const restrict tmp, const uint8_t *const restrict level_anchor, const float *const restrict edge_weights, const float *const restrict edge_weight_sum, const int coarse_w, const int coarse_h, const size_t cell_count, const int steered)
static __DT_CLONE_TARGETS__ void _cf_fill_relax_4(float *const restrict field, float *const restrict tmp, const uint8_t *const restrict level_anchor, const float *const restrict edge_weights, const float *const restrict edge_weight_sum, const int coarse_w, const int coarse_h, const size_t cell_count, const int steered)
__DT_CLONE_TARGETS__ void _cf_reconstruct(_hl_region_ctx_t *const ctx)
static __DT_CLONE_TARGETS__ void _cf_adaptive_tensor(const float *const restrict luminance, float *const restrict tensor_xx, float *const restrict tensor_xy, float *const restrict tensor_yy, float *const restrict scratch_lin, float *const restrict scratch_quad, const int region_w, const int region_h, const float k)
static __DT_CLONE_TARGETS__ void _cf_fill_relax_2(float *const restrict field, float *const restrict tmp, const uint8_t *const restrict level_anchor, const float *const restrict edge_weights, const float *const restrict edge_weight_sum, const int coarse_w, const int coarse_h, const size_t cell_count, const int steered)
static __DT_CLONE_TARGETS__ void _cf_harmonic_fill_n(float *const restrict *vals, const int n_planes_in, const uint8_t *const restrict hole, const int region_w, const int region_h, const int base_ds, const float *const restrict steer, const dt_dev_pixelpipe_t *pipe)
#define CF_M2(nb_index, coef_a, coef_b)
cl_int _cf_joint_stage_cl(const int devid, void *gd_void, cl_mem estimate, cl_mem valid, cl_mem model_quality, cl_mem mom0, cl_mem mom1, cl_mem mom2, cl_mem steer, const float *const restrict channel_means, const int region_w, const int region_h, const float cf_sigma, const float cf_fmin, const int c, const int guide1, const int guide2)
static __DT_CLONE_TARGETS__ void _cf_fill_relax_1(float *const restrict field, float *const restrict tmp, const uint8_t *const restrict level_anchor, const float *const restrict edge_weights, const float *const restrict edge_weight_sum, const int coarse_w, const int coarse_h, const size_t cell_count, const int steered)
static cl_int _cf_joint_fit_cl(const int devid, void *gd_void, cl_mem estimate, cl_mem valid, cl_mem mom0, cl_mem mom1, cl_mem mom2, cl_mem steer, const float *const restrict channel_means, const int region_w, const int region_h, const float cf_sigma, const float cf_fmin, const int c, const int guide1, const int guide2, cl_mem *ca_out, cl_mem *cb_out, cl_mem *cd_out, cl_mem *cr2_out)
void _cf_harmonic_fill(float *const restrict val, const uint8_t *const restrict hole, const int region_w, const int region_h, const int base_ds, const float *const restrict steer, const dt_dev_pixelpipe_t *pipe)
cl_int _hf_stage_cl(const int devid, void *gd_void, cl_mem estimate, cl_mem valid, cl_mem model_quality, cl_mem luminance, cl_mem steer, dt_gaussian_cl_t *gaussian, const int region_w, const int region_h, const float cf_sigma, const float cf_fmin, const float cf_binv)
#define DEFINE_CF_FILL_RELAX(NP)
static const float x
const dt_colormatrix_t dt_aligned_pixel_t out
const float delta
static void weight(const float *c1, const float *c2, const float sharpen, dt_aligned_pixel_t weight)
Definition eaw.c:29
static float gaussian(float x, float std)
Definition filmic.c:404
cl_int dt_gaussian_blur_cl(dt_gaussian_cl_t *g, cl_mem dev_in, cl_mem dev_out)
Definition gaussian.c:453
static float kernel(const float *x, const float *y)
static void _knee_blur(const float *const restrict in, float *const restrict out, const int width, const int height, const float sigma)
Definition knee.h:31
@ DT_DEBUG_ALWAYS
Definition logging.h:49
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
float *const restrict luminance
float *const restrict const size_t k
size_t size
Definition mipmap_cache.c:3
int dt_opencl_enqueue_kernel_2d(const int dev, const int kernel, const size_t *sizes)
Definition opencl.c:2554
void * dt_opencl_alloc_device_buffer(const int devid, const size_t size)
Definition opencl.c:2970
int dt_opencl_enqueue_copy_buffer_to_image(const int devid, cl_mem src_buffer, cl_mem dst_image, size_t offset, size_t *origin, size_t *region)
Definition opencl.c:2702
void * dt_opencl_alloc_device(const int devid, const int width, const int height, const int bpp)
Definition opencl.c:2894
int dt_opencl_set_kernel_arg(const int dev, const int kernel, const int num, const size_t size, const void *arg)
Definition opencl.c:2545
int dt_opencl_enqueue_copy_image_to_buffer(const int devid, cl_mem src_image, cl_mem dst_buffer, size_t *origin, size_t *region, size_t offset)
Definition opencl.c:2690
int dt_opencl_enqueue_kernel_2d_with_local(const int dev, const int kernel, const size_t *sizes, const size_t *local)
Definition opencl.c:2560
void dt_opencl_release_mem_object(cl_mem mem)
Definition opencl.c:2805
#define DT_OPENCL_DEFAULT_ERROR
Definition opencl.h:61
#define ROUNDUPDHT(a, b)
Definition opencl.h:86
#define ROUNDUPDWD(a, b)
Definition opencl.h:85
#define dt_pixelpipe_cache_alloc_align(size, pipe)
#define dt_pixelpipe_cache_free_align(mem)
#define dt_pixelpipe_cache_alloc_align_float(pixels, pipe)
#define DT_HL_FILL_CL_MAXP
#define DT_HL_CF_K
#define DT_HL_FILL_MAXP
#define CF_EDGE_RANGE
#define CF_EDGE_GUIDE_SIGMA
static float _hl_floor_worth(const float chroma_gain)
static float _hl_region_worth(const float region_benefit)
#define HL_PFOR(...)
const _hl_region_t * region
const dt_dev_pixelpipe_t * pipe
#define __DT_CLONE_TARGETS__
typedef double((*spd)(unsigned long int wavelength, double TempK))
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29