Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
masks_geometry.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 Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
38#include "darktable.h"
39#include "develop/develop.h"
42#include "develop/masks.h"
43#include "develop/masks_debug.h"
47#include "math/math.h"
48#include "system/mem_alloc.h"
49
50#include <cairo/cairo.h>
51#include <glib.h>
52#include <glib/gstdio.h>
53#include <math.h>
54#include <stdio.h>
55#include <stdint.h>
56#include <stdlib.h>
57#include <string.h>
58
59/* The reported raw's own size. Not an arbitrary canvas: several thresholds in the outline
60 * builder are in ABSOLUTE pixels -- the recursion splits until samples are within a pixel, the
61 * arc fillers bail when the arc is under two pixels long -- so which defects appear at all is
62 * scale-dependent. Reproducing what a user sees means rendering at the size they render at. */
63#define IMG_W 5184
64#define IMG_H 3888
65
66static int failures = 0;
67
68/* ------------------------------------------------------------------------------------- */
69
70static dt_masks_node_brush_t *_brush_node(const float x, const float y, const float c1x, const float c1y,
71 const float c2x, const float c2y, const float radius)
72{
74 n->node[0] = x; n->node[1] = y;
75 n->ctrl1[0] = c1x; n->ctrl1[1] = c1y;
76 n->ctrl2[0] = c2x; n->ctrl2[1] = c2y;
77 n->border[0] = n->border[1] = radius;
78 n->density = 1.0f;
79 n->fading = 0.66f;
81 return n;
82}
83
84static dt_masks_node_polygon_t *_polygon_node_state(const float x, const float y, const float c1x, const float c1y,
85 const float c2x, const float c2y, const float radius,
87{
89 n->node[0] = x; n->node[1] = y;
90 n->ctrl1[0] = c1x; n->ctrl1[1] = c1y;
91 n->ctrl2[0] = c2x; n->ctrl2[1] = c2y;
92 n->border[0] = n->border[1] = radius;
93 /* the state is not decoration: a CUSP node has both handles on the node, which is where the
94 * curve loses its direction and where both reported defects live */
95 n->state = state;
96 return n;
97}
98
99static dt_masks_node_polygon_t *_polygon_node(const float x, const float y, const float c1x, const float c1y,
100 const float c2x, const float c2y, const float radius)
101{
102 return _polygon_node_state(x, y, c1x, c1y, c2x, c2y, radius, DT_MASKS_POINT_STATE_NORMAL);
103}
104
105/* ------------------------------------------------------------------------------------- */
106/* Geometry decoded verbatim from the XMP attached to issue #1313's follow-up (mask_id
107 * 1788089411, "brush #1"). Node 8 is the cusp: both handles collapsed onto the node, arms
108 * meeting at a sharp angle. Kept as data rather than parsed from the sidecar at run time --
109 * the test then needs no file, no database and no XMP reader to reproduce the exact shape a
110 * user reported, and a diff shows when the geometry itself is edited. */
111static const float _brush_1313[11][9] = {
112 /* node.x, node.y, ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, border, density, fading */
113 { 0.471628428f, 0.0363773443f, 0.480041265f, 0.0386592895f, 0.46321559f, 0.0340954065f, 0.00624799589f, 1.0f, 0.66f },
114 { 0.446389854f, 0.0295315273f, 0.454724789f, 0.0302576013f, 0.438054889f, 0.0288054571f, 0.00704672467f, 1.0f, 0.66f },
115 { 0.421618611f, 0.0320209153f, 0.425721943f, 0.0304650553f, 0.417515308f, 0.0335767828f, 0.00707301404f, 1.0f, 0.66f },
116 { 0.395679384f, 0.0483706258f, 0.404481769f, 0.0413173735f, 0.38687706f, 0.0554238781f, 0.0267287921f, 1.0f, 0.66f },
117 { 0.368804485f, 0.0743404329f, 0.380839676f, 0.0592248067f, 0.356769323f, 0.0894560665f, 0.00639372552f, 1.0f, 0.66f },
118 { 0.323468447f, 0.139064401f, 0.336321443f, 0.124024376f, 0.31061548f, 0.154104441f, 0.0175853837f, 1.0f, 0.66f },
119 { 0.291686505f, 0.164580554f, 0.29908672f, 0.159394339f, 0.28428629f, 0.169766784f, 0.0175853837f, 1.0f, 0.66f },
120 { 0.279067189f, 0.170181692f, 0.285639763f, 0.161496982f, 0.272494644f, 0.178866416f, 0.0175853837f, 1.0f, 0.66f },
121 { 0.252251089f, 0.216688871f, 0.252251089f, 0.216688871f, 0.252251089f, 0.216688871f, 0.0175853837f, 1.0f, 0.66f }, /* CUSP */
122 { 0.229524732f, 0.17267108f, 0.237051532f, 0.181563243f, 0.221997947f, 0.163778931f, 0.0175853837f, 1.0f, 0.66f },
123 { 0.207090423f, 0.16333589f, 0.214568526f, 0.16644761f, 0.199612319f, 0.160224169f, 0.0175853837f, 1.0f, 0.66f },
124};
125
126static GList *_brush_from_table(const float table[][9], const int count)
127{
128 GList *points = NULL;
129 for(int i = 0; i < count; i++)
130 points = g_list_append(points, _brush_node(table[i][0], table[i][1], table[i][2], table[i][3],
131 table[i][4], table[i][5], table[i][6]));
132 return points;
133}
134
135/* ------------------------------------------------------------------------------------- */
136/* The oracle for a brush.
137 *
138 * A brush stroke IS the Minkowski sum of its centreline with a disc of the node's radius, so
139 * the reference can be constructed instead of remembered: sample the Bezier densely, stamp a
140 * disc at every sample, and any pixel the reference covers but the mask does not is coverage
141 * the rasteriser owed and did not deliver.
142 *
143 * This is what "enclosed holes" could not see. The defect reported against #1313 is a V that
144 * bites INTO the stroke from outside -- it is open, connected to the background, and no
145 * hole-counting metric will ever flag it. Measuring against the disc union does, and it says
146 * what the user said: the brush lost its radius near the cusp.
147 *
148 * The same construction, run the other way, answers the question issue #1360 asks: coverage
149 * the rasteriser delivered that NO disc owes. A border sample that lands far from its
150 * centreline paints a spoke out to wherever it landed, and the mask grows a region the stroke
151 * never covered -- in that report, a circle spanning the whole frame. The owed map cannot see
152 * it: every owed pixel is painted, and then some. So two maps are built from the same discs:
153 * OWED, the smaller radius shrunk by two pixels, which the mask must cover entirely; and
154 * PERMITTED, the larger radius grown by a margin, outside which the mask must be empty. */
156
157/* One disc of integer radius @p r_i at (cx, cy) into the row difference array: a row of it
158 * reaches |dx| <= floor(sqrt(r_i^2 - dy^2)), the discretisation the per-pixel form used. */
159static inline void _stamp_disc_rows(int32_t *const diff, const size_t stride, const int w, const int h,
160 const int cx, const int cy, const int r_i)
161{
162 for(int dy = -r_i; dy <= r_i; dy++)
163 {
164 const int y = cy + dy;
165 if(y < 0 || y >= h) continue;
166 const int half = (int)floorf(sqrtf((float)(r_i * r_i - dy * dy)));
167 const int x0 = MAX(cx - half, 0);
168 const int x1 = MIN(cx + half, w - 1);
169 if(x0 > x1) continue;
170 diff[(size_t)y * stride + x0] += 1;
171 diff[(size_t)y * stride + x1 + 1] -= 1;
172 }
173}
174
175/* Stamp the disc union of the stroke into @p map. Row spans through a difference array: a
176 * disc costs O(r) rather than O(r^2), which is what makes a 43-node, 176 px stroke measurable
177 * -- the per-pixel form of this loop is ten thousand million writes for that one case. The
178 * discretisation is the one the per-pixel form used: a row of a disc of integer radius r_i
179 * reaches |dx| <= floor(sqrt(r_i^2 - dy^2)), so the owed map is identical to before. */
180static void _brush_disc_union(const GList *const nodes, const int w, const int h, const _disc_pick_t pick,
181 uint8_t *const map)
182{
183 const float radius_scale = (float)MIN(w, h);
184 memset(map, 0, (size_t)w * h);
185
186 /* one spare slot per row: a span ending at the last pixel closes at x = w, in its own row */
187 const size_t stride = (size_t)w + 1;
188 int32_t *diff = (int32_t *)calloc(stride * h, sizeof(int32_t));
189 if(IS_NULL_PTR(diff)) return;
190
191 for(const GList *l = nodes; l && l->next; l = l->next)
192 {
193 const dt_masks_node_brush_t *const n0 = (const dt_masks_node_brush_t *)l->data;
194 const dt_masks_node_brush_t *const n1 = (const dt_masks_node_brush_t *)l->next->data;
195 /* the cubic: this node, its ctrl2, the next node's ctrl1, the next node */
196 const float p0x = n0->node[0] * w;
197 const float p0y = n0->node[1] * h;
198 const float p1x = n0->ctrl2[0] * w;
199 const float p1y = n0->ctrl2[1] * h;
200 const float p2x = n1->ctrl1[0] * w;
201 const float p2y = n1->ctrl1[1] * h;
202 const float p3x = n1->node[0] * w;
203 const float p3y = n1->node[1] * h;
204
205 /* The segment runs from this node's OUTGOING radius to the next node's INCOMING one --
206 * border[1] then border[0] -- which is how the builder reads them (see the pa/pb rows in
207 * _brush_get_pts_border()). Nodes with one radius store it in both. */
208 const float r_out = n0->border[1] * radius_scale;
209 const float r_in = n1->border[0] * radius_scale;
210
211 /* OWED: the SMALLER of the two node radii, not the interpolated one.
212 *
213 * A disc union and a normal-offset stroke are not the same shape wherever the radius is
214 * changing. The implementation offsets the centreline along its normal by the local
215 * radius; the envelope of a growing disc family leans outward from that normal by
216 * asin(dr/ds), so across a fast radius transition the disc union genuinely covers more
217 * than the rasteriser owes. Asserting the interpolated radius there flagged 3552 px of
218 * crescents hugging the OUTSIDE of the widest bulge -- a real difference between two
219 * definitions, and not a defect under either of them.
220 *
221 * The smaller radius is what both definitions agree on, so that is the strongest claim
222 * this oracle can honestly make. It costs nothing where it matters: a cusp is a direction
223 * reversal, not a radius change, so the two nodes bracketing one have near-equal radii and
224 * the V hole is still caught at full strength (verified by re-running the corpus against
225 * master, which still fails this case).
226 *
227 * Shrink by two pixels. A disc and a rasterised stroke never agree exactly along the
228 * perimeter -- the stroke is stamped from discrete spokes and anti-aliased -- so the
229 * outermost ring would report a one-pixel sliver on every well-behaved case and drown
230 * the signal. Two pixels in, any disagreement is interior, which is the only kind that
231 * means the stroke is missing.
232 *
233 * PERMITTED: the LARGER radius, grown by three pixels: one for the spoke's extra neighbour
234 * write, the rest for the same perimeter disagreement in the other direction. Nothing a
235 * correct stroke paints can lie outside it. */
236 const float r = (pick == DISC_OWED) ? MIN(r_out, r_in) : MAX(r_out, r_in);
237 const int r_i = (pick == DISC_OWED) ? MAX((int)floorf(r) - 2, 0) : (int)ceilf(r) + 3;
238
239 for(int k = 0; k <= 2000; k++)
240 {
241 const float t = (float)k / 2000.0f;
242 const float u = 1.0f - t;
243 const float bx = u*u*u*p0x + 3*u*u*t*p1x + 3*u*t*t*p2x + t*t*t*p3x;
244 const float by = u*u*u*p0y + 3*u*u*t*p1y + 3*u*t*t*p2y + t*t*t*p3y;
245 _stamp_disc_rows(diff, stride, w, h, (int)lrintf(bx), (int)lrintf(by), r_i);
246 }
247 }
248
249 for(int y = 0; y < h; y++)
250 {
251 int32_t running = 0;
252 for(int x = 0; x < w; x++)
253 {
254 running += diff[(size_t)y * stride + x];
255 map[(size_t)y * w + x] = (running > 0) ? 1 : 0;
256 }
257 }
258 free(diff);
259}
260
262
264typedef struct _runs_t
265{
266 int total;
268 int cx;
269 int cy;
271
272/* One connected component of @p flag from @p seed, 4-connected: its size and the sum of its
273 * coordinates, so the caller can place it. @p seen is marked as it goes. */
274static inline void _flood_component(const uint8_t *const flag, uint8_t *const seen, int *const stack, const int w,
275 const int h, const size_t seed, long *const sum)
276{
277 const size_t npix = (size_t)w * h;
278 int top = 0;
279 stack[top++] = (int)seed;
280 seen[seed] = 1;
281 sum[0] = 0;
282 sum[1] = 0;
283 sum[2] = 0;
284 while(top > 0)
285 {
286 const int cur = stack[--top];
287 const int px = cur % w;
288 const int py = cur / w;
289 sum[0]++;
290 sum[1] += px;
291 sum[2] += py;
292 const int dx[4] = { 1, -1, 0, 0 };
293 const int dy[4] = { 0, 0, 1, -1 };
294 for(int k = 0; k < 4; k++)
295 {
296 const int nx = px + dx[k];
297 const int ny = py + dy[k];
298 if(nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
299 const size_t ni = (size_t)ny * w + nx;
300 if(!flag[ni] || seen[ni]) continue;
301 seen[ni] = 1;
302 /* The push is bounded because a cell is marked BEFORE it is pushed, so none can enter
303 * the stack twice and `top' cannot pass npix. Stating that in code rather than only in
304 * a comment costs one compare per neighbour, and removes the reader's -- and the
305 * analyser's -- obligation to reconstruct the argument from two places. */
306 if((size_t)top < npix) stack[top++] = (int)ni;
307 }
308 }
309}
310
313static void _coverage_runs(const float *mask, const uint8_t *reference, const int w, const int h,
314 const _runs_mode_t mode, _runs_t *const out)
315{
316 out->total = 0;
317 out->largest = 0;
318 out->cx = -1;
319 out->cy = -1;
320
321 /* A frame with no pixels has nothing to measure, and saying so here is not only defensive: it
322 * is what lets a reader (and a static analyser) bound every index below. */
323 if(w <= 0 || h <= 0) return;
324 const size_t npix = (size_t)w * h;
325
326 uint8_t *flag = (uint8_t *)calloc(npix, 1);
327 uint8_t *seen = (uint8_t *)calloc(npix, 1);
328 int *stack = (int *)malloc(sizeof(int) * npix);
329 if(IS_NULL_PTR(flag) || IS_NULL_PTR(seen) || IS_NULL_PTR(stack))
330 {
331 free(flag);
332 free(seen);
333 free(stack);
334 return;
335 }
336
337 /* ANY coverage counts, not a thresholded core. `border' is the OUTER radius: the stroke is
338 * solid in the middle and fades to zero at that edge, so most of the disc legitimately holds
339 * values below a half. What cannot be legitimate is a pixel the disc covers with no coverage
340 * at all -- that is the stroke missing, which is the defect this corpus is about. */
341 for(size_t i = 0; i < npix; i++)
342 {
343 const gboolean flagged = (mode == RUNS_MISSING) ? (reference[i] && mask[i] <= 0.0f)
344 : (!reference[i] && mask[i] > 0.0f);
345 if(!flagged) continue;
346 flag[i] = 1;
347 out->total++;
348 }
349
350 for(size_t seed = 0; seed < npix; seed++)
351 {
352 if(!flag[seed] || seen[seed]) continue;
353 long sum[3];
354 _flood_component(flag, seen, stack, w, h, seed, sum);
355 if(sum[0] <= out->largest) continue;
356 out->largest = (int)sum[0];
357 out->cx = (int)(sum[1] / sum[0]);
358 out->cy = (int)(sum[2] / sum[0]);
359 }
360
361 free(flag);
362 free(seen);
363 free(stack);
364}
365
367static void _measure(const float *mask, const int w, const int h, double *coverage, int *hole_count,
368 int *largest_hole)
369{
370 *coverage = 0.0; *hole_count = 0; *largest_hole = 0;
371
372 /* A frame with no pixels has nothing to measure, and saying so here is not only defensive: it
373 * is what lets a reader (and a static analyser) bound every index below. */
374 if(w <= 0 || h <= 0) return;
375 const size_t npix = (size_t)w * h;
376
377 int *label = (int *)calloc(npix, sizeof(int));
378 int *stack = (int *)malloc(sizeof(int) * npix);
379 if(IS_NULL_PTR(label) || IS_NULL_PTR(stack)) { free(label); free(stack); return; }
380
381 size_t painted = 0;
382 for(size_t i = 0; i < npix; i++) if(mask[i] > 0.5f) painted++;
383 *coverage = (double)painted / (double)npix;
384
385 int next_label = 0;
386 for(int y = 0; y < h; y++)
387 for(int x = 0; x < w; x++)
388 {
389 const size_t seed = (size_t)y * w + x;
390 if(mask[seed] > 0.5f || label[seed]) continue;
391
392 next_label++;
393 int top = 0, size = 0;
394 gboolean touches_border = FALSE;
395 stack[top++] = (int)seed;
396 label[seed] = next_label;
397 while(top > 0)
398 {
399 const int cur = stack[--top];
400 const int cx = cur % w, cy = cur / w;
401 size++;
402 if(cx == 0 || cy == 0 || cx == w - 1 || cy == h - 1) touches_border = TRUE;
403 const int dx[4] = { 1, -1, 0, 0 }, dy[4] = { 0, 0, 1, -1 };
404 for(int k = 0; k < 4; k++)
405 {
406 const int nx = cx + dx[k], ny = cy + dy[k];
407 if(nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
408 const size_t ni = (size_t)ny * w + nx;
409 if(mask[ni] > 0.5f || label[ni]) continue;
410 label[ni] = next_label;
411 /* The push is bounded because a cell is marked BEFORE it is pushed, so none can enter
412 * the stack twice and `top' cannot pass npix. Stating that in code rather than only in
413 * a comment costs one compare per neighbour, and removes the reader's -- and the
414 * analyser's -- obligation to reconstruct the argument from two places. */
415 if((size_t)top < npix) stack[top++] = (int)ni;
416 }
417 }
418 if(!touches_border)
419 {
420 (*hole_count)++;
421 if(size > *largest_hole) *largest_hole = size;
422 }
423 }
424
425 free(label);
426 free(stack);
427}
428
429
430
431/* Synthetic classes, each isolating one way a stroke's outline can fail. Radii are large
432 * relative to the node spacing on purpose: that is the regime where a brush's offset curve
433 * folds over itself, which is where every defect in this family has come from. */
434
435/* A cusp: handles collapsed onto the middle node, arms at a sharp angle. */
436static const float _brush_cusp_tbl[3][9] = {
437 { 0.30f, 0.30f, 0.275f, 0.30f, 0.35f, 0.405f, 0.030f, 1.0f, 0.66f },
438 { 0.50f, 0.72f, 0.50f, 0.72f, 0.50f, 0.72f, 0.030f, 1.0f, 0.66f },
439 { 0.70f, 0.30f, 0.65f, 0.405f, 0.725f, 0.30f, 0.030f, 1.0f, 0.66f },
440};
441
442/* The same, tighter: the arms nearly parallel, so the two offset sides overlap along their
443 * whole length and the wedge at the tip is at its narrowest. */
444static const float _brush_hairpin_tbl[3][9] = {
445 { 0.42f, 0.20f, 0.40f, 0.20f, 0.44f, 0.345f, 0.035f, 1.0f, 0.66f },
446 { 0.50f, 0.78f, 0.50f, 0.78f, 0.50f, 0.78f, 0.035f, 1.0f, 0.66f },
447 { 0.58f, 0.20f, 0.56f, 0.345f, 0.60f, 0.20f, 0.035f, 1.0f, 0.66f },
448};
449
450/* Several sharp joints in a row: each one is another chance to drop a wedge, and a fix that
451 * only handles the first would pass the cusp case and fail here. */
452static const float _brush_zigzag_tbl[6][9] = {
453 { 0.15f, 0.35f, 0.113f, 0.35f, 0.187f, 0.425f, 0.022f, 1.0f, 0.66f },
454 { 0.30f, 0.65f, 0.30f, 0.65f, 0.30f, 0.65f, 0.022f, 1.0f, 0.66f },
455 { 0.45f, 0.35f, 0.45f, 0.35f, 0.45f, 0.35f, 0.022f, 1.0f, 0.66f },
456 { 0.60f, 0.65f, 0.60f, 0.65f, 0.60f, 0.65f, 0.022f, 1.0f, 0.66f },
457 { 0.75f, 0.35f, 0.75f, 0.35f, 0.75f, 0.35f, 0.022f, 1.0f, 0.66f },
458 { 0.88f, 0.60f, 0.847f, 0.545f, 0.913f, 0.60f, 0.022f, 1.0f, 0.66f },
459};
460
461/* SELF-INTERSECTING: the stroke crosses itself, so the offset curve does too and the crossing
462 * is real geometry rather than a fold to be cut away. The union must still be solid. */
463static const float _brush_selfcross_tbl[5][9] = {
464 { 0.25f, 0.30f, 0.20f, 0.30f, 0.35f, 0.34f, 0.030f, 1.0f, 0.66f },
465 { 0.62f, 0.42f, 0.52f, 0.39f, 0.70f, 0.44f, 0.030f, 1.0f, 0.66f },
466 { 0.62f, 0.62f, 0.72f, 0.58f, 0.52f, 0.66f, 0.030f, 1.0f, 0.66f },
467 { 0.30f, 0.44f, 0.40f, 0.50f, 0.24f, 0.41f, 0.030f, 1.0f, 0.66f },
468 { 0.30f, 0.24f, 0.27f, 0.30f, 0.33f, 0.20f, 0.030f, 1.0f, 0.66f },
469};
470
471/* CONCAVE, tighter than the radius: the classic fold. The inner offset curve loops, and the
472 * loop has to be removed without removing the stroke around it. */
473static const float _brush_concave_tbl[5][9] = {
474 { 0.20f, 0.30f, 0.16f, 0.30f, 0.28f, 0.32f, 0.045f, 1.0f, 0.66f },
475 { 0.44f, 0.36f, 0.36f, 0.34f, 0.50f, 0.38f, 0.045f, 1.0f, 0.66f },
476 { 0.50f, 0.50f, 0.50f, 0.44f, 0.50f, 0.56f, 0.045f, 1.0f, 0.66f },
477 { 0.44f, 0.64f, 0.50f, 0.62f, 0.36f, 0.66f, 0.045f, 1.0f, 0.66f },
478 { 0.20f, 0.70f, 0.28f, 0.68f, 0.16f, 0.70f, 0.045f, 1.0f, 0.66f },
479};
480
481/* THE THIRD REPORTED SHAPE. Issue #1360, "brush #1" of the attached sidecar, decoded verbatim.
482 * Drawn with a pen: the tablet delivered the first seventeen nodes within half a pixel of
483 * each other while the pressure -- mapped to opacity -- ramped from 0.05 to 0.91, and the
484 * last three the same way as the pen lifted. Every one of those density steps takes the
485 * stroke through the opacity-transition stamp, and every one of those coincident segments is
486 * degenerate: no direction to offset along. The reporter's mask grew a circle spanning the
487 * frame. Frame 5184x3888 is the reporter's own (Panasonic GX9). */
488static const float _brush_1360[43][11] = {
489 /* columns: node x y | ctrl1 x y | ctrl2 x y | border in out | density | fading | state */
490 { 0.22329402f, 0.437683344f, 0.223308727f, 0.437683344f, 0.223279327f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.0500000007f, 0.790861964f, 1 },
491 { 0.223249912f, 0.437683344f, 0.223257273f, 0.437683344f, 0.223242566f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.163728267f, 0.790861964f, 1 },
492 { 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.263469368f, 0.790861964f, 1 },
493 { 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.330415487f, 0.790861964f, 1 },
494 { 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.436690927f, 0.790861964f, 1 },
495 { 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.493527323f, 0.790861964f, 1 },
496 { 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.559117258f, 0.790861964f, 1 },
497 { 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.612624824f, 0.790861964f, 1 },
498 { 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.651337683f, 0.790861964f, 1 },
499 { 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.68659842f, 0.790861964f, 1 },
500 { 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.720872879f, 0.790861964f, 1 },
501 { 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.770558476f, 0.790861964f, 1 },
502 { 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.223249912f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.795832813f, 0.790861964f, 1 },
503 { 0.223249912f, 0.437683344f, 0.223242566f, 0.437683344f, 0.223257273f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.82949084f, 0.790861964f, 1 },
504 { 0.22329402f, 0.437683344f, 0.223271966f, 0.437683344f, 0.223316073f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.851436317f, 0.790861964f, 1 },
505 { 0.223382235f, 0.437683344f, 0.223367542f, 0.437683344f, 0.223396942f, 0.437683344f, 0.0340311043f, 0.0340311043f, 0.857847393f, 0.790861964f, 1 },
506 { 0.223382235f, 0.437683344f, 0.223374888f, 0.4377231f, 0.223389596f, 0.437643617f, 0.0340311043f, 0.0340311043f, 0.909628928f, 0.790861964f, 1 },
507 { 0.223426342f, 0.437444836f, 0.224293813f, 0.44460988f, 0.222558886f, 0.430279791f, 0.0340311043f, 0.0340311043f, 0.932067573f, 0.790861964f, 1 },
508 { 0.218177453f, 0.394693047f, 0.217354104f, 0.403786033f, 0.219000816f, 0.38560009f, 0.0340311043f, 0.0340311043f, 0.892984807f, 0.790861964f, 1 },
509 { 0.228366479f, 0.382887095f, 0.218559727f, 0.388342857f, 0.238173231f, 0.377431333f, 0.0340311043f, 0.0340311043f, 0.883491576f, 0.790861964f, 1 },
510 { 0.277017951f, 0.361958414f, 0.259396702f, 0.372373074f, 0.29463923f, 0.351543754f, 0.0340311043f, 0.0340311043f, 0.858956993f, 0.790861964f, 1 },
511 { 0.334094107f, 0.320399076f, 0.315803885f, 0.332115591f, 0.352384329f, 0.308682591f, 0.0340311043f, 0.0340311043f, 0.856861055f, 0.790861964f, 1 },
512 { 0.3867594f, 0.291659355f, 0.376151383f, 0.28612408f, 0.397367477f, 0.29719466f, 0.0340311043f, 0.0340311043f, 0.859450102f, 0.790861964f, 1 },
513 { 0.39774242f, 0.353610754f, 0.392405331f, 0.330813766f, 0.40307951f, 0.376407743f, 0.0340311043f, 0.0340311043f, 0.892984807f, 0.790861964f, 1 },
514 { 0.418782055f, 0.428441316f, 0.419451058f, 0.415542245f, 0.418113083f, 0.441340417f, 0.0340311043f, 0.0340311043f, 0.94464308f, 0.790861964f, 1 },
515 { 0.393728524f, 0.431005239f, 0.407460898f, 0.427636355f, 0.379996151f, 0.434374154f, 0.0340311043f, 0.0340311043f, 0.966835141f, 0.790861964f, 1 },
516 { 0.336387753f, 0.448654532f, 0.364014268f, 0.445325434f, 0.308761239f, 0.45198366f, 0.0340311043f, 0.0340311043f, 0.981383324f, 0.790861964f, 1 },
517 { 0.227969483f, 0.450979918f, 0.243914649f, 0.456723928f, 0.212024316f, 0.445235968f, 0.0340311043f, 0.0340311043f, 0.999876738f, 0.790861964f, 1 },
518 { 0.240716785f, 0.41419071f, 0.214722306f, 0.423989266f, 0.266711295f, 0.404392183f, 0.0340311043f, 0.0340311043f, 0.981383324f, 0.790861964f, 1 },
519 { 0.383936495f, 0.392188728f, 0.362793893f, 0.401897848f, 0.405079097f, 0.382479638f, 0.0340311043f, 0.0340311043f, 0.968807817f, 0.790861964f, 1 },
520 { 0.367572308f, 0.35593611f, 0.371056885f, 0.366261333f, 0.36408776f, 0.345610917f, 0.0340311043f, 0.0340311043f, 0.971273601f, 0.790861964f, 1 },
521 { 0.363029122f, 0.330237389f, 0.364991963f, 0.330992669f, 0.361066312f, 0.329482168f, 0.0340311043f, 0.0340311043f, 0.981383324f, 0.790861964f, 1 },
522 { 0.355795383f, 0.351404577f, 0.359191746f, 0.345690429f, 0.352399051f, 0.357118726f, 0.0340311043f, 0.0340311043f, 0.948095202f, 0.790861964f, 1 },
523 { 0.342651129f, 0.364522308f, 0.351039052f, 0.359195709f, 0.334263206f, 0.369848907f, 0.0340311043f, 0.0340311043f, 0.953026772f, 0.790861964f, 1 },
524 { 0.305467844f, 0.383364111f, 0.314164549f, 0.380064815f, 0.296771169f, 0.386663437f, 0.0340311043f, 0.0340311043f, 0.987671077f, 0.790861964f, 1 },
525 { 0.290470988f, 0.384318113f, 0.28567791f, 0.388144135f, 0.295264095f, 0.380492091f, 0.0340311043f, 0.0340311043f, 0.999630153f, 0.790861964f, 1 },
526 { 0.334226459f, 0.360408098f, 0.325316578f, 0.363538474f, 0.343136311f, 0.357277751f, 0.0340311043f, 0.0340311043f, 0.990999877f, 0.790861964f, 1 },
527 { 0.343930244f, 0.365535945f, 0.340533912f, 0.361550927f, 0.347326607f, 0.369520962f, 0.0340311043f, 0.0340311043f, 0.970780432f, 0.790861964f, 1 },
528 { 0.354604453f, 0.384318113f, 0.35239169f, 0.379160494f, 0.356817216f, 0.389475763f, 0.0340311043f, 0.0340311043f, 0.968807817f, 0.790861964f, 1 },
529 { 0.357206851f, 0.396481782f, 0.358831495f, 0.393172562f, 0.355582207f, 0.399791002f, 0.0340311043f, 0.0340311043f, 0.969794095f, 0.790861964f, 1 },
530 { 0.34485653f, 0.404173523f, 0.346914947f, 0.4029015f, 0.342798173f, 0.405445546f, 0.0340311043f, 0.0340311043f, 0.988164246f, 0.790861964f, 1 },
531 { 0.34485653f, 0.404113948f, 0.344812453f, 0.404282898f, 0.344900668f, 0.403945029f, 0.0340311043f, 0.0340311043f, 0.647515714f, 0.790861964f, 1 },
532 { 0.345121175f, 0.403159916f, 0.34503299f, 0.403477967f, 0.34520942f, 0.402841896f, 0.0340311043f, 0.0340311043f, 0.0500000007f, 0.790861964f, 1 },
533};
534
535/* THE FOURTH. Issue #1352, "brush #2": seven nodes, the sixth with a smaller radius than its
536 * neighbours and its handles pulled far apart -- the "fading handle near the end" the report
537 * describes. The radius step takes the builder through the size-transition arc, and the
538 * drawn border came out with straight chords across the stroke. Reported at an unknown
539 * frame size; the JPEG it was drawn on is not attached, so it runs at the corpus default. */
540static const float _brush_1352[7][11] = {
541 /* columns: node x y | ctrl1 x y | ctrl2 x y | border in out | density | fading | state */
542 { 0.465645701f, 0.0646421909f, 0.474728316f, 0.0589693412f, 0.456563115f, 0.0703150481f, 0.0239629708f, 0.0239629708f, 1.0f, 0.660000026f, 1 },
543 { 0.438397855f, 0.0816607475f, 0.445377052f, 0.0763829798f, 0.431418657f, 0.0869385153f, 0.0239629708f, 0.0239629708f, 1.0f, 0.660000026f, 1 },
544 { 0.423770666f, 0.0963087901f, 0.428022474f, 0.0899883211f, 0.419518888f, 0.102629259f, 0.0239629708f, 0.0239629708f, 1.0f, 0.660000026f, 1 },
545 { 0.412887126f, 0.119583569f, 0.415665925f, 0.10959287f, 0.410108328f, 0.129574269f, 0.0239629708f, 0.0239629708f, 1.0f, 0.660000026f, 1 },
546 { 0.407097936f, 0.156252995f, 0.410558552f, 0.147158578f, 0.40363735f, 0.165347442f, 0.0239629708f, 0.0239629708f, 1.0f, 0.660000026f, 1 },
547 { 0.392123342f, 0.174150184f, 0.395232916f, 0.141441733f, 0.389013737f, 0.206858635f, 0.018198235f, 0.018198235f, 1.0f, 0.660000026f, 2 },
548 { 0.36773169f, 0.193801045f, 0.375862241f, 0.187250778f, 0.35960114f, 0.200351343f, 0.0239629708f, 0.0239629708f, 1.0f, 0.660000026f, 1 },
549};
550
551/* _MG_1074.CR2, brush #4 of its sidecar (2026-09-08): eight nodes, the second with a radius
552 * 2.6 times its neighbours', so the stroke flares from node 0 into node 1 and back. Reported
553 * as "some dashed border missing near node 0" once the envelope samples had fixed the long
554 * sides elsewhere: the flare's rate sits near the tilt cap. Frame 5184x3456. */
555static const float _brush_1074[8][11] = {
556 /* columns: node x y | ctrl1 x y | ctrl2 x y | border in out | density | fading | state */
557 { 0.758693337f, 0.601638496f, 0.760810494f, 0.609281301f, 0.762927711f, 0.616924226f, 0.0375316516f, 0.0375316516f, 1.0f, 0.660000026f, 1 },
558 { 0.748442948f, 0.644278347f, 0.751883447f, 0.688123882f, 0.755324066f, 0.731969476f, 0.097397998f, 0.097397998f, 1.0f, 0.660000026f, 1 },
559 { 0.729495943f, 0.881070495f, 0.729165137f, 0.923407614f, 0.728834331f, 0.965744674f, 0.0375316516f, 0.0375316516f, 1.0f, 0.660000026f, 1 },
560 { 0.720828474f, 0.935475111f, 0.712888777f, 0.937285244f, 0.704949141f, 0.939095378f, 0.0375316516f, 0.0375316516f, 1.0f, 0.660000026f, 1 },
561 { 0.694164455f, 0.938894272f, 0.681527078f, 0.934268355f, 0.668889761f, 0.929642439f, 0.0375316516f, 0.0375316516f, 1.0f, 0.660000026f, 1 },
562 { 0.646394014f, 0.915664196f, 0.637064874f, 0.909529805f, 0.627735794f, 0.903395414f, 0.0375316516f, 0.0375316516f, 1.0f, 0.660000026f, 1 },
563 { 0.630183876f, 0.897663355f, 0.625552356f, 0.897462249f, 0.620920897f, 0.897261143f, 0.0375316516f, 0.0375316516f, 1.0f, 0.660000026f, 1 },
564 { 0.614701509f, 0.904702842f, 0.609275997f, 0.908323109f, 0.603850603f, 0.911943376f, 0.0375316516f, 0.0375316516f, 1.0f, 0.660000026f, 1 },
565};
566
567/* The same brush as the darkroom held it on the evening of 2026-09-08, rebuilt from the
568 * frame dump (MASKS_DUMP_OVERLAY) rather than the sidecar, which no longer matched: node 1 is
569 * 287 px from node 0 now, and the radius rate along that segment peaks near 1 -- the discs
570 * almost nest, and the union's top is the rear envelope of the flare, 40 px outside node 1's
571 * circle at a tilt of 64 degrees. Radii measured on the dump: 132 px everywhere, 342.5 at
572 * node 1; frame 5184x3456. */
573static const float _brush_1074b[8][11] = {
574 /* columns: node x y | ctrl1 x y | ctrl2 x y | border in out | density | fading | state */
575 { 0.751560571f, 0.636493056f, 0.749378858f, 0.628718171f, 0.753744213f, 0.64426794f, 0.0381944444f, 0.0381944444f, 1.0f, 0.660000026f, 1 },
576 { 0.745943287f, 0.718943866f, 0.742395833f, 0.674337384f, 0.749488812f, 0.763550347f, 0.0991030093f, 0.0991030093f, 1.0f, 0.660000026f, 1 },
577 { 0.722523148f, 0.958313079f, 0.722864583f, 0.915240162f, 0.722181713f, 1.001386f, 0.0381944444f, 0.0381944444f, 1.0f, 0.660000026f, 1 },
578 { 0.705744599f, 0.972430556f, 0.713929398f, 0.970590278f, 0.697559799f, 0.974273727f, 0.0381944444f, 0.0381944444f, 1.0f, 0.660000026f, 1 },
579 { 0.673414352f, 0.969363426f, 0.686442901f, 0.974068287f, 0.660387731f, 0.964655671f, 0.0381944444f, 0.0381944444f, 1.0f, 0.660000026f, 1 },
580 { 0.627581019f, 0.944195602f, 0.637197145f, 0.950434028f, 0.617962963f, 0.937954282f, 0.0381944444f, 0.0381944444f, 1.0f, 0.660000026f, 1 },
581 { 0.615711806f, 0.931918403f, 0.62048804f, 0.932120949f, 0.6109375f, 0.931712963f, 0.0381944444f, 0.0381944444f, 1.0f, 0.660000026f, 1 },
582 { 0.598933256f, 0.942965856f, 0.604527392f, 0.939282407f, 0.593341049f, 0.946649306f, 0.0381944444f, 0.0381944444f, 1.0f, 0.660000026f, 1 },
583};
584
585/* Point the dev's geometry at a given frame size. The chain must be rebuilt afterwards or it
586 * stops being authoritative and every outline comes back empty, silently. */
587static void _set_frame(dt_develop_t *dev, const int w, const int h)
588{
592}
593
594
595/* THE SECOND REPORTED SHAPE. Polygon #2 of the same sidecar, decoded verbatim like the brush
596 * above. Fifteen nodes; node 12 is a CUSP (both handles collapsed onto it, state 2) and nodes 0
597 * and 14 bracket the concavity where the outer border was reported self-intersecting. Kept as
598 * data for the same reason: the test needs no file, no database and no XMP reader to reproduce
599 * the exact shape a user reported, and a diff shows when the geometry itself is edited. */
600static const float _polygon_1788045925[15][8] = {
601 /* node.x, node.y, ctrl1.x, ctrl1.y, ctrl2.x, ctrl2.y, border, state */
602 { 0.109192476f, 0.048409186f, 0.102207638f, 0.045089375f, 0.116177313f, 0.051728997f, 0.021000000f, 1 },
603 { 0.141800687f, 0.053578191f, 0.133969516f, 0.053021252f, 0.149631873f, 0.054135136f, 0.021000000f, 1 },
604 { 0.156179547f, 0.051750816f, 0.152295023f, 0.049384728f, 0.160064086f, 0.054116912f, 0.021000000f, 1 },
605 { 0.165107980f, 0.067774758f, 0.159996808f, 0.066224061f, 0.170219168f, 0.069325462f, 0.021000000f, 1 },
606 { 0.186846718f, 0.061055038f, 0.181864932f, 0.064845644f, 0.191828534f, 0.057264429f, 0.021000000f, 1 },
607 { 0.194998786f, 0.045031108f, 0.191052154f, 0.047960225f, 0.198945418f, 0.042101998f, 0.021000000f, 1 },
608 { 0.210526481f, 0.043480407f, 0.206644550f, 0.040637448f, 0.214408413f, 0.046323366f, 0.021000000f, 1 },
609 { 0.218290374f, 0.062088847f, 0.215120122f, 0.055455282f, 0.221460640f, 0.068722412f, 0.021000000f, 1 },
610 { 0.229547918f, 0.083281785f, 0.225083694f, 0.080955729f, 0.234012157f, 0.085607842f, 0.021000000f, 1 },
611 { 0.245075703f, 0.076045178f, 0.241193786f, 0.074408330f, 0.248957604f, 0.077682026f, 0.021000000f, 1 },
612 { 0.252839506f, 0.093102910f, 0.253421843f, 0.087416999f, 0.252257198f, 0.098788828f, 0.021000000f, 1 },
613 { 0.241581917f, 0.110160641f, 0.254290909f, 0.109167710f, 0.228872970f, 0.111153573f, 0.021000000f, 1 },
614 { 0.176586017f, 0.099060528f, 0.176586017f, 0.099060528f, 0.176586017f, 0.099060528f, 0.021000000f, 2 },
615 { 0.099036753f, 0.105304882f, 0.111819178f, 0.116205104f, 0.086254336f, 0.094404668f, 0.021000000f, 1 },
616 { 0.099891551f, 0.033659276f, 0.098198950f, 0.043141894f, 0.101584166f, 0.024176663f, 0.021000000f, 1 },
617};
618
619
620/* ------------------------------------------------------------------------------------- */
621/* Baseline comparison.
622 *
623 * The oracle above answers "did the rasteriser deliver the coverage it owed", which is the
624 * question the reported defects were about. It cannot answer "did anything change" -- a shifted
625 * edge, a different feather ramp, an overlay that stopped drawing a handle all satisfy it. Most
626 * of what was actually found while fixing this series was found by rendering before and after and
627 * diffing, so that comparison belongs in the test rather than in somebody's scratch directory.
628 *
629 * The baselines live in the shared sample bank (tests/image_test/samples/baseline/masks-geometry),
630 * alongside the raw-export baselines and reviewed the same way -- regenerated deliberately,
631 * looked at, and committed. They are full resolution on purpose: the defects in this series are
632 * 1 to 5 pixels wide and a downscaled baseline would not see any of them.
633 *
634 * A tolerance rather than exact equality, because the overlay is antialiased by cairo and its
635 * output is not promised to be identical across cairo versions. Anything that moves geometry
636 * moves far more than this. */
637#define MASKS_BASELINE_MAX_DELTA 8 /* per channel, of 255 */
638#define MASKS_BASELINE_MAX_SHARE 0.0002 /* share of pixels allowed to differ at all */
639/* The per-pixel bound only means something once enough pixels carry it. A change that moves
640 * geometry moves hundreds of pixels; an outline sample moved by a float ulp moves a handful,
641 * because a dash edge on the antialiased overlay lands one pixel over. Measured: restructuring
642 * brush.c into named steps moved every outline sample by at most 0.0003 px -- FMA contraction
643 * landing differently across the new function boundaries, identical sample counts and skip
644 * ranges -- and one frame size out of sixteen came out with 4 pixels at a delta of 16. Below
645 * this many differing pixels, the worst delta is noise; at or above it, it is a change. */
646#define MASKS_BASELINE_NOISE_PX 64
647
652static void _surface_diff(cairo_surface_t *const a, cairo_surface_t *const b,
653 size_t *const differing, int *const worst, int *const wx, int *const wy)
654{
655 cairo_surface_flush(a);
656 cairo_surface_flush(b);
657
658 const int w = cairo_image_surface_get_width(a);
659 const int h = cairo_image_surface_get_height(a);
660 const int sa = cairo_image_surface_get_stride(a);
661 const int sb = cairo_image_surface_get_stride(b);
662 const uint8_t *const pa = cairo_image_surface_get_data(a);
663 const uint8_t *const pb = cairo_image_surface_get_data(b);
664
665 for(int y = 0; y < h; y++)
666 for(int x = 0; x < w; x++)
667 {
668 int pixel_worst = 0;
669 for(int c = 0; c < 3; c++)
670 {
671 const int d = abs((int)pa[y * sa + x * 4 + c] - (int)pb[y * sb + x * 4 + c]);
672 if(d > pixel_worst) pixel_worst = d;
673 }
674 if(pixel_worst == 0) continue;
675 (*differing)++;
676 if(pixel_worst > *worst)
677 {
678 *worst = pixel_worst;
679 *wx = x;
680 *wy = y;
681 }
682 }
683}
684
685static const char *baseline_dir = NULL;
686
687/* MASKS_OUTLINE_STEP=<n> has the corpus build every GUI outline every n image pixels -- the
688 * density the darkroom shows a zoomed-out image at -- so the band check judges what a user sees
689 * at fit zoom; unset, the corpus judges pixel-accurate outlines. The baselines are step-1
690 * pictures and are not compared at any other step. --time-overlay takes its density from its
691 * view (MASKS_OVERLAY_VIEW), the way the darkroom does: the expose publishes it from the
692 * transformed context, which is also why the corpus re-applies its own before every build --
693 * the overlay it writes beside each case is drawn at full resolution and publishes 1. */
695
696/* Give @p dev a GUI state with the density its outlines are built at: the override when set,
697 * else @p image_px_per_device_px. */
698static void _outline_density_apply(dt_develop_t *dev, const double image_px_per_device_px)
699{
700 if(IS_NULL_PTR(dev->form_gui))
701 {
702 dev->form_gui = (dt_masks_form_gui_t *)calloc(1, sizeof(dt_masks_form_gui_t));
703 if(IS_NULL_PTR(dev->form_gui)) return;
705 }
707 : image_px_per_device_px);
708}
709
710
711static gboolean baseline_update = FALSE;
712static gboolean time_overlay = FALSE;
713static int time_overlay_frames = 30;
714static int baseline_missing = 0;
715
718static gboolean _baseline_check(const char *path, const char *name)
719{
720 if(IS_NULL_PTR(baseline_dir)) return TRUE;
722 {
723 printf(" baseline: not compared, the outline is sampled every %d px\n", _outline_step_override);
724 return TRUE;
725 }
726
727 char *base = g_strdup_printf("%s/%s.png", baseline_dir, name);
728
730 {
731 /* never overwrite: an existing entry is reviewed, and silently replacing it is how a
732 * regression becomes the new reference. Delete it deliberately to refresh one. */
733 if(!g_file_test(base, G_FILE_TEST_EXISTS))
734 {
735 char *dirname = g_path_get_dirname(base);
736 g_mkdir_with_parents(dirname, 0755);
737 g_free(dirname);
738 GError *e = NULL;
739 char *content = NULL;
740 gsize len = 0;
741 if(g_file_get_contents(path, &content, &len, &e) && g_file_set_contents(base, content, len, &e))
742 printf(" baseline: added %s.png\n", name);
743 else
744 {
745 printf(" baseline: could NOT add %s.png (%s)\n", name, e ? e->message : "?");
746 if(e) g_error_free(e);
747 }
748 g_free(content);
749 }
750 g_free(base);
751 return TRUE;
752 }
753
754 if(!g_file_test(base, G_FILE_TEST_EXISTS))
755 {
757 g_free(base);
758 return TRUE; /* no baseline yet is not a failure; `update-baseline' adds it */
759 }
760
761 cairo_surface_t *a = cairo_image_surface_create_from_png(path);
762 cairo_surface_t *b = cairo_image_surface_create_from_png(base);
763 gboolean ok = TRUE;
764
765 if(cairo_surface_status(a) != CAIRO_STATUS_SUCCESS || cairo_surface_status(b) != CAIRO_STATUS_SUCCESS)
766 {
767 printf(" baseline: unreadable (%s)\n", name);
768 ok = FALSE;
769 }
770 else if(cairo_image_surface_get_width(a) != cairo_image_surface_get_width(b)
771 || cairo_image_surface_get_height(a) != cairo_image_surface_get_height(b))
772 {
773 printf(" baseline: %s is %dx%d, baseline is %dx%d\n", name,
774 cairo_image_surface_get_width(a), cairo_image_surface_get_height(a),
775 cairo_image_surface_get_width(b), cairo_image_surface_get_height(b));
776 ok = FALSE;
777 }
778 else
779 {
780 size_t differing = 0;
781 int worst = 0;
782 int wx = -1;
783 int wy = -1;
784 _surface_diff(a, b, &differing, &worst, &wx, &wy);
785
786 const double share = (double)differing / ((double)cairo_image_surface_get_width(a)
787 * cairo_image_surface_get_height(a));
788 if(share > MASKS_BASELINE_MAX_SHARE
789 || (worst > MASKS_BASELINE_MAX_DELTA && differing >= MASKS_BASELINE_NOISE_PX))
790 {
791 printf(" baseline: %s differs -- %zu px (%.4f%%), worst %d at (%d,%d)\n",
792 name, differing, 100.0 * share, worst, wx, wy);
793 ok = FALSE;
794 }
795 }
796
797 cairo_surface_destroy(a);
798 cairo_surface_destroy(b);
799 g_free(base);
800 return ok;
801}
802
806static void _write_missing_map(const char *dir, const char *name, const float *const mask,
807 const uint8_t *const reference, const int w, const int h)
808{
809 char *path = g_strdup_printf("%s/%s-missing.png", dir, name);
810 cairo_surface_t *surf = cairo_image_surface_create(CAIRO_FORMAT_RGB24, w, h);
811
812 if(cairo_surface_status(surf) == CAIRO_STATUS_SUCCESS)
813 {
814 cairo_surface_flush(surf);
815 uint8_t *const pixels = cairo_image_surface_get_data(surf);
816 const int stride = cairo_image_surface_get_stride(surf);
817 for(int y = 0; y < h; y++)
818 {
819 uint32_t *const row = (uint32_t *)(pixels + (size_t)y * stride);
820 for(int x = 0; x < w; x++)
821 {
822 const size_t i = (size_t)y * w + x;
823 const uint32_t g = (uint32_t)(CLAMPF(mask[i], 0.0f, 1.0f) * 255.0f + 0.5f);
824 row[x] = (reference[i] && mask[i] <= 0.0f) ? 0x00FF2020u : ((g << 16) | (g << 8) | g);
825 }
826 }
827 cairo_surface_mark_dirty(surf);
828 cairo_surface_write_to_png(surf, path);
829 }
830
831 cairo_surface_destroy(surf);
832 g_free(path);
833}
834
835/* An 11-column node: both radii, the density and the fading are data here, because the
836 * defects this corpus grew for depend on them -- issue #1360's stroke is a run of coincident
837 * nodes whose only difference is a pen-pressure density ramp, and the density step is what
838 * triggers the code path that misbehaves. A 9-column case cannot express it. */
839static GList *_brush_from_table11(const float table[][11], const int count)
840{
841 GList *points = NULL;
842 for(int i = 0; i < count; i++)
843 {
844 dt_masks_node_brush_t *n = _brush_node(table[i][0], table[i][1], table[i][2], table[i][3],
845 table[i][4], table[i][5], table[i][6]);
846 n->border[1] = table[i][7];
847 n->density = table[i][8];
848 n->fading = table[i][9];
849 n->state = (dt_masks_points_states_t)(int)table[i][10];
850 points = g_list_append(points, n);
851 }
852 return points;
853}
854
864typedef struct _band_t
865{
866 int kept;
869 int off_frame; /* kept samples past the edge of the frame, which no map can judge */
870 double seconds;
872
873static _band_t _outline_band_check(dt_develop_t *dev, dt_masks_form_t *form, const uint8_t *owed,
874 const uint8_t *permitted, const int w, const int h)
875{
876 _band_t band = { 0 };
877 float *points = NULL;
878 float *border = NULL;
879 int points_count = 0;
880 int border_count = 0;
881 int skip_count = 0;
882 dt_masks_skip_range_t *skips = NULL;
883
884 _outline_density_apply(dev, 1.0);
885 const double t0 = dt_get_wtime();
886 const dt_masks_raster_result_t st = dt_masks_get_points_border(dev, form, &points, &points_count, &border,
887 &border_count, &skips, &skip_count, 0, NULL);
888 band.seconds = dt_get_wtime() - t0;
889 if(st != DT_MASKS_RASTER_OK || IS_NULL_PTR(border)) goto done;
890
891 const int header = (int)g_list_length(form->points) * 3;
892 for(int i = header; i < border_count; i++)
893 {
894 if(dt_masks_skip_contains(skips, skip_count, i)) continue;
895 band.kept++;
896 const int x = (int)lrintf(border[i * 2]);
897 const int y = (int)lrintf(border[i * 2 + 1]);
898 if(x < 0 || y < 0 || x >= w || y >= h)
899 {
900 /* a shape drawn past the edge of the image has a boundary there the maps cannot judge;
901 * it is drawn all the same, and not a spoke to nowhere */
902 band.off_frame++;
903 continue;
904 }
905 const size_t at = (size_t)y * w + x;
906 if(owed[at]) band.inside++;
907 else if(!permitted[at]) band.outside++;
908 }
909
910done:
914 return band;
915}
916
918typedef struct _brush_case_t
919{
920 const char *name;
921 const char *dir;
923 int w;
924 int h;
926
927/* Everything that judges a rendered mask against its two maps and reports it: the raster in
928 * both directions, the drawn outline against the same maps, the renders, the baselines, and
929 * the artefacts a failure needs. The brush and the polygon differ only in how the maps are
930 * built, so this is where both meet. */
931static void _judge_raster(dt_develop_t *dev, dt_masks_form_t *form, const _brush_case_t *const c,
932 const float *const mask, const uint8_t *const owed, const uint8_t *const permitted)
933{
934 _runs_t missing = { 0, 0, -1, -1 };
935 _runs_t excess = { 0, 0, -1, -1 };
936 _band_t band = { 0 };
937 if(!IS_NULL_PTR(owed) && !IS_NULL_PTR(permitted))
938 {
939 _coverage_runs(mask, owed, c->w, c->h, RUNS_MISSING, &missing);
940 _coverage_runs(mask, permitted, c->w, c->h, RUNS_EXCESS, &excess);
941 band = _outline_band_check(dev, form, owed, permitted, c->w, c->h);
942 }
943
944 char *alpha_path = g_strdup_printf("%s/%s-alpha.png", c->dir, c->name);
945 char *over_path = g_strdup_printf("%s/%s-overlay.png", c->dir, c->name);
946 const dt_masks_debug_request_t alpha_req
947 = { .width = c->w, .height = c->h, .backdrop = DT_MASKS_DEBUG_BACKDROP_RASTER, .draw_overlay = FALSE };
948 const dt_masks_debug_request_t over_req
949 = { .width = c->w, .height = c->h, .backdrop = DT_MASKS_DEBUG_BACKDROP_RASTER, .draw_overlay = TRUE };
950 dt_masks_debug_write_png(dev, form, &alpha_req, alpha_path);
951 dt_masks_debug_write_png(dev, form, &over_req, over_path);
952
953 char *alpha_name = g_strdup_printf("%s-alpha", c->name);
954 char *over_name = g_strdup_printf("%s-overlay", c->name);
955 const gboolean baseline_ok = _baseline_check(alpha_path, alpha_name)
956 & _baseline_check(over_path, over_name);
957 g_free(alpha_name);
958 g_free(over_name);
959
960 /* A picture of exactly what is owed and missing: red where the disc union covers a pixel the
961 * rasteriser left empty, over the mask itself. This is the artefact to look at first when a
962 * case fails -- it says WHERE the stroke went missing, which no scalar can. */
963 if(!IS_NULL_PTR(owed) && missing.total > 0)
964 _write_missing_map(c->dir, c->name, mask, owed, c->w, c->h);
965
966 /* Either kind of disagreement is explained by the outline buffers and nothing else. */
967 const gboolean disagreement = (missing.total > 0 || excess.total > 0 || band.inside > 0 || band.outside > 0);
968 if(disagreement || !IS_NULL_PTR(g_getenv("MASKS_DUMP_OUTLINE")))
969 {
970 char *csv = g_strdup_printf("%s/%s-outline.csv", c->dir, c->name);
971 dt_masks_debug_write_outline_csv(dev, form, csv);
972 g_free(csv);
973 }
974
975 const gboolean ok = (missing.largest <= c->budget_px) && (excess.largest <= c->budget_px) && baseline_ok
976 && (band.inside <= c->budget_px) && (band.outside <= c->budget_px);
977 printf("[%s] %-22s %5dx%-5d missing %6d px (largest run %5d px", ok ? "PASS" : "FAIL",
978 c->name, c->w, c->h, missing.total, missing.largest);
979 if(missing.largest > 0) printf(" around (%d,%d)", missing.cx, missing.cy);
980 printf(") excess %7d px (largest run %7d px", excess.total, excess.largest);
981 if(excess.largest > 0) printf(" around (%d,%d)", excess.cx, excess.cy);
982 printf(") outline: %d kept, %d inside, %d outside, %d off frame, built in %.1f ms budget %d -> %s\n",
983 band.kept, band.inside, band.outside, band.off_frame, 1000.0 * band.seconds, c->budget_px, alpha_path);
984 if(!ok) failures++;
985
986 g_free(alpha_path);
987 g_free(over_path);
988}
989
990/* The oracle for a polygon.
991 *
992 * A polygon's mask is its path's interior, filled, plus the union of a disc of the local radius
993 * over every point of the path -- the feather. Both maps take the interior whole (the fill is
994 * exact to the pixel) and the discs at the segment's smaller radius shrunk two pixels (OWED)
995 * or its larger one grown three (PERMITTED), for the reasons the brush's oracle gives. The
996 * interior is an even-odd scanline over the same dense samples the discs are stamped from.
997 *
998 * Judging a polygon in both directions is what the previous, holes-only measure could not do:
999 * a fold of the outer border filled as shape is coverage nobody owes, and a feather spoke sent
1000 * to the wrong sample is coverage that goes missing -- neither is a hole. */
1001#define PATH_SAMPLES_PER_SEGMENT 2001
1002
1003/* The dense closed path into (px, py), stamping the feather discs as it goes. Returns the
1004 * sample count. */
1005static int _path_dense_samples(const GList *const nodes, const int w, const int h, const _disc_pick_t pick,
1006 float *const px, float *const py, int32_t *const diff)
1007{
1008 const size_t stride = (size_t)w + 1;
1009 const float radius_scale = (float)MIN(w, h);
1010 int count = 0;
1011 for(const GList *l = nodes; l; l = l->next)
1012 {
1013 const dt_masks_node_polygon_t *const n0 = (const dt_masks_node_polygon_t *)l->data;
1014 const dt_masks_node_polygon_t *const n1 = (const dt_masks_node_polygon_t *)(l->next ? l->next : nodes)->data;
1015 const float p0x = n0->node[0] * w;
1016 const float p0y = n0->node[1] * h;
1017 const float p1x = n0->ctrl2[0] * w;
1018 const float p1y = n0->ctrl2[1] * h;
1019 const float p2x = n1->ctrl1[0] * w;
1020 const float p2y = n1->ctrl1[1] * h;
1021 const float p3x = n1->node[0] * w;
1022 const float p3y = n1->node[1] * h;
1023 const float r_out = n0->border[1] * radius_scale;
1024 const float r_in = n1->border[0] * radius_scale;
1025 const float r = (pick == DISC_OWED) ? MIN(r_out, r_in) : MAX(r_out, r_in);
1026 const int r_i = (pick == DISC_OWED) ? MAX((int)floorf(r) - 2, 0) : (int)ceilf(r) + 3;
1027 for(int k = 0; k < PATH_SAMPLES_PER_SEGMENT; k++)
1028 {
1029 const float t = (float)k / (float)(PATH_SAMPLES_PER_SEGMENT - 1);
1030 const float u = 1.0f - t;
1031 const float bx = u*u*u*p0x + 3*u*u*t*p1x + 3*u*t*t*p2x + t*t*t*p3x;
1032 const float by = u*u*u*p0y + 3*u*u*t*p1y + 3*u*t*t*p2y + t*t*t*p3y;
1033 px[count] = bx;
1034 py[count] = by;
1035 count++;
1036 _stamp_disc_rows(diff, stride, w, h, (int)lrintf(bx), (int)lrintf(by), r_i);
1037 }
1038 }
1039 return count;
1040}
1041
1042/* The per-row crossing table: how many crossings each row has, where each row's run starts in
1043 * @p xs, and how many entries @p xs can hold at all. */
1044typedef struct _row_table_t
1045{
1046 int *count;
1047 const int *at;
1048 float *xs; /* NULL while only counting */
1051
1052/* One crossing into row @p y: counted always, written only while filling and only inside the
1053 * table's capacity. FALSE when the table refused it. */
1054static inline gboolean _row_table_put(const _row_table_t *const t, const int y, const float x)
1055{
1056 if(!IS_NULL_PTR(t->xs))
1057 {
1058 const int at = t->at[y] + t->count[y];
1059 if(at < 0 || at >= t->capacity) return FALSE;
1060 t->xs[at] = x;
1061 }
1062 t->count[y]++;
1063 return TRUE;
1064}
1065
1066/* Where the closed path crosses each row, sampled at the row's centre. Two passes: with
1067 * @p t->xs NULL only the per-row counts are taken; with it, the crossings are written at the
1068 * row offsets, never outside [0, capacity), and the counts rebuilt from what was written. The
1069 * two passes walk the same edges and agree, but the bound is what makes that a property of the
1070 * code rather than of the reader. */
1071static void _path_row_crossings(const float *const px, const float *const py, const int count, const int h,
1072 const _row_table_t *const t)
1073{
1074 memset(t->count, 0, sizeof(int) * (size_t)h);
1075 for(int i = 0; i < count; i++)
1076 {
1077 const int j = (i + 1) % count;
1078 float x0 = px[i];
1079 float y0 = py[i];
1080 float x1 = px[j];
1081 float y1 = py[j];
1082 if(y0 == y1) continue;
1083 if(y0 > y1)
1084 {
1085 const float sx = x0;
1086 const float sy = y0;
1087 x0 = x1;
1088 y0 = y1;
1089 x1 = sx;
1090 y1 = sy;
1091 }
1092 /* a row y is crossed if y0 <= y + 0.5 < y1 */
1093 const int ya = MAX((int)ceilf(y0 - 0.5f), 0);
1094 const int yb = MIN((int)ceilf(y1 - 0.5f), h);
1095 for(int y = ya; y < yb; y++)
1096 _row_table_put(t, y, x0 + (x1 - x0) * (((float)y + 0.5f) - y0) / (y1 - y0));
1097 }
1098}
1099
1100/* The interior of the closed dense path, even-odd, into the row difference array. */
1101static void _path_interior(const float *const px, const float *const py, const int count, const int w, const int h,
1102 int32_t *const diff, const size_t stride)
1103{
1104 int *row_count = (int *)calloc((size_t)h + 1, sizeof(int));
1105 int *row_at = (int *)calloc((size_t)h + 1, sizeof(int));
1106 if(IS_NULL_PTR(row_count) || IS_NULL_PTR(row_at))
1107 {
1108 free(row_count);
1109 free(row_at);
1110 return;
1111 }
1112 const _row_table_t counting = { .count = row_count, .at = row_at, .xs = NULL, .capacity = 0 };
1113 _path_row_crossings(px, py, count, h, &counting);
1114 int total = 0;
1115 for(int y = 0; y < h; y++)
1116 {
1117 row_at[y] = total;
1118 total += row_count[y];
1119 }
1120 /* zeroed, so that nothing the bound refused to write is ever read as a crossing */
1121 float *xs = (float *)calloc((size_t)total + 1, sizeof(float));
1122 if(IS_NULL_PTR(xs))
1123 {
1124 free(row_count);
1125 free(row_at);
1126 return;
1127 }
1128 const _row_table_t filling = { .count = row_count, .at = row_at, .xs = xs, .capacity = total };
1129 _path_row_crossings(px, py, count, h, &filling);
1130
1131 for(int y = 0; y < h; y++)
1132 {
1133 float *const row = xs + row_at[y];
1134 const int m = MIN(row_count[y], total - row_at[y]);
1135 /* insertion sort: a row rarely has more than a handful of crossings */
1136 for(int a = 1; a < m; a++)
1137 {
1138 const float v = row[a];
1139 int b = a - 1;
1140 while(b >= 0 && row[b] > v)
1141 {
1142 row[b + 1] = row[b];
1143 b--;
1144 }
1145 row[b + 1] = v;
1146 }
1147 for(int a = 0; a + 1 < m; a += 2)
1148 {
1149 const int xa = MAX((int)ceilf(row[a] - 0.5f), 0);
1150 const int xb = MIN((int)ceilf(row[a + 1] - 0.5f), w); /* half-open */
1151 if(xa >= xb) continue;
1152 diff[(size_t)y * stride + xa] += 1;
1153 diff[(size_t)y * stride + xb] -= 1;
1154 }
1155 }
1156 free(xs);
1157 free(row_count);
1158 free(row_at);
1159}
1160
1161static void _polygon_disc_union(const GList *const nodes, const int w, const int h, const _disc_pick_t pick,
1162 uint8_t *const map)
1163{
1164 memset(map, 0, (size_t)w * h);
1165 int n = 0;
1166 for(const GList *l = nodes; l; l = l->next) n++;
1167 if(n < 3) return;
1168
1169 const size_t stride = (size_t)w + 1;
1170 int32_t *diff = (int32_t *)calloc(stride * h, sizeof(int32_t));
1171 float *px = (float *)malloc(sizeof(float) * (size_t)n * PATH_SAMPLES_PER_SEGMENT);
1172 float *py = (float *)malloc(sizeof(float) * (size_t)n * PATH_SAMPLES_PER_SEGMENT);
1173 if(!IS_NULL_PTR(diff) && !IS_NULL_PTR(px) && !IS_NULL_PTR(py))
1174 {
1175 const int count = _path_dense_samples(nodes, w, h, pick, px, py, diff);
1176 _path_interior(px, py, count, w, h, diff, stride);
1177 for(int y = 0; y < h; y++)
1178 {
1179 int32_t running = 0;
1180 for(int x = 0; x < w; x++)
1181 {
1182 running += diff[(size_t)y * stride + x];
1183 map[(size_t)y * w + x] = (running > 0) ? 1 : 0;
1184 }
1185 }
1186 }
1187 free(diff);
1188 free(px);
1189 free(py);
1190}
1191
1193static void _run_polygon_form_at(dt_develop_t *dev, dt_masks_form_t *form, const _brush_case_t *const c)
1194{
1195 _set_frame(dev, c->w, c->h);
1196 float *mask = dt_masks_debug_rasterise(dev, form, c->w, c->h);
1197 if(IS_NULL_PTR(mask))
1198 {
1199 printf("[FAIL] %-22s rasterisation returned nothing\n", c->name);
1200 failures++;
1201 return;
1202 }
1203 uint8_t *owed = (uint8_t *)malloc((size_t)c->w * c->h);
1204 uint8_t *permitted = (uint8_t *)malloc((size_t)c->w * c->h);
1205 if(!IS_NULL_PTR(owed) && !IS_NULL_PTR(permitted))
1206 {
1207 _polygon_disc_union(form->points, c->w, c->h, DISC_OWED, owed);
1208 _polygon_disc_union(form->points, c->w, c->h, DISC_PERMITTED, permitted);
1209 }
1210 _judge_raster(dev, form, c, mask, owed, permitted);
1211 free(owed);
1212 free(permitted);
1213 dt_free_align(mask);
1214}
1215
1217static void _run_brush_nodes_at(dt_develop_t *dev, GList *nodes, const _brush_case_t *const c)
1218{
1219 _set_frame(dev, c->w, c->h);
1220 dt_masks_form_t form = { 0 };
1221 form.type = DT_MASKS_BRUSH;
1223 form.version = 6;
1224 form.formid = 900;
1225 g_strlcpy(form.name, c->name, sizeof(form.name));
1226 form.points = nodes;
1227
1228 float *mask = dt_masks_debug_rasterise(dev, &form, c->w, c->h);
1229 if(IS_NULL_PTR(mask))
1230 {
1231 printf("[FAIL] %-22s rasterisation returned nothing\n", c->name);
1232 failures++;
1233 g_list_free_full(form.points, free);
1234 return;
1235 }
1236 uint8_t *owed = (uint8_t *)malloc((size_t)c->w * c->h);
1237 uint8_t *permitted = (uint8_t *)malloc((size_t)c->w * c->h);
1238 if(!IS_NULL_PTR(owed) && !IS_NULL_PTR(permitted))
1239 {
1240 _brush_disc_union(form.points, c->w, c->h, DISC_OWED, owed);
1241 _brush_disc_union(form.points, c->w, c->h, DISC_PERMITTED, permitted);
1242 }
1243 _judge_raster(dev, &form, c, mask, owed, permitted);
1244 free(owed);
1245 free(permitted);
1246 dt_free_align(mask);
1247 g_list_free_full(form.points, free);
1248}
1249
1250static void _run_brush_case_at(dt_develop_t *dev, const float table[][9], const int count,
1251 const _brush_case_t *const c)
1252{
1253 _run_brush_nodes_at(dev, _brush_from_table(table, count), c);
1254}
1255
1256static void _run_brush_case11_at(dt_develop_t *dev, const float table[][11], const int count,
1257 const _brush_case_t *const c)
1258{
1259 _run_brush_nodes_at(dev, _brush_from_table11(table, count), c);
1260}
1261
1262static void _run_brush_case(dt_develop_t *dev, const float table[][9], const int count,
1263 const char *name, const char *dir, const int budget_px)
1264{
1265 const _brush_case_t c = { name, dir, budget_px, IMG_W, IMG_H };
1266 _run_brush_case_at(dev, table, count, &c);
1267}
1268
1269static void _run_case_at(dt_develop_t *dev, dt_masks_form_t *form, const char *name, const char *dir,
1270 const int max_holes, const int max_hole_px, const int img_w, const int img_h)
1271{
1272 _set_frame(dev, img_w, img_h);
1273 float *mask = dt_masks_debug_rasterise(dev, form, img_w, img_h);
1274 if(IS_NULL_PTR(mask))
1275 {
1276 printf("[FAIL] %-22s rasterisation returned nothing\n", name);
1277 failures++;
1278 return;
1279 }
1280
1281 double coverage = 0.0;
1282 int holes = 0, largest = 0;
1283 _measure(mask, img_w, img_h, &coverage, &holes, &largest);
1284 dt_free_align(mask);
1285
1286 char *alpha_path = g_strdup_printf("%s/%s-alpha.png", dir, name);
1287 char *over_path = g_strdup_printf("%s/%s-overlay.png", dir, name);
1288 const dt_masks_debug_request_t alpha_req
1289 = { .width = img_w, .height = img_h, .backdrop = DT_MASKS_DEBUG_BACKDROP_RASTER, .draw_overlay = FALSE };
1290 const dt_masks_debug_request_t over_req
1291 = { .width = img_w, .height = img_h, .backdrop = DT_MASKS_DEBUG_BACKDROP_RASTER, .draw_overlay = TRUE };
1292 dt_masks_debug_write_png(dev, form, &alpha_req, alpha_path);
1293 dt_masks_debug_write_png(dev, form, &over_req, over_path);
1294
1295 char *alpha_name = g_strdup_printf("%s-alpha", name);
1296 char *over_name = g_strdup_printf("%s-overlay", name);
1297 const gboolean baseline_ok = _baseline_check(alpha_path, alpha_name)
1298 & _baseline_check(over_path, over_name);
1299 g_free(alpha_name);
1300 g_free(over_name);
1301
1302 if(holes > max_holes || largest > max_hole_px)
1303 {
1304 char *csv = g_strdup_printf("%s/%s-outline.csv", dir, name);
1305 dt_masks_debug_write_outline_csv(dev, form, csv);
1306 g_free(csv);
1307 }
1308
1309 const gboolean ok = (holes <= max_holes) && (largest <= max_hole_px) && baseline_ok;
1310 printf("[%s] %-22s %5dx%-5d coverage %.4f enclosed holes %d (largest %d px) budget %d/%d -> %s\n",
1311 ok ? "PASS" : "FAIL", name, img_w, img_h, coverage, holes, largest, max_holes, max_hole_px, alpha_path);
1312 if(!ok) failures++;
1313
1314 g_free(alpha_path);
1315 g_free(over_path);
1316}
1317static void _run_case(dt_develop_t *dev, dt_masks_form_t *form, const char *name, const char *dir,
1318 const int max_holes, const int max_hole_px)
1319{
1320 _run_case_at(dev, form, name, dir, max_holes, max_hole_px, IMG_W, IMG_H);
1321}
1322
1323/* ------------------------------------------------------------------------------------- */
1324
1325
1326/* ---------------------------------------------------------------------------------------------
1327 * The overlay's per-frame cost, headless.
1328 *
1329 * What the darkroom pays on every motion event while a mask is being edited is the drawing of
1330 * every shape's outline into the view: a cairo path of one vertex per device pixel, stroked
1331 * twice. This mode reproduces that draw alone -- the outline buffers are built once and cached
1332 * by the geometry generation exactly as in the darkroom -- on a screen-sized surface with the
1333 * fit transform, and reports milliseconds per frame. Two states per case: the shape as one of
1334 * a group's members (path only) and as the selected member (path, dashed border, nodes and
1335 * handles), which are the two costs a drag frame is made of. The last frame is saved as
1336 * <case>-screen[-selected].png, so the pixels a change produces can be compared as well. */
1337#define OVERLAY_SCREEN_W 2560
1338#define OVERLAY_SCREEN_H 1440
1339
1340/* How much of an outline's border the boundary pass skipped: the sample count and the number
1341 * of ranges. */
1342static void _overlay_skipped(const dt_masks_form_gui_points_t *const gp, int *skipped, int *ranges)
1343{
1344 *skipped = 0;
1345 *ranges = gp->border_skip_count;
1346 for(int k = 0; k < gp->border_skip_count; k++)
1347 *skipped += gp->border_skips[k].resume_at - gp->border_skips[k].jump_from;
1348}
1349
1350/* The range that skips border sample @p i, or -1. */
1351static int _overlay_skip_of(const dt_masks_form_gui_points_t *const gp, const int i)
1352{
1353 for(int k = 0; k < gp->border_skip_count; k++)
1354 if(i >= gp->border_skips[k].jump_from && i < gp->border_skips[k].resume_at) return k;
1355 return -1;
1356}
1357
1358/* MASKS_DUMP_SKIPS=<dir>: every border sample of the selected state, raw coordinates, with the
1359 * range that skips it and its spine point, as <dir>/<case>-border.txt; and every range on
1360 * stdout with its ends on screen. This is what let the missing dashes be measured on the
1361 * renders instead of judged by eye. */
1362static void _overlay_dump_skips(const dt_masks_form_gui_points_t *const gp, const char *name,
1364{
1365 const char *dump_dir = g_getenv("MASKS_DUMP_SKIPS");
1366 if(IS_NULL_PTR(dump_dir)) return;
1367 const int border = gp->border_count;
1368 char *path = g_strdup_printf("%s/%s-border.txt", dump_dir, name);
1369 FILE *f = g_fopen(path, "w");
1370 if(!IS_NULL_PTR(f))
1371 {
1372 for(int i = 0; i < border; i++)
1373 fprintf(f, "%d %.2f %.2f %d %.2f %.2f\n", i, gp->border[2 * i], gp->border[2 * i + 1], _overlay_skip_of(gp, i),
1374 gp->points[2 * i], gp->points[2 * i + 1]);
1375 fclose(f);
1376 }
1377 g_free(path);
1378 for(int k = 0; k < gp->border_skip_count; k++)
1379 {
1380 const int a = gp->border_skips[k].jump_from;
1381 const int b = MIN(gp->border_skips[k].resume_at, border - 1);
1382 printf(" skip %2d: [%6d, %6d) %6d samples from (%.0f, %.0f) to (%.0f, %.0f) on screen\n", k, a,
1384 gp->border[2 * a] * transform->scale + transform->offset_x,
1385 gp->border[2 * a + 1] * transform->scale + transform->offset_y,
1386 gp->border[2 * b] * transform->scale + transform->offset_x,
1387 gp->border[2 * b + 1] * transform->scale + transform->offset_y);
1388 }
1389}
1390
1391/* Paint the background and draw one overlay frame at @p transform. */
1392static void _overlay_frame(dt_develop_t *dev, cairo_surface_t *surface, const dt_masks_overlay_transform_t *const transform)
1393{
1394 cairo_t *cr = cairo_create(surface);
1395 cairo_set_source_rgb(cr, 0.12, 0.12, 0.12);
1396 cairo_paint(cr);
1398 cairo_destroy(cr);
1399}
1400
1401/* A frame must leave nothing behind. Draw once more at a PANNED transform, then compare with
1402 * the same frame drawn onto a fresh surface after a rebuild: anything the frame left in the
1403 * canvas -- a handle painted outside the rectangle it composited and cleared -- lands in the
1404 * panned frame as pixels the fresh one does not have. Returns how many. */
1405static long _overlay_leftovers(dt_develop_t *dev, cairo_surface_t *surface,
1407{
1408 const dt_masks_overlay_transform_t panned
1409 = { .scale = transform->scale, .offset_x = transform->offset_x + 97.0, .offset_y = transform->offset_y - 61.0 };
1410 _overlay_frame(dev, surface, &panned);
1411 cairo_surface_t *fresh = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, OVERLAY_SCREEN_W, OVERLAY_SCREEN_H);
1412 /* the same device scale, or the two frames differ by construction */
1413 double ppd_x = 1.0;
1414 double ppd_y = 1.0;
1415 cairo_surface_get_device_scale(surface, &ppd_x, &ppd_y);
1416 cairo_surface_set_device_scale(fresh, ppd_x, ppd_y);
1417 dev->form_gui->formid = 0;
1418 dev->form_gui->geometry_generation = 0;
1419 _overlay_frame(dev, fresh, &panned);
1420 cairo_surface_flush(surface);
1421 cairo_surface_flush(fresh);
1422 const uint32_t *pa = (const uint32_t *)cairo_image_surface_get_data(surface);
1423 const uint32_t *pb = (const uint32_t *)cairo_image_surface_get_data(fresh);
1424 const int stride = cairo_image_surface_get_stride(surface) / 4;
1425 long leftovers = 0;
1426 int bx0 = OVERLAY_SCREEN_W;
1427 int by0 = OVERLAY_SCREEN_H;
1428 int bx1 = -1;
1429 int by1 = -1;
1430 for(int y = 0; y < OVERLAY_SCREEN_H; y++)
1431 for(int x = 0; x < OVERLAY_SCREEN_W; x++)
1432 if(pa[y * stride + x] != pb[y * stride + x])
1433 {
1434 leftovers++;
1435 bx0 = MIN(bx0, x);
1436 by0 = MIN(by0, y);
1437 bx1 = MAX(bx1, x);
1438 by1 = MAX(by1, y);
1439 }
1440 if(leftovers > 0 && !IS_NULL_PTR(g_getenv("MASKS_DUMP_SKIPS")))
1441 {
1442 printf(" leftovers within (%d,%d)-(%d,%d)\n", bx0, by0, bx1, by1);
1443 char *path = g_strdup_printf("%s/leftover-panned.png", g_getenv("MASKS_DUMP_SKIPS"));
1444 cairo_surface_write_to_png(surface, path);
1445 g_free(path);
1446 path = g_strdup_printf("%s/leftover-fresh.png", g_getenv("MASKS_DUMP_SKIPS"));
1447 cairo_surface_write_to_png(fresh, path);
1448 g_free(path);
1449 }
1450 cairo_surface_destroy(fresh);
1451 return leftovers;
1452}
1453
1454/* What one state of one case cost: the frame, and the first frame that also built the outline. */
1460
1461/* The measurement line of one state, with the outline's sample and skip counts. */
1462static void _overlay_report(dt_develop_t *dev, const char *name, const int img_w, const int img_h,
1463 const gboolean selected, const _overlay_timing_t *const timing,
1465{
1466 int points = 0;
1467 int border = 0;
1468 int skip_ranges = 0;
1469 int skipped = 0;
1470 const dt_masks_form_gui_points_t *const gp
1471 = (const dt_masks_form_gui_points_t *)g_list_nth_data(dev->form_gui->points, 0);
1472 if(!IS_NULL_PTR(gp))
1473 {
1474 points = gp->points_count;
1475 border = gp->border_count;
1476 _overlay_skipped(gp, &skipped, &skip_ranges);
1478 }
1479 printf("[TIME] %-26s %5dx%-4d %-8s %7.2f ms/frame (first frame incl. build %7.2f ms at step %d;"
1480 " %d outline samples, %d border samples, %d skipped in %d ranges)\n",
1481 name, img_w, img_h, selected ? "selected" : "member", timing->per_frame_ms, timing->build_ms,
1482 dt_masks_gui_outline_step(dev), points, border, skipped, skip_ranges);
1483}
1484
1485/* THE HIT TEST, timed. A pointer motion hit-tests the selected member only -- its nodes and
1486 * handles, then its samples through get_distance -- and a button press hit-tests every member
1487 * to choose one; both walk the outlines at the density they were built at. A grid of positions
1488 * over the whole image says what each costs per event, and how many positions find a shape. */
1489static void _time_hit_test(dt_develop_t *dev, dt_masks_form_t *form, const char *name, const int img_w,
1490 const int img_h)
1491{
1492 dt_masks_form_gui_t *gui = dev->form_gui;
1493 const int per_axis = 20;
1494 const float radius = 10.0f;
1495 dt_widget_set_mouse_radius(radius, radius);
1496 gui->group_selected = 0;
1497 /* nothing is being dragged: a drag index of 0 would answer every motion with "node 0" */
1499
1500 GList *members = NULL;
1501 if(form->type & DT_MASKS_GROUP)
1502 {
1503 for(const GList *node = form->points; node; node = g_list_next(node))
1504 {
1505 const dt_masks_form_group_t *entry = (const dt_masks_form_group_t *)node->data;
1506 dt_masks_form_t *member = dt_masks_get_from_id(dev, entry->formid);
1507 if(!IS_NULL_PTR(member)) members = g_list_append(members, member);
1508 }
1509 }
1510 else
1511 members = g_list_append(members, form);
1512 if(IS_NULL_PTR(members)) return;
1513 dt_masks_form_t *selected = (dt_masks_form_t *)members->data;
1514
1515 double hover_seconds = 0.0;
1516 double press_seconds = 0.0;
1517 int hits = 0;
1518 const int positions = per_axis * per_axis;
1519 for(int p = 0; p < positions; p++)
1520 {
1521 gui->pos[0] = (float)img_w * ((float)(p % per_axis) + 0.5f) / (float)per_axis;
1522 gui->pos[1] = (float)img_h * ((float)(p / per_axis) + 0.5f) / (float)per_axis;
1523 gui->raw_pos[0] = gui->pos[0];
1524 gui->raw_pos[1] = gui->pos[1];
1525
1526 double t0 = dt_get_wtime();
1527 if(!IS_NULL_PTR(selected->functions->update_hover)) selected->functions->update_hover(selected, gui, 0);
1528 hover_seconds += dt_get_wtime() - t0;
1529
1530 t0 = dt_get_wtime();
1531 int index = 0;
1532 for(const GList *node = members; node; node = g_list_next(node), index++)
1533 {
1534 dt_masks_form_t *member = (dt_masks_form_t *)node->data;
1535 if(IS_NULL_PTR(member->functions->get_distance)) continue;
1536 int inside = 0;
1537 int inside_border = 0;
1538 int near_handle = -1;
1539 int inside_source = 0;
1540 float dist = FLT_MAX;
1541 member->functions->get_distance(gui->pos[0], gui->pos[1], radius, gui, index, g_list_length(member->points),
1542 &inside, &inside_border, &near_handle, &inside_source, &dist);
1543 if(inside || inside_border || near_handle >= 0 || inside_source) hits++;
1544 }
1545 press_seconds += dt_get_wtime() - t0;
1546 }
1547 printf("[HIT] %-26s hover %.3f ms/motion (%s), press %.3f ms/click (%d members), %d hits over %d positions,"
1548 " at step %d\n",
1549 name, 1000.0 * hover_seconds / positions, selected->name, 1000.0 * press_seconds / positions,
1550 g_list_length(members), hits, positions, dt_masks_gui_outline_step(dev));
1551 g_list_free(members);
1552}
1553
1554static void _time_overlay_form(dt_develop_t *dev, dt_masks_form_t *form, const char *name, const char *dir,
1555 const int img_w, const int img_h, const int frames)
1556{
1557 _set_frame(dev, img_w, img_h);
1558 if(IS_NULL_PTR(dev->form_gui))
1559 {
1560 dev->form_gui = (dt_masks_form_gui_t *)calloc(1, sizeof(dt_masks_form_gui_t));
1561 if(IS_NULL_PTR(dev->form_gui)) return;
1563 }
1564 dev->form_gui->dev = dev;
1565 dev->form_gui->form_visible = form;
1566 dt_dev_geometry_set_processed_size(dev, img_w, img_h);
1567
1568 /* fit the image into the screen, centred: what the darkroom shows at zoom "fit" -- or, with
1569 * MASKS_OVERLAY_VIEW="scale,offset_x,offset_y[,ppd]", the view the darkroom is showing, in
1570 * user units, with the surface's device scale, to reproduce what a user sees at a zoom */
1571 const double scale = MIN((double)OVERLAY_SCREEN_W / img_w, (double)OVERLAY_SCREEN_H / img_h);
1573 = { .scale = scale,
1574 .offset_x = 0.5 * (OVERLAY_SCREEN_W - scale * img_w),
1575 .offset_y = 0.5 * (OVERLAY_SCREEN_H - scale * img_h) };
1576 double ppd = 1.0;
1577 const char *view = g_getenv("MASKS_OVERLAY_VIEW");
1578 if(!IS_NULL_PTR(view))
1579 {
1580 double v[4] = { scale, transform.offset_x, transform.offset_y, 1.0 };
1581 gchar **parts = g_strsplit(view, ",", 4);
1582 for(int i = 0; i < 4 && !IS_NULL_PTR(parts[i]); i++) v[i] = g_ascii_strtod(parts[i], NULL);
1583 g_strfreev(parts);
1584 transform.scale = v[0];
1585 transform.offset_x = v[1];
1586 transform.offset_y = v[2];
1587 ppd = (v[3] > 0.0) ? v[3] : 1.0;
1588 }
1589
1590 cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, OVERLAY_SCREEN_W, OVERLAY_SCREEN_H);
1591 if(cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS)
1592 {
1593 cairo_surface_destroy(surface);
1594 return;
1595 }
1596 cairo_surface_set_device_scale(surface, ppd, ppd);
1597
1598 for(int selected = 0; selected < 2; selected++)
1599 {
1600 dev->form_gui->formid = 0; /* rebuild the outline cache for this form */
1601 dev->form_gui->geometry_generation = 0;
1602 dev->form_gui->group_selected = selected ? 0 : -1;
1604
1605 /* the first frame builds the outline, which the darkroom also does once per edit; it is
1606 * not the per-frame cost and is timed apart */
1607 _overlay_timing_t timing = { 0 };
1608 const double build_start = dt_get_wtime();
1609 _overlay_frame(dev, surface, &transform);
1610 timing.build_ms = 1000.0 * (dt_get_wtime() - build_start);
1611
1612 const double start = dt_get_wtime();
1613 for(int f = 0; f < frames; f++) _overlay_frame(dev, surface, &transform);
1614 timing.per_frame_ms = 1000.0 * (dt_get_wtime() - start) / MAX(frames, 1);
1615 _overlay_report(dev, name, img_w, img_h, selected, &timing, &transform);
1616
1617 if(!IS_NULL_PTR(dir))
1618 {
1619 char *png = g_strdup_printf("%s/%s-screen%s.png", dir, name, selected ? "-selected" : "");
1620 cairo_surface_flush(surface);
1621 cairo_surface_write_to_png(surface, png);
1622 g_free(png);
1623 }
1624
1625 const long leftovers = _overlay_leftovers(dev, surface, &transform);
1626 if(leftovers > 0)
1627 {
1628 printf("[FAIL] %-26s %-8s left %ld pixel(s) behind for the next, panned frame\n", name,
1629 selected ? "selected" : "member", leftovers);
1630 failures++;
1631 }
1632 }
1633 _time_hit_test(dev, form, name, img_w, img_h);
1634 cairo_surface_destroy(surface);
1635}
1636
1637/* The painted pixels' bounding box of an overlay rendered onto @p surface. */
1638static gboolean _painted_bbox(cairo_surface_t *surface, int *x0, int *y0, int *x1, int *y1)
1639{
1640 cairo_surface_flush(surface);
1641 const uint32_t *px = (const uint32_t *)cairo_image_surface_get_data(surface);
1642 const int stride = cairo_image_surface_get_stride(surface) / 4;
1643 const int w = cairo_image_surface_get_width(surface);
1644 const int h = cairo_image_surface_get_height(surface);
1645 gboolean any = FALSE;
1646 const uint32_t background = px[0]; /* the corner is letterboxed, never painted */
1647 for(int y = 0; y < h; y++)
1648 for(int x = 0; x < w; x++)
1649 {
1650 const uint32_t p = px[y * stride + x];
1651 if(p == background) continue;
1652 if(!any)
1653 {
1654 *x0 = x;
1655 *x1 = x;
1656 *y0 = y;
1657 *y1 = y;
1658 any = TRUE;
1659 }
1660 *x0 = MIN(*x0, x);
1661 *x1 = MAX(*x1, x);
1662 *y0 = MIN(*y0, y);
1663 *y1 = MAX(*y1, y);
1664 }
1665 return any;
1666}
1667
1668/* A HiDPI view: the same pixels, a device scale of 2 on the surface, half the user units. The
1669 * overlay must land on the same pixels as at scale 1 -- measured as the painted bounding box,
1670 * to within the antialiasing and the thicker lines a 2x screen is entitled to. This is the
1671 * check the darkroom on a 2x screen made necessary: everything at half size in the top-left
1672 * quadrant, because cairo's device space is not the pixel grid. */
1673static void _check_hidpi_placement(dt_develop_t *dev, dt_masks_form_t *form, const char *name, const int img_w,
1674 const int img_h)
1675{
1676 _set_frame(dev, img_w, img_h);
1677 dev->form_gui->form_visible = form;
1678 dt_dev_geometry_set_processed_size(dev, img_w, img_h);
1679 const double scale = MIN((double)OVERLAY_SCREEN_W / img_w, (double)OVERLAY_SCREEN_H / img_h);
1680 int bbox[2][4] = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 } };
1681 gboolean any[2] = { FALSE, FALSE };
1682 for(int hidpi = 0; hidpi < 2; hidpi++)
1683 {
1684 const double ppd = hidpi ? 2.0 : 1.0;
1685 cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, OVERLAY_SCREEN_W, OVERLAY_SCREEN_H);
1686 cairo_surface_set_device_scale(surface, ppd, ppd);
1688 = { .scale = scale / ppd,
1689 .offset_x = 0.5 * (OVERLAY_SCREEN_W - scale * img_w) / ppd,
1690 .offset_y = 0.5 * (OVERLAY_SCREEN_H - scale * img_h) / ppd };
1691 dev->form_gui->formid = 0;
1692 dev->form_gui->geometry_generation = 0;
1693 dev->form_gui->group_selected = 0;
1694 cairo_t *cr = cairo_create(surface);
1695 cairo_set_source_rgb(cr, 0.12, 0.12, 0.12);
1696 cairo_paint(cr);
1697 dt_masks_events_post_expose_with(dev, NULL, cr, (int)(OVERLAY_SCREEN_W / ppd), (int)(OVERLAY_SCREEN_H / ppd), -1, -1,
1698 &transform);
1699 cairo_destroy(cr);
1700 any[hidpi] = _painted_bbox(surface, &bbox[hidpi][0], &bbox[hidpi][1], &bbox[hidpi][2], &bbox[hidpi][3]);
1701 cairo_surface_destroy(surface);
1702 }
1703 const int tolerance = 12; /* antialiasing plus the wider lines and handles of a 2x screen */
1704 gboolean ok = any[0] && any[1];
1705 for(int i = 0; i < 4; i++)
1706 if(abs(bbox[0][i] - bbox[1][i]) > tolerance) ok = FALSE;
1707 printf("[%s] %-26s hidpi placement: 1x bbox (%d,%d)-(%d,%d) 2x bbox (%d,%d)-(%d,%d)\n", ok ? "PASS" : "FAIL", name,
1708 bbox[0][0], bbox[0][1], bbox[0][2], bbox[0][3], bbox[1][0], bbox[1][1], bbox[1][2], bbox[1][3]);
1709 if(!ok) failures++;
1710}
1711
1712static void _time_overlay_brush(dt_develop_t *dev, GList *nodes, const char *name, const char *dir,
1713 const int img_w, const int img_h, const int frames)
1714{
1715 dt_masks_form_t form = { 0 };
1716 form.type = DT_MASKS_BRUSH;
1718 form.version = 6;
1719 form.formid = 900;
1720 g_strlcpy(form.name, name, sizeof(form.name));
1721 form.points = nodes;
1722 _time_overlay_form(dev, &form, name, dir, img_w, img_h, frames);
1723 _check_hidpi_placement(dev, &form, name, img_w, img_h);
1724 g_list_free_full(form.points, free);
1725}
1726
1727/* A refcounted form the group can find in dev->forms, from a node list the harness built. */
1729 const dt_masks_functions_t *functions, GList *nodes, const int formid,
1730 const char *name)
1731{
1733 if(IS_NULL_PTR(form)) return NULL;
1734 form->functions = functions;
1735 form->version = 6;
1736 form->formid = formid;
1737 g_strlcpy(form->name, name, sizeof(form->name));
1738 form->points = nodes;
1739 dt_masks_append_form(dev, form);
1740 return form;
1741}
1742
1743/* The darkroom's actual frame: a group of many members, one of them selected, every one of
1744 * them stroked on every full frame. The single-shape cases price a shape; this prices what a
1745 * pipe frame pays for a mask-heavy edit, and what a static layer for the members that did not
1746 * change saves. Eleven members: every brush and polygon of the corpus in one group. */
1747static void _time_overlay_group(dt_develop_t *dev, const char *dir, const int frames)
1748{
1749 GList *members = NULL;
1750 int id = 1000;
1751 members = g_list_append(members, _group_member(dev, DT_MASKS_BRUSH, &dt_masks_functions_brush,
1752 _brush_from_table(_brush_1313, 11), id++, "brush-1313-cusp"));
1753 members = g_list_append(members, _group_member(dev, DT_MASKS_BRUSH, &dt_masks_functions_brush,
1754 _brush_from_table(_brush_cusp_tbl, 3), id++, "brush-cusp"));
1755 members = g_list_append(members, _group_member(dev, DT_MASKS_BRUSH, &dt_masks_functions_brush,
1756 _brush_from_table(_brush_hairpin_tbl, 3), id++, "brush-hairpin"));
1757 members = g_list_append(members, _group_member(dev, DT_MASKS_BRUSH, &dt_masks_functions_brush,
1758 _brush_from_table(_brush_zigzag_tbl, 6), id++, "brush-zigzag"));
1759 members = g_list_append(members, _group_member(dev, DT_MASKS_BRUSH, &dt_masks_functions_brush,
1760 _brush_from_table(_brush_selfcross_tbl, 5), id++, "brush-selfcross"));
1761 members = g_list_append(members, _group_member(dev, DT_MASKS_BRUSH, &dt_masks_functions_brush,
1762 _brush_from_table(_brush_concave_tbl, 5), id++, "brush-concave"));
1763 members = g_list_append(members, _group_member(dev, DT_MASKS_BRUSH, &dt_masks_functions_brush,
1764 _brush_from_table11(_brush_1360, 43), id++, "brush-1360"));
1765 members = g_list_append(members, _group_member(dev, DT_MASKS_BRUSH, &dt_masks_functions_brush,
1766 _brush_from_table11(_brush_1352, 7), id++, "brush-1352"));
1767 members = g_list_append(members, _group_member(dev, DT_MASKS_BRUSH, &dt_masks_functions_brush,
1768 _brush_from_table11(_brush_1074b, 8), id++, "brush-1074b"));
1769 {
1770 GList *nodes = NULL;
1771 for(int i = 0; i < 15; i++)
1772 {
1773 const float *r = _polygon_1788045925[i];
1774 nodes = g_list_append(nodes, _polygon_node(r[0], r[1], r[2], r[3], r[4], r[5], r[6]));
1775 }
1776 members = g_list_append(members, _group_member(dev, DT_MASKS_POLYGON, &dt_masks_functions_polygon, nodes, id++,
1777 "polygon-1788045925"));
1778 }
1779 {
1780 GList *nodes = NULL;
1781 const float radius = 0.028f;
1782 for(int i = 0; i < 5; i++)
1783 {
1784 const float x = 0.20f + 0.13f * i;
1785 nodes = g_list_append(nodes, _polygon_node(x, 0.35f, x, 0.35f, x, 0.35f, radius));
1786 nodes = g_list_append(nodes, _polygon_node(x + 0.05f, 0.62f, x + 0.05f, 0.62f, x + 0.05f, 0.62f, radius));
1787 }
1788 nodes = g_list_append(nodes, _polygon_node(0.80f, 0.78f, 0.80f, 0.78f, 0.80f, 0.78f, radius));
1789 nodes = g_list_append(nodes, _polygon_node(0.20f, 0.78f, 0.20f, 0.78f, 0.20f, 0.78f, radius));
1790 members = g_list_append(members, _group_member(dev, DT_MASKS_POLYGON, &dt_masks_functions_polygon, nodes, id++,
1791 "polygon-comb"));
1792 }
1793
1795 if(IS_NULL_PTR(group)) return;
1796 group->formid = 999;
1797 g_strlcpy(group->name, "group-11", sizeof(group->name));
1798 int count = 0;
1799 for(const GList *node = members; node; node = g_list_next(node))
1800 {
1801 const dt_masks_form_t *member = (const dt_masks_form_t *)node->data;
1802 if(IS_NULL_PTR(member)) continue;
1804 entry->formid = member->formid;
1805 entry->parentid = group->formid;
1807 entry->opacity = 1.0f;
1808 group->points = g_list_append(group->points, entry);
1809 count++;
1810 }
1811 char name[64];
1812 g_snprintf(name, sizeof(name), "group-%d", count);
1813 _time_overlay_form(dev, group, name, dir, IMG_W, IMG_H, frames);
1814
1815 /* the members leave dev->forms with the list's own references; the harness's follow */
1816 for(const GList *node = members; node; node = g_list_next(node))
1817 {
1818 dt_masks_form_t *member = (dt_masks_form_t *)node->data;
1819 if(IS_NULL_PTR(member)) continue;
1820 dev->forms = g_list_remove(dev->forms, member);
1821 dt_masks_form_unref(member); /* dev->forms's claim */
1822 dt_masks_form_unref(member); /* the harness's */
1823 }
1824 g_list_free(members);
1825 dt_masks_form_unref(group);
1826}
1827
1828static void _time_overlay_all(dt_develop_t *dev, const char *dir, const int frames)
1829{
1830 printf("overlay timing: %dx%d screen, fit zoom, %d frames per measurement\n", OVERLAY_SCREEN_W,
1831 OVERLAY_SCREEN_H, frames);
1832 _time_overlay_brush(dev, _brush_from_table(_brush_1313, 11), "brush-1313-cusp", dir, 5198, 3904, frames);
1833 _time_overlay_brush(dev, _brush_from_table(_brush_cusp_tbl, 3), "brush-cusp", dir, IMG_W, IMG_H, frames);
1834 _time_overlay_brush(dev, _brush_from_table(_brush_hairpin_tbl, 3), "brush-hairpin", dir, IMG_W, IMG_H, frames);
1835 _time_overlay_brush(dev, _brush_from_table(_brush_zigzag_tbl, 6), "brush-zigzag", dir, IMG_W, IMG_H, frames);
1836 _time_overlay_brush(dev, _brush_from_table(_brush_selfcross_tbl, 5), "brush-selfcross", dir, IMG_W, IMG_H, frames);
1837 _time_overlay_brush(dev, _brush_from_table(_brush_concave_tbl, 5), "brush-concave", dir, IMG_W, IMG_H, frames);
1838 _time_overlay_brush(dev, _brush_from_table11(_brush_1360, 43), "brush-1360-pressure-ramp", dir, 5184, 3888, frames);
1839 _time_overlay_brush(dev, _brush_from_table11(_brush_1352, 7), "brush-1352-radius-step", dir, IMG_W, IMG_H, frames);
1840 _time_overlay_brush(dev, _brush_from_table11(_brush_1074, 8), "brush-1074-flare", dir, 5184, 3456, frames);
1841 _time_overlay_brush(dev, _brush_from_table11(_brush_1074b, 8), "brush-1074-flare-b", dir, 5184, 3456, frames);
1842
1843 {
1844 dt_masks_form_t form = { 0 };
1845 form.type = DT_MASKS_POLYGON;
1847 form.version = 6;
1848 form.formid = 106;
1849 g_strlcpy(form.name, "polygon-1788045925", sizeof(form.name));
1850 for(int i = 0; i < 15; i++)
1851 {
1852 const float *r = _polygon_1788045925[i];
1853 form.points = g_list_append(form.points, _polygon_node(r[0], r[1], r[2], r[3], r[4], r[5], r[6]));
1854 }
1855 _time_overlay_form(dev, &form, "polygon-1788045925", dir, 5198, 3904, frames);
1856 _check_hidpi_placement(dev, &form, "polygon-1788045925", 5198, 3904);
1857 g_list_free_full(form.points, free);
1858 }
1859 {
1860 dt_masks_form_t form = { 0 };
1861 form.type = DT_MASKS_POLYGON;
1863 form.version = 6;
1864 form.formid = 104;
1865 g_strlcpy(form.name, "polygon-comb", sizeof(form.name));
1866 const float radius = 0.028f;
1867 for(int i = 0; i < 5; i++)
1868 {
1869 const float x = 0.20f + 0.13f * i;
1870 form.points = g_list_append(form.points, _polygon_node(x, 0.35f, x, 0.35f, x, 0.35f, radius));
1871 form.points = g_list_append(form.points,
1872 _polygon_node(x + 0.05f, 0.62f, x + 0.05f, 0.62f, x + 0.05f, 0.62f, radius));
1873 }
1874 form.points = g_list_append(form.points, _polygon_node(0.80f, 0.78f, 0.80f, 0.78f, 0.80f, 0.78f, radius));
1875 form.points = g_list_append(form.points, _polygon_node(0.20f, 0.78f, 0.20f, 0.78f, 0.20f, 0.78f, radius));
1876 _time_overlay_form(dev, &form, "polygon-comb", dir, IMG_W, IMG_H, frames);
1877 _check_hidpi_placement(dev, &form, "polygon-comb", IMG_W, IMG_H);
1878 g_list_free_full(form.points, free);
1879 }
1880 _time_overlay_group(dev, dir, frames);
1881}
1882
1883int main(int argc, char *argv[])
1884{
1885 /* Defaulting to "/tmp" wrote three dozen renders and CSVs to PREDICTABLE names in a
1886 * world-writable directory: anyone can pre-create those names, or leave symlinks under them
1887 * pointing elsewhere, and this follows them. ctest always passes an explicit directory, so it
1888 * only ever affected a manual run -- which is exactly when someone is poking at this as root.
1889 * g_mkdtemp() creates one atomically, mode 0700, under a name nobody can guess, and two
1890 * concurrent manual runs stop overwriting each other's output as a side effect. */
1891 char *scratch_out = NULL;
1892 const char *dir = (argc > 1 && argv[1][0] != '-') ? argv[1] : NULL;
1893 if(IS_NULL_PTR(dir))
1894 {
1895 scratch_out = g_strdup_printf("%s/ansel-test-masks-out-XXXXXX", g_get_tmp_dir());
1896 dir = g_mkdtemp(scratch_out);
1897 if(IS_NULL_PTR(dir))
1898 {
1899 fprintf(stderr, "[FAIL] could not create an output directory\n");
1900 g_free(scratch_out);
1901 return 1;
1902 }
1903 printf("output directory: %s\n", dir);
1904 }
1905 else
1906 g_mkdir_with_parents(dir, 0755);
1907
1908 /* Baselines live in the shared sample bank, beside the raw-export ones and reviewed the same
1909 * way. The bank is a plain clone, not a submodule (the superproject is public), so presence is
1910 * decided by what is on disk -- exactly as tests/image_test.sh decides it. Nothing to compare
1911 * against is not a failure: a fresh checkout without the bank runs the oracle and says so. */
1912 char *default_baseline
1913 = g_strdup(ANSEL_TEST_SOURCE_DIR "/tests/image_test/samples/baseline/masks-geometry");
1914 for(int i = 1; i < argc; i++)
1915 {
1916 if(!strcmp(argv[i], "--update-baseline")) baseline_update = TRUE;
1917 else if(!strcmp(argv[i], "--baseline") && i + 1 < argc)
1918 {
1919 g_free(default_baseline);
1920 default_baseline = g_strdup(argv[++i]);
1921 }
1922 else if(!strcmp(argv[i], "--time-overlay"))
1923 {
1925 if(i + 1 < argc && argv[i + 1][0] != '-') time_overlay_frames = atoi(argv[++i]);
1926 }
1927 else if(!strcmp(argv[i], "--no-baseline"))
1928 {
1929 g_free(default_baseline);
1930 default_baseline = NULL;
1931 }
1932 }
1933 if(!IS_NULL_PTR(default_baseline)
1934 && (baseline_update || g_file_test(default_baseline, G_FILE_TEST_IS_DIR)))
1935 baseline_dir = default_baseline;
1936
1937 /* The masks code allocates through the pixelpipe cache, whose lock dt_init() creates, and
1938 * reads conf for per-shape defaults -- so a geometry test still needs a booted instance,
1939 * just not a GUI one. Everything below is scratch: an in-memory library and temp dirs. */
1940 char *config_dir = g_strdup_printf("%s/ansel-test-masks-config-XXXXXX", g_get_tmp_dir());
1941 char *cache_dir = g_strdup_printf("%s/ansel-test-masks-cache-XXXXXX", g_get_tmp_dir());
1942 char *tmp_dir = g_strdup_printf("%s/ansel-test-masks-tmp-XXXXXX", g_get_tmp_dir());
1943 if(IS_NULL_PTR(g_mkdtemp(config_dir)) || IS_NULL_PTR(g_mkdtemp(cache_dir))
1944 || IS_NULL_PTR(g_mkdtemp(tmp_dir)))
1945 {
1946 fprintf(stderr, "[FAIL] could not create scratch directories\n");
1947 return 1;
1948 }
1949
1950 /* MASKS_DEBUG=1 turns on the masks and perf traces, which is how the per-stage cost of an
1951 * overlay frame is read; the corpus itself does not want them. */
1952 const gboolean debug = !IS_NULL_PTR(g_getenv("MASKS_DEBUG"));
1953 char *argv_override[] = {
1954 "ansel-test-masks-geometry",
1955 debug ? "-d" : "--conf", debug ? "masks" : "write_sidecar_files=FALSE",
1956 debug ? "-d" : "--conf", debug ? "perf" : "write_sidecar_files=FALSE",
1957 "--library", ":memory:",
1958 "--datadir", ANSEL_TEST_SOURCE_DIR "/data",
1959 // the build tree keeps its modules under src/, laid out as the installed tree expects
1960 "--moduledir", ANSEL_TEST_BINARY_DIR "/src",
1961 "--configdir", config_dir,
1962 "--cachedir", cache_dir,
1963 "--tmpdir", tmp_dir,
1964 "--disable-opencl",
1965 "--conf", "write_sidecar_files=FALSE",
1966 "-t", "1",
1967 NULL
1968 };
1969 const int argc_override = sizeof(argv_override) / sizeof(*argv_override) - 1;
1970 if(dt_init(argc_override, argv_override, FALSE, FALSE))
1971 {
1972 fprintf(stderr, "[FAIL] dt_init\n");
1973 return 1;
1974 }
1975
1976 dt_develop_t dev = { 0 };
1980 /* The GUI outline builder composes through the geometry chain, so a dev that never went
1981 * through dt_dev_init() needs one: without it the outlines never build and the overlay draws
1982 * nothing at all -- silently, because an empty outline is a legitimate result. */
1984 /* The outline builder composes through the geometry service, which refuses to answer until
1985 * the chain is AUTHORITATIVE -- a guard against transforming against a half-published chain.
1986 * Rebuilding it here over an empty module list publishes the only honest answer for a dev
1987 * with no pipeline: the identity. That is also what a geometry regression wants, so a
1988 * difference means the mask code changed and not some module's distortion. Without this the
1989 * builder returns ERROR and both the outline and the overlay come back empty -- silently,
1990 * because an empty outline is a legitimate result. */
1994 printf("geometry chain authoritative: %s\n",
1995 dt_geometry_chain_authoritative(dev.geometry_chain) ? "yes" : "NO -- outlines will be empty");
1996
1997 const char *step_env = g_getenv("MASKS_OUTLINE_STEP");
1998 if(!IS_NULL_PTR(step_env)) _outline_step_override = MAX(0, atoi(step_env));
2000 printf("GUI outlines sampled every %d px\n", _outline_step_override);
2001
2002 if(time_overlay)
2003 {
2005 return 0;
2006 }
2007 printf("mask geometry corpus -> %s\n", dir);
2008
2009 /* 0. THE REPORTED SHAPE. Issue #1313's follow-up: brush #1, cusp at node 8. The stroke loses
2010 * its radius toward the point of the cusp and leaves a V that is OPEN to the background --
2011 * which is why it must be measured against the disc union and not by counting holes.
2012 * Budget 0: any owed pixel the rasteriser does not deliver is the bug. */
2013 /* SWEEP THE FRAME SIZE, and that is not thoroughness for its own sake.
2014 *
2015 * The defect this case exists for is a floating-point cancellation, so whether it appears
2016 * at all depends on the pixel COORDINATES the normalised nodes land on -- that is, on the
2017 * frame size. At the cusp the two products 3*p2 and 3*p3 are mathematically equal and
2018 * cancel; what survives is the rounding of the -p0*a + p1*b terms above them, which are
2019 * tiny but not zero because the recursion never samples t at exactly 1. When that residue
2020 * is zero the old code took its degenerate branch and came out round; when it is not, the
2021 * code normalised the residue and the border direction became noise, leaving the reported
2022 * V hole.
2023 *
2024 * MEASURED, with the fix reverted: 14 of these 16 frames come out clean and two do not --
2025 * 5000x3750 (3936 px missing at the cusp) and 2999x2251 (1358 px, the same place scaled).
2026 * The reporter's own 5198x3904 is among the clean ones. A corpus pinned to a single frame
2027 * size would therefore have passed this shape while the reported defect was live, which is
2028 * exactly what it did for a whole round of this investigation. Do not reduce this list to
2029 * one size; if it ever needs trimming, keep 5000x3750 and 2999x2251, which are the two
2030 * that actually detect. */
2031 static const int frames[][2] = {
2032 { 5198, 3904 }, // the reporter's own frame -- clean, which is the trap
2033 { 5184, 3888 },
2034 { 5000, 3750 }, // DETECTS
2035 { 4321, 3241 },
2036 { 4000, 3000 },
2037 { 2999, 2251 }, // DETECTS
2038 { 2137, 1603 },
2039 { 1234, 987 },
2040 };
2041 for(int f = 0; f < (int)(sizeof(frames) / sizeof(*frames)); f++)
2042 {
2043 char *nm = g_strdup_printf("brush-1313-cusp-%dx%d", frames[f][0], frames[f][1]);
2044 const _brush_case_t c = { nm, dir, 0, frames[f][0], frames[f][1] };
2045 _run_brush_case_at(&dev, _brush_1313, 11, &c);
2046 g_free(nm);
2047 }
2048
2049 _run_brush_case(&dev, _brush_cusp_tbl, 3, "brush-cusp", dir, 0);
2050 _run_brush_case(&dev, _brush_hairpin_tbl, 3, "brush-hairpin", dir, 0);
2051 _run_brush_case(&dev, _brush_zigzag_tbl, 6, "brush-zigzag", dir, 0);
2052 _run_brush_case(&dev, _brush_selfcross_tbl, 5, "brush-selfcross", dir, 0);
2053 _run_brush_case(&dev, _brush_concave_tbl, 5, "brush-concave", dir, 0);
2054
2055 /* 3a. THE THIRD AND FOURTH REPORTED SHAPES. Both are judged in BOTH directions: #1360's
2056 * defect is coverage nobody owes (a circle the size of the frame), which the owed map
2057 * cannot see, and #1352's is the drawn border, which the overlay baseline sees. */
2058 {
2059 const _brush_case_t c1360 = { "brush-1360-pressure-ramp", dir, 0, 5184, 3888 };
2060 const _brush_case_t c1352 = { "brush-1352-radius-step", dir, 0, IMG_W, IMG_H };
2061 const _brush_case_t c1352_small = { "brush-1352-radius-step-2999x2251", dir, 0, 2999, 2251 };
2062 _run_brush_case11_at(&dev, _brush_1360, 43, &c1360);
2063 _run_brush_case11_at(&dev, _brush_1352, 7, &c1352);
2064 _run_brush_case11_at(&dev, _brush_1352, 7, &c1352_small);
2065 const _brush_case_t c1074 = { "brush-1074-flare", dir, 0, 5184, 3456 };
2066 _run_brush_case11_at(&dev, _brush_1074, 8, &c1074);
2067 const _brush_case_t c1074b = { "brush-1074-flare-b", dir, 0, 5184, 3456 };
2068 _run_brush_case11_at(&dev, _brush_1074b, 8, &c1074b);
2069 }
2070
2071 /* 3b. THE SECOND REPORTED SHAPE, polygon #2. Two defects were reported against it: the outer
2072 * border self-intersecting between nodes 0 and 14, where the outline runs into a
2073 * concavity, and a missing radial spoke in the feather at node 12, which is the shape's
2074 * one cusp. The second is a RASTER defect, so it is measured on the mask and not just
2075 * looked at. Frame-swept for the same reason the brush is: the geometry that decides both
2076 * is evaluated in pixels. */
2077 {
2078 static const int poly_frames[][2] = { { 5198, 3904 }, { 4000, 3000 }, { 2137, 1603 } };
2079 for(int f = 0; f < (int)(sizeof(poly_frames) / sizeof(*poly_frames)); f++)
2080 {
2081 dt_masks_form_t form = { 0 };
2082 form.type = DT_MASKS_POLYGON;
2084 form.version = 6;
2085 form.formid = 106;
2086 g_strlcpy(form.name, "polygon #2", sizeof(form.name));
2087 for(int i = 0; i < 15; i++)
2088 {
2089 const float *r = _polygon_1788045925[i];
2090 form.points = g_list_append(form.points,
2091 _polygon_node_state(r[0], r[1], r[2], r[3], r[4], r[5], r[6],
2092 (dt_masks_points_states_t)(int)r[7]));
2093 }
2094 char *nm = g_strdup_printf("polygon-1788045925-%dx%d", poly_frames[f][0], poly_frames[f][1]);
2095 const _brush_case_t c = { nm, dir, 0, poly_frames[f][0], poly_frames[f][1] };
2096 _run_polygon_form_at(&dev, &form, &c);
2097 g_free(nm);
2098 g_list_free_full(form.points, free);
2099 }
2100 }
2101
2102 /* 4. A polygon whose concave runs are tighter than its feather: its offset curve
2103 * self-intersects at every one of them, which is the geometry issue #1313 turned on --
2104 * the cuts that remove those folds must not remove anything else. */
2105 {
2106 dt_masks_form_t form = { 0 };
2107 form.type = DT_MASKS_POLYGON;
2109 form.version = 6;
2110 form.formid = 104;
2111 g_strlcpy(form.name, "comb polygon", sizeof(form.name));
2112 const float radius = 0.028f;
2113 const int teeth = 5;
2114 for(int i = 0; i < teeth; i++)
2115 {
2116 const float x = 0.20f + 0.13f * i;
2117 form.points = g_list_append(form.points, _polygon_node(x, 0.35f, x, 0.35f, x, 0.35f, radius));
2118 form.points = g_list_append(form.points, _polygon_node(x + 0.05f, 0.62f, x + 0.05f, 0.62f,
2119 x + 0.05f, 0.62f, radius));
2120 }
2121 form.points = g_list_append(form.points, _polygon_node(0.80f, 0.78f, 0.80f, 0.78f, 0.80f, 0.78f, radius));
2122 form.points = g_list_append(form.points, _polygon_node(0.20f, 0.78f, 0.20f, 0.78f, 0.20f, 0.78f, radius));
2123 const _brush_case_t c = { "polygon-comb", dir, 0, IMG_W, IMG_H };
2124 _run_polygon_form_at(&dev, &form, &c);
2125 g_list_free_full(form.points, free);
2126 }
2127
2128 /* 5. The two shapes the corpus had no case for at all. A circle is a degenerate ellipse and
2129 * the two files share most of their rasteriser by copy-paste, so anything factored out of
2130 * one has to be answerable for in the other -- and until now only the circle was covered.
2131 * Both are rotated and non-axis-aligned on purpose: an axis-aligned ellipse hides a whole
2132 * class of transform error, and a gradient at 0 or 90 degrees hides another. */
2133 {
2134 dt_masks_form_t form = { 0 };
2135 form.type = DT_MASKS_ELLIPSE;
2137 form.version = 6;
2138 form.formid = 107;
2139 g_strlcpy(form.name, "ellipse", sizeof(form.name));
2141 e->center[0] = 0.42f;
2142 e->center[1] = 0.55f;
2143 e->radius[0] = 0.20f;
2144 e->radius[1] = 0.09f;
2145 e->rotation = 27.0f;
2146 e->border = 0.04f;
2148 form.points = g_list_append(form.points, e);
2149 _run_case(&dev, &form, "ellipse-rotated", dir, 0, 0);
2150 g_list_free_full(form.points, free);
2151 }
2152
2153 {
2154 dt_masks_form_t form = { 0 };
2155 form.type = DT_MASKS_GRADIENT;
2157 form.version = 6;
2158 form.formid = 108;
2159 g_strlcpy(form.name, "gradient", sizeof(form.name));
2162 g->center[0] = 0.5f;
2163 g->center[1] = 0.5f;
2164 g->rotation = 34.0f;
2165 g->extent = 0.12f;
2166 g->steepness = 0.0f;
2167 g->curvature = 0.3f;
2169 form.points = g_list_append(form.points, g);
2170 /* A gradient covers the frame edge to edge, so "enclosed holes" is the only thing to assert
2171 * and coverage is whatever the ramp gives; the baseline is what actually pins its shape. */
2172 _run_case(&dev, &form, "gradient-curved", dir, 0, 0);
2173 g_list_free_full(form.points, free);
2174 }
2175
2176 /* 6. A circle, as the control: no joints, no folds. If this ever grows a hole the fault is
2177 * in the fill, not in any of the geometry above. */
2178 {
2179 dt_masks_form_t form = { 0 };
2180 form.type = DT_MASKS_CIRCLE;
2182 form.version = 6;
2183 form.formid = 105;
2184 g_strlcpy(form.name, "circle", sizeof(form.name));
2186 c->center[0] = 0.5f; c->center[1] = 0.5f;
2187 c->radius = 0.15f; c->border = 0.03f;
2188 form.points = g_list_append(form.points, c);
2189 _run_case(&dev, &form, "circle-control", dir, 0, 0);
2190 g_list_free_full(form.points, free);
2191 }
2192
2195
2197 printf("baseline: not compared (no %s)\n",
2198 "tests/image_test/samples/baseline/masks-geometry -- clone the bank to enable it");
2199 else if(baseline_missing > 0)
2200 printf("baseline: %d render(s) have no entry yet -- run with --update-baseline to add them\n",
2202
2203 printf("%s: %d failing case(s)\n", failures ? "FAIL" : "PASS", failures);
2204 g_free(scratch_out);
2205
2206 dt_cleanup();
2207 g_free(config_dir);
2208 g_free(cache_dir);
2209 g_free(tmp_dir);
2210 return failures ? 1 : 0;
2211}
2212
2213// clang-format off
2214// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
2215// vim: shiftwidth=2 expandtab tabstop=2 cindent
2216// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
2217// clang-format on
static double dist(double x1, double y1, double x2, double y2)
Definition ashift_lsd.c:250
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
#define m
Definition basecurve.c:283
const dt_masks_functions_t dt_masks_functions_circle
Definition circle.c:1078
static void transform(float *x, float *o, const float *m, const float t_h, const float t_v)
Definition clipping.c:487
static const float x
const float f
const int t
const float v
const dt_colormatrix_t dt_aligned_pixel_t out
const float top
static const int row
void dt_cleanup()
Definition darktable.c:1864
int dt_init(int argc, char *argv[], const gboolean init_gui, const gboolean load_data)
Definition darktable.c:885
void dt_dev_geometry_set_processed_size(dt_develop_t *dev, const int32_t width, const int32_t height)
void dt_dev_geometry_init(dt_develop_t *dev)
void dt_dev_geometry_set_raw_size(dt_develop_t *dev, const int32_t width, const int32_t height, const gboolean valid)
const dt_masks_functions_t dt_masks_functions_brush
GHashTable * selected
set of checked row labels, mirrored to conf on every change
GtkWidget * view
the tree view
static int dt_pthread_rwlock_destroy(dt_pthread_rwlock_t *lock)
Definition dtpthread.h:212
static int dt_pthread_rwlock_init(dt_pthread_rwlock_t *lock, const pthread_rwlockattr_t *attr)
Definition dtpthread.h:192
const dt_masks_functions_t dt_masks_functions_ellipse
Definition ellipse.c:1643
dt_geometry_chain_t * dt_geometry_chain_new(void)
Definition geometry.c:119
gboolean dt_geometry_chain_authoritative(const dt_geometry_chain_t *chain)
Can this chain answer questions yet?
Definition geometry.c:324
void dt_geometry_chain_rebuild(dt_develop_t *dev)
Rebuild the chain from the dev's current modules and history. GUI thread only.
Definition geometry.c:247
Where things are on the image, answered without a pipeline.
const dt_masks_functions_t dt_masks_functions_gradient
Definition gradient.c:1470
const char flag
Definition image.h:311
_lib_location_type_t type
Definition location.c:1
dt_map_box_t bbox
Definition location.c:4
float *const restrict const size_t k
#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
dt_masks_raster_result_t dt_masks_get_points_border(dt_develop_t *develop, dt_masks_form_t *mask_form, float **point_buffer, int *point_count, float **border_buffer, int *border_count, dt_masks_skip_range_t **border_skips, int *border_skip_count, int source, dt_iop_module_t *module)
Definition masks.c:238
gboolean dt_masks_skip_contains(const dt_masks_skip_range_t *skips, const int skip_count, const int index)
Definition masks.c:306
dt_masks_raster_result_t
What a rasterisation attempt produced.
Definition masks.h:313
@ DT_MASKS_RASTER_OK
Definition masks.h:314
@ DT_MASKS_ELLIPSE_EQUIDISTANT
Definition masks.h:182
dt_masks_form_t * dt_masks_create(dt_masks_type_t type)
Definition masks.c:851
dt_masks_form_t * dt_masks_get_from_id(dt_develop_t *dev, int id)
Definition masks.c:909
dt_masks_points_states_t
Definition masks.h:155
@ DT_MASKS_POINT_STATE_NORMAL
Definition masks.h:156
@ DT_MASKS_GRADIENT_STATE_SIGMOIDAL
Definition masks.h:163
gboolean dt_masks_debug_write_outline_csv(dt_develop_t *dev, dt_masks_form_t *form, const char *path)
Write the shape's outline buffers to path as CSV: index, centreline, border.
Definition masks_debug.c:87
float * dt_masks_debug_rasterise(dt_develop_t *dev, dt_masks_form_t *form, const int width, const int height)
Rasterise form into a freshly allocated width*height float buffer, 0..1.
Definition masks_debug.c:37
Render a mask's rasterisation and its GUI overlay to an image file, headlessly.
@ DT_MASKS_DEBUG_BACKDROP_RASTER
Definition masks_debug.h:59
What a mask outline needs in order to place itself, and who supplies it.
int dt_masks_gui_outline_step(const dt_develop_t *dev)
The density the dev's GUI shows its outlines at, in image pixels between consecutive samples: what th...
Definition masks_gui.c:1908
The per-shape function table, private to the masks implementation.
const dt_masks_functions_t dt_masks_functions_polygon
Definition polygon.c:3630
static void _run_polygon_form_at(dt_develop_t *dev, dt_masks_form_t *form, const _brush_case_t *const c)
static int baseline_missing
static void _stamp_disc_rows(int32_t *const diff, const size_t stride, const int w, const int h, const int cx, const int cy, const int r_i)
static gboolean time_overlay
static const float _brush_selfcross_tbl[5][9]
static int _path_dense_samples(const GList *const nodes, const int w, const int h, const _disc_pick_t pick, float *const px, float *const py, int32_t *const diff)
static void _time_overlay_brush(dt_develop_t *dev, GList *nodes, const char *name, const char *dir, const int img_w, const int img_h, const int frames)
static void _overlay_frame(dt_develop_t *dev, cairo_surface_t *surface, const dt_masks_overlay_transform_t *const transform)
static dt_masks_node_brush_t * _brush_node(const float x, const float y, const float c1x, const float c1y, const float c2x, const float c2y, const float radius)
static void _check_hidpi_placement(dt_develop_t *dev, dt_masks_form_t *form, const char *name, const int img_w, const int img_h)
static dt_masks_node_polygon_t * _polygon_node(const float x, const float y, const float c1x, const float c1y, const float c2x, const float c2y, const float radius)
static gboolean _painted_bbox(cairo_surface_t *surface, int *x0, int *y0, int *x1, int *y1)
static gboolean _row_table_put(const _row_table_t *const t, const int y, const float x)
static dt_masks_form_t * _group_member(dt_develop_t *dev, const dt_masks_type_t type, const dt_masks_functions_t *functions, GList *nodes, const int formid, const char *name)
static GList * _brush_from_table(const float table[][9], const int count)
static void _time_overlay_form(dt_develop_t *dev, dt_masks_form_t *form, const char *name, const char *dir, const int img_w, const int img_h, const int frames)
static void _time_overlay_all(dt_develop_t *dev, const char *dir, const int frames)
_disc_pick_t
@ DISC_OWED
@ DISC_PERMITTED
static void _flood_component(const uint8_t *const flag, uint8_t *const seen, int *const stack, const int w, const int h, const size_t seed, long *const sum)
static void _run_brush_nodes_at(dt_develop_t *dev, GList *nodes, const _brush_case_t *const c)
#define MASKS_BASELINE_MAX_DELTA
static long _overlay_leftovers(dt_develop_t *dev, cairo_surface_t *surface, const dt_masks_overlay_transform_t *const transform)
static void _run_brush_case_at(dt_develop_t *dev, const float table[][9], const int count, const _brush_case_t *const c)
_runs_mode_t
@ RUNS_MISSING
@ RUNS_EXCESS
#define OVERLAY_SCREEN_W
#define MASKS_BASELINE_NOISE_PX
#define MASKS_BASELINE_MAX_SHARE
static int failures
static const char * baseline_dir
static void _overlay_dump_skips(const dt_masks_form_gui_points_t *const gp, const char *name, const dt_masks_overlay_transform_t *const transform)
static const float _brush_concave_tbl[5][9]
static dt_masks_node_polygon_t * _polygon_node_state(const float x, const float y, const float c1x, const float c1y, const float c2x, const float c2y, const float radius, const dt_masks_points_states_t state)
static void _write_missing_map(const char *dir, const char *name, const float *const mask, const uint8_t *const reference, const int w, const int h)
static gboolean _baseline_check(const char *path, const char *name)
static void _run_brush_case11_at(dt_develop_t *dev, const float table[][11], const int count, const _brush_case_t *const c)
static void _surface_diff(cairo_surface_t *const a, cairo_surface_t *const b, size_t *const differing, int *const worst, int *const wx, int *const wy)
#define PATH_SAMPLES_PER_SEGMENT
static const float _brush_1352[7][11]
static void _run_case(dt_develop_t *dev, dt_masks_form_t *form, const char *name, const char *dir, const int max_holes, const int max_hole_px)
static int _overlay_skip_of(const dt_masks_form_gui_points_t *const gp, const int i)
static void _path_interior(const float *const px, const float *const py, const int count, const int w, const int h, int32_t *const diff, const size_t stride)
static void _time_overlay_group(dt_develop_t *dev, const char *dir, const int frames)
static const float _brush_1360[43][11]
static int time_overlay_frames
static void _path_row_crossings(const float *const px, const float *const py, const int count, const int h, const _row_table_t *const t)
#define IMG_H
static void _polygon_disc_union(const GList *const nodes, const int w, const int h, const _disc_pick_t pick, uint8_t *const map)
static const float _brush_1313[11][9]
static void _brush_disc_union(const GList *const nodes, const int w, const int h, const _disc_pick_t pick, uint8_t *const map)
static gboolean baseline_update
#define IMG_W
static _band_t _outline_band_check(dt_develop_t *dev, dt_masks_form_t *form, const uint8_t *owed, const uint8_t *permitted, const int w, const int h)
static const float _brush_hairpin_tbl[3][9]
static const float _polygon_1788045925[15][8]
static const float _brush_1074[8][11]
static const float _brush_zigzag_tbl[6][9]
static const float _brush_cusp_tbl[3][9]
#define OVERLAY_SCREEN_H
static int _outline_step_override
static const float _brush_1074b[8][11]
static void _run_brush_case(dt_develop_t *dev, const float table[][9], const int count, const char *name, const char *dir, const int budget_px)
static void _overlay_skipped(const dt_masks_form_gui_points_t *const gp, int *skipped, int *ranges)
static void _coverage_runs(const float *mask, const uint8_t *reference, const int w, const int h, const _runs_mode_t mode, _runs_t *const out)
static GList * _brush_from_table11(const float table[][11], const int count)
static void _measure(const float *mask, const int w, const int h, double *coverage, int *hole_count, int *largest_hole)
static void _outline_density_apply(dt_develop_t *dev, const double image_px_per_device_px)
static void _set_frame(dt_develop_t *dev, const int w, const int h)
static void _run_case_at(dt_develop_t *dev, dt_masks_form_t *form, const char *name, const char *dir, const int max_holes, const int max_hole_px, const int img_w, const int img_h)
static void _overlay_report(dt_develop_t *dev, const char *name, const int img_w, const int img_h, const gboolean selected, const _overlay_timing_t *const timing, const dt_masks_overlay_transform_t *const transform)
static void _judge_raster(dt_develop_t *dev, dt_masks_form_t *form, const _brush_case_t *const c, const float *const mask, const uint8_t *const owed, const uint8_t *const permitted)
static void _time_hit_test(dt_develop_t *dev, dt_masks_form_t *form, const char *name, const int img_w, const int img_h)
void dt_masks_gui_reset_dragging(dt_masks_form_gui_t *gui)
Definition masks_gui.c:1463
void dt_masks_gui_set_outline_density(dt_masks_form_gui_t *mask_gui, const double image_px_per_device_px)
Definition masks_gui.c:1901
void dt_masks_append_form(dt_develop_t *develop, dt_masks_form_t *mask_form)
Definition masks_gui.c:2505
void dt_masks_events_post_expose_with(dt_develop_t *dev, struct dt_iop_module_t *module, cairo_t *cr, int32_t width, int32_t height, int32_t pointerx, int32_t pointery, const dt_masks_overlay_transform_t *transform)
Draw the mask overlay with an explicit mapping; transform NULL means the viewport's.
Definition masks_gui.c:4663
gboolean dt_masks_debug_write_png(dt_develop_t *dev, dt_masks_form_t *form, const dt_masks_debug_request_t *request, const char *path)
Render form per request and write it to path as a PNG.
Definition masks_gui.c:5910
void dt_masks_init_form_gui(dt_develop_t *dev, dt_masks_form_gui_t *mask_gui)
Definition masks_gui.c:1847
void dt_masks_form_unref(dt_masks_form_t *form)
@ DT_MASKS_STATE_SHOW
Definition masks_types.h:90
@ DT_MASKS_STATE_USE
Definition masks_types.h:89
dt_masks_type_t
Definition masks_types.h:62
@ DT_MASKS_POLYGON
Definition masks_types.h:65
@ DT_MASKS_BRUSH
Definition masks_types.h:70
@ DT_MASKS_ELLIPSE
Definition masks_types.h:69
@ DT_MASKS_GRADIENT
Definition masks_types.h:68
@ DT_MASKS_CIRCLE
Definition masks_types.h:64
@ DT_MASKS_GROUP
Definition masks_types.h:66
#define CLAMPF(a, mn, mx)
Definition math.h:91
#define dt_free_align(ptr)
Release memory from dt_alloc_align() and set ptr to NULL.
Definition mem_alloc.h:214
size_t size
Definition mipmap_cache.c:3
static double reference(const op_t op, const double x)
const char * name
Definition pdf.h:90
#define dt_pixelpipe_cache_free_align(mem)
int main()
Definition prova.c:47
const float uint32_t state[4]
const float r
double seconds
const char * name
const char * dir
const int * at
dt_pthread_rwlock_t history_mutex
Definition develop.h:247
struct dt_masks_form_gui_t * form_gui
Definition develop.h:310
dt_pthread_rwlock_t masks_mutex
Definition develop.h:316
struct dt_geometry_chain_t * geometry_chain
Definition develop.h:491
GList * forms
Definition develop.h:304
What to render. Zeroed means: raster only, at full image resolution, on black.
Definition masks_debug.h:64
dt_masks_skip_range_t * border_skips
Definition masks_gui.h:70
uint64_t geometry_generation
Definition masks_gui.h:200
dt_masks_form_t * form_visible
Definition masks_gui.h:105
gboolean form_selected
Definition masks_gui.h:151
struct dt_develop_t * dev
Definition masks_gui.h:101
const dt_masks_functions_t * functions
Definition masks.h:255
dt_masks_type_t type
Definition masks.h:254
char name[128]
Definition masks.h:277
GList * points
Definition masks.h:253
void(* get_distance)(float x, float y, float as, struct dt_masks_form_gui_t *gui, int index, int num_points, int *inside, int *inside_border, int *near_handle, int *inside_source, float *dist)
dt_masks_ellipse_flags_t flags
Definition masks.h:208
How the overlay maps image coordinates onto its cairo target.
Definition masks_gui.h:598
One cut in a shape's border outline: while walking the border buffer forward, on reaching index jump_...
typedef double((*spd)(unsigned long int wavelength, double TempK))
#define ANSEL_TEST_SOURCE_DIR
#define ANSEL_TEST_BINARY_DIR
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29
static double dt_get_wtime(void)
Definition times.h:43
void dt_widget_set_mouse_radius(float radius, float clamped)