Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
pixelpipe_cache.c
Go to the documentation of this file.
1/*
2 This file is part of darktable,
3 Copyright (C) 2009-2012, 2015 johannes hanika.
4 Copyright (C) 2010-2011 Henrik Andersson.
5 Copyright (C) 2011 Robert Bieber.
6 Copyright (C) 2011 Rostyslav Pidgornyi.
7 Copyright (C) 2012 Richard Wonka.
8 Copyright (C) 2012-2014, 2016 Tobias Ellinghaus.
9 Copyright (C) 2013-2014, 2016 Roman Lebedev.
10 Copyright (C) 2014 Ulrich Pegelow.
11 Copyright (C) 2019, 2023-2026 Aurélien PIERRE.
12 Copyright (C) 2019-2021 Pascal Obry.
13 Copyright (C) 2020, 2022 Hanno Schwalm.
14 Copyright (C) 2020 Ralf Brown.
15 Copyright (C) 2021 Aldric Renaudin.
16 Copyright (C) 2021 Dan Torop.
17 Copyright (C) 2022 Martin Bařinka.
18 Copyright (C) 2023 lologor.
19 Copyright (C) 2024 Alynx Zhou.
20 Copyright (C) 2025-2026 Guillaume Stutin.
21 Copyright (C) 2025 Miguel Moquillon.
22
23 darktable is free software: you can redistribute it and/or modify
24 it under the terms of the GNU General Public License as published by
25 the Free Software Foundation, either version 3 of the License, or
26 (at your option) any later version.
27
28 darktable is distributed in the hope that it will be useful,
29 but WITHOUT ANY WARRANTY; without even the implied warranty of
30 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 GNU General Public License for more details.
32
33 You should have received a copy of the GNU General Public License
34 along with darktable. If not, see <http://www.gnu.org/licenses/>.
35*/
36
37#include <inttypes.h>
38#include <glib.h>
39#include <stdlib.h>
40#include <signal.h>
41#include <string.h>
42
47#include "common/opencl.h"
48#include "pixel/format.h"
49/* For dt_iop_module_t: the cache reads `module->op` to special-case the gamma module
50 * and calls `module->name()` for its diagnostics. That is the last edge keeping this
51 * file above develop/; taking a name string instead of a module would cut it. */
52#include "develop/imageop.h"
53
54/* Set once by dt_dev_pixelpipe_cache_init(); see the header for why they are not read live. */
55static gboolean _verbose = FALSE;
56static gboolean _verbose_detail = FALSE;
57
58/* Statement-safe: a bare `if(_verbose) dt_print(...)` swallows a following `else`. */
59#define _cache_print(channel, ...) \
60 do { \
61 if(_verbose) dt_print((channel), __VA_ARGS__); \
62 } while(0)
63
64
65
66/* PRIVATE. Nothing outside this file reads a field of it -- the header exposes the
67 * type as an opaque handle so that it cannot. */
69{
70 GHashTable *entries;
71 // External (temporary) buffers keyed by address hash, separate from pipeline cache entries.
72 GHashTable *external_entries;
76 size_t max_memory;
78 // System memory-pressure probe cache, guarded by `lock` (see the pressure valve
79 // in pixelpipe_cache.c): last probed system-wide available RAM, decremented by
80 // our own allocations between two rate-limited probes. The estimate legitimately
81 // reaches 0 under pressure, so whether the platform answers at all is a separate
82 // flag rather than an `est == 0` sentinel.
86 // When the pressure valve last told the user of a refusal, per module and image, keyed by both as
87 // text: `gint64 *` monotonic times, guarded by `lock`. Entries older than the alert period go at
88 // the next refusal.
89 GHashTable *pressure_alerts;
90 /* Kernel memory pressure: how much the cache may hold while the machine stalls, and what
91 * decides it -- caches/pixelpipe_cache_pressure.c, which this file feeds through _pressure_sink()
92 * and which never touches an entry itself. Guarded by `lock`, like the fields above. */
94 dt_pthread_mutex_t lock; // mutex to protect the cache entries
97
98
99/* The cache instance, owned HERE. darktable.c used to hold it on the application struct and
100 * implement the accessor; it still decides HOW BIG the cache is and still retries smaller on
101 * failure -- that is resource policy, not a cache decision -- but the pointer lives here. */
103
105{
107}
108
109/* Installed by the orchestrator; see dt_dev_pixelpipe_cache_set_handlers(). NULL means
110 * nobody is listening, which is a working configuration, not an error. */
115
124
129
130/* For an allocation the cache refused: the module that asked fails, and its pipe publishes
131 * nothing. That stays true long after a toast is gone, so it goes to the alert handler, and to
132 * stderr when no GUI installed one, with the item on the line under the message: the warn handler
133 * is a toast there, which no view draws. */
134static void _alert_user(const char *message, const char *item)
135{
137 _alert_handler(message, item);
138 else if(!IS_NULL_PTR(item))
139 fprintf(stderr, "%s\n %s\n", message, item);
140 else
141 fprintf(stderr, "%s\n", message);
142}
143
144/* For what is fine to miss: the cache went over its budget, but the allocation went ahead. */
145static void _warn_user(const char *message)
146{
147 if(!IS_NULL_PTR(_warn_handler) && !IS_NULL_PTR(message)) _warn_handler(message);
148}
149
150/* "The pipeline cache is full while allocating ...", in the four shapes the catalogs already
151 * translate, depending on what is known of the request. Freed by the caller. */
152static gchar *_cache_full_sentence(const char *name, const char *module)
153{
154 if(!IS_NULL_PTR(name) && !IS_NULL_PTR(module))
155 return g_strdup_printf(_("The pipeline cache is full while allocating\n"
156 "`%s` (module `%s`).\n"
157 "Either your RAM settings are too frugal or your RAM is too small."),
158 name, module);
159 if(!IS_NULL_PTR(name))
160 return g_strdup_printf(_("The pipeline cache is full while allocating\n"
161 "`%s`.\n"
162 "Either your RAM settings are too frugal or your RAM is too small."),
163 name);
164 if(!IS_NULL_PTR(module))
165 return g_strdup_printf(_("The pipeline cache is full while processing module\n"
166 "`%s`.\n"
167 "Either your RAM settings are too frugal or your RAM is too small."),
168 module);
169 return g_strdup(_("The pipeline cache is full.\n"
170 "Either your RAM settings are too frugal or your RAM is too small."));
171}
172
173/* The cache refused an allocation: say what it was, then what follows from it. The buffer is a
174 * module's output or one of its working buffers, so the module fails and the pipe stops without
175 * publishing. Nothing retries in smaller tiles: whether to tile was decided before the module
176 * ran (pixelpipe_cpu.c), from the memory that looked free then. The darkroom keeps showing its
177 * last complete rendering, and only the next change runs the pipe again (develop.c). */
178static void _alert_cache_refused(const char *name, const char *module)
179{
180 // What was being allocated -- the cacheline's name, the module that asked, or both -- is the item:
181 // the message stays the same whatever was refused, so a run of refusals shares one window.
182 gchar *item = NULL;
183 if(!IS_NULL_PTR(name) && !IS_NULL_PTR(module))
184 item = g_strdup_printf(_("`%s` (module `%s`)"), name, module);
185 else if(!IS_NULL_PTR(module))
186 item = g_strdup_printf(_("module `%s`"), module);
187 else
188 item = g_strdup(name);
189 gchar *message = g_strdup_printf("%s\n%s",
190 _("The pipeline cache is full.\n"
191 "Either your RAM settings are too frugal or your RAM is too small."),
192 _("The module fails and the image is not updated: what is shown stays "
193 "the last complete rendering, until the next change tries again."));
194 _alert_user(message, item);
195 dt_free(message);
196 dt_free(item);
197}
198
199static inline gboolean _observed(void)
200{
201 return _observer && _observer->active && _observer->active();
202}
203
204/* Each member is checked at its own call: `active` returning TRUE says the supervisor is
205 * watching, not that it implements every hook. */
206static inline void _observe_read(uint64_t hash, size_t size)
207{
209}
210static inline void _observe_delete(uint64_t hash, size_t size, int owner_pipe_id, const char *name)
211{
213}
214static inline void _observe_rekey(uint64_t old_hash, uint64_t new_hash)
215{
216 if(_observed() && _observer->rekey) _observer->rekey(old_hash, new_hash);
217}
218
219
220static __thread const char *dt_pixelpipe_cache_current_module = NULL;
221// The image that module runs on, set alongside it, as text its caller wrote: what the user needs to
222// find in a refusal's message. The cache only prints it, and knows nothing of images.
223static __thread const char *dt_pixelpipe_cache_current_image = NULL;
224
225/* Allocations refused to this thread for lack of memory: by _free_space_to_alloc() when the
226 * budget is spent and every line is in use, and by _log_arena_allocation_failure() when the
227 * arena has no run long enough or the system is out of RAM. Only ever counted up, so two readings
228 * tell a caller whether anything was refused in between, whoever else reads it. */
229static __thread uint32_t dt_pixelpipe_cache_alloc_refusals = 0;
230
232 const uint64_t key);
233
234static inline const char *_cache_debug_module_name(void)
235{
237}
238
239static void _trace_exact_hit(const char *phase, const uint64_t hash, dt_pixel_cache_entry_t *cache_entry,
240 void *data, void *cl_mem_output, const int preferred_devid, const gboolean verbose)
241{
242 if(!_verbose) return;
243 if(verbose && !_verbose_detail) return;
244
246 "[pixelpipe_cache] exact-hit %s req=%" PRIu64 " entry=%" PRIu64 "/%" PRIu64
247 " data=%p cl=%p refs=%i auto=%i dev=%i module=%s name=%s\n",
248 phase, hash, cache_entry ? cache_entry->hash : DT_PIXELPIPE_CACHE_HASH_INVALID,
249 cache_entry ? cache_entry->serial : 0, data, cl_mem_output,
250 cache_entry ? dt_atomic_get_int(&cache_entry->refcount) : -1,
251 cache_entry ? cache_entry->auto_destroy : -1, preferred_devid, _cache_debug_module_name(),
252 (cache_entry && cache_entry->name) ? cache_entry->name : "-");
253}
254
255const char *dt_pixelpipe_cache_set_current_module(const char *module)
256{
257 const char *previous = dt_pixelpipe_cache_current_module;
259 return previous;
260}
261
262const char *dt_pixelpipe_cache_set_current_image(const char *image)
263{
264 const char *previous = dt_pixelpipe_cache_current_image;
266 return previous;
267}
268
273
274typedef struct dt_cache_clmem_t
275{
276 void *host_ptr;
277 void *mem;
278 int refs;
280
289
290
292 dt_pixel_cache_entry_t *cache_entry);
293static void _free_cache_entry(dt_pixel_cache_entry_t *cache_entry);
294static void _pixelpipe_cache_finalize_entry(dt_pixel_cache_entry_t *cache_entry, void **data,
295 const char *message);
296int _non_thread_safe_cache_remove(dt_dev_pixelpipe_cache_t *cache, const gboolean force,
297 dt_pixel_cache_entry_t *cache_entry, GHashTable *table);
298
300 const uint64_t hash, const size_t size,
301 const char *name, const int id);
303 const char *name, const int id,
304 dt_dev_pixelpipe_cache_t *cache, gboolean alloc,
305 GHashTable *table);
306static gboolean _cache_entry_clmem_flush_device(dt_pixel_cache_entry_t *entry, const int devid);
307static gboolean _cache_entry_materialize_host_data_locked(dt_pixel_cache_entry_t *entry, int preferred_devid,
308 gboolean prefer_device_payload);
311
312#ifdef HAVE_OPENCL
313static gboolean _cache_entry_clmem_flush_host_pinned_locked(dt_pixel_cache_entry_t *entry, void *host_ptr, int devid);
314#endif
315
316/* Single writer of `cache_entry->age`, and the only way an entry's MRU timestamp moves. EVERY
317 * path that hands an entry over to a consumer must call this -- the intra-run producer->consumer
318 * handoff included, since that is still a use as far as eviction is concerned. An entry whose age
319 * is only ever set at creation is aged by the eviction sweeps as if it had never been read, so the
320 * cacheline the pipeline consumes on every frame becomes the first LRU victim.
321 *
322 * This deliberately does NOT touch `hits`. `age` answers "when was this last needed" and every
323 * use, reuse or passthrough, answers it. `hits` answers "has this ever been reused ASYNCHRONOUSLY"
324 * and passthrough does not: a module publishing its output with one reference reserved for the
325 * next module, which reopens it as its input and releases it, is the normal pipeline flow, not a
326 * cache win. Only the lookups that also count a `cache->queries`/`cache->hits` pair may bump it. */
327static inline void _pixel_cache_touch(dt_pixel_cache_entry_t *cache_entry)
328{
329 if(IS_NULL_PTR(cache_entry)) return;
330 dt_atomic_set_uint64(&cache_entry->age, (uint64_t)g_get_monotonic_time());
331}
332
333static inline int64_t _pixel_cache_get_age(dt_pixel_cache_entry_t *cache_entry)
334{
335 return (int64_t)dt_atomic_get_uint64(&cache_entry->age);
336}
337
338
340{
341 if(IS_NULL_PTR(cache) || IS_NULL_PTR(host_ptr)) return NULL;
342
343 const uint64_t hash = (uint64_t)(uintptr_t)host_ptr;
345 if(entry && entry->external_alloc && entry->data == host_ptr) return entry;
346 return NULL;
347}
348
350 const uint64_t key)
351{
352 dt_pixel_cache_entry_t *entry = (dt_pixel_cache_entry_t *)g_hash_table_lookup(table, &key);
353 return entry;
354}
355
356
358{
360 if(hash == DT_PIXELPIPE_CACHE_HASH_INVALID) return NULL;
363 /* Refresh recency only. `develop/pixelpipe_hb.c` reopens each module's INPUT cacheline through
364 * here on every recursion step: that is the producer->consumer handoff of one pipeline run, a
365 * use for eviction purposes but not an asynchronous reuse, so neither `hits` counter moves. */
366 _pixel_cache_touch(entry);
368 return entry;
369}
370
371
373{
375 if(!cache || !data) return NULL;
376
378
379 GHashTableIter iter;
380 gpointer key, value;
381
382 /* Search regular entries table */
383 g_hash_table_iter_init(&iter, cache->entries);
384 while(g_hash_table_iter_next(&iter, &key, &value))
385 {
387 if(entry && entry->data == data)
388 {
389 _pixel_cache_touch(entry);
391 return entry;
392 }
393 }
394
395 /* Search external entries table */
396 g_hash_table_iter_init(&iter, cache->external_entries);
397 while(g_hash_table_iter_next(&iter, &key, &value))
398 {
400 if(entry && entry->data == data)
401 {
402 _pixel_cache_touch(entry);
404 return entry;
405 }
406 }
407
409 return NULL;
410}
411
412
414{
415 return cache_entry->size / (1024 * 1024);
416}
417
418
419static void _pixel_cache_message(dt_pixel_cache_entry_t *cache_entry, const char *message, gboolean verbose)
420{
421 if(!_verbose) return;
422 if(verbose && !_verbose_detail) return;
424 "[pixelpipe] cache entry %" PRIu64 "/%" PRIu64 ": %s (data=%p - %" G_GSIZE_FORMAT " MiB - age %" PRId64
425 " - hits %i - refs %i - auto %i - ext %i - id %i - stale %i - module %s) %s\n",
426 cache_entry->hash, cache_entry->serial,
427 cache_entry->name ? cache_entry->name : "-", cache_entry->data,
428 _pixel_cache_get_size(cache_entry), _pixel_cache_get_age(cache_entry), cache_entry->hits,
429 dt_atomic_get_int(&cache_entry->refcount), cache_entry->auto_destroy,
430 cache_entry->external_alloc, cache_entry->id, dt_atomic_get_int(&cache_entry->host_stale),
431 _cache_debug_module_name(), message);
432}
433
434static void _pixelpipe_cache_finalize_entry(dt_pixel_cache_entry_t *cache_entry, void **data,
435 const char *message)
436{
437 _pixel_cache_touch(cache_entry);
438 if(data)
439 {
440 void *pixels = dt_pixel_cache_entry_get_data(cache_entry);
441 *data = !IS_NULL_PTR(pixels) ? __builtin_assume_aligned(pixels, DT_CACHELINE_BYTES) : NULL;
442 }
443 _pixel_cache_message(cache_entry, message, FALSE);
444}
445
447 void **data,
449{
451 if(!IS_NULL_PTR(data)) *data = NULL;
452 if(!IS_NULL_PTR(entry)) *entry = NULL;
454
456 cache->queries++;
457
459 if(!IS_NULL_PTR(cache_entry) && !cache_entry->auto_destroy)
460 {
461 cache->hits++;
462 cache_entry->hits++;
463 _non_thread_safe_cache_ref_count_entry(cache, TRUE, cache_entry);
464 _pixelpipe_cache_finalize_entry(cache_entry, data, "ref-by-hash");
465 if(!IS_NULL_PTR(entry)) *entry = cache_entry;
466 }
467
468 const gboolean found = !IS_NULL_PTR(cache_entry) && !cache_entry->auto_destroy;
469 const size_t found_size = found ? cache_entry->size : 0;
471
472 if(found ) _observe_read(hash, found_size);
473
474 return found;
475}
476
477
479 void **data,
481{
483 if(!IS_NULL_PTR(data)) *data = NULL;
484 if(!IS_NULL_PTR(entry)) *entry = NULL;
486
487 gboolean found = FALSE;
488 size_t found_size = 0;
489
491 cache->queries++;
492
494 if(!IS_NULL_PTR(cache_entry) && !cache_entry->auto_destroy
496 {
497 /* The same rejection `dt_dev_pixelpipe_cache_peek()` applies, for the same reason: a reusable
498 * output cacheline is rekeyed to its new hash BEFORE the recompute that fills it starts, so an
499 * entry somebody currently holds the write lock on carries stale or half-written pixels under a
500 * key that already claims to describe the new ones. Never WAIT on that lock -- report a miss and
501 * let the caller queue for the publication instead. */
502 if(dt_pthread_rwlock_tryrdlock(&cache_entry->lock) == 0)
503 {
504 dt_pthread_rwlock_unlock(&cache_entry->lock);
505 cache->hits++;
506 cache_entry->hits++;
507 _non_thread_safe_cache_ref_count_entry(cache, TRUE, cache_entry);
508 _pixelpipe_cache_finalize_entry(cache_entry, data, "ref-host-by-hash");
509 if(!IS_NULL_PTR(entry)) *entry = cache_entry;
510 found = TRUE;
511 found_size = cache_entry->size;
512 }
513 }
514
516
517 if(found) _observe_read(hash, found_size);
518
519 return found;
520}
521
522// remove the cache entry with the given hash and update the cache memory usage
523// WARNING: not internally thread-safe, protect its calls with mutex lock
524// return 0 on success, 1 on error
526 dt_pixel_cache_entry_t *cache_entry, GHashTable *table)
527{
528 if(!IS_NULL_PTR(cache_entry))
529 {
530 // Returns 1 if the lock is captured by another thread
531 // 0 if WE capture the lock, and then need to release it
532 gboolean locked = dt_pthread_rwlock_trywrlock(&cache_entry->lock);
533 if(!locked) dt_pthread_rwlock_unlock(&cache_entry->lock);
534 gboolean used = dt_atomic_get_int(&cache_entry->refcount) > 0;
535
536 /* Force-removal may bypass caller lifecycle checks but must never destroy
537 * an entry that still has active readers/writers. Active users can still
538 * access cl_mem_list after this call (for example borrowed GPU payloads),
539 * so removing a referenced entry here would create dangling pointers. */
540 if(!used && (!locked || force))
541 {
542 // Note: the free callback takes care of flushing OpenCL buffers too
543 g_hash_table_remove(table, &cache_entry->hash);
544 return 0;
545 }
546 else if(used)
547 _pixel_cache_message(cache_entry, "cannot remove: used", TRUE);
548 else if(locked)
549 _pixel_cache_message(cache_entry, "cannot remove: locked", TRUE);
550 }
551 else
552 {
553 _cache_print(DT_DEBUG_PIPECACHE, "[pixelpipe] cache entry not found, will not be removed\n");
554 }
555 return 1;
556}
557
558#ifdef HAVE_OPENCL
559static gboolean _cache_entry_materialize_host_data_locked(dt_pixel_cache_entry_t *entry, int preferred_devid,
560 gboolean prefer_device_payload)
561{
562 dt_cache_clmem_t *source = NULL;
563 gboolean ok = FALSE;
565
566 /* We materialize RAM from the most authoritative cached payload in one pass instead of
567 * walking the list multiple times with slightly different predicates:
568 * - when RAM existed before, prefer pinned host-backed payloads first because they should
569 * already alias the cacheline or be the cheapest path back to host,
570 * - when RAM has just been allocated for a GPU-only cacheline, prefer device payloads first,
571 * - if a preferred OpenCL device is known, rank payloads from that device ahead of the rest.
572 * This keeps the fallback order explicit without scattering it over six loops. */
574 for(GList *l = g_list_first(entry->cl_mem_list); l; l = g_list_next(l))
575 {
576 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
577 if(IS_NULL_PTR(c) || IS_NULL_PTR(c->mem)) continue;
578
579 /* We are looking for one authoritative cached payload to materialize back to RAM.
580 * Only consider records whose live OpenCL context still matches the recorded device.
581 * Other cached payloads may belong to a different pipeline/device and must stay untouched. */
582 const int mem_devid = dt_opencl_get_mem_context_id((cl_mem)c->mem);
583 if(mem_devid != preferred_devid) continue;
584
585 const gboolean host_backed = (c->host_ptr == entry->data);
586 const gboolean device_only = (IS_NULL_PTR(c->host_ptr));
587 if(!host_backed && !device_only) continue;
588
590 if(!prefer_device_payload)
591 {
592 if(host_backed && (preferred_devid < 0 || mem_devid == preferred_devid))
594 else if(device_only && preferred_devid >= 0 && mem_devid == preferred_devid)
596 else if(device_only)
598 else if(host_backed)
600 }
601 else
602 {
603 if(device_only && preferred_devid >= 0 && mem_devid == preferred_devid)
605 else if(device_only)
607 else if(host_backed && (preferred_devid < 0 || mem_devid == preferred_devid))
609 else if(host_backed)
611 }
612
613 if(rank > best_rank)
614 {
615 best_rank = rank;
616 source = c;
618 }
619 }
620
621 if(source)
622 {
623
624 const int devid = dt_opencl_get_mem_context_id(source->mem);
625 const int width = dt_opencl_get_image_width(source->mem);
626 const int height = dt_opencl_get_image_height(source->mem);
627 const int bpp = dt_opencl_get_image_element_size(source->mem);
628
629 if(dt_opencl_is_pinned_memory((cl_mem)source->mem) && source->host_ptr == entry->data)
630 {
631 void *mapped = dt_opencl_map_image(devid, (cl_mem)source->mem, TRUE, CL_MAP_READ,
632 width, height, bpp);
633 ok = (dt_opencl_unmap_mem_object(devid, (cl_mem)source->mem, mapped) == CL_SUCCESS);
634 }
635 if(!ok)
636 {
637 ok = (dt_opencl_read_host_from_device(devid, entry->data, source->mem,
638 width, height, bpp) == CL_SUCCESS);
639 }
640 }
641
642 // The payload copied back is the one published under the entry's current hash.
643 if(ok) dt_atomic_set_int(&entry->host_stale, FALSE);
644
646 return ok;
647}
648#else
649static gboolean _cache_entry_materialize_host_data_locked(dt_pixel_cache_entry_t *entry, int preferred_devid,
650 gboolean prefer_device_payload)
651{
652 (void)preferred_devid;
653 (void)prefer_device_payload;
654 return entry && !IS_NULL_PTR(entry->data);
655}
656#endif
657
658static gboolean _cache_entry_materialize_host_data(dt_dev_pixelpipe_cache_t *cache, int preferred_devid,
660{
661 if(IS_NULL_PTR(cache) || IS_NULL_PTR(entry)) return FALSE;
662 if(preferred_devid < 0 && dt_pixel_cache_entry_get_data(entry) == NULL) return FALSE;
663
665 gboolean use_host_ptr = TRUE;
667 {
669 use_host_ptr = FALSE;
670 }
671 const gboolean ok = _cache_entry_materialize_host_data_locked(entry, preferred_devid, use_host_ptr);
673
674 return ok;
675}
676
677#ifdef HAVE_OPENCL
678static gboolean _cache_entry_clmem_has_host_pinned_locked(dt_pixel_cache_entry_t *entry, void *host_ptr, int devid)
679{
680 if(IS_NULL_PTR(entry) || IS_NULL_PTR(host_ptr)) return FALSE;
681
682 gboolean found = FALSE;
684 for(GList *l = g_list_first(entry->cl_mem_list); l; l = g_list_next(l))
685 {
686 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
687 if(IS_NULL_PTR(c) || IS_NULL_PTR(c->mem)) continue;
688
689 if(c->refs == 0 && devid == dt_opencl_get_mem_context_id((cl_mem)c->mem))
690 {
691 found = TRUE;
692 break;
693 }
694 }
696
697 return found;
698}
699
700static gboolean _cache_entry_clmem_flush_host_pinned_locked(dt_pixel_cache_entry_t *entry, void *host_ptr, int devid)
701{
702 // If host_ptr is NULL, we don't have RAM cache for this buffer,
703 // so we can't flush the vRAM cache or we would loose it forever.
704 if(IS_NULL_PTR(entry) || IS_NULL_PTR(host_ptr)) return FALSE;
705
706 gboolean flushed = FALSE;
707
709 for(GList *l = g_list_first(entry->cl_mem_list); l;)
710 {
711 GList *next = g_list_next(l);
712 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
713 if(IS_NULL_PTR(c->mem))
714 {
715 // Current cacheline holds an empty buffer, no point keeping it
716 entry->cl_mem_list = g_list_delete_link(entry->cl_mem_list, l);
717 dt_free(c);
718 l = next;
719 continue;
720 }
721 if(dt_opencl_get_mem_context_id(c->mem) != devid)
722 {
723 // Current cacheline doesn't belong to current OpenCL devide: don't touch it
724 l = next;
725 continue;
726 }
727
728 entry->cl_mem_list = g_list_delete_link(entry->cl_mem_list, l);
730 dt_free(c);
731 flushed = TRUE;
732 l = next;
733 }
734
736
737 return flushed;
738}
739#endif
740
742{
744 // devid < 0 means the calling pipe never used OpenCL: it owns no device-side
745 // payload in the cache, so there is nothing of its own to release here.
746 //
747 // This used to also support devid == -1 as "drop cl_mem from every device,
748 // regardless of who else is using it", called from per-pipe cleanup. That ran
749 // without holding any dev[].lock, so it could race the eventlist/cl_mem
750 // bookkeeping of whichever OTHER pixelpipe was concurrently running on that
751 // device, corrupting it and crashing inside clGetEventInfo/clWaitForEvents
752 // (see #859 and #864). A global, all-devices teardown is never needed during
753 // normal operation: dt_cleanup() already finishes every device and
754 // dt_dev_pixelpipe_cache_cleanup() unconditionally releases all remaining
755 // cl_mem objects at application exit, when nothing else is running.
756 if(devid < 0) return;
757
758 // NOTE: the caller must hold the device lock (dt_opencl_reserve_device_by_id()) -- either
759 // because it IS the pixelpipe currently running on that device (the lock
760 // dt_opencl_reserve_device_for_pipe() handed it for the duration of its run), or because
761 // it explicitly took that lock to safely flush this device's cache entries
762 // after its own run finished (see dt_dev_pixelpipe_cache_flush_clmem_for_pipe()).
764
766 GHashTableIter iter;
767 gpointer key, value;
768 g_hash_table_iter_init(&iter, cache->entries);
769 while(g_hash_table_iter_next(&iter, &key, &value))
770 {
772
773 /* Only idle cachelines may have their vRAM reclaimed. An entry that is referenced or
774 * write-locked is somebody's live (or about-to-be-consumed) buffer: the recursion reserves
775 * an entry-level ref for the next consumer before that consumer borrows the cl_mem payload,
776 * so a payload can be unborrowed (per-payload refs == 0) yet still belong to an in-flight
777 * pipe. Honoring the same protection the LRU/removal paths use (refcount + non-blocking
778 * write-lock probe) keeps us from yanking the sole vRAM copy of another pipe's input out
779 * from under it -- which left a husk and produced skull thumbnails (issue #817). The
780 * trywrlock never waits, so this stays lightweight and cannot deadlock against renders that
781 * already hold entry locks. */
782 const gboolean used = dt_atomic_get_int(&entry->refcount) > 0;
783 gboolean locked = dt_pthread_rwlock_trywrlock(&entry->lock);
784 if(!locked) dt_pthread_rwlock_unlock(&entry->lock);
785 if(used || locked)
786 {
789 "[dt_dev_pixelpipe_cache_flush_clmem] entry %" PRIu64 " is in use (refcount=%i locked=%i), "
790 "keeping its vRAM\n", entry->hash, dt_atomic_get_int(&entry->refcount), locked);
791 continue;
792 }
793
796 "[dt_dev_pixelpipe_cache_flush_clmem] trying to flush vRAM for entry %" PRIu64 " on device %d...\n",
797 entry->hash, devid);
798
799 /* If reclaiming this device's vRAM leaves the entry with no buffer at all, delete it now
800 * instead of letting a payload-less husk persist as a cache hit. We hold cache->lock for the
801 * whole iteration, and lookups bump the consumer ref under that same lock, so no consumer can
802 * be mid-acquisition of this (refcount == 0) entry. iter_remove runs _free_cache_entry, which
803 * releases any remaining resources. */
804 if(_cache_entry_clmem_flush_device(entry, devid))
805 g_hash_table_iter_remove(&iter);
806 }
808}
809
810#ifdef HAVE_OPENCL
812{
813 // Like dt_dev_pixelpipe_cache_flush_clmem(), but for callers that do NOT
814 // currently hold the device lock (dt_opencl_reserve_device_by_id()) -- typically a pipe's own
815 // cleanup, running after dt_dev_pixelpipe_process() already released that
816 // lock. Taking it here ensures we can't race the eventlist/cl_mem bookkeeping
817 // of whichever OTHER pixelpipe is now running on that device.
818 if(devid < 0 || !dt_opencl_is_inited()) return;
819
823}
824#else
826{
827 (void)devid;
828}
829#endif
830
831typedef struct _cache_lru_t
832{
833 int64_t max_age;
836 gboolean host_only; // only lines with a host buffer: the others give no RAM back
838
839
840/* What eviction may take: an entry nobody holds and nobody is writing. The LRU and the room told
841 * to tile planners both ask this, so planners are never promised memory eviction cannot give back.
842 * WARNING: non thread-safe, the caller holds the cache lock. */
843static gboolean _entry_evictable(dt_pixel_cache_entry_t *cache_entry, gboolean *used)
844{
845 *used = dt_atomic_get_int(&cache_entry->refcount) > 0;
846 // Nobody is writing it if its write lock can be taken: taken, it goes straight back.
847 if(dt_pthread_rwlock_trywrlock(&cache_entry->lock)) return FALSE;
848 dt_pthread_rwlock_unlock(&cache_entry->lock);
849 return !*used;
850}
851
852// find the cache entry hash with the oldest use
853static void _cache_get_oldest(gpointer key, gpointer value, gpointer user_data)
854{
856 _cache_lru_t *lru = (_cache_lru_t *)user_data;
857 if(lru->host_only && IS_NULL_PTR(cache_entry->data)) return;
858
859 // Don't remove LRU entries that are still in use.
860 // `refcount > 0` is the only thing standing between a holder and this eviction, so a release
861 // nobody took is not harmless: it lets an entry that is still held look free, the entry is
862 // freed under its holder, and the holder's own release -- by pointer, for every long-lived
863 // reference -- then writes into freed memory. Release only what you retained: a
864 // dt_dev_pixelpipe_cache_peek() retains nothing.
865 const int64_t age = _pixel_cache_get_age(cache_entry);
866 if(age < lru->max_age)
867 {
868 gboolean used = FALSE;
869 if(_entry_evictable(cache_entry, &used))
870 {
871 lru->max_age = age;
872 lru->hash = cache_entry->hash;
873 lru->cache_entry = cache_entry;
874 _pixel_cache_message(cache_entry, "candidate for deletion", TRUE);
875 }
876 else
877 _pixel_cache_message(cache_entry, used ? "cannot be deleted: used" : "cannot be deleted: locked", TRUE);
878 }
879}
880
881/* The bytes eviction can take back: [0] against the budget, [1] of host RAM -- a line kept on a
882 * device only has no host buffer, so evicting it gives the system nothing. */
883static void _cache_sum_evictable(__attribute__((unused)) gpointer key, gpointer value, gpointer user_data)
884{
886 size_t *evictable = (size_t *)user_data;
887 gboolean used = FALSE;
888 if(!_entry_evictable(cache_entry, &used)) return;
889
890 evictable[0] += cache_entry->size;
891 if(!IS_NULL_PTR(cache_entry->data)) evictable[1] += cache_entry->size;
892}
893
894static void _print_cache_lines(gpointer key, gpointer value, gpointer user_data)
895{
897 _pixel_cache_message(cache_entry, "", FALSE);
898}
899
900
901// remove the least used cache entry -- with `host_only`, the least used one holding host RAM
902// return 0 on success, 1 on error
903// error is : we couldn't find a candidate for deletion because all entries are either locked or in use
904// or we found one but failed to remove it.
906{
907 _cache_lru_t *lru = (_cache_lru_t *)malloc(sizeof(_cache_lru_t));
908 lru->max_age = g_get_monotonic_time();
909 lru->hash = 0;
910 lru->cache_entry = NULL;
911 lru->host_only = host_only;
912 int error = 1;
913 g_hash_table_foreach(cache->entries, _cache_get_oldest, lru);
914
915 if(lru->hash > 0)
916 {
918 if(error)
919 _cache_print(DT_DEBUG_PIPECACHE, "[pixelpipe] couldn't remove LRU %" PRIu64 "\n", lru->hash);
920 else
921 _cache_print(DT_DEBUG_PIPECACHE, "[pixelpipe] LRU %" PRIu64 " removed. Total cache size: %" G_GSIZE_FORMAT " MiB\n",
922 lru->hash, cache->current_memory / (1024 * 1024));
923 }
924 else
925 {
926 _cache_print(DT_DEBUG_PIPECACHE, "[pixelpipe] couldn't remove LRU, %i items and all are used\n", g_hash_table_size(cache->entries));
927 g_hash_table_foreach(cache->entries, _print_cache_lines, NULL);
928 }
929
930 dt_free(lru);
931 return error;
932}
933
934// return 0 on success 1 on error
943
944#ifdef HAVE_OPENCL
945static void *_pixel_cache_clmem_get(dt_pixel_cache_entry_t *entry, void *host_ptr, int devid,
946 int width, int height, int bpp, int flags)
947{
949
952 "[_pixel_cache_clmem_get] %u output entries in %" PRIu64 "\n",
953 g_list_length(entry->cl_mem_list), entry->hash);
954
955 for(GList *l = g_list_first(entry->cl_mem_list); l;)
956 {
957 GList *next = g_list_next(l);
958 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
959 if(IS_NULL_PTR(c->mem))
960 {
961 // No point in keeping buffer-less cachelines
962 entry->cl_mem_list = g_list_delete_link(entry->cl_mem_list, l);
963 dt_free(c);
964 l = next;
965 continue;
966 }
967
968 // Buffer reuse must stay on the same OpenCL device and ensure proper size
969 if(dt_opencl_get_mem_context_id(c->mem) == devid
970 && dt_opencl_get_image_width(c->mem) == width
973 && c->refs == 0)
974 {
975 // Destroy the current OpenCL cacheline and return the buffer, the cacheline will be recreated
976 // when we are done consuming the buffer
977 entry->cl_mem_list = g_list_delete_link(entry->cl_mem_list, l);
978 void *mem = c->mem;
979 dt_free(c);
980 // Reusing the entry's vRAM payload is a use of the entry, host side read or not.
981 _pixel_cache_touch(entry);
983 return mem;
984 }
985
986 l = next;
987 }
989
990 return NULL;
991}
992#endif
993
995 int width, int height, int bpp)
996{
997#ifdef HAVE_OPENCL
998
1000
1001 for(GList *l = g_list_first(entry->cl_mem_list); l; l = g_list_next(l))
1002 {
1003 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
1004 if(dt_opencl_get_mem_context_id(c->mem) == devid
1005 && dt_opencl_get_image_width(c->mem) == width
1008 {
1009 c->refs++;
1010 void *mem = c->mem;
1011 _pixel_cache_touch(entry);
1013 return mem;
1014 }
1015 }
1017
1018#endif
1019
1020 return NULL;
1021}
1022
1024{
1025#ifdef HAVE_OPENCL
1026
1027 if(IS_NULL_PTR(entry) || IS_NULL_PTR(mem)) return;
1028
1030 for(GList *l = entry->cl_mem_list; l; l = g_list_next(l))
1031 {
1032 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
1033 if(c && c->mem == mem)
1034 {
1035 if(c->refs > 0) c->refs--;
1036 break;
1037 }
1038 }
1040
1041#else
1042 (void)entry;
1043 (void)mem;
1044#endif
1045}
1046
1047#ifdef HAVE_OPENCL
1057static int _pixel_cache_clmem_put(dt_pixel_cache_entry_t *entry, void *host_ptr, void *mem)
1058{
1059 cl_mem clmem = (cl_mem)mem;
1060 const int devid = dt_opencl_get_mem_context_id(clmem);
1061
1063 for(GList *l = g_list_first(entry->cl_mem_list); l; l = g_list_next(l))
1064 {
1065 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
1066 if(c->mem == mem)
1067 {
1069 return 3;
1070 }
1071 if(dt_opencl_get_mem_context_id(c->mem) == devid)
1072 {
1073 // We keep one GPU cacheline per GPU device per pipeline cache entry
1074 // If refs > 0 here, we have a problem earlier.
1075 if(c->refs > 0) continue;
1076
1077 void *old = c->mem;
1078 c->mem = mem;
1079 c->host_ptr = host_ptr;
1082 return 2;
1083 }
1084 }
1085
1086 dt_cache_clmem_t *c = (dt_cache_clmem_t *)g_malloc0(sizeof(*c));
1087 if(IS_NULL_PTR(c))
1088 {
1091 return 0;
1092 }
1093
1094 c->host_ptr = host_ptr;
1095 c->mem = mem;
1096 entry->cl_mem_list = g_list_prepend(entry->cl_mem_list, c);
1098 return 1;
1099}
1100
1102{
1103 if(IS_NULL_PTR(entry) || IS_NULL_PTR(mem)) return;
1104
1106 for(GList *l = entry->cl_mem_list; l;)
1107 {
1108 GList *next = g_list_next(l);
1109 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
1110 if(c && c->mem == mem)
1111 {
1112 entry->cl_mem_list = g_list_delete_link(entry->cl_mem_list, l);
1113 dt_free(c);
1114 }
1115 l = next;
1116 }
1118}
1119#endif
1120
1122{
1124 for(GList *l = entry->cl_mem_list; l;)
1125 {
1126 GList *next = g_list_next(l);
1127 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
1128 if(c->refs > 0)
1129 {
1130 l = next;
1131 continue;
1132 }
1133
1134 entry->cl_mem_list = g_list_delete_link(entry->cl_mem_list, l);
1136 dt_free(c);
1137 l = next;
1138 }
1140}
1141
1142#ifdef HAVE_OPENCL
1144 dt_pixel_cache_entry_t *entry_hint, int devid,
1145 int width, int height, int bpp, int flags,
1146 gboolean *out_reused)
1147{
1149 if(!IS_NULL_PTR(out_reused)) *out_reused = FALSE;
1150 if(devid < 0 || width <= 0 || height <= 0 || bpp <= 0) return NULL;
1151
1152 // Pinning is enabled if the calling function requests it and if it is allowed by user for this device
1153 gboolean use_pinned = dt_opencl_use_pinned_memory(devid) && (flags & CL_MEM_USE_HOST_PTR);
1154
1155 // If no pinning, remove the allocation flag now because pinning happens at vRAM alloc time
1156 if(!use_pinned) flags &= ~CL_MEM_USE_HOST_PTR;
1157
1158 // Reuse the entry hint if available, else find the cache entry attached to the host_ptr
1159 dt_pixel_cache_entry_t *entry = entry_hint;
1160 if(IS_NULL_PTR(entry))
1161 {
1162 dt_pthread_mutex_lock(&cache->lock);
1163 entry = _cache_entry_for_host_ptr_locked(cache, host_ptr);
1165 }
1166
1167 // Reuse the vRAM buffer attached to the cache entry if any
1168 void *mem = NULL;
1169 if(entry)
1170 {
1171 mem = _pixel_cache_clmem_get(entry, host_ptr, devid, width, height, bpp, flags);
1172 if(!IS_NULL_PTR(mem) && !IS_NULL_PTR(out_reused)) *out_reused = TRUE;
1173 }
1174
1175 // If no vRAM buffer was found, allocate a new one, pinning the host_ptr memory if the option is enabled
1176 if(IS_NULL_PTR(mem))
1177 {
1178 mem = dt_opencl_alloc_device_use_host_pointer(devid, width, height, bpp, use_pinned ? host_ptr : NULL, flags);
1179 if(IS_NULL_PTR(mem)) return NULL;
1180 }
1181
1182 gboolean synced = FALSE;
1183
1184 // Synchronize host_ptr with mem
1186 {
1187 // Zero-copy for pinned buffers : note that some drivers may still use non-zero-copy,
1188 // in which case that degrades to basic memory copy.
1189 void *mapped = dt_opencl_map_image(devid, mem, TRUE, CL_MAP_WRITE, width, height, bpp);
1190 synced = (dt_opencl_unmap_mem_object(devid, mem, mapped) == CL_SUCCESS);
1191 }
1192
1193 if(!synced)
1194 {
1195 // Zero-copy failed or pinned memory is disabled for this device : use plain memory transfer
1196 if(dt_opencl_write_host_to_device(devid, host_ptr, mem, width, height, bpp) != CL_SUCCESS)
1197 {
1198 // Clean everything up on error and abort
1199 if(entry) _pixel_cache_clmem_remove(entry, mem);
1201 _cache_print(DT_DEBUG_OPENCL, "[dt_dev_pixelpipe_cache_get_pinned_image] failed to synchronize\n");
1202 return NULL;
1203 }
1204 else
1205 {
1206 _cache_print(DT_DEBUG_OPENCL, "[dt_dev_pixelpipe_cache_get_pinned_image] synchronized with write_host_to_device\n");
1207 }
1208 }
1209 else
1210 {
1211 _cache_print(DT_DEBUG_OPENCL, "[dt_dev_pixelpipe_cache_get_pinned_image] synchronized with mapping/unmapping\n");
1212 }
1213
1214 return mem;
1215}
1216
1218 dt_pixel_cache_entry_t *entry_hint, void **mem)
1219{
1220 if(IS_NULL_PTR(mem) || IS_NULL_PTR(*mem) || IS_NULL_PTR(host_ptr)) return;
1221 dt_pixel_cache_entry_t *entry = entry_hint;
1222 if(IS_NULL_PTR(entry))
1223 {
1225 _cache_print(DT_DEBUG_OPENCL, "[dt_dev_pixelpipe_cache_put_pinned_image] no cache entry to put the vRAM buffer\n");
1226 return;
1227 }
1228
1229 // FIXME: is it safe to cache non-pinned vRAM buffers (aka no CL_MEM_USE_HOST_PTR in flags) ?
1230 const int state = _pixel_cache_clmem_put(entry, host_ptr, (cl_mem)*mem);
1231 *mem = NULL;
1233 _cache_print(DT_DEBUG_OPENCL, "[dt_dev_pixelpipe_cache_put_pinned_image] cache entry put the vRAM buffer (state=%i) in %p\n", state, entry);
1234}
1235
1237 dt_pixel_cache_entry_t *entry_hint, int devid)
1238{
1240 if(IS_NULL_PTR(cache) || IS_NULL_PTR(host_ptr)) return FALSE;
1241
1242 dt_pixel_cache_entry_t *entry = entry_hint;
1243 if(IS_NULL_PTR(entry))
1244 {
1245 dt_pthread_mutex_lock(&cache->lock);
1246 entry = _cache_entry_for_host_ptr_locked(cache, host_ptr);
1248 }
1249
1250 if(IS_NULL_PTR(entry)) return FALSE;
1251 if(!_cache_entry_clmem_has_host_pinned_locked(entry, host_ptr, devid)) return FALSE;
1252
1253 if(devid >= 0) dt_opencl_events_wait_for(devid);
1255 const gboolean flushed = _cache_entry_clmem_flush_host_pinned_locked(entry, host_ptr, devid);
1257 return flushed;
1258}
1259
1260#else
1261
1262void dt_dev_pixelpipe_cache_put_pinned_image(void *host_ptr,
1263 dt_pixel_cache_entry_t *entry_hint, void **mem)
1264{
1265 (void)host_ptr;
1266 (void)entry_hint;
1267 if(mem) *mem = NULL;
1268}
1269
1271 dt_pixel_cache_entry_t *entry_hint, int devid)
1272{
1273 (void)host_ptr;
1274 (void)entry_hint;
1275 (void)devid;
1276 return FALSE;
1277}
1278
1279void dt_dev_pixelpipe_cache_resync_host_pinned_image(dt_dev_pixelpipe_cache_t *cache, void *host_ptr,
1280 dt_pixel_cache_entry_t *entry_hint, int devid)
1281{
1282 (void)host_ptr;
1283 (void)entry_hint;
1284 (void)devid;
1285}
1286#endif
1287
1288#ifdef HAVE_OPENCL
1289static inline gboolean _is_gamma_rgba8_output(const dt_iop_module_t *module, const size_t bpp,
1290 const char *message)
1291{
1292 return module && message && bpp == 4 * sizeof(uint8_t) && strcmp(module->op, "gamma") == 0
1293 && strcmp(message, "output") == 0;
1294}
1295
1296void *dt_dev_pixelpipe_cache_alloc_cl_device_buffer(int devid, const dt_iop_roi_t *roi, const size_t bpp,
1297 const dt_iop_module_t *module, const char *message,
1298 void *keep)
1299{
1300 const gboolean gamma_rgba8 = _is_gamma_rgba8_output(module, bpp, message);
1301 const int cl_bpp = gamma_rgba8 ? DT_OPENCL_BPP_ENCODE_RGBA8((int)bpp) : (int)bpp;
1302 return dt_opencl_alloc_device(devid, roi->width, roi->height, cl_bpp);
1303}
1304
1305void *dt_dev_pixelpipe_cache_get_cl_buffer(int devid, void *const host_ptr, const dt_iop_roi_t *roi,
1306 const size_t bpp, dt_iop_module_t *module,
1307 const char *message, dt_pixel_cache_entry_t *cache_entry,
1308 gboolean *out_reused, void *keep)
1309{
1310 // Need to use read-write mode because of in-place color space conversions.
1311 void *cl_mem_input = NULL;
1312 gboolean reused_from_cache = FALSE;
1313 const gboolean gamma_rgba8 = _is_gamma_rgba8_output(module, bpp, message);
1314 const int cl_bpp = gamma_rgba8 ? DT_OPENCL_BPP_ENCODE_RGBA8((int)bpp) : (int)bpp;
1315 static dt_atomic_int clmem_reuse_hits;
1316 static dt_atomic_int clmem_reuse_misses;
1317
1318 if(out_reused) *out_reused = FALSE;
1319
1320 if(host_ptr && dt_opencl_use_pinned_memory(devid))
1321 {
1322 const int flags = CL_MEM_READ_WRITE | CL_MEM_USE_HOST_PTR;
1323
1324 // Try to reuse existing buffer
1325 if(cache_entry)
1326 {
1327 cl_mem_input = _pixel_cache_clmem_get(cache_entry, host_ptr, devid, roi->width, roi->height,
1328 (int)bpp, flags);
1329 reused_from_cache = (!IS_NULL_PTR(cl_mem_input));
1330 }
1331
1332 // This will internally try to free up cache space if first alloc fails
1333 if(IS_NULL_PTR(cl_mem_input))
1334 {
1335 cl_mem_input = dt_opencl_alloc_device_use_host_pointer(devid, roi->width, roi->height, cl_bpp,
1336 host_ptr, flags);
1338 _cache_print(DT_DEBUG_OPENCL, "[dev_pixelpipe] allocated a pinned GPU buffer for %s %s\n", module->name(), message);
1339 }
1340 }
1341 else
1342 {
1343 if(cache_entry)
1344 {
1345 /* Device-only allocations are tracked with a NULL host_ptr key and a normalized READ_WRITE
1346 * flag so scratch buffers can be reused deterministically across drivers. */
1347 cl_mem_input = _pixel_cache_clmem_get(cache_entry, NULL, devid, roi->width, roi->height,
1348 (int)bpp, CL_MEM_READ_WRITE);
1349 reused_from_cache = (!IS_NULL_PTR(cl_mem_input));
1350 }
1351
1352 // This will internally try to free up cache space if first alloc fails
1353 if(IS_NULL_PTR(cl_mem_input))
1354 {
1355 cl_mem_input = dt_dev_pixelpipe_cache_alloc_cl_device_buffer(devid, roi, bpp, module, message, keep);
1356
1358 _cache_print(DT_DEBUG_OPENCL, "[dev_pixelpipe] allocated a device-only GPU buffer for %s %s\n", module->name(), message);
1359 }
1360 }
1361
1362 if(IS_NULL_PTR(cl_mem_input))
1363 {
1364 _cache_print(DT_DEBUG_OPENCL, "[dev_pixelpipe] couldn't allocate GPU buffer for module %s %s\n", module->name(), message);
1365 }
1366 else if(reused_from_cache)
1367 {
1368 const int hits = dt_atomic_add_int(&clmem_reuse_hits, 1) + 1;
1369 const int misses = dt_atomic_get_int(&clmem_reuse_misses);
1372 "[dev_pixelpipe] reused GPU buffer from cache (hits=%d, misses=%d) for module %s %s\n",
1373 hits, misses, module->name(), message);
1374 }
1375 else
1376 {
1377 dt_atomic_add_int(&clmem_reuse_misses, 1);
1378 }
1379
1380 if(out_reused) *out_reused = reused_from_cache;
1381 return cl_mem_input;
1382}
1383
1407 void *host_ptr, const gboolean cache_device)
1408{
1409 if(!IS_NULL_PTR(cl_mem_buffer) && !IS_NULL_PTR(*cl_mem_buffer))
1410 {
1411 cl_mem mem = *cl_mem_buffer;
1412 if(cache_device && !IS_NULL_PTR(cache_entry))
1413 {
1414 _pixel_cache_clmem_put(cache_entry, host_ptr, mem);
1415 }
1416 else
1417 {
1418 if(!IS_NULL_PTR(cache_entry)) _pixel_cache_clmem_remove(cache_entry, mem);
1420 }
1421 *cl_mem_buffer = NULL;
1422 }
1423}
1424
1454int dt_dev_pixelpipe_cache_sync_cl_buffer(const int devid, void *host_ptr, void *cl_mem_buffer,
1455 const dt_iop_roi_t *roi, int cl_mode, size_t bpp,
1456 dt_iop_module_t *module, const char *message)
1457{
1458 if(IS_NULL_PTR(host_ptr) || IS_NULL_PTR(cl_mem_buffer)) return 1;
1459
1460 const cl_mem mem = (cl_mem)cl_mem_buffer;
1461
1462 // Fast path for true zero-copy pinned images: map/unmap is enough to synchronize host<->device.
1464 {
1465 void *mapped = dt_opencl_map_image(devid, mem, TRUE, cl_mode, roi->width, roi->height, (int)bpp);
1466 if(dt_opencl_unmap_mem_object(devid, mem, mapped) == CL_SUCCESS)
1467 {
1469 "[dev_pixelpipe] successfully synced image %s via map/unmap for module %s (%s)\n",
1470 (cl_mode == CL_MAP_WRITE) ? "host to device" : "device to host",
1471 (module) ? module->op : "base buffer", message);
1472 return 0;
1473 }
1474 }
1475
1476 // Fallback: explicit blocking transfers (safe on all drivers).
1477 cl_int err = CL_SUCCESS;
1478 if(cl_mode == CL_MAP_WRITE)
1479 err = dt_opencl_write_host_to_device(devid, host_ptr, mem, roi->width, roi->height, (int)bpp);
1480 else if(cl_mode == CL_MAP_READ)
1481 err = dt_opencl_read_host_from_device(devid, host_ptr, mem, roi->width, roi->height, (int)bpp);
1482 else
1483 return 1;
1484
1485 if(err != CL_SUCCESS)
1486 {
1487 _cache_print(DT_DEBUG_OPENCL, "[dev_pixelpipe] couldn't copy image %s for module %s (%s)\n",
1488 (cl_mode == CL_MAP_WRITE) ? "host to device" : "device to host",
1489 (module) ? module->op : "base buffer", message);
1490 return 1;
1491 }
1492
1493 _cache_print(DT_DEBUG_OPENCL, "[dev_pixelpipe] successfully copied image %s for module %s (%s)\n",
1494 (cl_mode == CL_MAP_WRITE) ? "host to device" : "device to host",
1495 (module) ? module->op : "base buffer", message);
1496 return 0;
1497}
1498
1513float *dt_dev_pixelpipe_cache_restore_cl_buffer(dt_dev_pixelpipe_t *pipe, float *input, void *cl_mem_input,
1514 const dt_iop_roi_t *roi_in, dt_iop_module_t *module,
1515 const size_t in_bpp, dt_pixel_cache_entry_t *input_entry,
1516 const char *message)
1517{
1518 if(IS_NULL_PTR(cl_mem_input)) return input;
1520
1521 const int fail = dt_dev_pixelpipe_cache_sync_cl_buffer(pipe->devid, input, cl_mem_input, roi_in,
1522 CL_MAP_READ, in_bpp, module, message);
1523 if(!fail) dt_dev_pixelpipe_cache_flag_host_written(input_entry);
1525 return fail ? NULL : input;
1526}
1527
1556 float *input, void **cl_mem_input,
1557 const dt_iop_roi_t *roi_in, const size_t in_bpp,
1558 dt_pixel_cache_entry_t *input_entry,
1559 dt_pixel_cache_entry_t **locked_input_entry, void *keep)
1560{
1561 if(IS_NULL_PTR(locked_input_entry)) return 1;
1562 *locked_input_entry = NULL;
1563
1564 if(!IS_NULL_PTR(*cl_mem_input))
1565 {
1566 // We passed the OpenCL memory buffer through directly on vRAM from previous module.
1567 // This is fast and efficient.
1568 // If it's a true zero-copy pinned image, keep the input cache entry read-locked until kernels complete,
1569 // otherwise another thread may overwrite host memory while the GPU is still reading it.
1570 _cache_print(DT_DEBUG_OPENCL, "[dev_pixelpipe] %s will use its input directly from vRAM\n", module->name());
1571 const cl_mem mem = (cl_mem)*cl_mem_input;
1573 {
1575 *locked_input_entry = input_entry;
1576 }
1577 return 0;
1578 }
1579
1580 if(IS_NULL_PTR(input))
1581 {
1582 _cache_print(DT_DEBUG_OPENCL, "[dev_pixelpipe] %s has no input (cache)\n", module->name());
1583 return 1;
1584 }
1585
1587
1588 // Try to reuse a cached pinned buffer; otherwise allocate a new pinned image backed by `input`.
1589 gboolean input_reused_from_cache = FALSE;
1590 *cl_mem_input = dt_dev_pixelpipe_cache_get_cl_buffer(pipe->devid, input, roi_in, in_bpp, module,
1591 "input", input_entry,
1592 &input_reused_from_cache, keep);
1593 int fail = (IS_NULL_PTR(*cl_mem_input));
1594
1595 // If the input is true zero-copy, the GPU will access host memory asynchronously: keep the cache
1596 // entry read-locked until all kernels have completed. If not, drivers may use a device-side copy
1597 // which must be synchronized from the host before running kernels.
1598 gboolean keep_lock = FALSE;
1599 cl_mem mem = NULL;
1600 if(!fail && *cl_mem_input)
1601 {
1602 mem = (cl_mem)*cl_mem_input;
1603 keep_lock = dt_opencl_is_pinned_memory(mem);
1604 }
1605
1606 /* A reused cached pinned image already carries the authoritative device payload from the
1607 * previous module output. Re-uploading host RAM here would overwrite that valid vRAM state
1608 * with whatever stale contents the host buffer still has when the previous stage stayed GPU-only.
1609 * Only freshly allocated pinned inputs need an explicit host->device copy. */
1610 if(!fail && mem && !keep_lock && !input_reused_from_cache)
1611 {
1612 const cl_int err = dt_opencl_write_host_to_device(pipe->devid, input, mem, roi_in->width, roi_in->height,
1613 (int)in_bpp);
1614 if(err != CL_SUCCESS)
1615 {
1616 _cache_print(DT_DEBUG_OPENCL, "[dev_pixelpipe] couldn't copy image host to device for module %s (%s)\n",
1617 (module) ? module->op : "base buffer", "cache to input");
1618 fail = TRUE;
1619 }
1620 else
1621 {
1622 _cache_print(DT_DEBUG_OPENCL, "[dev_pixelpipe] successfully copied image host to device for module %s (%s)\n",
1623 (module) ? module->op : "base buffer", "cache to input");
1624 }
1625 }
1626
1627 // Enforce sync with the CPU/RAM cache so lock validity is guaranteed.
1629
1630 if(keep_lock)
1631 *locked_input_entry = input_entry;
1632 else
1634
1635 return fail ? 1 : 0;
1636}
1637#else
1638void *dt_dev_pixelpipe_cache_get_cl_buffer(int devid, void *host_ptr, const dt_iop_roi_t *roi,
1639 size_t bpp, dt_iop_module_t *module, const char *message,
1641 gboolean *out_reused, void *keep)
1642{
1643 (void)devid;
1644 (void)host_ptr;
1645 (void)roi;
1646 (void)bpp;
1647 (void)module;
1648 (void)message;
1649 (void)entry;
1650 (void)keep;
1651 if(out_reused) *out_reused = FALSE;
1652 return NULL;
1653}
1654
1655void *dt_dev_pixelpipe_cache_alloc_cl_device_buffer(int devid, const dt_iop_roi_t *roi, size_t bpp,
1656 const dt_iop_module_t *module,
1657 const char *message, void *keep)
1658{
1659 (void)devid;
1660 (void)roi;
1661 (void)bpp;
1662 (void)module;
1663 (void)message;
1664 (void)keep;
1665 return NULL;
1666}
1667
1669 void *host_ptr, gboolean cache_device)
1670{
1671 (void)entry;
1672 (void)host_ptr;
1673 (void)cache_device;
1674 if(cl_mem_buffer) *cl_mem_buffer = NULL;
1675}
1676
1677int dt_dev_pixelpipe_cache_sync_cl_buffer(int devid, void *host_ptr, void *cl_mem_buffer,
1678 const dt_iop_roi_t *roi, int cl_mode, size_t bpp,
1679 dt_iop_module_t *module, const char *message)
1680{
1681 (void)devid;
1682 (void)host_ptr;
1683 (void)cl_mem_buffer;
1684 (void)roi;
1685 (void)cl_mode;
1686 (void)bpp;
1687 (void)module;
1688 (void)message;
1689 return 1;
1690}
1691
1693 void *cl_mem_input, const dt_iop_roi_t *roi_in,
1694 dt_iop_module_t *module, size_t in_bpp,
1695 dt_pixel_cache_entry_t *input_entry,
1696 const char *message)
1697{
1698 (void)pipe;
1699 (void)cl_mem_input;
1700 (void)roi_in;
1701 (void)module;
1702 (void)in_bpp;
1703 (void)input_entry;
1704 (void)message;
1705 return input;
1706}
1707
1709 float *input, void **cl_mem_input,
1710 const dt_iop_roi_t *roi_in, size_t in_bpp,
1711 dt_pixel_cache_entry_t *input_entry,
1712 dt_pixel_cache_entry_t **locked_input_entry,
1713 void *keep)
1714{
1715 (void)pipe;
1716 (void)module;
1717 (void)input;
1718 (void)cl_mem_input;
1719 (void)roi_in;
1720 (void)in_bpp;
1721 (void)input_entry;
1722 (void)locked_input_entry;
1723 (void)keep;
1724 return 1;
1725}
1726#endif
1727
1729{
1731 if(IS_NULL_PTR(cache) || IS_NULL_PTR(host_ptr)) return NULL;
1732
1733 dt_pthread_mutex_lock(&cache->lock);
1735 if(entry)
1738
1739 return entry;
1740}
1741
1742/* Allocations at or below this size bypass the system-pressure valve: they cannot
1743 * change the pressure meaningfully, and refusing them breaks functionality for no
1744 * benefit. Deliberately small -- this is an escape hatch for scratch and bookkeeping
1745 * buffers, not a hole large enough for pipeline tiles. */
1746#define DT_PIXELPIPE_CACHE_PRESSURE_EXEMPT_SIZE ((size_t)1024 * 1024)
1747
1748// The pressure valve tells the user of one module's refusals on one image at most this often.
1749#define DT_PIXELPIPE_CACHE_PRESSURE_ALERT_PERIOD_US ((gint64)10 * G_USEC_PER_SEC)
1750
1751static gboolean _pressure_alert_expired(gpointer key __attribute__((unused)), gpointer value, gpointer user_data)
1752{
1753 return *(const gint64 *)user_data - *(const gint64 *)value > DT_PIXELPIPE_CACHE_PRESSURE_ALERT_PERIOD_US;
1754}
1755
1756/* System memory-pressure valve (issue #1083).
1757 *
1758 * The internal budget (max_memory) is only a plan made at startup: it says nothing
1759 * about what the system can actually back RIGHT NOW, with other applications competing
1760 * for the same physical RAM. Before growing our committed footprint by `request_size`,
1761 * make sure the system-wide available memory keeps the configured floor
1762 * (dt_get_memory_pressure_floor()); when it doesn't, evict LRU entries to cover the
1763 * deficit, hand their pages back to the OS for real, and re-read what that actually
1764 * bought — so the system OOM-killer never has a reason to look at us.
1765 *
1766 * The probe is rate-limited and the cached value is decremented by our own
1767 * allocations in between, so the hot path pays one probe per PROBE_PERIOD at most.
1768 * That running estimate legitimately reaches 0 under sustained pressure, which is why
1769 * "the platform answers at all" is tracked separately (`sys_probe_valid`) instead of
1770 * being read off a 0 estimate: conflating the two would disable the valve at exactly
1771 * the moment it matters.
1772 *
1773 * Returns TRUE when the allocation may proceed. Returns FALSE when, even after
1774 * shedding everything evictable, the system could not take `request_size` more bytes
1775 * without dropping under HALF the floor: the caller must fail the allocation cleanly —
1776 * a failed pipeline with a message beats a silent SIGKILL from the OOM-killer.
1777 */
1778static gboolean _system_memory_pressure_valve(dt_dev_pixelpipe_cache_t *cache, size_t request_size)
1779{
1780 const size_t pressure_floor = dt_get_memory_pressure_floor();
1781 if(pressure_floor == 0) return TRUE;
1782
1783 dt_pthread_mutex_lock(&cache->lock);
1784
1785 gint64 now = g_get_monotonic_time();
1786 const gint64 PROBE_PERIOD_US = 100000; // 100 ms
1787 if(cache->sys_probe_time_us == 0 || now - cache->sys_probe_time_us > PROBE_PERIOD_US)
1788 {
1789 const size_t probed = dt_get_system_available_mem();
1790 cache->sys_probe_valid = (probed > 0);
1791 cache->sys_available_est = probed;
1792 cache->sys_probe_time_us = now;
1793 }
1794
1795 // The platform gives us no way to know: pressure handling disabled, never refuse.
1796 if(!cache->sys_probe_valid)
1797 {
1799 return TRUE;
1800 }
1801
1802 const gboolean negligible = request_size <= DT_PIXELPIPE_CACHE_PRESSURE_EXEMPT_SIZE;
1803
1804 if(cache->sys_available_est < request_size + pressure_floor)
1805 {
1806 const size_t deficit = request_size + pressure_floor - cache->sys_available_est;
1807 size_t freed = 0;
1808 /* The deficit is host RAM: a line kept on a device only would count its size as freed and
1809 * give the system nothing. */
1810 while(freed < deficit && g_hash_table_size(cache->entries) > 0)
1811 {
1812 const size_t before = cache->current_memory;
1814 freed += before - cache->current_memory;
1815 }
1816
1817 /* Evicting only returns the pages to the ARENA. How much of that reaches the OS
1818 * is not ours to guess: the per-free lazy release is a no-op on Windows until a
1819 * decommit runs, and even where MADV_FREE applies, the kernel decides when those
1820 * pages stop counting against us. So hand them over for real, then take the new
1821 * number from the OS instead of crediting what we think we released — an
1822 * over-credit here would let the allocation through on a system that is still
1823 * just as full, which is precisely the OOM this valve exists to prevent.
1824 * Ordering note: cache->lock is held and dt_cache_arena_trim() takes arena.lock,
1825 * matching the cache->lock → arena.lock order the rest of this file already uses
1826 * (see the defrag path below). */
1827 if(freed) dt_cache_arena_trim(&cache->arena);
1828
1829 /* Re-read even when nothing was evicted: between probes the estimate is debited by
1830 * every allocation and credited by no free, so what other pipes gave back since the
1831 * last probe is not in it. An allocation is refused on what the OS says, never on it.
1832 * A negligible one is never refused, so it does not pay for the read. */
1833 if(freed || !negligible)
1834 {
1836 const size_t probed = dt_get_system_available_mem();
1837 cache->sys_probe_valid = (probed > 0);
1838 cache->sys_available_est = probed;
1839 cache->sys_probe_time_us = g_get_monotonic_time();
1840 }
1841
1842 if(freed)
1844 "[pixelpipe_cache] system memory pressure: shed %" G_GSIZE_FORMAT " MiB of cache "
1845 "(%" G_GSIZE_FORMAT " MiB available after trim, floor %" G_GSIZE_FORMAT " MiB)\n",
1846 freed / (1024 * 1024), cache->sys_available_est / (1024 * 1024),
1847 pressure_floor / (1024 * 1024));
1848 }
1849
1850 // The post-trim re-probe can itself come back unanswered; absence of information is
1851 // never a reason to refuse (see the dt_get_system_available_mem() header contract).
1852 /* A request too small to move the needle must never be refused. The valve exists to
1853 * stop the cache COMMITTING LARGE BUFFERS into the memory the OS and other
1854 * applications need -- not to make Ansel fail. Because the floor term dominates the
1855 * comparison below, a system already under the floor refused EVERY allocation
1856 * regardless of size, printing the giveaway line
1857 *
1858 * [pixelpipe_cache] refusing to allocate 0 MiB: the system has only 1638 MiB ...
1859 *
1860 * and turning a transient shortage into a hard failure (it segfaulted startup through
1861 * an unchecked allocation in common/points.h). Anything at or below this size cannot
1862 * meaningfully change system pressure, and refusing it buys nothing. Larger requests
1863 * remain fully gated.
1864 *
1865 * The comparison below uses the WHOLE floor, not half of it. The floor used to be a
1866 * large derived number where a half-way hard limit made sense as hysteresis; it is now
1867 * an absolute reserve (DT_MEMORY_PRESSURE_FLOOR_DEFAULT, 200 MiB), and "always leave
1868 * 200 MiB to the OS" has to mean 200, not 100. The shed/trim step above already ran at
1869 * this same threshold, so reaching here means trimming did not recover enough. */
1870 const gboolean allowed = negligible
1871 || !cache->sys_probe_valid
1872 || (cache->sys_available_est >= request_size + pressure_floor);
1873
1874 if(allowed)
1875 {
1876 // Optimistically debit the estimate now that the caller will commit these pages;
1877 // self-corrects at the next probe if the arena allocation fails afterwards.
1878 cache->sys_available_est
1879 = (cache->sys_available_est > request_size) ? cache->sys_available_est - request_size : 0;
1881 return TRUE;
1882 }
1883
1884 // Alert the user of one module on one image at most once per alert period: this fires per
1885 // failed allocation, on a system that is already drowning. Per module and image, not once for
1886 // all: on 2026-10-06 two raws whose thumbnails were both refused got one line in the window, as
1887 // the first refusal silenced the second for 10 s. The refused allocation makes the module that
1888 // asked for it fail, and a pipe that failed publishes nothing: say so where it cannot be
1889 // missed, or the user waits for an image that will not come.
1890 const char *module = dt_pixelpipe_cache_current_module;
1891 const char *image = dt_pixelpipe_cache_current_image;
1892 g_hash_table_foreach_remove(cache->pressure_alerts, _pressure_alert_expired, &now);
1893 gchar *who = g_strdup_printf("%s\n%s", module ? module : "", image ? image : "");
1894 if(g_hash_table_contains(cache->pressure_alerts, who))
1895 {
1896 dt_free(who);
1897 }
1898 else
1899 {
1900 gint64 *when = g_new(gint64, 1);
1901 *when = now;
1902 g_hash_table_insert(cache->pressure_alerts, who, when);
1903 // The size, the module that asked for it, its image and the memory left are the item, so that
1904 // the message is the same at every refusal. What is left is counted above the floor, as the
1905 // valve counts it, so it always reads less than the size; the system's own figure includes
1906 // the floor and can read more. Two decimals: in whole MiB, a 1.71 MiB request read "1 MiB",
1907 // which the valve lets through. Not G_GSIZE_FORMAT: xgettext does not expand macros, and
1908 // would cut the msgid there.
1909 const double request_mib = (double)request_size / (1024 * 1024);
1910 const double left_mib
1911 = (double)(MAX(cache->sys_available_est, pressure_floor) - pressure_floor) / (1024 * 1024);
1912 gchar *item = NULL;
1913 if(IS_NULL_PTR(module))
1914 item = g_strdup_printf(_("%.2f MiB: only %.2f MiB available"), request_mib, left_mib);
1915 else if(IS_NULL_PTR(image))
1916 item = g_strdup_printf(_("%.2f MiB for module `%s`: only %.2f MiB available"), request_mib, module,
1917 left_mib);
1918 else
1919 item = g_strdup_printf(_("%.2f MiB for module `%s` on %s: only %.2f MiB available"),
1920 request_mib, module, image, left_mib);
1921 _alert_user(_("Your system is running out of memory:\n"
1922 "A module could not get the memory it needs, "
1923 "so it fails and the image is not updated.\n\n"
1924 "Close other applications or add more RAM to your system."),
1925 item);
1926 dt_free(item);
1927 }
1928 fprintf(stdout,
1929 "[pixelpipe_cache] refusing to allocate %" G_GSIZE_FORMAT " MiB: the system has only "
1930 "%" G_GSIZE_FORMAT " MiB of available RAM left (pressure floor: %" G_GSIZE_FORMAT " MiB)\n",
1931 request_size / (1024 * 1024), cache->sys_available_est / (1024 * 1024),
1932 pressure_floor / (1024 * 1024));
1933
1935 return FALSE;
1936}
1937
1938// Attempt to allocate from the arena; if fragmentation prevents it, evict LRU cache lines
1939// until a sufficiently large contiguous run is available (or nothing remains to evict).
1940// On NULL, `system_refused` says which of the two refused: the system, through the pressure valve,
1941// which has already told the user, or the arena.
1942static inline void *_arena_alloc_with_defrag(dt_dev_pixelpipe_cache_t *cache, size_t request_size,
1943 size_t *actual_size, gboolean *system_refused)
1944{
1945 // Never grow the committed footprint past what the system can actually take,
1946 // whatever our internal budget still allows.
1947 *system_refused = !_system_memory_pressure_valve(cache, request_size);
1948 if(*system_refused) return NULL;
1949
1950 void *buf = dt_cache_arena_alloc(&cache->arena, request_size, actual_size);
1951 if(!IS_NULL_PTR(buf)) return buf;
1952
1953 uint32_t pages_needed = 0;
1954 if(dt_cache_arena_calc(&cache->arena, request_size, &pages_needed, NULL))
1955 {
1956 dt_pthread_mutex_lock(&cache->lock);
1957 uint32_t total_free_pages = 0, largest_free_run_pages = 0;
1958 dt_cache_arena_stats(&cache->arena, &total_free_pages, &largest_free_run_pages);
1959
1960 while(largest_free_run_pages < pages_needed && g_hash_table_size(cache->entries) > 0)
1961 {
1963 dt_cache_arena_stats(&cache->arena, &total_free_pages, &largest_free_run_pages);
1964 }
1966 }
1967
1968 return dt_cache_arena_alloc(&cache->arena, request_size, actual_size);
1969}
1970
1971static inline void _arena_stats_bytes(dt_dev_pixelpipe_cache_t *cache, uint32_t *total_pages,
1972 uint32_t *largest_pages, size_t *total_bytes, size_t *largest_bytes)
1973{
1974 dt_cache_arena_stats(&cache->arena, total_pages, largest_pages);
1975 const size_t page_size = cache->arena.page_size ? cache->arena.page_size : 1;
1976 if(total_bytes) *total_bytes = (size_t)(*total_pages) * page_size;
1977 if(largest_bytes) *largest_bytes = (size_t)(*largest_pages) * page_size;
1978}
1979
1980/* Largest contiguous free run in the arena, in bytes. The arena serves every
1981 * allocation from ONE contiguous run, and pinned entries partition its
1982 * address space — so at tiling-planning time this, not the byte headroom, is
1983 * the number a module's working set must actually fit (see the available-
1984 * memory cap in develop/tiling.c). Locked because the arena bitmap mutates
1985 * under concurrent pipes. */
1987{
1989 uint32_t total_pages = 0, largest_pages = 0;
1990 size_t total_bytes = 0, largest_bytes = 0;
1991 dt_pthread_mutex_lock(&cache->lock);
1992 _arena_stats_bytes(cache, &total_pages, &largest_pages, &total_bytes, &largest_bytes);
1994 return largest_bytes;
1995}
1996
1997static inline void _log_arena_allocation_failure(dt_dev_pixelpipe_cache_t *cache, size_t request_size,
1998 const char *entry_name, const char *module, uint64_t hash,
1999 gboolean name_is_file, gboolean system_refused)
2000{
2002
2003 uint32_t total_free_pages = 0, largest_free_run_pages = 0;
2004 size_t total_free_bytes = 0, largest_free_bytes = 0;
2005 _arena_stats_bytes(cache, &total_free_pages, &largest_free_run_pages, &total_free_bytes, &largest_free_bytes);
2006
2007 if(entry_name)
2008 fprintf(stdout,
2009 "[pixelpipe_cache] failed to allocate %" G_GSIZE_FORMAT " bytes for entry %" PRIu64 " (%s, module=%s) "
2010 "[arena largest=%" G_GSIZE_FORMAT " MiB, total=%" G_GSIZE_FORMAT " MiB, cache=%" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT " MiB]\n",
2011 request_size, hash, entry_name, module ? module : "unknown",
2012 largest_free_bytes / (1024 * 1024), total_free_bytes / (1024 * 1024),
2013 cache->current_memory / (1024 * 1024), cache->max_memory / (1024 * 1024));
2014 else
2015 fprintf(stdout,
2016 "[pixelpipe_cache] failed to allocate %" G_GSIZE_FORMAT " bytes for entry %" PRIu64 " (module=%s) "
2017 "[arena largest=%" G_GSIZE_FORMAT " MiB, total=%" G_GSIZE_FORMAT " MiB, cache=%" G_GSIZE_FORMAT "/%" G_GSIZE_FORMAT " MiB]\n",
2018 request_size, hash, module ? module : "unknown",
2019 largest_free_bytes / (1024 * 1024), total_free_bytes / (1024 * 1024),
2020 cache->current_memory / (1024 * 1024), cache->max_memory / (1024 * 1024));
2021
2022 // Every caller gets NULL back. When the pressure valve refused, it has already told the user that
2023 // the system is running out of memory, and "the pipeline cache is full" on top of it is false: on
2024 // 2026-10-06 the valve refused lens 370 MiB with the cache at 741 of 4461 MiB, 370 of them lens's
2025 // own unallocated entry. Only a refusal by the arena itself is the cache's own.
2026 if(!system_refused) _alert_cache_refused(entry_name, module);
2027
2028 (void)name_is_file; // kept for signature symmetry if future callers need it.
2029}
2030
2031// keep: OpenCL buffer to NOT release
2032#ifdef HAVE_OPENCL
2033// Release this device's vRAM payloads for one entry. The caller has already established that
2034// the entry is idle (refcount == 0, not write-locked), so the device buffers are nobody's live
2035// input and reclaiming them honors the flush's purpose: free vRAM for later allocations.
2036// Returns TRUE if the entry holds no buffer at all afterwards (no host RAM, no vRAM on any
2037// device) and should therefore be evicted entirely instead of lingering as a husk.
2038static gboolean _cache_entry_clmem_flush_device(dt_pixel_cache_entry_t *entry, const int devid)
2039{
2040 // devid is always >= 0 here: dt_dev_pixelpipe_cache_flush_clmem() early-returns
2041 // otherwise. Only cachelines living on this specific device are candidates.
2043
2044 for(GList *l = g_list_first(entry->cl_mem_list); l;)
2045 {
2046 GList *next = g_list_next(l);
2047 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
2048 if(IS_NULL_PTR(c->mem))
2049 {
2050 // Don't keep cacheline with NULL buffer
2051 entry->cl_mem_list = g_list_delete_link(entry->cl_mem_list, l);
2052 dt_free(c);
2053 l = next;
2054 continue;
2055 }
2056
2057 gboolean referenced = c->refs > 0;
2058 gboolean not_ours = dt_opencl_get_mem_context_id(c->mem) != devid;
2059
2060 if(referenced || not_ours)
2061 {
2062 // Don't flush cachelines that don't belong to the current OpenCL device,
2063 // or are still borrowed by an in-flight GPU module (per-payload refs > 0).
2066 "[dt_dev_pixelpipe_cache_flush_clmem] for entry %" PRIu64 ": couldn't flush %p "
2067 "(referenced=%i not ours=%i)\n",
2068 entry->hash, c->mem, referenced, not_ours);
2069 l = next;
2070 continue;
2071 }
2072
2073 entry->cl_mem_list = g_list_delete_link(entry->cl_mem_list, l);
2075 dt_free(c);
2076 l = next;
2077 }
2078
2079 // A cacheline that now carries neither host pixels nor any vRAM is a husk: the cache would
2080 // still hand it out as a hit, making a later consumer abort with "has no RAM nor vRAM input"
2081 // (a skull thumbnail). Signal the caller to delete it entirely. A stale host buffer counts as
2082 // none: the vRAM just released was the only copy of this hash's pixels.
2083 const gboolean empty
2086 return empty;
2087}
2088#else
2089static gboolean _cache_entry_clmem_flush_device(dt_pixel_cache_entry_t *entry, const int devid)
2090{
2091 return FALSE;
2092}
2093#endif
2094
2096{
2098 // allocate the data buffer
2099 if(IS_NULL_PTR(cache_entry->data))
2100 {
2101 gboolean system_refused = FALSE;
2102 cache_entry->data = _arena_alloc_with_defrag(cache, cache_entry->size, &cache_entry->size, &system_refused);
2103
2104 if(IS_NULL_PTR(cache_entry->data))
2105 {
2106 const char *module = dt_pixelpipe_cache_current_module;
2107 _log_arena_allocation_failure(cache, cache_entry->size, cache_entry->name, module,
2108 cache_entry->hash, FALSE, system_refused);
2109 }
2110 }
2111
2112 return cache_entry->data;
2113}
2114
2116{
2117 return (!IS_NULL_PTR(entry) && !dt_atomic_get_int(&entry->host_stale)) ? entry->data : NULL;
2118}
2119
2121{
2122 return entry->data;
2123}
2124
2129
2131{
2132 return entry ? entry->size : 0;
2133}
2134
2135/* Kernel memory pressure: the cache's half of it.
2136 *
2137 * caches/pixelpipe_cache_pressure.c decides WHEN memory must go back to the system and HOW MUCH,
2138 * from the kernel counters system/memory_pressure.c reads; this is the part only the cache can
2139 * answer -- what it holds, and what giving some of it back costs. Both run under `lock`, taken by
2140 * whoever called into the monitor, the watcher's own thread included. */
2141static size_t _pressure_held(void *user)
2142{
2143 return ((dt_dev_pixelpipe_cache_t *)user)->current_memory;
2144}
2145
2146static size_t _pressure_shed(void *user, const size_t target, size_t *given_back)
2147{
2149
2150 while(cache->current_memory > target && g_hash_table_size(cache->entries) > 0)
2152
2153 // Hand the pages over now rather than when the kernel gets to MADV_FREE, and make the
2154 // available-RAM valve re-read the system instead of trusting its pre-shed estimate.
2155 *given_back = dt_cache_arena_trim(&cache->arena);
2157 cache->sys_probe_time_us = 0;
2158 return cache->current_memory;
2159}
2160
2167
2168// The kernel woke the watcher, on a thread of its own: take the lock the monitor is guarded by.
2169static void _pressure_wake(void *user)
2170{
2173 dt_pthread_mutex_lock(&cache->lock);
2176}
2177
2178// What allocations evict down to: the plan, or less under kernel memory pressure.
2179// WARNING: non thread-safe
2180static inline size_t _cache_budget_locked(const dt_dev_pixelpipe_cache_t *cache)
2181{
2183}
2184
2185// Close the kernel-pressure window and act on it. WARNING: non thread-safe
2191
2192// WARNING: non thread-safe
2193static int _free_space_to_alloc(dt_dev_pixelpipe_cache_t *cache, const size_t size, const uint64_t hash,
2194 const char *name)
2195{
2196 /* Under kernel memory pressure, keep to the lowered budget as far as eviction allows. It is a
2197 * target, never a reason to fail: when everything left is in use the allocation still goes
2198 * ahead, and only the plan below is a hard limit. */
2200 const size_t budget = _cache_budget_locked(cache);
2201 while(cache->current_memory + size > budget && g_hash_table_size(cache->entries) > 0)
2203
2204 // Free up space if needed to match the max memory limit
2205 // If error, all entries are currently locked or in use, so we cannot free space to allocate a new entry.
2206 int error = 0;
2207 while(cache->current_memory + size > cache->max_memory && g_hash_table_size(cache->entries) > 0 && !error)
2209
2210 if(cache->current_memory + size > cache->max_memory)
2211 {
2212 const char *module = dt_pixelpipe_cache_current_module;
2213 const gboolean name_is_file = (!IS_NULL_PTR(name)) && (strchr(name, '/') != NULL) && (strchr(name, ':') != NULL);
2214 if(IS_NULL_PTR(name)) name = g_strdup("unknown");
2215
2216 if(hash)
2217 fprintf(stdout, "[pixelpipe] cache is full, cannot allocate new entry %" PRIu64 " (%s)\n", hash, name);
2218 else
2219 fprintf(stdout, "[pixelpipe] cache is full, cannot allocate new entry (%s)\n", name);
2220 // The module is named only beside a file-like entry name, as before.
2221 if(error)
2222 {
2223 // Everything left is in use: the caller is refused, and gets NULL.
2224 _alert_cache_refused(name, name_is_file ? module : NULL);
2225 }
2226 else
2227 {
2228 // Nothing was left to evict -- the memory is held by working buffers, not cachelines -- so
2229 // the allocation goes ahead past the budget, and nothing fails.
2230 gchar *what = _cache_full_sentence(name, name_is_file ? module : NULL);
2231 _warn_user(what);
2232 dt_free(what);
2233 }
2234 }
2235
2236 // Both callers return NULL on an error: the allocation is refused.
2238
2239 return error;
2240}
2241
2243 const char *name)
2244{
2246 // Free up space if needed to match the max memory limit
2247 // If error, all entries are currently locked or in use, so we cannot free space to allocate a new entry.
2248 dt_pthread_mutex_lock(&cache->lock);
2249 int error = _free_space_to_alloc(cache, size, 0, name);
2251
2252 if(error) return NULL;
2253
2254 // Page size is the desired size + AVX/SSE rounding
2255 size_t page_size = 0;
2256 gboolean system_refused = FALSE;
2257 void *buf = _arena_alloc_with_defrag(cache, size, &page_size, &system_refused);
2258
2259 if(IS_NULL_PTR(buf))
2260 {
2261 _log_arena_allocation_failure(cache, size, name, NULL, 0, FALSE, system_refused);
2262 return NULL;
2263 }
2264
2265 void *aligned = __builtin_assume_aligned(buf, DT_CACHELINE_BYTES);
2266
2267 const uint64_t hash = (uint64_t)(uintptr_t)(aligned);
2268
2269 dt_pthread_mutex_lock(&cache->lock);
2270 dt_pixel_cache_entry_t *cache_entry
2271 = dt_pixel_cache_new_entry(hash, page_size, name, id, cache, FALSE, cache->external_entries);
2272
2273 if(IS_NULL_PTR(cache_entry))
2274 {
2276 dt_cache_arena_free(&cache->arena, buf, page_size);
2277 return NULL;
2278 }
2279
2280 // Keep this entry marked as "used" for diagnostics/bookkeeping.
2281 // Note that external_entries are not subject to LRU eviction, so we must not keep
2282 // a thread-owned rwlock held across the lifetime of the buffer (it may be freed
2283 // from a different thread during cleanup paths).
2284 _non_thread_safe_cache_ref_count_entry(cache, TRUE, cache_entry);
2285 cache_entry->data = aligned;
2286 _pixel_cache_touch(cache_entry);
2287 cache_entry->external_alloc = TRUE;
2289 return aligned;
2290}
2291
2292void dt_pixelpipe_cache_free_align_cache(void **mem, const char *message)
2293{
2295 if(IS_NULL_PTR(mem) || !*mem) return;
2296
2297 dt_pthread_mutex_lock(&cache->lock);
2298 const uint64_t hash = (uint64_t)(uintptr_t)(*mem);
2300 if(IS_NULL_PTR(cache_entry) || !cache_entry->external_alloc)
2301 {
2303 fprintf(stdout, "error while freeing cache entry: no entry found but we have a buffer, %s.\n", message);
2304 raise(SIGSEGV); // triggers dt_set_signal_handlers() backtrace on Unix
2305 return;
2306 }
2307
2308 _non_thread_safe_cache_ref_count_entry(cache, FALSE, cache_entry);
2309 g_hash_table_remove(cache->external_entries, &cache_entry->hash);
2310 *mem = NULL;
2311
2313}
2314
2315
2316// WARNING: not thread-safe, protect its calls with mutex lock
2318 const char *name, const int id,
2319 dt_dev_pixelpipe_cache_t *cache, gboolean alloc,
2320 GHashTable *table)
2321{
2322 uint32_t pages_needed = 0;
2323 size_t rounded_size = 0;
2324 if(!dt_cache_arena_calc(&cache->arena, size, &pages_needed, &rounded_size))
2325 {
2326 fprintf(stderr, "[pixelpipe] invalid cache entry size %" G_GSIZE_FORMAT " for %s\n", size, name);
2327 return NULL;
2328 }
2329
2330 int error = _free_space_to_alloc(cache, rounded_size, hash, name);
2331 if(error) return NULL;
2332
2334 if(IS_NULL_PTR(cache_entry)) return NULL;
2335
2336 // Metadata, easy to free in batch if need be
2337 cache_entry->size = rounded_size;
2338 /* A fresh entry is the MOST recently used one, not the oldest: leaving `age` at 0 until the
2339 * caller reaches _pixelpipe_cache_finalize_entry() makes it the unconditional LRU victim and
2340 * gives _for_each_remove_old() a `delta` of the whole process uptime in between. */
2341 dt_atomic_set_uint64(&cache_entry->age, (uint64_t)g_get_monotonic_time());
2342 cache_entry->hits = 0;
2343 cache_entry->hash = hash;
2344 cache_entry->serial = cache->next_serial++;
2345 cache_entry->id = id;
2347 cache_entry->refcount = 0;
2348 cache_entry->auto_destroy = FALSE;
2349 cache_entry->external_alloc = FALSE;
2350 cache_entry->data = NULL;
2351 cache_entry->cache = cache;
2352 cache_entry->cl_mem_list = NULL;
2353 dt_atomic_set_int(&cache_entry->host_stale, FALSE);
2354 dt_pthread_mutex_init(&cache_entry->cl_mem_lock, NULL);
2355
2356 // Optionally alloc the actual buffer, but still record its size in cache
2357 if(alloc) dt_pixel_cache_alloc(cache_entry);
2358
2359 if(alloc && IS_NULL_PTR(cache_entry->data))
2360 {
2361 dt_free(cache_entry);
2362 return NULL;
2363 }
2364
2365 // Metadata that need alloc
2366 cache_entry->name = g_strdup(name);
2367 dt_pthread_rwlock_init(&cache_entry->lock, NULL);
2368
2369 uint64_t *key = g_malloc(sizeof(*key));
2370 if(IS_NULL_PTR(key))
2371 {
2372 dt_pthread_rwlock_destroy(&cache_entry->lock);
2373 dt_free(cache_entry->name);
2375 dt_free(cache_entry);
2376 return NULL;
2377 }
2378 *key = hash;
2379 g_hash_table_insert(table, key, cache_entry);
2380
2381 // Note : we grow the cache size even though the data buffer is not yet allocated
2382 // This is planning
2383 cache->current_memory += rounded_size;
2384
2385 return cache_entry;
2386}
2387
2388
2390{
2391 if(IS_NULL_PTR(cache_entry)) return;
2392
2393 _pixel_cache_message(cache_entry, "freed", FALSE);
2394
2395 _observe_delete(cache_entry->hash, cache_entry->size, cache_entry->id,
2396 cache_entry->name);
2397
2398 /* Every live entry belongs to the one and only global pixelpipe cache, so its back-reference
2399 * must match it. If it doesn't, the entry struct has been corrupted (we have seen a single
2400 * flipped bit in the pointer from faulty RAM) or is stale: reaching cache->arena or
2401 * cache->current_memory through it would dereference a wild pointer and turn an innocuous
2402 * teardown into a SIGSEGV. Skip the arena free and the accounting in that case -- the arena is
2403 * unmapped wholesale right after, so nothing actually leaks. */
2404 dt_dev_pixelpipe_cache_t *cache = cache_entry->cache;
2405 if(cache != _pixelpipe_cache)
2406 {
2407 fprintf(stderr, "[pixelpipe] cache entry %p has a corrupted back-reference (%p, expected %p); "
2408 "skipping arena free to avoid a crash\n",
2409 (void *)cache_entry, (void *)cache, (void *)_pixelpipe_cache);
2410 cache = NULL;
2411 }
2412
2413 if(cache_entry->data && cache)
2414 {
2415#ifdef HAVE_OPENCL
2416 dt_pthread_mutex_lock(&cache_entry->cl_mem_lock);
2417 for(GList *l = cache_entry->cl_mem_list; l; l = g_list_next(l))
2418 {
2419 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
2420 if(IS_NULL_PTR(c) || c->host_ptr != cache_entry->data) continue;
2421
2422 /* Host-backed OpenCL images may still dereference `cache_entry->data` asynchronously until their
2423 * queued work completes. We therefore wait for the owning device before releasing the host arena slot,
2424 * otherwise an auto-destroyed intermediate can be recycled into another module output while the GPU
2425 * is still reading the previous pixels.
2426 *
2427 * dt_opencl_finish() flushes that device's event list, which is only thread-safe while we hold
2428 * the device lock (dt_opencl_reserve_device_by_id()). Touching it without that lock races the eventlist/cl_mem
2429 * bookkeeping of whichever pixelpipe is currently running on that device and crashes inside
2430 * clWaitForEvents (issues #859, #864, #131742439 -- the 3-min GUI garbage collection timeout
2431 * dt_dev_pixelpipe_cache_flush_old() owns no device lock). _free_cache_entry() also runs from LRU
2432 * eviction under a live pipe that already holds this very lock, so we can neither assume we hold it
2433 * nor block on it: trylock. If we acquire the device it is idle and we drain it safely; if we don't,
2434 * the owner is draining it itself at the end of its run and this refcount==0 entry is not one of its
2435 * live borrows, so skipping the finish is safe. */
2436 const int mem_devid = dt_opencl_get_mem_context_id((cl_mem)c->mem);
2437 if(mem_devid >= 0 && dt_opencl_is_inited()
2439 {
2440 dt_opencl_finish(mem_devid);
2441 dt_opencl_release_device(mem_devid);
2442 }
2443 }
2444 dt_pthread_mutex_unlock(&cache_entry->cl_mem_lock);
2445#endif
2446
2448 dt_cache_arena_free(&cache->arena, cache_entry->data, cache_entry->size);
2449 }
2450 else
2451 {
2453 }
2454
2455 cache_entry->data = NULL;
2456 if(cache) cache->current_memory -= cache_entry->size;
2457 dt_pthread_rwlock_destroy(&cache_entry->lock);
2459 dt_free(cache_entry->name);
2460 dt_free(cache_entry);
2461}
2462
2463static int garbage_collection = 0;
2464static int pressure_shedding = 0;
2465
2466gboolean dt_dev_pixelpipe_cache_init(size_t max_memory, const gboolean verbose,
2467 const gboolean verbose_detail)
2468{
2469 _verbose = verbose;
2470 _verbose_detail = verbose_detail;
2472 dt_pthread_mutex_init(&cache->lock, NULL);
2473 cache->entries = g_hash_table_new_full(g_int64_hash, g_int64_equal, dt_free_gpointer, (GDestroyNotify)_free_cache_entry);
2474 cache->external_entries = g_hash_table_new_full(g_int64_hash, g_int64_equal, dt_free_gpointer, (GDestroyNotify)_free_cache_entry);
2475 cache->max_memory = max_memory;
2476 cache->current_memory = 0;
2477 cache->next_serial = 1;
2478 cache->queries = cache->hits = 0;
2479 cache->sys_probe_time_us = 0;
2480 cache->sys_available_est = 0;
2481 cache->sys_probe_valid = FALSE;
2482 cache->pressure_alerts = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
2483 dt_pixelpipe_cache_pressure_monitor_init(&cache->psi, max_memory);
2484
2486 {
2487 if(cache->entries) g_hash_table_destroy(cache->entries);
2488 if(cache->external_entries) g_hash_table_destroy(cache->external_entries);
2489 if(cache->pressure_alerts) g_hash_table_destroy(cache->pressure_alerts);
2491 dt_free(cache);
2492 return FALSE;
2493 }
2494
2495 if(dt_cache_arena_init(&cache->arena, cache->max_memory))
2496 {
2498 g_hash_table_destroy(cache->pressure_alerts);
2499 g_hash_table_destroy(cache->external_entries);
2500 g_hash_table_destroy(cache->entries);
2501 dt_free(cache);
2502 return FALSE;
2503 }
2504
2505 // Run every 3 minutes
2506 garbage_collection = g_timeout_add(3 * 60 * 1000, (GSourceFunc)dt_dev_pixelpipe_cache_flush_old, cache);
2507
2508 // React within seconds when ANOTHER application's allocations push the system
2509 // toward memory starvation while we sit idle (the alloc-time pressure valve only
2510 // runs when we allocate). No-ops when the system has RAM to spare. Every 2 s, one
2511 // kernel-pressure window (DT_PIXELPIPE_CACHE_PSI_WINDOW_US).
2512 pressure_shedding = g_timeout_add_seconds(2, (GSourceFunc)_memory_pressure_shedder, cache);
2513
2514 // And react without waiting for either, since a thrashing machine runs neither.
2516
2517 _pixelpipe_cache = cache;
2518 return TRUE;
2519}
2520
2521
2523{
2525
2526 // Before anything it holds goes away: the watcher and both timeouts are handed `cache`,
2527 // take `lock` and walk the entries.
2529
2530 if(garbage_collection != 0)
2531 {
2532 g_source_remove(garbage_collection);
2534 }
2535
2536 if(pressure_shedding != 0)
2537 {
2538 g_source_remove(pressure_shedding);
2540 }
2541
2542 // _free_cache_entry() checks each entry's back-reference against _pixelpipe_cache, so the
2543 // instance stays published until the tables are gone.
2544 g_hash_table_destroy(cache->external_entries);
2545 g_hash_table_destroy(cache->entries);
2546 g_hash_table_destroy(cache->pressure_alerts);
2549
2550 _pixelpipe_cache = NULL;
2551 dt_free(cache);
2552}
2553
2555 const uint64_t hash, const size_t size,
2556 const char *name, const int id)
2557{
2558 dt_pixel_cache_entry_t *cache_entry = dt_pixel_cache_new_entry(hash, size, name, id, cache, FALSE, cache->entries);
2559 if(IS_NULL_PTR(cache_entry)) return NULL;
2560
2561 // Increase ref_count, consumer will have to decrease it
2562 _non_thread_safe_cache_ref_count_entry(cache, TRUE, cache_entry);
2563
2564 // Acquire write lock so caller can populate data safely
2566
2567 return cache_entry;
2568}
2569
2571 const uint64_t new_hash, const size_t size,
2572 const dt_pixel_cache_entry_t *reuse_hint)
2573{
2574 if(IS_NULL_PTR(cache) || IS_NULL_PTR(reuse_hint)) return NULL;
2575
2576 const uint64_t old_hash = reuse_hint->hash;
2577 if(old_hash == DT_PIXELPIPE_CACHE_HASH_INVALID || old_hash == new_hash) return NULL;
2578 if(reuse_hint->size < size) return NULL;
2579
2580 dt_pixel_cache_entry_t *cache_entry = _non_threadsafe_cache_get_entry(cache, cache->entries, old_hash);
2581 if(IS_NULL_PTR(cache_entry)) return NULL;
2582 if(cache_entry->serial != reuse_hint->serial) return NULL;
2583 if(cache_entry->auto_destroy) return NULL;
2584 if(cache_entry->size < size) return NULL;
2585 if(_non_threadsafe_cache_get_entry(cache, cache->entries, new_hash)) return NULL;
2586
2587 /* Anybody else holding this entry keeps it: the same `refcount > 0' the LRU, the vRAM flush
2588 * and the removal path all refuse to touch, now applied to reuse as well.
2589 *
2590 * The write lock below makes a rekey safe against someone READING the pixels, and that is a
2591 * different question from whether the entry may be rekeyed at all. Rekeying moves it to a new
2592 * hash, so the hash it was published under stops resolving -- and the backbuffer's consumers
2593 * find their frame by exactly that hash. Reuse the entry currently on screen and there is a
2594 * window, from here until the replacement frame is published, where the published hash names
2595 * nothing: `dt_dev_lock_pipe_surface()' misses, keeps the previous surface by design, and the
2596 * canvas alternates between the frame being drawn and the one before it. That is what this
2597 * looked like in the darkroom -- a shape flashing on and off while painting -- and no amount
2598 * of locking fixes it, because nothing was ever read at the wrong time.
2599 *
2600 * With the guard the producer simply takes another entry, so a held cacheline is republished
2601 * rather than overwritten, and the two the backbuffer alternates between each keep their own
2602 * pinned OpenCL payload: the reuse this exists for survives, measured at 99%.
2603 *
2604 * A transient output nobody holds -- every module that is not published to a consumer -- has
2605 * refcount 0 here and is rekeyed exactly as before. */
2606 if(dt_atomic_get_int(&cache_entry->refcount) > 0) return NULL;
2607
2608 _non_thread_safe_cache_ref_count_entry(cache, TRUE, cache_entry);
2610
2611 /* Rekey reuse transfers the RAM arena slot to a completely different hash. Any cached OpenCL payload
2612 * still attached to the previous owner would otherwise remain reachable through the new hash and could
2613 * later be materialized as if it belonged to the new module output. Bail out if some GPU path is still
2614 * borrowing one of those payloads, otherwise flush the stale bookkeeping before publishing the new hash. */
2615 dt_pthread_mutex_lock(&cache_entry->cl_mem_lock);
2616 for(GList *l = cache_entry->cl_mem_list; l; l = g_list_next(l))
2617 {
2618 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
2619 if(c && c->refs > 0)
2620 {
2621 dt_pthread_mutex_unlock(&cache_entry->cl_mem_lock);
2623 // _non_thread_safe_: our caller holds cache->lock (see the function name), and the
2624 // locking variant would take it again on a non-recursive mutex.
2625 _non_thread_safe_cache_ref_count_entry(cache, FALSE, cache_entry);
2626 return NULL;
2627 }
2628 }
2629 dt_pthread_mutex_unlock(&cache_entry->cl_mem_lock);
2630
2631 gpointer stolen_key = NULL;
2632 gpointer stolen_value = NULL;
2633 if(!g_hash_table_steal_extended(cache->entries, &old_hash, &stolen_key, &stolen_value)
2634 || stolen_value != cache_entry)
2635 {
2636 if(stolen_key && stolen_value) g_hash_table_insert(cache->entries, stolen_key, stolen_value);
2638 // _non_thread_safe_: same reason as the bail path above -- cache->lock is already ours.
2639 _non_thread_safe_cache_ref_count_entry(cache, FALSE, cache_entry);
2640 return NULL;
2641 }
2642
2643 *(uint64_t *)stolen_key = new_hash;
2644 cache_entry->hash = new_hash;
2645 /* The host buffer still holds the old hash's pixels, which an output kept on the device never
2646 * overwrites: the entry reports no host pixels until they are rewritten for the new hash. */
2647 dt_atomic_set_int(&cache_entry->host_stale, TRUE);
2648 g_hash_table_insert(cache->entries, stolen_key, cache_entry);
2649
2650 _observe_rekey(old_hash, new_hash);
2651
2653 "[pixelpipe_cache] writable rekey old=%" PRIu64 " new=%" PRIu64 " entry=%" PRIu64 "/%" PRIu64
2654 " refs=%i auto=%i data=%p module=%s\n",
2655 old_hash, new_hash, cache_entry->hash, cache_entry->serial,
2656 dt_atomic_get_int(&cache_entry->refcount), cache_entry->auto_destroy, cache_entry->data,
2658 return cache_entry;
2659}
2660
2661
2663 const size_t size, const char *name, const int id,
2664 const gboolean alloc, void **data,
2665 dt_pixel_cache_entry_t **entry)
2666{
2669 {
2670 _cache_print(DT_DEBUG_PIPECACHE, "[pixelpipe_cache] refusing invalid hash allocation for %s\n",
2671 name ? name : "unknown");
2672 if(data) *data = NULL;
2673 if(entry) *entry = NULL;
2674 return 1;
2675 }
2676
2677 // Search or create cache entry (under cache lock)
2678 dt_pthread_mutex_lock(&cache->lock);
2679 cache->queries++;
2680
2682 if(!IS_NULL_PTR(cache_entry) && cache_entry->auto_destroy)
2683 {
2684 _pixel_cache_message(cache_entry, "dropping auto-destroy entry before cache_get reuse", FALSE);
2685 if(_non_thread_safe_cache_remove(cache, FALSE, cache_entry, cache->entries) == 0)
2686 cache_entry = NULL;
2687 }
2688
2689 if(!IS_NULL_PTR(cache_entry))
2690 {
2691 cache->hits++;
2692 cache_entry->hits++;
2693 _non_thread_safe_cache_ref_count_entry(cache, TRUE, cache_entry);
2695
2696 // Allocate on demand if requested (e.g. when falling back from vRAM-only buffers).
2697 if(alloc && IS_NULL_PTR(cache_entry->data))
2698 {
2700 dt_pixel_cache_alloc(cache_entry);
2702 }
2703
2704 _pixelpipe_cache_finalize_entry(cache_entry, data, "found");
2705 if(entry) *entry = cache_entry;
2706 // existing output reused: a cache hit (entry pinned by the ref above, safe to read)
2707 _observe_read(hash, cache_entry->size);
2708
2709 return 0;
2710 }
2711
2712 cache_entry = _pixelpipe_cache_create_entry_locked(cache, hash, size, name, id);
2713 if(IS_NULL_PTR(cache_entry))
2714 {
2715 _cache_print(DT_DEBUG_PIPECACHE, "couldn't allocate new cache entry %" PRIu64 "\n", hash);
2717 if(entry) *entry = NULL;
2718 return 1;
2719 }
2720
2721 // Release cache lock AFTER acquiring entry locks to prevent other threads to capture it in-between
2723
2724 // Alloc after releasing the lock for better runtimes
2725 if(alloc) dt_pixel_cache_alloc(cache_entry);
2726
2727 _cache_print(DT_DEBUG_PIPECACHE, "[pixelpipe_cache] Write-lock on entry (new cache entry %" PRIu64 " for %s pipeline)\n",
2728 hash, name);
2729 _pixelpipe_cache_finalize_entry(cache_entry, data, "created");
2730
2731 if(entry) *entry = cache_entry;
2732 return 1;
2733}
2734
2737 const size_t size, const char *name, const int id,
2738 const gboolean alloc, const gboolean allow_rekey_reuse,
2739 const dt_pixel_cache_entry_t *reuse_hint,
2740 const dt_pixel_cache_entry_t *reuse_hint_prev,
2741 void **data,
2742 dt_pixel_cache_entry_t **entry)
2743{
2746 {
2747 if(data) *data = NULL;
2748 if(entry) *entry = NULL;
2750 }
2751
2752 dt_pthread_mutex_lock(&cache->lock);
2753 cache->queries++;
2754
2756 if(!IS_NULL_PTR(cache_entry) && cache_entry->auto_destroy)
2757 {
2758 _pixel_cache_message(cache_entry, "dropping auto-destroy entry before writable reuse", FALSE);
2759 if(_non_thread_safe_cache_remove(cache, FALSE, cache_entry, cache->entries) == 0)
2760 cache_entry = NULL;
2761 }
2762
2763 if(!IS_NULL_PTR(cache_entry))
2764 {
2765 /* Another pipe already owns this exact hash: the caller consumes that cacheline instead of
2766 * writing its own, so this is a genuine hit. `queries` was already counted above; count the
2767 * hit and refresh the MRU too, or a cacheline shared between the preview and the full pipe
2768 * ages as if only its original producer ever touched it. */
2769 cache->hits++;
2770 cache_entry->hits++;
2771 _pixel_cache_touch(cache_entry);
2772 // Referenced before the lock goes: nobody may hold this line, and the caller keeps it.
2773 _non_thread_safe_cache_ref_count_entry(cache, TRUE, cache_entry);
2775 if(!IS_NULL_PTR(data)) *data = NULL;
2776 if(!IS_NULL_PTR(entry)) *entry = cache_entry;
2778 }
2779
2780 if(allow_rekey_reuse)
2781 {
2782 /* Two candidates, most recent first. The second only ever answers for an output a consumer
2783 * holds across frames -- the backbuffer -- where the first is refused by the guard inside
2784 * because it is the cacheline currently on screen. Anything nobody holds is served by the
2785 * first and never reaches the second, so no module keeps a cacheline alive on its account. */
2786 cache_entry = _cache_try_rekey_reuse_locked(cache, hash, size, reuse_hint);
2787 if(IS_NULL_PTR(cache_entry))
2788 cache_entry = _cache_try_rekey_reuse_locked(cache, hash, size, reuse_hint_prev);
2789 if(!IS_NULL_PTR(cache_entry))
2790 {
2792 if(alloc && IS_NULL_PTR(cache_entry->data)) dt_pixel_cache_alloc(cache_entry);
2793 _pixelpipe_cache_finalize_entry(cache_entry, data, "writable-rekeyed");
2794 if(entry) *entry = cache_entry;
2796 }
2797 }
2798
2799 cache_entry = _pixelpipe_cache_create_entry_locked(cache, hash, size, name, id);
2800 if(IS_NULL_PTR(cache_entry))
2801 {
2803 if(data) *data = NULL;
2804 if(entry) *entry = NULL;
2806 }
2807
2809
2810 if(alloc) dt_pixel_cache_alloc(cache_entry);
2811 _pixelpipe_cache_finalize_entry(cache_entry, data, "writable-created");
2812 if(entry) *entry = cache_entry;
2814}
2815
2817 void **data)
2818{
2819 dt_pthread_mutex_lock(&cache->lock);
2820 cache->queries++;
2822
2823 const gboolean hit = !IS_NULL_PTR(cache_entry);
2824 const size_t hit_size = hit ? cache_entry->size : 0;
2825 if(hit)
2826 {
2827 cache->hits++;
2828 cache_entry->hits++;
2829 _pixelpipe_cache_finalize_entry(cache_entry, data, "found");
2830 }
2831
2833
2834 if(hit ) _observe_read(hash, hit_size);
2835
2836 return cache_entry;
2837}
2838
2839#ifdef HAVE_OPENCL
2841 const int preferred_devid, void **cl_mem_output)
2842{
2843 if(IS_NULL_PTR(cache_entry) || IS_NULL_PTR(cl_mem_output) || !IS_NULL_PTR(*cl_mem_output) || preferred_devid < 0)
2844 return FALSE;
2845
2846 dt_pthread_mutex_lock(&cache_entry->cl_mem_lock);
2847 for(GList *l = cache_entry->cl_mem_list; l;)
2848 {
2849 GList *next = g_list_next(l);
2850 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
2851 if(!IS_NULL_PTR(c->mem) && c->refs == 0
2852 && dt_opencl_get_mem_context_id((cl_mem)c->mem) == preferred_devid)
2853 {
2854 cache_entry->cl_mem_list = g_list_delete_link(cache_entry->cl_mem_list, l);
2855 *cl_mem_output = c->mem;
2856 dt_free(c);
2857 _pixel_cache_touch(cache_entry);
2858 break;
2859 }
2860 l = next;
2861 }
2862 dt_pthread_mutex_unlock(&cache_entry->cl_mem_lock);
2863
2864 return !IS_NULL_PTR(*cl_mem_output);
2865}
2866#else
2868 const int preferred_devid, void **cl_mem_output)
2869{
2870 return FALSE;
2871}
2872#endif
2873
2875 const int preferred_devid, void **data)
2876{
2878 if(data) *data = NULL;
2879 if(IS_NULL_PTR(cache) || IS_NULL_PTR(cache_entry)) return FALSE;
2880
2881 if(dt_pixel_cache_entry_get_data(cache_entry) != NULL)
2882 {
2883 _pixel_cache_touch(cache_entry);
2884 if(!IS_NULL_PTR(data)) *data = dt_pixel_cache_entry_get_data(cache_entry);
2885 return TRUE;
2886 }
2887
2888 if(!_cache_entry_materialize_host_data(cache, preferred_devid, cache_entry))
2889 return FALSE;
2890
2891 _pixel_cache_touch(cache_entry);
2892 if(!IS_NULL_PTR(data)) *data = dt_pixel_cache_entry_get_data(cache_entry);
2893 return dt_pixel_cache_entry_get_data(cache_entry) != NULL;
2894}
2895
2896gboolean dt_dev_pixelpipe_cache_peek(const uint64_t hash, void **data,
2897 dt_pixel_cache_entry_t **entry, const int preferred_devid,
2898 void **cl_mem_output)
2899{
2901 if(data) *data = NULL;
2902 if(entry) *entry = NULL;
2903 if(cl_mem_output) *cl_mem_output = NULL;
2904
2906 return FALSE;
2907
2908 dt_pixel_cache_entry_t *cache_entry = _cache_lookup_existing(cache, hash, data);
2909 if(IS_NULL_PTR(cache_entry)) return FALSE;
2910
2911 if(data) *data = dt_pixel_cache_entry_get_data(cache_entry);
2912
2913 /* Exact-hit callers treat the returned payload as already published. Reject
2914 * cachelines that are still write-locked: reusable output cachelines are
2915 * rekeyed to their new hash before recompute starts, so exposing them here
2916 * would let concurrent pipes consume stale or half-written buffers. */
2917 if(dt_pthread_rwlock_tryrdlock(&cache_entry->lock) != 0)
2918 {
2919 _trace_exact_hit("locked", hash, cache_entry, data ? *data : NULL,
2920 cl_mem_output ? *cl_mem_output : NULL, preferred_devid, FALSE);
2921 if(data) *data = NULL;
2922 return FALSE;
2923 }
2924 dt_pthread_rwlock_unlock(&cache_entry->lock);
2925
2926 if(IS_NULL_PTR(data) && IS_NULL_PTR(cl_mem_output))
2927 {
2928 if(entry) *entry = cache_entry;
2929 return TRUE;
2930 }
2931
2932 /* Picker-triggered aborts can leave a cacheline temporarily present under its
2933 * hash while it is already marked auto-destroy. Those entries must never exact-hit:
2934 * they belong to the aborted lifecycle and must force a rebuild on the next run. */
2935 if(cache_entry->auto_destroy)
2936 {
2937 _trace_exact_hit("auto-destroy", hash, cache_entry, data ? *data : NULL,
2938 cl_mem_output ? *cl_mem_output : NULL, preferred_devid, FALSE);
2939 if(data) *data = NULL;
2940 return FALSE;
2941 }
2942
2943 if(dt_pixel_cache_entry_get_data(cache_entry) != NULL)
2944 {
2945 if(data) *data = dt_pixel_cache_entry_get_data(cache_entry);
2946 _cache_try_restore_device_payload(cache_entry, preferred_devid, cl_mem_output);
2947
2948 _trace_exact_hit("host", hash, cache_entry, data ? *data : NULL,
2949 cl_mem_output ? *cl_mem_output : NULL, preferred_devid, FALSE);
2950 if(entry) *entry = cache_entry;
2951 return TRUE;
2952 }
2953
2954 /* `preferred_devid < 0` means the caller is on a CPU path and does not own any
2955 * OpenCL device. In that case, hostless cachelines are not consumable here:
2956 * reopening device-only payloads would enqueue hidden GPU work without a locked
2957 * device, while reporting a device-only exact-hit would let CPU callers sample
2958 * an uninitialized host buffer. */
2959 if(preferred_devid < 0)
2960 {
2961 _trace_exact_hit("cpu-no-device", hash, cache_entry, NULL, NULL, preferred_devid, FALSE);
2962 return FALSE;
2963 }
2964
2965 if(_cache_try_restore_device_payload(cache_entry, preferred_devid, cl_mem_output))
2966 {
2967 _trace_exact_hit("device", hash, cache_entry, data ? *data : NULL,
2968 cl_mem_output ? *cl_mem_output : NULL, preferred_devid, FALSE);
2969 if(entry) *entry = cache_entry;
2970 return TRUE;
2971 }
2972
2973 if(!IS_NULL_PTR(data) && dt_dev_pixelpipe_cache_restore_host_payload(cache_entry, preferred_devid, data))
2974 {
2975 _trace_exact_hit("restore-host", hash, cache_entry, data ? *data : NULL,
2976 cl_mem_output ? *cl_mem_output : NULL, preferred_devid, FALSE);
2977 if(entry) *entry = cache_entry;
2978 return TRUE;
2979 }
2980
2981 _trace_exact_hit("drop-invalid", hash, cache_entry, data ? *data : NULL,
2982 cl_mem_output ? *cl_mem_output : NULL, preferred_devid, FALSE);
2984 "[pixelpipe] cache entry %" PRIu64 " has no authoritative RAM nor vRAM payload and will be removed\n",
2985 hash);
2986 /* Removed, or flagged for its last holder to remove, in one hold of the lock, and looked up again
2987 * there: a peek holds no reference that would keep `cache_entry' alive until then. */
2988 dt_pthread_mutex_lock(&cache->lock);
2990 if(invalid == cache_entry && _non_thread_safe_cache_remove(cache, TRUE, invalid, cache->entries))
2991 invalid->auto_destroy = TRUE;
2993 if(data) *data = NULL;
2994 return FALSE;
2995}
2996
2997
2998static gboolean _for_each_remove(gpointer key, gpointer value, gpointer user_data)
2999{
3001 const int id = GPOINTER_TO_INT(user_data);
3002
3003 // Returns 1 if the lock is captured by another thread
3004 // 0 if WE capture the lock, and then need to release it
3005 gboolean locked = dt_pthread_rwlock_trywrlock(&cache_entry->lock);
3006 if(!locked) dt_pthread_rwlock_unlock(&cache_entry->lock);
3007 gboolean used = dt_atomic_get_int(&cache_entry->refcount) > 0;
3008
3009 /* The lock alone does not tell whether somebody holds the line: a backbuffer keepalive, or a
3010 * module keeping its preview across runs, holds a reference and no lock. Freeing that line hands
3011 * its pixels back to the arena while the holder still reads them, and its eventual release then
3012 * decrements freed memory. Same guard as _non_thread_safe_cache_remove(). */
3013 return (cache_entry->id == id || id == -1) && !used && !locked;
3014}
3015
3016
3018{
3020 dt_pthread_mutex_lock(&cache->lock);
3021 g_hash_table_foreach_remove(cache->entries, _for_each_remove, GINT_TO_POINTER(id));
3023}
3024
3026 const size_t count)
3027{
3029 int retained = 0;
3030 dt_pthread_mutex_lock(&cache->lock);
3031
3032 // We are invalidating the cumulative outputs from one pipeline stage onward.
3033 // Look them up under the same cache lock used for removal so a shared preview
3034 // pipe cannot replace an entry between lookup and invalidation.
3035 for(size_t k = 0; k < count; k++)
3036 {
3037 if(hashes[k] == DT_PIXELPIPE_CACHE_HASH_INVALID) continue;
3038
3040 = _non_threadsafe_cache_get_entry(cache, cache->entries, hashes[k]);
3041 if(IS_NULL_PTR(entry)) continue;
3042
3043 // A displayed backbuffer or an in-flight consumer may still own this
3044 // shared state. Leave it valid; cache bypass on the retry still walks
3045 // through downstream stages after the provider has been regenerated.
3046 if(_non_thread_safe_cache_remove(cache, FALSE, entry, cache->entries))
3047 retained++;
3048 }
3049
3051 return retained;
3052}
3053
3054
3055// Find the age of the most recently used entry in the table.
3056static void _cache_get_newest(gpointer key, gpointer value, gpointer user_data)
3057{
3059 int64_t *newest = (int64_t *)user_data;
3060 const int64_t age = _pixel_cache_get_age(cache_entry);
3061 if(age > *newest) *newest = age;
3062}
3063
3064/* `user_data` is the age cutoff computed by dt_dev_pixelpipe_cache_flush_old(): entries last used
3065 * before it are candidates for collection. */
3066static gboolean _for_each_remove_old(gpointer key, gpointer value, gpointer user_data)
3067{
3069 const int64_t cutoff = *(const int64_t *)user_data;
3070
3071 // Returns 1 if the lock is captured by another thread
3072 // 0 if WE capture the lock, and then need to release it
3073 gboolean locked = dt_pthread_rwlock_trywrlock(&cache_entry->lock);
3074 if(!locked) dt_pthread_rwlock_unlock(&cache_entry->lock);
3075 gboolean used = dt_atomic_get_int(&cache_entry->refcount) > 0;
3076
3077 /* Two independent signals, and both must agree before an entry is dropped: it was last used
3078 * before the cutoff, i.e. the cache has moved on from it, and `hits` says it was never
3079 * asynchronously reused either, so a cacheline that has repeatedly proven reusable across runs
3080 * is kept for the next one. */
3081 const gboolean too_old = (_pixel_cache_get_age(cache_entry) < cutoff) && (cache_entry->hits < 4);
3082
3083 return too_old && !used && !locked;
3084}
3085
3087{
3088 // Don't hang the GUI thread if the cache is locked by a pipeline.
3089 // Better luck next time.
3090 if(dt_pthread_mutex_trylock(&cache->lock)) return G_SOURCE_CONTINUE;
3091
3092 /* Age entries against the cache's OWN most recent activity, not against wall-clock now.
3093 *
3094 * "Older than now - 5 min" measures how long the user has been away from the application, which
3095 * is not what this sweep is about: leave the darkroom open over a coffee break and it wipes a
3096 * perfectly warm working set the next interaction would have reused, paying a full recompute for
3097 * a delay the cache had no say in. Anchoring on the newest cacheline instead makes the window
3098 * relative to the PIPELINE's activity: "the cache has moved on by more than 10 minutes of work
3099 * since this entry was last needed". An idle cache's newest entry ages alongside everything
3100 * else, so the whole set stays inside the window and nothing is dropped -- the sweep only bites
3101 * while something is actively producing newer cachelines, which is exactly when the memory is
3102 * worth reclaiming.
3103 *
3104 * Both passes run under the same lock hold, so the cutoff describes the table it is applied to. */
3105 int64_t newest = INT64_MIN;
3106 g_hash_table_foreach(cache->entries, _cache_get_newest, &newest);
3107 if(newest != INT64_MIN)
3108 {
3109 // 10 min in microseconds
3110 const int64_t ten_min = 10 * 60 * 1000 * 1000;
3111 int64_t cutoff = newest - ten_min;
3112 g_hash_table_foreach_remove(cache->entries, _for_each_remove_old, &cutoff);
3113 }
3114
3116
3117 // Hand free pages back to the OS while we're at it, but only when the system
3118 // actually runs lowish: the per-free MADV_FREE already makes them reclaimable
3119 // (they count as available), and a hard trim of a large resident free set is
3120 // O(resident pages) under the arena lock — a needless stall of this (GUI)
3121 // thread when RAM is plentiful.
3122 const size_t available = dt_get_system_available_mem();
3123 const size_t pressure_floor = dt_get_memory_pressure_floor();
3124 if(available > 0 && pressure_floor > 0 && available < 2 * pressure_floor)
3125 dt_cache_arena_trim(&cache->arena);
3126 return G_SOURCE_CONTINUE;
3127}
3128
3129/* Periodic system memory-pressure shedder (issue #1083), the idle-time counterpart of
3130 * the alloc-time _system_memory_pressure_valve(): when OTHER applications push the
3131 * system toward starvation while we are not allocating anything, nobody runs the valve,
3132 * so watch the system-wide available RAM here and shed cache below the floor. The
3133 * hard trim matters: the per-free lazy release (MADV_FREE) technically keeps the pages
3134 * ours until the kernel scavenges them, while decommitting hands them over NOW. */
3136{
3137 // Kernel memory pressure first. A pipeline holding the lock is allocating, and allocations
3138 // react to it themselves (_free_space_to_alloc()).
3139 if(!dt_pthread_mutex_trylock(&cache->lock))
3140 {
3143 }
3144
3145 const size_t pressure_floor = dt_get_memory_pressure_floor();
3146 if(pressure_floor == 0) return G_SOURCE_CONTINUE;
3147
3148 const size_t available = dt_get_system_available_mem();
3149 if(available == 0 || available >= pressure_floor) return G_SOURCE_CONTINUE;
3150
3151 // Don't hang the GUI thread if the cache is locked by a pipeline.
3152 // The valve covers pressure handling while pipelines are allocating anyway.
3153 if(dt_pthread_mutex_trylock(&cache->lock)) return G_SOURCE_CONTINUE;
3154
3155 const size_t deficit = pressure_floor - available;
3156 size_t freed = 0;
3157 // Host RAM, as in the valve: only lines with a host buffer give any back.
3158 while(freed < deficit && g_hash_table_size(cache->entries) > 0)
3159 {
3160 const size_t before = cache->current_memory;
3162 freed += before - cache->current_memory;
3163 }
3164
3165 // Same reasoning as in the valve: hand the freed pages over for real, then let the
3166 // OS — not our own bookkeeping — say what that bought, and seed the valve's probe
3167 // cache with that ground truth.
3168 const size_t trimmed = dt_cache_arena_trim(&cache->arena);
3170 const size_t available_after = dt_get_system_available_mem();
3171 cache->sys_probe_valid = (available_after > 0);
3172 cache->sys_available_est = available_after;
3173 cache->sys_probe_time_us = g_get_monotonic_time();
3175
3177 "[pixelpipe_cache] system memory pressure while idle: %" G_GSIZE_FORMAT " MiB available "
3178 "under the %" G_GSIZE_FORMAT " MiB floor — shed %" G_GSIZE_FORMAT " MiB of cache, "
3179 "returned %" G_GSIZE_FORMAT " MiB to the OS, now %" G_GSIZE_FORMAT " MiB available\n",
3180 available / (1024 * 1024), pressure_floor / (1024 * 1024), freed / (1024 * 1024),
3181 trimmed / (1024 * 1024), available_after / (1024 * 1024));
3182 return G_SOURCE_CONTINUE;
3183}
3184
3190
3191
3193 dt_pixel_cache_entry_t *cache_entry)
3194{
3195 if(IS_NULL_PTR(cache_entry)) return;
3196
3197 if(lock)
3198 {
3199 dt_atomic_add_int(&cache_entry->refcount, 1);
3200 // Pinning an entry is a use: refresh the MRU timestamp so it does not age while held.
3201 _pixel_cache_touch(cache_entry);
3202 _pixel_cache_message(cache_entry, "ref count ++", TRUE);
3203 }
3204 else
3205 {
3206 dt_atomic_sub_int(&cache_entry->refcount, 1);
3207 _pixel_cache_message(cache_entry, "ref count --", TRUE);
3208 }
3209}
3210
3211
3213 dt_pixel_cache_entry_t *cache_entry)
3214{
3216 dt_pthread_mutex_lock(&cache->lock);
3217 _non_thread_safe_cache_ref_count_entry(cache, lock, cache_entry);
3218
3219 /* A disposable entry goes with its last reference, in this same hold of the lock: once the lock
3220 * is released, another thread's eviction may free it, so no later call could name it. Another
3221 * holder keeps it until the last release; a lock still on it leaves it to the LRU. */
3222 if(!lock && !IS_NULL_PTR(cache_entry) && cache_entry->auto_destroy)
3223 {
3224 const uint64_t hash = cache_entry->hash;
3225 if(_non_thread_safe_cache_remove(cache, FALSE, cache_entry, cache->entries) == 0)
3226 _cache_print(DT_DEBUG_PIPECACHE, "[pixelpipe] cache entry %" PRIu64 " removed on its last release\n",
3227 hash);
3228 }
3229
3231}
3232
3233
3235 dt_pixel_cache_entry_t *cache_entry)
3236{
3237 if(lock)
3238 {
3239 dt_pthread_rwlock_wrlock(&cache_entry->lock);
3240 _pixel_cache_message(cache_entry, "write lock", TRUE);
3241 }
3242 else
3243 {
3244 dt_pthread_rwlock_unlock(&cache_entry->lock);
3245 _pixel_cache_message(cache_entry, "write unlock", TRUE);
3246 // The producer node key travels alongside the hash so GUI waiters can match by
3247 // the node that produced this output even when the exact output hash the GUI
3248 // predicted has drifted from the one the worker actually published (the
3249 // never-served case, doc/pipeline-cache.md §8). INVALID for non-module outputs
3250 // (raster masks, republished inputs): waiters simply fall back to hash match.
3251 if(cache_entry && cache_entry->hash != DT_PIXELPIPE_CACHE_HASH_INVALID && _ready_handler)
3252 _ready_handler(cache_entry->hash, cache_entry->producer_node_key);
3253 }
3254}
3255
3256
3258 dt_pixel_cache_entry_t *cache_entry)
3259{
3260 if(lock)
3261 {
3262 dt_pthread_rwlock_rdlock(&cache_entry->lock);
3263 _pixel_cache_message(cache_entry, "read lock", TRUE);
3264 }
3265 else
3266 {
3267 dt_pthread_rwlock_unlock(&cache_entry->lock);
3268 _pixel_cache_message(cache_entry, "read unlock", TRUE);
3269 }
3270}
3271
3272
3274{
3276 dt_pthread_mutex_lock(&cache->lock);
3277 if(IS_NULL_PTR(cache_entry))
3278 {
3280 return;
3281 }
3282
3283 cache_entry->auto_destroy = TRUE;
3284 _pixel_cache_message(cache_entry, "auto destroy flagged", TRUE);
3286}
3287
3288
3290{
3291 /* No cache lock and no lookup: the caller names the entry it referenced, and holding a
3292 * reference is what guarantees that object is still there to be named. */
3293 if(IS_NULL_PTR(entry)) return;
3295}
3296
3298{
3301
3302 dt_pthread_mutex_lock(&cache->lock);
3303 cache->queries++;
3306
3307 if(cache_entry)
3309}
3310
3312 const uint64_t new_hash, dt_pixel_cache_entry_t *entry)
3313{
3315 if(IS_NULL_PTR(cache)) return 1;
3316 if(old_hash == new_hash) return 0;
3317
3318 dt_pthread_mutex_lock(&cache->lock);
3319
3320 if(IS_NULL_PTR(entry)) entry = _non_threadsafe_cache_get_entry(cache, cache->entries, old_hash);
3321 if(IS_NULL_PTR(entry))
3322 {
3324 "[pixelpipe_cache] rekey miss old=%" PRIu64 " new=%" PRIu64 " module=%s\n",
3325 old_hash, new_hash, _cache_debug_module_name());
3327 return 1;
3328 }
3329
3330 dt_pixel_cache_entry_t *conflict = _non_threadsafe_cache_get_entry(cache, cache->entries, new_hash);
3331 if(conflict && conflict != entry)
3332 {
3334 "[pixelpipe_cache] rekey conflict old=%" PRIu64 " new=%" PRIu64
3335 " entry=%" PRIu64 "/%" PRIu64 " conflict=%" PRIu64 "/%" PRIu64 " module=%s\n",
3336 old_hash, new_hash, entry->hash, entry->serial, conflict->hash, conflict->serial,
3339 return 1;
3340 }
3341
3342 gpointer stolen_key = NULL;
3343 gpointer stolen_value = NULL;
3344 if(!g_hash_table_steal_extended(cache->entries, &old_hash, &stolen_key, &stolen_value))
3345 {
3347 "[pixelpipe_cache] rekey steal-miss old=%" PRIu64 " new=%" PRIu64
3348 " entry=%" PRIu64 "/%" PRIu64 " module=%s\n",
3349 old_hash, new_hash, entry->hash, entry->serial, _cache_debug_module_name());
3351 return 1;
3352 }
3353
3354 if(stolen_value != entry)
3355 {
3357 "[pixelpipe_cache] rekey stolen-entry mismatch old=%" PRIu64 " new=%" PRIu64
3358 " expected=%" PRIu64 "/%" PRIu64 " got=%" PRIu64 "/%" PRIu64 " module=%s\n",
3359 old_hash, new_hash, entry->hash, entry->serial,
3360 ((dt_pixel_cache_entry_t *)stolen_value)->hash, ((dt_pixel_cache_entry_t *)stolen_value)->serial,
3362 g_hash_table_insert(cache->entries, stolen_key, stolen_value);
3364 return 1;
3365 }
3366
3367 /* Explicit rekeying also changes cacheline ownership. The OpenCL payload cache is only valid for the
3368 * previous hash, so do not let the new hash inherit stale device-side state. If some GPU code is still
3369 * borrowing one of these payloads, refuse the rekey instead of publishing an ambiguous cache entry. */
3371 for(GList *l = entry->cl_mem_list; l; l = g_list_next(l))
3372 {
3373 dt_cache_clmem_t *c = (dt_cache_clmem_t *)l->data;
3374 if(c && c->refs > 0)
3375 {
3377 g_hash_table_insert(cache->entries, stolen_key, stolen_value);
3379 return 1;
3380 }
3381 }
3384
3385 *(uint64_t *)stolen_key = new_hash;
3386 entry->hash = new_hash;
3387 g_hash_table_insert(cache->entries, stolen_key, stolen_value);
3389 "[pixelpipe_cache] rekey old=%" PRIu64 " new=%" PRIu64 " entry=%" PRIu64 "/%" PRIu64
3390 " refs=%i auto=%i data=%p module=%s\n",
3391 old_hash, new_hash, entry->hash, entry->serial, dt_atomic_get_int(&entry->refcount),
3392 entry->auto_destroy, entry->data, _cache_debug_module_name());
3393
3395
3396 _observe_rekey(old_hash, new_hash);
3397 return 0;
3398}
3399
3400
3402{
3404 if(!_verbose) return;
3405
3406 _cache_print(DT_DEBUG_PIPECACHE, "[pixelpipe] cache hit rate so far: %.3f%% - size: %" G_GSIZE_FORMAT " MiB over %" G_GSIZE_FORMAT " MiB - %i items\n",
3407 100. * (cache->hits) / (float)cache->queries, cache->current_memory / (1024 * 1024),
3408 cache->max_memory / (1024 * 1024),
3409 g_hash_table_size(cache->entries));
3410}
3411
3412void dt_dev_pixelpipe_cache_get_usage(size_t *current, size_t *max)
3413{
3415 if(current) *current = 0;
3416 if(max) *max = 0;
3417 if(IS_NULL_PTR(cache)) return;
3418 dt_pthread_mutex_lock(&cache->lock);
3419 if(current) *current = cache->current_memory;
3420 // The budget allocations currently evict down to -- never under what is held, since the
3421 // callers compute `max - current` unsigned. Tiling plans against dt_dev_pixelpipe_cache_get_available().
3424}
3425
3427{
3429 size_t evictable[2] = { 0, 0 };
3430 dt_pthread_mutex_lock(&cache->lock);
3431 g_hash_table_foreach(cache->entries, _cache_sum_evictable, evictable);
3432 // The budget an allocation is refused on is the plan: under kernel pressure the lowered one is
3433 // only what eviction aims for (_free_space_to_alloc()).
3434 const size_t kept = cache->current_memory - evictable[0];
3435 const size_t budget_room = (cache->max_memory > kept) ? cache->max_memory - kept : 0;
3437
3438 // What the valve lets through: the system's available RAM over the floor, plus what evicting
3439 // hands back to the OS (_system_memory_pressure_valve()).
3440 const size_t sys_available = dt_get_system_available_mem();
3441 if(sys_available == 0) return budget_room; // no information on this platform
3442
3443 const size_t pressure_floor = dt_get_memory_pressure_floor();
3444 const size_t sys_room
3445 = ((sys_available > pressure_floor) ? sys_available - pressure_floor : 0) + evictable[1];
3446 return MIN(budget_room, sys_room);
3447}
3448
3450{
3451 // Written once, by dt_dev_pixelpipe_cache_init().
3453 return IS_NULL_PTR(cache) ? 0 : cache->max_memory;
3454}
3455
3456static void _stats_entry_clear(gpointer data)
3457{
3459 g_free(s->devices);
3460}
3461
3462// WARNING: not thread-safe, protect its calls with mutex lock
3463static void _non_threadsafe_stats_append(GArray *out, GHashTable *table, const gboolean allocations,
3464 const int64_t now, const int num_devices)
3465{
3466 GHashTableIter it;
3467 gpointer key, value;
3468 g_hash_table_iter_init(&it, table);
3469 while(g_hash_table_iter_next(&it, &key, &value))
3470 {
3472 if(IS_NULL_PTR(e)) continue;
3474 s.hash = e->hash;
3475 s.size = e->size;
3476 s.allocation = allocations;
3477 // Touched from paths holding no cache lock: it may be newer than `now`.
3478 s.age_us = MAX(now - _pixel_cache_get_age(e), 0);
3479 // A line an OpenCL module keeps on the device is created without a host buffer: its size is
3480 // only reserved against the budget.
3481 s.host_bytes = IS_NULL_PTR(e->data) ? 0 : e->size;
3484 s.hits = e->hits;
3485 /* An allocation is named after the `__FILE__:__LINE__` that made it: an absolute path, whose
3486 * root says nothing and would cut the line number off a name this short. */
3487 const char *src = (allocations && e->name) ? g_strrstr(e->name, "/src/") : NULL;
3488 if(e->name) g_strlcpy(s.name, src ? src + 5 : e->name, sizeof(s.name));
3489
3490#ifdef HAVE_OPENCL
3491 // Account the OpenCL device buffers attached to this entry. trylock avoids
3492 // both deadlock and racing the list against concurrent clmem mutation.
3493 if(num_devices > 0) s.devices = g_new0(dt_pixel_cache_stats_device_t, num_devices);
3495 {
3496 for(GList *l = e->cl_mem_list; l; l = g_list_next(l))
3497 {
3498 const dt_cache_clmem_t *c = (const dt_cache_clmem_t *)l->data;
3499 if(IS_NULL_PTR(c) || IS_NULL_PTR(c->mem)) continue;
3500 /* The device and size recorded at allocation, as the release path reads them: some
3501 * drivers fault in clGetMemObjectInfo under vRAM pressure. The driver is asked only about
3502 * an object nothing recorded. */
3503 int devid = -1;
3504 size_t bytes = 0;
3505 if(!dt_opencl_get_mem_record((cl_mem)c->mem, &devid, &bytes))
3506 {
3507 devid = dt_opencl_get_mem_context_id((cl_mem)c->mem);
3508 bytes = dt_opencl_get_mem_object_size((cl_mem)c->mem);
3509 }
3510 s.cl_count++;
3511 s.cl_bytes += bytes;
3512 if(devid >= 0 && devid < num_devices)
3513 {
3514 s.devices[devid].cl_count++;
3515 s.devices[devid].cl_bytes += bytes;
3516 }
3517 }
3519 }
3520#endif
3521
3522 g_array_append_val(out, s);
3523 }
3524}
3525
3527{
3529 GArray *out = g_array_new(FALSE, FALSE, sizeof(dt_pixel_cache_stats_entry_t));
3530 g_array_set_clear_func(out, _stats_entry_clear);
3531 if(IS_NULL_PTR(cache)) return out;
3532 const int num_devices = dt_opencl_get_num_devices();
3533
3534 const int64_t now = g_get_monotonic_time();
3535 dt_pthread_mutex_lock(&cache->lock);
3536 _non_threadsafe_stats_append(out, cache->entries, FALSE, now, num_devices);
3537 _non_threadsafe_stats_append(out, cache->external_entries, TRUE, now, num_devices);
3539 return out;
3540}
3541
3542
3543// clang-format off
3544// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
3545// vim: shiftwidth=2 expandtab tabstop=2 cindent
3546// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
3547// clang-format on
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void dt_atomic_set_int(dt_atomic_int *var, int value)
int dt_atomic_get_int(dt_atomic_int *var)
uint64_t dt_atomic_get_uint64(const dt_atomic_uint64 *var)
int dt_atomic_sub_int(dt_atomic_int *var, int decr)
int dt_atomic_add_int(dt_atomic_int *var, int incr)
void dt_atomic_set_uint64(dt_atomic_uint64 *var, uint64_t value)
atomic_int dt_atomic_int
Definition atomic.h:68
typedef void((*dt_cache_allocate_t)(void *userdata, dt_cache_entry_t *entry))
const float max
const dt_colormatrix_t dt_aligned_pixel_t out
void dt_invalidate_system_available_mem(void)
Definition darktable.c:2367
size_t dt_get_system_available_mem(void)
Definition darktable.c:2348
size_t dt_get_memory_pressure_floor(void)
Definition darktable.c:2427
static int dt_pthread_rwlock_wrlock(dt_pthread_rwlock_t *rwlock) ACQUIRE(rwlock) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:299
static int dt_pthread_rwlock_trywrlock(dt_pthread_rwlock_t *rwlock) TRY_ACQUIRE(0
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_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:127
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_rwlock_tryrdlock(dt_pthread_rwlock_t *rwlock) TRY_ACQUIRE_SHARED(0
static int dt_pthread_mutex_trylock(dt_pthread_mutex_t *mutex) TRY_ACQUIRE(0
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
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:117
int bpp
static const dt_dev_pixelpipe_t * pipe
Definition layers.h:49
hash
Definition layers.h:90
@ DT_DEBUG_OPENCL
Definition logging.h:57
@ DT_DEBUG_PIPECACHE
Definition logging.h:56
@ DT_DEBUG_MEMORY
Definition logging.h:59
void dt_print(dt_debug_thread_t thread, const char *msg,...) __attribute__((format(printf
Print to stdout when thread is enabled, prefixed with seconds since startup.
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
static void dt_free_gpointer(gpointer ptr)
g_free() one pointer, with the signature GDestroyNotify wants.
Definition mem_alloc.h:184
#define dt_free(ptr)
g_free() ptr and set it to NULL, skipping both if it is already NULL.
Definition mem_alloc.h:171
#define DT_CACHELINE_BYTES
Definition mem_alloc.h:66
void dt_cache_arena_stats(dt_cache_arena_t *a, uint32_t *out_total_free_pages, uint32_t *out_largest_free_run_pages)
void dt_cache_arena_cleanup(dt_cache_arena_t *a)
gboolean dt_cache_arena_calc(const dt_cache_arena_t *a, size_t size, uint32_t *out_pages, size_t *out_size)
int dt_cache_arena_init(dt_cache_arena_t *a, size_t total_size)
void dt_cache_arena_free(dt_cache_arena_t *a, void *ptr, size_t size)
void * dt_cache_arena_alloc(dt_cache_arena_t *a, size_t size, size_t *out_size)
size_t dt_cache_arena_trim(dt_cache_arena_t *a)
char * key
uint32_t width
Definition mipmap_cache.c:0
uint32_t height
Definition mipmap_cache.c:1
size_t size
Definition mipmap_cache.c:3
dt_mipmap_buffer_dsc_flags flags
Definition mipmap_cache.c:4
void * dt_opencl_alloc_device_use_host_pointer(const int devid, const int width, const int height, const int bpp, void *host, const int flags)
Definition opencl.c:2975
size_t dt_opencl_get_mem_object_size(cl_mem mem)
Definition opencl.c:3034
gboolean dt_opencl_is_pinned_memory(cl_mem mem)
Definition opencl.c:238
void * dt_opencl_alloc_device(const int devid, const int width, const int height, const int bpp)
Definition opencl.c:2952
int dt_opencl_is_inited(void)
Definition opencl.c:3328
int dt_opencl_get_mem_context_id(cl_mem mem)
Definition opencl.c:3044
int dt_opencl_get_image_height(cl_mem mem)
Definition opencl.c:3082
void dt_opencl_reserve_device_by_id(const int devid)
Reserve a device the caller has already identified, blocking until it is free.
Definition opencl.c:2024
int dt_opencl_try_reserve_device_by_id(const int devid)
Reserve a device only if it is free right now.
Definition opencl.c:2035
int dt_opencl_unmap_mem_object(const int devid, cl_mem mem_object, void *mapped_ptr)
Definition opencl.c:2910
gboolean dt_opencl_use_pinned_memory(const int devid)
Definition opencl.c:231
int dt_opencl_get_image_width(cl_mem mem)
Definition opencl.c:3071
void * dt_opencl_map_image(const int devid, cl_mem buffer, const int blocking, const int flags, size_t width, size_t height, int bpp)
Definition opencl.c:2890
gboolean dt_opencl_finish(const int devid)
Definition opencl.c:1712
void dt_opencl_events_wait_for(const int devid)
Definition opencl.c:3527
int dt_opencl_read_host_from_device(const int devid, void *host, void *device, const int width, const int height, const int bpp)
Definition opencl.c:2645
void dt_opencl_release_device(const int devid)
Release a device reserved by either reserve function.
Definition opencl.c:2013
void dt_opencl_release_mem_object(cl_mem mem)
Definition opencl.c:2863
int dt_opencl_get_image_element_size(cl_mem mem)
Definition opencl.c:3093
int dt_opencl_get_num_devices(void)
Number of usable OpenCL devices; 0 when OpenCL is unavailable.
Definition opencl.c:2043
gboolean dt_opencl_get_mem_record(cl_mem mem, int *devid, size_t *size)
Definition opencl.c:3171
int dt_opencl_write_host_to_device(const int devid, void *host, void *device, const int width, const int height, const int bpp)
Definition opencl.c:2692
#define DT_OPENCL_BPP_ENCODE_RGBA8(bpp)
Definition opencl.h:89
const char * name
Definition pdf.h:90
static __thread const char * dt_pixelpipe_cache_current_module
void dt_dev_pixelpipe_cache_wrlock_entry(gboolean lock, dt_pixel_cache_entry_t *cache_entry)
Lock or release the write lock on the entry.
static const char * _cache_debug_module_name(void)
static size_t _pressure_held(void *user)
static void _trace_exact_hit(const char *phase, const uint64_t hash, dt_pixel_cache_entry_t *cache_entry, void *data, void *cl_mem_output, const int preferred_devid, const gboolean verbose)
void * dt_dev_pixelpipe_cache_borrow_cl_payload(dt_pixel_cache_entry_t *entry, int devid, int width, int height, int bpp)
Borrow a cached OpenCL payload attached to a cache entry.
static void _pressure_wake(void *user)
GArray * dt_dev_pixelpipe_cache_get_entries_stats(void)
static void _observe_rekey(uint64_t old_hash, uint64_t new_hash)
static int _free_space_to_alloc(dt_dev_pixelpipe_cache_t *cache, const size_t size, const uint64_t hash, const char *name)
static gboolean _cache_entry_clmem_flush_device(dt_pixel_cache_entry_t *entry, const int devid)
int dt_dev_pixelpipe_cache_invalidate_hashes(const uint64_t *hashes, const size_t count)
Invalidate cache lines matching an explicit list of hashes.
void * dt_dev_pixelpipe_cache_get_pinned_image(void *host_ptr, dt_pixel_cache_entry_t *entry_hint, int devid, int width, int height, int bpp, int flags, gboolean *out_reused)
Acquire a pinned OpenCL image for a host buffer tracked by the pixelpipe cache.
static void _cache_get_newest(gpointer key, gpointer value, gpointer user_data)
static void _arena_stats_bytes(dt_dev_pixelpipe_cache_t *cache, uint32_t *total_pages, uint32_t *largest_pages, size_t *total_bytes, size_t *largest_bytes)
void dt_dev_pixelpipe_cache_unref_entry(dt_pixel_cache_entry_t *entry)
Release one reference held on entry, by POINTER.
const char * dt_pixelpipe_cache_set_current_image(const char *image)
Set the image the current module runs on, for the message that reports a refused allocation (thread-l...
size_t dt_dev_pixelpipe_cache_get_available(void)
static int pressure_shedding
static void _alert_user(const char *message, const char *item)
static void _log_arena_allocation_failure(dt_dev_pixelpipe_cache_t *cache, size_t request_size, const char *entry_name, const char *module, uint64_t hash, gboolean name_is_file, gboolean system_refused)
static __thread uint32_t dt_pixelpipe_cache_alloc_refusals
void dt_dev_pixelpipe_cache_cleanup(void)
Release everything dt_dev_pixelpipe_cache_init() created, the instance included. Only after a success...
void dt_dev_pixelpipe_cache_set_alert_handler(dt_pixelpipe_cache_alert_handler_t alert)
Install the alert handler. The GUI installs it, once it exists, apart from the others: without a GUI ...
static int garbage_collection
void * dt_pixelpipe_cache_alloc_align_cache_impl(size_t size, int id, const char *name)
Allocate aligned memory tracked by the pixelpipe cache. This allows LRU cache entries to be evicted i...
void * dt_pixel_cache_entry_get_data(dt_pixel_cache_entry_t *entry)
Peek the host pixels a cache entry holds for its current hash, without allocating.
static int dt_dev_pixelpipe_cache_flush_old(dt_dev_pixelpipe_cache_t *cache)
static gboolean _verbose
static void _stats_entry_clear(gpointer data)
static void _pixel_cache_clmem_remove(dt_pixel_cache_entry_t *entry, void *mem)
void dt_dev_pixelpipe_cache_ref_count_entry(gboolean lock, dt_pixel_cache_entry_t *cache_entry)
Take or release one reference on a cache line. A referenced line is never evicted,...
gboolean dt_dev_pixelpipe_cache_peek(const uint64_t hash, void **data, dt_pixel_cache_entry_t **entry, const int preferred_devid, void **cl_mem_output)
Non-owning lookup of an existing cache line.
static dt_pixel_cache_entry_t * _cache_entry_for_host_ptr_locked(dt_dev_pixelpipe_cache_t *cache, void *host_ptr)
void dt_dev_pixelpipe_cache_flush_clmem(const int devid)
Release cached OpenCL buffers for a single device.
static void _cache_get_oldest(gpointer key, gpointer value, gpointer user_data)
int dt_dev_pixel_pipe_cache_remove_lru(void)
#define _cache_print(channel,...)
static dt_pixel_cache_entry_t * _pixelpipe_cache_create_entry_locked(dt_dev_pixelpipe_cache_t *cache, const uint64_t hash, const size_t size, const char *name, const int id)
static void _print_cache_lines(gpointer key, gpointer value, gpointer user_data)
static void * _arena_alloc_with_defrag(dt_dev_pixelpipe_cache_t *cache, size_t request_size, size_t *actual_size, gboolean *system_refused)
static int64_t _pixel_cache_get_age(dt_pixel_cache_entry_t *cache_entry)
static void _observe_delete(uint64_t hash, size_t size, int owner_pipe_id, const char *name)
size_t dt_dev_pixelpipe_cache_get_max_memory(void)
static gchar * _cache_full_sentence(const char *name, const char *module)
static gboolean _cache_entry_clmem_has_host_pinned_locked(dt_pixel_cache_entry_t *entry, void *host_ptr, int devid)
static gboolean _for_each_remove(gpointer key, gpointer value, gpointer user_data)
void dt_dev_pixelpipe_cache_rdlock_entry(gboolean lock, dt_pixel_cache_entry_t *cache_entry)
Lock or release the read lock on the entry.
void dt_dev_pixelpipe_cache_flush_clmem_for_pipe(const int devid)
Like dt_dev_pixelpipe_cache_flush_clmem(), for callers that do not hold the device lock (dt_opencl_re...
void dt_dev_pixelpipe_cache_release_cl_buffer(void **cl_mem_buffer, dt_pixel_cache_entry_t *cache_entry, void *host_ptr, const gboolean cache_device)
Release or cache an OpenCL image associated with a host cache line.
static int _non_thread_safe_pixel_pipe_cache_remove_lru(dt_dev_pixelpipe_cache_t *cache, const gboolean host_only)
static gboolean _pressure_alert_expired(gpointer key __attribute__((unused)), gpointer value, gpointer user_data)
void dt_dev_pixelpipe_cache_flush(const int id)
Remove cache lines matching id. Entries locked in read/write or having reference count greater than 0...
static gboolean _entry_evictable(dt_pixel_cache_entry_t *cache_entry, gboolean *used)
void dt_dev_pixelpipe_cache_print(void)
static void _pressure_react_locked(dt_dev_pixelpipe_cache_t *cache)
void dt_dev_pixelpipe_cache_flush_entry_clmem(dt_pixel_cache_entry_t *entry)
Flush all reusable OpenCL payloads cached on one cache entry.
static dt_pixel_cache_entry_t * _cache_try_rekey_reuse_locked(dt_dev_pixelpipe_cache_t *cache, const uint64_t new_hash, const size_t size, const dt_pixel_cache_entry_t *reuse_hint)
size_t dt_pixelpipe_cache_get_largest_free_run(void)
static const dt_pixelpipe_cache_observer_t * _observer
static dt_pixel_cache_entry_t * _cache_lookup_existing(dt_dev_pixelpipe_cache_t *cache, const uint64_t hash, void **data)
void dt_dev_pixelpipe_cache_put_pinned_image(void *host_ptr, dt_pixel_cache_entry_t *entry_hint, void **mem)
Release or cache a pinned OpenCL image acquired with dt_dev_pixelpipe_cache_get_pinned_image().
gboolean dt_dev_pixelpipe_cache_restore_host_payload(dt_pixel_cache_entry_t *cache_entry, const int preferred_devid, void **data)
Materialize a host payload for a live cache entry from its cached device payload.
void * dt_dev_pixelpipe_cache_get_cl_buffer(int devid, void *const host_ptr, const dt_iop_roi_t *roi, const size_t bpp, dt_iop_module_t *module, const char *message, dt_pixel_cache_entry_t *cache_entry, gboolean *out_reused, void *keep)
static int _pixel_cache_clmem_put(dt_pixel_cache_entry_t *entry, void *host_ptr, void *mem)
gboolean dt_dev_pixelpipe_cache_flush_host_pinned_image(void *host_ptr, dt_pixel_cache_entry_t *entry_hint, int devid)
Drop cached pinned OpenCL images associated with a given host buffer.
int dt_dev_pixelpipe_cache_rekey(const uint64_t old_hash, const uint64_t new_hash, dt_pixel_cache_entry_t *entry)
Change the hash/key of an existing cache line in place, without freeing, reallocating or invalidating...
dt_pixel_cache_materialize_source_rank_t
@ DT_PIXEL_CACHE_MATERIALIZE_SOURCE_SECONDARY_PREFERRED
@ DT_PIXEL_CACHE_MATERIALIZE_SOURCE_PRIMARY_ANY
@ DT_PIXEL_CACHE_MATERIALIZE_SOURCE_SECONDARY_ANY
@ DT_PIXEL_CACHE_MATERIALIZE_SOURCE_NONE
@ DT_PIXEL_CACHE_MATERIALIZE_SOURCE_PRIMARY_PREFERRED
dt_pixel_cache_entry_t * dt_dev_pixelpipe_cache_get_entry_by_data(void *data)
static size_t _pixel_cache_get_size(dt_pixel_cache_entry_t *cache_entry)
static dt_pixelpipe_cache_pressure_sink_t _pressure_sink(dt_dev_pixelpipe_cache_t *cache)
void * dt_pixel_cache_alloc(dt_pixel_cache_entry_t *cache_entry)
Actually allocate the memory buffer attached to the cache entry once you create it with dt_dev_pixelp...
static dt_pixelpipe_cache_warn_handler_t _warn_handler
static gboolean _cache_entry_clmem_flush_host_pinned_locked(dt_pixel_cache_entry_t *entry, void *host_ptr, int devid)
static __thread const char * dt_pixelpipe_cache_current_image
static void _free_cache_entry(dt_pixel_cache_entry_t *cache_entry)
void * dt_dev_pixelpipe_cache_alloc_cl_device_buffer(int devid, const dt_iop_roi_t *roi, const size_t bpp, const dt_iop_module_t *module, const char *message, void *keep)
void * dt_pixel_cache_entry_get_buffer(dt_pixel_cache_entry_t *entry)
The host buffer of a cache entry, whatever hash its content describes.
static void _observe_read(uint64_t hash, size_t size)
static gboolean _system_memory_pressure_valve(dt_dev_pixelpipe_cache_t *cache, size_t request_size)
void dt_dev_pixelpipe_cache_get_usage(size_t *current, size_t *max)
int dt_dev_pixelpipe_cache_get(const uint64_t hash, const size_t size, const char *name, const int id, const gboolean alloc, void **data, dt_pixel_cache_entry_t **entry)
Get a cache line from the cache.
dt_pixel_cache_entry_t * dt_dev_pixelpipe_cache_get_entry(const uint64_t hash)
Get an internal reference to the cache entry matching hash. If you are going to access this entry mor...
static gboolean _observed(void)
static void _non_threadsafe_stats_append(GArray *out, GHashTable *table, const gboolean allocations, const int64_t now, const int num_devices)
int dt_dev_pixelpipe_cache_prepare_cl_input(dt_dev_pixelpipe_t *pipe, dt_iop_module_t *module, float *input, void **cl_mem_input, const dt_iop_roi_t *roi_in, const size_t in_bpp, dt_pixel_cache_entry_t *input_entry, dt_pixel_cache_entry_t **locked_input_entry, void *keep)
Prepare/obtain the OpenCL input image for a module.
static gboolean _for_each_remove_old(gpointer key, gpointer value, gpointer user_data)
static int _memory_pressure_shedder(dt_dev_pixelpipe_cache_t *cache)
#define DT_PIXELPIPE_CACHE_PRESSURE_EXEMPT_SIZE
static gboolean _is_gamma_rgba8_output(const dt_iop_module_t *module, const size_t bpp, const char *message)
gboolean dt_dev_pixelpipe_cache_init(size_t max_memory, const gboolean verbose, const gboolean verbose_detail)
Reserve the cache's arena and start it.
static void _pixelpipe_cache_finalize_entry(dt_pixel_cache_entry_t *cache_entry, void **data, const char *message)
int dt_dev_pixelpipe_cache_sync_cl_buffer(const int devid, void *host_ptr, void *cl_mem_buffer, const dt_iop_roi_t *roi, int cl_mode, size_t bpp, dt_iop_module_t *module, const char *message)
Synchronize between host memory and a pinned OpenCL image.
void dt_dev_pixelpipe_cache_return_cl_payload(dt_pixel_cache_entry_t *entry, void *mem)
Return a borrowed cached OpenCL payload to its cache entry.
static gboolean _verbose_detail
static size_t _cache_budget_locked(const dt_dev_pixelpipe_cache_t *cache)
static void _alert_cache_refused(const char *name, const char *module)
static size_t _pressure_shed(void *user, const size_t target, size_t *given_back)
void dt_dev_pixelpipe_cache_unref_hash(const uint64_t hash)
Find the entry matching hash, and decrease its ref_count if found.
static void _cache_sum_evictable(__attribute__((unused)) gpointer key, gpointer value, gpointer user_data)
size_t dt_pixel_cache_entry_get_size(const dt_pixel_cache_entry_t *entry)
void dt_pixelpipe_cache_free_align_cache(void **mem, const char *message)
Free aligned memory allocated with dt_pixelpipe_cache_alloc_align_cache.
int _non_thread_safe_cache_remove(dt_dev_pixelpipe_cache_t *cache, const gboolean force, dt_pixel_cache_entry_t *cache_entry, GHashTable *table)
static gboolean _cache_try_restore_device_payload(dt_pixel_cache_entry_t *cache_entry, const int preferred_devid, void **cl_mem_output)
static void _pixel_cache_message(dt_pixel_cache_entry_t *cache_entry, const char *message, gboolean verbose)
static void * _pixel_cache_clmem_get(dt_pixel_cache_entry_t *entry, void *host_ptr, int devid, int width, int height, int bpp, int flags)
static dt_pixel_cache_entry_t * _non_threadsafe_cache_get_entry(dt_dev_pixelpipe_cache_t *cache, GHashTable *table, const uint64_t key)
void dt_dev_pixelpipe_cache_flag_host_written(dt_pixel_cache_entry_t *entry)
Record that the host buffer of entry now holds the pixels of its current hash.
static dt_pixelpipe_cache_ready_handler_t _ready_handler
static gboolean _cache_entry_materialize_host_data(dt_dev_pixelpipe_cache_t *cache, int preferred_devid, dt_pixel_cache_entry_t *entry)
dt_pixel_cache_entry_t * dt_dev_pixelpipe_cache_ref_entry_for_host_ptr(void *host_ptr)
Resolve and retain the cache entry owning a host pointer.
#define DT_PIXELPIPE_CACHE_PRESSURE_ALERT_PERIOD_US
static dt_dev_pixelpipe_cache_t * _pixelpipe_cache
static gboolean _cache_entry_materialize_host_data_locked(dt_pixel_cache_entry_t *entry, int preferred_devid, gboolean prefer_device_payload)
gboolean dt_dev_pixelpipe_cache_ref_entry_by_hash(const uint64_t hash, void **data, dt_pixel_cache_entry_t **entry)
Resolve and retain an existing cache entry by hash.
static void _pixel_cache_touch(dt_pixel_cache_entry_t *cache_entry)
void dt_dev_pixelpipe_cache_flag_auto_destroy(dt_pixel_cache_entry_t *cache_entry)
Flag a cache line as disposable (auto_destroy), for short-lived lines nothing will need again: the re...
static void _warn_user(const char *message)
void dt_dev_pixelpipe_cache_set_handlers(dt_pixelpipe_cache_warn_handler_t warn, dt_pixelpipe_cache_ready_handler_t ready, const dt_pixelpipe_cache_observer_t *observer)
Install the handlers. Call once, from the orchestrator, before any pipe runs.
static dt_pixel_cache_entry_t * dt_pixel_cache_new_entry(const uint64_t hash, const size_t size, const char *name, const int id, dt_dev_pixelpipe_cache_t *cache, gboolean alloc, GHashTable *table)
void _non_thread_safe_cache_ref_count_entry(dt_dev_pixelpipe_cache_t *cache, gboolean lock, dt_pixel_cache_entry_t *cache_entry)
static dt_pixelpipe_cache_alert_handler_t _alert_handler
dt_dev_pixelpipe_cache_writable_status_t dt_dev_pixelpipe_cache_get_writable(const uint64_t hash, const size_t size, const char *name, const int id, const gboolean alloc, const gboolean allow_rekey_reuse, const dt_pixel_cache_entry_t *reuse_hint, const dt_pixel_cache_entry_t *reuse_hint_prev, void **data, dt_pixel_cache_entry_t **entry)
float * dt_dev_pixelpipe_cache_restore_cl_buffer(dt_dev_pixelpipe_t *pipe, float *input, void *cl_mem_input, const dt_iop_roi_t *roi_in, dt_iop_module_t *module, const size_t in_bpp, dt_pixel_cache_entry_t *input_entry, const char *message)
Force device → host resynchronization of the pixelpipe input cache line.
gboolean dt_dev_pixelpipe_cache_ref_host_entry_by_hash(const uint64_t hash, void **data, dt_pixel_cache_entry_t **entry)
Resolve and retain an existing cacheline that already holds HOST pixels.
const char * dt_pixelpipe_cache_set_current_module(const char *module)
Set the current module name for cache diagnostics (thread-local).
uint32_t dt_pixelpipe_cache_get_alloc_refusals(void)
Count the allocations the cache refused to the calling thread for lack of memory (thread-local).
gboolean dt_dev_pixelpipe_cache_is_ready(void)
Has the pixelpipe cache been initialised? Callers that run before dt_dev_pixelpipe_cache_init() succe...
Pixelpipe cache for storing intermediate results in the pixelpipe.
#define DT_PIXELPIPE_CACHE_HASH_INVALID
void(* dt_pixelpipe_cache_warn_handler_t)(const char *message)
Tell the user something that is fine to miss: the cache went over its budget, but the allocation went...
void(* dt_pixelpipe_cache_alert_handler_t)(const char *message, const char *item)
Tell the user something that must not go unseen: what it says stays true after a toast would have gon...
void(* dt_pixelpipe_cache_ready_handler_t)(uint64_t hash, uint64_t producer_node_key)
A cacheline finished and is readable.
dt_dev_pixelpipe_cache_writable_status_t
@ DT_DEV_PIXELPIPE_CACHE_WRITABLE_REKEYED
@ DT_DEV_PIXELPIPE_CACHE_WRITABLE_ERROR
@ DT_DEV_PIXELPIPE_CACHE_WRITABLE_CREATED
@ DT_DEV_PIXELPIPE_CACHE_WRITABLE_EXACT_HIT
void dt_pixelpipe_cache_pressure_monitor_init(dt_pixelpipe_cache_pressure_monitor_t *m, const size_t plan)
void dt_pixelpipe_cache_pressure_triggered(dt_pixelpipe_cache_pressure_monitor_t *m, const dt_pixelpipe_cache_pressure_sink_t *sink)
void dt_pixelpipe_cache_pressure_watch_start(dt_pixelpipe_cache_pressure_monitor_t *m, void(*wake)(void *user), void *user)
size_t dt_pixelpipe_cache_pressure_react(dt_pixelpipe_cache_pressure_monitor_t *m, const dt_pixelpipe_cache_pressure_sink_t *sink)
void dt_pixelpipe_cache_pressure_watch_stop(dt_pixelpipe_cache_pressure_monitor_t *m)
static size_t dt_pixelpipe_cache_pressure_reported(const dt_pixelpipe_cache_pressure_t *p, const size_t current)
static size_t dt_pixelpipe_cache_pressure_budget(const dt_pixelpipe_cache_pressure_t *p)
float dt_aligned_pixel_simd_t __attribute__((vector_size(16), aligned(16)))
Apply one channel's tone curve to each of the three colour channels, or pass the channel through unto...
Definition simd.h:55
static const dt_aligned_pixel_simd_t value
Definition simd.h:144
const float uint32_t state[4]
unsigned __int64 uint64_t
Definition strptime.c:75
dt_pixel_cache_entry_t * cache_entry
dt_pthread_mutex_t lock
dt_pixelpipe_cache_pressure_monitor_t psi
dt_dev_operation_t op
Definition imageop.h:259
Region of interest passed through the pixelpipe.
Definition format.h:49
int width
Definition format.h:50
int height
Definition format.h:50
uint64_t hash
gboolean auto_destroy
dt_atomic_int refcount
gboolean external_alloc
void * data
dt_atomic_uint64 age
size_t size
dt_atomic_int host_stale
uint64_t serial
dt_dev_pixelpipe_cache_t * cache
dt_pthread_rwlock_t lock
dt_pthread_mutex_t cl_mem_lock
GList * cl_mem_list
char * name
int hits
int id
uint64_t producer_node_key
int refcount
char name[64]
int host_stale
int hits
gboolean allocation
dt_pixel_cache_stats_device_t * devices
size_t size
size_t cl_bytes
size_t host_bytes
uint64_t hash
int64_t age_us
int cl_count
The supervisor's view of the cache. All four may be NULL; active gates the other three so the cache p...
void(* cacheline_delete)(uint64_t hash, size_t size, int owner_pipe_id, const char *name)
void(* cacheline_read)(uint64_t hash, size_t size)
void(* rekey)(uint64_t old_hash, uint64_t new_hash)
dt_pthread_mutex_t lock
Definition supervisor.c:123
typedef double((*spd)(unsigned long int wavelength, double TempK))
#define MIN(a, b)
Definition thinplate.c:32
#define MAX(a, b)
Definition thinplate.c:29