diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-13 09:04:46 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-13 09:04:46 +0300 |
| commit | 48aa1b3cdbf27be2e016e8b532687226f9249043 (patch) | |
| tree | 89d3ceab9194d755aeb3219dda5c84cbfe8370f1 /src/texturecache.c | |
| parent | 89727590e718d3628a0847689182e896df646b8a (diff) | |
responsive: async loader, texture cache LRU, prefetch, tests (it0)
M3 (task it0): responsive + prefetch.
- src/loader/loader.{c,h}: loader_load_async/_finish wrap the sync
loader_load in a GTask worker; the task's source object is the GFile so the
finish callback can check it against navigator.current (last-write-wins).
- src/texturecache.{c,h}: bounded LRU (cap 4) of GFile->GdkTexture, hash+GQueue
O(1) get/put, entry owns the key (no double-unref), evicts LRU on overflow.
- src/window.c: async visible load (cancel-then-recreate single GCancellable);
cache hit -> show+prefetch, miss -> async load; last-write-wins guard
(g_file_equal(current, loaded)) before showing; prefetch next/prev into the
cache via a separate prefetch cancellable (cancelled each round). Finish
callbacks hold a ref on the window (released in the callback) so they never
deref a freed window; dispose cancels both cancellables.
- tests: unit test_texturecache (LRU evict/order/replace/miss), integration
test_responsive_nav (10 rapid next -> last-write-wins). M1/M2 tests updated
to pump the main loop (loads are now async) + drain_main before exit.
- meson: texturecache in libggae; GIO_USE_VFS=local in fixtures_env.
Fixed: texturecache double-unref (key ownership), async-callback UAF (window
ref in callback data + drain in tests), a test g_build_filename leak. Sub-agent
review issues addressed (dispose + cache-hit cancel in-flight loads).
Diffstat (limited to 'src/texturecache.c')
| -rw-r--r-- | src/texturecache.c | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/src/texturecache.c b/src/texturecache.c new file mode 100644 index 0000000..f92f0c4 --- /dev/null +++ b/src/texturecache.c @@ -0,0 +1,117 @@ +/*:* + * ggaze — decoded-texture cache + * + * Bounded LRU (GFile -> GdkTexture) with O(1) get/put via a hash mapping keys + * to GQueue nodes. Main-thread only. + * + * Copyright (c) 2026 ggaze contributors + * SPDX-License-Identifier: GPL-3.0-or-later + *:*/ + +#include "texturecache.h" + +typedef struct { + GFile *file; /* owned ref */ + GdkTexture *tex; /* owned ref */ + GList *link; /* node in the order queue (MRU at tail) */ +} CacheEntry; + +struct TextureCache { + guint u_cap; + GHashTable *p_map; /* GFile* (owned) -> CacheEntry* (owned) */ + GQueue *p_order; /* CacheEntry* MRU at tail */ +}; + +static void +_entry_free(gpointer p_void) { + CacheEntry *p_e = (CacheEntry *)p_void; + g_clear_object(&p_e->file); + g_clear_object(&p_e->tex); + g_free(p_e); +} + +TextureCache * +texturecache_new(guint u_cap) { + TextureCache *p_c = g_new(TextureCache, 1); + p_c->u_cap = (u_cap == 0) ? 1 : u_cap; + p_c->p_map = + g_hash_table_new_full((GHashFunc)g_file_hash, (GEqualFunc)g_file_equal, + NULL, _entry_free); /* entry owns the key */ + p_c->p_order = g_queue_new(); + return (p_c); +} + +void +texturecache_delete(TextureCache *p_cache) { + if (p_cache == NULL) { + return; + } + /* Clearing the hash frees entries (which are not in the queue order list as + * separate refs — the queue holds the same pointers, so free the queue list + * itself without touching the data). */ + g_queue_free(p_cache->p_order); + g_hash_table_unref(p_cache->p_map); + g_free(p_cache); +} + +GdkTexture * +texturecache_get(TextureCache *p_cache, GFile *p_file) { + g_return_val_if_fail(p_cache != NULL, NULL); + CacheEntry *p_e = (CacheEntry *)g_hash_table_lookup(p_cache->p_map, p_file); + if (p_e == NULL) { + return (NULL); + } + /* Mark most-recently-used: move to tail. */ + g_queue_unlink(p_cache->p_order, p_e->link); + g_queue_push_tail_link(p_cache->p_order, p_e->link); + return (p_e->tex); +} + +void +texturecache_put(TextureCache *p_cache, GFile *p_file, GdkTexture *p_tex) { + g_return_if_fail(p_cache != NULL); + g_return_if_fail(G_IS_FILE(p_file)); + g_return_if_fail(GDK_IS_TEXTURE(p_tex)); + + CacheEntry *p_e = (CacheEntry *)g_hash_table_lookup(p_cache->p_map, p_file); + if (p_e != NULL) { + /* Replace the texture; keep MRU position fresh. */ + g_set_object(&p_e->tex, p_tex); + g_queue_unlink(p_cache->p_order, p_e->link); + g_queue_push_tail_link(p_cache->p_order, p_e->link); + return; + } + + p_e = g_new(CacheEntry, 1); + p_e->file = (GFile *)g_object_ref(p_file); + p_e->tex = (GdkTexture *)g_object_ref(p_tex); + p_e->link = g_list_alloc(); + p_e->link->data = p_e; + g_queue_push_tail_link(p_cache->p_order, p_e->link); + g_hash_table_insert(p_cache->p_map, p_e->file, p_e); + + /* Evict LRU (head) while over capacity. */ + while (g_queue_get_length(p_cache->p_order) > p_cache->u_cap) { + GList *p_head = g_queue_pop_head_link(p_cache->p_order); + CacheEntry *p_old = (CacheEntry *)p_head->data; + /* Removing from the hash frees the entry (and its file/tex). The link is + * freed by g_list_free below. */ + g_hash_table_remove(p_cache->p_map, p_old->file); + g_list_free(p_head); + } +} + +guint +texturecache_get_size(TextureCache *p_cache) { + g_return_val_if_fail(p_cache != NULL, 0); + return ((guint)g_queue_get_length(p_cache->p_order)); +} + +void +texturecache_clear(TextureCache *p_cache) { + g_return_if_fail(p_cache != NULL); + /* Removing all hash entries frees the CacheEntry structs; the queue list + * nodes are cleared without freeing the data again. */ + g_queue_clear(p_cache->p_order); + g_hash_table_remove_all(p_cache->p_map); +}
\ No newline at end of file |
