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