This is an automated email from the git hooks/post-receive script.

git pushed a commit to branch gl-parity
in repository expedite.

View the commit online.

commit b9a0f9ddf59f5bf647b8421a3917fb1e5abfccb2
Author: [email protected] <[email protected]>
AuthorDate: Mon Aug 10 17:06:11 2026 -0600

    add engine-vs-engine rendering parity testing
    
    Expedite could already dump a deterministic frame with "-o", but that path
    hardcoded ecore_evas_buffer_new(), so it could only ever produce a software
    rendering.  There was no way to get pixels back out of a GL run, which meant
    a rendering regression in an accelerated backend could only be found by
    eyeballing the animation.
    
    Teach "-o" about "-e": when the requested engine is not the buffer one, run
    the test in a real window of that engine and read the result back through a
    full canvas snapshot object.  The engine renders the scene below the snapshot
    into an offscreen surface, which the GL engines can read back with
    glReadPixels; saving is only legal from inside a post-render event, hence the
    callback.  This works for any windowed engine, not just GL.
    
    The comparison is inherently a tolerance one - backends are never bit exact
    against each other, as antialiasing, gradient ramp interpolation and
    premultiplication rounding all differ - so add expedite-imgcmp, which reports
    the worst per-channel delta and how many pixels exceed a threshold, and
    expedite-parity, which walks the test list rendering each test on both
    engines and diffing the pair.
    
    Both "-o" paths now render every frame rather than only the last one.  The
    frame sequence is what exposes incremental redraw and caching bugs, and
    walking it identically on both sides is what makes the comparison meaningful.
    This is safe to compare across engines because expedite is deterministic:
    rnd() is a fixed table reset per test, and the tests key their animation off
    the frame counter rather than wall clock time.
    
    For reference, opengl_x11 against buffer currently sits at a noise floor of
    3 to 6 levels of per-channel delta on the vector tests.
---
 src/bin/Makefile.am     |   7 ++-
 src/bin/expedite-parity | 103 ++++++++++++++++++++++++++++++++++
 src/bin/imgcmp.c        | 146 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/bin/main.c          |  95 ++++++++++++++++++++++++++++++-
 src/bin/meson.build     |  10 ++++
 5 files changed, 357 insertions(+), 4 deletions(-)

diff --git a/src/bin/Makefile.am b/src/bin/Makefile.am
index c84619c..1cfd1a3 100644
--- a/src/bin/Makefile.am
+++ b/src/bin/Makefile.am
@@ -8,8 +8,8 @@ AM_CPPFLAGS = \
 -DPACKAGE_DATA_DIR=\"$(datadir)/$(PACKAGE)\" \
 @EFL_CFLAGS@
 
-bin_PROGRAMS = expedite
-bin_SCRIPTS = expedite-cmp
+bin_PROGRAMS = expedite expedite-imgcmp
+bin_SCRIPTS = expedite-cmp expedite-parity
 
 expedite_SOURCES = \
 main.c main.h \
@@ -143,4 +143,7 @@ snapshot_widgets_file_icons.c
 
 expedite_LDADD = @EFL_LIBS@ -lm
 
+expedite_imgcmp_SOURCES = imgcmp.c
+expedite_imgcmp_LDADD = @EFL_LIBS@ -lm
+
 EXTRA_DIST = $(bin_SCRIPTS)
