This is an automated email from the git hooks/post-receive script.

git pushed a commit to reference refs/pull/114/head
in repository efl.

View the commit online.

commit 69232d7d1e04f7cb998b48005b3b6bb12ca9d6d3
Author: [email protected] <[email protected]>
AuthorDate: Mon Aug 10 20:56:15 2026 -0600

    fix(evas_ector_gl): do not evict gradient atlas rows in use this pass
    
    Span draws are batched and submitted at the end of the pass, and the atlas
    row coordinate travels per-vertex, so a row overwritten mid-pass changes the
    colour of shapes whose draws were already recorded.  Nothing prevented that:
    _alloc_row had no pin, refcount, or pre-eviction flush.
    
    The LRU was degenerate as well.  last_used is stamped with a per-pass
    counter, so once 64 rows are touched in one pass they all compare equal and
    the strict < never beats index 0 - the pass evicted its own earliest row,
    then did it again for every further gradient.
    
    Skip rows stamped with the current pass when choosing a victim, and when all
    of them are, drain the pending draws through a flush callback before
    reusing.  Two unit tests cover the exhaustion case; they need no GL context
    thanks to the existing test_skip_gl hook.
---
 .../engines/gl_generic/evas_ector_gl_grad_atlas.c  | 43 ++++++++++++---
 .../engines/gl_generic/evas_ector_gl_grad_atlas.h  |  9 ++++
 src/modules/evas/engines/gl_generic/evas_engine.c  | 12 +++++
 src/tests/ector/suite/ector_test_grad_atlas.c      | 62 ++++++++++++++++++++++
 4 files changed, 119 insertions(+), 7 deletions(-)

diff --git a/src/modules/evas/engines/gl_generic/evas_ector_gl_grad_atlas.c b/src/modules/evas/engines/gl_generic/evas_ector_gl_grad_atlas.c
index 9db85de342..fbb11bc01a 100644
--- a/src/modules/evas/engines/gl_generic/evas_ector_gl_grad_atlas.c
+++ b/src/modules/evas/engines/gl_generic/evas_ector_gl_grad_atlas.c
@@ -113,6 +113,15 @@ _ensure_gl(Span_Grad_Atlas *a)
    return 1;
 }
 
+void
+span_grad_atlas_flush_cb_set(Span_Grad_Atlas *a,
+                             void (*cb)(void *data), void *data)
+{
+   if (!a) return;
+   a->flush_cb   = cb;
+   a->flush_data = data;
+}
+
 /* Find row by (grad_id, version) — O(64). Returns row idx or -1. */
 static int
 _find_identity(Span_Grad_Atlas *a, void *grad_id, uint32_t version)
@@ -141,19 +150,39 @@ _find_by_content(Span_Grad_Atlas *a, uint32_t hash, const uint8_t *bytes)
    return -1;
 }
 
