Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
memory_arena.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel
3 Copyright (C) 2026 - Aurélien PIERRE
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with darktable. If not, see <http://www.gnu.org/licenses/>.
17*/
18
19#define _GNU_SOURCE
20
21#include "common/darktable.h"
22#include "common/memory_arena.h"
23
24#include <errno.h>
25#include <stdio.h>
26#include <string.h>
27
28#ifdef _WIN32
29#include <windows.h>
30#else
31#include <sys/mman.h>
32#endif
33
34typedef struct dt_free_run_t
35{
36 uint32_t start;
37 uint32_t length;
39
41 size_t size,
42 uint32_t *out_pages,
43 size_t *out_size)
44{
45 if(IS_NULL_PTR(a) || IS_NULL_PTR(a->base) || IS_NULL_PTR(out_pages) || a->page_size == 0 || a->num_pages == 0) return FALSE;
46 if(size == 0) return FALSE;
47 if(size > SIZE_MAX - (a->page_size - 1)) return FALSE;
48
49 const size_t pages = (size + a->page_size - 1) / a->page_size;
50 if(pages > a->num_pages || pages > UINT32_MAX) return FALSE;
51 if(out_size)
52 {
53 if(pages > SIZE_MAX / a->page_size) return FALSE;
54 *out_size = pages * a->page_size;
55 }
56
57 *out_pages = (uint32_t)pages;
58 return TRUE;
59}
60
61/*
62 * Allocate from the arena in page-sized chunks.
63 * Uses a best-fit scan over the sorted free-run list, then consumes from
64 * the beginning of the selected run. On success, returns a pointer into the
65 * arena and writes the page-rounded allocation size to out_size.
66 */
68 size_t size,
69 size_t *out_size)
70{
71 if(IS_NULL_PTR(a) || IS_NULL_PTR(a->base) || !out_size) return NULL;
72
73 uint32_t pages_needed = 0;
74 size_t rounded_size = 0;
75 if(!dt_cache_arena_calc(a, size, &pages_needed, &rounded_size)) return NULL;
76
78
79 guint best_index = G_MAXUINT;
80 uint32_t best_length = UINT32_MAX;
81
82 for(guint i = 0; i < a->free_runs->len; i++)
83 {
84 dt_free_run_t *r = &g_array_index(a->free_runs, dt_free_run_t, i);
85 if(r->length >= pages_needed && r->length < best_length)
86 {
87 best_index = i;
88 best_length = r->length;
89 if(best_length == pages_needed) break; // exact fit
90 }
91 }
92
93 if(best_index == G_MAXUINT)
94 {
96 return NULL;
97 }
98
99 dt_free_run_t *r = &g_array_index(a->free_runs, dt_free_run_t, best_index);
100 const uint32_t first = r->start;
101
102 /* consume from the front of the run so the list stays sorted */
103 r->start += pages_needed;
104 r->length -= pages_needed;
105
106 /* remove empty run after consumption */
107 if(r->length == 0)
108 g_array_remove_index(a->free_runs, best_index);
109
111
112 uint8_t *ptr = a->base + (size_t)first * a->page_size;
113
114#ifdef _WIN32
115 /* The arena is only MEM_RESERVE'd up front (see dt_cache_arena_init): commit
116 * physical/pagefile backing for just this range, on demand, so the process's
117 * commit charge tracks actual live cache usage instead of the full arena size. */
118 if(IS_NULL_PTR(VirtualAlloc(ptr, rounded_size, MEM_COMMIT, PAGE_READWRITE)))
119 {
120 const DWORD err = GetLastError();
121 fprintf(stderr, "couldn't commit cache page range (VirtualAlloc error %lu)\n", (unsigned long)err);
122 /* hand the pages back before failing so the arena stays consistent */
123 dt_cache_arena_free(a, ptr, rounded_size);
124 return NULL;
125 }
126#endif
127
128 *out_size = rounded_size;
129 return ptr;
130}
131
132
133/*
134 * Return a previously allocated region to the arena.
135 * The pointer must refer to the arena base, and size is rounded up to pages.
136 * The freed run is inserted in order and coalesced with adjacent runs.
137 */
139 void *ptr,
140 size_t size)
141{
142 if(IS_NULL_PTR(a) || IS_NULL_PTR(a->base) || !a->free_runs || a->page_size == 0 || a->num_pages == 0)
143 return;
144 if(IS_NULL_PTR(ptr) || size == 0) return;
145
146 const uintptr_t base = (uintptr_t)a->base;
147 const uintptr_t addr = (uintptr_t)ptr;
148 if(addr < base || addr >= base + a->size)
149 {
150 fprintf(stderr, "[pixelpipe] arena free: pointer out of range\n");
151 return;
152 }
153 if(((addr - base) % a->page_size) != 0)
154 {
155 fprintf(stderr, "[pixelpipe] arena free: pointer not page-aligned\n");
156 return;
157 }
158
159 uint32_t pages = 0;
160 if(!dt_cache_arena_calc(a, size, &pages, NULL))
161 {
162 fprintf(stderr, "[pixelpipe] arena free: invalid size\n");
163 return;
164 }
165
166 const size_t first_sz = (addr - base) / a->page_size;
167 if(first_sz >= a->num_pages || pages > a->num_pages - first_sz)
168 {
169 fprintf(stderr, "[pixelpipe] arena free: range out of bounds\n");
170 return;
171 }
172
173 const uint32_t first = (uint32_t)first_sz;
174
176
177 /* insert a new free run, keeping free_runs sorted by start page */
178 guint i = 0;
179 while(i < a->free_runs->len &&
180 g_array_index(a->free_runs, dt_free_run_t, i).start < first)
181 i++;
182
183 if(i > 0)
184 {
185 dt_free_run_t *prev = &g_array_index(a->free_runs, dt_free_run_t, i - 1);
186 if(prev->start + prev->length > first)
187 {
189 fprintf(stderr, "[pixelpipe] arena free: overlap with previous run\n");
190 return;
191 }
192 }
193 if(i < a->free_runs->len)
194 {
195 dt_free_run_t *next = &g_array_index(a->free_runs, dt_free_run_t, i);
196 if(first + pages > next->start)
197 {
199 fprintf(stderr, "[pixelpipe] arena free: overlap with next run\n");
200 return;
201 }
202 }
203
204 dt_free_run_t new = { first, pages };
205 g_array_insert_val(a->free_runs, i, new);
206
207 /* coalesce with next run if adjacent */
208 if(i + 1 < a->free_runs->len)
209 {
210 dt_free_run_t *cur = &g_array_index(a->free_runs, dt_free_run_t, i);
211 dt_free_run_t *next = &g_array_index(a->free_runs, dt_free_run_t, i + 1);
212 if(cur->start + cur->length == next->start)
213 {
214 cur->length += next->length;
215 g_array_remove_index(a->free_runs, i + 1);
216 }
217 }
218
219 /* coalesce with previous run if adjacent */
220 if(i > 0)
221 {
222 dt_free_run_t *prev = &g_array_index(a->free_runs, dt_free_run_t, i - 1);
223 dt_free_run_t *cur = &g_array_index(a->free_runs, dt_free_run_t, i);
224 if(prev->start + prev->length == cur->start)
225 {
226 prev->length += cur->length;
227 g_array_remove_index(a->free_runs, i);
228 }
229 }
230
232}
233
235 uint32_t *out_total_free_pages,
236 uint32_t *out_largest_free_run_pages)
237{
238 if(out_total_free_pages) *out_total_free_pages = 0;
239 if(out_largest_free_run_pages) *out_largest_free_run_pages = 0;
240 if(IS_NULL_PTR(a) || !a->free_runs) return;
241
243 uint32_t total = 0;
244 uint32_t largest = 0;
245 for(guint i = 0; i < a->free_runs->len; i++)
246 {
247 dt_free_run_t *r = &g_array_index(a->free_runs, dt_free_run_t, i);
248 total += r->length;
249 if(r->length > largest) largest = r->length;
250 }
252
253 if(out_total_free_pages) *out_total_free_pages = total;
254 if(out_largest_free_run_pages) *out_largest_free_run_pages = largest;
255}
256
258{
259 if(IS_NULL_PTR(a)) return;
260
262 g_array_free(a->free_runs, TRUE);
264
266
267 /* 5. Release the virtual memory block */
268 if(a->base && a->size)
269 {
270#ifdef _WIN32
271 VirtualFree(a->base, 0, MEM_RELEASE);
272#else
273 munmap(a->base, a->size);
274#endif
275 }
276
277 /* 6. Poison the struct (defensive) */
278 a->base = NULL;
279 a->size = 0;
280 a->num_pages = 0;
281 a->page_size = 0;
282}
283
284// return 0 on success 1 on error
285int dt_cache_arena_init(dt_cache_arena_t *a, size_t total_size)
286{
287 const size_t page_size = 64 * 1024; // 64 KiB cache pages
288 const size_t pages = total_size / page_size;
289
290#ifdef _WIN32
291 // Unlike Linux's mmap(MAP_PRIVATE | MAP_ANONYMOUS), which only reserves address
292 // space and lets the kernel commit physical/swap backing lazily as pages are
293 // first touched (overcommit), Windows' MEM_COMMIT charges the *entire*
294 // total_size against the system commit limit (RAM + pagefile) immediately.
295 // total_size here is sized to use most of system RAM (see
296 // dt_configure_runtime_performance() in darktable.c), so an eager commit can
297 // exceed the commit limit even though actual cache usage never does. Reserve
298 // the range only; dt_cache_arena_alloc() commits each carved-out sub-range on
299 // demand, mirroring the lazy-commit behavior mmap gives on Linux.
300 a->base = (uint8_t *)VirtualAlloc(NULL, total_size,
301 MEM_RESERVE,
302 PAGE_READWRITE);
303 if(IS_NULL_PTR(a->base))
304 {
305 const DWORD err = GetLastError();
306 fprintf(stderr, "couldn't alloc map (VirtualAlloc error %lu)\n", (unsigned long)err);
307 return 1;
308 }
309#else
310 a->base = mmap(NULL, total_size,
311 PROT_READ | PROT_WRITE,
312 MAP_PRIVATE | MAP_ANONYMOUS,
313 -1, 0);
314
315 if(a->base == MAP_FAILED)
316 {
317 a->base = NULL;
318 fprintf(stderr, "couldn't alloc map (mmap error %d: %s)\n", errno, strerror(errno));
319 return 1;
320 }
321#endif
322
323 a->size = total_size;
324 a->page_size = page_size;
325 a->num_pages = pages;
326
327 a->free_runs = g_array_new(FALSE, FALSE, sizeof(dt_free_run_t));
328 if(!a->free_runs)
329 {
330#ifdef _WIN32
331 VirtualFree(a->base, 0, MEM_RELEASE);
332#else
333 munmap(a->base, a->size);
334#endif
335 a->base = NULL;
336 a->size = 0;
337 a->page_size = 0;
338 a->num_pages = 0;
339 fprintf(stderr, "couldn't alloc free run list\n");
340 return 1;
341 }
342
343 /* start with one free run covering the whole arena */
344 dt_free_run_t full = {
345 .start = 0,
346 .length = a->num_pages
347 };
348
349 g_array_append_val(a->free_runs, full);
350
351 dt_pthread_mutex_init(&a->lock, NULL);
352 return 0;
353}
354
355gboolean dt_cache_arena_ptr_in(const dt_cache_arena_t *a, const void *ptr)
356{
357 if(IS_NULL_PTR(a) || IS_NULL_PTR(a->base) || IS_NULL_PTR(ptr)) return FALSE;
358 const uintptr_t base = (uintptr_t)a->base;
359 const uintptr_t addr = (uintptr_t)ptr;
360 return addr >= base && addr < base + a->size;
361}
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
#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 darktable.h:293
static int dt_pthread_mutex_unlock(dt_pthread_mutex_t *mutex) RELEASE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:384
static int dt_pthread_mutex_init(dt_pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
Definition dtpthread.h:369
static int dt_pthread_mutex_destroy(dt_pthread_mutex_t *mutex)
Definition dtpthread.h:389
static int dt_pthread_mutex_lock(dt_pthread_mutex_t *mutex) ACQUIRE(mutex) NO_THREAD_SAFETY_ANALYSIS
Definition dtpthread.h:374
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_ptr_in(const dt_cache_arena_t *a, const void *ptr)
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 size
Definition mipmap_cache.c:3
const float r
dt_pthread_mutex_t lock
uint32_t start
uint32_t length