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 "system/macros.h"
32#include "system/simd.h"
33#include "iop/noise_generator.h"
34
35#include <math.h>
36#include <string.h>
37
39static inline float _clamp01(const float v)
40{
41 return fminf(fmaxf(v, 0.0f), 1.0f);
42}
43
45static inline float _lerpf(const float a, const float b, const float t)
46{
47 return a + (b - a) * t;
48}
49
51static inline float _cell_hash01_from_seed(const uint64_t cell_seed, const uint64_t salt)
52{
53 return (float)splitmix32(cell_seed ^ salt) / (float)UINT32_MAX;
54}
55
57static inline float _cellular_grain_2d(const uint64_t seed, const float x, const float y)
58{
59 const int cell_x = (int)floorf(x);
60 const int cell_y = (int)floorf(y);
61 float accum = 0.0f;
62 float weight_sum = 0.0f;
63
64 for(int oy = -1; oy <= 1; oy++)
65 {
66 for(int ox = -1; ox <= 1; ox++)
67 {
68 const int ix = cell_x + ox;
69 const int iy = cell_y + oy;
70 const uint64_t cell_seed = seed
71 ^ ((uint64_t)(uint32_t)ix * 0x9e3779b185ebca87ull)
72 ^ ((uint64_t)(uint32_t)iy * 0xc2b2ae3d27d4eb4full);
73 const float jitter_x = _cell_hash01_from_seed(cell_seed, 0x94d049bb133111ebull);
74 const float jitter_y = _cell_hash01_from_seed(cell_seed, 0xbf58476d1ce4e5b9ull);
75 const float grain_gain = 0.65f + 0.35f * _cell_hash01_from_seed(cell_seed, 0xda942042e4dd58b5ull);
76 const float dx = x - ((float)ix + jitter_x);
77 const float dy = y - ((float)iy + jitter_y);
78 const float dist2 = dx * dx + dy * dy;
79 const float radius = 0.42f + 0.22f * _cell_hash01_from_seed(cell_seed, 0x369dea0f31a53f85ull);
80 const float grain = fmaxf(0.0f, 1.0f - dist2 / (radius * radius));
81 const float shaped = grain * grain * (3.0f - 2.0f * grain);
82 accum += grain_gain * shaped;
83 weight_sum += grain_gain;
84 }
85 }
86
87 return (weight_sum > 1e-6f) ? _clamp01(accum / weight_sum) : 0.0f;
88}
89
91static inline void _sprinkle_octave_weights(const float coarseness, float *w0, float *w1, float *w2)
92{
93 const float c = 1.0f - _clamp01(coarseness);
94 if(c <= 0.5f)
95 {
96 const float t = c * 2.0f;
97 if(!IS_NULL_PTR(w0)) *w0 = _lerpf(1.0f, 1.0f / 3.0f, t);
98 if(!IS_NULL_PTR(w1)) *w1 = _lerpf(0.0f, 1.0f / 3.0f, t);
99 if(!IS_NULL_PTR(w2)) *w2 = _lerpf(0.0f, 1.0f / 3.0f, t);
100 }
101 else
102 {
103 const float t = (c - 0.5f) * 2.0f;
104 if(!IS_NULL_PTR(w0)) *w0 = _lerpf(1.0f / 3.0f, 0.0f, t);
105 if(!IS_NULL_PTR(w1)) *w1 = _lerpf(1.0f / 3.0f, 0.0f, t);
106 if(!IS_NULL_PTR(w2)) *w2 = _lerpf(1.0f / 3.0f, 1.0f, t);
107 }
108}
109
111static inline float _sprinkle_noise_at_pixel_precomputed(const float px, const float py,
112 const float scale, const float strength,
113 const float w0, const float w1, const float w2,
114 const uint64_t seed0, const uint64_t seed1, const uint64_t seed2)
115{
116 if(strength <= 1e-6f) return 1.0f;
117 const float x = px * scale;
118 const float y = py * scale;
119 const float g0 = (w0 > 1e-6f) ? _cellular_grain_2d(seed0, x, y) : 0.0f;
120 const float g1 = (w1 > 1e-6f) ? _cellular_grain_2d(seed1, x * 1.93f + 4.7f, y * 1.93f - 2.9f) : 0.0f;
121 const float g2 = (w2 > 1e-6f) ? _cellular_grain_2d(seed2, x * 3.71f - 6.2f, y * 3.71f + 8.4f) : 0.0f;
122 const float field = w0 * g0 + w1 * g1 + w2 * g2;
123 const float centered = 2.0f * field - 1.0f;
124 return fmaxf(0.0f, 1.0f + strength * centered);
125}
126
140
142 const float center_x, const float center_y,
143 const float radius,
145{
147 return;
148
150 .scale = 1.0f,
151 .strength = 0.0f,
152 .w0 = 0.0f,
153 .w1 = 0.0f,
154 .w2 = 0.0f,
155 .seed0 = 0u,
156 .seed1 = 0u,
157 .seed2 = 0u,
158 .gain = 1.0f,
159 .enabled = FALSE,
160 };
161
162 if(IS_NULL_PTR(dab) || dab->sprinkles <= 1e-6f)
163 return;
164
165 preview->scale = 1.0f / fmaxf(dab->sprinkle_size, 1.0f);
166 preview->strength = _clamp01(dab->sprinkles);
168 preview->seed0 = ((uint64_t)dab->stroke_batch << 32) ^ 0x7f4a7c159e3779b9ull;
169 preview->seed1 = preview->seed0 ^ 0xbf58476d1ce4e5b9ull;
170 preview->seed2 = preview->seed0 ^ 0x94d049bb133111ebull;
171 preview->enabled = TRUE;
172
173 float noise_sum = 0.0f;
174 int noise_count = 0;
175 for(int sy = -2; sy <= 2; sy++)
176 {
177 for(int sx = -2; sx <= 2; sx++)
178 {
179 const float nx = 0.4f * (float)sx;
180 const float ny = 0.4f * (float)sy;
181 if(nx * nx + ny * ny > 1.0f) continue;
182 const int pixel_x = (int)lrintf(center_x + nx * radius);
183 const int pixel_y = (int)lrintf(center_y + ny * radius);
184 noise_sum += _sprinkle_noise_at_pixel_precomputed(pixel_x, pixel_y,
185 preview->scale, preview->strength,
186 preview->w0, preview->w1, preview->w2,
187 preview->seed0, preview->seed1, preview->seed2);
188 noise_count++;
189 }
190 }
191
192 if(noise_count > 0)
193 {
194 const float mean_noise = noise_sum / (float)noise_count;
195 preview->gain = (mean_noise > 1e-6f) ? (1.0f / mean_noise) : 1.0f;
196 }
197}
198
200 const float px, const float py)
201{
202 if(IS_NULL_PTR(preview) || !preview->enabled) return 1.0f;
204 preview->scale, preview->strength,
205 preview->w0, preview->w1, preview->w2,
206 preview->seed0, preview->seed1, preview->seed2) * preview->gain;
207}
208
236
238{
239 /* Per-pixel analytic evaluation (profile and resolved alpha controls). */
240 float profile;
246
252 const dt_drawlayer_brush_dab_t *dab,
253 const int origin_x, const int origin_y,
254 const float scale,
256{
257 if(IS_NULL_PTR(bounds) || IS_NULL_PTR(dab) || IS_NULL_PTR(view) || !bounds->valid) return FALSE;
258 if(bounds->se[0] <= bounds->nw[0] || bounds->se[1] <= bounds->nw[1]) 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 = *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;
291 return TRUE;
292}
293
295 const dt_drawlayer_brush_dab_t *dab,
296 const int origin_x, const int origin_y,
297 const float scale,
299{
300 if(!stroke) return FALSE;
301 return _brush_runtime_view_from_bounds(&stroke->bounds, dab, origin_x, origin_y, scale, view);
302}
303
310 const int pixel_x, const int pixel_y)
311{
312 float alpha_noise = 1.0f;
313 if(!IS_NULL_PTR(view) && view->have_sprinkles)
314 {
315 const float layer_x = ((float)pixel_x + 0.5f) * view->sprinkle_coord_scale;
316 const float layer_y = ((float)pixel_y + 0.5f) * view->sprinkle_coord_scale;
317 alpha_noise *= _sprinkle_noise_at_pixel_precomputed(layer_x, layer_y,
318 view->sprinkle_scale,
319 view->sprinkle_strength,
320 view->sprinkle_w0,
321 view->sprinkle_w1,
322 view->sprinkle_w2,
323 view->sprinkle_seed0,
324 view->sprinkle_seed1,
325 view->sprinkle_seed2);
326 }
327 return alpha_noise;
328}
329
333{
334 if(IS_NULL_PTR(dab) || IS_NULL_PTR(view) || !view->have_sprinkles) return 1.0f;
335
336 float noise_sum = 0.0f;
337 int noise_count = 0;
338 for(int sy = -2; sy <= 2; sy++)
339 {
340 for(int sx = -2; sx <= 2; sx++)
341 {
342 const float nx = 0.4f * (float)sx;
343 const float ny = 0.4f * (float)sy;
344 if(nx * nx + ny * ny > 1.0f) continue;
345
346 const int pixel_x = (int)lrintf(view->sample_origin_x + view->center_x + nx * view->scaled_radius);
347 const int pixel_y = (int)lrintf(view->sample_origin_y + view->center_y + ny * view->scaled_radius);
348 noise_sum += _sample_alpha_noise_raw(dab, view, pixel_x, pixel_y);
349 noise_count++;
350 }
351 }
352
353 if(noise_count <= 0) return 1.0f;
354 const float mean_noise = noise_sum / (float)noise_count;
355 return (mean_noise > 1e-6f) ? (1.0f / mean_noise) : 1.0f;
356}
357
365static inline float _stroke_flow_alpha(const dt_drawlayer_brush_dab_t *dab, const float opacity, const float flow,
366 const float sample_opacity_scale, const float profile, const float brush_alpha,
367 const float old_alpha, const float stroke_old_alpha,
368 const gboolean have_stroke_alpha)
369{
370 (void)profile;
371 const float opacity_scale
372 = isfinite(sample_opacity_scale) ? CLAMP(sample_opacity_scale, 1e-6f, 1.0f) : 1.0f;
373
376 {
377 /* Replacement modes mix their sampled source against the destination, they
378 * do not build over destination alpha like paint mode. */
379 const float normalized = 1.0f - powf(fmaxf(1.0f - brush_alpha, 0.0f), opacity_scale);
380 return _clamp01(normalized);
381 }
382
383 const float flow_ref_alpha = have_stroke_alpha ? stroke_old_alpha
384 : ((dab->mode == DT_DRAWLAYER_BRUSH_MODE_ERASE) ? 0.0f : old_alpha);
385 /* Capped watercolor path:
386 * a stroke may build locally, but the stroke-local alpha must never exceed
387 * the user-requested stroke opacity. The current dab may only contribute its
388 * own local brush alpha, clipped by the remaining headroom to that cap. */
389 const float stroke_cap = _clamp01(opacity);
390 const float remaining_to_cap = fmaxf(stroke_cap - flow_ref_alpha, 0.0f);
391 const float capped_alpha = fminf(_clamp01(brush_alpha),
392 remaining_to_cap / fmaxf(1.0f - flow_ref_alpha, 1e-6f));
393
394 /* `flow` is a per-dab constant and is exactly 0 at the shipped default (UI Flow 100%,
395 * inverted at the top of dt_drawlayer_brush_rasterize). `_lerpf(a, b, 0)` is `a + (b-a)*0`,
396 * which is `a` exactly for any finite b -- so at the default this `powf` was evaluated once
397 * per inside-disc pixel of every dab and then multiplied away. That is ~412k discarded libm
398 * calls per heartbeat batch at r=64 with a 16-thread batch, and a call boundary the enclosing
399 * loop cannot vectorize across. Returning here is bit-identical, not an approximation. */
400 if(flow <= 0.0f) return _clamp01(capped_alpha);
401
402 const float accum_alpha = 1.0f - powf(fmaxf(1.0f - brush_alpha, 0.0f), opacity_scale);
403 /* Internal flow convention is inverse of UI flow:
404 * - internal flow=0 (UI 100%) -> union/capped watercolor behavior,
405 * - internal flow=1 (UI 0%) -> accumulative highlighter behavior. */
406 return _clamp01(_lerpf(capped_alpha, accum_alpha, flow));
407}
408
414 const float sample_opacity_scale,
415 float *stroke_mask, const int stroke_mask_width,
416 const int stroke_mask_height,
417 const int x, const int y, const float old_alpha,
419{
420 if(IS_NULL_PTR(view) || IS_NULL_PTR(pixel_eval)) return FALSE;
421 const dt_drawlayer_brush_dab_t *dab = view->dab;
422
423 const float dy = ((float)y + 0.5f - view->center_y) * view->inv_radius;
424 const float dx = ((float)x + 0.5f - view->center_x) * view->inv_radius;
425 const float norm2 = dx * dx + dy * dy;
426 pixel_eval->profile = dt_drawlayer_brush_profile_eval_fast(&view->profile, norm2);
427 if(pixel_eval->profile <= 0.0f) return FALSE;
428
429 const float alpha_noise = fmaxf(0.0f, _sample_alpha_noise_raw(dab, view,
430 view->sample_origin_x + x,
431 view->sample_origin_y + y)
432 * view->alpha_noise_gain);
433
434 pixel_eval->brush_alpha = _clamp01(dab->opacity * pixel_eval->profile * alpha_noise);
435 if(pixel_eval->brush_alpha <= 0.0f) return FALSE;
436
437 // Flow caps the stroke-wise opacity to the user-specified value
438 // For this reason, we need to resolve first the stroke over transparent content,
439 // then slap the transparent layer over the background. Aka temporary buffer.
440 if(view->use_stroke_mask)
441 {
442 pixel_eval->stroke_alpha = stroke_mask + (size_t)y * stroke_mask_width + x;
443 pixel_eval->stroke_old_alpha = _clamp01(*pixel_eval->stroke_alpha);
444 }
445
446 pixel_eval->src_alpha
447 = _stroke_flow_alpha(dab, dab->opacity, dab->flow, sample_opacity_scale,
448 pixel_eval->profile, pixel_eval->brush_alpha, old_alpha,
449 pixel_eval->stroke_old_alpha, !IS_NULL_PTR(pixel_eval->stroke_alpha));
450 return pixel_eval->src_alpha > 0.0f;
451}
452
457static gboolean _prepare_blur_context(dt_aligned_pixel_simd_t *blur_px, const float *buffer, const int width,
458 const int height, const int source_origin_x, const int source_origin_y,
459 const int patch_origin_x, const int patch_origin_y,
461{
462 if(IS_NULL_PTR(blur_px)) return FALSE;
463 float blur_weight_sum = 0.0f;
464 dt_aligned_pixel_simd_t blur_sum = dt_simd_set1(0.0f);
465
466 for(int y = view->bounds.nw[1]; y < view->bounds.se[1]; y++)
467 {
468 const float dy = ((float)y + 0.5f - view->center_y) * view->inv_radius;
469 const float dy2 = dy * dy;
470 for(int x = view->bounds.nw[0]; x < view->bounds.se[0]; x++)
471 {
472 const float dx = ((float)x + 0.5f - view->center_x) * view->inv_radius;
473 const float blur_weight = dt_drawlayer_brush_profile_eval_fast(&view->profile, dx * dx + dy2);
474 if(blur_weight <= 0.0f) continue;
475
476 const int source_x = x + patch_origin_x - source_origin_x;
477 const int source_y = y + patch_origin_y - source_origin_y;
478 if(source_x < 0 || source_y < 0 || source_x >= width || source_y >= height) continue;
479 const float *pixel = buffer + 4 * ((size_t)source_y * width + source_x);
480 blur_sum += dt_load_simd(pixel) * dt_simd_set1(blur_weight);
481 blur_weight_sum += blur_weight;
482 }
483 }
484
485 if(blur_weight_sum <= 1e-8f) return FALSE;
486 *blur_px = blur_sum * dt_simd_set1(1.0f / blur_weight_sum);
487 return TRUE;
488}
489
490
492static inline float _smudge_hash_signed(const int x, const int y, const int lane)
493{
494 guint32 h = (guint32)(x * 73856093u) ^ (guint32)(y * 19349663u) ^ (guint32)(lane * 83492791u);
495 h ^= h >> 13;
496 h *= 1274126177u;
497 h ^= h >> 16;
498 return ((h & 0xffffu) / 32767.5f) - 1.0f;
499}
500
505static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
506_sample_rgba_float_bilinear(const float *buffer, const int width, const int height, const float x,
507 const float y)
508{
509 if(IS_NULL_PTR(buffer) || width <= 0 || height <= 0) return dt_simd_set1(0.0f);
510
511 const float fx = CLAMP(x, 0.0f, (float)(width - 1));
512 const float fy = CLAMP(y, 0.0f, (float)(height - 1));
513 const int x0 = (int)floorf(fx);
514 const int y0 = (int)floorf(fy);
515 const int x1 = MIN(width - 1, x0 + 1);
516 const int y1 = MIN(height - 1, y0 + 1);
517 const float tx = fx - x0;
518 const float ty = fy - y0;
519
520 const float *p00 = buffer + 4 * ((size_t)y0 * width + x0);
521 const float *p10 = buffer + 4 * ((size_t)y0 * width + x1);
522 const float *p01 = buffer + 4 * ((size_t)y1 * width + x0);
523 const float *p11 = buffer + 4 * ((size_t)y1 * width + x1);
524 const dt_aligned_pixel_simd_t p00v = dt_load_simd(p00);
525 const dt_aligned_pixel_simd_t p10v = dt_load_simd(p10);
526 const dt_aligned_pixel_simd_t p01v = dt_load_simd(p01);
527 const dt_aligned_pixel_simd_t p11v = dt_load_simd(p11);
528 const dt_aligned_pixel_simd_t txv = dt_simd_set1(tx);
529 const dt_aligned_pixel_simd_t tyv = dt_simd_set1(ty);
530 const dt_aligned_pixel_simd_t av = p00v + (p10v - p00v) * txv;
531 const dt_aligned_pixel_simd_t bv = p01v + (p11v - p01v) * txv;
532 return av + (bv - av) * tyv;
533}
534
540static inline __attribute__((always_inline)) dt_aligned_pixel_simd_t
541_sample_smudge_source_float(const float *buffer, const int width, const int height, const float sx,
542 const float sy, const float motion_dx, const float motion_dy,
543 const int jitter_x, const int jitter_y)
544{
545 dt_aligned_pixel_simd_t rgba_sum = dt_simd_set1(0.0f);
546 if(IS_NULL_PTR(buffer) || width <= 0 || height <= 0) return rgba_sum;
547
548 float dir_x = motion_dx;
549 float dir_y = motion_dy;
550 const float motion = hypotf(dir_x, dir_y);
551 if(motion > 1e-6f)
552 {
553 dir_x /= motion;
554 dir_y /= motion;
555 }
556 else
557 {
558 dir_x = 1.0f;
559 dir_y = 0.0f;
560 }
561
562 const float perp_x = -dir_y;
563 const float perp_y = dir_x;
564 const float jitter = 0.60f * _smudge_hash_signed(jitter_x, jitter_y, 0);
565 const float side = 0.90f + 0.30f * _smudge_hash_signed(jitter_x, jitter_y, 1);
566 const float trail = 0.80f + 0.25f * _smudge_hash_signed(jitter_x, jitter_y, 2);
567
568 const float taps[7][3] = {
569 { 0.00f, jitter, 0.24f },
570 { -trail, 0.25f + jitter, 0.18f },
571 { -0.45f, -0.35f + jitter, 0.15f },
572 { -0.15f, side + jitter, 0.11f },
573 { -0.15f, -side + jitter, 0.11f },
574 { 0.25f, 0.45f * side + jitter, 0.11f },
575 { 0.25f, -0.45f * side + jitter, 0.10f },
576 };
577
578 float weight_sum = 0.0f;
579 for(int i = 0; i < 7; i++)
580 {
581 const float px = sx + dir_x * taps[i][0] + perp_x * taps[i][1];
582 const float py = sy + dir_y * taps[i][0] + perp_y * taps[i][1];
583 const float w = taps[i][2];
584 rgba_sum += _sample_rgba_float_bilinear(buffer, width, height, px, py) * dt_simd_set1(w);
585 weight_sum += w;
586 }
587
588 if(weight_sum > 1e-8f)
589 rgba_sum *= dt_simd_set1(1.0f / weight_sum);
590 return rgba_sum;
591}
592
594static inline float _smudge_deposit_alpha(const float src_alpha, const float carried_alpha, const float opacity)
595{
596 const float carry = _clamp01(carried_alpha);
597 const float base = _clamp01(opacity);
598 const float influence = base + (1.0f - base) * carry;
599 return _clamp01(src_alpha * influence);
600}
601
606static dt_aligned_pixel_simd_t _apply_smudge_stroke_mode(const float *source_buffer, const int source_width,
607 const int source_height, const int source_origin_x,
608 const int source_origin_y,
609 dt_drawlayer_paint_stroke_t *runtime_private,
611 const float scale, const int patch_origin_x,
612 const int patch_origin_y, const int x, const int y,
613 const float src_alpha,
614 const dt_aligned_pixel_simd_t old_px)
615{
616 const dt_drawlayer_brush_dab_t *dab = view->dab;
617 const float source_x_offset = (float)(patch_origin_x - source_origin_x);
618 const float source_y_offset = (float)(patch_origin_y - source_origin_y);
619 const float center_abs_x = view->center_x + (float)patch_origin_x;
620 const float center_abs_y = view->center_y + (float)patch_origin_y;
621 float sample_x = (float)x + source_x_offset;
622 float sample_y = (float)y + source_y_offset;
623 float motion_dx = 0.0f;
624 float motion_dy = 0.0f;
625 float *smudge_pixels = dt_drawlayer_paint_runtime_smudge_pixels(runtime_private);
626 const int smudge_width = dt_drawlayer_paint_runtime_smudge_width(runtime_private);
627
628 if(runtime_private && dt_drawlayer_paint_runtime_have_smudge_pickup(runtime_private))
629 {
630 /* Smudge samples from a lagging pickup point that follows dab centers. */
631 float pickup_center_x = 0.0f;
632 float pickup_center_y = 0.0f;
633 dt_drawlayer_paint_runtime_get_smudge_pickup(runtime_private, &pickup_center_x, &pickup_center_y);
634 pickup_center_x *= scale;
635 pickup_center_y *= scale;
636 sample_x += pickup_center_x - center_abs_x;
637 sample_y += pickup_center_y - center_abs_y;
638 motion_dx = center_abs_x - pickup_center_x;
639 motion_dy = center_abs_y - pickup_center_y;
640 }
641
642 const dt_aligned_pixel_simd_t sampled_px
643 = _sample_smudge_source_float(source_buffer, source_width, source_height, sample_x, sample_y,
644 motion_dx, motion_dy,
645 x - view->bounds.nw[0], y - view->bounds.nw[1]);
646
647 if(IS_NULL_PTR(smudge_pixels) || smudge_width <= 0) return old_px;
648 float *carry = smudge_pixels + 4 * ((size_t)(y - view->bounds.nw[1]) * smudge_width + (x - view->bounds.nw[0]));
649 const dt_aligned_pixel_simd_t carried_px = dt_load_simd(carry);
650 const float pickup_blend = _clamp01(dab->opacity);
651 const float carried_alpha = _clamp01(carry[3]);
652 const float deposit_alpha = _smudge_deposit_alpha(src_alpha, carried_alpha, dab->opacity);
653 const float inv_alpha = 1.0f - deposit_alpha;
654
655 const dt_aligned_pixel_simd_t out_px = carried_px * dt_simd_set1(deposit_alpha)
656 + old_px * dt_simd_set1(inv_alpha);
657 const dt_aligned_pixel_simd_t next_carry = carried_px
658 + (sampled_px - carried_px) * dt_simd_set1(pickup_blend);
659 dt_store_simd(carry, next_carry);
660 return out_px;
661}
662
668 dt_drawlayer_cache_patch_t *patch, const float scale,
669 const dt_drawlayer_brush_dab_t *dab,
670 const float sample_opacity_scale,
671 dt_drawlayer_cache_patch_t *stroke_mask,
672 dt_drawlayer_paint_stroke_t *runtime_private)
673{
674 /* Entry point for dab-level rasterization.
675 * Steps:
676 * 1) precompute dab bounds and orientation,
677 * 2) resolve per-pixel alpha (profile/noise/flow),
678 * 3) blend into target buffer and update stroke-local alpha mask. */
679 if(IS_NULL_PTR(patch) || IS_NULL_PTR(patch->pixels) || IS_NULL_PTR(dab) || !runtime_private) return FALSE;
680 if(dab->radius <= 0.0f || dab->opacity <= 0.0f || scale <= 0.0f) return FALSE;
681
682 float *const buffer = patch->pixels;
683 const int width = patch->width;
684 const int height = patch->height;
685 const int origin_x = patch->x;
686 const int origin_y = patch->y;
687 const dt_drawlayer_cache_patch_t *const source_patch
688 = (sample_patch && sample_patch->pixels) ? sample_patch : patch;
689 const float *const source_buffer = source_patch->pixels;
690 const int source_width = source_patch->width;
691 const int source_height = source_patch->height;
692 const int source_origin_x = source_patch->x;
693 const int source_origin_y = source_patch->y;
694 float *const stroke_mask_pixels = stroke_mask ? stroke_mask->pixels : NULL;
695 const int stroke_mask_width = stroke_mask ? stroke_mask->width : 0;
696 const int stroke_mask_height = stroke_mask ? stroke_mask->height : 0;
697
698 dt_drawlayer_brush_dab_t prepared = *dab;
699 prepared.opacity = _clamp01(prepared.opacity);
700 /* Internal flow convention is inverse of UI flow. */
701 prepared.flow = 1.0f - _clamp01(prepared.flow);
702
703 if(!dt_drawlayer_paint_runtime_prepare_dab_context(runtime_private, &prepared, width, height,
704 origin_x, origin_y, scale))
705 return FALSE;
706
708 if(!_brush_runtime_view_from_state(runtime_private, &prepared, origin_x, origin_y, scale, &view))
709 return FALSE;
711
712 dt_aligned_pixel_simd_t blur_px = dt_simd_set1(0.0f);
713 switch(view.dab->mode)
714 {
716 if(!_prepare_blur_context(&blur_px, source_buffer, source_width, source_height, source_origin_x,
717 source_origin_y, origin_x, origin_y, &view))
718 return FALSE;
719 break;
722 view.bounds.se[0] - view.bounds.nw[0],
723 view.bounds.se[1] - view.bounds.nw[1]))
724 return FALSE;
725 break;
728 default:
729 break;
730 }
731
732 if(view.dab->mode == DT_DRAWLAYER_BRUSH_MODE_SMUDGE)
733 {
734 for(int y = view.bounds.nw[1]; y < view.bounds.se[1]; y++)
735 {
736 for(int x = view.bounds.nw[0]; x < view.bounds.se[0]; x++)
737 {
738 float *pixel = buffer + 4 * ((size_t)y * width + x);
739 const float old_alpha = _clamp01(pixel[3]);
740 const dt_aligned_pixel_simd_t old_px = (old_alpha > 1e-8f) ? dt_load_simd(pixel) : dt_simd_set1(0.0f);
741 dt_drawlayer_brush_pixel_eval_t pixel_eval = { 0 };
742 if(!_prepare_analytic_pixel_context(&view, sample_opacity_scale,
743 stroke_mask_pixels, stroke_mask_width, stroke_mask_height,
744 x, y, old_alpha, &pixel_eval))
745 continue;
746
747 const dt_aligned_pixel_simd_t out_px
748 = _apply_smudge_stroke_mode(source_buffer, source_width, source_height, source_origin_x,
749 source_origin_y, runtime_private, &view,
750 scale, origin_x, origin_y,
751 x, y, pixel_eval.src_alpha, old_px);
752 dt_store_simd(pixel, out_px);
753
754 if(pixel_eval.stroke_alpha)
755 {
756 *pixel_eval.stroke_alpha
757 = pixel_eval.src_alpha
758 + pixel_eval.stroke_old_alpha * (1.0f - pixel_eval.src_alpha);
759 }
760 }
761 }
762 }
763 else
764 {
765#if defined(_OPENMP) && !OUTER_LOOP
766#pragma omp parallel for default(firstprivate) collapse(2)
767#endif
768 for(int y = view.bounds.nw[1]; y < view.bounds.se[1]; y++)
769 {
770 for(int x = view.bounds.nw[0]; x < view.bounds.se[0]; x++)
771 {
772 float *pixel = buffer + 4 * ((size_t)y * width + x);
773 const float old_alpha = _clamp01(pixel[3]);
774 const dt_aligned_pixel_simd_t old_px = (old_alpha > 1e-8f) ? dt_load_simd(pixel) : dt_simd_set1(0.0f);
775 dt_drawlayer_brush_pixel_eval_t pixel_eval = { 0 };
776 if(!_prepare_analytic_pixel_context(&view, sample_opacity_scale,
777 stroke_mask_pixels, stroke_mask_width, stroke_mask_height,
778 x, y, old_alpha, &pixel_eval))
779 continue;
780
781 dt_aligned_pixel_simd_t out_px;
782 const dt_aligned_pixel_simd_t inv_alpha = dt_simd_set1(1.0f - pixel_eval.src_alpha);
783 switch(view.dab->mode)
784 {
786 out_px = old_px * inv_alpha;
787 break;
789 out_px = blur_px * dt_simd_set1(pixel_eval.src_alpha) + old_px * inv_alpha;
790 break;
792 default:
793 out_px = dt_load_simd(view.dab->color) * dt_simd_set1(pixel_eval.src_alpha) + old_px * inv_alpha;
794 break;
795 }
796
797 dt_store_simd(pixel, out_px);
798
799 if(pixel_eval.stroke_alpha)
800 {
801 *pixel_eval.stroke_alpha
802 = pixel_eval.src_alpha
803 + pixel_eval.stroke_old_alpha * (1.0f - pixel_eval.src_alpha);
804 }
805 }
806 }
807 }
808
809 return TRUE;
810}
811
812/* ---------------------------------------------------------------------------
813 * Batch path: accumulate coverage once, composite once.
814 *
815 * At the shipped defaults the spacing is 1 layer px against a 128 px diameter, so every
816 * stroke pixel is composited ~128 times, each time a full RGBA read-modify-write. It does
817 * not have to be. With the stroke mask present and the internal flow at 0, the per-pixel
818 * alpha reduces to
819 *
820 * capped = min(ba, (cap - s) / (1 - s)) and s' = 1 - (1 - capped)(1 - s)
821 *
822 * and the two cases of that min collapse to the single closed form
823 *
824 * s' = min( 1 - (1 - ba)(1 - s), cap )
825 *
826 * -- the cap is ABSORBING: once it binds, every later dab leaves s at cap. So over a batch
827 *
828 * s_final = min( 1 - PROD_i (1 - ba_i) * (1 - s_0), cap )
829 *
830 * a product of per-dab transmittance factors, clamped once at the end. Two consequences:
831 *
832 * 1) The product is commutative and associative, so pass 1 is ORDER-INDEPENDENT. That is
833 * what lets it run lock-free over disjoint rows and be deterministic -- the tile-lock
834 * grid it replaces gave mutual exclusion but never ordering, so the old parallel path
835 * produced a different picture run to run.
836 * 2) Only the product is needed per pixel, not the colour. Pass 1 touches 4 bytes and
837 * never loads the destination; pass 2 does one float4 composite over the batch bbox.
838 *
839 * TRANSMITTANCE, not coverage, is what is accumulated: `T *= (1 - ba)` reproduces the
840 * existing sequence of operations exactly, whereas the algebraically equal
841 * `A = 1 - (1 - A)(1 - ba)` would drift by an ulp per dab because `1 - (1 - x)` is not
842 * exact in binary floating point. It is also one multiply instead of three operations.
843 *
844 * The derivation assumes a SHARED cap, colour and mode across the batch, and an internal
845 * flow of 0. `dt_drawlayer_brush_batch_is_uniform` is that gate; anything else -- a
846 * pressure-to-opacity map, a mid-batch mode change, SMUDGE or BLUR (which genuinely read
847 * the destination per dab) -- falls back to the serial per-dab path.
848 */
849
852 const int x, const int y)
853{
854 const float dy = ((float)y + 0.5f - view->center_y) * view->inv_radius;
855 const float dx = ((float)x + 0.5f - view->center_x) * view->inv_radius;
856 const float profile = dt_drawlayer_brush_profile_eval_fast(&view->profile, dx * dx + dy * dy);
857 if(profile <= 0.0f) return 0.0f;
858
859 const float alpha_noise = fmaxf(0.0f, _sample_alpha_noise_raw(view->dab, view,
860 view->sample_origin_x + x,
861 view->sample_origin_y + y)
862 * view->alpha_noise_gain);
863 return _clamp01(view->dab->opacity * profile * alpha_noise);
864}
865
868 const int x, const int y, const float raw_noise)
869{
870 const float dy = ((float)y + 0.5f - view->center_y) * view->inv_radius;
871 const float dx = ((float)x + 0.5f - view->center_x) * view->inv_radius;
872 const float profile = dt_drawlayer_brush_profile_eval_fast(&view->profile, dx * dx + dy * dy);
873 if(profile <= 0.0f) return 0.0f;
874
875 const float alpha_noise = fmaxf(0.0f, raw_noise * view->alpha_noise_gain);
876 return _clamp01(view->dab->opacity * profile * alpha_noise);
877}
878
879gboolean dt_drawlayer_brush_batch_is_uniform(const dt_drawlayer_brush_dab_t *dabs, const guint count,
881{
882 if(IS_NULL_PTR(dabs) || count == 0 || IS_NULL_PTR(out)) return FALSE;
883
884 const dt_drawlayer_brush_dab_t *const first = &dabs[0];
886 return FALSE;
887 /* The rasterizer inverts flow: UI 100% -> internal 0, which is the regime the closed form
888 * above is derived for. Anything else keeps `accum_alpha`, whose dependence on the running
889 * destination alpha does not collapse into a product. */
890 if(_clamp01(first->flow) < 1.0f) return FALSE;
891
892 for(guint i = 1; i < count; i++)
893 {
894 const dt_drawlayer_brush_dab_t *const d = &dabs[i];
895 if(d->mode != first->mode) return FALSE;
896 if(d->opacity != first->opacity) return FALSE;
897 if(_clamp01(d->flow) < 1.0f) return FALSE;
898 if(d->color[0] != first->color[0] || d->color[1] != first->color[1]
899 || d->color[2] != first->color[2] || d->color[3] != first->color[3])
900 return FALSE;
901 /* The sprinkle field is shared across the batch, so its parameters must be too. */
902 if(d->sprinkles != first->sprinkles || d->sprinkle_size != first->sprinkle_size
903 || d->sprinkle_coarseness != first->sprinkle_coarseness
904 || d->stroke_batch != first->stroke_batch)
905 return FALSE;
906 }
907
908 out->dabs = dabs;
909 out->count = count;
910 out->mode = first->mode;
911 out->cap = _clamp01(first->opacity);
912 for(int c = 0; c < 4; c++) out->color[c] = first->color[c];
913 out->sprinkles = (first->sprinkles > 1e-6f);
914 return TRUE;
915}
916
918 dt_drawlayer_cache_patch_t *patch, const float scale,
919 dt_drawlayer_cache_patch_t *stroke_mask,
920 float *const transmittance,
921 float *const noise_scratch,
922 dt_drawlayer_damaged_rect_t *batch_damage)
923{
924 if(IS_NULL_PTR(batch) || IS_NULL_PTR(patch) || IS_NULL_PTR(patch->pixels) || IS_NULL_PTR(stroke_mask)
925 || IS_NULL_PTR(stroke_mask->pixels) || IS_NULL_PTR(transmittance) || batch->count == 0 || scale <= 0.0f)
926 return FALSE;
927
928 const int width = patch->width;
929 const int height = patch->height;
930 if(width <= 0 || height <= 0) return FALSE;
931 if(stroke_mask->width != width || stroke_mask->height != height) return FALSE;
932
933 /* Per-dab setup is serial and cheap -- one view per dab, no per-thread runtime objects, no
934 * locks, and the batch bounding box falls out of the same walk that builds them (the old
935 * path computed it twice, in two different coordinate frames). */
936 dt_drawlayer_brush_dab_t *const prepared = g_malloc_n(batch->count, sizeof(*prepared));
937 dt_drawlayer_brush_runtime_view_t *const views = g_malloc_n(batch->count, sizeof(*views));
938 if(IS_NULL_PTR(prepared) || IS_NULL_PTR(views))
939 {
940 g_free(prepared);
941 g_free(views);
942 return FALSE;
943 }
944
945 guint live = 0;
947 for(guint i = 0; i < batch->count; i++)
948 {
949 dt_drawlayer_brush_dab_t dab = batch->dabs[i];
950 dab.opacity = _clamp01(dab.opacity);
951 dab.flow = 1.0f - _clamp01(dab.flow);
952 if(dab.radius <= 0.0f || dab.opacity <= 0.0f) continue;
953
954 dt_drawlayer_damaged_rect_t bounds = { .valid = TRUE };
955 bounds.nw[0] = MAX(0, (int)floorf((dab.x - dab.radius) * scale) - patch->x);
956 bounds.nw[1] = MAX(0, (int)floorf((dab.y - dab.radius) * scale) - patch->y);
957 bounds.se[0] = MIN(width, (int)ceilf((dab.x + dab.radius) * scale) - patch->x + 1);
958 bounds.se[1] = MIN(height, (int)ceilf((dab.y + dab.radius) * scale) - patch->y + 1);
959 if(bounds.se[0] <= bounds.nw[0] || bounds.se[1] <= bounds.nw[1]) continue;
960
961 prepared[live] = dab;
962 if(!_brush_runtime_view_from_bounds(&bounds, &prepared[live], patch->x, patch->y, scale, &views[live]))
963 continue;
964 views[live].alpha_noise_gain = _estimate_alpha_noise_gain(&prepared[live], &views[live]);
966 live++;
967 }
968
969 if(live == 0 || !bbox.valid)
970 {
971 g_free(prepared);
972 g_free(views);
973 return FALSE;
974 }
975
976 const int y0 = bbox.nw[1];
977 const int y1 = bbox.se[1];
978 const int x0 = bbox.nw[0];
979 const int x1 = bbox.se[0];
980
981 /* The sprinkle field is a function of LAYER position and the stroke's seed, so every dab of
982 * the batch samples the same value at the same pixel -- and `_cellular_grain_2d` costs 9
983 * cells x 4 splitmix32 per octave, up to 108 hashes. Evaluating it once over the batch box
984 * instead of once per dab per pixel divides that by the overdraw factor, which at the
985 * default spacing is the whole point. The per-dab `alpha_noise_gain` stays per dab; only
986 * the raw field is shared. */
987 const gboolean shared_noise = batch->sprinkles && !IS_NULL_PTR(noise_scratch) && views[0].have_sprinkles;
988 if(shared_noise)
989 {
990#ifdef _OPENMP
991#pragma omp parallel for default(firstprivate) schedule(static)
992#endif
993 for(int y = y0; y < y1; y++)
994 {
995 float *const nrow = noise_scratch + (size_t)y * width;
996 for(int x = x0; x < x1; x++)
997 nrow[x] = _sample_alpha_noise_raw(views[0].dab, &views[0], views[0].sample_origin_x + x,
998 views[0].sample_origin_y + y);
999 }
1000 }
1001
1002 /* Pass 1 -- transmittance. A row is owned by exactly one thread, so the writes are
1003 * disjoint and no lock is needed; dabs are still applied in index order within a row,
1004 * so the result does not depend on the schedule. */
1005#ifdef _OPENMP
1006#pragma omp parallel for default(firstprivate) schedule(static)
1007#endif
1008 for(int y = y0; y < y1; y++)
1009 {
1010 float *const row = transmittance + (size_t)y * width;
1011 const float *const nrow = shared_noise ? (noise_scratch + (size_t)y * width) : NULL;
1012 for(int x = x0; x < x1; x++) row[x] = 1.0f;
1013
1014 for(guint i = 0; i < live; i++)
1015 {
1017 if(y < view->bounds.nw[1] || y >= view->bounds.se[1]) continue;
1018
1019 for(int x = view->bounds.nw[0]; x < view->bounds.se[0]; x++)
1020 {
1021 const float brush_alpha
1022 = nrow ? _brush_alpha_at_noise(view, x, y, nrow[x]) : _brush_alpha_at(view, x, y);
1023 if(brush_alpha <= 0.0f) continue;
1024 row[x] *= (1.0f - brush_alpha);
1025 }
1026 }
1027 }
1028
1029 /* Pass 2 -- one composite over the batch box. */
1030 const gboolean erase = (batch->mode == DT_DRAWLAYER_BRUSH_MODE_ERASE);
1031 const float cap = batch->cap;
1032
1033#ifdef _OPENMP
1034#pragma omp parallel for default(firstprivate) schedule(static)
1035#endif
1036 for(int y = y0; y < y1; y++)
1037 {
1038 const float *const trow = transmittance + (size_t)y * width;
1039 float *const mrow = stroke_mask->pixels + (size_t)y * width;
1040 float *const prow = patch->pixels + 4 * ((size_t)y * width);
1041
1042 for(int x = x0; x < x1; x++)
1043 {
1044 const float t = trow[x];
1045 if(t >= 1.0f) continue; /* no dab of this batch reached the pixel */
1046
1047 const float stroke_old_alpha = _clamp01(mrow[x]);
1048 float stroke_alpha = 1.0f - t * (1.0f - stroke_old_alpha);
1049 if(stroke_alpha > cap) stroke_alpha = cap;
1050
1051 /* The increment that, composited over what the stroke already laid down, lands on
1052 * `stroke_alpha`. Negative when the cap was already exceeded -- clamped to nothing,
1053 * exactly as the per-dab `remaining_to_cap` was. */
1054 const float src_alpha
1055 = _clamp01((stroke_alpha - stroke_old_alpha) / fmaxf(1.0f - stroke_old_alpha, 1e-6f));
1056 if(src_alpha <= 0.0f) continue;
1057
1058 float *const pixel = prow + 4 * x;
1059 const float old_alpha = _clamp01(pixel[3]);
1060 const dt_aligned_pixel_simd_t old_px = (old_alpha > 1e-8f) ? dt_load_simd(pixel) : dt_simd_set1(0.0f);
1061 const dt_aligned_pixel_simd_t inv_alpha = dt_simd_set1(1.0f - src_alpha);
1062 const dt_aligned_pixel_simd_t out_px
1063 = erase ? (old_px * inv_alpha)
1064 : (dt_load_simd(batch->color) * dt_simd_set1(src_alpha) + old_px * inv_alpha);
1065 dt_store_simd(pixel, out_px);
1066 mrow[x] = stroke_alpha;
1067 }
1068 }
1069
1070 if(!IS_NULL_PTR(batch_damage)) dt_drawlayer_paint_runtime_note_dab_damage(batch_damage, &bbox);
1071
1072 g_free(prepared);
1073 g_free(views);
1074 return TRUE;
1075}
1076
1082 const int width, const int height, const int stride,
1083 const float center_x, const float center_y,
1084 const float opacity_multiplier)
1085{
1086 /* Lightweight GUI preview dab renderer (ARGB8), independent from stroke logic. */
1087 if(IS_NULL_PTR(dab) || IS_NULL_PTR(argb) || width <= 0 || height <= 0 || stride < 4 * width) return FALSE;
1088
1089 memset(argb, 0, (size_t)stride * height);
1090 if(dab->radius <= 0.0f) return FALSE;
1091
1093 preview.opacity = _clamp01(preview.opacity * _clamp01(opacity_multiplier));
1094 if(preview.opacity <= 0.0f) return FALSE;
1095
1096 const float radius = fmaxf(preview.radius, 0.5f);
1097 const float inv_radius = 1.0f / radius;
1098 const int x0 = (int)fmaxf(0.0f, floorf(center_x - radius));
1099 const int y0 = (int)fmaxf(0.0f, floorf(center_y - radius));
1100 const int x1 = (int)fminf((float)width, ceilf(center_x + radius) + 1.0f);
1101 const int y1 = (int)fminf((float)height, ceilf(center_y + radius) + 1.0f);
1102
1103 const float disp_r = _clamp01(preview.display_color[0]);
1104 const float disp_g = _clamp01(preview.display_color[1]);
1105 const float disp_b = _clamp01(preview.display_color[2]);
1106 dt_drawlayer_sprinkle_preview_t sprinkle = { 0 };
1107 _prepare_sprinkle_preview(&preview, center_x, center_y, radius, &sprinkle);
1108
1109 for(int y = y0; y < y1; y++)
1110 {
1111 const float dy = ((float)y + 0.5f - center_y) * inv_radius;
1112 const float dy2 = dy * dy;
1113 for(int x = x0; x < x1; x++)
1114 {
1115 const float dx = ((float)x + 0.5f - center_x) * inv_radius;
1116 const float profile = dt_drawlayer_brush_profile_eval(&preview, dx * dx + dy2);
1117 if(profile <= 0.0f) continue;
1118
1119 const float alpha_noise = _sample_sprinkle_preview(&sprinkle, x, y);
1120 const float alpha = _clamp01(preview.opacity * profile * alpha_noise);
1121 if(alpha <= 0.0f) continue;
1122
1123 uint8_t *pixel = argb + (size_t)y * stride + 4 * x;
1124 pixel[0] = (uint8_t)fminf(fmaxf(roundf(255.0f * _clamp01(disp_b * alpha)), 0.0f), 255.0f);
1125 pixel[1] = (uint8_t)fminf(fmaxf(roundf(255.0f * _clamp01(disp_g * alpha)), 0.0f), 255.0f);
1126 pixel[2] = (uint8_t)fminf(fmaxf(roundf(255.0f * _clamp01(disp_r * alpha)), 0.0f), 255.0f);
1127 pixel[3] = (uint8_t)fminf(fmaxf(roundf(255.0f * alpha), 0.0f), 255.0f);
1128 }
1129 }
1130
1131 return TRUE;
1132}
1133
1135 const int width, const int height,
1136 const float center_x, const float center_y,
1137 const float opacity_multiplier,
1138 const float background_rgb[3])
1139{
1140 if(IS_NULL_PTR(dab) || IS_NULL_PTR(rgba) || width <= 0 || height <= 0) return FALSE;
1141
1142 const float bg_r = background_rgb ? _clamp01(background_rgb[0]) : 1.0f;
1143 const float bg_g = background_rgb ? _clamp01(background_rgb[1]) : 1.0f;
1144 const float bg_b = background_rgb ? _clamp01(background_rgb[2]) : 1.0f;
1145 for(int i = 0; i < width * height; i++)
1146 {
1147 rgba[4 * i + 0] = bg_r;
1148 rgba[4 * i + 1] = bg_g;
1149 rgba[4 * i + 2] = bg_b;
1150 rgba[4 * i + 3] = 1.0f;
1151 }
1152
1153 if(dab->radius <= 0.0f) return FALSE;
1154
1156 preview.opacity = _clamp01(preview.opacity * _clamp01(opacity_multiplier));
1157 if(preview.opacity <= 0.0f) return FALSE;
1158
1159 const float radius = fmaxf(preview.radius, 0.5f);
1160 const float inv_radius = 1.0f / radius;
1161 const int x0 = (int)fmaxf(0.0f, floorf(center_x - radius));
1162 const int y0 = (int)fmaxf(0.0f, floorf(center_y - radius));
1163 const int x1 = (int)fminf((float)width, ceilf(center_x + radius) + 1.0f);
1164 const int y1 = (int)fminf((float)height, ceilf(center_y + radius) + 1.0f);
1165
1166 const float src_r = _clamp01(preview.color[0]);
1167 const float src_g = _clamp01(preview.color[1]);
1168 const float src_b = _clamp01(preview.color[2]);
1169 dt_drawlayer_sprinkle_preview_t sprinkle = { 0 };
1170 _prepare_sprinkle_preview(&preview, center_x, center_y, radius, &sprinkle);
1171
1172 for(int y = y0; y < y1; y++)
1173 {
1174 const float dy = ((float)y + 0.5f - center_y) * inv_radius;
1175 const float dy2 = dy * dy;
1176 for(int x = x0; x < x1; x++)
1177 {
1178 const float dx = ((float)x + 0.5f - center_x) * inv_radius;
1179 const float profile = dt_drawlayer_brush_profile_eval(&preview, dx * dx + dy2);
1180 if(profile <= 0.0f) continue;
1181
1182 const float alpha_noise = _sample_sprinkle_preview(&sprinkle, x, y);
1183 const float alpha = _clamp01(preview.opacity * profile * alpha_noise);
1184 if(alpha <= 0.0f) continue;
1185
1186 float *pixel = rgba + 4 * ((size_t)y * width + x);
1187 const float inv_alpha = 1.0f - alpha;
1188 pixel[0] = src_r * alpha + pixel[0] * inv_alpha;
1189 pixel[1] = src_g * alpha + pixel[1] * inv_alpha;
1190 pixel[2] = src_b * alpha + pixel[2] * inv_alpha;
1191 pixel[3] = 1.0f;
1192 }
1193 }
1194
1195 return TRUE;
1196}
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
const char ** views(dt_lib_module_t *self)
Dab-level brush rasterization API for drawlayer.
@ DT_DRAWLAYER_BRUSH_MODE_ERASE
Definition brush.h:51
@ DT_DRAWLAYER_BRUSH_MODE_PAINT
Definition brush.h:50
@ DT_DRAWLAYER_BRUSH_MODE_BLUR
Definition brush.h:52
@ DT_DRAWLAYER_BRUSH_MODE_SMUDGE
Definition brush.h:53
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.
static float dt_drawlayer_brush_profile_eval_fast(const dt_drawlayer_brush_profile_const_t *pc, const float norm2)
Evaluate the profile against pre-resolved dab constants.
static void dt_drawlayer_brush_profile_prepare(const dt_drawlayer_brush_dab_t *dab, dt_drawlayer_brush_profile_const_t *out)
Resolve the per-dab half of the profile once.
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
return vector dt_simd_set1(valid ?(scaling+NORM_MIN) :NORM_MIN)
static const float x
const int t
const float v
const dt_colormatrix_t dt_aligned_pixel_t out
static const int row
static float strength(float value, float strength)
Definition colorzones.c:431
static unsigned int splitmix32(const unsigned long seed)
GtkWidget * preview
what the selected row actually captures
GtkWidget * view
the tree view
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)
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 gboolean _brush_runtime_view_from_bounds(const dt_drawlayer_damaged_rect_t *bounds, 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 _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.
static float _brush_alpha_at(const dt_drawlayer_brush_runtime_view_t *const view, const int x, const int y)
Per-pixel brush alpha, the part of the pixel context that owes nothing to the destination.
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.
gboolean dt_drawlayer_brush_batch_is_uniform(const dt_drawlayer_brush_dab_t *dabs, const guint count, dt_drawlayer_brush_batch_t *out)
Decide whether a run of dabs can take the batch path, and describe it if so.
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.
static float _brush_alpha_at_noise(const dt_drawlayer_brush_runtime_view_t *const view, const int x, const int y, const float raw_noise)
As _brush_alpha_at, with the shared sprinkle field already sampled.
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.
gboolean dt_drawlayer_brush_rasterize_batch(const dt_drawlayer_brush_batch_t *batch, dt_drawlayer_cache_patch_t *patch, const float scale, dt_drawlayer_cache_patch_t *stroke_mask, float *const transmittance, float *const noise_scratch, dt_drawlayer_damaged_rect_t *batch_damage)
Rasterize a whole batch in two passes: accumulate transmittance, then composite once.
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.
void dt_drawlayer_paint_runtime_note_dab_damage(dt_drawlayer_damaged_rect_t *state, const dt_drawlayer_damaged_rect_t *dab_rect)
Merge one dab rectangle into an accumulator rectangle.
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 dt_dev_pixelpipe_t const dt_dev_pixelpipe_iop_t int int int int * origin_y
Definition layers.h:52
static const dt_dev_pixelpipe_t const dt_dev_pixelpipe_iop_t int int int * origin_x
Definition layers.h:51
#define w2
Definition lmmse.c:60
#define w1
Definition lmmse.c:59
dt_map_box_t bbox
Definition location.c:4
#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 macros.h:96
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
dt_store_simd(out, value)
float dt_aligned_pixel_simd_t __attribute__((vector_size(16), aligned(16)))
Apply one channel's tone curve to each of the three colour channels, or pass the channel through unto...
Definition simd.h:55
unsigned __int64 uint64_t
Definition strptime.c:75
A run of dabs that share everything the batch composite needs to be a single step.
Definition brush.h:120
const dt_drawlayer_brush_dab_t * dabs
Definition brush.h:121
Fully resolved input dab descriptor.
Definition brush.h:66
uint32_t stroke_batch
Definition brush.h:84
The part of the profile that depends on the dab and not on the pixel.
dt_drawlayer_damaged_rect_t bounds
const dt_drawlayer_brush_dab_t * dab
dt_drawlayer_brush_profile_const_t profile
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
#define MAX(a, b)
Definition thinplate.c:29