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.

Reply via email to