/*
* This file is part of libplacebo.
*
* libplacebo is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* libplacebo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with libplacebo. If not, see .
*/
#include
#include
#include
#include "common.h"
#include "cache.h"
#include "log.h"
#include "pl_thread.h"
const struct pl_cache_params pl_cache_default_params = {0};
struct priv {
pl_log log;
pl_mutex lock;
PL_ARRAY(pl_cache_obj) objects;
size_t total_size;
};
int pl_cache_objects(pl_cache cache)
{
if (!cache)
return 0;
struct priv *p = PL_PRIV(cache);
pl_mutex_lock(&p->lock);
int num = p->objects.num;
pl_mutex_unlock(&p->lock);
return num;
}
size_t pl_cache_size(pl_cache cache)
{
if (!cache)
return 0;
struct priv *p = PL_PRIV(cache);
pl_mutex_lock(&p->lock);
size_t size = p->total_size;
pl_mutex_unlock(&p->lock);
return size;
}
pl_cache pl_cache_create(const struct pl_cache_params *params)
{
struct pl_cache_t *cache = pl_zalloc_obj(NULL, cache, struct priv);
struct priv *p = PL_PRIV(cache);
pl_mutex_init(&p->lock);
if (params) {
cache->params = *params;
p->log = params->log;
}
// Sanitize size limits
size_t total_size = PL_DEF(cache->params.max_total_size, SIZE_MAX);
size_t object_size = PL_DEF(cache->params.max_object_size, SIZE_MAX);
object_size = PL_MIN(total_size, object_size);
cache->params.max_total_size = total_size;
cache->params.max_object_size = object_size;
return cache;
}
static void remove_obj(pl_cache cache, pl_cache_obj obj)
{
struct priv *p = PL_PRIV(cache);
p->total_size -= obj.size;
if (obj.free)
obj.free(obj.data);
}
void pl_cache_destroy(pl_cache *pcache)
{
pl_cache cache = *pcache;
if (!cache)
return;
struct priv *p = PL_PRIV(cache);
for (int i = 0; i < p->objects.num; i++)
remove_obj(cache, p->objects.elem[i]);
pl_assert(p->total_size == 0);
pl_mutex_destroy(&p->lock);
pl_free((void *) cache);
*pcache = NULL;
}
void pl_cache_reset(pl_cache cache)
{
if (!cache)
return;
struct priv *p = PL_PRIV(cache);
pl_mutex_lock(&p->lock);
for (int i = 0; i < p->objects.num; i++)
remove_obj(cache, p->objects.elem[i]);
p->objects.num = 0;
pl_assert(p->total_size == 0);
pl_mutex_unlock(&p->lock);
}
static bool try_set(pl_cache cache, pl_cache_obj obj)
{
struct priv *p = PL_PRIV(cache);
// Remove any existing entry with this key
for (int i = p->objects.num - 1; i >= 0; i--) {
pl_cache_obj prev = p->objects.elem[i];
if (prev.key == obj.key) {
PL_TRACE(p, "Removing out-of-date object 0x%"PRIx64, prev.key);
remove_obj(cache, prev);
PL_ARRAY_REMOVE_AT(p->objects, i);
break;
}
}
if (!obj.size) {
PL_TRACE(p, "Deleted object 0x%"PRIx64, obj.key);
return true;
}
if (obj.size > cache->params.max_object_size) {
PL_DEBUG(p, "Object 0x%"PRIx64" (size %zu) exceeds max size %zu, discarding",
obj.key, obj.size, cache->params.max_object_size);
return false;
}
// Make space by deleting old objects
while (p->total_size + obj.size > cache->params.max_total_size ||
p->objects.num == INT_MAX)
{
pl_assert(p->objects.num);
pl_cache_obj old = p->objects.elem[0];
PL_TRACE(p, "Removing object 0x%"PRIx64" (size %zu) to make room",
old.key, old.size);
remove_obj(cache, old);
PL_ARRAY_REMOVE_AT(p->objects, 0);
}
if (!obj.free) {
obj.data = pl_memdup(NULL, obj.data, obj.size);
obj.free = pl_free;
}
PL_TRACE(p, "Inserting new object 0x%"PRIx64" (size %zu)", obj.key, obj.size);
PL_ARRAY_APPEND((void *) cache, p->objects, obj);
p->total_size += obj.size;
return true;
}
static pl_cache_obj strip_obj(pl_cache_obj obj)
{
return (pl_cache_obj) { .key = obj.key };
}
bool pl_cache_try_set(pl_cache cache, pl_cache_obj *pobj)
{
if (!cache)
return false;
pl_cache_obj obj = *pobj;
struct priv *p = PL_PRIV(cache);
pl_mutex_lock(&p->lock);
bool ok = try_set(cache, obj);
pl_mutex_unlock(&p->lock);
if (ok) {
*pobj = strip_obj(obj); // ownership transfers, clear ptr
} else {
obj = strip_obj(obj); // ownership remains with caller, clear copy
}
if (cache->params.set)
cache->params.set(cache->params.priv, obj);
return ok;
}
void pl_cache_set(pl_cache cache, pl_cache_obj *obj)
{
if (!pl_cache_try_set(cache, obj)) {
if (obj->free)
obj->free(obj->data);
*obj = (pl_cache_obj) { .key = obj->key };
}
}
static void noop(void *ignored)
{
(void) ignored;
}
bool pl_cache_get(pl_cache cache, pl_cache_obj *out_obj)
{
const uint64_t key = out_obj->key;
if (!cache)
goto fail;
struct priv *p = PL_PRIV(cache);
pl_mutex_lock(&p->lock);
// Search backwards to prioritize recently added entries
for (int i = p->objects.num - 1; i >= 0; i--) {
pl_cache_obj obj = p->objects.elem[i];
if (obj.key == key) {
PL_ARRAY_REMOVE_AT(p->objects, i);
p->total_size -= obj.size;
pl_mutex_unlock(&p->lock);
pl_assert(obj.free);
*out_obj = obj;
return true;
}
}
pl_mutex_unlock(&p->lock);
if (!cache->params.get)
goto fail;
pl_cache_obj obj = cache->params.get(cache->params.priv, key);
if (!obj.size)
goto fail;
// Sanitize object
obj.key = key;
obj.free = PL_DEF(obj.free, noop);
*out_obj = obj;
return true;
fail:
*out_obj = (pl_cache_obj) { .key = key };
return false;
}
void pl_cache_iterate(pl_cache cache,
void (*cb)(void *priv, pl_cache_obj obj),
void *priv)
{
if (!cache)
return;
struct priv *p = PL_PRIV(cache);
pl_mutex_lock(&p->lock);
for (int i = 0; i < p->objects.num; i++)
cb(priv, p->objects.elem[i]);
pl_mutex_unlock(&p->lock);
}
// --- Saving/loading
#define CACHE_MAGIC "pl_cache"
#define CACHE_VERSION 1
#define PAD_ALIGN(x) PL_ALIGN2(x, sizeof(uint32_t))
struct __attribute__((__packed__)) cache_header {
char magic[8];
uint32_t version;
uint32_t num_entries;
};
struct __attribute__((__packed__)) cache_entry {
uint64_t key;
uint64_t size;
uint64_t hash;
};
pl_static_assert(sizeof(struct cache_header) % alignof(struct cache_entry) == 0);
int pl_cache_save_ex(pl_cache cache,
void (*write)(void *priv, size_t size, const void *ptr),
void *priv)
{
if (!cache)
return 0;
struct priv *p = PL_PRIV(cache);
pl_mutex_lock(&p->lock);
pl_clock_t start = pl_clock_now();
const int num_objects = p->objects.num;
const size_t saved_bytes = p->total_size;
write(priv, sizeof(struct cache_header), &(struct cache_header) {
.magic = CACHE_MAGIC,
.version = CACHE_VERSION,
.num_entries = num_objects,
});
for (int i = 0; i < num_objects; i++) {
pl_cache_obj obj = p->objects.elem[i];
PL_TRACE(p, "Saving object 0x%"PRIx64" (size %zu)", obj.key, obj.size);
write(priv, sizeof(struct cache_entry), &(struct cache_entry) {
.key = obj.key,
.size = obj.size,
.hash = pl_mem_hash(obj.data, obj.size),
});
static const uint8_t padding[PAD_ALIGN(1)] = {0};
write(priv, obj.size, obj.data);
write(priv, PAD_ALIGN(obj.size) - obj.size, padding);
}
pl_mutex_unlock(&p->lock);
pl_log_cpu_time(p->log, start, pl_clock_now(), "saving cache");
if (num_objects)
PL_DEBUG(p, "Saved %d objects, totalling %zu bytes", num_objects, saved_bytes);
return num_objects;
}
int pl_cache_load_ex(pl_cache cache,
bool (*read)(void *priv, size_t size, void *ptr),
void *priv)
{
if (!cache)
return 0;
struct priv *p = PL_PRIV(cache);
struct cache_header header;
if (!read(priv, sizeof(header), &header)) {
PL_ERR(p, "Failed loading cache: file seems empty or truncated");
return -1;
}
if (memcmp(header.magic, CACHE_MAGIC, sizeof(header.magic)) != 0) {
PL_ERR(p, "Failed loading cache: invalid magic bytes");
return -1;
}
if (header.version != CACHE_VERSION) {
PL_INFO(p, "Failed loading cache: wrong version... skipping");
return 0;
}
if (header.num_entries > INT_MAX) {
PL_ERR(p, "Failed loading cache: %"PRIu32" entries overflows int",
header.num_entries);
return 0;
}
int num_loaded = 0;
size_t loaded_bytes = 0;
pl_mutex_lock(&p->lock);
pl_clock_t start = pl_clock_now();
for (int i = 0; i < header.num_entries; i++) {
struct cache_entry entry;
if (!read(priv, sizeof(entry), &entry)) {
PL_WARN(p, "Cache seems truncated, missing objects.. ignoring rest");
goto error;
}
if (entry.size > SIZE_MAX) {
PL_WARN(p, "Cache object size %"PRIu64" overflows SIZE_MAX.. "
"suspect broken file, ignoring rest", entry.size);
goto error;
}
void *buf = pl_alloc(NULL, PAD_ALIGN(entry.size));
if (!read(priv, PAD_ALIGN(entry.size), buf)) {
PL_WARN(p, "Cache seems truncated, missing objects.. ignoring rest");
pl_free(buf);
goto error;
}
uint64_t checksum = pl_mem_hash(buf, entry.size);
if (checksum != entry.hash) {
PL_WARN(p, "Cache entry seems corrupt, checksum mismatch.. ignoring rest");
pl_free(buf);
goto error;
}
pl_cache_obj obj = {
.key = entry.key,
.size = entry.size,
.data = buf,
.free = pl_free,
};
PL_TRACE(p, "Loading object 0x%"PRIx64" (size %zu)", obj.key, obj.size);
if (try_set(cache, obj)) {
num_loaded++;
loaded_bytes += entry.size;
} else {
pl_free(buf);
}
}
pl_log_cpu_time(p->log, start, pl_clock_now(), "loading cache");
if (num_loaded)
PL_DEBUG(p, "Loaded %d objects, totalling %zu bytes", num_loaded, loaded_bytes);
// fall through
error:
pl_mutex_unlock(&p->lock);
return num_loaded;
}
// Save/load wrappers
struct ptr_ctx {
uint8_t *data; // base pointer
size_t size; // total size
size_t pos; // read/write index
};
static void write_ptr(void *priv, size_t size, const void *ptr)
{
struct ptr_ctx *ctx = priv;
size_t end = PL_MIN(ctx->pos + size, ctx->size);
if (end > ctx->pos)
memcpy(ctx->data + ctx->pos, ptr, end - ctx->pos);
ctx->pos += size;
}
static bool read_ptr(void *priv, size_t size, void *ptr)
{
struct ptr_ctx *ctx = priv;
if (ctx->pos + size > ctx->size)
return false;
memcpy(ptr, ctx->data + ctx->pos, size);
ctx->pos += size;
return true;
}
size_t pl_cache_save(pl_cache cache, uint8_t *data, size_t size)
{
struct ptr_ctx ctx = { data, size };
pl_cache_save_ex(cache, write_ptr, &ctx);
return ctx.pos;
}
int pl_cache_load(pl_cache cache, const uint8_t *data, size_t size)
{
return pl_cache_load_ex(cache, read_ptr, &(struct ptr_ctx) {
.data = (uint8_t *) data,
.size = size,
});
}