-/* Free row, else evict the smallest-last_used row.  Returns idx. */
+/* Free row, else evict the least recently used row that is NOT already in
+ * use by the current pass.
+ *
+ * Rows stamped with current_frame are referenced by draws that have been
+ * recorded but not yet submitted - span pushes are batched and flushed at
+ * the end of the pass, and grad_ramp_y travels per-vertex.  Overwriting such
+ * a row makes the earlier shape sample the newer ramp.  When every row is
+ * pinned, drain the pending draws first; after that the rows are free to
+ * reuse.
+ *
+ * This also repairs a degenerate LRU: last_used is a per-pass counter, so
+ * once the atlas fills within one pass every row compares equal and the
+ * strict < below never beats index 0, meaning the pass evicted its own
+ * earliest row every time. */
 static int
 _alloc_row(Span_Grad_Atlas *a)
 {
    for (int i = 0; i < SPAN_GRAD_ATLAS_H; i++)
      if (!a->rows[i].occupied) return i;
 
-   int      best     = 0;
-   uint32_t best_age = a->rows[0].last_used;
-   for (int i = 1; i < SPAN_GRAD_ATLAS_H; i++)
-     if (a->rows[i].last_used < best_age)
-       { best = i; best_age = a->rows[i].last_used; }
-   return best;
+   int      best     = -1;
+   uint32_t best_age = 0;
+   for (int i = 0; i < SPAN_GRAD_ATLAS_H; i++)
+     {
+        if (a->rows[i].last_used == a->current_frame) continue; /* pinned */
+        if (best < 0 || a->rows[i].last_used < best_age)
+          { best = i; best_age = a->rows[i].last_used; }
+     }
+   if (best >= 0) return best;
+
+   /* Every row is pinned by this pass.  Drain, then any row may be reused. */
+   if (a->flush_cb) a->flush_cb(a->flush_data);
+   return 0;
 }
 
 /* Upload bytes to row idx via glTexSubImage2D and copy to mirror.
diff --git a/src/modules/evas/engines/gl_generic/evas_ector_gl_grad_atlas.h b/src/modules/evas/engines/gl_generic/evas_ector_gl_grad_atlas.h
index 02b2669e08..acd38a578a 100644
--- a/src/modules/evas/engines/gl_generic/evas_ector_gl_grad_atlas.h
+++ b/src/modules/evas/engines/gl_generic/evas_ector_gl_grad_atlas.h
@@ -49,6 +49,8 @@ struct _Span_Grad_Atlas
    Span_Grad_Atlas_Row  rows[SPAN_GRAD_ATLAS_H];
    uint32_t             current_frame;        /* monotonic, incremented per render pass */
    int                  disabled;             /* 1 if alloc failed; gradient path skips */
+   void               (*flush_cb)(void *data);  /* drains pending draws */
+   void                *flush_data;
 #ifdef SPAN_GRAD_ATLAS_TEST_BUILD
    int                  test_skip_gl;         /* bypass GL; uploads are no-ops */
 #endif
@@ -63,6 +65,13 @@ void span_grad_atlas_free(Span_Grad_Atlas *a);
 /* Begin a new render pass — bumps the LRU frame counter. */
 void span_grad_atlas_frame_begin(Span_Grad_Atlas *a);
 
+/* Register a callback that drains any draw calls referencing atlas rows.
+ * Called when every row has already been used in the current pass and a new
+ * ramp needs one; without it an in-use row would be overwritten before the
+ * draws referencing it were submitted. */
+void span_grad_atlas_flush_cb_set(Span_Grad_Atlas *a,
+                                  void (*cb)(void *data), void *data);
+
 /* Hash 4096 bytes of ramp content.  Public so tests can reach it. */
 uint32_t span_grad_atlas_hash(const uint8_t *bytes);
 
diff --git a/src/modules/evas/engines/gl_generic/evas_engine.c b/src/modules/evas/engines/gl_generic/evas_engine.c
index 63a022c471..a6c54d0a75 100644
--- a/src/modules/evas/engines/gl_generic/evas_engine.c
+++ b/src/modules/evas/engines/gl_generic/evas_engine.c
@@ -150,6 +150,15 @@ egl_display_get(Render_Engine_GL_Generic *engine)
 
 void eng_image_free(void *engine, void *image);
 
+/* Drain span draws that reference atlas rows, so a row can be safely
+ * overwritten.  See _alloc_row in evas_ector_gl_grad_atlas.c. */
+static void
+_span_grad_atlas_flush_cb(void *data)
+{
+   Evas_Engine_GL_Context *gc = gl_generic_context_find(data, EINA_FALSE);
+   if (gc) evas_gl_common_context_flush(gc);
+}
+
 static void *
 eng_engine_new(void)
 {
@@ -162,6 +171,9 @@ eng_engine_new(void)
    /* Gradient ramp atlas: NULL return means atlas unavailable — gradient
     * shapes will be skipped per the spec error table (no-op, non-fatal). */
    engine->grad_atlas = span_grad_atlas_new();
+   if (engine->grad_atlas)
+     span_grad_atlas_flush_cb_set(engine->grad_atlas,
+                                  _span_grad_atlas_flush_cb, engine);
 
    return engine;
 }
