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 ed3deef8700541670b6a3752b519581a2334e113
Author: Cedric BAIL <[email protected]>
AuthorDate: Fri Aug 14 11:10:48 2026 -0600
tests(evas): cover vector group opacity and mixed object sizes
Two scenes, run on every engine that can be brought up. That last part
is the point: vector shapes go through a span-buffer path on the GL
engine that the software engine does not use, so bugs in it are
invisible to a buffer-engine test, and all three of the bugs these
cover were GL-only. Engines that cannot be created - no display, not
built - are skipped rather than failed, so the buffer engine always
runs and the rest join in where they can.
The scene is a narrow vector object followed by a much wider one whose
root is a container with alpha below 255. Both halves matter. Every
vector object on a canvas shares one ector surface, so the narrow one
fixes that surface's stride and the wide one then renders through it;
the group-opacity path additionally swaps the surface's pixel buffer
out and back while it renders. Between them that reached a buffer sized
from the wrong width, a use-after-free when the swap freed a buffer the
surface owned, and a group whose alpha never reached the framebuffer.
The first test asserts nothing beyond surviving, which is most of what
was wanted - it segfaulted before those fixes. The second samples the
centre pixel and requires the group's alpha to have been applied, with
a wide tolerance, since it is checking that the alpha arrived at all
rather than the exact rounding of the blend.
Worth knowing for anything added here later: meson sets
MALLOC_PERTURB_, which is what turned the span page's teardown
use-after-free from an occasional crash into a reliable one. Running
these by hand without it can look clean when they are not.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
src/tests/evas/evas_suite.c | 1 +
src/tests/evas/evas_suite.h | 1 +
src/tests/evas/evas_test_vg.c | 186 ++++++++++++++++++++++++++++++++++++++++++
src/tests/evas/meson.build | 1 +
4 files changed, 189 insertions(+)
diff --git a/src/tests/evas/evas_suite.c b/src/tests/evas/evas_suite.c
index 05faf08ff8..9ed7a22a33 100644
--- a/src/tests/evas/evas_suite.c
+++ b/src/tests/evas/evas_suite.c
@@ -27,6 +27,7 @@ static const Efl_Test_Case etc[] = {
{ "Efl Canvas Animation", efl_test_canvas_animation },
{ "Map", evas_test_map },
{ "Premul", evas_test_premul },
+ { "VG", evas_test_vg },
{ NULL, NULL }
};
diff --git a/src/tests/evas/evas_suite.h b/src/tests/evas/evas_suite.h
index 76a10a19b5..0a5e66fd60 100644
--- a/src/tests/evas/evas_suite.h
+++ b/src/tests/evas/evas_suite.h
@@ -11,6 +11,7 @@ void evas_test_textblock(TCase *tc);
void evas_test_text(TCase *tc);
void evas_test_callbacks(TCase *tc);
void evas_test_render_engines(TCase *tc);
+void evas_test_vg(TCase *tc);
void evas_test_filters(TCase *tc);
void evas_test_image_object(TCase *tc);
void evas_test_image_object2(TCase *tc);
diff --git a/src/tests/evas/evas_test_vg.c b/src/tests/evas/evas_test_vg.c
new file mode 100644
index 0000000000..5cb11c9841
--- /dev/null
+++ b/src/tests/evas/evas_test_vg.c
@@ -0,0 +1,186 @@
+/* Vector object rendering tests.
+ *
+ * These run on every engine that can be brought up, because the interesting
+ * failures have been engine-specific: the GL engine rasterises vector shapes
+ * through a span-buffer path that the software engine does not use, so a bug
+ * in it is invisible to a buffer-engine test. Engines that cannot be created
+ * here (no display, not built) are skipped rather than failed.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#define EFL_BETA_API_SUPPORT 1
+#define EFL_EO_API_SUPPORT 1
+
+#include <stdio.h>
+
+#include <Evas.h>
+#include <Ecore_Evas.h>
+
+#include "evas_suite.h"
+
+#define WIN_W 256
+#define WIN_H 256
+
+/* Engines to attempt. "buffer" is always available and headless; the rest
+ * are tried and skipped when they cannot be created. */
+static const char *_engines[] = { "buffer", "opengl_x11", NULL };
+
+static Eo *
+_vg_object_add(Evas *e, int x, int y, int w, int h, int alpha)
+{
+ Eo *vg, *shape, *root;
+
+ vg = efl_add(EFL_CANVAS_VG_OBJECT_CLASS, e);
+ efl_gfx_entity_size_set(vg, EINA_SIZE2D(w, h));
+ efl_gfx_entity_position_set(vg, EINA_POSITION2D(x, y));
+ efl_gfx_entity_visible_set(vg, EINA_TRUE);
+
+ if (alpha < 255)
+ {
+ /* A container whose colour has alpha below 255 asks for group
+ * opacity, which is a different rendering path from a shape that is
+ * merely translucent. */
+ root = efl_add(EFL_CANVAS_VG_CONTAINER_CLASS, vg);
+ efl_gfx_color_set(root, alpha, alpha, alpha, alpha);
+ shape = efl_add(EFL_CANVAS_VG_SHAPE_CLASS, root);
+ }
+ else
+ {
+ shape = efl_add(EFL_CANVAS_VG_SHAPE_CLASS, vg);
+ root = shape;
+ }
+
+ /* Opaque white, so that whatever alpha survives to the framebuffer is
+ * readable straight off one channel. */
+ efl_gfx_path_append_rect(shape, 0, 0, w, h, 0, 0);
+ efl_gfx_color_set(shape, 255, 255, 255, 255);
+
+ efl_canvas_vg_object_root_node_set(vg, root);
+ return vg;
+}
+
+/* Render the scene @p build makes and return the pixel at (@p px, @p py),
+ * or EINA_FALSE when this engine is unavailable. */
+static Eina_Bool
+_scene_sample(const char *engine, void (*build)(Evas *e),
+ int frames, int px, int py, unsigned int *out)
+{
+ Ecore_Evas *ee;
+ Evas *e;
+ Evas_Object *snap, *bg;
+ unsigned int *pixels;
+ int i, sw = 0;
+
+ ee = ecore_evas_new(engine, 0, 0, WIN_W, WIN_H, NULL);
+ if (!ee)
+ {
+ printf("Skipping: cannot create ecore_evas for '%s'\n", engine);
+ return EINA_FALSE;
+ }
+ ecore_evas_show(ee);
+ ecore_evas_manual_render_set(ee, EINA_TRUE);
+ e = ecore_evas_get(ee);
+
+ /* Opaque black behind, so a sampled pixel reports coverage directly. */
+ bg = evas_object_rectangle_add(e);
+ evas_object_color_set(bg, 0, 0, 0, 255);
+ evas_object_geometry_set(bg, 0, 0, WIN_W, WIN_H);
+ evas_object_show(bg);
+
+ build(e);
+
+ /* Several frames: the first one populates caches, and failures in the
+ * vector paths have tended to need a second pass to show up. */
+ for (i = 0; i < frames; i++)
+ ecore_evas_manual_render(ee);
+
+ /* A snapshot reads back uniformly whether the engine renders to memory
+ * or to a window. */
+ snap = evas_object_image_filled_add(e);
+ evas_object_image_snapshot_set(snap, EINA_TRUE);
+ evas_object_geometry_set(snap, 0, 0, WIN_W, WIN_H);
+ evas_object_show(snap);
+ ecore_evas_manual_render(ee);
+
+ evas_object_image_size_get(snap, &sw, NULL);
+ pixels = evas_object_image_data_get(snap, EINA_FALSE);
+ if (pixels && sw > 0) *out = pixels[py * sw + px];
+ else *out = 0;
+ if (pixels) evas_object_image_data_set(snap, pixels);
+
+ ecore_evas_free(ee);
+ return (pixels != NULL) && (sw > 0);
+}
+
+/* A narrow object followed by a much wider one, the wider one asking for
+ * group opacity.
+ *
+ * Both halves matter. Every vector object on a canvas shares one ector
+ * surface, so the narrow object is what fixes that surface's stride, and the
+ * wide one then renders through it. The group-opacity path additionally
+ * swaps the surface's pixel buffer out and back while it renders, which is
+ * what turned a mis-sized buffer into a use-after-free on the GL engine. */
+static void
+_build_narrow_then_wide(Evas *e)
+{
+ _vg_object_add(e, 0, 0, 16, 16, 255);
+ _vg_object_add(e, 32, 32, 192, 192, 128);
+}
+
+EFL_START_TEST(evas_vg_mixed_sizes_group_opacity)
+{
+ const char **eng;
+
+ for (eng = _engines; *eng; eng++)
+ {
+ unsigned int px = 0;
+
+ if (!_scene_sample(*eng, _build_narrow_then_wide, 4, 128, 128, &px))
+ continue;
+
+ /* Surviving this far is most of the point: before the buffer was
+ * sized and owned correctly, the GL engine wrote about a megabyte
+ * past a 57 KB allocation here and then crashed on a later frame. */
+ printf("engine %s: centre pixel %08x\n", *eng, px);
+ }
+}
+EFL_END_TEST
+
+/* Group opacity has to reach the framebuffer.
+ *
+ * An opaque white shape inside a container with alpha 128, over black, must
+ * land near 128 on every channel. The GL engine used to composite the group
+ * into a buffer it never read back, so the group arrived fully opaque while
+ * the software engine had it right - a visible difference between engines
+ * for the same scene. */
+EFL_START_TEST(evas_vg_container_alpha_is_applied)
+{
+ const char **eng;
+
+ for (eng = _engines; *eng; eng++)
+ {
+ unsigned int px = 0;
+ int r;
+
+ if (!_scene_sample(*eng, _build_narrow_then_wide, 4, 128, 128, &px))
+ continue;
+
+ r = (px >> 16) & 0xff;
+ /* Wide tolerance: this is checking that the alpha was applied at
+ * all, not the exact rounding of the blend. */
+ ck_assert_msg(r > 100 && r < 160,
+ "engine %s: group alpha 128 over black should give a "
+ "channel near 128, got %d (pixel %08x)",
+ *eng, r, px);
+ }
+}
+EFL_END_TEST
+
+void evas_test_vg(TCase *tc)
+{
+ tcase_add_test(tc, evas_vg_mixed_sizes_group_opacity);
+ tcase_add_test(tc, evas_vg_container_alpha_is_applied);
+}
diff --git a/src/tests/evas/meson.build b/src/tests/evas/meson.build
index 63670c7986..7b1861fecb 100644
--- a/src/tests/evas/meson.build
+++ b/src/tests/evas/meson.build
@@ -9,6 +9,7 @@ evas_suite_src = [
'evas_test_text.c',
'evas_test_callbacks.c',
'evas_test_render_engines.c',
+ 'evas_test_vg.c',
'evas_test_filters.c',
'evas_test_image.c',
'evas_test_mask.c',
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.