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 171ada3dd56b7fdd066c77a9c607f5f5409de8cb
Author: [email protected] <[email protected]>
AuthorDate: Mon Aug 10 21:34:39 2026 -0600

    fix(evas_ector_gl): apply VG composite masks in span-fallback mode
    
    Whole-branch review of the Stage-1 span-buffer remediation found that
    efl_canvas_vg_container.c's GL-mask path is gated only on
    ENFN->gl_surface_read_pixels, not on span_path_usable(). With
    EVAS_GL_SPAN_TIER=off, eng_ector_mask_surface_create delegated to the
    CPU-backed eng_ector_surface_create, so the container rasterized the
    mask, uploaded it, then returned NULL for "no CPU comp buffer" -
    skipping the gl_mask_fallback: path that actually applies a mask in
    software. The mask was computed and silently discarded. Confirmed via
    expedite-imgcmp: test 124 (VG Composite All Modes) passed on the span
    path but failed 9.56% over tolerance under EVAS_GL_SPAN_TIER=off.
    
    Fix: eng_ector_mask_surface_create() now fails (*error = EINA_TRUE,
    return NULL) when the span path is unusable, so the container's own
    gl_mask_fallback logic runs instead. After the fix both test 123 and
    124 pass against the software backend at tolerance 8/0.02% (previously
    22.84% and 9.56% over tolerance).
    
    Also, from the same review:
    
    - _span_tier_get() no longer memoizes SPAN_TIER_OFF when
      glGetIntegerv(GL_MAX_VERTEX_ATTRIBS/VARYING_VECTORS) both return 0,
      which indicates the query failed (e.g. no current GL context) rather
      than a genuine zero-capability device. The resolution is left
      pending so a later call, once a context is current, can retry.
    
    - span_shader_shutdown() now also resets _span_tier_resolved and
      _span_fs_highp, not just _span_shader_state, so a shutdown/reinit
      cycle re-probes everything consistently - this is what would make
      the above retry actually effective across a context loss.
    
    - Removed span_shader_available(): dead code, zero callers since
      span_path_usable() replaced it in Tasks 4/5.
    
    - evas_ector_gl_grad_atlas.c: guard `current_frame - 1` against
      uint32_t underflow when the all-pinned _alloc_row() branch is
      reached before the first frame_begin() (current_frame == 0).
      Unguarded, every row's age wraps to UINT32_MAX and never compares
      less again, permanently flattening the atlas's LRU to always
      returning row 0. Added a regression test exercising this path.
    
    - Removed a dangling "CONTROLLER DIRECTION" doc cross-reference in
      the Stage 1 pre-notes baseline; the referenced section was never
      written.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../engines/gl_generic/evas_ector_gl_grad_atlas.c  |  7 ++-
 .../evas/engines/gl_generic/evas_ector_gl_span.h   |  3 --
 .../engines/gl_generic/evas_ector_gl_span_shader.c | 19 +++++---
 src/modules/evas/engines/gl_generic/evas_engine.c  | 11 ++++-
 src/tests/ector/suite/ector_test_grad_atlas.c      | 55 ++++++++++++++++++++++
 5 files changed, 83 insertions(+), 12 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 0cf1f8382a..2389ffae74 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
@@ -190,7 +190,12 @@ _alloc_row(Span_Grad_Atlas *a)
         a->flush_cb(a->flush_data);
         for (int i = 0; i < SPAN_GRAD_ATLAS_H; i++)
           if (a->rows[i].last_used == a->current_frame)
-            a->rows[i].last_used = a->current_frame - 1;
+            /* current_frame is uint32_t; guard against underflow when this
+             * branch is reached before the first frame_begin() (current_frame
+             * == 0).  Without the guard every row's age would wrap to
+             * UINT32_MAX and never be beaten again, flattening the LRU for
+             * the atlas's lifetime. */
+            a->rows[i].last_used = a->current_frame ? a->current_frame - 1 : 0;
 
         best     = 0;
         best_age = a->rows[0].last_used;
diff --git a/src/modules/evas/engines/gl_generic/evas_ector_gl_span.h b/src/modules/evas/engines/gl_generic/evas_ector_gl_span.h
index 7bcc10b2bf..e65331360e 100644
--- a/src/modules/evas/engines/gl_generic/evas_ector_gl_span.h
+++ b/src/modules/evas/engines/gl_generic/evas_ector_gl_span.h
@@ -377,9 +377,6 @@ void span_collector_delete_textures(Span_Collector *sc);
  */
 Eina_Bool span_shader_init(void);
 
-/* EINA_TRUE only when span_shader_init() has run and every variant linked. */
-Eina_Bool span_shader_available(void);
-
 /* EINA_TRUE when the span rendering path may be used: the tier setting
  * permits it and the shaders linked.  When EINA_FALSE the caller must not
  * install span collectors. */
diff --git a/src/modules/evas/engines/gl_generic/evas_ector_gl_span_shader.c b/src/modules/evas/engines/gl_generic/evas_ector_gl_span_shader.c
index ce2d432b2e..05a6ec5721 100644
--- a/src/modules/evas/engines/gl_generic/evas_ector_gl_span_shader.c
+++ b/src/modules/evas/engines/gl_generic/evas_ector_gl_span_shader.c
@@ -282,6 +282,17 @@ _span_tier_get(void)
    varyings /= 4;
 #endif
 
