This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch devs/cedric/wl/all
in repository efl.
View the commit online.
commit d43c94394d4bab01325851b5fe01863c345cf0f3
Author: Cedric BAIL <[email protected]>
AuthorDate: Sun Aug 16 23:22:36 2026 -0600
evas/gl: honour image orientation on native surfaces
Setting an orientation on an image backed by a native surface took the
process down, and would have rendered wrong had it not.
eng_image_orient_set() rebuilt the image from its RGBA_Image. A native
surface has none - evas_gl_common_image_native_enable() drops it and
builds the texture from the imported buffer instead - so it handed NULL
to evas_gl_common_image_new_from_rgbaimage(), which does not fail
politely: the reuse loop matches any cached image that also has a NULL
im and then drops a NULL cache entry, and failing that it dereferences
the NULL outright. Branch before the rebuild and turn the orientation
over on the wrapper itself. There is no w/h to swap alongside it,
because eng_image_size_get() already derives the swapped dimensions
from im->orient.
evas_gl_common_context_image_push() then skipped the eight-way UV
rotation for y-inverted textures, which is what a dmabuf import gives
you, behind a FIXME asking how that case could be tested. It can be
tested with an X11 pixmap native surface, which reaches the same path -
neither installs a native.func.yinvert callback - and the answer is
that nothing needs composing with the y-invert: the V flip further down
only fires when native.yinvert is clear, so a y-inverted texture sees
exactly the same UV maths as a non-native image. Measured against a
plain image on the same canvas, all eight orientations now agree to the
pixel.
That guard also mattered more than it looked. The source rectangle
fix-ups above it were never guarded on yinvert, so a y-inverted native
image had its source rectangle adjusted for a rotation the UVs never
received. Both are now driven off tex->im alone and cannot disagree.
evas_gl_common_context_image_map_push() is untouched: it has no
orientation handling at all, so a native image drawn through a map
still renders unrotated.
Test walks the eight orientations on an X11 pixmap native surface,
checking both the reported size and the drawn arrangement against a
plain image on the same canvas. It returns quietly where there is no GL
X11 canvas to be had.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01A8z6XzawMHBiuUjmM3YXV8
---
.../evas/engines/gl_common/evas_gl_context.c | 21 +-
src/modules/evas/engines/gl_generic/evas_engine.c | 23 ++
src/tests/evas/evas_suite.c | 3 +
src/tests/evas/evas_suite.h | 3 +
src/tests/evas/evas_test_native_orient.c | 318 +++++++++++++++++++++
src/tests/evas/meson.build | 22 +-
6 files changed, 374 insertions(+), 16 deletions(-)
diff --git a/src/modules/evas/engines/gl_common/evas_gl_context.c b/src/modules/evas/engines/gl_common/evas_gl_context.c
index 5c7deb4b30..78d7712388 100644
--- a/src/modules/evas/engines/gl_common/evas_gl_context.c
+++ b/src/modules/evas/engines/gl_common/evas_gl_context.c
@@ -2194,7 +2194,6 @@ evas_gl_common_context_image_push(Evas_Engine_GL_Context *gc,
Evas_GL_Program *prog;
int pn = 0, render_op = gc->dc->render_op, nomul = 0;
Shader_Sampling sam = 0, masksam = 0;
- int yinvert = 0;
Shader_Type shd_in = SHD_IMAGE;
int tex_target = GL_TEXTURE_2D;
@@ -2353,17 +2352,17 @@ evas_gl_common_context_image_push(Evas_Engine_GL_Context *gc,
ox4 = sx;
oy4 = sy + sh;
- if ((tex->im) && (tex->im->native.data))
+ /* This used to be skipped for y-inverted native surfaces, with a FIXME
+ * asking how to test that case. It can be tested with an X11 pixmap native
+ * surface, which reports yinvert just as a dmabuf import does, and the
+ * answer is that nothing needs composing: the V flip below only fires when
+ * native.yinvert is clear, so a y-inverted texture reaches exactly the same
+ * UV maths as a non-native image. Skipping the rotation here while the
+ * source rectangle above was still being adjusted for it is what made
+ * y-inverted native surfaces render wrong rather than merely unrotated;
+ * both are now driven off tex->im alone and so cannot disagree. */
+ if (tex->im)
{
- if (tex->im->native.func.yinvert)
- yinvert = tex->im->native.func.yinvert(tex->im);
- else
- yinvert = tex->im->native.yinvert;
- }
-
- if ((tex->im) && (!yinvert))
- {
- // FIXME: What if yinvert is true? How to test that?
switch (tex->im->orient)
{
case EVAS_IMAGE_ORIENT_NONE:
diff --git a/src/modules/evas/engines/gl_generic/evas_engine.c b/src/modules/evas/engines/gl_generic/evas_engine.c
index c1612b5535..ac1e68f68e 100644
--- a/src/modules/evas/engines/gl_generic/evas_engine.c
+++ b/src/modules/evas/engines/gl_generic/evas_engine.c
@@ -1152,6 +1152,29 @@ eng_image_orient_set(void *engine, void *image, Evas_Image_Orient orient)
gl_generic_window_find(engine);
+ /* No RGBA_Image to rebuild from: either a native surface, whose pixels are
+ * a texture imported from outside, or a render surface. Turn the
+ * orientation over on the wrapper itself. Passing a NULL RGBA_Image to
+ * evas_gl_common_image_new_from_rgbaimage() below would not fail politely -
+ * it matches any cached image that also has a NULL im and then drops a NULL
+ * cache entry. Note there is no w/h to swap here: eng_image_size_get()
+ * derives the swapped dimensions from im->orient, so doing it again would
+ * cancel out. */
+ if (!im->im)
+ {
+ /* Evas_GL_Image is refcounted; mutating a shared one would turn every
+ * object drawing it. Native images are dropped from the shared cache by
+ * evas_gl_common_image_native_enable(), so this should not happen. */
+ if (im->references > 1)
+ {
+ ERR("Refusing to orient an image shared by %i references",
+ im->references);
+ return im;
+ }
+ im->orient = orient;
+ return im;
+ }
+
evas_gl_common_image_update(im->gc, im);
im_new = evas_gl_common_image_new_from_rgbaimage(im->gc, im->im, &im->load_opts, NULL);
diff --git a/src/tests/evas/evas_suite.c b/src/tests/evas/evas_suite.c
index 05faf08ff8..e203455866 100644
--- a/src/tests/evas/evas_suite.c
+++ b/src/tests/evas/evas_suite.c
@@ -27,6 +27,9 @@ static const Efl_Test_Case etc[] = {
{ "Efl Canvas Animation", efl_test_canvas_animation },
{ "Map", evas_test_map },
{ "Premul", evas_test_premul },
+#ifdef HAVE_NATIVE_ORIENT_TEST
+ { "Native Orient", evas_test_native_orient },
+#endif
{ NULL, NULL }
};
diff --git a/src/tests/evas/evas_suite.h b/src/tests/evas/evas_suite.h
index 76a10a19b5..3c31348366 100644
--- a/src/tests/evas/evas_suite.h
+++ b/src/tests/evas/evas_suite.h
@@ -21,5 +21,8 @@ void evas_test_events(TCase *tc);
void efl_test_canvas_animation(TCase *tc);
void evas_test_map(TCase *tc);
void evas_test_premul(TCase *tc);
+#ifdef HAVE_NATIVE_ORIENT_TEST
+void evas_test_native_orient(TCase *tc);
+#endif
#endif /* _EVAS_SUITE_H */
diff --git a/src/tests/evas/evas_test_native_orient.c b/src/tests/evas/evas_test_native_orient.c
new file mode 100644
index 0000000000..1898cdfe4f
--- /dev/null
+++ b/src/tests/evas/evas_test_native_orient.c
@@ -0,0 +1,318 @@
+/* Image orientation on a GL native surface.
+ *
+ * A native surface has no RGBA_Image behind it - its pixels are a texture
+ * imported from outside - so eng_image_orient_set() cannot rebuild the image
+ * from a buffer the way it does for a loaded one, and the draw path has to
+ * rotate the texture's UVs instead. Both halves used to be missing: the engine
+ * dereferenced a NULL RGBA_Image, and the push path skipped the UV rotation
+ * for y-inverted textures while still adjusting the source rectangle for it.
+ *
+ * An X11 pixmap is used as the native surface because it lands on the same
+ * push path as a dmabuf import: neither installs a native.func.yinvert
+ * callback, so evas_gl_common_context_image_push() reads native.yinvert
+ * directly for both.
+ *
+ * This needs a real GL X11 canvas and returns quietly when it cannot have one.
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <Evas.h>
+#include <Ecore.h>
+#include <Ecore_Evas.h>
+#include <Ecore_X.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+
+#include "evas_suite.h"
+
+/* Deliberately non-square, so the w/h swap the quarter turns and the two
+ * diagonal flips are supposed to report is actually visible. */
+#define IW 64
+#define IH 32
+#define WW 256
+#define WH 256
+
+#define C_TL 0x00ff0000u /* R */
+#define C_TR 0x0000ff00u /* G */
+#define C_BL 0x000000ffu /* B */
+#define C_BR 0x00ffff00u /* Y */
+
+typedef struct
+{
+ Evas_Image_Orient orient;
+ const char *name;
+ const char *quadrants; /* TL TR BL BR of the drawn result */
+ int w, h; /* what image_size_get should report */
+} Orient_Case;
+
+/* Measured, not derived. Source arrangement is TL=R TR=G BL=B BR=Y, and Evas
+ * ORIENT_N is N degrees clockwise. Note TRANSPOSE comes out as the main
+ * diagonal and TRANSVERSE as the anti-diagonal, which is the opposite of how
+ * Evas' own documentation names them - trust these, they are what it draws. */
+static const Orient_Case cases[] = {
+ { EVAS_IMAGE_ORIENT_0, "ORIENT_0", "RGBY", IW, IH },
+ { EVAS_IMAGE_ORIENT_90, "ORIENT_90", "BRYG", IH, IW },
+ { EVAS_IMAGE_ORIENT_180, "ORIENT_180", "YBGR", IW, IH },
+ { EVAS_IMAGE_ORIENT_270, "ORIENT_270", "GYRB", IH, IW },
+ { EVAS_IMAGE_FLIP_HORIZONTAL, "FLIP_HORIZONTAL", "GRYB", IW, IH },
+ { EVAS_IMAGE_FLIP_VERTICAL, "FLIP_VERTICAL", "BYRG", IW, IH },
+ { EVAS_IMAGE_FLIP_TRANSPOSE, "FLIP_TRANSPOSE", "RBGY", IH, IW },
+ { EVAS_IMAGE_FLIP_TRANSVERSE, "FLIP_TRANSVERSE", "YGBR", IH, IW },
+};
+
+#define NCASES ((int)(sizeof(cases) / sizeof(cases[0])))
+
+static char
+_name_of(unsigned int px)
+{
+ unsigned int r = (px >> 16) & 0xff, g = (px >> 8) & 0xff, b = px & 0xff;
+ int hr = r > 0x80, hg = g > 0x80, hb = b > 0x80;
+
+ if (hr && !hg && !hb) return 'R';
+ if (!hr && hg && !hb) return 'G';
+ if (!hr && !hg && hb) return 'B';
+ if (hr && hg && !hb) return 'Y';
+ if (!hr && !hg && !hb) return '.';
+ return '?';
+}
+
+static Eina_Bool
+_readback(Display *dpy, Window win, char out[5])
+{
+ XImage *xi;
+ int qx[4] = { WW / 4, (WW * 3) / 4, WW / 4, (WW * 3) / 4 };
+ int qy[4] = { WH / 4, WH / 4, (WH * 3) / 4, (WH * 3) / 4 };
+ int i;
+
+ xi = XGetImage(dpy, win, 0, 0, WW, WH, AllPlanes, ZPixmap);
+ if (!xi) return EINA_FALSE;
+
+ for (i = 0; i < 4; i++)
+ out[i] = _name_of((unsigned int)XGetPixel(xi, qx[i], qy[i]));
+ out[4] = '\0';
+
+ XDestroyImage(xi);
+ return EINA_TRUE;
+}
+
+static Pixmap
+_pixmap_make(Display *dpy, Visual **vis_out)
+{
+ XVisualInfo vinfo;
+ Pixmap pm;
+ GC gc;
+ XGCValues gcv;
+
+ if (!XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo))
+ return None;
+
+ pm = XCreatePixmap(dpy, DefaultRootWindow(dpy), IW, IH, 32);
+ if (!pm) return None;
+
+ memset(&gcv, 0, sizeof(gcv));
+ gc = XCreateGC(dpy, pm, 0, &gcv);
+
+ XSetForeground(dpy, gc, 0xff000000u | C_TL);
+ XFillRectangle(dpy, pm, gc, 0, 0, IW / 2, IH / 2);
+ XSetForeground(dpy, gc, 0xff000000u | C_TR);
+ XFillRectangle(dpy, pm, gc, IW / 2, 0, IW / 2, IH / 2);
+ XSetForeground(dpy, gc, 0xff000000u | C_BL);
+ XFillRectangle(dpy, pm, gc, 0, IH / 2, IW / 2, IH / 2);
+ XSetForeground(dpy, gc, 0xff000000u | C_BR);
+ XFillRectangle(dpy, pm, gc, IW / 2, IH / 2, IW / 2, IH / 2);
+
+ XFreeGC(dpy, gc);
+ XSync(dpy, False);
+
+ *vis_out = vinfo.visual;
+ return pm;
+}
+
+static void
+_plain_fill(Evas_Object *o)
+{
+ unsigned int *d;
+ int x, y;
+
+ evas_object_image_size_set(o, IW, IH);
+ evas_object_image_alpha_set(o, EINA_TRUE);
+ d = evas_object_image_data_get(o, EINA_TRUE);
+ if (!d) return;
+
+ for (y = 0; y < IH; y++)
+ for (x = 0; x < IW; x++)
+ {
+ unsigned int c;
+ if (y < IH / 2) c = (x < IW / 2) ? C_TL : C_TR;
+ else c = (x < IW / 2) ? C_BL : C_BR;
+ d[(y * IW) + x] = 0xff000000u | c;
+ }
+
+ evas_object_image_data_set(o, d);
+ evas_object_image_data_update_add(o, 0, 0, IW, IH);
+}
+
+/* Walk the eight orientations. Always checks the reported size; fills got[] if
+ * the window could be read back. Returns EINA_FALSE if readback failed. */
+static Eina_Bool
+_walk(Ecore_Evas *ee, Evas *e, Evas_Object *o, Display *dpy, Window win,
+ char got[NCASES][5], const char *what)
+{
+ Eina_Bool readable = EINA_TRUE;
+ int i;
+
+ for (i = 0; i < NCASES; i++)
+ {
+ int w = 0, h = 0;
+
+ evas_object_image_orient_set(o, cases[i].orient);
+ ck_assert_int_eq(evas_object_image_orient_get(o), cases[i].orient);
+
+ /* The size has to come back swapped for the quarter turns and the two
+ * diagonals - the canvas re-reads it from the engine after every
+ * orientation change. */
+ evas_object_image_size_get(o, &w, &h);
+ ck_assert_msg((w == cases[i].w) && (h == cases[i].h),
+ "%s %s: size is %dx%d, expected %dx%d",
+ what, cases[i].name, w, h, cases[i].w, cases[i].h);
+
+ evas_object_image_pixels_dirty_set(o, EINA_TRUE);
+ evas_damage_rectangle_add(e, 0, 0, WW, WH);
+ ecore_evas_manual_render(ee);
+ XSync(dpy, False);
+ ecore_main_loop_iterate();
+
+ if (!_readback(dpy, win, got[i])) readable = EINA_FALSE;
+ }
+
+ return readable;
+}
+
+EFL_START_TEST(evas_object_image_native_orient)
+{
+ Ecore_Evas *ee;
+ Evas *e;
+ Evas_Object *o;
+ Evas_Native_Surface ns;
+ Display *dpy;
+ Window win;
+ Pixmap pm;
+ Visual *vis = NULL;
+ char plain[NCASES][5], native[NCASES][5];
+ Eina_Bool plain_ok, native_ok;
+ int i;
+
+ ee = ecore_evas_gl_x11_new(NULL, 0, 0, 0, WW, WH);
+ if (!ee)
+ {
+ fprintf(stderr, "no gl_x11 canvas, skipping native orient test\n");
+ return;
+ }
+
+ ecore_evas_show(ee);
+ ecore_evas_manual_render_set(ee, EINA_TRUE);
+ e = ecore_evas_get(ee);
+ win = ecore_evas_gl_x11_window_get(ee);
+
+ dpy = ecore_x_display_get();
+ if (!dpy) { ecore_evas_free(ee); return; }
+
+ pm = _pixmap_make(dpy, &vis);
+ if (pm == None)
+ {
+ fprintf(stderr, "no 32bit visual, skipping native orient test\n");
+ ecore_evas_free(ee);
+ return;
+ }
+
+ /* XGetImage on an unmapped window fails. */
+ for (i = 0; i < 500; i++)
+ {
+ XWindowAttributes wa;
+ ecore_main_loop_iterate();
+ if (XGetWindowAttributes(dpy, win, &wa) && (wa.map_state == IsViewable))
+ break;
+ usleep(10000);
+ }
+ for (i = 0; i < 20; i++) ecore_main_loop_iterate();
+
+ /* Control first: a plain image on the same canvas goes down the already
+ * working non-native path, and is what the native one has to match. */
+ o = evas_object_image_add(e);
+ evas_object_image_filled_set(o, EINA_TRUE);
+ evas_object_geometry_set(o, 0, 0, WW, WH);
+ evas_object_show(o);
+ _plain_fill(o);
+ plain_ok = _walk(ee, e, o, dpy, win, plain, "plain");
+ evas_object_del(o);
+
+ /* Then the native surface. eng_image_native_set() returns NULL when the
+ * object has no engine image yet and the surface is not of type OPENGL, so
+ * give it a size before handing over the pixmap. */
+ o = evas_object_image_add(e);
+ evas_object_image_filled_set(o, EINA_TRUE);
+ evas_object_geometry_set(o, 0, 0, WW, WH);
+ evas_object_show(o);
+ evas_object_image_alpha_set(o, EINA_TRUE);
+ evas_object_image_size_set(o, IW, IH);
+
+ memset(&ns, 0, sizeof(ns));
+ ns.version = EVAS_NATIVE_SURFACE_VERSION;
+ ns.type = EVAS_NATIVE_SURFACE_X11;
+ ns.data.x11.visual = vis;
+ ns.data.x11.pixmap = pm;
+ evas_object_image_native_surface_set(o, &ns);
+
+ if (!evas_object_image_native_surface_get(o))
+ {
+ fprintf(stderr, "native surface did not attach, skipping\n");
+ evas_object_del(o);
+ XFreePixmap(dpy, pm);
+ ecore_evas_free(ee);
+ return;
+ }
+
+ /* Reaching here at all is the first half of the fix: this used to take the
+ * compositor down inside evas_gl_common_image_new_from_rgbaimage(). */
+ native_ok = _walk(ee, e, o, dpy, win, native, "native");
+
+ /* Only trust the pixels if the control read back the way it must. If it did
+ * not, the window is obscured or unredirected and neither result means
+ * anything - the size checks above have already run either way. */
+ if (!plain_ok || !native_ok)
+ fprintf(stderr, "window not readable, skipping pixel comparison\n");
+ else
+ {
+ Eina_Bool control_sane = EINA_TRUE;
+
+ for (i = 0; i < NCASES; i++)
+ if (strcmp(plain[i], cases[i].quadrants)) control_sane = EINA_FALSE;
+
+ if (!control_sane)
+ fprintf(stderr, "control did not match, skipping pixel comparison\n");
+ else
+ for (i = 0; i < NCASES; i++)
+ ck_assert_msg(!strcmp(native[i], cases[i].quadrants),
+ "native %s: drew %s, expected %s",
+ cases[i].name, native[i], cases[i].quadrants);
+ }
+
+ evas_object_del(o);
+ XFreePixmap(dpy, pm);
+ ecore_evas_free(ee);
+}
+EFL_END_TEST
+
+void
+evas_test_native_orient(TCase *tc)
+{
+ tcase_add_test(tc, evas_object_image_native_orient);
+}
diff --git a/src/tests/evas/meson.build b/src/tests/evas/meson.build
index 63670c7986..50e5c9d1cf 100644
--- a/src/tests/evas/meson.build
+++ b/src/tests/evas/meson.build
@@ -25,13 +25,25 @@ evas_suite_src = [
'evas_test_premul.c',
]
-evas_suite = executable('evas_suite',
- evas_suite_src,
- dependencies: [evas_bin, evas, ecore_evas, dl, check, evas_ext_none_static_deps, eet], #external deps needed here since tests do include internal headers
- include_directories: include_directories(join_paths('..', '..', 'modules', 'evas', 'engines', 'buffer')),
- c_args : [
+evas_suite_deps = [evas_bin, evas, ecore_evas, dl, check, evas_ext_none_static_deps, eet]
+evas_suite_args = [
'-DTESTS_BUILD_DIR="'+meson.current_build_dir()+'"',
'-DTESTS_SRC_DIR="'+meson.current_source_dir()+'"']
+
+# Orientation on a GL native surface needs a real X11 pixmap to import, so it
+# only builds where x11 does. It returns quietly when there is no GL X11
+# canvas to be had at run time.
+if get_option('x11')
+ evas_suite_src += 'evas_test_native_orient.c'
+ evas_suite_deps += [ecore_x, x11]
+ evas_suite_args += '-DHAVE_NATIVE_ORIENT_TEST=1'
+endif
+
+evas_suite = executable('evas_suite',
+ evas_suite_src,
+ dependencies: evas_suite_deps, #external deps needed here since tests do include internal headers
+ include_directories: include_directories(join_paths('..', '..', 'modules', 'evas', 'engines', 'buffer')),
+ c_args : evas_suite_args
)
test('evas-suite', evas_suite,
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.