summaryrefslogtreecommitdiff
path: root/src/texturecache.c
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-13 09:04:46 +0300
committerPaul Buetow <paul@buetow.org>2026-07-13 09:04:46 +0300
commit48aa1b3cdbf27be2e016e8b532687226f9249043 (patch)
tree89d3ceab9194d755aeb3219dda5c84cbfe8370f1 /src/texturecache.c
parent89727590e718d3628a0847689182e896df646b8a (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.c117
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