Blender V4.3
moviecache.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2024 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
9#undef DEBUG_MESSAGES
10
11#include <cstdlib> /* for qsort */
12#include <memory.h>
13#include <mutex>
14
16#include "MEM_guardedalloc.h"
17
18#include "BLI_ghash.h"
19#include "BLI_mempool.h"
20#include "BLI_string.h"
21#include "BLI_utildefines.h"
22
23#include "IMB_moviecache.hh"
24
25#include "IMB_imbuf.hh"
26#include "IMB_imbuf_types.hh"
27
28#ifdef DEBUG_MESSAGES
29# if defined __GNUC__
30# define PRINT(format, args...) printf(format, ##args)
31# else
32# define PRINT(format, ...) printf(__VA_ARGS__)
33# endif
34#else
35# define PRINT(format, ...)
36#endif
37
38static MEM_CacheLimiterC *limitor = nullptr;
39
40/* Image buffers managed by a moviecache might be using their own movie caches (used by color
41 * management). In practice this means that, for example, freeing MovieCache used by MovieClip
42 * will request freeing MovieCache owned by ImBuf. Freeing MovieCache needs to be thread-safe,
43 * so regular mutex will not work here, hence the recursive lock. */
44static std::recursive_mutex limitor_lock;
45
69
74
80 /* Indicates that #ibuf is null, because there was an error during load. */
82};
83
84static uint moviecache_hashhash(const void *keyv)
85{
86 const MovieCacheKey *key = (const MovieCacheKey *)keyv;
87
88 return key->cache_owner->hashfp(key->userkey);
89}
90
91static bool moviecache_hashcmp(const void *av, const void *bv)
92{
93 const MovieCacheKey *a = (const MovieCacheKey *)av;
94 const MovieCacheKey *b = (const MovieCacheKey *)bv;
95
96 return a->cache_owner->cmpfp(a->userkey, b->userkey);
97}
98
99static void moviecache_keyfree(void *val)
100{
101 MovieCacheKey *key = (MovieCacheKey *)val;
102
104
106}
107
108static void moviecache_valfree(void *val)
109{
110 MovieCacheItem *item = (MovieCacheItem *)val;
111 MovieCache *cache = item->cache_owner;
112
113 PRINT("%s: cache '%s' free item %p buffer %p\n", __func__, cache->name, item, item->ibuf);
114
115 if (item->c_handle) {
116 limitor_lock.lock();
118 limitor_lock.unlock();
119 }
120
121 if (item->ibuf) {
122 IMB_freeImBuf(item->ibuf);
123 }
124
125 if (item->priority_data && cache->prioritydeleterfp) {
126 cache->prioritydeleterfp(item->priority_data);
127 }
128
130}
131
132static void check_unused_keys(MovieCache *cache)
133{
134 GHashIterator gh_iter;
135
136 BLI_ghashIterator_init(&gh_iter, cache->hash);
137
138 while (!BLI_ghashIterator_done(&gh_iter)) {
139 const MovieCacheKey *key = (const MovieCacheKey *)BLI_ghashIterator_getKey(&gh_iter);
140 const MovieCacheItem *item = (const MovieCacheItem *)BLI_ghashIterator_getValue(&gh_iter);
141
142 BLI_ghashIterator_step(&gh_iter);
143
144 if (item->added_empty) {
145 /* Don't remove entries that have been added empty. Those indicate that the image couldn't be
146 * loaded correctly. */
147 continue;
148 }
149
150 bool remove = !item->ibuf;
151
152 if (remove) {
153 PRINT("%s: cache '%s' remove item %p without buffer\n", __func__, cache->name, item);
154 }
155
156 if (remove) {
158 }
159 }
160}
161
162static int compare_int(const void *av, const void *bv)
163{
164 const int *a = (int *)av;
165 const int *b = (int *)bv;
166 return *a - *b;
167}
168
169static void moviecache_destructor(void *p)
170{
171 MovieCacheItem *item = (MovieCacheItem *)p;
172
173 if (item && item->ibuf) {
174 MovieCache *cache = item->cache_owner;
175
176 PRINT("%s: cache '%s' destroy item %p buffer %p\n", __func__, cache->name, item, item->ibuf);
177
178 IMB_freeImBuf(item->ibuf);
179
180 item->ibuf = nullptr;
181 item->c_handle = nullptr;
182
183 /* force cached segments to be updated */
184 MEM_SAFE_FREE(cache->points);
185 }
186}
187
188static size_t get_size_in_memory(ImBuf *ibuf)
189{
190 /* Keep textures in the memory to avoid constant file reload on viewport update. */
191 if (ibuf->userflags & IB_PERSISTENT) {
192 return 0;
193 }
194
195 return IMB_get_size_in_memory(ibuf);
196}
197static size_t get_item_size(void *p)
198{
199 size_t size = sizeof(MovieCacheItem);
200 MovieCacheItem *item = (MovieCacheItem *)p;
201
202 if (item->ibuf) {
203 size += get_size_in_memory(item->ibuf);
204 }
205
206 return size;
207}
208
209static int get_item_priority(void *item_v, int default_priority)
210{
211 MovieCacheItem *item = (MovieCacheItem *)item_v;
212 MovieCache *cache = item->cache_owner;
213 int priority;
214
215 if (!cache->getitempriorityfp) {
216 PRINT("%s: cache '%s' item %p use default priority %d\n",
217 __func__,
218 cache->name,
219 item,
220 default_priority);
221
222 return default_priority;
223 }
224
225 priority = cache->getitempriorityfp(cache->last_userkey, item->priority_data);
226
227 PRINT("%s: cache '%s' item %p priority %d\n", __func__, cache->name, item, priority);
228
229 return priority;
230}
231
232static bool get_item_destroyable(void *item_v)
233{
234 MovieCacheItem *item = (MovieCacheItem *)item_v;
235 if (item->ibuf == nullptr) {
236 return true;
237 }
238 /* IB_BITMAPDIRTY means image was modified from inside blender and
239 * changes are not saved to disk.
240 *
241 * Such buffers are never to be freed.
242 */
243 if ((item->ibuf->userflags & IB_BITMAPDIRTY) || (item->ibuf->userflags & IB_PERSISTENT)) {
244 return false;
245 }
246 return true;
247}
248
256
258{
259 if (limitor) {
261 limitor = nullptr;
262 }
263}
264
266 int keysize,
267 GHashHashFP hashfp,
268 GHashCmpFP cmpfp)
269{
270 MovieCache *cache;
271
272 PRINT("%s: cache '%s' create\n", __func__, name);
273
274 cache = (MovieCache *)MEM_callocN(sizeof(MovieCache), "MovieCache");
275
276 STRNCPY(cache->name, name);
277
280 cache->userkeys_pool = BLI_mempool_create(keysize, 0, 64, BLI_MEMPOOL_NOP);
281 cache->hash = BLI_ghash_new(
282 moviecache_hashhash, moviecache_hashcmp, "MovieClip ImBuf cache hash");
283
284 cache->keysize = keysize;
285 cache->hashfp = hashfp;
286 cache->cmpfp = cmpfp;
287 cache->proxy = -1;
288
289 return cache;
290}
291
293{
294 cache->getdatafp = getdatafp;
295}
296
298 MovieCacheGetPriorityDataFP getprioritydatafp,
299 MovieCacheGetItemPriorityFP getitempriorityfp,
300 MovieCachePriorityDeleterFP prioritydeleterfp)
301{
302 cache->last_userkey = MEM_mallocN(cache->keysize, "movie cache last user key");
303
304 cache->getprioritydatafp = getprioritydatafp;
305 cache->getitempriorityfp = getitempriorityfp;
306 cache->prioritydeleterfp = prioritydeleterfp;
307}
308
309static void do_moviecache_put(MovieCache *cache, void *userkey, ImBuf *ibuf, bool need_lock)
310{
311 MovieCacheKey *key;
312 MovieCacheItem *item;
313
314 if (!limitor) {
316 }
317
318 if (ibuf != nullptr) {
319 IMB_refImBuf(ibuf);
320 }
321
323 key->cache_owner = cache;
325 memcpy(key->userkey, userkey, cache->keysize);
326
328
329 PRINT("%s: cache '%s' put %p, item %p\n", __func__, cache->name, ibuf, item);
330
331 item->ibuf = ibuf;
332 item->cache_owner = cache;
333 item->c_handle = nullptr;
334 item->priority_data = nullptr;
335 item->added_empty = ibuf == nullptr;
336
337 if (cache->getprioritydatafp) {
338 item->priority_data = cache->getprioritydatafp(userkey);
339 }
340
342
343 if (cache->last_userkey) {
344 memcpy(cache->last_userkey, userkey, cache->keysize);
345 }
346
347 if (need_lock) {
348 limitor_lock.lock();
349 }
350
352
356
357 if (need_lock) {
358 limitor_lock.unlock();
359 }
360
361 /* cache limiter can't remove unused keys which points to destroyed values */
362 check_unused_keys(cache);
363
364 MEM_SAFE_FREE(cache->points);
365}
366
367void IMB_moviecache_put(MovieCache *cache, void *userkey, ImBuf *ibuf)
368{
369 do_moviecache_put(cache, userkey, ibuf, true);
370}
371
372bool IMB_moviecache_put_if_possible(MovieCache *cache, void *userkey, ImBuf *ibuf)
373{
374 size_t mem_in_use, mem_limit, elem_size;
375 bool result = false;
376
377 elem_size = (ibuf == nullptr) ? 0 : get_size_in_memory(ibuf);
378 mem_limit = MEM_CacheLimiter_get_maximum();
379
380 limitor_lock.lock();
382
383 if (mem_in_use + elem_size <= mem_limit) {
384 do_moviecache_put(cache, userkey, ibuf, false);
385 result = true;
386 }
387
388 limitor_lock.unlock();
389
390 return result;
391}
392
393void IMB_moviecache_remove(MovieCache *cache, void *userkey)
394{
395 MovieCacheKey key;
396 key.cache_owner = cache;
397 key.userkey = userkey;
399}
400
401ImBuf *IMB_moviecache_get(MovieCache *cache, void *userkey, bool *r_is_cached_empty)
402{
403 MovieCacheKey key;
404 MovieCacheItem *item;
405
406 key.cache_owner = cache;
407 key.userkey = userkey;
408 item = (MovieCacheItem *)BLI_ghash_lookup(cache->hash, &key);
409
410 if (r_is_cached_empty) {
411 *r_is_cached_empty = false;
412 }
413
414 if (item) {
415 if (item->ibuf) {
416 limitor_lock.lock();
418 limitor_lock.unlock();
419
420 IMB_refImBuf(item->ibuf);
421
422 return item->ibuf;
423 }
424 if (r_is_cached_empty && item->added_empty) {
425 *r_is_cached_empty = true;
426 }
427 }
428
429 return nullptr;
430}
431
432bool IMB_moviecache_has_frame(MovieCache *cache, void *userkey)
433{
434 MovieCacheKey key;
435 MovieCacheItem *item;
436
437 key.cache_owner = cache;
438 key.userkey = userkey;
439 item = (MovieCacheItem *)BLI_ghash_lookup(cache->hash, &key);
440
441 return item != nullptr;
442}
443
445{
446 PRINT("%s: cache '%s' free\n", __func__, cache->name);
447
449
453
454 if (cache->points) {
455 MEM_freeN(cache->points);
456 }
457
458 if (cache->last_userkey) {
459 MEM_freeN(cache->last_userkey);
460 }
461
462 MEM_freeN(cache);
463}
464
466 bool(cleanup_check_cb)(ImBuf *ibuf, void *userkey, void *userdata),
467 void *userdata)
468{
469 GHashIterator gh_iter;
470
471 check_unused_keys(cache);
472
473 BLI_ghashIterator_init(&gh_iter, cache->hash);
474
475 while (!BLI_ghashIterator_done(&gh_iter)) {
478
479 BLI_ghashIterator_step(&gh_iter);
480
481 if (cleanup_check_cb(item->ibuf, key->userkey, userdata)) {
482 PRINT("%s: cache '%s' remove item %p\n", __func__, cache->name, item);
483
485 }
486 }
487}
488
490 MovieCache *cache, int proxy, int render_flags, int *r_totseg, int **r_points)
491{
492 *r_totseg = 0;
493 *r_points = nullptr;
494
495 if (!cache->getdatafp) {
496 return;
497 }
498
499 if (cache->proxy != proxy || cache->render_flags != render_flags) {
500 MEM_SAFE_FREE(cache->points);
501 }
502
503 if (cache->points) {
504 *r_totseg = cache->totseg;
505 *r_points = cache->points;
506 }
507 else {
508 int totframe = BLI_ghash_len(cache->hash);
509 int *frames = (int *)MEM_callocN(totframe * sizeof(int), "movieclip cache frames");
510 int a, totseg = 0;
511 GHashIterator gh_iter;
512
513 a = 0;
514 GHASH_ITER (gh_iter, cache->hash) {
517 int framenr, curproxy, curflags;
518
519 if (item->ibuf) {
520 cache->getdatafp(key->userkey, &framenr, &curproxy, &curflags);
521
522 if (curproxy == proxy && curflags == render_flags) {
523 frames[a++] = framenr;
524 }
525 }
526 }
527
528 qsort(frames, totframe, sizeof(int), compare_int);
529
530 /* count */
531 for (a = 0; a < totframe; a++) {
532 if (a && frames[a] - frames[a - 1] != 1) {
533 totseg++;
534 }
535
536 if (a == totframe - 1) {
537 totseg++;
538 }
539 }
540
541 if (totseg) {
542 int b, *points;
543
544 points = (int *)MEM_callocN(sizeof(int[2]) * totseg, "movieclip cache segments");
545
546 /* fill */
547 for (a = 0, b = 0; a < totframe; a++) {
548 if (a == 0) {
549 points[b++] = frames[a];
550 }
551
552 if (a && frames[a] - frames[a - 1] != 1) {
553 points[b++] = frames[a - 1];
554 points[b++] = frames[a];
555 }
556
557 if (a == totframe - 1) {
558 points[b++] = frames[a];
559 }
560 }
561
562 *r_totseg = totseg;
563 *r_points = points;
564
565 cache->totseg = totseg;
566 cache->points = points;
567 cache->proxy = proxy;
568 cache->render_flags = render_flags;
569 }
570
571 MEM_freeN(frames);
572 }
573}
574
575MovieCacheIter *IMB_moviecacheIter_new(MovieCache *cache)
576{
577 GHashIterator *iter;
578
579 check_unused_keys(cache);
580 iter = BLI_ghashIterator_new(cache->hash);
581
582 return (MovieCacheIter *)iter;
583}
584
585void IMB_moviecacheIter_free(MovieCacheIter *iter)
586{
588}
589
590bool IMB_moviecacheIter_done(MovieCacheIter *iter)
591{
593}
594
595void IMB_moviecacheIter_step(MovieCacheIter *iter)
596{
598}
599
600ImBuf *IMB_moviecacheIter_getImBuf(MovieCacheIter *iter)
601{
603 return item->ibuf;
604}
605
606void *IMB_moviecacheIter_getUserKey(MovieCacheIter *iter)
607{
609 return key->userkey;
610}
BLI_INLINE void * BLI_ghashIterator_getKey(GHashIterator *ghi) ATTR_WARN_UNUSED_RESULT
Definition BLI_ghash.h:299
bool BLI_ghash_reinsert(GHash *gh, void *key, void *val, GHashKeyFreeFP keyfreefp, GHashValFreeFP valfreefp)
Definition BLI_ghash.c:712
void BLI_ghashIterator_step(GHashIterator *ghi)
Definition BLI_ghash.c:911
void BLI_ghashIterator_free(GHashIterator *ghi)
Definition BLI_ghash.c:925
bool(* GHashCmpFP)(const void *a, const void *b)
Definition BLI_ghash.h:37
BLI_INLINE void * BLI_ghashIterator_getValue(GHashIterator *ghi) ATTR_WARN_UNUSED_RESULT
Definition BLI_ghash.h:303
#define GHASH_ITER(gh_iter_, ghash_)
Definition BLI_ghash.h:322
GHash * BLI_ghash_new(GHashHashFP hashfp, GHashCmpFP cmpfp, const char *info) ATTR_MALLOC ATTR_WARN_UNUSED_RESULT
Definition BLI_ghash.c:686
GHashIterator * BLI_ghashIterator_new(GHash *gh) ATTR_MALLOC ATTR_WARN_UNUSED_RESULT
Definition BLI_ghash.c:888
unsigned int(* GHashHashFP)(const void *key)
Definition BLI_ghash.h:35
unsigned int BLI_ghash_len(const GHash *gh) ATTR_WARN_UNUSED_RESULT
Definition BLI_ghash.c:702
bool BLI_ghash_remove(GHash *gh, const void *key, GHashKeyFreeFP keyfreefp, GHashValFreeFP valfreefp)
Definition BLI_ghash.c:787
void * BLI_ghash_lookup(const GHash *gh, const void *key) ATTR_WARN_UNUSED_RESULT
Definition BLI_ghash.c:731
void BLI_ghash_free(GHash *gh, GHashKeyFreeFP keyfreefp, GHashValFreeFP valfreefp)
Definition BLI_ghash.c:860
void BLI_ghashIterator_init(GHashIterator *ghi, GHash *gh)
Definition BLI_ghash.c:895
BLI_INLINE bool BLI_ghashIterator_done(const GHashIterator *ghi) ATTR_WARN_UNUSED_RESULT
Definition BLI_ghash.h:311
void * BLI_mempool_alloc(BLI_mempool *pool) ATTR_MALLOC ATTR_WARN_UNUSED_RESULT ATTR_RETURNS_NONNULL ATTR_NONNULL(1)
void BLI_mempool_free(BLI_mempool *pool, void *addr) ATTR_NONNULL(1
BLI_mempool * BLI_mempool_create(unsigned int esize, unsigned int elem_num, unsigned int pchunk, unsigned int flag) ATTR_MALLOC ATTR_WARN_UNUSED_RESULT ATTR_RETURNS_NONNULL
@ BLI_MEMPOOL_NOP
Definition BLI_mempool.h:86
void BLI_mempool_destroy(BLI_mempool *pool) ATTR_NONNULL(1)
#define STRNCPY(dst, src)
Definition BLI_string.h:593
unsigned int uint
void IMB_refImBuf(ImBuf *ibuf)
size_t IMB_get_size_in_memory(ImBuf *ibuf)
Contains defines and structs used throughout the imbuf module.
@ IB_PERSISTENT
@ IB_BITMAPDIRTY
int(*)(void *last_userkey, void *priority_data) MovieCacheGetItemPriorityFP
void(*)(void *userkey, int *framenr, int *proxy, int *render_flags) MovieCacheGetKeyDataFP
void *(*)(void *userkey) MovieCacheGetPriorityDataFP
void(*)(void *priority_data) MovieCachePriorityDeleterFP
void MEM_CacheLimiter_ItemPriority_Func_set(MEM_CacheLimiterC *This, MEM_CacheLimiter_ItemPriority_Func item_priority_func)
void MEM_CacheLimiter_enforce_limits(MEM_CacheLimiterC *This)
void MEM_CacheLimiter_touch(MEM_CacheLimiterHandleC *handle)
void MEM_CacheLimiter_unref(MEM_CacheLimiterHandleC *handle)
void delete_MEM_CacheLimiter(MEM_CacheLimiterC *This)
void MEM_CacheLimiter_ItemDestroyable_Func_set(MEM_CacheLimiterC *This, MEM_CacheLimiter_ItemDestroyable_Func item_destroyable_func)
size_t MEM_CacheLimiter_get_memory_in_use(MEM_CacheLimiterC *This)
size_t MEM_CacheLimiter_get_maximum()
MEM_CacheLimiterC * new_MEM_CacheLimiter(MEM_CacheLimiter_Destruct_Func data_destructor, MEM_CacheLimiter_DataSize_Func data_size)
MEM_CacheLimiterHandleC * MEM_CacheLimiter_insert(MEM_CacheLimiterC *This, void *data)
void MEM_CacheLimiter_ref(MEM_CacheLimiterHandleC *handle)
void MEM_CacheLimiter_unmanage(MEM_CacheLimiterHandleC *handle)
struct MEM_CacheLimiterHandle_s MEM_CacheLimiterHandleC
struct MEM_CacheLimiter_s MEM_CacheLimiterC
Read Guarded memory(de)allocation.
#define MEM_SAFE_FREE(v)
static DBVT_INLINE btScalar size(const btDbvtVolume &a)
Definition btDbvt.cpp:52
local_group_size(16, 16) .push_constant(Type b
void IMB_freeImBuf(ImBuf *)
void *(* MEM_mallocN)(size_t len, const char *str)
Definition mallocn.cc:44
void MEM_freeN(void *vmemh)
Definition mallocn.cc:105
void *(* MEM_callocN)(size_t len, const char *str)
Definition mallocn.cc:42
static size_t mem_in_use
bool IMB_moviecache_put_if_possible(MovieCache *cache, void *userkey, ImBuf *ibuf)
MovieCacheIter * IMB_moviecacheIter_new(MovieCache *cache)
static std::recursive_mutex limitor_lock
Definition moviecache.cc:44
static size_t get_size_in_memory(ImBuf *ibuf)
void * IMB_moviecacheIter_getUserKey(MovieCacheIter *iter)
static bool moviecache_hashcmp(const void *av, const void *bv)
Definition moviecache.cc:91
static void moviecache_destructor(void *p)
void IMB_moviecache_free(MovieCache *cache)
bool IMB_moviecache_has_frame(MovieCache *cache, void *userkey)
void IMB_moviecache_cleanup(MovieCache *cache, bool(cleanup_check_cb)(ImBuf *ibuf, void *userkey, void *userdata), void *userdata)
static void check_unused_keys(MovieCache *cache)
ImBuf * IMB_moviecache_get(MovieCache *cache, void *userkey, bool *r_is_cached_empty)
static int get_item_priority(void *item_v, int default_priority)
void IMB_moviecacheIter_free(MovieCacheIter *iter)
bool IMB_moviecacheIter_done(MovieCacheIter *iter)
void IMB_moviecache_put(MovieCache *cache, void *userkey, ImBuf *ibuf)
static uint moviecache_hashhash(const void *keyv)
Definition moviecache.cc:84
static size_t get_item_size(void *p)
static int compare_int(const void *av, const void *bv)
void IMB_moviecache_set_getdata_callback(MovieCache *cache, MovieCacheGetKeyDataFP getdatafp)
static bool get_item_destroyable(void *item_v)
void IMB_moviecache_set_priority_callback(MovieCache *cache, MovieCacheGetPriorityDataFP getprioritydatafp, MovieCacheGetItemPriorityFP getitempriorityfp, MovieCachePriorityDeleterFP prioritydeleterfp)
void IMB_moviecacheIter_step(MovieCacheIter *iter)
static void do_moviecache_put(MovieCache *cache, void *userkey, ImBuf *ibuf, bool need_lock)
void IMB_moviecache_destruct()
void IMB_moviecache_init()
static void moviecache_keyfree(void *val)
Definition moviecache.cc:99
ImBuf * IMB_moviecacheIter_getImBuf(MovieCacheIter *iter)
static void moviecache_valfree(void *val)
void IMB_moviecache_get_cache_segments(MovieCache *cache, int proxy, int render_flags, int *r_totseg, int **r_points)
static MEM_CacheLimiterC * limitor
Definition moviecache.cc:38
MovieCache * IMB_moviecache_create(const char *name, int keysize, GHashHashFP hashfp, GHashCmpFP cmpfp)
#define PRINT(format,...)
Definition moviecache.cc:35
void IMB_moviecache_remove(MovieCache *cache, void *userkey)
MovieCache * cache_owner
Definition moviecache.cc:76
void * priority_data
Definition moviecache.cc:79
MEM_CacheLimiterHandleC * c_handle
Definition moviecache.cc:78
MovieCache * cache_owner
Definition moviecache.cc:71
BLI_mempool * keys_pool
Definition moviecache.cc:58
MovieCacheGetKeyDataFP getdatafp
Definition moviecache.cc:52
GHashHashFP hashfp
Definition moviecache.cc:50
BLI_mempool * userkeys_pool
Definition moviecache.cc:60
void * last_userkey
Definition moviecache.cc:64
MovieCacheGetItemPriorityFP getitempriorityfp
Definition moviecache.cc:55
MovieCachePriorityDeleterFP prioritydeleterfp
Definition moviecache.cc:56
int render_flags
Definition moviecache.cc:66
char name[64]
Definition moviecache.cc:47
BLI_mempool * items_pool
Definition moviecache.cc:59
GHashCmpFP cmpfp
Definition moviecache.cc:51
int * points
Definition moviecache.cc:66
GHash * hash
Definition moviecache.cc:49
MovieCacheGetPriorityDataFP getprioritydatafp
Definition moviecache.cc:54