This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch span-gl-clean
in repository efl.
View the commit online.
commit 76f8edddd3fb399a2635ea3a8064be225d557901
Author: Cedric BAIL <[email protected]>
AuthorDate: Fri Aug 14 08:02:16 2026 -0600
fix(evas): budget the ector surface cache by bytes, not entry count
Instrumenting the VG surface cache while looking at why every expedite
VG frame re-rasterizes showed a 0% hit rate: over 300 frames of test
119 with 128 vector objects, _render_to_buffer ran 38272 times, every
one of them a miss, while the vector content changed only on the first
frame.
The cache was capped at fifty entries. Evas walks objects in the same
order every frame, so 128 objects through a 50-entry LRU is a cyclic
scan - the access pattern LRU handles worst. Each insertion evicts the
entry that will be wanted next, and the hit rate is not merely poor but
exactly zero. That is worse than having no cache at all: every frame
pays the insert, the eviction, the surface free, and then the full
rasterization anyway.
Fifty entries also cannot express what this cache holds. It stores
rendered vector surfaces, and fifty 4K surfaces are hundreds of
megabytes while three hundred icon-sized ones are a few. Budget by
bytes instead: the cache takes an optional size callback, tracks the
total, and trims from the least-recently-used end until it is back
inside 8 MB, overridable through EVAS_SURFACE_CACHE_SIZE in kilobytes.
A 4096-entry ceiling remains so that a cache of very small surfaces
cannot grow the list without bound, and a NULL size callback keeps the
old count behaviour for any caller that does not opt in. Both engines
that own such a cache - gl_generic and software_generic - report the
32bpp size of their render targets.
Two smaller things in the same code:
- Eviction gave up at the first entry still handed out (ref > 1)
instead of skipping it, so one long-lived surface could pin the
cache above its budget indefinitely. The sweep now skips them.
- Every lookup walked the LRU list to find the node to promote. The
entry now holds its own node, making promotion O(1); at 128 lookups
a frame against a cache large enough to be useful, the walk was
becoming the thing it was meant to avoid.
Expedite VG tests, best of two 300-frame runs, gl_generic:
117 VG Basic Rect 176 -> 1203 6.8x
118 VG Basic Circle 157 -> 1223 7.8x
119 VG Basic Gradient 138 -> 1185 8.6x
120 VG Radial Gradient 139 -> 1188 8.6x
121 VG Basic Batman 111 -> 1104 10.0x
123 VG Basic Composite 173 -> 1088 6.3x
126 VG Grad Multi-Shape 90 -> 952 10.6x
Test 122 has a single vector object, so it never exceeded fifty
entries and is unchanged. Test 125 resizes every object every frame,
which drops the cached surface legitimately, and is also unchanged.
This is independent of the span-buffer work: the cache is untouched by
that series, so the same 0% hit rate applies on master and to the
software engine, where test 119 in the buffer engine now also runs at
over 1200 FPS.
All ten VG expedite tests render byte-identical frames; ector-suite and
evas-suite pass.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
src/lib/evas/common/evas_common_generic_cache.c | 107 ++++++++++++++++-----
src/lib/evas/include/evas_common_private.h | 13 +++
src/modules/evas/engines/gl_generic/evas_engine.c | 13 +++
.../evas/engines/software_generic/evas_engine.c | 13 +++
4 files changed, 124 insertions(+), 22 deletions(-)
diff --git a/src/lib/evas/common/evas_common_generic_cache.c b/src/lib/evas/common/evas_common_generic_cache.c
index db979de552..d17245440e 100644
--- a/src/lib/evas/common/evas_common_generic_cache.c
+++ b/src/lib/evas/common/evas_common_generic_cache.c
@@ -1,5 +1,64 @@
#include "evas_common_private.h"
+/* Default memory budget for a size-aware generic cache. Override with
+ * EVAS_SURFACE_CACHE_SIZE, in kilobytes. */
+#define GENERIC_CACHE_DEFAULT_BUDGET (8 * 1024 * 1024)
+
+/* Hard ceiling on entries, so a cache of tiny surfaces cannot grow the LRU
+ * list without bound. Only reached when the byte budget has not been. */
+#define GENERIC_CACHE_MAX_ENTRIES 4096
+
+static size_t
+_generic_cache_budget(void)
+{
+ static size_t v = 0;
+
+ if (!v)
+ {
+ const char *e = getenv("EVAS_SURFACE_CACHE_SIZE");
+ long kb = e ? atol(e) : 0;
+
+ v = (kb > 0) ? (size_t)kb * 1024 : GENERIC_CACHE_DEFAULT_BUDGET;
+ }
+ return v;
+}
+
+/* Drop entries from the least-recently-used end until the cache is back
+ * inside its budget. Entries still handed out (ref > 1) are skipped rather
+ * than aborting the sweep - stopping at the first one would let a single
+ * long-lived surface pin the cache above its budget forever. */
+static void
+_generic_cache_trim(Generic_Cache *cache)
+{
+ Eina_List *l, *prev;
+ int count = (int)eina_list_count(cache->lru_list);
+
+ if (!cache->size_func && count <= 50) return;
+
+ for (l = eina_list_last(cache->lru_list); l; l = prev)
+ {
+ Generic_Cache_Entry *entry = eina_list_data_get(l);
+
+ if (cache->size_func)
+ {
+ if (cache->bytes <= cache->budget &&
+ count <= GENERIC_CACHE_MAX_ENTRIES) break;
+ }
+ else if (count <= 50) break;
+
+ prev = eina_list_prev(l);
+ if (!entry || entry->ref > 1) continue;
+
+ eina_hash_del(cache->hash, &entry->key, entry);
+ cache->lru_list = eina_list_remove_list(cache->lru_list, l);
+ if (cache->bytes >= entry->size) cache->bytes -= entry->size;
+ else cache->bytes = 0;
+ count--;
+ cache->free_func(cache->user_data, entry->data);
+ free(entry);
+ }
+}
+
EVAS_API Generic_Cache*
generic_cache_new(void *user_data, Generic_Cache_Free func)
{
@@ -8,6 +67,7 @@ generic_cache_new(void *user_data, Generic_Cache_Free func)
cache->hash = eina_hash_int32_new(NULL);
cache->user_data = user_data;
cache->free_func = func;
+ cache->budget = _generic_cache_budget();
return cache;
}
@@ -39,53 +99,51 @@ generic_cache_dump(Generic_Cache *cache)
cache->free_func(cache->user_data, entry->data);
free(entry);
}
+ cache->bytes = 0;
}
}
+EVAS_API void
+generic_cache_size_func_set(Generic_Cache *cache, Generic_Cache_Size func)
+{
+ if (cache) cache->size_func = func;
+}
+
EVAS_API void
generic_cache_data_set(Generic_Cache *cache, void *key, void *surface)
{
Generic_Cache_Entry *entry = NULL;
- int count;
entry = calloc(1, sizeof(Generic_Cache_Entry));
+ if (!entry) return;
entry->key = key;
entry->data = ""
entry->ref = 1;
+ if (cache->size_func) entry->size = cache->size_func(cache->user_data, surface);
eina_hash_add(cache->hash, &key, entry);
cache->lru_list = eina_list_prepend(cache->lru_list, entry);
- count = eina_list_count(cache->lru_list);
- if (count > 50)
- {
- entry = eina_list_data_get(eina_list_last(cache->lru_list));
- // if its still being ref.
- if (entry->ref > 1) return;
- eina_hash_del(cache->hash, &entry->key, entry);
- cache->lru_list = eina_list_remove_list(cache->lru_list, eina_list_last(cache->lru_list));
- cache->free_func(cache->user_data, entry->data);
- free(entry);
- }
+ entry->node = cache->lru_list;
+ cache->bytes += entry->size;
+
+ _generic_cache_trim(cache);
}
EVAS_API void *
generic_cache_data_get(Generic_Cache *cache, void *key)
{
- Generic_Cache_Entry *entry = NULL, *lru_data;
- Eina_List *l;
+ Generic_Cache_Entry *entry = NULL;
entry = eina_hash_find(cache->hash, &key);
if (entry)
{
// update the ref
entry->ref += 1;
- // promote in lru
- EINA_LIST_FOREACH(cache->lru_list, l, lru_data)
+ // promote in lru - the entry knows its own node, so this does not
+ // walk the list. It used to, on every lookup.
+ if (entry->node)
{
- if (lru_data == entry)
- {
- cache->lru_list = eina_list_promote_list(cache->lru_list, l);
- break;
- }
+ cache->lru_list = eina_list_promote_list(cache->lru_list, entry->node);
+ entry->node = cache->lru_list;
}
return entry->data;
}
@@ -105,7 +163,12 @@ generic_cache_data_drop(Generic_Cache *cache, void *key)
if (entry->ref) return;
eina_hash_del(cache->hash, &entry->key, entry);
// find and remove from lru list
- cache->lru_list = eina_list_remove(cache->lru_list, entry);
+ if (entry->node)
+ cache->lru_list = eina_list_remove_list(cache->lru_list, entry->node);
+ else
+ cache->lru_list = eina_list_remove(cache->lru_list, entry);
+ if (cache->bytes >= entry->size) cache->bytes -= entry->size;
+ else cache->bytes = 0;
cache->free_func(cache->user_data, entry->data);
free(entry);
}
diff --git a/src/lib/evas/include/evas_common_private.h b/src/lib/evas/include/evas_common_private.h
index 3758ec61ee..1adb9c93ed 100644
--- a/src/lib/evas/include/evas_common_private.h
+++ b/src/lib/evas/include/evas_common_private.h
@@ -1146,9 +1146,13 @@ struct _Generic_Cache_Entry
void *key; // pointer
void *data; // engine image
int ref;
+ size_t size; // bytes this entry accounts for, 0 if unknown
+ Eina_List *node; // own node in lru_list, for O(1) promotion
};
typedef void (*Generic_Cache_Free)(void *user_data, void *data);
+/** Report how many bytes @p data occupies, for the cache's memory budget. */
+typedef size_t (*Generic_Cache_Size)(void *user_data, void *data);
struct _Generic_Cache
{
@@ -1156,9 +1160,18 @@ struct _Generic_Cache
Eina_List *lru_list;
void *user_data;
Generic_Cache_Free free_func;
+ /* Budgeting by bytes rather than by entry count. A count cap cannot
+ * describe this cache: fifty 4K surfaces are hundreds of megabytes while
+ * three hundred icon-sized ones are a few. NULL size_func falls back to
+ * the count cap. */
+ Generic_Cache_Size size_func;
+ size_t bytes;
+ size_t budget;
};
EVAS_API Generic_Cache* generic_cache_new(void *user_data, Generic_Cache_Free func);
+/** Switch @p cache to a memory budget, using @p func to size entries. */
+EVAS_API void generic_cache_size_func_set(Generic_Cache *cache, Generic_Cache_Size func);
EVAS_API void generic_cache_destroy(Generic_Cache *cache);
EVAS_API void generic_cache_dump(Generic_Cache *cache);
EVAS_API void generic_cache_data_set(Generic_Cache *cache, void *key, void *data);
diff --git a/src/modules/evas/engines/gl_generic/evas_engine.c b/src/modules/evas/engines/gl_generic/evas_engine.c
index a869371819..fb9890684a 100644
--- a/src/modules/evas/engines/gl_generic/evas_engine.c
+++ b/src/modules/evas/engines/gl_generic/evas_engine.c
@@ -159,6 +159,17 @@ _span_grad_atlas_flush_cb(void *data)
if (gc) evas_gl_common_context_flush(gc);
}
+/* Byte size of a cached ector surface, for the surface cache's memory
+ * budget. These are render targets, so they are always 32bpp. */
+static size_t
+_ector_surface_cache_size(void *engine EINA_UNUSED, void *surface)
+{
+ Evas_GL_Image *im = surface;
+
+ if (!im || im->w <= 0 || im->h <= 0) return 0;
+ return (size_t)im->w * (size_t)im->h * 4;
+}
+
static void *
eng_engine_new(void)
{
@@ -167,6 +178,8 @@ eng_engine_new(void)
engine = calloc(1, sizeof (Render_Engine_GL_Generic));
if (!engine) return NULL;
engine->software.surface_cache = generic_cache_new(engine, eng_image_free);
+ generic_cache_size_func_set(engine->software.surface_cache,
+ _ector_surface_cache_size);
/* Gradient ramp atlas: NULL return means atlas unavailable — gradient
* shapes will be skipped per the spec error table (no-op, non-fatal). */
diff --git a/src/modules/evas/engines/software_generic/evas_engine.c b/src/modules/evas/engines/software_generic/evas_engine.c
index 77455962a2..0e5106d789 100644
--- a/src/modules/evas/engines/software_generic/evas_engine.c
+++ b/src/modules/evas/engines/software_generic/evas_engine.c
@@ -3780,6 +3780,17 @@ eng_gl_rotation_angle_get(void *data EINA_UNUSED)
//------------------------------------------------//
+/* Byte size of a cached ector surface, for the surface cache's memory
+ * budget. These are render targets, so they are always 32bpp. */
+static size_t
+_ector_surface_cache_size(void *engine EINA_UNUSED, void *surface)
+{
+ Image_Entry *ie = surface;
+
+ if (!ie) return 0;
+ return (size_t)ie->w * (size_t)ie->h * 4;
+}
+
/* The following function require that any engine
inheriting from software generic to have at the
top of their render engine structure a
@@ -3796,6 +3807,8 @@ eng_engine_new(void)
if (!engine) return NULL;
engine->surface_cache = generic_cache_new(engine, eng_image_free);
+ generic_cache_size_func_set(engine->surface_cache,
+ _ector_surface_cache_size);
return engine;
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.