Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
test_history_snapshot.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
19/* The contract between a history snapshot and the writer's copy-on-write gate.
20 *
21 * dt_dev_pixelpipe_change() resyncs a pipe against a dt_dev_history_snapshot_t with
22 * history_mutex RELEASED. That is only sound if two things hold, and this file pins both:
23 *
24 * 1. the snapshot holds one reference per item, so nothing it walks can be freed under it;
25 * 2. dt_dev_history_cow_touch() clones a shared item instead of mutating it in place, so
26 * nothing the snapshot walks can CHANGE under it either -- and the pipe's last-synced
27 * marker, which the GUI thread re-points at the clone, keeps every refcount balanced.
28 *
29 * A dt_develop_t is built by hand here: the functions under test touch dev->history,
30 * dev->history_mutex, dev->pipe and dev->preview_pipe and nothing else, and dt_dev_init()
31 * would drag the whole application in for no gain. */
32
33#include "develop/develop.h"
34#include "develop/dev_history.h"
36#include "system/atomic.h"
37#include "system/dtpthread.h"
38
39#include <glib.h>
40#include <stdarg.h>
41#include <stddef.h>
42#include <stdlib.h>
43// cmocka.h declares `extern jmp_buf global_expect_assert_env' at file scope without including
44// <setjmp.h> itself. Same suppression as the other tests here, and for the same reason.
45#include <setjmp.h> // NOLINT(misc-include-cleaner)
46#include <stdint.h>
47#include <cmocka.h>
48
49#ifdef _WIN32
50#include "win/main_wrapper.h"
51#endif
52
58
59static int _setup(void **state)
60{
61 _fixture_t *f = calloc(1, sizeof(_fixture_t));
62 if(!f) return -1;
63 dt_pthread_rwlock_init(&f->dev.history_mutex, NULL);
64 dt_pthread_mutex_init(&f->dev.transient_params_mutex, NULL);
65 f->dev.pipe = &f->pipe;
66 f->dev.preview_pipe = NULL;
67 *state = f;
68 return 0;
69}
70
71static int _teardown(void **state)
72{
73 _fixture_t *f = *state;
74 // Whatever a test left in the pipe's marker and in history is released here, so a test
75 // that asserts on refcounts mid-way does not have to also be its own janitor.
76 dt_dev_free_history_item(dt_atomic_exch_ptr(&f->pipe.last_history_item, NULL));
77 g_list_free_full(f->dev.history, dt_dev_free_history_item);
78 f->dev.history = NULL;
79 dt_free(f->dev.transient_params.params);
80 dt_free(f->dev.transient_params.blend_params);
81 dt_pthread_mutex_destroy(&f->dev.transient_params_mutex);
82 dt_pthread_rwlock_destroy(&f->dev.history_mutex);
83 free(f);
84 return 0;
85}
86
87static int _refs(const dt_dev_history_item_t *item)
88{
89 return dt_atomic_get_int(&((dt_dev_history_item_t *)item)->refcount);
90}
91
93{
95 assert_non_null(item);
96 item->hash = hash;
97 dev->history = g_list_append(dev->history, item);
98 item->num = g_list_index(dev->history, item);
99 return item;
100}
101
103{
104 _fixture_t *f = *state;
105 dt_dev_history_item_t *a = _append(&f->dev, 0xA);
106 dt_dev_history_item_t *b = _append(&f->dev, 0xB);
107 dt_dev_history_item_t *c = _append(&f->dev, 0xC);
108 f->dev.history_end = 3;
109 dt_dev_set_history_hash(&f->dev, 0xABC);
110
112 dt_pthread_rwlock_rdlock(&f->dev.history_mutex);
113 dt_dev_history_snapshot_take(&f->dev, &snap);
114 dt_pthread_rwlock_unlock(&f->dev.history_mutex);
115
116 // Same items, same order, one extra reference each. No copy: the pointers are identical.
117 assert_int_equal(g_list_length(snap.items), 3);
118 assert_ptr_equal(g_list_nth_data(snap.items, 0), a);
119 assert_ptr_equal(g_list_nth_data(snap.items, 1), b);
120 assert_ptr_equal(g_list_nth_data(snap.items, 2), c);
121 assert_int_equal(_refs(a), 2);
122 assert_int_equal(_refs(b), 2);
123 assert_int_equal(_refs(c), 2);
124
125 // end and hash travel with the list.
126 assert_int_equal(snap.history_end, 3);
127 assert_int_equal(snap.history_hash, 0xABC);
128
130 assert_null(snap.items);
131 assert_int_equal(_refs(a), 1);
132 assert_int_equal(_refs(b), 1);
133 assert_int_equal(_refs(c), 1);
134}
135
137{
138 _fixture_t *f = *state;
139 _append(&f->dev, 0x1);
140 _append(&f->dev, 0x2);
141 f->dev.history_end = 99; // stale, larger than the list
142
144 dt_pthread_rwlock_rdlock(&f->dev.history_mutex);
145 dt_dev_history_snapshot_take(&f->dev, &snap);
146 dt_pthread_rwlock_unlock(&f->dev.history_mutex);
147
148 assert_int_equal(snap.history_end, 2);
150}
151
153{
154 _fixture_t *f = *state;
155 dt_dev_history_item_t *item = _append(&f->dev, 0x1);
156 assert_int_equal(_refs(item), 1);
157
158 dt_dev_history_item_t *touched = dt_dev_history_cow_touch(&f->dev, item);
159
160 // Nobody else can see it: mutate in place, no clone, no churn.
161 assert_ptr_equal(touched, item);
162 assert_int_equal(_refs(item), 1);
163 assert_ptr_equal(g_list_nth_data(f->dev.history, 0), item);
164}
165
167{
168 _fixture_t *f = *state;
169 dt_dev_history_item_t *original = _append(&f->dev, 0x1);
170 original->enabled = TRUE;
171 f->dev.history_end = 1;
172
174 dt_pthread_rwlock_rdlock(&f->dev.history_mutex);
175 dt_dev_history_snapshot_take(&f->dev, &snap);
176 dt_pthread_rwlock_unlock(&f->dev.history_mutex);
177 assert_int_equal(_refs(original), 2);
178
179 dt_dev_history_item_t *clone = dt_dev_history_cow_touch(&f->dev, original);
180
181 // The writer got a fresh object and dev->history now names it ...
182 assert_non_null(clone);
183 assert_ptr_not_equal(clone, original);
184 assert_ptr_equal(g_list_nth_data(f->dev.history, 0), clone);
185 assert_int_equal(_refs(clone), 1);
186 assert_int_equal(clone->hash, original->hash);
187 assert_true(clone->enabled);
188
189 // ... while the snapshot still names the original, which it alone now keeps alive.
190 assert_ptr_equal(g_list_nth_data(snap.items, 0), original);
191 assert_int_equal(_refs(original), 1);
192
193 // A mutation of the clone is invisible through the snapshot -- the whole point.
194 clone->enabled = FALSE;
195 clone->hash = 0x2;
196 assert_true(((dt_dev_history_item_t *)g_list_nth_data(snap.items, 0))->enabled);
197 assert_int_equal(((dt_dev_history_item_t *)g_list_nth_data(snap.items, 0))->hash, 0x1);
198
199 dt_dev_history_snapshot_release(&snap); // drops the original's last reference
200}
201
203{
204 _fixture_t *f = *state;
205 dt_dev_history_item_t *original = _append(&f->dev, 0x1);
206 f->dev.history_end = 1;
207
208 // The pipe recorded this item as its last-synced marker, holding a reference on it, the
209 // way _pipe_set_last_history_item() does at the end of a resync.
210 dt_dev_history_item_ref(original);
211 dt_atomic_set_ptr(&f->pipe.last_history_item, original);
212 assert_int_equal(_refs(original), 2); // history + pipe
213
215 dt_pthread_rwlock_rdlock(&f->dev.history_mutex);
216 dt_dev_history_snapshot_take(&f->dev, &snap);
217 dt_pthread_rwlock_unlock(&f->dev.history_mutex);
218 assert_int_equal(_refs(original), 3); // history + pipe + snapshot
219
220 dt_dev_history_item_t *clone = dt_dev_history_cow_touch(&f->dev, original);
221 assert_ptr_not_equal(clone, original);
222
223 // The marker followed the clone, so an in-place top-entry rewrite keeps its bounded resync;
224 // the reference the marker held on the original moved with it.
225 assert_ptr_equal(dt_atomic_get_ptr(&f->pipe.last_history_item), clone);
226 assert_int_equal(_refs(clone), 2); // history + pipe
227 assert_int_equal(_refs(original), 1); // snapshot only
228
230
231 // Dropping the pipe's marker the way dt_dev_pixelpipe_cleanup() does leaves the clone
232 // owned by history alone: nothing leaked, nothing double-released.
233 dt_dev_free_history_item(dt_atomic_exch_ptr(&f->pipe.last_history_item, NULL));
234 assert_int_equal(_refs(clone), 1);
235}
236
238{
239 _fixture_t *f = *state;
240 dt_dev_history_item_t *first = _append(&f->dev, 0x1);
241 dt_dev_history_item_t *second = _append(&f->dev, 0x2);
242 f->dev.history_end = 2;
243
245 dt_atomic_set_ptr(&f->pipe.last_history_item, first);
246
248 dt_pthread_rwlock_rdlock(&f->dev.history_mutex);
249 dt_dev_history_snapshot_take(&f->dev, &snap);
250 dt_pthread_rwlock_unlock(&f->dev.history_mutex);
251
252 dt_dev_history_item_t *clone = dt_dev_history_cow_touch(&f->dev, second);
253 assert_ptr_not_equal(clone, second);
254
255 // The marker named `first', not the item being cloned: it must not move.
256 assert_ptr_equal(dt_atomic_get_ptr(&f->pipe.last_history_item), first);
257 assert_int_equal(_refs(first), 3); // history + pipe + snapshot
258 assert_int_equal(_refs(clone), 1); // history
259 assert_int_equal(_refs(second), 1); // snapshot
260
262}
263
282{
283 _fixture_t *f = *state;
284 dt_iop_module_t module = { 0 };
285 module.dev = &f->dev;
286
287 uint32_t params = 0x1111u;
288 uint32_t out = 0u;
289 uint64_t first = 0u;
290 uint64_t second = 0u;
291 uint64_t repeat = 0u;
292
293 dt_dev_transient_params_set(&module, &params, sizeof(params), NULL, 0);
294 assert_true(dt_dev_transient_params_get(&f->dev, &module, &out, sizeof(out), NULL, 0, NULL, &first));
295 assert_int_equal(out, 0x1111u);
296 assert_true(first != 0u);
297
298 /* A second publication of DIFFERENT params must not collide with the first. */
299 params = 0x2222u;
300 dt_dev_transient_params_set(&module, &params, sizeof(params), NULL, 0);
301 assert_true(dt_dev_transient_params_get(&f->dev, &module, &out, sizeof(out), NULL, 0, NULL, &second));
302 assert_int_equal(out, 0x2222u);
303 assert_true(second != first);
304
305 /* And a publication of the SAME bytes must still advance it. A realtime stroke can repaint
306 * an identical blob -- what makes the frame new is the canvas the module reads, not the
307 * params -- so a serial that only moved on a byte change would drop those frames. */
308 dt_dev_transient_params_set(&module, &params, sizeof(params), NULL, 0);
309 assert_true(dt_dev_transient_params_get(&f->dev, &module, &out, sizeof(out), NULL, 0, NULL, &repeat));
310 assert_true(repeat != second);
311
312 /* A module that published nothing gets no serial and no params. */
313 dt_iop_module_t other = { 0 };
314 other.dev = &f->dev;
315 uint64_t none = 12345u;
316 assert_false(dt_dev_transient_params_get(&f->dev, &other, &out, sizeof(out), NULL, 0, NULL, &none));
317 assert_int_equal(none, 0u);
318}
319
320int main(int argc, char *argv[])
321{
322 const struct CMUnitTest tests[] = {
325 cmocka_unit_test_setup_teardown(_cow_touch_leaves_an_exclusively_owned_item_alone, _setup, _teardown),
326 cmocka_unit_test_setup_teardown(_cow_touch_clones_an_item_a_snapshot_is_holding, _setup, _teardown),
330 };
331
332 return cmocka_run_group_tests(tests, NULL, NULL);
333}
334
335// clang-format off
336// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
337// vim: shiftwidth=2 expandtab tabstop=2 cindent
338// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
339// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
int dt_atomic_get_int(dt_atomic_int *var)
static void * dt_atomic_exch_ptr(dt_atomic_ptr *var, void *value)
Definition atomic.h:80
static void * dt_atomic_get_ptr(const dt_atomic_ptr *var)
Definition atomic.h:76
static void dt_atomic_set_ptr(dt_atomic_ptr *var, void *value)
Definition atomic.h:75
const float f
const dt_colormatrix_t dt_aligned_pixel_t out
gboolean dt_dev_transient_params_get(dt_develop_t *dev, const dt_iop_module_t *module, void *out_params, const size_t out_params_size, void *out_blend, const size_t out_blend_size, gboolean *out_has_blend, uint64_t *out_serial)
void dt_dev_history_snapshot_release(dt_dev_history_snapshot_t *snapshot)
Release every reference a snapshot holds and reset it. Needs no lock.
dt_dev_history_item_t * dt_dev_history_item_create(void)
Allocate a fresh, blank history item with refcount 1.
void dt_dev_transient_params_set(dt_iop_module_t *module, const void *params, const size_t params_size, const void *blend_params, const size_t blend_size)
Out-of-history transient param channel.
void dt_dev_free_history_item(gpointer data)
Release a reference to a history item (used as GList free callback).
dt_dev_history_item_t * dt_dev_history_item_ref(dt_dev_history_item_t *item)
Take a reference on a history item. Pair with dt_dev_free_history_item().
void dt_dev_history_snapshot_take(dt_develop_t *dev, dt_dev_history_snapshot_t *snapshot) REQUIRES_SHARED(dev -> history_mutex)
Take a snapshot of dev->history. Caller holds history_mutex (reader suffices).
dt_dev_history_item_t * dt_dev_history_cow_touch(dt_develop_t *dev, dt_dev_history_item_t *hist)
Copy-on-write gate for mutating a history item in place.
static void dt_dev_set_history_hash(dt_develop_t *dev, const uint64_t history_hash)
Definition develop.h:499
static int dt_pthread_rwlock_unlock(dt_pthread_rwlock_t *rwlock) RELEASE_GENERIC(rwlock) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:217
static int dt_pthread_rwlock_rdlock(dt_pthread_rwlock_t *rwlock) ACQUIRE_SHARED(rwlock) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:267
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Initialise a mutex. With mutexattr NULL – which is how 54 of the 56 call sites in this tree spell it ...
Definition dtpthread.h:104
static int dt_pthread_rwlock_destroy(dt_pthread_rwlock_t *lock)
Definition dtpthread.h:212
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:132
static int dt_pthread_rwlock_init(dt_pthread_rwlock_t *lock, const pthread_rwlockattr_t *attr)
Definition dtpthread.h:192
hash
Definition layers.h:90
static const dt_iop_drawlayer_params_t * params
Definition layers.h:80
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
int main()
Definition prova.c:47
const float uint32_t state[4]
unsigned __int64 uint64_t
Definition strptime.c:75
dt_dev_pixelpipe_t pipe
struct dt_iop_module_t *gboolean enabled
Definition dev_history.h:52
A reference-sharing snapshot of dev->history, for readers that outlive a lock.
int32_t history_end
Definition develop.h:256
GList * history
Definition develop.h:259
struct dt_develop_t * dev
Definition imageop.h:311
static void _history_end_is_clamped_to_the_list_at_snapshot_time(void **state)
static void _cow_touch_leaves_an_exclusively_owned_item_alone(void **state)
static int _setup(void **state)
static int _teardown(void **state)
static void _a_snapshot_references_every_item_and_releases_them(void **state)
static void _transient_params_serial_advances_with_every_publication(void **state)
A transient publication must be distinguishable from the one before it.
static void _cow_touch_ignores_a_marker_naming_a_different_item(void **state)
static void _cow_touch_repoints_the_pipe_marker_and_balances_every_reference(void **state)
static void _cow_touch_clones_an_item_a_snapshot_is_holding(void **state)
static dt_dev_history_item_t * _append(dt_develop_t *dev, const uint64_t hash)
static int _refs(const dt_dev_history_item_t *item)