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.