diff --git a/tools/piglit-android/README.md b/tools/piglit-android/README.md new file mode 100644 index 00000000..d75caee1 --- /dev/null +++ b/tools/piglit-android/README.md @@ -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.1–3.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. diff --git a/tools/piglit-android/compare_results.py b/tools/piglit-android/compare_results.py new file mode 100644 index 00000000..c94a5fc8 --- /dev/null +++ b/tools/piglit-android/compare_results.py @@ -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() diff --git a/tools/piglit-android/cross-example/android-arm64.ini b/tools/piglit-android/cross-example/android-arm64.ini new file mode 100644 index 00000000..b2b1bede --- /dev/null +++ b/tools/piglit-android/cross-example/android-arm64.ini @@ -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' diff --git a/tools/piglit-android/cross-example/egl.pc b/tools/piglit-android/cross-example/egl.pc new file mode 100644 index 00000000..aa6190b8 --- /dev/null +++ b/tools/piglit-android/cross-example/egl.pc @@ -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 diff --git a/tools/piglit-android/patches/piglit-mobilegl-android.patch b/tools/piglit-android/patches/piglit-mobilegl-android.patch new file mode 100644 index 00000000..0399801d --- /dev/null +++ b/tools/piglit-android/patches/piglit-mobilegl-android.patch @@ -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; + } + diff --git a/tools/piglit-android/patches/waffle-mobilegl-android.patch b/tools/piglit-android/patches/waffle-mobilegl-android.patch new file mode 100644 index 00000000..d7e3e8d7 --- /dev/null +++ b/tools/piglit-android/patches/waffle-mobilegl-android.patch @@ -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 ++#include ++#include + + #include + #include +@@ -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 + #include +-#include + + #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 ++#include ++#include ++ ++// 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); diff --git a/tools/piglit-android/run_piglit_android.py b/tools/piglit-android/run_piglit_android.py new file mode 100644 index 00000000..432dc02d --- /dev/null +++ b/tools/piglit-android/run_piglit_android.py @@ -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} &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()) diff --git a/tools/piglit-android/skills/piglit-on-android/SKILL.md b/tools/piglit-android/skills/piglit-on-android/SKILL.md new file mode 100644 index 00000000..7396d9f2 --- /dev/null +++ b/tools/piglit-android/skills/piglit-on-android/SKILL.md @@ -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.1–3.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. diff --git a/tools/piglit-android/skills/piglit-on-android/agents/openai.yaml b/tools/piglit-android/skills/piglit-on-android/agents/openai.yaml new file mode 100644 index 00000000..6954f95e --- /dev/null +++ b/tools/piglit-android/skills/piglit-on-android/agents/openai.yaml @@ -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."