diff --git a/src/bin/expedite-parity b/src/bin/expedite-parity
new file mode 100755
index 0000000..616e781
--- /dev/null
+++ b/src/bin/expedite-parity
@@ -0,0 +1,103 @@
+#!/bin/sh
+# expedite-parity - render the same frames with two engines and diff them.
+#
+# The software (buffer) engine is the reference; the engine under test is run
+# in a real window and captured through a snapshot object. Both walk the exact
+# same frame sequence, and expedite's PRNG is a fixed table reset per test, so
+# frame N is reproducible across engines.
+#
+#   expedite-parity [-e ENGINE] [-c FRAMES] [-p RESOLUTION] [-t TOL] [-r RATIO]
+#                   [-o OUTDIR] [-k] [TEST ...]
+#
+# With no TEST given, every test expedite lists is compared.
+
+set -u
+
+EXPEDITE=${EXPEDITE:-expedite}
+IMGCMP=${IMGCMP:-expedite-imgcmp}
+
+engine=gl_x11
+frames=20
+resolution=svga
+tolerance=8
+ratio=0.02
+outdir=
+keep=0
+
+usage()
+{
+    sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
+    exit "$1"
+}
+
+while getopts "e:c:p:t:r:o:kh" opt; do
+    case $opt in
+        e) engine=$OPTARG ;;
+        c) frames=$OPTARG ;;
+        p) resolution=$OPTARG ;;
+        t) tolerance=$OPTARG ;;
+        r) ratio=$OPTARG ;;
+        o) outdir=$OPTARG; keep=1 ;;
+        k) keep=1 ;;
+        h) usage 0 ;;
+        *) usage 2 ;;
+    esac
+done
+shift $((OPTIND - 1))
+
+if [ -z "$outdir" ]; then
+    outdir=$(mktemp -d "${TMPDIR:-/tmp}/expedite-parity.XXXXXX") || exit 2
+fi
+mkdir -p "$outdir" || exit 2
+
+tests=$*
+if [ -z "$tests" ]; then
+    # "-l" prints "%3i - name (Weight %f)" on stderr, one line per test.
+    tests=$("$EXPEDITE" -l 2>&1 | sed -n 's/^ *\([0-9][0-9]*\) - .*/\1/p')
+fi
+
+if [ -z "$tests" ]; then
+    echo "No tests to run." >&2
+    exit 2
+fi
+
+pass=0
+fail=0
+skip=0
+failed_list=
+
+for t in $tests; do
+    ref=$outdir/test$t-buffer.png
+    cand=$outdir/test$t-$engine.png
+
+    if ! "$EXPEDITE" -e buffer -p "$resolution" -t "$t" -c "$frames" \
+                     -o "$ref" >/dev/null 2>&1; then
+        echo "SKIP test $t (reference render failed)"
+        skip=$((skip + 1))
+        continue
+    fi
+    if ! "$EXPEDITE" -e "$engine" -p "$resolution" -t "$t" -c "$frames" \
+                     -o "$cand" >/dev/null 2>&1; then
+        echo "SKIP test $t ($engine render failed)"
+        skip=$((skip + 1))
+        continue
+    fi
+
+    if out=$("$IMGCMP" "$ref" "$cand" "$tolerance" "$ratio" 2>&1); then
+        echo "PASS test $t   $(echo "$out" | sed -n 's/^max delta : /max delta /p')"
+        pass=$((pass + 1))
+        [ "$keep" = 1 ] || rm -f "$ref" "$cand"
+    else
+        echo "FAIL test $t"
+        echo "$out" | sed 's/^/       /'
+        fail=$((fail + 1))
+        failed_list="$failed_list $t"
+    fi
+done
+
+echo
+echo "engine $engine vs buffer: $pass passed, $fail failed, $skip skipped"
+[ "$fail" = 0 ] || echo "failing tests:$failed_list"
+[ "$keep" = 1 ] && echo "frames kept in $outdir"
+
+[ "$fail" = 0 ]
diff --git a/src/bin/imgcmp.c b/src/bin/imgcmp.c
new file mode 100644
index 0000000..2145e60
--- /dev/null
+++ b/src/bin/imgcmp.c
@@ -0,0 +1,146 @@
+/* expedite-imgcmp - compare two frames dumped by "expedite -o".
+ *
+ * Rendering backends are never bit exact against each other: antialiasing,
+ * gradient ramp interpolation and premultiplication rounding all differ
+ * between the software rasterizer and the GPU. So a useful comparison is a
+ * tolerance one: how far off is the worst pixel, and how many pixels are off
+ * by more than we are willing to accept.
+ *
+ * Exits 0 when the two images match within tolerance, 1 when they do not and
+ * 2 on any error (missing or unloadable file, size mismatch).
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <Eina.h>
+#include <Evas.h>
+#include <Ecore_Evas.h>
+
+static Evas_Object *
+_load(Evas *e, const char *file, int *w, int *h)
+{
+   Evas_Object *im;
+   int err;
+
+   im = evas_object_image_add(e);
+   evas_object_image_file_set(im, file, NULL);
+   err = evas_object_image_load_error_get(im);
+   if (err != EVAS_LOAD_ERROR_NONE)
+     {
+        fprintf(stderr, "Could not load '%s': %s\n", file,
+                evas_load_error_str(err));
+        return NULL;
+     }
+   evas_object_image_size_get(im, w, h);
+   return im;
+}
+
+int
+main(int argc, char **argv)
+{
+   Ecore_Evas *ee;
+   Evas *e;
+   Evas_Object *a, *b;
+   const unsigned int *pa, *pb;
+   int aw, ah, bw, bh, x, y;
+   int tolerance = 4;
+   double max_ratio = 0.0;
+   long long total, differing = 0, over = 0, sum = 0;
+   int worst = 0, worst_x = 0, worst_y = 0;
+   const char *fa, *fb;
+   int ret;
+
+   if ((argc < 3) || (argc > 5))
+     {
+        fprintf(stderr,
+                "Usage: %s <reference.png> <candidate.png> "
+                "[per-channel-tolerance] [max-ratio-of-pixels-over]\n"
+                "\n"
+                "  per-channel-tolerance    accepted delta on any of B, G, R, A"
+                " [default 4]\n"
+                "  max-ratio-of-pixels-over ratio of pixels allowed to exceed"
+                " it, 0.0-1.0 [default 0.0]\n",
+                argv[0]);
+        return 2;
+     }
+   fa = argv[1];
+   fb = argv[2];
+   if (argc > 3) tolerance = atoi(argv[3]);
+   if (argc > 4) max_ratio = atof(argv[4]);
+
+   if (!ecore_evas_init()) return 2;
+   ee = ecore_evas_buffer_new(1, 1);
+   if (!ee)
+     {
+        fprintf(stderr, "Could not create a buffer canvas to decode with.\n");
+        return 2;
+     }
+   e = ecore_evas_get(ee);
+
+   a = _load(e, fa, &aw, &ah);
+   b = _load(e, fb, &bw, &bh);
+   if (!a || !b) return 2;
+
+   if ((aw != bw) || (ah != bh))
+     {
+        fprintf(stderr, "Size mismatch: '%s' is %ix%i, '%s' is %ix%i\n",
+                fa, aw, ah, fb, bw, bh);
+        return 2;
+     }
+
+   pa = evas_object_image_data_get(a, EINA_FALSE);
+   pb = evas_object_image_data_get(b, EINA_FALSE);
+   if (!pa || !pb)
+     {
+        fprintf(stderr, "Could not access decoded pixels.\n");
+        return 2;
+     }
+
+   total = (long long)aw * ah;
+   for (y = 0; y < ah; y++)
+     for (x = 0; x < aw; x++)
+       {
+          unsigned int va = pa[y * aw + x];
+          unsigned int vb = pb[y * aw + x];
+          int c, worst_here = 0;
+
+          if (va == vb) continue;
+          differing++;
+          for (c = 0; c < 32; c += 8)
+            {
+               int d = (int)((va >> c) & 0xff) - (int)((vb >> c) & 0xff);
+
+               if (d < 0) d = -d;
+               if (d > worst_here) worst_here = d;
+            }
+          sum += worst_here;
+          if (worst_here > worst)
+            {
+               worst = worst_here;
+               worst_x = x;
+               worst_y = y;
+            }
+          if (worst_here > tolerance) over++;
+       }
+
+   printf("reference : %s\n", fa);
+   printf("candidate : %s\n", fb);
+   printf("size      : %ix%i (%lli pixels)\n", aw, ah, total);
+   printf("differing : %lli (%.4f%%)\n", differing,
+          100.0 * (double)differing / (double)total);
+   printf("mean delta: %.4f (over differing pixels)\n",
+          differing ? (double)sum / (double)differing : 0.0);
+   printf("max delta : %i at %i,%i\n", worst, worst_x, worst_y);
+   printf("over tol %i: %lli (%.4f%%, allowed %.4f%%)\n", tolerance, over,
+          100.0 * (double)over / (double)total, 100.0 * max_ratio);
+
+   ret = ((double)over / (double)total) > max_ratio;
+   printf("result    : %s\n", ret ? "FAIL" : "PASS");
+
+   evas_object_image_data_set(a, (void *)pa);
+   evas_object_image_data_set(b, (void *)pb);
+   ecore_evas_free(ee);
+   ecore_evas_shutdown();
+
+   return ret;
+}
diff --git a/src/bin/main.c b/src/bin/main.c
index a111501..ea7d99d 100644
--- a/src/bin/main.c
+++ b/src/bin/main.c
@@ -1203,7 +1203,10 @@ static const Ecore_Getopt optdesc = {
     ECORE_GETOPT_STORE_INT('c', "count", "Number of main loop run per tests"),
     ECORE_GETOPT_STORE_TRUE('f', "fullscreen", "Make window fullscreen"),
     ECORE_GETOPT_STORE_STR('d', "datadir", "Define expedite data directory"),
-    ECORE_GETOPT_STORE_STR('o', "output", "Output frame specified by count from test to specified file"),
+    ECORE_GETOPT_STORE_STR('o', "output", "Output frame specified by count from test to specified file. "
+                          "Uses the buffer (software) engine unless --engine "
+                          "names another one, which is then run in a real "
+                          "window and captured through a snapshot"),
     ECORE_GETOPT_STORE_INT('t', "test", "Run a single test"),
     ECORE_GETOPT_STORE_TRUE('l', "list-tests", "List all tests"),
     ECORE_GETOPT_STORE_TRUE('m', "compat", "Output compatible with expedite-cmp"),
@@ -1329,6 +1332,28 @@ _cb_post_render(Ecore_Evas *ee EINA_UNUSED)
      }
 }
 
+/* Frame capture on engines that render to a window instead of to a memory
+ * buffer (gl_x11, wayland_egl, drm, ...). A full canvas snapshot object is
+ * dropped on top of the scene just before the last render; the engine renders
+ * the scene below it into an offscreen surface which can then be read back
+ * (glReadPixels for the GL engines). Saving is only legal from within a
+ * post-render event, hence the callback. */
+static Evas_Object *_snap_obj = NULL;
+static const char *_snap_file = NULL;
+static Eina_Bool _snap_ok = EINA_FALSE;
+
+static void
+_cb_snapshot_save(void *data EINA_UNUSED, Evas *e EINA_UNUSED,
+                  void *event_info EINA_UNUSED)
+{
+   if (!_snap_obj || !_snap_file) return;
+   _snap_ok = evas_object_image_save(_snap_obj, _snap_file, NULL, NULL);
+   if (!_snap_ok)
+     fprintf(stderr, "Failed to save frame to '%s'.\n", _snap_file);
+   /* only capture the first post-render after the snapshot went up */
+   _snap_file = NULL;
+}
+
 int
 main(int argc, char **argv)
 {
@@ -1462,6 +1487,72 @@ main(int argc, char **argv)
 
    ecore_app_args_set(argc, (const char **) argv);
 
+   /* Frame dump on a windowed engine: run the test in a real window of the
+    * requested engine and read the result back through a snapshot object.
+    * This is what makes it possible to compare a GL rendering against the
+    * software one produced by the buffer path below. */
+   if (output && engine && strcmp(engine, "buffer"))
+     {
+        int w = resolutions[resolution_index].width;
+        int h = resolutions[resolution_index].height;
+
+        ee = ecore_evas_new(engine, 0, 0, w, h, NULL);
+        if (!ee)
+          {
+             fprintf(stderr, "Failed to create Ecore_Evas. Please check engine "
+                             "name \"%s\"\n", engine);
+             return -1;
+          }
+
+        choosen_engine = ecore_evas_engine_name_get(ee);
+        evas = ecore_evas_get(ee);
+
+        evas_font_path_append(evas, datadir);
+        evas_font_hinting_set(evas, EVAS_FONT_HINTING_AUTO);
+
+        ui_setup(w, h);
+
+        if (test > 0)
+          {
+             _one_test_ = 1;
+             ui_num(test);
+          }
+
+        ecore_evas_show(ee);
+
+        /* Render every frame, not just the last one: incremental redraw and
+         * caching bugs only show up if the engine actually walks the whole
+         * frame sequence. */
+        while (loops > 0)
+          {
+             ui_loop(NULL);
+             ecore_evas_manual_render(ee);
+             loops--;
+          }
+
+        _snap_obj = evas_object_image_filled_add(evas);
+        evas_object_image_snapshot_set(_snap_obj, EINA_TRUE);
+        evas_object_move(_snap_obj, 0, 0);
+        evas_object_resize(_snap_obj, w, h);
+        evas_object_show(_snap_obj);
+
+        _snap_file = output;
+        evas_event_callback_add(evas, EVAS_CALLBACK_RENDER_POST,
+                                _cb_snapshot_save, NULL);
+        ecore_evas_manual_render(ee);
+        evas_event_callback_del(evas, EVAS_CALLBACK_RENDER_POST,
+                                _cb_snapshot_save);
+
+        if (!_snap_ok)
+          {
+             fprintf(stderr, "No frame captured from engine \"%s\".\n",
+                     choosen_engine);
+             return -1;
+          }
+
+        goto done;
+     }
+
    if (output)
      {
         Ecore_Evas *pee;
@@ -1504,10 +1595,10 @@ main(int argc, char **argv)
         while (loops > 0)
           {
              ui_loop(NULL);
+             ecore_evas_manual_render(pee);
              loops--;
           }
 
-        ecore_evas_manual_render(pee);
         evas_object_image_save(im, output, NULL, NULL);
 
         goto done;
diff --git a/src/bin/meson.build b/src/bin/meson.build
index 672e82d..ef3aa47 100644
--- a/src/bin/meson.build
+++ b/src/bin/meson.build
@@ -138,4 +138,14 @@ executable('expedite',
 	include_directories: config_dir,
 	dependencies: expedite_dependencies)
 
+executable('expedite-imgcmp',
+	'imgcmp.c',
+	install: true,
+	include_directories: config_dir,
+	dependencies: expedite_dependencies)
+
+install_data('expedite-parity',
+	install_dir: get_option('bindir'),
+	install_mode: 'rwxr-xr-x')
+
 

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

Reply via email to