Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
dome.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// Biharmonic luminance dome solve (CPU + OpenCL). (implementation; see dome.h for the public API.)
20
21#include "system/openmp.h"
24#include "math/choleski.h"
25#include "iop/highlights/dome.h"
26#include "iop/highlights/pde.h"
27#include <math.h>
28#include <string.h>
29
30// ===== well-posedness: restrict the solve to the sub-domain enclosed by anchors ===============
31// The restricted biharmonic A = R Delta^2 R' factors as (Delta R')'(Delta R'), hence is positive
32// SEMI-definite -- but only while every one of the 13 stencil taps lands inside the grid. Where a
33// hole cell sits within 2 cells of the grid border its outer taps are CLAMPED back inside, and the
34// clamped 13-point kernel is NOT the square of the clamped Laplacian: the assembled matrix loses
35// positive-definiteness (measured: 6 to 9 non-positive eigenvalues), so the sparse up-looking
36// factorization aborts on a non-positive pivot AND the dense Cholesky returns NaNs, and the dome
37// silently degrades to a flat anchor-mean fill.
38//
39// Callers whose hole is the CLIPPED ZONE never meet this: _segment_clipped_regions pads the region
40// window with valid data, so the hole is enclosed (measured: 0-3% of the grid border). But
41// _chromaticity_gradient's hole is the COMPLEMENT of a sparse anchor set (bright AND fully valid
42// AND clear of the guard ring), which covers essentially the whole border -- measured 88-100% of
43// it on issue #1094's file, where all 90 of its domes failed.
44//
45// The cure is to solve only where the problem is an INTERPOLATION: a cell stays an unknown when an
46// anchor lies at least 2 cells away along each of the four axis directions ("inside the anchor
47// hull"), which puts every tap in range and makes the assembly the true restricted Delta^2. Cells
48// outside the hull are extrapolation; they are demoted to Dirichlet data, keeping their own
49// downsampled value where they have one and taking the anchor mean where they do not (the same
50// last-resort value this function already used for the entire hole when the solve failed). Four
51// running sweeps, O(coarse_pixels). Returns the number of cells demoted.
52static size_t _dome_restrict_to_anchor_hull(uint8_t *const restrict coarse_hole,
53 float *const restrict coarse_field, const int coarse_w,
54 const int coarse_h, const dt_dev_pixelpipe_t *pipe)
55{
56 const size_t coarse_pixels = (size_t)coarse_w * coarse_h;
57 uint8_t *const restrict hull = (uint8_t *)dt_pixelpipe_cache_alloc_align(coarse_pixels, pipe);
58 if(!hull) return 0; // no scratch: leave the system as it is, the caller's fallbacks still apply
59
60 // hull = AND over the four directions of "an anchor lies >= 2 cells back along this ray"
61 for(size_t i = 0; i < coarse_pixels; i++) hull[i] = 1;
62 for(int y = 0; y < coarse_h; y++)
63 {
64 int seen_left = 0, seen_right = 0;
65 for(int x = 0; x < coarse_w; x++)
66 {
67 if(x >= 2 && !coarse_hole[(size_t)y * coarse_w + x - 2]) seen_left = 1;
68 hull[(size_t)y * coarse_w + x] &= seen_left;
69 const int xr = coarse_w - 1 - x;
70 if(xr + 2 < coarse_w && !coarse_hole[(size_t)y * coarse_w + xr + 2]) seen_right = 1;
71 hull[(size_t)y * coarse_w + xr] &= seen_right;
72 }
73 }
74 for(int x = 0; x < coarse_w; x++)
75 {
76 int seen_up = 0, seen_down = 0;
77 for(int y = 0; y < coarse_h; y++)
78 {
79 if(y >= 2 && !coarse_hole[(size_t)(y - 2) * coarse_w + x]) seen_up = 1;
80 hull[(size_t)y * coarse_w + x] &= seen_up;
81 const int yd = coarse_h - 1 - y;
82 if(yd + 2 < coarse_h && !coarse_hole[(size_t)(yd + 2) * coarse_w + x]) seen_down = 1;
83 hull[(size_t)yd * coarse_w + x] &= seen_down;
84 }
85 }
86
87 double anchor_sum = 0.0;
88 size_t anchor_count = 0;
89 for(size_t i = 0; i < coarse_pixels; i++)
90 if(!coarse_hole[i])
91 {
92 anchor_sum += coarse_field[i];
93 anchor_count++;
94 }
95 const float anchor_mean = anchor_count ? (float)(anchor_sum / (double)anchor_count) : 0.f;
96
97 size_t demoted = 0;
98 for(size_t i = 0; i < coarse_pixels; i++)
99 if(coarse_hole[i] && !hull[i])
100 {
101 coarse_hole[i] = 0;
102 // a coarse cell whose block held no valid pixel at all downsampled to 0 (see the box
103 // downsample); 0 is "no data", not data, and must never become a Dirichlet value
104 if(coarse_field[i] == 0.f) coarse_field[i] = anchor_mean;
105 demoted++;
106 }
107
109 return demoted;
110}
111
113void _biharmonic_dome(float *const restrict field, const uint8_t *const restrict hole, const int region_w,
114 const int region_h, const int forced_downsample, const dt_dev_pixelpipe_t *pipe)
115{
116 const size_t region_pixels = (size_t)region_w * region_h;
117 size_t n_hole_fine = 0;
118 for(size_t i = 0; i < region_pixels; i++)
119 if(hole[i]) n_hole_fine++;
120 if(n_hole_fine == 0) return;
121
122 // pick a downsampling factor so the coarse hole has at most ~DT_HL_DOME_NMAX unknowns (the dense
123 // Cholesky is O(N^3)). Raise DT_HL_DOME_NMAX to make the dome grid finer / exact (downsample -> 1)
124 // at more cost -- a quick way to test whether the coarse approximation matters for a given image.
125 const int max_unknowns = DT_HL_DOME_NMAX_SPARSE;
126 // The caller may force the factor (forced_downsample > 0) so several per-channel domes share ONE
127 // grid resolution. With a per-channel factor (each channel picking its own from its own hole size)
128 // the three domes are approximated at different scales, their ratio drifts, and a saturated colour
129 // collapses off-hue. forced_downsample == 0 keeps the standalone behaviour (auto from this hole).
130 int downsample = (forced_downsample > 0) ? forced_downsample
131 : MAX(1, (int)ceilf(sqrtf((float)n_hole_fine / (float)max_unknowns)));
132 int coarse_w = (region_w + downsample - 1) / downsample;
133 int coarse_h = (region_h + downsample - 1) / downsample;
134 const size_t coarse_pixels = (size_t)coarse_w * coarse_h;
135
136 float *const restrict coarse_field = dt_pixelpipe_cache_alloc_align_float(coarse_pixels, pipe);
137 uint8_t *const restrict coarse_hole
138 = (uint8_t *)dt_pixelpipe_cache_alloc_align(sizeof(uint8_t) * coarse_pixels, pipe);
139 int *const restrict coarse_index = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * coarse_pixels, pipe);
140 if(!coarse_field || !coarse_hole || !coarse_index)
141 {
142 dt_pixelpipe_cache_free_align(coarse_field);
144 dt_pixelpipe_cache_free_align(coarse_index);
145 return;
146 }
147
148 // box-downsample: coarse value = mean of the block's VALID (non-hole) fine pixels; a coarse cell
149 // is a hole if the majority of its block is hole (so boundary cells keep real rim data)
150 __OMP_PARALLEL_FOR__(collapse(2))
151 for(int coarse_y = 0; coarse_y < coarse_h; coarse_y++)
152 for(int coarse_x = 0; coarse_x < coarse_w; coarse_x++)
153 {
154 double accum = 0.0;
155 int n_valid = 0, n_hole_block = 0, n_total = 0;
156 for(int fine_y = coarse_y * downsample; fine_y < MIN((coarse_y + 1) * downsample, region_h); fine_y++)
157 for(int fine_x = coarse_x * downsample; fine_x < MIN((coarse_x + 1) * downsample, region_w); fine_x++)
158 {
159 const size_t fine_index = (size_t)fine_y * region_w + fine_x;
160 n_total++;
161 if(hole[fine_index])
162 {
163 n_hole_block++;
164 }
165 else
166 {
167 accum += field[fine_index];
168 n_valid++;
169 }
170 }
171 const size_t coarse_i = (size_t)coarse_y * coarse_w + coarse_x;
172 coarse_hole[coarse_i] = (2 * n_hole_block > n_total) ? 1 : 0;
173 coarse_field[coarse_i] = (n_valid > 0) ? (float)(accum / n_valid) : 0.f;
174 }
175
176 // keep the solve inside the anchor hull, so the assembled Delta^2 is the true restricted
177 // operator and the direct solvers stay well-posed (see _dome_restrict_to_anchor_hull)
178 _dome_restrict_to_anchor_hull(coarse_hole, coarse_field, coarse_w, coarse_h, pipe);
179
180 // enumerate coarse hole unknowns
181 int n_unknowns = 0;
182 for(size_t coarse_i = 0; coarse_i < coarse_pixels; coarse_i++)
183 coarse_index[coarse_i] = coarse_hole[coarse_i] ? n_unknowns++ : -1;
184
185 if(n_unknowns > 0)
186 {
187 // 13-point Delta^2 stencil (Laplacian of the 5-point Laplacian): the discrete biharmonic
188 // operator Delta^2 u = Delta(Delta u), reaching TWO rings out (hence the +-2 taps and the
189 // 2-ring Dirichlet). Weights {20,-8,-8,-8,-8, 2,2,2,2, 1,1,1,1} = the standard 5-point
190 // Laplacian convolved with itself (center 20, edge -8, diagonal 2, far-axis 1).
191 const int stencil_dy[13] = { 0, -1, 1, 0, 0, -1, -1, 1, 1, -2, 2, 0, 0 };
192 const int stencil_dx[13] = { 0, 0, 0, -1, 1, -1, 1, -1, 1, 0, 0, -2, 2 };
193 const float stencil_weight[13] = { 20.f, -8.f, -8.f, -8.f, -8.f, 2.f, 2.f, 2.f, 2.f, 1.f, 1.f, 1.f, 1.f };
194 int solved = 0;
195
196 // ---- sparse direct solve (the DT_HL_DOME_NMAX_SPARSE-sized grid) ----
197 {
198 int *unknown_x = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * n_unknowns, pipe);
199 int *unknown_y = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * n_unknowns, pipe);
200 int *permutation = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * n_unknowns, pipe);
201 int *inverse_perm = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * n_unknowns, pipe);
202 int *matrix_col_ptr = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * (n_unknowns + 1), pipe);
203 double *right_hand_side = (double *)dt_pixelpipe_cache_alloc_align(sizeof(double) * n_unknowns, pipe);
204 int *matrix_row_index = NULL;
205 double *matrix_values = NULL;
206
207 if(unknown_x && unknown_y && permutation && inverse_perm && matrix_col_ptr && right_hand_side)
208 {
209 for(size_t coarse_i = 0; coarse_i < coarse_pixels; coarse_i++)
210 if(coarse_hole[coarse_i])
211 {
212 unknown_x[coarse_index[coarse_i]] = (int)(coarse_i % coarse_w);
213 unknown_y[coarse_index[coarse_i]] = (int)(coarse_i / coarse_w);
214 }
215
216 for(int i = 0; i < n_unknowns; i++) permutation[i] = i;
217 _sp_nd_order(permutation, n_unknowns, unknown_x, unknown_y, 2);
218 for(int perm_index = 0; perm_index < n_unknowns; perm_index++)
219 inverse_perm[permutation[perm_index]] = perm_index;
220
221 // assembly (count pass, then fill), upper triangle, permuted indexing; border-clamped
222 // rows keep the later-eliminated unknown's row value, matching the dense solver's
223 // lower-triangle convention (see the same note in _sp_pde_assemble)
224 int success = 1;
225 int targets[13];
226 double target_weights[13];
227
228 for(int pass = 0; pass < 2 && success; pass++)
229 {
230 if(pass == 1)
231 {
232 int total = 0;
233 for(int perm_index = 0; perm_index < n_unknowns; perm_index++)
234 {
235 const int col_count = matrix_col_ptr[perm_index];
236 matrix_col_ptr[perm_index] = total;
237 total += col_count;
238 }
239 matrix_col_ptr[n_unknowns] = total;
240 matrix_row_index = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * total, pipe);
241 matrix_values = (double *)dt_pixelpipe_cache_alloc_align(sizeof(double) * total, pipe);
242 if(!matrix_row_index || !matrix_values) success = 0;
243 }
244
245 for(int perm_index = 0; perm_index < n_unknowns && success; perm_index++)
246 {
247 const int coarse_y = unknown_y[permutation[perm_index]];
248 const int coarse_x = unknown_x[permutation[perm_index]];
249
250 // row of the 13-point stencil at (coarse_y, coarse_x), clamped, duplicates summed:
251 // one row of Delta^2 u = 0 restricted to the hole unknowns
252 int count = 0;
253 double boundary_sum = 0.0;
254 for(int k = 0; k < 13; k++)
255 {
256 const int neighbour_y = CLAMP(coarse_y + stencil_dy[k], 0, coarse_h - 1);
257 const int neighbour_x = CLAMP(coarse_x + stencil_dx[k], 0, coarse_w - 1);
258 const size_t neighbour_i = (size_t)neighbour_y * coarse_w + neighbour_x;
259 if(!coarse_hole[neighbour_i])
260 {
261 // Dirichlet boundary term: a non-hole neighbour is fixed data (u|dOmega = u_valid),
262 // so its stencil contribution moves to the RHS as -weight * u_valid
263 boundary_sum -= (double)stencil_weight[k] * coarse_field[neighbour_i];
264 continue;
265 }
266 const int target = neighbour_y * coarse_w + neighbour_x;
267 int slot = 0;
268 for(; slot < count; slot++)
269 if(targets[slot] == target)
270 {
271 target_weights[slot] += stencil_weight[k];
272 break;
273 }
274 if(slot == count)
275 {
276 targets[count] = target;
277 target_weights[count] = stencil_weight[k];
278 count++;
279 }
280 }
281 if(pass == 1) right_hand_side[perm_index] = boundary_sum;
282
283 int n_col_entries = 0;
284 for(int slot = 0; slot < count; slot++)
285 {
286 const int target_row = inverse_perm[coarse_index[targets[slot]]];
287 if(target_row > perm_index) continue;
288 // border rows: keep the row value (the dense solver's lower-triangle convention)
289 const double value = target_weights[slot];
290 if(pass == 1)
291 {
292 matrix_row_index[matrix_col_ptr[perm_index] + n_col_entries] = target_row;
293 matrix_values[matrix_col_ptr[perm_index] + n_col_entries] = value;
294 }
295 n_col_entries++;
296 }
297 if(pass == 0) matrix_col_ptr[perm_index] = n_col_entries;
298 }
299 }
300
301 if(success)
302 {
303 // solve the restricted biharmonic system A u = b (A = Delta^2 over the hole unknowns,
304 // b = boundary_sum). A is symmetric positive-definite, so the sparse Cholesky applies
305 // (SPD factorization annotated in common/solvers/sparse_cholesky.h); a DIRECT solve is
306 // exact regardless of conditioning, unlike CG which stalls in float at kappa ~ L^4.
307 _sp_chol_t *factor = _sp_chol_factor(n_unknowns, matrix_col_ptr, matrix_row_index, matrix_values, pipe->type);
308 if(factor)
309 {
310 _sp_chol_solve(factor, right_hand_side);
311 for(size_t coarse_i = 0; coarse_i < coarse_pixels; coarse_i++)
312 if(coarse_hole[coarse_i])
313 coarse_field[coarse_i] = (float)right_hand_side[(size_t)inverse_perm[coarse_index[coarse_i]]];
314 solved = 1;
316 }
317 }
318 }
319
323 dt_pixelpipe_cache_free_align(inverse_perm);
324 dt_pixelpipe_cache_free_align(matrix_col_ptr);
325 dt_pixelpipe_cache_free_align(matrix_row_index);
326 dt_pixelpipe_cache_free_align(matrix_values);
327 dt_pixelpipe_cache_free_align(right_hand_side);
328 }
329
330 if(!solved && n_unknowns <= DT_HL_DOME_NMAX)
331 {
332 // dense fallback (previous solver), only affordable on the small dense-era grids
333 float *const restrict matrix = dt_pixelpipe_cache_alloc_align_float((size_t)n_unknowns * n_unknowns, pipe);
334 float *const restrict right_hand_side = dt_pixelpipe_cache_alloc_align_float((size_t)n_unknowns, pipe);
335 if(matrix && right_hand_side)
336 {
337 memset(matrix, 0, (size_t)n_unknowns * n_unknowns * sizeof(float));
339 for(int coarse_y = 0; coarse_y < coarse_h; coarse_y++)
340 for(int coarse_x = 0; coarse_x < coarse_w; coarse_x++)
341 {
342 const size_t coarse_i = (size_t)coarse_y * coarse_w + coarse_x;
343 if(!coarse_hole[coarse_i]) continue;
344 const int unknown_index = coarse_index[coarse_i];
345 float boundary_sum = 0.f;
346 for(int k = 0; k < 13; k++)
347 {
348 const int neighbour_y = CLAMP(coarse_y + stencil_dy[k], 0, coarse_h - 1);
349 const int neighbour_x = CLAMP(coarse_x + stencil_dx[k], 0, coarse_w - 1);
350 const size_t neighbour_i = (size_t)neighbour_y * coarse_w + neighbour_x;
351 if(coarse_hole[neighbour_i])
352 matrix[(size_t)unknown_index * n_unknowns + coarse_index[neighbour_i]] += stencil_weight[k];
353 else
354 boundary_sum -= stencil_weight[k] * coarse_field[neighbour_i];
355 }
356 right_hand_side[unknown_index] = boundary_sum;
357 }
358
359 // direct SPD solve (dense Cholesky) of the same restricted Delta^2 u = 0 system, only for
360 // the small dense-era grids. right_hand_side holds the solution on return.
361 if(solve_hermitian(matrix, right_hand_side, (size_t)n_unknowns, TRUE) == 0)
362 {
363 for(size_t coarse_i = 0; coarse_i < coarse_pixels; coarse_i++)
364 if(coarse_hole[coarse_i]) coarse_field[coarse_i] = right_hand_side[coarse_index[coarse_i]];
365 solved = 1;
366 }
367 }
369 dt_pixelpipe_cache_free_align(right_hand_side);
370 }
371
372 if(!solved)
373 {
374 // last resort (OOM): fill the coarse hole with the anchor mean -- never leave the zeroed
375 // hole cells to be upsampled as a black dome. Say so: this replaces the dome with a
376 // CONSTANT, which is a visible loss, and it used to be reached silently whenever
377 // n_unknowns exceeded the dense fallback's DT_HL_DOME_NMAX (only the smaller systems ever
378 // reported anything, through the solver's own NaN message -- see issue #1094).
380 "[highlights] dome: the %d-unknown biharmonic solve failed, filling the hole flat\n",
381 n_unknowns);
382 double anchor_sum = 0.0;
383 size_t anchor_count = 0;
384 for(size_t coarse_i = 0; coarse_i < coarse_pixels; coarse_i++)
385 if(!coarse_hole[coarse_i])
386 {
387 anchor_sum += coarse_field[coarse_i];
388 anchor_count++;
389 }
390 const float anchor_mean = anchor_count ? (float)(anchor_sum / (double)anchor_count) : 0.f;
391 for(size_t coarse_i = 0; coarse_i < coarse_pixels; coarse_i++)
392 if(coarse_hole[coarse_i]) coarse_field[coarse_i] = anchor_mean;
393 }
394 }
395
396 // bilinear-upsample the coarse dome into the fine hole
397 __OMP_PARALLEL_FOR__(collapse(2))
398 for(int y = 0; y < region_h; y++)
399 for(int x = 0; x < region_w; x++)
400 {
401 const size_t fine_index = (size_t)y * region_w + x;
402 if(!hole[fine_index]) continue;
403 const float grid_x = ((float)x + 0.5f) / downsample - 0.5f;
404 const float grid_y = ((float)y + 0.5f) / downsample - 0.5f;
405 const int x_lo = CLAMP((int)floorf(grid_x), 0, coarse_w - 1);
406 const int y_lo = CLAMP((int)floorf(grid_y), 0, coarse_h - 1);
407 const int x_hi = MIN(x_lo + 1, coarse_w - 1);
408 const int y_hi = MIN(y_lo + 1, coarse_h - 1);
409 const float frac_x = CLAMP(grid_x - x_lo, 0.f, 1.f);
410 const float frac_y = CLAMP(grid_y - y_lo, 0.f, 1.f);
411 const float interp_top = coarse_field[(size_t)y_lo * coarse_w + x_lo] * (1.f - frac_x)
412 + coarse_field[(size_t)y_lo * coarse_w + x_hi] * frac_x;
413 const float interp_bottom = coarse_field[(size_t)y_hi * coarse_w + x_lo] * (1.f - frac_x)
414 + coarse_field[(size_t)y_hi * coarse_w + x_hi] * frac_x;
415 field[fine_index] = interp_top * (1.f - frac_y) + interp_bottom * frac_y;
416 }
417
418 dt_pixelpipe_cache_free_align(coarse_field);
420 dt_pixelpipe_cache_free_align(coarse_index);
421}
422
423// ===== anisotropic chroma diffusion (structure-steered, coarse-to-fine) ======================
424// The guided ladder recovers MAGNITUDE well but its chroma carries guide-flip seams and scale
425// hand-off patches. Chromaticity (est_c / L) is a BOUNDED quantity, so interpolation is the right
426// tool for it -- provided it flows ALONG image structure, never across it, or unrelated colours
427// (warm horizon glow vs cool upper sky) mix into magenta. This implements the diffuse.c model on
428// the region buffer: per-pixel diffusion tensor D = t x t + exp(-|grad L|/k) * g x g, where g is
429// the unit gradient of the RECOVERED luminance (content!) and t its orthogonal (the isophote).
430// Explicit iterations only travel ~sqrt(iters) pixels, so a COARSE-TO-FINE pyramid seeds the whole
431// hole at the coarsest level first (the "unreached interior stays magenta" fix), like diffuse.c's
432// multiscale scheme.
433//
434// MATHS BRIDGE -- Step 8 / E_chrominance anisotropic (article §"The optimization problem" term 3,
435// §"Chrominance coherence", §"The saturation floors, as obstacles"): the whole block minimizes
436// int_Omega grad(r_c)^T D grad(r_c) dOmega subject to the obstacle r_c >= c0/L_sum, whose
437// Euler-Lagrange (unconstrained) is the divergence-form steered fill div(D grad r) = 0. D here is
438// the structure-steered tensor built from the recovered luminance: gradient-dominant on a clean
439// halo ramp (transport radially inward), isophote-dominant where a hard edge crosses (transport
440// along level lines, never across a boundary). r = RGB/L_sum, recombined RGB = L_sum * r.
441
442// ============================ OpenCL ============================
443
444#if defined(HAVE_OPENCL) && DT_HL_SPARSE_SOLVE
445cl_int _biharmonic_dome_cl(const int devid, void *gd_void, cl_mem field, cl_mem hole, const int region_w,
446 const int region_h, const int downsample, const dt_dev_pixelpipe_t *pipe)
447{
449 const int coarse_w = (region_w + downsample - 1) / downsample;
450 const int coarse_h = (region_h + downsample - 1) / downsample;
451 const size_t coarse_pixels = (size_t)coarse_w * coarse_h;
452 cl_int cl_err = DT_OPENCL_DEFAULT_ERROR;
453
454 cl_mem dval = dt_opencl_alloc_device_buffer(devid, sizeof(float) * coarse_pixels);
455 cl_mem dhole = dt_opencl_alloc_device_buffer(devid, coarse_pixels);
456 float *cf = dt_pixelpipe_cache_alloc_align_float(coarse_pixels, pipe);
457 uint8_t *coarse_hole = (uint8_t *)dt_pixelpipe_cache_alloc_align(coarse_pixels, pipe);
458 int *idx = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * coarse_pixels, pipe);
459 _sp_chol_cl_t *factor = NULL;
460 double *rhs = NULL;
461 int *matrix_col_ptr = NULL, *matrix_row_index = NULL;
462 double *matrix_values = NULL;
463 cl_mem solution_device = NULL;
464 if(!dval || !dhole || !cf || !coarse_hole || !idx) goto out;
465
466 // coarse-grid reduction on device: average the full-res field/hole into the ds-downsampled grid
467 {
468 const int kernel = global_data->kernel_hl_dome_down;
469 size_t work_size[3] = { ROUNDUPDWD(coarse_w, devid), ROUNDUPDHT(coarse_h, devid), 1 };
470 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &field);
471 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &hole);
472 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &dval);
473 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(cl_mem), &dhole);
474 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &region_w);
475 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &region_h);
476 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &coarse_w);
477 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &coarse_h);
478 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &downsample);
479 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_size);
480 if(cl_err != CL_SUCCESS) goto out;
481 }
482
483 // coarse metadata to host: assembly + symbolic analysis (integer work)
484 cl_err = dt_opencl_read_buffer_from_device(devid, cf, dval, 0, sizeof(float) * coarse_pixels, CL_TRUE);
485 if(cl_err != CL_SUCCESS) goto out;
486 cl_err = dt_opencl_read_buffer_from_device(devid, coarse_hole, dhole, 0, coarse_pixels, CL_TRUE);
487 if(cl_err != CL_SUCCESS) goto out;
488
489 // same well-posedness restriction as the CPU dome, on the same host-side coarse mask, so both
490 // paths assemble the identical system (the device `dhole` is not read again from here on)
491 _dome_restrict_to_anchor_hull(coarse_hole, cf, coarse_w, coarse_h, pipe);
492
493 // number the coarse hole cells: these are the unknowns of the linear system
494 int unknown_count = 0;
495 for(size_t coarse_index = 0; coarse_index < coarse_pixels; coarse_index++)
496 idx[coarse_index] = coarse_hole[coarse_index] ? unknown_count++ : -1;
497 // Nh == 0 (no coarse cell reached hole majority -- thin streaks, speckle holes): skip the
498 // solve but STILL upsample the coarse block means into the fine holes, exactly like the CPU
499 // dome, whose bilinear upsample runs unconditionally. Early-exiting here left the field
500 // untouched and diverged from the CPU on thin-hole topologies.
501 if(unknown_count > 0)
502 {
503
504 {
505 // assemble the 13-point biharmonic operator Delta^2 = Delta(Delta) (the 5-point Laplacian
506 // convolved with itself: center 20, edge -8, diagonal 2, far-axis 1; reaches two rings out),
507 // with the unknowns permuted by geometric nested dissection (the CPU dome's exact system)
508 static const int stencil_off_y[13] = { 0, -1, 1, 0, 0, -1, -1, 1, 1, -2, 2, 0, 0 };
509 static const int stencil_off_x[13] = { 0, 0, 0, -1, 1, -1, 1, -1, 1, 0, 0, -2, 2 };
510 static const double stencil_coef[13] = { 20., -8., -8., -8., -8., 2., 2., 2., 2., 1., 1., 1., 1. };
511
512 int *unknown_x = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * unknown_count, pipe);
513 int *unknown_y = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * unknown_count, pipe);
514 int *perm = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * unknown_count, pipe);
515 int *inv_perm = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * unknown_count, pipe);
516 matrix_col_ptr = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * (unknown_count + 1), pipe);
517 matrix_row_index = (int *)dt_pixelpipe_cache_alloc_align(sizeof(int) * (size_t)unknown_count * 13, pipe);
518 matrix_values = (double *)dt_pixelpipe_cache_alloc_align(sizeof(double) * (size_t)unknown_count * 13, pipe);
519 rhs = (double *)dt_pixelpipe_cache_alloc_align(sizeof(double) * unknown_count, pipe);
520 int alloc_ok = (unknown_x && unknown_y && perm && inv_perm && matrix_col_ptr && matrix_row_index
521 && matrix_values && rhs);
522 if(alloc_ok)
523 {
524 for(size_t coarse_index = 0; coarse_index < coarse_pixels; coarse_index++)
525 if(coarse_hole[coarse_index])
526 {
527 unknown_x[idx[coarse_index]] = (int)(coarse_index % coarse_w);
528 unknown_y[idx[coarse_index]] = (int)(coarse_index / coarse_w);
529 }
530 for(int i = 0; i < unknown_count; i++) perm[i] = i;
531 _sp_nd_order(perm, unknown_count, unknown_x, unknown_y, 2);
532 for(int perm_index = 0; perm_index < unknown_count; perm_index++) inv_perm[perm[perm_index]] = perm_index;
533
534 int n_nonzero = 0;
535 for(int perm_index = 0; perm_index < unknown_count; perm_index++)
536 {
537 const int cell_y = unknown_y[perm[perm_index]], cell_x = unknown_x[perm[perm_index]];
538 matrix_col_ptr[perm_index] = n_nonzero;
539 double rhs_accum = 0.0;
540 for(int stencil = 0; stencil < 13; stencil++)
541 {
542 const int neighbor_y = CLAMP(cell_y + stencil_off_y[stencil], 0, coarse_h - 1);
543 const int neighbor_x = CLAMP(cell_x + stencil_off_x[stencil], 0, coarse_w - 1);
544 const size_t neighbor_index = (size_t)neighbor_y * coarse_w + neighbor_x;
545 if(!coarse_hole[neighbor_index])
546 {
547 // Dirichlet boundary: a non-hole neighbour is fixed data (u|dOmega = u_valid), so its
548 // stencil term moves to the RHS as -coef * u_valid
549 rhs_accum -= stencil_coef[stencil] * cf[neighbor_index];
550 continue;
551 }
552 const int row_index = inv_perm[idx[neighbor_index]];
553 if(row_index > perm_index) continue;
554 int fill_index = matrix_col_ptr[perm_index];
555 for(; fill_index < n_nonzero; fill_index++)
556 if(matrix_row_index[fill_index] == row_index)
557 {
558 matrix_values[fill_index] += stencil_coef[stencil];
559 break;
560 }
561 if(fill_index == n_nonzero)
562 {
563 matrix_row_index[n_nonzero] = row_index;
564 matrix_values[n_nonzero] = stencil_coef[stencil];
565 n_nonzero++;
566 }
567 }
568 rhs[perm_index] = rhs_accum;
569 }
570 matrix_col_ptr[unknown_count] = n_nonzero;
571
572 // factor + solve A u = b, A = the restricted Delta^2 (SPD), b = the boundary_sum RHS:
573 // the exact biharmonic dome on the coarse hole (GPU sparse Cholesky)
574 factor = _sp_chol_factor_cl(devid, _hl_sp_chol_kernels(gd_void), unknown_count, matrix_col_ptr,
575 matrix_row_index, matrix_values);
576 int solved = 0;
577 if(factor)
578 {
579 cl_mem rhs_device = _sp_cl_upload(devid, rhs, sizeof(double) * unknown_count);
580 if(rhs_device && !_sp_chol_solve_cl(factor, _hl_sp_chol_kernels(gd_void), rhs_device)
581 && dt_opencl_read_buffer_from_device(devid, rhs, rhs_device, 0, sizeof(double) * unknown_count,
582 CL_TRUE)
583 == CL_SUCCESS)
584 {
585 // the GPU factorization does not abort on a non-positive pivot the way the CPU
586 // up-looking factor does -- it silently produces NaN/inf. Validate the solution
587 // like the CPU validates the factor, and take the same fallback chain when the
588 // clamped-border row-assembly breaks SPD on an unlucky hole topology.
589 solved = 1;
590 for(int k = 0; k < unknown_count && solved; k++)
591 if(!isfinite(rhs[k])) solved = 0;
592 if(solved)
593 for(size_t coarse_index = 0; coarse_index < coarse_pixels; coarse_index++)
594 if(coarse_hole[coarse_index]) cf[coarse_index] = (float)rhs[(size_t)inv_perm[idx[coarse_index]]];
595 }
597 }
598
599 if(!solved && unknown_count <= DT_HL_DOME_NMAX)
600 {
601 // dense fallback, exactly the CPU dome's second stage
602 float *const restrict dense_matrix
603 = dt_pixelpipe_cache_alloc_align_float((size_t)unknown_count * unknown_count, pipe);
604 float *const restrict dense_rhs = dt_pixelpipe_cache_alloc_align_float((size_t)unknown_count, pipe);
605 if(dense_matrix && dense_rhs)
606 {
607 memset(dense_matrix, 0, (size_t)unknown_count * unknown_count * sizeof(float));
608 for(int cell_y = 0; cell_y < coarse_h; cell_y++)
609 for(int cell_x = 0; cell_x < coarse_w; cell_x++)
610 {
611 const size_t coarse_index = (size_t)cell_y * coarse_w + cell_x;
612 if(!coarse_hole[coarse_index]) continue;
613 const int k = idx[coarse_index];
614 float rhs_accum = 0.f;
615 for(int stencil = 0; stencil < 13; stencil++)
616 {
617 const int neighbor_y = CLAMP(cell_y + stencil_off_y[stencil], 0, coarse_h - 1);
618 const int neighbor_x = CLAMP(cell_x + stencil_off_x[stencil], 0, coarse_w - 1);
619 const size_t neighbor_index = (size_t)neighbor_y * coarse_w + neighbor_x;
620 if(coarse_hole[neighbor_index])
621 dense_matrix[(size_t)k * unknown_count + idx[neighbor_index]] += stencil_coef[stencil];
622 else
623 rhs_accum -= stencil_coef[stencil] * cf[neighbor_index];
624 }
625 dense_rhs[k] = rhs_accum;
626 }
627 if(solve_hermitian(dense_matrix, dense_rhs, (size_t)unknown_count, TRUE) == 0)
628 {
629 for(size_t coarse_index = 0; coarse_index < coarse_pixels; coarse_index++)
630 if(coarse_hole[coarse_index]) cf[coarse_index] = dense_rhs[idx[coarse_index]];
631 solved = 1;
632 }
633 }
634 dt_pixelpipe_cache_free_align(dense_matrix);
636 }
637
638 if(!solved)
639 {
640 // last resort, exactly the CPU dome's: anchor-mean fill (never upsample a black dome)
642 "[highlights] dome: the %d-unknown biharmonic solve failed, filling the hole flat\n",
643 unknown_count);
644 double asum = 0.0;
645 size_t acnt = 0;
646 for(size_t coarse_index = 0; coarse_index < coarse_pixels; coarse_index++)
647 if(!coarse_hole[coarse_index])
648 {
649 asum += cf[coarse_index];
650 acnt++;
651 }
652 const float amean = acnt ? (float)(asum / (double)acnt) : 0.f;
653 for(size_t coarse_index = 0; coarse_index < coarse_pixels; coarse_index++)
654 if(coarse_hole[coarse_index]) cf[coarse_index] = amean;
655 }
656 cl_err = CL_SUCCESS;
657 }
658 else
664 if(cl_err != CL_SUCCESS) goto out;
665 }
666 }
667
668 // upload the coarse solution and upsample into the full-res holes (hl_fill_up wants the
669 // ANCHOR mask; our `hole` buffer holds holes, so pass an inverted... hl_fill_up tests
670 // anc[i] -> skip: we need "write where hole": pass hole through a dedicated path -- reuse
671 // hl_fill_up by noting its test `if(anc[i]) return` writes where the mask is ZERO: our hole
672 // mask is 1 on holes -> invert on upload? Simplest: hl_fill_up writes where mask==0, so
673 // pass the INVERTED hole mask... we don't have it on device. Use hl_dome_up = hl_fill_up
674 // with the hole convention: kernel reuse trick -- write a tiny inverter is more code than
675 // benefit; instead upload solution and run hl_fill_up with `anc` = a mask we build by one
676 // extra kernel... For now: build the inverted mask on host (we HAVE ch/full-res? no, full
677 // -res hole only on device). Add: reuse hl_fill_jacobi convention... -> dedicated kernel
678 // exists: hl_fill_up(anc) -- we need anc = !hole full-res. One-line kernel would be
679 // cleaner; reuse hl_lsb_hole? No. We add hl_not_mask below in basic.cl? To avoid another
680 // kernel this call allocates an inverted mask via clEnqueue... keep it simple:
681 //
682 // PLAIN-WORDS SUMMARY of the design notes above: upload the coarse solution and upsample
683 // it into the full-res holes. Mask-convention mismatch: hl_fill_up writes only where its
684 // `anc` (anchor) mask is ZERO, i.e. it expects 1 = trusted / 0 = hole, while this function
685 // receives `hole` with 1 = hole. The full-res inverted mask exists nowhere (host or
686 // device), so invert `hole` once on device with the tiny hl_not_mask kernel and feed that
687 // to hl_fill_up.
688 {
689 // inverted mask via a tiny kernel would be ideal; as the region planes also need the
690 // anchor mask elsewhere, callers of _biharmonic_dome_cl pass `hole`; invert here once.
691 solution_device = _sp_cl_upload(devid, cf, sizeof(float) * coarse_pixels);
692 if(!solution_device)
693 {
695 goto out;
696 }
697 {
698 // upsample the coarse dome into the full-resolution hole pixels; the mask is in the
699 // hole convention (1 = fill), which hl_fill_up handles directly via mask_is_hole
700 const int kernel = global_data->kernel_hl_fill_up;
701 const int mask_is_hole = 1;
702 size_t work_size[3] = { ROUNDUPDWD(region_w, devid), ROUNDUPDHT(region_h, devid), 1 };
703 dt_opencl_set_kernel_arg(devid, kernel, 0, sizeof(cl_mem), &field);
704 dt_opencl_set_kernel_arg(devid, kernel, 1, sizeof(cl_mem), &hole);
705 dt_opencl_set_kernel_arg(devid, kernel, 2, sizeof(cl_mem), &solution_device);
706 dt_opencl_set_kernel_arg(devid, kernel, 3, sizeof(int), &region_w);
707 dt_opencl_set_kernel_arg(devid, kernel, 4, sizeof(int), &region_h);
708 dt_opencl_set_kernel_arg(devid, kernel, 5, sizeof(int), &coarse_w);
709 dt_opencl_set_kernel_arg(devid, kernel, 6, sizeof(int), &coarse_h);
710 dt_opencl_set_kernel_arg(devid, kernel, 7, sizeof(int), &downsample);
711 dt_opencl_set_kernel_arg(devid, kernel, 8, sizeof(int), &mask_is_hole);
712 cl_err = dt_opencl_enqueue_kernel_2d(devid, kernel, work_size);
713 }
714 }
715
716out:
719 dt_opencl_release_mem_object(solution_device);
723 dt_pixelpipe_cache_free_align(matrix_col_ptr);
724 dt_pixelpipe_cache_free_align(matrix_row_index);
725 dt_pixelpipe_cache_free_align(matrix_values);
728 return cl_err;
729}
730
731#endif // HAVE_OPENCL && DT_HL_SPARSE_SOLVE
#define TRUE
Definition ashift_lsd.c:162
static int solve_hermitian(const float *const restrict A, float *const restrict y, const size_t n, const int checks)
Definition choleski.h:270
static const float x
const dt_colormatrix_t dt_aligned_pixel_t out
const dt_colormatrix_t matrix
__DT_CLONE_TARGETS__ void _biharmonic_dome(float *const restrict field, const uint8_t *const restrict hole, const int region_w, const int region_h, const int forced_downsample, const dt_dev_pixelpipe_t *pipe)
Definition dome.c:113
static size_t _dome_restrict_to_anchor_hull(uint8_t *const restrict coarse_hole, float *const restrict coarse_field, const int coarse_w, const int coarse_h, const dt_dev_pixelpipe_t *pipe)
Definition dome.c:52
static int perm[512]
Definition grain.c:174
static int permutation[]
Definition grain.c:160
static float kernel(const float *x, const float *y)
@ DT_DEBUG_PIPE
Definition logging.h:76
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 const size_t k
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_read_buffer_from_device(const int devid, void *host, void *device, const size_t offset, const size_t size, const int blocking)
Definition opencl.c:2727
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
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 __OMP_PARALLEL_FOR__(...)
Definition openmp.h:95
static _sp_chol_cl_kernels_t _hl_sp_chol_kernels(void *gd_void)
Definition pde.h:107
const float factor
Definition pdf.h:91
#define dt_pixelpipe_cache_alloc_align(size, pipe)
#define dt_pixelpipe_cache_free_align(mem)
#define dt_pixelpipe_cache_alloc_align_float(pixels, pipe)
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
static void _sp_nd_order(int *const restrict unknown_ids, const int count, const int *const restrict unknown_x, const int *const restrict unknown_y, const int reach)
static void _sp_chol_free(_sp_chol_t *factor)
static void _sp_chol_solve(const _sp_chol_t *const factor, double *const restrict rhs)
static _sp_chol_t * _sp_chol_factor(const int dimension, const int *const restrict matrix_col_ptr, const int *const restrict matrix_row_index, const double *const restrict matrix_values, const int cache_id)
static void _sp_chol_cl_free(_sp_chol_cl_t *factor)
static _sp_chol_cl_t * _sp_chol_factor_cl(const int devid, const _sp_chol_cl_kernels_t kernels, const int dimension, const int *const restrict matrix_col_ptr, const int *const restrict matrix_row_index, const double *const restrict matrix_values)
static int _sp_chol_solve_cl(const _sp_chol_cl_t *const factor, const _sp_chol_cl_kernels_t kernels, cl_mem rhs)
static cl_mem _sp_cl_upload(const int devid, const void *data, const size_t bytes)
#define DT_HL_DOME_NMAX_SPARSE
#define DT_HL_DOME_NMAX
dt_dev_pixelpipe_type_t type
#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