Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
iop/drawlayer/brush.c
Go to the documentation of this file.
1/*
2 This file is part of the Ansel project.
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 Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19/*
20 * drawlayer public brush API implementation
21 *
22 * Self-contained dab-level rasterization and profile math.
23 * This file is intentionally independent from drawlayer module internals.
24 */
25
26#include "iop/drawlayer/brush.h"
27#include "iop/drawlayer/cache.h"
28#include "iop/drawlayer/paint.h"
30
31#include "common/darktable.h"
33
34#include <math.h>
35#include <string.h>
36
38static inline float _clamp01(const float v)
39{
40 return fminf(fmaxf(v, 0.0f), 1.0f);
41}
42
44static inline float _lerpf(const float a, const float b, const float t)
45{
46 return a + (b - a) * t;
47}
48
50static inline float _cell_hash01_from_seed(const uint64_t cell_seed, const uint64_t salt)
51{
52 return (float)splitmix32(cell_seed ^ salt) / (float)UINT32_MAX;
53}
54
56static inline float _cellular_grain_2d(const uint64_t seed, const float x, const float y)
57{
58 const int cell_x = (int)floorf(x);
59 const int cell_y = (int)floorf(y);
60 float accum = 0.0f;
61 float weight_sum = 0.0f;
62
63 for(int oy = -1; oy <= 1; oy++)
64 {
65 for(int ox = -1; ox <= 1; ox++)
66 {
67 const int ix = cell_x + ox;
68 const int iy = cell_y + oy;
69 const uint64_t cell_seed = seed
70 ^ ((uint64_t)(uint32_t)ix * 0x9e3779b185ebca87ull)
71 ^ ((uint64_t)(uint32_t)iy * 0xc2b2ae3d27d4eb4full);
72 const float jitter_x = _cell_hash01_from_seed(cell_seed, 0x94d049bb133111ebull);
73 const float jitter_y = _cell_hash01_from_seed(cell_seed, 0xbf58476d1ce4e5b9ull);
74 const float grain_gain = 0.65f + 0.35f * _cell_hash01_from_seed(cell_seed, 0xda942042e4dd58b5ull);
75 const float dx = x - ((float)ix + jitter_x);
76 const float dy = y - ((float)iy + jitter_y);
77 const float dist2 = dx * dx + dy * dy;
78 const float radius = 0.42f + 0.22f * _cell_hash01_from_seed(cell_seed, 0x369dea0f31a53f85ull);
79 const float grain = fmaxf(0.0f, 1.0f - dist2 / (radius * radius));
80 const float shaped = grain * grain * (3.0f - 2.0f * grain);
81 accum += grain_gain * shaped;
82 weight_sum += grain_gain;
83 }
84 }
85
86 return (weight_sum > 1e-6f) ? _clamp01(accum / weight_sum) : 0.0f;
87}
88
90static inline void _sprinkle_octave_weights(const float coarseness, float *w0, float *w1, float *w2)
91{
92 const float c = 1.0f - _clamp01(coarseness);
93 if(c <= 0.5f)
94 {
95 const float t = c * 2.0f;
96 if(!IS_NULL_PTR(w0)) *w0 = _lerpf(1.0f, 1.0f / 3.0f, t);
97 if(!IS_NULL_PTR(w1)) *w1 = _lerpf(0.0f, 1.0f / 3.0f, t);
98 if(!IS_NULL_PTR(w2)) *w2 = _lerpf(0.0f, 1.0f / 3.0f, t);
99 }
100 else
101 {
102 const float t = (c - 0.5f) * 2.0f;
103 if(!IS_NULL_PTR(w0)) *w0 = _lerpf(1.0f / 3.0f, 0.0f, t);
104 if(!IS_NULL_PTR(w1)) *w1 = _lerpf(1.0f / 3.0f, 0.0f, t);
105 if(!IS_NULL_PTR(w2)) *w2 = _lerpf(1.0f / 3.0f, 1.0f, t);
106 }
107}
108
110static inline float _sprinkle_noise_at_pixel_precomputed(const float px, const float py,
111 const float scale, const float strength,
112 const float w0, const float w1, const float w2,
113 const uint64_t seed0, const uint64_t seed1, const uint64_t seed2)
114{
115 if(strength <= 1e-6f) return 1.0f;
116 const float x = px * scale;
117 const float y = py * scale;
118 const float g0 = (w0 > 1e-6f) ? _cellular_grain_2d(seed0, x, y) : 0.0f;
119 const float g1 = (w1 > 1e-6f) ? _cellular_grain_2d(seed1, x * 1.93f + 4.7f, y * 1.93f - 2.9f) : 0.0f;
120 const float g2 = (w2 > 1e-6f) ? _cellular_grain_2d(seed2, x * 3.71f - 6.2f, y * 3.71f + 8.4f) : 0.0f;
121 const float field = w0 * g0 + w1 * g1 + w2 * g2;
122 const float centered = 2.0f * field - 1.0f;
123 return fmaxf(0.0f, 1.0f + strength * centered);
124}
125
139
141 const float center_x, const float center_y,
142 const float radius,
144{
145 if(IS_NULL_PTR(preview))
146 return;
147
149 .scale = 1.0f,
150 .strength = 0.0f,
151 .w0 = 0.0f,
152 .w1 = 0.0f,
153 .w2 = 0.0f,
154 .seed0 = 0u,
155 .seed1 = 0u,
156 .seed2 = 0u,
157 .gain = 1.0f,
158 .enabled = FALSE,
159 };
160
161 if(IS_NULL_PTR(dab) || dab->sprinkles <= 1e-6f)
162 return;
163
164 preview->scale = 1.0f / fmaxf(dab->sprinkle_size, 1.0f);
165 preview->strength = _clamp01(dab->sprinkles);
166 _sprinkle_octave_weights(dab->sprinkle_coarseness, &preview->w0, &preview->w1, &preview->w2);
167 preview->seed0 = ((uint64_t)dab->stroke_batch << 32) ^ 0x7f4a7c159e3779b9ull;
168 preview->seed1 = preview->seed0 ^ 0xbf58476d1ce4e5b9ull;
169 preview->seed2 = preview->seed0 ^ 0x94d049bb133111ebull;
170 preview->enabled = TRUE;
171
172 float noise_sum = 0.0f;
173 int noise_count = 0;
174 for(int sy = -2; sy <= 2; sy++)
175 {
176 for(int sx = -2; sx <= 2; sx++)
177 {
178 const float nx = 0.4f * (float)sx;
179 const float ny = 0.4f * (float)sy;
180 if(nx * nx + ny * ny > 1.0f) continue;
181 const int pixel_x = (int)lrintf(center_x + nx * radius);
182 const int pixel_y = (int)lrintf(center_y + ny * radius);
183 noise_sum += _sprinkle_noise_at_pixel_precomputed(pixel_x, pixel_y,
184 preview->scale, preview->strength,
185 preview->w0, preview->w1, preview->w2,
186 preview->seed0, preview->seed1, preview->seed2);
187 noise_count++;
188 }
189 }
190
191 if(noise_count > 0)
192 {
193 const float mean_noise = noise_sum / (float)noise_count;
194 preview->gain = (mean_noise > 1e-6f) ? (1.0f / mean_noise) : 1.0f;
195 }
196}
197
199 const float px, const float py)
200{
201 if(IS_NULL_PTR(preview) || !preview->enabled) return 1.0f;
203 preview->scale, preview->strength,
204 preview->w0, preview->w1, preview->w2,
205 preview->seed0, preview->seed1, preview->seed2) * preview->gain;
206}
207
234
236{
237 /* Per-pixel analytic evaluation (profile and resolved alpha controls). */
238 float profile;
244
250 const dt_drawlayer_brush_dab_t *dab,
251 const int origin_x, const int origin_y,
252 const float scale,
254{
255 if(!stroke || IS_NULL_PTR(dab) || IS_NULL_PTR(view) || !stroke->bounds.valid) return FALSE;
256 if(stroke->bounds.se[0] <= stroke->bounds.nw[0]
257 || stroke->bounds.se[1] <= stroke->bounds.nw[1])
258 return FALSE;
259
260 const float dir_len = hypotf(dab->dir_x, dab->dir_y);
261 float sprinkle_w0 = 0.0f, sprinkle_w1 = 0.0f, sprinkle_w2 = 0.0f;
262 _sprinkle_octave_weights(dab->sprinkle_coarseness, &sprinkle_w0, &sprinkle_w1, &sprinkle_w2);
263 const uint64_t sprinkle_seed0 = ((uint64_t)dab->stroke_batch << 32) ^ 0x7f4a7c159e3779b9ull;
264
266 .dab = dab,
267 .bounds = stroke->bounds,
268 .sample_origin_x = origin_x,
269 .sample_origin_y = origin_y,
270 .scaled_radius = fmaxf(dab->radius * scale, 0.5f),
271 .center_x = dab->x * scale - (float)origin_x,
272 .center_y = dab->y * scale - (float)origin_y,
273 .inv_radius = 0.0f,
274 .tx = (dir_len > 1e-6f) ? (dab->dir_x / dir_len) : 0.0f,
275 .ty = (dir_len > 1e-6f) ? (dab->dir_y / dir_len) : 1.0f,
276 .alpha_noise_gain = 1.0f,
277 .sprinkle_coord_scale = 1.0f / fmaxf(scale, 1e-6f),
278 .sprinkle_scale = 1.0f / fmaxf(dab->sprinkle_size, 1.0f),
279 .sprinkle_strength = _clamp01(dab->sprinkles),
280 .sprinkle_w0 = sprinkle_w0,
281 .sprinkle_w1 = sprinkle_w1,
282 .sprinkle_w2 = sprinkle_w2,
283 .sprinkle_seed0 = sprinkle_seed0,
284 .sprinkle_seed1 = sprinkle_seed0 ^ 0xbf58476d1ce4e5b9ull,
285 .sprinkle_seed2 = sprinkle_seed0 ^ 0x94d049bb133111ebull,
286 .use_stroke_mask = (dab->mode == DT_DRAWLAYER_BRUSH_MODE_PAINT || dab->mode == DT_DRAWLAYER_BRUSH_MODE_ERASE),
287 .have_sprinkles = (dab->sprinkles > 1e-6f),
288 };
289 view->inv_radius = 1.0f / view->scaled_radius;
290 return TRUE;
291}
292
299 const int pixel_x, const int pixel_y)
300{
301 float alpha_noise = 1.0f;
302 if(!IS_NULL_PTR(view) && view->have_sprinkles)
303 {
304 const float layer_x = ((float)pixel_x + 0.5f) * view->sprinkle_coord_scale;
305 const float layer_y = ((float)pixel_y + 0.5f) * view->sprinkle_coord_scale;
306 alpha_noise *= _sprinkle_noise_at_pixel_precomputed(layer_x, layer_y,
307 view->sprinkle_scale,
308 view->sprinkle_strength,
309 view->sprinkle_w0,
310 view->sprinkle_w1,
311 view->sprinkle_w2,
312 view->sprinkle_seed0,
313 view->sprinkle_seed1,
314 view->sprinkle_seed2);
315 }
316 return alpha_noise;
317}
318
322{
323 if(IS_NULL_PTR(dab) || IS_NULL_PTR(view) || !view->have_sprinkles) return 1.0f;
324
325 float noise_sum = 0.0f;
326 int noise_count = 0;
327 for(int sy = -2; sy <= 2; sy++)
328 {
329 for(int sx = -2; sx <= 2; sx++)
330 {
331 const float nx = 0.4f * (float)sx;
332 const float ny = 0.4f * (float)sy;
333 if(nx * nx + ny * ny > 1.0f) continue;
334
335 const int pixel_x = (int)lrintf(view->sample_origin_x + view->center_x + nx * view->scaled_radius);
336 const int pixel_y = (int)lrintf(view->sample_origin_y + view->center_y + ny * view->scaled_radius);
337 noise_sum += _sample_alpha_noise_raw(dab, view, pixel_x, pixel_y);
338 noise_count++;
339 }
340 }
341
342 if(noise_count <= 0) return 1.0f;
343 const float mean_noise = noise_sum / (float)noise_count;
344 return (mean_noise > 1e-6f) ? (1.0f / mean_noise) : 1.0f;
345}
346
354static inline float _stroke_flow_alpha(const dt_drawlayer_brush_dab_t *dab, const float opacity, const float flow,
355 const float sample_opacity_scale, const float profile, const float brush_alpha,
356 const float old_alpha, const float stroke_old_alpha,
357 const gboolean have_stroke_alpha)
358{
359 (void)profile;
360 const float opacity_scale
361 = isfinite(sample_opacity_scale) ? CLAMP(sample_opacity_scale, 1e-6f, 1.0f) : 1.0f;
362
365 {
366 /* Replacement modes mix their sampled source against the destination, they
367 * do not build over destination alpha like paint mode. */
368 const float normalized = 1.0f - powf(fmaxf(1.0f - brush_alpha, 0.0f), opacity_scale);
369 return _clamp01(normalized);
370 }
371
372 const float flow_ref_alpha = have_stroke_alpha ? stroke_old_alpha
373 : ((dab->mode == DT_DRAWLAYER_BRUSH_MODE_ERASE) ? 0.0f : old_alpha);
374 /* Capped watercolor path:
375 * a stroke may build locally, but the stroke-local alpha must never exceed
376 * the user-requested stroke opacity. The current dab may only contribute its
377 * own local brush alpha, clipped by the remaining headroom to that cap. */
378 const float stroke_cap = _clamp01(opacity);
379 const float remaining_to_cap = fmaxf(stroke_cap - flow_ref_alpha, 0.0f);
380 const float capped_alpha = fminf(_clamp01(brush_alpha),
381 remaining_to_cap / fmaxf(1.0f - flow_ref_alpha, 1e-6f));
382 const float accum_alpha = 1.0f - powf(fmaxf(1.0f - brush_alpha, 0.0f), opacity_scale);
383 /* Internal flow convention is inverse of UI flow:
384 * - internal flow=0 (UI 100%) -> union/capped watercolor behavior,
385 * - internal flow=1 (UI 0%) -> accumulative highlighter behavior. */
386 return _clamp01(_lerpf(capped_alpha, accum_alpha, flow));
387}
388
394 const float sample_opacity_scale,
395 float *stroke_mask, const int stroke_mask_width,
396 const int stroke_mask_height,
397 const int x, const int y, const float old_alpha,
399{
400 if(IS_NULL_PTR(view) || IS_NULL_PTR(pixel_eval)) return FALSE;
401 const dt_drawlayer_brush_dab_t *dab = view->dab;
402
403 const float dy = ((float)y + 0.5f - view->center_y) * view->inv_radius;
404 const float dx = ((float)x + 0.5f - view->center_x) * view->inv_radius;
405 const float norm2 = dx * dx + dy * dy;
406 pixel_eval->profile = dt_drawlayer_brush_profile_eval(dab, norm2);
407 if(pixel_eval->profile <= 0.0f) return FALSE;
408
409 const float alpha_noise = fmaxf(0.0f, _sample_alpha_noise_raw(dab, view,
410 view->sample_origin_x + x,
411 view->sample_origin_y + y)
412 * view->alpha_noise_gain);
413
414 pixel_eval->brush_alpha = _clamp01(dab->opacity * pixel_eval->profile * alpha_noise);
415 if(pixel_eval->brush_alpha <= 0.0f) return FALSE;
416
417 // Flow caps the stroke-wise opacity to the user-specified value
418 // For this reason, we need to resolve first the stroke over transparent content,
419 // then slap the transparent layer over the background. Aka temporary buffer.
420 if(view->use_stroke_mask)
421 {
422 pixel_eval->stroke_alpha = stroke_mask + (size_t)y * stroke_mask_width + x;
423 pixel_eval->stroke_old_alpha = _clamp01(*pixel_eval->stroke_alpha);
424 }
425
426 pixel_eval->src_alpha
427 = _stroke_flow_alpha(dab, dab->opacity, dab->flow, sample_opacity_scale,
428 pixel_eval->profile, pixel_eval->brush_alpha, old_alpha,
429 pixel_eval->stroke_old_alpha, !IS_NULL_PTR(pixel_eval->stroke_alpha));
430 return pixel_eval->src_alpha > 0.0f;
431}
432
437static gboolean _prepare_blur_context(dt_aligned_pixel_simd_t *blur_px, const float *buffer, const int width,
438 const int height, const int source_origin_x, const int source_origin_y,
439 const int patch_origin_x, const int patch_origin_y,
441{
442 if(IS_NULL_PTR(blur_px)) return FALSE;
443 float blur_weight_sum = 0.0f;
444 dt_aligned_pixel_simd_t blur_sum = dt_simd_set1(0.0f);
445 const dt_drawlayer_brush_dab_t *dab = view->dab;
446
447 for(int y = view->bounds.nw[1]; y < view->bounds.se[1]; y++)
448 {
449 const float dy = ((float)y + 0.5f - view->center_y) * view->inv_radius;
450 const float dy2 = dy * dy;
451 for(int x = view->bounds.nw[0]; x < view->bounds.se[0]; x++)
452 {
453 const float dx = ((float)x + 0.5f - view->center_x) * view->inv_radius;
454 const float blur_weight = dt_drawlayer_brush_profile_eval(dab, dx * dx + dy2);
455 if(blur_weight <= 0.0f) continue;
456
457 const int source_x = x + patch_origin_x - source_origin_x;
458 const int source_y = y + patch_origin_y - source_origin_y;
459 if(source_x < 0 || source_y < 0 || source_x >= width || source_y >= height) continue;
460 const float *pixel = buffer + 4 * ((size_t)source_y * width + source_x);
461 blur_sum += dt_load_simd(pixel) * dt_simd_set1(blur_weight);
462 blur_weight_sum += blur_weight;
463 }
464 }
465
466 if(blur_weight_sum <= 1e-8f) return FALSE;
467 *blur_px = blur_sum * dt_simd_set1(1.0f / blur_weight_sum);
468 return TRUE;
469}
470
471
473static inline float _smudge_hash_signed(const int x, const int y, const int lane)
474{
475 guint32 h = (guint32)(x * 73856093u) ^ (guint32)(y * 19349663u) ^ (guint32)(lane * 83492791u);
476 h ^= h >> 13;
477 h *= 1274126177u;
478 h ^= h >> 16;
479 return ((h & 0xffffu) / 32767.5f) - 1.0f;
480}
481
486static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
487_sample_rgba_float_bilinear(const float *buffer, const int width, const int height, const float x,
488 const float y)
489{
490 if(IS_NULL_PTR(buffer) || width <= 0 || height <= 0) return dt_simd_set1(0.0f);
491
492 const float fx = CLAMP(x, 0.0f, (float)(width - 1));
493 const float fy = CLAMP(y, 0.0f, (float)(height - 1));
494 const int x0 = (int)floorf(fx);
495 const int y0 = (int)floorf(fy);
496 const int x1 = MIN(width - 1, x0 + 1);
497 const int y1 = MIN(height - 1, y0 + 1);
498 const float tx = fx - x0;
499 const float ty = fy - y0;
500
501 const float *p00 = buffer + 4 * ((size_t)y0 * width + x0);
502 const float *p10 = buffer + 4 * ((size_t)y0 * width + x1);
503 const float *p01 = buffer + 4 * ((size_t)y1 * width + x0);
504 const float *p11 = buffer + 4 * ((size_t)y1 * width + x1);
505 const dt_aligned_pixel_simd_t p00v = dt_load_simd(p00);
506 const dt_aligned_pixel_simd_t p10v = dt_load_simd(p10);
507 const dt_aligned_pixel_simd_t p01v = dt_load_simd(p01);
508 const dt_aligned_pixel_simd_t p11v = dt_load_simd(p11);
509 const dt_aligned_pixel_simd_t txv = dt_simd_set1(tx);
510 const dt_aligned_pixel_simd_t tyv = dt_simd_set1(ty);
511 const dt_aligned_pixel_simd_t av = p00v + (p10v - p00v) * txv;
512 const dt_aligned_pixel_simd_t bv = p01v + (p11v - p01v) * txv;
513 return av + (bv - av) * tyv;
514}
515
521static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
522_sample_smudge_source_float(const float *buffer, const int width, const int height, const float sx,
523 const float sy, const float motion_dx, const float motion_dy,
524 const int jitter_x, const int jitter_y)
525{
526 dt_aligned_pixel_simd_t rgba_sum = dt_simd_set1(0.0f);
527 if(IS_NULL_PTR(buffer) || width <= 0 || height <= 0) return rgba_sum;
528
529 float dir_x = motion_dx;
530 float dir_y = motion_dy;
531 const float motion = hypotf(dir_x, dir_y);
532 if(motion > 1e-6f)
533 {
534 dir_x /= motion;
535 dir_y /= motion;
536 }
537 else
538 {
539 dir_x = 1.0f;
540 dir_y = 0.0f;
541 }
542
543 const float perp_x = -dir_y;
544 const float perp_y = dir_x;
545 const float jitter = 0.60f * _smudge_hash_signed(jitter_x, jitter_y, 0);
546 const float side = 0.90f + 0.30f * _smudge_hash_signed(jitter_x, jitter_y, 1);
547 const float trail = 0.80f + 0.25f * _smudge_hash_signed(jitter_x, jitter_y, 2);
548
549 const float taps[7][3] = {
550 { 0.00f, jitter, 0.24f },
551 { -trail, 0.25f + jitter, 0.18f },
552 { -0.45f, -0.35f + jitter, 0.15f },
553 { -0.15f, side + jitter, 0.11f },
554 { -0.15f, -side + jitter, 0.11f },
555 { 0.25f, 0.45f * side + jitter, 0.11f },
556 { 0.25f, -0.45f * side + jitter, 0.10f },
557 };
558
559 float weight_sum = 0.0f;
560 for(int i = 0; i < 7; i++)
561 {
562 const float px = sx + dir_x * taps[i][0] + perp_x * taps[i][1];
563 const float py = sy + dir_y * taps[i][0] + perp_y * taps[i][1];
564 const float w = taps[i][2];
565 rgba_sum += _sample_rgba_float_bilinear(buffer, width, height, px, py) * dt_simd_set1(w);
566 weight_sum += w;
567 }
568
569 if(weight_sum > 1e-8f)
570 rgba_sum *= dt_simd_set1(1.0f / weight_sum);
571 return rgba_sum;
572}
573
575static inline float _smudge_deposit_alpha(const float src_alpha, const float carried_alpha, const float opacity)
576{
577 const float carry = _clamp01(carried_alpha);
578 const float base = _clamp01(opacity);
579 const float influence = base + (1.0f - base) * carry;
580 return _clamp01(src_alpha * influence);
581}
582
587static dt_aligned_pixel_simd_t _apply_smudge_stroke_mode(const float *source_buffer, const int source_width,
588 const int source_height, const int source_origin_x,
589 const int source_origin_y,
590 dt_drawlayer_paint_stroke_t *runtime_private,
592 const float scale, const int patch_origin_x,
593 const int patch_origin_y, const int x, const int y,
594 const float src_alpha,
595 const dt_aligned_pixel_simd_t old_px)
596{
597 const dt_drawlayer_brush_dab_t *dab = view->dab;
598 const float source_x_offset = (float)(patch_origin_x - source_origin_x);
599 const float source_y_offset = (float)(patch_origin_y - source_origin_y);
600 const float center_abs_x = view->center_x + (float)patch_origin_x;
601 const float center_abs_y = view->center_y + (float)patch_origin_y;
602 float sample_x = (float)x + source_x_offset;
603 float sample_y = (float)y + source_y_offset;
604 float motion_dx = 0.0f;
605 float motion_dy = 0.0f;
606 float *smudge_pixels = dt_drawlayer_paint_runtime_smudge_pixels(runtime_private);
607 const int smudge_width = dt_drawlayer_paint_runtime_smudge_width(runtime_private);
608
609 if(runtime_private && dt_drawlayer_paint_runtime_have_smudge_pickup(runtime_private))
610 {
611 /* Smudge samples from a lagging pickup point that follows dab centers. */
612 float pickup_center_x = 0.0f;
613 float pickup_center_y = 0.0f;
614 dt_drawlayer_paint_runtime_get_smudge_pickup(runtime_private, &pickup_center_x, &pickup_center_y);
615 pickup_center_x *= scale;
616 pickup_center_y *= scale;
617 sample_x += pickup_center_x - center_abs_x;
618 sample_y += pickup_center_y - center_abs_y;
619 motion_dx = center_abs_x - pickup_center_x;
620 motion_dy = center_abs_y - pickup_center_y;
621 }
622
623 const dt_aligned_pixel_simd_t sampled_px
624 = _sample_smudge_source_float(source_buffer, source_width, source_height, sample_x, sample_y,
625 motion_dx, motion_dy,
626 x - view->bounds.nw[0], y - view->bounds.nw[1]);
627
628 if(IS_NULL_PTR(smudge_pixels) || smudge_width <= 0) return old_px;
629 float *carry = smudge_pixels + 4 * ((size_t)(y - view->bounds.nw[1]) * smudge_width + (x - view->bounds.nw[0]));
630 const dt_aligned_pixel_simd_t carried_px = dt_load_simd(carry);
631 const float pickup_blend = _clamp01(dab->opacity);
632 const float carried_alpha = _clamp01(carry[3]);
633 const float deposit_alpha = _smudge_deposit_alpha(src_alpha, carried_alpha, dab->opacity);
634 const float inv_alpha = 1.0f - deposit_alpha;
635
636 const dt_aligned_pixel_simd_t out_px = carried_px * dt_simd_set1(deposit_alpha)
637 + old_px * dt_simd_set1(inv_alpha);
638 const dt_aligned_pixel_simd_t next_carry = carried_px
639 + (sampled_px - carried_px) * dt_simd_set1(pickup_blend);
640 dt_store_simd(carry, next_carry);
641 return out_px;
642}
643
649 dt_drawlayer_cache_patch_t *patch, const float scale,
650 const dt_drawlayer_brush_dab_t *dab,
651 const float sample_opacity_scale,
652 dt_drawlayer_cache_patch_t *stroke_mask,
653 dt_drawlayer_paint_stroke_t *runtime_private)
654{
655 /* Entry point for dab-level rasterization.
656 * Steps:
657 * 1) precompute dab bounds and orientation,
658 * 2) resolve per-pixel alpha (profile/noise/flow),
659 * 3) blend into target buffer and update stroke-local alpha mask. */
660 if(IS_NULL_PTR(patch) || IS_NULL_PTR(patch->pixels) || IS_NULL_PTR(dab) || !runtime_private) return FALSE;
661 if(dab->radius <= 0.0f || dab->opacity <= 0.0f || scale <= 0.0f) return FALSE;
662
663 float *const buffer = patch->pixels;
664 const int width = patch->width;
665 const int height = patch->height;
666 const int origin_x = patch->x;
667 const int origin_y = patch->y;
668 const dt_drawlayer_cache_patch_t *const source_patch
669 = (sample_patch && sample_patch->pixels) ? sample_patch : patch;
670 const float *const source_buffer = source_patch->pixels;
671 const int source_width = source_patch->width;
672 const int source_height = source_patch->height;
673 const int source_origin_x = source_patch->x;
674 const int source_origin_y = source_patch->y;
675 float *const stroke_mask_pixels = stroke_mask ? stroke_mask->pixels : NULL;
676 const int stroke_mask_width = stroke_mask ? stroke_mask->width : 0;
677 const int stroke_mask_height = stroke_mask ? stroke_mask->height : 0;
678
679 dt_drawlayer_brush_dab_t prepared = *dab;
680 prepared.opacity = _clamp01(prepared.opacity);
681 /* Internal flow convention is inverse of UI flow. */
682 prepared.flow = 1.0f - _clamp01(prepared.flow);
683
684 if(!dt_drawlayer_paint_runtime_prepare_dab_context(runtime_private, &prepared, width, height,
685 origin_x, origin_y, scale))
686 return FALSE;
687
689 if(!_brush_runtime_view_from_state(runtime_private, &prepared, origin_x, origin_y, scale, &view))
690 return FALSE;
692
693 dt_aligned_pixel_simd_t blur_px = dt_simd_set1(0.0f);
694 switch(view.dab->mode)
695 {
697 if(!_prepare_blur_context(&blur_px, source_buffer, source_width, source_height, source_origin_x,
698 source_origin_y, origin_x, origin_y, &view))
699 return FALSE;
700 break;
703 view.bounds.se[0] - view.bounds.nw[0],
704 view.bounds.se[1] - view.bounds.nw[1]))
705 return FALSE;
706 break;
709 default:
710 break;
711 }
712
713 if(view.dab->mode == DT_DRAWLAYER_BRUSH_MODE_SMUDGE)
714 {
715 for(int y = view.bounds.nw[1]; y < view.bounds.se[1]; y++)
716 {
717 for(int x = view.bounds.nw[0]; x < view.bounds.se[0]; x++)
718 {
719 float *pixel = buffer + 4 * ((size_t)y * width + x);
720 const float old_alpha = _clamp01(pixel[3]);
721 const dt_aligned_pixel_simd_t old_px = (old_alpha > 1e-8f) ? dt_load_simd(pixel) : dt_simd_set1(0.0f);
722 dt_drawlayer_brush_pixel_eval_t pixel_eval = { 0 };
723 if(!_prepare_analytic_pixel_context(&view, sample_opacity_scale,
724 stroke_mask_pixels, stroke_mask_width, stroke_mask_height,
725 x, y, old_alpha, &pixel_eval))
726 continue;
727
728 const dt_aligned_pixel_simd_t out_px
729 = _apply_smudge_stroke_mode(source_buffer, source_width, source_height, source_origin_x,
730 source_origin_y, runtime_private, &view,
731 scale, origin_x, origin_y,
732 x, y, pixel_eval.src_alpha, old_px);
733 dt_store_simd(pixel, out_px);
734
735 if(pixel_eval.stroke_alpha)
736 {
737 *pixel_eval.stroke_alpha
738 = pixel_eval.src_alpha
739 + pixel_eval.stroke_old_alpha * (1.0f - pixel_eval.src_alpha);
740 }
741 }
742 }
743 }
744 else
745 {
746#if defined(_OPENMP) && !OUTER_LOOP
747#pragma omp parallel for default(firstprivate) collapse(2)
748#endif
749 for(int y = view.bounds.nw[1]; y < view.bounds.se[1]; y++)
750 {
751 for(int x = view.bounds.nw[0]; x < view.bounds.se[0]; x++)
752 {
753 float *pixel = buffer + 4 * ((size_t)y * width + x);
754 const float old_alpha = _clamp01(pixel[3]);
755 const dt_aligned_pixel_simd_t old_px = (old_alpha > 1e-8f) ? dt_load_simd(pixel) : dt_simd_set1(0.0f);
756 dt_drawlayer_brush_pixel_eval_t pixel_eval = { 0 };
757 if(!_prepare_analytic_pixel_context(&view, sample_opacity_scale,
758 stroke_mask_pixels, stroke_mask_width, stroke_mask_height,
759 x, y, old_alpha, &pixel_eval))
760 continue;
761
762 dt_aligned_pixel_simd_t out_px;
763 const dt_aligned_pixel_simd_t inv_alpha = dt_simd_set1(1.0f - pixel_eval.src_alpha);
764 switch(view.dab->mode)
765 {
767 out_px = old_px * inv_alpha;
768 break;
770 out_px = blur_px * dt_simd_set1(pixel_eval.src_alpha) + old_px * inv_alpha;
771 break;
773 default:
774 out_px = dt_load_simd(view.dab->color) * dt_simd_set1(pixel_eval.src_alpha) + old_px * inv_alpha;
775 break;
776 }
777
778 dt_store_simd(pixel, out_px);
779
780 if(pixel_eval.stroke_alpha)
781 {
782 *pixel_eval.stroke_alpha
783 = pixel_eval.src_alpha
784 + pixel_eval.stroke_old_alpha * (1.0f - pixel_eval.src_alpha);
785 }
786 }
787 }
788 }
789
790 return TRUE;
791}
792
798 const int width, const int height, const int stride,
799 const float center_x, const float center_y,
800 const float opacity_multiplier)
801{
802 /* Lightweight GUI preview dab renderer (ARGB8), independent from stroke logic. */
803 if(IS_NULL_PTR(dab) || IS_NULL_PTR(argb) || width <= 0 || height <= 0 || stride < 4 * width) return FALSE;
804
805 memset(argb, 0, (size_t)stride * height);
806 if(dab->radius <= 0.0f) return FALSE;
807
808 dt_drawlayer_brush_dab_t preview = *dab;
809 preview.opacity = _clamp01(preview.opacity * _clamp01(opacity_multiplier));
810 if(preview.opacity <= 0.0f) return FALSE;
811
812 const float radius = fmaxf(preview.radius, 0.5f);
813 const float inv_radius = 1.0f / radius;
814 const int x0 = (int)fmaxf(0.0f, floorf(center_x - radius));
815 const int y0 = (int)fmaxf(0.0f, floorf(center_y - radius));
816 const int x1 = (int)fminf((float)width, ceilf(center_x + radius) + 1.0f);
817 const int y1 = (int)fminf((float)height, ceilf(center_y + radius) + 1.0f);
818
819 const float disp_r = _clamp01(preview.display_color[0]);
820 const float disp_g = _clamp01(preview.display_color[1]);
821 const float disp_b = _clamp01(preview.display_color[2]);
822 dt_drawlayer_sprinkle_preview_t sprinkle = { 0 };
823 _prepare_sprinkle_preview(&preview, center_x, center_y, radius, &sprinkle);
824
825 for(int y = y0; y < y1; y++)
826 {
827 const float dy = ((float)y + 0.5f - center_y) * inv_radius;
828 const float dy2 = dy * dy;
829 for(int x = x0; x < x1; x++)
830 {
831 const float dx = ((float)x + 0.5f - center_x) * inv_radius;
832 const float profile = dt_drawlayer_brush_profile_eval(&preview, dx * dx + dy2);
833 if(profile <= 0.0f) continue;
834
835 const float alpha_noise = _sample_sprinkle_preview(&sprinkle, x, y);
836 const float alpha = _clamp01(preview.opacity * profile * alpha_noise);
837 if(alpha <= 0.0f) continue;
838
839 uint8_t *pixel = argb + (size_t)y * stride + 4 * x;
840 pixel[0] = (uint8_t)fminf(fmaxf(roundf(255.0f * _clamp01(disp_b * alpha)), 0.0f), 255.0f);
841 pixel[1] = (uint8_t)fminf(fmaxf(roundf(255.0f * _clamp01(disp_g * alpha)), 0.0f), 255.0f);
842 pixel[2] = (uint8_t)fminf(fmaxf(roundf(255.0f * _clamp01(disp_r * alpha)), 0.0f), 255.0f);
843 pixel[3] = (uint8_t)fminf(fmaxf(roundf(255.0f * alpha), 0.0f), 255.0f);
844 }
845 }
846
847 return TRUE;
848}
849
851 const int width, const int height,
852 const float center_x, const float center_y,
853 const float opacity_multiplier,
854 const float background_rgb[3])
855{
856 if(IS_NULL_PTR(dab) || IS_NULL_PTR(rgba) || width <= 0 || height <= 0) return FALSE;
857
858 const float bg_r = background_rgb ? _clamp01(background_rgb[0]) : 1.0f;
859 const float bg_g = background_rgb ? _clamp01(background_rgb[1]) : 1.0f;
860 const float bg_b = background_rgb ? _clamp01(background_rgb[2]) : 1.0f;
861 for(int i = 0; i < width * height; i++)
862 {
863 rgba[4 * i + 0] = bg_r;
864 rgba[4 * i + 1] = bg_g;
865 rgba[4 * i + 2] = bg_b;
866 rgba[4 * i + 3] = 1.0f;
867 }
868
869 if(dab->radius <= 0.0f) return FALSE;
870
871 dt_drawlayer_brush_dab_t preview = *dab;
872 preview.opacity = _clamp01(preview.opacity * _clamp01(opacity_multiplier));
873 if(preview.opacity <= 0.0f) return FALSE;
874
875 const float radius = fmaxf(preview.radius, 0.5f);
876 const float inv_radius = 1.0f / radius;
877 const int x0 = (int)fmaxf(0.0f, floorf(center_x - radius));
878 const int y0 = (int)fmaxf(0.0f, floorf(center_y - radius));
879 const int x1 = (int)fminf((float)width, ceilf(center_x + radius) + 1.0f);
880 const int y1 = (int)fminf((float)height, ceilf(center_y + radius) + 1.0f);
881
882 const float src_r = _clamp01(preview.color[0]);
883 const float src_g = _clamp01(preview.color[1]);
884 const float src_b = _clamp01(preview.color[2]);
885 dt_drawlayer_sprinkle_preview_t sprinkle = { 0 };
886 _prepare_sprinkle_preview(&preview, center_x, center_y, radius, &sprinkle);
887
888 for(int y = y0; y < y1; y++)
889 {
890 const float dy = ((float)y + 0.5f - center_y) * inv_radius;
891 const float dy2 = dy * dy;
892 for(int x = x0; x < x1; x++)
893 {
894 const float dx = ((float)x + 0.5f - center_x) * inv_radius;
895 const float profile = dt_drawlayer_brush_profile_eval(&preview, dx * dx + dy2);
896 if(profile <= 0.0f) continue;
897
898 const float alpha_noise = _sample_sprinkle_preview(&sprinkle, x, y);
899 const float alpha = _clamp01(preview.opacity * profile * alpha_noise);
900 if(alpha <= 0.0f) continue;
901
902 float *pixel = rgba + 4 * ((size_t)y * width + x);
903 const float inv_alpha = 1.0f - alpha;
904 pixel[0] = src_r * alpha + pixel[0] * inv_alpha;
905 pixel[1] = src_g * alpha + pixel[1] * inv_alpha;
906 pixel[2] = src_b * alpha + pixel[2] * inv_alpha;
907 pixel[3] = 1.0f;
908 }
909 }
910
911 return TRUE;
912}
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
int width
Definition bilateral.h:1
int height
Definition bilateral.h:1
Dab-level brush rasterization API for drawlayer.
@ DT_DRAWLAYER_BRUSH_MODE_ERASE
Definition brush.h:49
@ DT_DRAWLAYER_BRUSH_MODE_PAINT
Definition brush.h:48
@ DT_DRAWLAYER_BRUSH_MODE_BLUR
Definition brush.h:50
@ DT_DRAWLAYER_BRUSH_MODE_SMUDGE
Definition brush.h:51
Inline brush profile and mass primitives shared by paint/brush code.
static float dt_drawlayer_brush_profile_eval(const dt_drawlayer_brush_dab_t *dab, const float norm2)
Evaluate normalized brush profile at squared normalized radius.
return vector dt_simd_set1(valid ?(scaling+NORM_MIN) :NORM_MIN)
static float strength(float value, float strength)
Definition colorzones.c:420
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
uint32_t view(const dt_view_t *self)
Definition darkroom.c:227
dt_store_simd(out, value)
float dt_aligned_pixel_simd_t __attribute__((vector_size(16), aligned(16)))
Enable aggressive floating-point arithmetic optimizations, in denormals handling. Set through user pr...
Definition darktable.h:524
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition darktable.h:281
static unsigned int splitmix32(const unsigned long seed)
static gboolean _brush_runtime_view_from_state(const dt_drawlayer_paint_stroke_t *stroke, const dt_drawlayer_brush_dab_t *dab, const int origin_x, const int origin_y, const float scale, dt_drawlayer_brush_runtime_view_t *view)
Build immutable per-dab raster view from stroke runtime state.
static float _cellular_grain_2d(const uint64_t seed, const float x, const float y)
Grain-like round cellular field. Peaks at grain centers, falls off radially.
gboolean dt_drawlayer_brush_rasterize_dab_rgbaf(const dt_drawlayer_brush_dab_t *dab, float *rgba, const int width, const int height, const float center_x, const float center_y, const float opacity_multiplier, const float background_rgb[3])
Rasterize a single dab preview in linear float RGBA over an opaque background.
static float _sample_alpha_noise_raw(const dt_drawlayer_brush_dab_t *dab, const dt_drawlayer_brush_runtime_view_t *view, const int pixel_x, const int pixel_y)
Resolve multiplicative alpha noise at one pixel.
static void _prepare_sprinkle_preview(const dt_drawlayer_brush_dab_t *dab, const float center_x, const float center_y, const float radius, dt_drawlayer_sprinkle_preview_t *preview)
static gboolean _prepare_blur_context(dt_aligned_pixel_simd_t *blur_px, const float *buffer, const int width, const int height, const int source_origin_x, const int source_origin_y, const int patch_origin_x, const int patch_origin_y, const dt_drawlayer_brush_runtime_view_t *view)
Build blur gather color for current dab footprint.
static float _smudge_hash_signed(const int x, const int y, const int lane)
Stable signed pseudo-random helper in [-1,1].
static float _clamp01(const float v)
Clamp scalar to [0,1].
static float _smudge_deposit_alpha(const float src_alpha, const float carried_alpha, const float opacity)
Resolve effective smudge deposit alpha for one pixel.
static float _stroke_flow_alpha(const dt_drawlayer_brush_dab_t *dab, const float opacity, const float flow, const float sample_opacity_scale, const float profile, const float brush_alpha, const float old_alpha, const float stroke_old_alpha, const gboolean have_stroke_alpha)
Compute per-pixel source alpha from opacity/flow model.
gboolean dt_drawlayer_brush_rasterize_dab_argb8(const dt_drawlayer_brush_dab_t *dab, uint8_t *argb, const int width, const int height, const int stride, const float center_x, const float center_y, const float opacity_multiplier)
Render one dab to 8-bit ARGB surface for GUI cursor preview.
static void _sprinkle_octave_weights(const float coarseness, float *w0, float *w1, float *w2)
Resolve octave weights from coarseness control.
static float _sprinkle_noise_at_pixel_precomputed(const float px, const float py, const float scale, const float strength, const float w0, const float w1, const float w2, const uint64_t seed0, const uint64_t seed1, const uint64_t seed2)
Evaluate sprinkle modulation from precomputed dab constants.
static float _estimate_alpha_noise_gain(const dt_drawlayer_brush_dab_t *dab, const dt_drawlayer_brush_runtime_view_t *view)
Estimate per-dab texture gain so noise preserves average opacity across samples.
static float _sample_sprinkle_preview(const dt_drawlayer_sprinkle_preview_t *preview, const float px, const float py)
static float _cell_hash01_from_seed(const uint64_t cell_seed, const uint64_t salt)
Stable scalar hash in [0,1] from precomputed cell seed.
static float _lerpf(const float a, const float b, const float t)
Linear interpolation helper.
static dt_aligned_pixel_simd_t _apply_smudge_stroke_mode(const float *source_buffer, const int source_width, const int source_height, const int source_origin_x, const int source_origin_y, dt_drawlayer_paint_stroke_t *runtime_private, const dt_drawlayer_brush_runtime_view_t *view, const float scale, const int patch_origin_x, const int patch_origin_y, const int x, const int y, const float src_alpha, const dt_aligned_pixel_simd_t old_px)
Apply smudge mode for one pixel and update carried sample.
static gboolean _prepare_analytic_pixel_context(const dt_drawlayer_brush_runtime_view_t *view, const float sample_opacity_scale, float *stroke_mask, const int stroke_mask_width, const int stroke_mask_height, const int x, const int y, const float old_alpha, dt_drawlayer_brush_pixel_eval_t *pixel_eval)
Compute full analytic per-pixel brush context.
gboolean dt_drawlayer_brush_rasterize(const dt_drawlayer_cache_patch_t *sample_patch, dt_drawlayer_cache_patch_t *patch, const float scale, const dt_drawlayer_brush_dab_t *dab, const float sample_opacity_scale, dt_drawlayer_cache_patch_t *stroke_mask, dt_drawlayer_paint_stroke_t *runtime_private)
Public dab rasterization entry point.
Patch/cache helpers for drawlayer process and preview buffers.
float * dt_drawlayer_paint_runtime_smudge_pixels(dt_drawlayer_paint_stroke_t *state)
Get smudge carry buffer pointer (RGBA float).
gboolean dt_drawlayer_paint_runtime_have_smudge_pickup(const dt_drawlayer_paint_stroke_t *state)
Query whether smudge pickup coordinates are initialized.
void dt_drawlayer_paint_runtime_get_smudge_pickup(const dt_drawlayer_paint_stroke_t *state, float *x, float *y)
Read smudge pickup coordinates.
int dt_drawlayer_paint_runtime_smudge_width(const dt_drawlayer_paint_stroke_t *state)
Get smudge carry buffer width.
gboolean dt_drawlayer_paint_runtime_prepare_dab_context(dt_drawlayer_paint_stroke_t *state, const dt_drawlayer_brush_dab_t *dab, const int width, const int height, const int origin_x, const int origin_y, const float scale)
Compute current dab footprint bounds in target buffer coordinates.
gboolean dt_drawlayer_paint_runtime_ensure_smudge_pixels(dt_drawlayer_paint_stroke_t *state, const int width, const int height)
Ensure smudge carry buffer allocation for given footprint dimensions.
Stroke-level path sampling and runtime-state API for drawlayer.
static const float x
const int t
const float v
#define w2
Definition lmmse.c:60
#define w1
Definition lmmse.c:59
unsigned __int64 uint64_t
Definition strptime.c:75
Fully resolved input dab descriptor.
Definition brush.h:64
float display_color[3]
Definition brush.h:81
uint32_t stroke_batch
Definition brush.h:84
dt_drawlayer_damaged_rect_t bounds
const dt_drawlayer_brush_dab_t * dab
Generic float RGBA patch stored either in malloc memory or pixel cache.
Integer axis-aligned rectangle in buffer coordinates.
Mutable stroke runtime state owned by worker/backend code.
dt_drawlayer_damaged_rect_t bounds
#define MIN(a, b)
Definition thinplate.c:32