[Test] (piglit-android): add on-device piglit harness for MobileGL - patched waffle (WAFFLE_EGL_LIBRARY/WAFFLE_GL_LIBRARY overrides so waffle drives libMobileGL.so directly, AImageReader-backed windows for DirectVulkan since Android ICDs lack VK_EXT_headless_surface, WAFFLE_FORCE_GL_CONTEXT_VERSION to upgrade piglit's low compat context requests to 3.3 core, meson cross fixes) and patched piglit (Android platform support, EGL support decoupled from the X11-dependent EGL tests, and a dispatch-init fix: the waffle resolvers were never installed because gl_fw is NULL during framework construction, so gl* silently bound to the system driver via the DT_NEEDED libEGL's eglGetProcAddress), plus the adb chunked runner with PIGLIT-result parsing, a results comparator, cross-file examples, and the piglit-on-android skill

This commit is contained in:
2026-07-26 11:57:11 -04:00
parent b8a8a660e1
commit c8632dfefe
9 changed files with 1272 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
# piglit on Android against MobileGL
Run [piglit](https://gitlab.freedesktop.org/mesa/piglit) desktop-GL tests on a
connected Android device with **MobileGL as the OpenGL implementation**, for
both backends:
- `DirectGLES` — MobileGL over the system GLES driver (or ANGLE with
`--use-angle`)
- `DirectVulkan` — MobileGL over the system Vulkan driver
No APK and no on-device python: piglit test binaries run as the adb shell user
from `/data/local/tmp`, create contexts through waffle's `surfaceless_egl`
platform, and waffle is pointed at `libMobileGL.so` (which exports the full
`egl*`/`gl*` API under real names).
## How it fits together
```
piglit test binary (aarch64, bionic)
└─ waffle surfaceless_egl (patched)
├─ WAFFLE_EGL_LIBRARY=libMobileGL.so → dlopen MobileGL as the EGL impl
├─ WAFFLE_GL_LIBRARY=libMobileGL.so → waffle_dl_sym resolves gl* here
├─ WAFFLE_FORCE_GL_CONTEXT_VERSION=33core
│ upgrades low compat context requests to GL 3.3 core (never
│ downgrades) so piglit's supports_gl_compat_version=10 tests run
└─ WAFFLE_ANDROID_WINDOW=imagereader (DirectVulkan only)
windows are AImageReader ANativeWindows instead of EGL pbuffers,
because Android ICDs lack VK_EXT_headless_surface which the
MobileGL pbuffer path needs
└─ libMobileGL.so
├─ DirectGLES: dlopens the real system libEGL.so internally
└─ DirectVulkan: links libvulkan.so
```
Key rule: **never name MobileGL `libEGL.so`** anywhere on `LD_LIBRARY_PATH`
the DirectGLES backend loads the system driver with a bare-soname
`dlopen("libEGL.so")` and would recursively pick itself up.
## One-time setup (host: macOS/Linux with the Android NDK)
```sh
WORK=path/to/workdir && cd $WORK
git clone --depth 1 https://gitlab.freedesktop.org/mesa/piglit.git
git clone --depth 1 https://gitlab.freedesktop.org/mesa/waffle.git
git -C waffle apply $MOBILEGL/tools/piglit-android/patches/waffle-mobilegl-android.patch
git -C piglit apply $MOBILEGL/tools/piglit-android/patches/piglit-mobilegl-android.patch
python3 -m venv venv && ./venv/bin/pip install mako numpy packaging
```
The piglit patch matters beyond build fixes: upstream's
`piglit_dispatch_default_init` runs while the waffle framework is still being
constructed (`gl_fw` is NULL), so the waffle resolvers were never installed and
GL functions bound through the **system** libEGL's `eglGetProcAddress` — every
test silently ran on the raw GLES driver instead of MobileGL.
Build MobileGL for Android:
```sh
cmake -S $MOBILEGL -B $MOBILEGL/build-android-arm64 -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-26 \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build $MOBILEGL/build-android-arm64 --target MobileGL -j
$NDK/toolchains/llvm/prebuilt/*/bin/llvm-strip --strip-unneeded \
-o $WORK/libMobileGL-stripped.so $MOBILEGL/build-android-arm64/libMobileGL.so
```
Cross-build waffle (meson; a cross file and a stub `egl.pc` pointing at
MobileGL's bundled EGL 1.5 headers are needed — see `cross-example/`):
```sh
cd $WORK/waffle
meson setup build-android --cross-file $WORK/cross/android-arm64.ini \
-Dbuildtype=release -Dsurfaceless_egl=enabled \
-Dglx=disabled -Dx11_egl=disabled -Dgbm=disabled -Dwayland=disabled \
-Dbuild-tests=false -Dbuild-examples=false -Dprefix=$WORK/prefix
ninja -C build-android && meson install -C build-android
```
Cross-build piglit (needs `PKG_CONFIG_LIBDIR` with the installed `waffle-1.pc`
plus the stub `egl.pc`):
```sh
cd $WORK/piglit && export PKG_CONFIG_LIBDIR=$WORK/prefix/lib/pkgconfig:$WORK/cross/pkgconfig
cmake -S . -B build-android -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-26 \
-DCMAKE_BUILD_TYPE=Release \
-DPIGLIT_USE_WAFFLE=ON -DPIGLIT_BUILD_GL_TESTS=ON \
-DPIGLIT_BUILD_GLES1_TESTS=OFF -DPIGLIT_BUILD_GLES2_TESTS=OFF \
-DPIGLIT_BUILD_GLES3_TESTS=OFF -DPIGLIT_BUILD_EGL_TESTS=OFF \
-DPIGLIT_BUILD_GLX_TESTS=OFF -DPIGLIT_BUILD_WGL_TESTS=OFF \
-DPIGLIT_BUILD_CL_TESTS=OFF -DPIGLIT_BUILD_VK_TESTS=OFF \
-DPIGLIT_BUILD_DMA_BUF_TESTS=OFF -DPIGLIT_USE_GBM=OFF \
-DPIGLIT_USE_WAYLAND=OFF -DPIGLIT_USE_X11=OFF \
-DPYTHON_EXECUTABLE=$WORK/venv/bin/python \
-DOPENGL_INCLUDE_DIR=$MOBILEGL/include \
-DOPENGL_gl_LIBRARY=$SYSROOT/usr/lib/aarch64-linux-android/26/libEGL.so \
-DGLEXT_INCLUDE_DIR=$MOBILEGL/include
ninja -C build-android
```
## Selecting tests
Enumerate on the host with piglit's own profiles (no device needed):
```sh
cd $WORK/piglit
for prof in opengl shader glslparser; do
PIGLIT_BUILD_DIR=$PWD/build-android ./venv/bin/python ./piglit print-cmd \
-t "spec@!opengl 3[.]" -t "spec@glsl-3[.]30" $prof
done > /tmp/gl33.list
```
Group names use `@` separators (`spec@!opengl 3.3@minmax`). The version groups
(`spec@!opengl 1.x…3.3`), GLSL groups (`spec@glsl-1.10…3.30`) plus the ARB
extension groups folded into GL 3.13.3 core give a comprehensive "GL 3.3
core" suite (~15k tests).
## Running
```sh
python3 $MOBILEGL/tools/piglit-android/run_piglit_android.py \
--piglit-root $WORK/piglit --list /tmp/gl33.list \
--backend DirectGLES \
--mobilegl-lib $WORK/libMobileGL-stripped.so \
--waffle-lib $WORK/waffle/build-android/src/waffle/libwaffle-1.so \
--out results-gles
# then the same with --backend DirectVulkan --out results-vk
```
The runner pushes binaries/libs/data (incremental; `--repush` forces), executes
tests serially in chunked on-device shell scripts under `timeout`, parses the
`PIGLIT: {...}` result lines, and writes `results.json` + `summary.txt` +
`raw.log`. Exit-code semantics: parsed result wins; nonzero exit without a
result line = `crash`; toybox timeout exits = `timeout`.
Quick sanity check for the whole stack (waffle build also produces `wflinfo`):
```sh
adb shell 'cd /data/local/tmp/piglit-mgl && env LD_LIBRARY_PATH=$PWD/lib \
WAFFLE_EGL_LIBRARY=libMobileGL.so WAFFLE_GL_LIBRARY=libMobileGL.so \
MOBILEGL_BACKEND_TYPE=DirectVulkan WAFFLE_ANDROID_WINDOW=imagereader \
./wflinfo --platform surfaceless_egl --api gl --version 3.3 --profile core'
```
Expect `OpenGL version string: 3.3.0 MobileGL …, Direct (Vulkan) Backend`.
## Known caveats
- MSAA winsys configs never match (MobileGL exposes two RGBA8888 configs,
samples=0); MSAA FBO tests are unaffected.
- Tests that genuinely require compatibility-profile features will fail on the
forced 3.3 core context; that is honest for a core-only implementation.
- `eglTerminate` at test exit now tears MobileGL down deterministically (see
the EGL-lifecycle refactor); a device-side `mobilegl.log` is written per run
directory for debugging.
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
# MobileGL - tools/piglit-android/compare_results.py
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""Compare two run_piglit_android.py results.json files (e.g. DirectGLES vs
DirectVulkan) and write a markdown report of totals plus categorized diffs."""
import argparse
import json
from collections import Counter
from pathlib import Path
BAD = ('crash', 'timeout', 'fail', 'missing', 'notrun', 'warn')
def load(path):
d = json.loads(Path(path).read_text())
return d
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('a', help='first results.json')
ap.add_argument('b', help='second results.json')
ap.add_argument('-o', '--out', help='markdown output path')
args = ap.parse_args()
da, db = load(args.a), load(args.b)
na, nb = da['backend'], db['backend']
ta, tb = da['tests'], db['tests']
names = sorted(set(ta) | set(tb))
lines = [f'# piglit: {na} vs {nb}', '']
lines.append(f'| | {na} | {nb} |')
lines.append('|---|---|---|')
ca = Counter(o["status"] for o in ta.values())
cb = Counter(o["status"] for o in tb.values())
for k in sorted(set(ca) | set(cb)):
lines.append(f'| {k} | {ca.get(k, 0)} | {cb.get(k, 0)} |')
lines.append(f'| total | {len(ta)} | {len(tb)} |')
lines.append(f'| elapsed | {da.get("elapsed_sec")}s | {db.get("elapsed_sec")}s |')
lines.append('')
def bucket(pred, title):
rows = [n for n in names
if pred(ta.get(n, {}).get('status', 'absent'),
tb.get(n, {}).get('status', 'absent'))]
if rows:
lines.append(f'## {title} ({len(rows)})')
lines.append('')
for n in rows:
sa = ta.get(n, {}).get('status', 'absent')
sb = tb.get(n, {}).get('status', 'absent')
lines.append(f'- `{n}` — {na}: {sa}, {nb}: {sb}')
lines.append('')
bucket(lambda a, b: a in BAD and b in BAD, 'Bad on both (likely frontend/state-tracker)')
bucket(lambda a, b: a in BAD and b == 'pass', f'Bad only on {na}')
bucket(lambda a, b: a == 'pass' and b in BAD, f'Bad only on {nb}')
bucket(lambda a, b: a == 'skip' and b == 'pass' or a == 'pass' and b == 'skip',
'Skip on one side only')
text = '\n'.join(lines) + '\n'
if args.out:
Path(args.out).write_text(text)
print(f'wrote {args.out}')
else:
print(text)
if __name__ == '__main__':
main()
@@ -0,0 +1,19 @@
; meson cross file for waffle -> aarch64 Android
; Replace NDK_TOOLCHAIN with e.g.
; $HOME/Library/Android/sdk/ndk/27.3.13750724/toolchains/llvm/prebuilt/darwin-x86_64
; and PKGCONFIG_DIR with the directory holding the stub egl.pc.
[binaries]
c = 'NDK_TOOLCHAIN/bin/aarch64-linux-android26-clang'
cpp = 'NDK_TOOLCHAIN/bin/aarch64-linux-android26-clang++'
ar = 'NDK_TOOLCHAIN/bin/llvm-ar'
strip = 'NDK_TOOLCHAIN/bin/llvm-strip'
pkg-config = '/usr/bin/pkg-config'
[host_machine]
system = 'android'
cpu_family = 'aarch64'
cpu = 'aarch64'
endian = 'little'
[properties]
pkg_config_libdir = 'PKGCONFIG_DIR'
@@ -0,0 +1,8 @@
# Stub egl.pc for cross-building waffle/piglit against the NDK.
# The NDK's own EGL headers are 1.4-era; point Cflags at MobileGL's bundled
# EGL 1.5 headers (include/EGL) instead. -lEGL resolves to the NDK stub.
Name: egl
Description: EGL 1.5 headers (MobileGL bundled) + NDK libEGL stub
Version: 1.5
Libs: -lEGL
Cflags: -IMOBILEGL_REPO/include
@@ -0,0 +1,104 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1a38c21..b0b6ce2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -22,7 +22,7 @@ INCLUDE (FindPkgConfig)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
-if(${CMAKE_SYSTEM_NAME} MATCHES "Linux|FreeBSD")
+if(${CMAKE_SYSTEM_NAME} MATCHES "Linux|FreeBSD|Android")
set(DEFAULT_EGL ON)
set(DEFAULT_GLX ON)
set(DEFAULT_WGL OFF)
@@ -499,13 +499,18 @@ if(GBM_FOUND)
endif(HAVE_LIBCACA)
endif(GBM_FOUND)
-if(PIGLIT_BUILD_EGL_TESTS)
- pkg_check_modules(EGL REQUIRED egl)
+# EGL *support* (PIGLIT_HAS_EGL, e.g. the surfaceless_egl waffle platform) is
+# independent of building the EGL test binaries, which additionally need X11.
+pkg_check_modules(EGL egl)
+if(EGL_FOUND)
set(PIGLIT_HAS_EGL True)
add_definitions(-DPIGLIT_HAS_EGL)
include_directories(${EGL_INCLUDE_DIRS})
add_definitions (${EGL_CFLAGS_OTHER})
endif()
+if(PIGLIT_BUILD_EGL_TESTS AND NOT EGL_FOUND)
+ message(FATAL_ERROR "PIGLIT_BUILD_EGL_TESTS requires EGL")
+endif()
# Put all executables into the bin subdirectory
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${piglit_BINARY_DIR}/bin)
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e3c964a..36e2109 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -35,9 +35,9 @@ add_subdirectory (llvmpipe)
add_subdirectory (perf)
add_subdirectory (wgl)
-IF(EGL_FOUND)
+IF(PIGLIT_BUILD_EGL_TESTS)
add_subdirectory (egl)
-ENDIF(EGL_FOUND)
+ENDIF(PIGLIT_BUILD_EGL_TESTS)
IF(PIGLIT_BUILD_CL_TESTS)
add_subdirectory (cl)
diff --git a/tests/util/piglit-dispatch-init.c b/tests/util/piglit-dispatch-init.c
index 3d9772d..e036eac 100644
--- a/tests/util/piglit-dispatch-init.c
+++ b/tests/util/piglit-dispatch-init.c
@@ -322,14 +322,19 @@ piglit_dispatch_default_init(piglit_dispatch_api api)
break;
}
- if (gl_fw) {
- piglit_dispatch_init(api,
- get_wfl_core_proc,
- get_wfl_ext_proc,
- default_unsupported,
- default_get_proc_address_failure);
- } else
-#endif
+ /* MobileGL piglit harness: always use the waffle resolvers in waffle
+ * builds. This function runs while the waffle framework is still being
+ * constructed, so gl_fw is NULL here and the gl_fw branch never fired;
+ * the fallback resolvers bind GL functions via the SYSTEM libEGL's
+ * eglGetProcAddress (a DT_NEEDED symbol), which on Android is the raw
+ * GLES driver rather than the waffle-selected EGL implementation.
+ */
+ piglit_dispatch_init(api,
+ get_wfl_core_proc,
+ get_wfl_ext_proc,
+ default_unsupported,
+ default_get_proc_address_failure);
+#else
{
piglit_dispatch_init(api,
@@ -338,6 +343,7 @@ piglit_dispatch_default_init(piglit_dispatch_api api)
default_unsupported,
default_get_proc_address_failure);
}
+#endif
already_initialized = true;
}
diff --git a/tests/util/piglit-util-gl.c b/tests/util/piglit-util-gl.c
index 16eb425..7f2796c 100644
--- a/tests/util/piglit-util-gl.c
+++ b/tests/util/piglit-util-gl.c
@@ -47,6 +47,9 @@ bool piglit_is_core_profile;
bool piglit_is_gles(void)
{
const char *version_string = (const char *) glGetString(GL_VERSION);
+ if (getenv("PIGLIT_DEBUG_VERSION_STRING"))
+ printf("piglit: debug: GL_VERSION = \"%s\"\n",
+ version_string ? version_string : "(null)");
return strncmp("OpenGL ES", version_string, 9) == 0;
}
@@ -0,0 +1,445 @@
diff --git a/meson.build b/meson.build
index 7060fce..dcd45ed 100644
--- a/meson.build
+++ b/meson.build
@@ -78,7 +78,11 @@ else
dep_egl = dependency('egl', required : get_option('surfaceless_egl'))
build_surfaceless = dep_egl.found()
- dep_egl = dependency('egl', required : get_option('wayland'))
+ # Don't clobber a dep_egl found for surfaceless_egl when wayland is
+ # disabled (a disabled-feature dependency() returns not-found).
+ if not dep_egl.found()
+ dep_egl = dependency('egl', required : get_option('wayland'))
+ endif
dep_wayland_client = dependency(
'wayland-client', version : '>= 1.10', required : get_option('wayland'),
)
diff --git a/src/waffle/egl/wegl_context.c b/src/waffle/egl/wegl_context.c
index 26362a9..a26c27f 100644
--- a/src/waffle/egl/wegl_context.c
+++ b/src/waffle/egl/wegl_context.c
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: BSD-2-Clause
#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
@@ -19,6 +21,25 @@
#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS 0x31B2
#endif
+// MobileGL piglit harness: WAFFLE_FORCE_GL_CONTEXT_VERSION="33core" upgrades
+// every desktop-GL context request to at least that version/profile (never
+// downgrades). MobileGL implements exactly GL 3.3 core; piglit tests that ask
+// for a low compat context (supports_gl_compat_version=10) would otherwise
+// get a context whose version string reflects the raw backend, and skip.
+static bool
+force_gl_context_version(EGLint *major, EGLint *minor, bool *core)
+{
+ const char *env = getenv("WAFFLE_FORCE_GL_CONTEXT_VERSION");
+ if (!env || strlen(env) < 2)
+ return false;
+ if (env[0] < '1' || env[0] > '9' || env[1] < '0' || env[1] > '9')
+ return false;
+ *major = env[0] - '0';
+ *minor = env[1] - '0';
+ *core = strstr(env, "core") != NULL;
+ return true;
+}
+
static bool
bind_api(struct wegl_platform *plat, int32_t waffle_context_api)
{
@@ -61,13 +82,30 @@ create_real_context(struct wegl_config *config,
context_flags |= EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR;
}
+ // Effective version/profile for desktop GL, possibly upgraded by
+ // WAFFLE_FORCE_GL_CONTEXT_VERSION (never downgraded).
+ EGLint eff_major = attrs->context_major_version;
+ EGLint eff_minor = attrs->context_minor_version;
+ int32_t eff_profile = attrs->context_profile;
+ if (waffle_context_api == WAFFLE_CONTEXT_OPENGL) {
+ EGLint f_major, f_minor;
+ bool f_core;
+ if (force_gl_context_version(&f_major, &f_minor, &f_core) &&
+ 10 * f_major + f_minor > 10 * eff_major + eff_minor) {
+ eff_major = f_major;
+ eff_minor = f_minor;
+ if (f_core)
+ eff_profile = WAFFLE_CONTEXT_CORE_PROFILE;
+ }
+ }
+
switch (waffle_context_api) {
case WAFFLE_CONTEXT_OPENGL:
if (dpy->KHR_create_context) {
attrib_list[i++] = EGL_CONTEXT_MAJOR_VERSION_KHR;
- attrib_list[i++] = attrs->context_major_version;
+ attrib_list[i++] = eff_major;
attrib_list[i++] = EGL_CONTEXT_MINOR_VERSION_KHR;
- attrib_list[i++] = attrs->context_minor_version;
+ attrib_list[i++] = eff_minor;
}
else {
assert(attrs->context_major_version == 1);
@@ -92,9 +130,9 @@ create_real_context(struct wegl_config *config,
}
}
- if (wcore_config_attrs_version_ge(attrs, 32)) {
+ if (10 * eff_major + eff_minor >= 32) {
assert(dpy->KHR_create_context);
- switch (attrs->context_profile) {
+ switch (eff_profile) {
case WAFFLE_CONTEXT_CORE_PROFILE:
attrib_list[i++] = EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR;
attrib_list[i++] = EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR;
diff --git a/src/waffle/egl/wegl_platform.c b/src/waffle/egl/wegl_platform.c
index 6fe72ba..c3d72b1 100644
--- a/src/waffle/egl/wegl_platform.c
+++ b/src/waffle/egl/wegl_platform.c
@@ -13,11 +13,24 @@
#include "wegl_platform.h"
#ifdef WAFFLE_HAS_ANDROID
-static const char *libEGL_filename = "libEGL.so";
+static const char *libEGL_default_filename = "libEGL.so";
#else
-static const char *libEGL_filename = "libEGL.so.1";
+static const char *libEGL_default_filename = "libEGL.so.1";
#endif
+// MobileGL piglit harness: allow overriding which library provides the EGL
+// entry points (e.g. WAFFLE_EGL_LIBRARY=libMobileGL.so). MobileGL exports the
+// full EGL API under the real egl* names, so waffle can drive it directly
+// while the system libEGL.so stays untouched for MobileGL's own backend use.
+static const char *
+egl_library_name(void)
+{
+ const char *env = getenv("WAFFLE_EGL_LIBRARY");
+ if (env && env[0])
+ return env;
+ return libEGL_default_filename;
+}
+
static bool
supports_egl_khr_display(const struct wegl_platform *plat,
const char *client_extensions)
@@ -120,7 +133,7 @@ wegl_platform_teardown(struct wegl_platform *self)
ok = false;
wcore_errorf(WAFFLE_ERROR_UNKNOWN,
"dlclose(\"%s\") failed: %s",
- libEGL_filename, dlerror());
+ egl_library_name(), dlerror());
}
self->egl.handle = NULL;
}
@@ -132,12 +145,19 @@ wegl_platform_teardown(struct wegl_platform *self)
bool
wegl_platform_init(struct wegl_platform *self, EGLenum egl_platform)
{
- static const char *const dso_names[] = {
+ static const char *dso_names[] = {
"libGL.so.1",
"libGLESv1_CM.so.1",
"libGLESv2.so.2",
};
+ // MobileGL piglit harness: let waffle_dl_sym(WAFFLE_DL_OPENGL, ...)
+ // resolve desktop-GL symbols from an alternate library (MobileGL exports
+ // real gl* names). Android has no libGL.so.1.
+ const char *gl_override = getenv("WAFFLE_GL_LIBRARY");
+ if (gl_override && gl_override[0])
+ dso_names[0] = gl_override;
+
wcore_platform_init(&self->wcore);
posix_platform_init(&self->wcore, dso_names);
@@ -146,11 +166,11 @@ wegl_platform_init(struct wegl_platform *self, EGLenum egl_platform)
// Most Waffle platforms will call eglCreateWindowSurface.
self->egl_surface_type_mask = EGL_WINDOW_BIT;
- self->egl.handle = dlopen(libEGL_filename, RTLD_LAZY | RTLD_LOCAL);
+ self->egl.handle = dlopen(egl_library_name(), RTLD_LAZY | RTLD_LOCAL);
if (!self->egl.handle) {
wcore_errorf(WAFFLE_ERROR_FATAL,
"dlopen(\"%s\") failed: %s",
- libEGL_filename, dlerror());
+ egl_library_name(), dlerror());
return false;
}
@@ -158,7 +178,7 @@ wegl_platform_init(struct wegl_platform *self, EGLenum egl_platform)
self->egl.function = dlsym(self->egl.handle, "egl" #function); \
if (!self->egl.function) { \
wcore_errorf(WAFFLE_ERROR_FATAL, "dlsym(\"%s\", \"%s\") failed: %s", \
- libEGL_filename, "egl" #function, dlerror()); \
+ egl_library_name(), "egl" #function, dlerror()); \
goto error; \
}
diff --git a/src/waffle/meson.build b/src/waffle/meson.build
index 2ae4c40..e336566 100644
--- a/src/waffle/meson.build
+++ b/src/waffle/meson.build
@@ -22,6 +22,16 @@ deps_for_waffle = [
idep_threads,
]
+# MobileGL piglit harness: the Android imagereader window mode in
+# surfaceless_egl needs AImageReader (libmediandk) and ANativeWindow
+# (libandroid).
+if build_surfaceless and host_machine.system() == 'android'
+ deps_for_waffle += [
+ cc.find_library('mediandk'),
+ cc.find_library('android'),
+ ]
+endif
+
files_libwaffle = files(
'api/api_priv.c',
'api/waffle_attrib_list.c',
diff --git a/src/waffle/surfaceless_egl/sl_platform.c b/src/waffle/surfaceless_egl/sl_platform.c
index 30018c1..2c0024c 100644
--- a/src/waffle/surfaceless_egl/sl_platform.c
+++ b/src/waffle/surfaceless_egl/sl_platform.c
@@ -101,7 +101,7 @@ static const struct wcore_platform_vtbl sl_platform_vtbl = {
.destroy = sl_window_destroy,
.show = sl_window_show,
.resize = sl_window_resize,
- .swap_buffers = wegl_surface_swap_buffers,
+ .swap_buffers = sl_window_swap_buffers,
.get_native = sl_window_get_native,
},
};
diff --git a/src/waffle/surfaceless_egl/sl_platform.h b/src/waffle/surfaceless_egl/sl_platform.h
index a97fabe..a1c91aa 100644
--- a/src/waffle/surfaceless_egl/sl_platform.h
+++ b/src/waffle/surfaceless_egl/sl_platform.h
@@ -5,7 +5,6 @@
#include <stdbool.h>
#include <stdlib.h>
-#include <gbm.h>
#undef linux
diff --git a/src/waffle/surfaceless_egl/sl_window.c b/src/waffle/surfaceless_egl/sl_window.c
index 0c54613..d18f8b3 100644
--- a/src/waffle/surfaceless_egl/sl_window.c
+++ b/src/waffle/surfaceless_egl/sl_window.c
@@ -9,18 +9,108 @@
#include "wcore_tinfo.h"
#include "wegl_config.h"
+#include "wegl_display.h"
+#include "wegl_platform.h"
#include "wegl_util.h"
#include "sl_display.h"
#include "sl_platform.h"
#include "sl_window.h"
+#ifdef __ANDROID__
+#include <android/hardware_buffer.h>
+#include <android/native_window.h>
+#include <media/NdkImageReader.h>
+
+// MobileGL piglit harness: back "windows" with an AImageReader-provided
+// ANativeWindow instead of an EGL pbuffer. MobileGL's DirectVulkan backend
+// needs a real ANativeWindow on Android because the device ICD lacks
+// VK_EXT_headless_surface (which its pbuffer path requires).
+static bool
+sl_android_window_mode(void)
+{
+ const char *env = getenv("WAFFLE_ANDROID_WINDOW");
+ return env && strcmp(env, "imagereader") == 0;
+}
+
+static bool
+sl_imagereader_init(struct sl_window *self,
+ struct wcore_config *wc_config,
+ int32_t width, int32_t height)
+{
+ struct wegl_config *config = wegl_config(wc_config);
+ struct wegl_display *dpy = wegl_display(wc_config->display);
+ struct wegl_platform *plat = wegl_platform(dpy->wcore.platform);
+ AImageReader *reader = NULL;
+ ANativeWindow *window = NULL;
+
+ media_status_t st = AImageReader_newWithUsage(
+ width, height, AIMAGE_FORMAT_RGBA_8888,
+ AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT |
+ AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE,
+ 8, &reader);
+ if (st != AMEDIA_OK || !reader) {
+ wcore_errorf(WAFFLE_ERROR_UNKNOWN,
+ "AImageReader_newWithUsage(%dx%d) failed: %d",
+ width, height, (int)st);
+ return false;
+ }
+
+ st = AImageReader_getWindow(reader, &window);
+ if (st != AMEDIA_OK || !window) {
+ wcore_errorf(WAFFLE_ERROR_UNKNOWN,
+ "AImageReader_getWindow failed: %d", (int)st);
+ AImageReader_delete(reader);
+ return false;
+ }
+
+ // MobileGL reads EGL_WIDTH/EGL_HEIGHT from the window-surface attrib
+ // list (non-standard); unknown attribs are ignored.
+ EGLint attrib_list[] = {
+ EGL_RENDER_BUFFER, EGL_BACK_BUFFER,
+ EGL_WIDTH, width,
+ EGL_HEIGHT, height,
+ EGL_NONE,
+ };
+
+ self->wegl.egl = plat->egl.CreateWindowSurface(
+ dpy->egl, config->egl, (EGLNativeWindowType)window, attrib_list);
+ if (!self->wegl.egl) {
+ wegl_emit_error(plat, "eglCreateWindowSurface");
+ AImageReader_delete(reader);
+ return false;
+ }
+
+ self->reader = reader;
+ return true;
+}
+
+static void
+sl_imagereader_drain(struct sl_window *self)
+{
+ if (!self->reader)
+ return;
+
+ // Keep the BufferQueue from filling up: nobody displays these frames,
+ // so consume-and-drop the latest (which also releases all older ones).
+ AImage *image = NULL;
+ if (AImageReader_acquireLatestImage(self->reader, &image) == AMEDIA_OK &&
+ image)
+ AImage_delete(image);
+}
+#endif // __ANDROID__
+
bool
sl_window_destroy(struct wcore_window *wc_self)
{
struct sl_window *self = sl_window(wegl_surface(wc_self));
bool ok = wegl_surface_teardown(&self->wegl);
+#ifdef __ANDROID__
+ if (self->reader)
+ AImageReader_delete(self->reader);
+#endif
+
free(self);
return ok;
}
@@ -52,6 +142,11 @@ sl_window_create(struct wcore_platform *wc_plat,
wcore_window_init(&self->wegl.wcore, wc_config);
+#ifdef __ANDROID__
+ if (sl_android_window_mode())
+ ok = sl_imagereader_init(self, wc_config, width, height);
+ else
+#endif
ok = wegl_pbuffer_init(&self->wegl, wc_config, width, height);
if (!ok)
goto error;
@@ -71,6 +166,18 @@ sl_window_show(struct wcore_window *wc_self)
return true;
}
+bool
+sl_window_swap_buffers(struct wcore_window *wc_self)
+{
+ bool ok = wegl_surface_swap_buffers(wc_self);
+
+#ifdef __ANDROID__
+ sl_imagereader_drain(sl_window(wegl_surface(wc_self)));
+#endif
+
+ return ok;
+}
+
bool
sl_window_resize(struct wcore_window *wc_self,
int32_t width, int32_t height)
@@ -83,6 +190,36 @@ sl_window_resize(struct wcore_window *wc_self,
struct wcore_tinfo *tinfo;
bool ok = true;
+#ifdef __ANDROID__
+ if (self->reader) {
+ // ImageReader-backed window: build a fresh reader + EGL window
+ // surface at the new size, make it current, then retire the old one.
+ struct sl_window new_win;
+ memset(&new_win, 0, sizeof(new_win));
+ wcore_window_init(&new_win.wegl.wcore, self->wc_config);
+
+ ok = sl_imagereader_init(&new_win, self->wc_config, width, height);
+ if (!ok)
+ return false;
+
+ tinfo = wcore_tinfo_get();
+ wc_ctx = tinfo->current_context;
+
+ ok = wegl_make_current(wc_plat, wc_dpy, &new_win.wegl.wcore, wc_ctx);
+ if (!ok) {
+ wegl_surface_teardown(&new_win.wegl);
+ AImageReader_delete(new_win.reader);
+ return false;
+ }
+
+ wegl_surface_teardown(&self->wegl);
+ AImageReader_delete(self->reader);
+ self->wegl.egl = new_win.wegl.egl;
+ self->reader = new_win.reader;
+ return true;
+ }
+#endif
+
// Create a new pbuffer for the resized window.
ok = wegl_pbuffer_init(&new_wegl, self->wc_config, width, height);
if (!ok)
diff --git a/src/waffle/surfaceless_egl/sl_window.h b/src/waffle/surfaceless_egl/sl_window.h
index 8a96239..65a86ad 100644
--- a/src/waffle/surfaceless_egl/sl_window.h
+++ b/src/waffle/surfaceless_egl/sl_window.h
@@ -9,9 +9,18 @@
struct wcore_platform;
+#ifdef __ANDROID__
+typedef struct AImageReader AImageReader;
+#endif
+
struct sl_window {
struct wegl_surface wegl;
struct wcore_config *wc_config;
+#ifdef __ANDROID__
+ // Non-NULL when the window is backed by an AImageReader ANativeWindow
+ // (WAFFLE_ANDROID_WINDOW=imagereader) instead of an EGL pbuffer.
+ AImageReader *reader;
+#endif
};
DEFINE_CONTAINER_CAST_FUNC(sl_window,
@@ -31,6 +40,9 @@ sl_window_destroy(struct wcore_window *wc_self);
bool
sl_window_show(struct wcore_window *wc_self);
+bool
+sl_window_swap_buffers(struct wcore_window *wc_self);
+
bool
sl_window_resize(struct wcore_window *wc_self,
int32_t width, int32_t height);
+347
View File
@@ -0,0 +1,347 @@
#!/usr/bin/env python3
# MobileGL - tools/piglit-android/run_piglit_android.py
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""Run piglit GL tests on a connected Android device against MobileGL.
Pipeline:
1. Read a test list produced by `piglit print-cmd` ("name ::: command" lines).
2. Rewrite host paths to on-device paths and collect referenced data files.
3. Push binaries/libs/data to the device (incremental, marker-based).
4. Execute tests serially on-device in chunked shell scripts under `timeout`,
with MobileGL served to waffle via WAFFLE_EGL_LIBRARY=libMobileGL.so.
5. Parse "PIGLIT: {...}" result lines, classify, and write results + summary.
The device never needs python or an APK; everything runs as the adb shell user
from /data/local/tmp.
"""
import argparse
import json
import os
import re
import shlex
import subprocess
import sys
import tarfile
import tempfile
import time
from pathlib import Path
RESULT_LINE = re.compile(rb'PIGLIT: *({.*})')
MARK_START = re.compile(rb'@@@T (\d+) START')
MARK_EXIT = re.compile(rb'@@@T (\d+) EXIT (\d+)')
# toybox timeout exit codes: 124 = timed out (SIGTERM), 137 = SIGKILL after -k.
TIMEOUT_EXITS = {124, 137, 142}
def adb(args, serial=None, **kw):
cmd = ['adb'] + (['-s', serial] if serial else []) + args
return subprocess.run(cmd, **kw)
def adb_check(args, serial=None):
r = adb(args, serial=serial, capture_output=True)
if r.returncode != 0:
raise RuntimeError(f"adb {' '.join(args)} failed: {r.stderr.decode(errors='replace')}")
return r.stdout
def parse_list(path):
tests = []
for line in Path(path).read_text().splitlines():
line = line.strip()
if not line or ' ::: ' not in line:
continue
name, cmd = line.split(' ::: ', 1)
tests.append((name.strip(), shlex.split(cmd)))
return tests
def rewrite_cmd(argv, piglit_root, build_dir, device_dir):
"""Map host paths in a test command to device paths.
Returns (device_argv, referenced_host_files).
"""
build_abs = str((piglit_root / build_dir).resolve())
src_abs = str(piglit_root.resolve())
out = []
refs = []
for i, arg in enumerate(argv):
a = arg
if i == 0:
# program: "build-android/bin/foo" or absolute
prog = a if os.path.isabs(a) else str((piglit_root / a).resolve())
refs.append(prog)
out.append(f'{device_dir}/bin/{os.path.basename(prog)}')
continue
p = a if os.path.isabs(a) else None
if p and p.startswith(build_abs + '/generated_tests/'):
refs.append(p)
out.append(p.replace(build_abs + '/generated_tests', device_dir + '/generated_tests', 1))
elif p and p.startswith(build_abs + '/tests/'):
# Serialized profiles record data paths under the build dir, but
# only *generated* data lives there in an out-of-tree build;
# plain test files stay in the source tests/ dir.
if not os.path.exists(p):
p = p.replace(build_abs + '/tests', src_abs + '/tests', 1)
refs.append(p)
out.append(p.replace(src_abs + '/tests', device_dir + '/tests', 1))
else:
refs.append(p)
out.append(p.replace(build_abs + '/tests', device_dir + '/build-tests', 1))
elif p and p.startswith(src_abs + '/tests/'):
refs.append(p)
out.append(p.replace(src_abs + '/tests', device_dir + '/tests', 1))
else:
out.append(a)
return out, refs
def backend_env(backend, device_dir, use_angle=False):
env = {
'LD_LIBRARY_PATH': f'{device_dir}/lib',
'WAFFLE_EGL_LIBRARY': 'libMobileGL.so',
'WAFFLE_GL_LIBRARY': 'libMobileGL.so',
# Upgrade low compat context requests to what MobileGL implements;
# without this, piglit's supports_gl_compat_version=10 tests get the
# raw backend version string and skip themselves.
'WAFFLE_FORCE_GL_CONTEXT_VERSION': '33core',
'PIGLIT_PLATFORM': 'surfaceless_egl',
'PIGLIT_SOURCE_DIR': device_dir,
'MOBILEGL_BACKEND_TYPE': backend,
'MOBILEGL_LOG_FILE_PATH': f'{device_dir}/mobilegl.log',
}
if backend == 'DirectGLES':
env['MOBILEGL_USE_ANGLE'] = '1' if use_angle else '0'
if backend == 'DirectVulkan':
# The device ICD usually lacks VK_EXT_headless_surface, which the
# MobileGL pbuffer path needs; use a real ANativeWindow from
# AImageReader instead (patched waffle surfaceless_egl platform).
env['WAFFLE_ANDROID_WINDOW'] = 'imagereader'
return env
def push_tree(host, dev, serial, marker_name, force=False):
"""Push a directory once, tracked by a marker file on the device."""
marker = f'{dev}.pushed-{marker_name}'
if not force:
r = adb(['shell', f'test -e {shlex.quote(marker)} && echo yes'],
serial=serial, capture_output=True)
if r.stdout.strip() == b'yes':
return False
print(f' pushing {host} -> {dev}')
adb_check(['shell', f'rm -rf {shlex.quote(dev)}'], serial=serial)
adb_check(['push', str(host), dev], serial=serial)
adb_check(['shell', f'touch {shlex.quote(marker)}'], serial=serial)
return True
def push_files(files, strip_prefix, dev_prefix, serial):
"""Tar up referenced data files (relative to strip_prefix) and unpack on device."""
files = sorted(set(files))
if not files:
return
with tempfile.NamedTemporaryFile(suffix='.tar', delete=False) as tf:
tar_path = tf.name
with tarfile.open(tar_path, 'w') as tar:
for f in files:
rel = os.path.relpath(f, strip_prefix)
if rel.startswith('..'):
raise RuntimeError(f'file {f} not under {strip_prefix}')
tar.add(f, arcname=rel)
dev_tar = dev_prefix + '/.data.tar'
adb_check(['shell', f'mkdir -p {shlex.quote(dev_prefix)}'], serial=serial)
adb_check(['push', tar_path, dev_tar], serial=serial)
adb_check(['shell', f'cd {shlex.quote(dev_prefix)} && tar xf .data.tar && rm .data.tar'],
serial=serial)
os.unlink(tar_path)
def classify(exit_code, piglit_results, timed_out):
if timed_out:
return 'timeout'
result = None
for r in piglit_results:
if 'result' in r:
result = r['result']
if result is None:
return 'crash' if exit_code != 0 else 'notrun'
if exit_code not in (0, 1):
return 'crash'
return result
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--piglit-root', required=True, type=Path,
help='piglit source checkout (with the Android build dir inside)')
ap.add_argument('--build-dir', default='build-android',
help='Android build dir name inside piglit root')
ap.add_argument('--list', required=True,
help='test list file from `piglit print-cmd` (name ::: cmd)')
ap.add_argument('--backend', required=True, choices=['DirectGLES', 'DirectVulkan'])
ap.add_argument('--use-angle', action='store_true',
help='DirectGLES only: let MobileGL load ANGLE instead of the system driver')
ap.add_argument('--mobilegl-lib', required=True, type=Path,
help='stripped libMobileGL.so for the device')
ap.add_argument('--waffle-lib', required=True, type=Path,
help='cross-built libwaffle-1.so')
ap.add_argument('--device-dir', default='/data/local/tmp/piglit-mgl')
ap.add_argument('--out', required=True, type=Path, help='host results directory')
ap.add_argument('--serial', default=None, help='adb device serial')
ap.add_argument('--timeout', type=int, default=60, help='per-test timeout (seconds)')
ap.add_argument('--chunk', type=int, default=200, help='tests per device-side script')
ap.add_argument('--repush', action='store_true', help='force re-push of bin/lib/tests trees')
args = ap.parse_args()
piglit_root = args.piglit_root.resolve()
build = piglit_root / args.build_dir
dev = args.device_dir.rstrip('/')
args.out.mkdir(parents=True, exist_ok=True)
tests = parse_list(args.list)
if not tests:
print('no tests in list', file=sys.stderr)
return 2
print(f'[1/4] {len(tests)} tests; rewriting paths')
dev_cmds = []
data_refs = []
for name, argv in tests:
dcmd, refs = rewrite_cmd(argv, piglit_root, args.build_dir, dev)
dev_cmds.append((name, dcmd))
data_refs.extend(r for r in refs if not r.split('/')[-2:][0] == 'bin')
print('[2/4] pushing artifacts')
adb_check(['shell', f'mkdir -p {dev} {dev}/lib {dev}/chunks {dev}/logs'], serial=args.serial)
push_tree(build / 'bin', f'{dev}/bin', args.serial, 'bin', args.repush)
adb_check(['shell', f'chmod -R 755 {dev}/bin'], serial=args.serial)
# libs: piglit utils + waffle + MobileGL in one LD_LIBRARY_PATH dir
for lib in sorted((build / 'lib').glob('*.so')):
adb_check(['push', str(lib), f'{dev}/lib/'], serial=args.serial)
adb_check(['push', str(args.waffle_lib), f'{dev}/lib/libwaffle-1.so'], serial=args.serial)
adb_check(['push', str(args.mobilegl_lib), f'{dev}/lib/libMobileGL.so'], serial=args.serial)
# data files referenced by this run (tests/, build-tests/, generated_tests/)
src_abs = str(piglit_root)
build_abs = str(build)
groups = {
(src_abs + '/tests', f'{dev}/tests'): [],
(build_abs + '/tests', f'{dev}/build-tests'): [],
(build_abs + '/generated_tests', f'{dev}/generated_tests'): [],
}
for r in set(data_refs):
for (host_prefix, dev_prefix), bucket in groups.items():
if r.startswith(host_prefix + '/'):
bucket.append(r)
break
for (host_prefix, dev_prefix), bucket in groups.items():
push_files(bucket, host_prefix, dev_prefix, args.serial)
env = backend_env(args.backend, dev, args.use_angle)
env_lines = '\n'.join(f'export {k}={shlex.quote(v)}' for k, v in env.items())
print(f'[3/4] running {len(tests)} tests on {args.backend} '
f'(timeout {args.timeout}s/test, chunks of {args.chunk})')
raw_log = (args.out / 'raw.log').open('wb')
t0 = time.time()
outcomes = {}
chunks = [dev_cmds[i:i + args.chunk] for i in range(0, len(dev_cmds), args.chunk)]
idx_base = 0
for ci, chunk in enumerate(chunks):
lines = ['#!/system/bin/sh', env_lines, f'cd {dev}']
for j, (name, dcmd) in enumerate(chunk):
idx = idx_base + j
quoted = ' '.join(shlex.quote(a) for a in dcmd)
lines.append(f'echo "@@@T {idx} START"')
lines.append(f'timeout -k 5 {args.timeout} {quoted} </dev/null 2>&1')
lines.append(f'echo "@@@T {idx} EXIT $?"')
script = '\n'.join(lines) + '\n'
with tempfile.NamedTemporaryFile('w', suffix='.sh', delete=False) as tf:
tf.write(script)
host_script = tf.name
dev_script = f'{dev}/chunks/chunk{ci}.sh'
adb_check(['push', host_script, dev_script], serial=args.serial)
os.unlink(host_script)
r = adb(['shell', f'sh {dev_script}'], serial=args.serial, capture_output=True)
raw_log.write(r.stdout)
raw_log.flush()
# parse this chunk
cur = None
cur_results = []
cur_out = []
for line in r.stdout.splitlines():
m = MARK_START.search(line)
if m:
cur = int(m.group(1))
cur_results = []
cur_out = []
continue
m = MARK_EXIT.search(line)
if m and cur is not None and int(m.group(1)) == cur:
code = int(m.group(2))
name = dev_cmds[cur][0]
status = classify(code, cur_results, code in TIMEOUT_EXITS)
outcomes[name] = {
'status': status,
'exit': code,
'subtests': {k: v for r_ in cur_results if 'subtest' in r_
for k, v in r_['subtest'].items()},
'tail': b'\n'.join(cur_out[-8:]).decode(errors='replace')
if status in ('crash', 'timeout', 'fail', 'notrun') else '',
}
cur = None
continue
if cur is not None:
cur_out.append(line)
m = RESULT_LINE.search(line)
if m:
try:
cur_results.append(json.loads(m.group(1)))
except json.JSONDecodeError:
pass
# tests whose markers never appeared (adb drop, device reboot)
for j, (name, _) in enumerate(chunk):
outcomes.setdefault(dev_cmds[idx_base + j][0], {
'status': 'missing', 'exit': -1, 'subtests': {}, 'tail': ''})
idx_base += len(chunk)
done = idx_base
print(f' chunk {ci + 1}/{len(chunks)} done ({done}/{len(dev_cmds)}, '
f'{time.time() - t0:.0f}s elapsed)')
raw_log.close()
print('[4/4] writing results')
counts = {}
for name, o in outcomes.items():
counts[o['status']] = counts.get(o['status'], 0) + 1
result_doc = {
'backend': args.backend,
'use_angle': args.use_angle,
'device_dir': dev,
'timeout': args.timeout,
'elapsed_sec': round(time.time() - t0, 1),
'totals': counts,
'tests': outcomes,
}
(args.out / 'results.json').write_text(json.dumps(result_doc, indent=1, sort_keys=True))
lines = [f"backend: {args.backend} tests: {len(outcomes)} elapsed: {result_doc['elapsed_sec']}s"]
lines.append('totals: ' + ', '.join(f'{k}={v}' for k, v in sorted(counts.items())))
for status in ('crash', 'timeout', 'fail', 'missing', 'notrun', 'warn'):
bad = sorted(n for n, o in outcomes.items() if o['status'] == status)
if bad:
lines.append(f'\n== {status} ({len(bad)}):')
lines.extend(f' {n}' for n in bad)
(args.out / 'summary.txt').write_text('\n'.join(lines) + '\n')
print('\n'.join(lines[:2]))
print(f"results: {args.out / 'results.json'}")
return 0
if __name__ == '__main__':
sys.exit(main())
@@ -0,0 +1,112 @@
---
name: piglit-on-android
description: Run piglit desktop-GL tests on a connected Android device with MobileGL as the OpenGL implementation - cross-build waffle (patched) and piglit for aarch64, push to /data/local/tmp, execute over adb against the DirectGLES (system driver or ANGLE) and DirectVulkan backends, and produce pass/fail/crash summaries. Use when validating MobileGL's OpenGL 3.3 core conformance on-device or bisecting a piglit regression.
---
# piglit on Android against MobileGL
## Variables
```sh
export REPO="$PWD" # MobileGL checkout
export WORK="$HOME/piglit-android" # piglit/waffle workdir (outside the repo)
export NDK="$HOME/Library/Android/sdk/ndk/27.3.13750724"
export TOOLS="$REPO/tools/piglit-android"
export DEVICE_DIR="/data/local/tmp/piglit-mgl"
```
## Architecture (read first)
- MobileGL exports the full `egl*`/`gl*` API with real symbol names from
`libMobileGL.so`; waffle's `surfaceless_egl` platform is pointed at it via
`WAFFLE_EGL_LIBRARY=libMobileGL.so` and `WAFFLE_GL_LIBRARY=libMobileGL.so`.
- **Never ship MobileGL under the name `libEGL.so`**: the DirectGLES backend
dlopens the system driver by the bare soname `libEGL.so` and would load
itself recursively.
- `WAFFLE_FORCE_GL_CONTEXT_VERSION=33core` (patched waffle) upgrades piglit's
low compat context requests (`supports_gl_compat_version=10`) to GL 3.3
core; without it those tests see a raw backend version string and skip.
- DirectVulkan cannot use MobileGL's EGL-pbuffer path on real devices (Android
ICDs lack `VK_EXT_headless_surface`), so the patched waffle creates windows
from an `AImageReader` ANativeWindow: `WAFFLE_ANDROID_WINDOW=imagereader`.
The runner sets this automatically for `--backend DirectVulkan`.
- The piglit patch also fixes `piglit_dispatch_default_init` to always install
the waffle resolvers; upstream silently bound gl* through the SYSTEM
libEGL's `eglGetProcAddress`, running every test on the raw GLES driver.
- MobileGL's lifecycle is owned by the EGL layer (lazy init on first EGL call,
full teardown on the last `eglTerminate`), so piglit processes start and
exit cleanly with no static ctor/dtor involvement.
## Steps
1. **Clone + patch** piglit and waffle (shallow clones are fine; never pull
git-LFS):
```sh
mkdir -p "$WORK" && cd "$WORK"
git clone --depth 1 https://gitlab.freedesktop.org/mesa/piglit.git
git clone --depth 1 https://gitlab.freedesktop.org/mesa/waffle.git
git -C waffle apply "$TOOLS/patches/waffle-mobilegl-android.patch"
git -C piglit apply "$TOOLS/patches/piglit-mobilegl-android.patch"
python3 -m venv venv && ./venv/bin/pip install mako numpy packaging
```
2. **Build libMobileGL.so** for arm64 (plain CMake + NDK; gradle not needed)
and strip it. See `$TOOLS/README.md` for exact invocations.
3. **Cross-build waffle** with meson (`surfaceless_egl` enabled, everything
else disabled) using `$TOOLS/cross-example/android-arm64.ini` and the stub
`egl.pc` (Cflags → `$REPO/include` for EGL 1.5 headers). `meson install` to
`$WORK/prefix` so piglit's pkg-config finds `waffle-1.pc`.
4. **Cross-build piglit** with the NDK toolchain file; the full CMake flag set
is in `$TOOLS/README.md`. All GL test binaries land in
`piglit/build-android/bin` (~1600), shared utils in `lib/`.
5. **Smoke test** with wflinfo on both backends before any long run:
```sh
adb shell "cd $DEVICE_DIR && env LD_LIBRARY_PATH=$DEVICE_DIR/lib \
WAFFLE_EGL_LIBRARY=libMobileGL.so WAFFLE_GL_LIBRARY=libMobileGL.so \
MOBILEGL_BACKEND_TYPE=DirectVulkan WAFFLE_ANDROID_WINDOW=imagereader \
./wflinfo --platform surfaceless_egl --api gl --version 3.3 --profile core"
```
Expect `3.3.0 MobileGL … Direct (Vulkan) Backend` and exit code 0.
6. **Enumerate tests on the host** with `piglit print-cmd` (profiles: `opengl`,
`shader`, `glslparser`; group separator is `@`). For the GL 3.3 core suite
use the version groups + GLSL groups + the ARB extension groups folded into
3.13.3 core (~15k tests).
7. **Run per backend** with the driver script (serial, chunked, per-test
timeout; DirectGLES first, then DirectVulkan):
```sh
python3 "$TOOLS/run_piglit_android.py" \
--piglit-root "$WORK/piglit" --list gl33-core.list \
--backend DirectGLES \
--mobilegl-lib "$WORK/libMobileGL-stripped.so" \
--waffle-lib "$WORK/waffle/build-android/src/waffle/libwaffle-1.so" \
--out results-gles
```
`--use-angle` switches DirectGLES to packaged ANGLE; default is the system
GLES driver. `--repush` forces re-pushing bin/lib/tests trees (needed after
rebuilds or if the device wiped `/data/local/tmp`).
8. **Compare backends**: `results.json` has per-test status
(`pass/fail/skip/crash/timeout/missing`) and output tails for non-passes;
`summary.txt` lists the bad tests. Diff the two runs' failing sets to
separate frontend issues (fail on both) from backend-specific ones.
## Gotchas
- A test crash after `PIGLIT: {"result": ...}` was printed is classified by
the result line, not the exit code, except that unknown nonzero exits count
as `crash`.
- The device may clear `/data/local/tmp` (vendor cleaners); `--repush` recovers.
- Keep chunks ≤ ~250 tests: one adb connection per chunk bounds the damage of
USB hiccups, and progress prints per chunk.
- MobileGL writes `$DEVICE_DIR/mobilegl.log` (set by the runner); check it and
`adb logcat -b crash` when triaging crashes.
@@ -0,0 +1,4 @@
interface:
display_name: "MobileGL piglit on Android"
short_description: "Run piglit GL tests on-device against MobileGL backends"
default_prompt: "Use $piglit-on-android to run the piglit OpenGL 3.3 core suite on the connected Android device against MobileGL's DirectGLES and DirectVulkan backends and summarize the results."