+   if (attribs == 0 && varyings == 0)
+     {
+        /* Both queries came back 0: this means glGetIntegerv failed (e.g. no
+         * current GL context yet), not that the device genuinely reports 0
+         * attributes/varyings.  Do not memoize — leave _span_tier_resolved
+         * unresolved so the next call (once a context is current) retries. */
+        INF("span tier query returned 0/0; assuming no current GL context, "
+            "will retry on next call");
+        return SPAN_TIER_OFF;
+     }
+
    if (attribs  < SPAN_WIDE_MAX_ATTRIBS ||
        varyings < SPAN_WIDE_MAX_VARYINGS)
      {
@@ -962,12 +973,6 @@ span_shader_init(void)
    return EINA_TRUE;
 }
 
-Eina_Bool
-span_shader_available(void)
-{
-   return _span_shader_state == SPAN_SHADER_OK;
-}
-
 Eina_Bool
 span_path_usable(void)
 {
@@ -981,6 +986,8 @@ span_shader_shutdown(void)
    int kind, b, mask;
 
    _span_shader_state = SPAN_SHADER_UNTRIED;
+   _span_tier_resolved = -1;
+   _span_fs_highp = -1;
 
    for (kind = 0; kind < 2; kind++)
      for (b = 0; b < (int)SPAN_BIND_COUNT; b++)
diff --git a/src/modules/evas/engines/gl_generic/evas_engine.c b/src/modules/evas/engines/gl_generic/evas_engine.c
index 089de72ed5..153d9353d6 100644
--- a/src/modules/evas/engines/gl_generic/evas_engine.c
+++ b/src/modules/evas/engines/gl_generic/evas_engine.c
@@ -2660,8 +2660,15 @@ eng_ector_mask_surface_create(void *engine, int width, int height, int *error)
      }
    else
      {
-        /* CPU fallback: no atlas concern, delegate to the regular path. */
-        return eng_ector_surface_create(engine, width, height, error);
+        /* Span path unusable: this GL-mask branch has no CPU-backed
+         * counterpart that eng_ector_end() knows how to consume — the mask
+         * it produces is only ever read from espd->gl_comp_surface inside
+         * the span draw loop.  Fail here so efl_canvas_vg_container.c's
+         * caller takes its "gl_mask_fallback" path instead, which uses the
+         * software rasterizer's own CPU comp-buffer masking and is the one
+         * that actually applies the mask in fallback mode. */
+        *error = EINA_TRUE;
+        return NULL;
      }
 }
 
diff --git a/src/tests/ector/suite/ector_test_grad_atlas.c b/src/tests/ector/suite/ector_test_grad_atlas.c
index 536904edfa..bfc54b89c4 100644
--- a/src/tests/ector/suite/ector_test_grad_atlas.c
+++ b/src/tests/ector/suite/ector_test_grad_atlas.c
@@ -280,6 +280,60 @@ EFL_START_TEST(grad_atlas_flush_cb_optional)
 }
 EFL_END_TEST
 
+/* Reproduces the all-pinned branch of _alloc_row() being reached while
+ * current_frame == 0 (i.e. before the first span_grad_atlas_frame_begin()
+ * call).  Regression test for a uint32_t underflow: `current_frame - 1`
+ * used to wrap to UINT32_MAX, flattening the LRU forever (row 0 would be
+ * returned for the rest of the atlas's lifetime, even in later frames).
+ * With the fix, ageing is clamped at frame 0 instead of underflowing. */
+EFL_START_TEST(grad_atlas_all_pinned_at_frame_zero)
+{
+   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];
+
+   /* Deliberately do NOT call span_grad_atlas_frame_begin(): current_frame
+    * stays at its calloc'd value of 0, matching every row's initial
+    * last_used, so the atlas is "all pinned" as soon as it fills up. */
+   for (int i = 0; i < SPAN_GRAD_ATLAS_H; i++)
+     {
+        _fill_ramp(ramp, (uint32_t)(i + 1));
+        int r = span_grad_atlas_lookup(a, (void *)(uintptr_t)(0x5000 + i),
+                                       (uint32_t)(i + 1), ramp);
+        ck_assert_int_ge(r, 0);
+     }
+   ck_assert_int_eq(_flush_calls, 0);
+
+   /* The 65th distinct ramp forces the all-pinned branch at current_frame
+    * == 0.  It must not crash and must drain via the flush callback.
+    *
+    * Note: at current_frame == 0 the post-flush ageing is clamped to 0
+    * (guarded, see the fix), so aged rows still compare equal to
+    * current_frame and read as "pinned" again on the very next miss.  This
+    * means every further miss in this same frame-0 pass re-triggers a
+    * flush - a degraded-but-safe outcome, not the resumed-LRU behaviour
+    * normal passes (current_frame > 0) get.  The regression this guards
+    * against is the uint32_t underflow that made rows unevictable *forever*
+    * (UINT32_MAX never ages further); what matters here is that every
+    * lookup keeps succeeding and the row index stays valid. */
+   for (int i = 0; i < 6; i++)
+     {
+        _fill_ramp(ramp, (uint32_t)(30000 + i));
+        int r = span_grad_atlas_lookup(a, (void *)(uintptr_t)(0x6000 + i),
+                                       (uint32_t)(30000 + i), ramp);
+        ck_assert_int_ge(r, 0);
+        ck_assert_int_lt(r, SPAN_GRAD_ATLAS_H);
+     }
+   ck_assert_int_gt(_flush_calls, 0);
+
+   span_grad_atlas_free(a);
+}
+EFL_END_TEST
+
 void
 ector_test_grad_atlas(TCase *tc)
 {
@@ -291,4 +345,5 @@ ector_test_grad_atlas(TCase *tc)
    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);
+   tcase_add_test(tc, grad_atlas_all_pinned_at_frame_zero);
 }

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

Reply via email to