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.

Reply via email to