diff --git a/src/tests/ector/suite/ector_test_grad_atlas.c b/src/tests/ector/suite/ector_test_grad_atlas.c
index 5b23d8817e..48eb2fd5c2 100644
--- a/src/tests/ector/suite/ector_test_grad_atlas.c
+++ b/src/tests/ector/suite/ector_test_grad_atlas.c
@@ -195,6 +195,66 @@ EFL_START_TEST(grad_atlas_version_change_evicts_or_refreshes)
 }
 EFL_END_TEST
 
+/* Counts flush-callback invocations for the exhaustion test. */
+static int _flush_calls = 0;
+static void _count_flush(void *data EINA_UNUSED) { _flush_calls++; }
+
+EFL_START_TEST(grad_atlas_no_eviction_of_rows_used_this_frame)
+{
+   Span_Grad_Atlas *a = span_grad_atlas_new();
+   ck_assert_ptr_nonnull(a);
+   span_grad_atlas_test_enable(a);
+   _flush_calls = 0;
+   span_grad_atlas_flush_cb_set(a, _count_flush, NULL);
+
+   uint8_t ramp[SPAN_GRAD_ATLAS_ROW_BYTES];
+
+   /* Fill all 64 rows within a SINGLE frame. */
+   span_grad_atlas_frame_begin(a);
+   int rows[SPAN_GRAD_ATLAS_H];
+   for (int i = 0; i < SPAN_GRAD_ATLAS_H; i++)
+     {
+        _fill_ramp(ramp, (uint32_t)(i + 1));
+        rows[i] = span_grad_atlas_lookup(a, (void *)(uintptr_t)(0x2000 + i),
+                                         (uint32_t)(i + 1), ramp);
+        ck_assert_int_ge(rows[i], 0);
+     }
+
+   /* No flush needed yet: every row was free when it was taken. */
+   ck_assert_int_eq(_flush_calls, 0);
+
+   /* The 65th distinct ramp, still in the same frame, must force a flush
+    * before reusing a row that this frame's pending draws still reference. */
+   _fill_ramp(ramp, 9999);
+   int new_row = span_grad_atlas_lookup(a, (void *)0xCAFE, 9999, ramp);
+   ck_assert_int_ge(new_row, 0);
+   ck_assert_int_eq(_flush_calls, 1);
+
+   span_grad_atlas_free(a);
+}
+EFL_END_TEST
+
+EFL_START_TEST(grad_atlas_flush_cb_optional)
+{
+   /* With no callback registered the atlas must still make progress rather
+    * than fail the lookup. */
+   Span_Grad_Atlas *a = span_grad_atlas_new();
+   ck_assert_ptr_nonnull(a);
+   span_grad_atlas_test_enable(a);
+
+   uint8_t ramp[SPAN_GRAD_ATLAS_ROW_BYTES];
+   span_grad_atlas_frame_begin(a);
+   for (int i = 0; i < SPAN_GRAD_ATLAS_H + 1; i++)
+     {
+        _fill_ramp(ramp, (uint32_t)(i + 1));
+        int r = span_grad_atlas_lookup(a, (void *)(uintptr_t)(0x3000 + i),
+                                       (uint32_t)(i + 1), ramp);
+        ck_assert_int_ge(r, 0);
+     }
+   span_grad_atlas_free(a);
+}
+EFL_END_TEST
+
 void
 ector_test_grad_atlas(TCase *tc)
 {
@@ -204,4 +264,6 @@ ector_test_grad_atlas(TCase *tc)
    tcase_add_test(tc, grad_atlas_content_dedup_across_distinct_grad_ids);
    tcase_add_test(tc, grad_atlas_hash_collision_distinguished_by_memcmp);
    tcase_add_test(tc, grad_atlas_version_change_evicts_or_refreshes);
+   tcase_add_test(tc, grad_atlas_no_eviction_of_rows_used_this_frame);
+   tcase_add_test(tc, grad_atlas_flush_cb_optional);
 }

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.

Reply via email to