From d2996ba1cfda53bcd42e9981b05d18f79f8ec39c Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 26 Jul 2026 08:05:02 -0400 Subject: [PATCH] [Test] (CTS): run VK-GL-CTS KHR-GL33 against MobileGL on Android via a standalone glcts binary --- tools/cts/README.md | 85 ++++ tools/cts/platform/tcuMobileGLPlatform.cpp | 473 ++++++++++++++++++ tools/cts/platform/tcuMobileGLPlatform.hpp | 33 ++ tools/cts/probe/.gitignore | 2 + tools/cts/probe/mgprobe.c | 318 ++++++++++++ tools/cts/scripts/qpa_report.py | 204 ++++++++ tools/cts/scripts/run_cts.py | 266 ++++++++++ tools/cts/scripts/sync_to_cts.py | 53 ++ tools/cts/skills/README.md | 18 + tools/cts/skills/gl-cts-on-mobilegl/SKILL.md | 192 +++++++ .../gl-cts-on-mobilegl/agents/openai.yaml | 4 + tools/cts/targets/mobilegl.cmake | 36 ++ tools/cts/targets/ndk-modern.cmake | 61 +++ 13 files changed, 1745 insertions(+) create mode 100644 tools/cts/README.md create mode 100644 tools/cts/platform/tcuMobileGLPlatform.cpp create mode 100644 tools/cts/platform/tcuMobileGLPlatform.hpp create mode 100644 tools/cts/probe/.gitignore create mode 100644 tools/cts/probe/mgprobe.c create mode 100644 tools/cts/scripts/qpa_report.py create mode 100644 tools/cts/scripts/run_cts.py create mode 100644 tools/cts/scripts/sync_to_cts.py create mode 100644 tools/cts/skills/README.md create mode 100644 tools/cts/skills/gl-cts-on-mobilegl/SKILL.md create mode 100644 tools/cts/skills/gl-cts-on-mobilegl/agents/openai.yaml create mode 100644 tools/cts/targets/mobilegl.cmake create mode 100644 tools/cts/targets/ndk-modern.cmake diff --git a/tools/cts/README.md b/tools/cts/README.md new file mode 100644 index 00000000..b8d35b08 --- /dev/null +++ b/tools/cts/README.md @@ -0,0 +1,85 @@ +# Running the OpenGL CTS (VK-GL-CTS / KHR-GL33) against MobileGL on Android + +Goal: measure how much of the OpenGL 3.3 core-profile conformance suite MobileGL +passes, separately for each backend (`DirectGLES`, `DirectVulkan`). + +## How MobileGL is reached from a test binary + +MobileGL ships its own EGL implementation alongside its desktop-GL implementation +in a single `libMobileGL.so`. A plain arm64 ELF in `/data/local/tmp` can therefore +drive it with no APK and no Activity: + +1. `setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES"|"DirectVulkan")` **before** the + library is mapped — MobileGL parses its configuration from an ELF constructor. +2. `dlopen("libMobileGL.so")`, then `dlsym` the `egl*` and `gl*` entry points. + MobileGL exports 45 EGL symbols and the desktop GL functions directly; + `eglGetProcAddress` resolves the same set. +3. `eglBindAPI(EGL_OPENGL_API)`, choose a config with `EGL_RENDERABLE_TYPE = + EGL_OPENGL_BIT`, then `eglCreateContext` with + `EGL_CONTEXT_OPENGL_PROFILE_MASK = EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT` and + major/minor `3`/`3`. + +This yields a genuine GL 3.3 core context (`GL_CONTEXT_PROFILE_MASK == 0x1`). + +## Surface type, per backend + +| backend | pbuffer (headless) | window | +|---|---|---| +| `DirectGLES` | works | works | +| `DirectVulkan` | **unusable** | works | + +`DirectVulkan`'s pbuffer path builds a headless `VkSurfaceKHR` and so requires the +`VK_EXT_headless_surface` instance extension, which Adreno's Android driver does +not expose. It fails inside `eglMakeCurrent`, not at surface creation. + +The workaround that keeps everything in a shell process: obtain a real +`ANativeWindow` from **`AImageReader`** (`AImageReader_newWithUsage` + +`AImageReader_getWindow`). It is an ordinary BufferQueue producer, so +`vkCreateAndroidSurfaceKHR` accepts it, and no Activity is involved. Register an +`onImageAvailable` listener that acquires and deletes each image — otherwise the +producer blocks once `maxImages` buffers are in flight and the next swap hangs. + +## Why the suite must render into an FBO + +On a window surface, `DirectVulkan`'s `glReadPixels` from the **default +framebuffer** returns all zeros, with no GL error, both before and after +`eglSwapBuffers`. `DirectGLES` on the identical window is correct, and readback +from a **user FBO is correct on both backends**. + +Verified on two SoCs and two drivers, so this is MobileGL's behaviour rather than +a driver quirk: + +| device | GPU | driver | default-FB | user FBO | +|---|---|---|---|---| +| Xiaomi 24129PN74C | Adreno 830 | Vulkan 1.3.284 / 512.800.46 | zeros | ok | +| Lenovo TB321FU | Adreno 750 | Vulkan 1.3.128 / 512.762.28 | zeros | ok | + +dEQP verifies nearly every case through `glReadPixels`, so running it against the +default framebuffer would score `DirectVulkan` near zero for a reason unrelated to +conformance. The runs therefore use `--deqp-surface-type=fbo`, uniformly for both +backends so the two numbers stay comparable. + +## Other constraints the harness must respect + +- `eglMakeCurrent` requires **draw == read** and rejects `EGL_NO_SURFACE` with + `EGL_BAD_MATCH`. dEQP's `surfaceless` platform is therefore unusable, which is + why this port supplies its own `tcu::Platform`. +- MobileGL aborts during static teardown (`FORTIFY: pthread_mutex_lock called on a + destroyed mutex`) *after* all work completes. Flush and `_exit()` so the exit + code and the `.qpa` log survive. + +## Contents + + probe/mgprobe.c preflight gate: one backend x one surface type, checks + context version/profile and both readback paths + scripts/qpa_report.py .qpa -> pass rate, status histogram, worst groups + +### Preflight + + aarch64-linux-android26-clang -O1 -o mgprobe mgprobe.c -ldl -llog -landroid -lmediandk + adb push mgprobe libMobileGL.so /data/local/tmp/mgcts/ + adb shell 'cd /data/local/tmp/mgcts && LD_LIBRARY_PATH=. ./mgprobe \ + --backend DirectVulkan --surface imagereader --lib ./libMobileGL.so' + +Exit status is 0 when a 3.3 core context came up and FBO readback is correct. +Default-framebuffer readback is reported but deliberately does not gate. diff --git a/tools/cts/platform/tcuMobileGLPlatform.cpp b/tools/cts/platform/tcuMobileGLPlatform.cpp new file mode 100644 index 00000000..190c1a8f --- /dev/null +++ b/tools/cts/platform/tcuMobileGLPlatform.cpp @@ -0,0 +1,473 @@ +/*------------------------------------------------------------------------- + * dEQP platform port for MobileGL on Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *//*! + * \file + * \brief MobileGL platform. + * + * Modelled on the surfaceless platform, but adapted to MobileGL, which ships + * its own EGL implementation inside libMobileGL.so: + * + * - Every EGL call goes through the dynamically loaded library. The + * surfaceless port mixes wrapper calls with globally linked egl* symbols; + * doing that here would silently reach Android's system EGL instead. + * - Desktop-GL configs are selected with EGL_OPENGL_BIT. The surfaceless port + * always asks for an ES bit, which cannot satisfy a GL 3.3 core context. + * - A real surface is always created. MobileGL rejects EGL_NO_SURFACE with + * EGL_BAD_MATCH, and --deqp-surface-type=fbo asks the platform for + * SURFACETYPE_DONT_CARE, so "no surface" is not an option. + * - Window surfaces are backed by an AImageReader rather than an Activity, + * which is what lets the suite run as a plain adb-shell binary. DirectVulkan + * needs this: its pbuffer path requires VK_EXT_headless_surface, which + * Adreno's Android driver does not expose. + * + * Environment: + * MOBILEGL_CTS_LIB path/soname of the MobileGL library (default libMobileGL.so) + * MOBILEGL_CTS_SURFACE "window" (default) or "pbuffer" + * MOBILEGL_BACKEND_TYPE read by MobileGL itself; set it before launching + *//*--------------------------------------------------------------------*/ + +#include "tcuMobileGLPlatform.hpp" + +#include +#include +#include + +#include "deDynamicLibrary.hpp" +#include "egluUtil.hpp" +#include "eglwEnums.hpp" +#include "eglwLibrary.hpp" +#include "gluPlatform.hpp" +#include "gluRenderConfig.hpp" +#include "gluRenderContext.hpp" +#include "glwInitFunctions.hpp" +#include "tcuCommandLine.hpp" +#include "tcuPixelFormat.hpp" +#include "tcuPlatform.hpp" +#include "tcuRenderTarget.hpp" + +#include +#include +#include + +using std::string; +using std::vector; + +#if !defined(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR) +#define EGL_CONTEXT_FLAGS_KHR 0x30FC +#define EGL_CONTEXT_MAJOR_VERSION_KHR 0x3098 +#define EGL_CONTEXT_MINOR_VERSION_KHR 0x30FB +#define EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR 0x00000002 +#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR 0x00000001 +#define EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR 0x00000001 +#define EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR 0x00000002 +#define EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR 0x30FD +#define EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR 0x00000004 +#endif + +namespace tcu +{ +namespace mobilegl +{ + +static string getLibraryName(void) +{ + const char *env = std::getenv("MOBILEGL_CTS_LIB"); + return (env && env[0]) ? string(env) : string("libMobileGL.so"); +} + +//! Window surfaces default on: they are the only kind DirectVulkan can use. +static bool useWindowSurface(void) +{ + const char *env = std::getenv("MOBILEGL_CTS_SURFACE"); + return !(env && string(env) == "pbuffer"); +} + +/*--------------------------------------------------------------------*//*! + * \brief A real ANativeWindow with no Activity behind it. + * + * AImageReader's window is an ordinary BufferQueue producer, so both + * eglCreateWindowSurface and vkCreateAndroidSurfaceKHR accept it. The image + * listener must drain the queue: without it the producer blocks once maxImages + * buffers are in flight and the next swap deadlocks. + *//*--------------------------------------------------------------------*/ +class ImageReaderWindow +{ +public: + ImageReaderWindow(int width, int height) : m_reader(nullptr), m_window(nullptr) + { + const media_status_t status = + AImageReader_newWithUsage(width, height, AIMAGE_FORMAT_RGBA_8888, + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | + AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT, + kMaxImages, &m_reader); + if (status != AMEDIA_OK || m_reader == nullptr) + throw tcu::ResourceError("AImageReader_newWithUsage() failed"); + + AImageReader_ImageListener listener = {this, onImageAvailable}; + AImageReader_setImageListener(m_reader, &listener); + + if (AImageReader_getWindow(m_reader, &m_window) != AMEDIA_OK || m_window == nullptr) + { + AImageReader_delete(m_reader); + m_reader = nullptr; + throw tcu::ResourceError("AImageReader_getWindow() failed"); + } + ANativeWindow_acquire(m_window); + } + + ~ImageReaderWindow(void) + { + if (m_window != nullptr) + ANativeWindow_release(m_window); + if (m_reader != nullptr) + { + AImageReader_setImageListener(m_reader, nullptr); + AImageReader_delete(m_reader); + } + } + + ANativeWindow *getWindow(void) const + { + return m_window; + } + +private: + static const int kMaxImages = 4; + + static void onImageAvailable(void *, AImageReader *reader) + { + AImage *image = nullptr; + if (AImageReader_acquireNextImage(reader, &image) == AMEDIA_OK && image != nullptr) + AImage_delete(image); + } + + ImageReaderWindow(const ImageReaderWindow &); + ImageReaderWindow &operator=(const ImageReaderWindow &); + + AImageReader *m_reader; + ANativeWindow *m_window; +}; + +class GetProcFuncLoader : public glw::FunctionLoader +{ +public: + GetProcFuncLoader(const eglw::Library &egl) : m_egl(egl) + { + } + + glw::GenericFuncType get(const char *name) const + { + return (glw::GenericFuncType)m_egl.getProcAddress(name); + } + +protected: + const eglw::Library &m_egl; +}; + +class EglRenderContext : public glu::RenderContext +{ +public: + EglRenderContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine, + const glu::RenderContext *sharedContext); + ~EglRenderContext(void); + + glu::ContextType getType(void) const + { + return m_contextType; + } + eglw::EGLContext getEglContext(void) const + { + return m_eglContext; + } + const glw::Functions &getFunctions(void) const + { + return m_glFunctions; + } + const tcu::RenderTarget &getRenderTarget(void) const + { + return m_renderTarget; + } + void postIterate(void); + void makeCurrent(void); + + glw::GenericFuncType getProcAddress(const char *name) const + { + return (glw::GenericFuncType)m_egl.getProcAddress(name); + } + +private: + const eglw::DefaultLibrary m_egl; + const glu::ContextType m_contextType; + eglw::EGLDisplay m_eglDisplay; + eglw::EGLContext m_eglContext; + eglw::EGLSurface m_eglSurface; + ImageReaderWindow *m_window; + glw::Functions m_glFunctions; + tcu::RenderTarget m_renderTarget; + eglw::EGLContext m_sharedEglContext; +}; + +class ContextFactory : public glu::ContextFactory +{ +public: + ContextFactory(void) : glu::ContextFactory("default", "MobileGL EGL context") + { + } + + glu::RenderContext *createContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine, + const glu::RenderContext *sharedContext) const + { + return new EglRenderContext(config, cmdLine, sharedContext); + } +}; + +class Platform : public tcu::Platform, public glu::Platform +{ +public: + Platform(void) + { + m_contextFactoryRegistry.registerFactory(new ContextFactory()); + } + + const glu::Platform &getGLPlatform(void) const + { + return *this; + } +}; + +EglRenderContext::EglRenderContext(const glu::RenderConfig &config, const tcu::CommandLine &cmdLine, + const glu::RenderContext *sharedContext) + : m_egl(getLibraryName().c_str()) + , m_contextType(config.type) + , m_eglDisplay(EGL_NO_DISPLAY) + , m_eglContext(EGL_NO_CONTEXT) + , m_eglSurface(EGL_NO_SURFACE) + , m_window(nullptr) + , m_renderTarget(config.width, config.height, + tcu::PixelFormat(config.redBits, config.greenBits, config.blueBits, config.alphaBits), + config.depthBits, config.stencilBits, config.numSamples) + , m_sharedEglContext(EGL_NO_CONTEXT) +{ + DE_UNREF(cmdLine); + + const glu::ContextType &contextType = config.type; + const bool isES = glu::isContextTypeES(contextType); + eglw::EGLint eglMajorVersion = 0; + eglw::EGLint eglMinorVersion = 0; + + m_eglDisplay = m_egl.getDisplay(EGL_DEFAULT_DISPLAY); + EGLU_CHECK_MSG(m_egl, "eglGetDisplay()"); + if (m_eglDisplay == EGL_NO_DISPLAY) + throw tcu::ResourceError("eglGetDisplay() failed"); + + EGLU_CHECK_CALL(m_egl, initialize(m_eglDisplay, &eglMajorVersion, &eglMinorVersion)); + + // MobileGL cannot make a context current without a surface, so + // SURFACETYPE_DONT_CARE (which is what --deqp-surface-type=fbo requests) + // still gets a real one. + bool wantWindow = false; + switch (config.surfaceType) + { + case glu::RenderConfig::SURFACETYPE_WINDOW: + wantWindow = true; + break; + case glu::RenderConfig::SURFACETYPE_OFFSCREEN_NATIVE: + case glu::RenderConfig::SURFACETYPE_OFFSCREEN_GENERIC: + wantWindow = false; + break; + case glu::RenderConfig::SURFACETYPE_DONT_CARE: + wantWindow = useWindowSurface(); + break; + default: + TCU_CHECK_INTERNAL(false); + } + + const int width = (config.width == glu::RenderConfig::DONT_CARE) ? 256 : config.width; + const int height = (config.height == glu::RenderConfig::DONT_CARE) ? 256 : config.height; + + vector cfgAttribs; + cfgAttribs.push_back(EGL_RENDERABLE_TYPE); + if (isES) + { + switch (contextType.getMajorVersion()) + { + case 3: + cfgAttribs.push_back(EGL_OPENGL_ES3_BIT); + break; + case 2: + cfgAttribs.push_back(EGL_OPENGL_ES2_BIT); + break; + default: + cfgAttribs.push_back(EGL_OPENGL_ES_BIT); + } + } + else + { + // Desktop GL, which is the whole point of this port. + cfgAttribs.push_back(EGL_OPENGL_BIT); + } + + cfgAttribs.push_back(EGL_SURFACE_TYPE); + cfgAttribs.push_back(wantWindow ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT); + + static const struct + { + eglw::EGLint attrib; + int glu::RenderConfig::*field; + } s_sizeAttribs[] = { + {EGL_RED_SIZE, &glu::RenderConfig::redBits}, {EGL_GREEN_SIZE, &glu::RenderConfig::greenBits}, + {EGL_BLUE_SIZE, &glu::RenderConfig::blueBits}, {EGL_ALPHA_SIZE, &glu::RenderConfig::alphaBits}, + {EGL_DEPTH_SIZE, &glu::RenderConfig::depthBits}, {EGL_STENCIL_SIZE, &glu::RenderConfig::stencilBits}, + {EGL_SAMPLES, &glu::RenderConfig::numSamples}, + }; + for (size_t ndx = 0; ndx < DE_LENGTH_OF_ARRAY(s_sizeAttribs); ndx++) + { + const int value = config.*(s_sizeAttribs[ndx].field); + if (value != glu::RenderConfig::DONT_CARE) + { + cfgAttribs.push_back(s_sizeAttribs[ndx].attrib); + cfgAttribs.push_back(value); + } + } + cfgAttribs.push_back(EGL_NONE); + + eglw::EGLConfig eglConfig = nullptr; + eglw::EGLint numConfigs = 0; + EGLU_CHECK_CALL(m_egl, chooseConfig(m_eglDisplay, &cfgAttribs[0], &eglConfig, 1, &numConfigs)); + if (numConfigs < 1) + throw tcu::NotSupportedError("No matching EGL config for the requested context"); + + if (wantWindow) + { + m_window = new ImageReaderWindow(width, height); + + eglw::EGLint visualId = 0; + if (m_egl.getConfigAttrib(m_eglDisplay, eglConfig, EGL_NATIVE_VISUAL_ID, &visualId) && visualId != 0) + ANativeWindow_setBuffersGeometry(m_window->getWindow(), width, height, visualId); + + m_eglSurface = m_egl.createWindowSurface(m_eglDisplay, eglConfig, + (eglw::EGLNativeWindowType)m_window->getWindow(), nullptr); + EGLU_CHECK_MSG(m_egl, "eglCreateWindowSurface()"); + } + else + { + const eglw::EGLint surfaceAttribs[] = {EGL_WIDTH, width, EGL_HEIGHT, height, EGL_NONE}; + m_eglSurface = m_egl.createPbufferSurface(m_eglDisplay, eglConfig, surfaceAttribs); + EGLU_CHECK_MSG(m_egl, "eglCreatePbufferSurface()"); + } + + if (m_eglSurface == EGL_NO_SURFACE) + throw tcu::ResourceError("Failed to create EGL surface"); + + vector ctxAttribs; + ctxAttribs.push_back(EGL_CONTEXT_MAJOR_VERSION_KHR); + ctxAttribs.push_back(contextType.getMajorVersion()); + ctxAttribs.push_back(EGL_CONTEXT_MINOR_VERSION_KHR); + ctxAttribs.push_back(contextType.getMinorVersion()); + + switch (contextType.getProfile()) + { + case glu::PROFILE_ES: + EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_ES_API)); + break; + case glu::PROFILE_CORE: + EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_API)); + ctxAttribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR); + ctxAttribs.push_back(EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR); + break; + case glu::PROFILE_COMPATIBILITY: + EGLU_CHECK_CALL(m_egl, bindAPI(EGL_OPENGL_API)); + ctxAttribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR); + ctxAttribs.push_back(EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT_KHR); + break; + default: + TCU_CHECK_INTERNAL(false); + } + + eglw::EGLint flags = 0; + if ((contextType.getFlags() & glu::CONTEXT_DEBUG) != 0) + flags |= EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR; + if ((contextType.getFlags() & glu::CONTEXT_ROBUST) != 0) + flags |= EGL_CONTEXT_OPENGL_ROBUST_ACCESS_BIT_KHR; + if ((contextType.getFlags() & glu::CONTEXT_FORWARD_COMPATIBLE) != 0) + flags |= EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE_BIT_KHR; + if (flags != 0) + { + ctxAttribs.push_back(EGL_CONTEXT_FLAGS_KHR); + ctxAttribs.push_back(flags); + } + ctxAttribs.push_back(EGL_NONE); + + const EglRenderContext *sharedEglRenderContext = dynamic_cast(sharedContext); + m_sharedEglContext = sharedEglRenderContext ? sharedEglRenderContext->getEglContext() : EGL_NO_CONTEXT; + + m_eglContext = m_egl.createContext(m_eglDisplay, eglConfig, m_sharedEglContext, &ctxAttribs[0]); + EGLU_CHECK_MSG(m_egl, "eglCreateContext()"); + if (!m_eglContext) + throw tcu::ResourceError("eglCreateContext() failed"); + + // MobileGL requires draw == read. + EGLU_CHECK_CALL(m_egl, makeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext)); + + // MobileGL advertises EGL 1.5, so eglGetProcAddress resolves core entry + // points too; there is no separate GL library to dlopen. + GetProcFuncLoader funcLoader(m_egl); + glu::initCoreFunctions(&m_glFunctions, &funcLoader, contextType.getAPI()); + glu::initExtensionFunctions(&m_glFunctions, &funcLoader, contextType.getAPI()); +} + +EglRenderContext::~EglRenderContext(void) +{ + try + { + if (m_eglDisplay != EGL_NO_DISPLAY) + { + m_egl.makeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + + if (m_eglContext != EGL_NO_CONTEXT) + m_egl.destroyContext(m_eglDisplay, m_eglContext); + + if (m_eglSurface != EGL_NO_SURFACE) + m_egl.destroySurface(m_eglDisplay, m_eglSurface); + + if (m_sharedEglContext == EGL_NO_CONTEXT) + m_egl.terminate(m_eglDisplay); + } + } + catch (...) + { + } + + delete m_window; +} + +void EglRenderContext::makeCurrent(void) +{ + EGLU_CHECK_CALL(m_egl, makeCurrent(m_eglDisplay, m_eglSurface, m_eglSurface, m_eglContext)); +} + +void EglRenderContext::postIterate(void) +{ + m_glFunctions.finish(); +} + +} // namespace mobilegl +} // namespace tcu + +tcu::Platform *createPlatform(void) +{ + return new tcu::mobilegl::Platform(); +} diff --git a/tools/cts/platform/tcuMobileGLPlatform.hpp b/tools/cts/platform/tcuMobileGLPlatform.hpp new file mode 100644 index 00000000..722fbe39 --- /dev/null +++ b/tools/cts/platform/tcuMobileGLPlatform.hpp @@ -0,0 +1,33 @@ +#ifndef _TCUMOBILEGLPLATFORM_HPP +#define _TCUMOBILEGLPLATFORM_HPP +/*------------------------------------------------------------------------- + * dEQP platform port for MobileGL on Android + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + *//*! + * \file + * \brief MobileGL platform - drives libMobileGL.so's own EGL from a bare + * Android process, with no Activity and no system EGL involved. + *//*--------------------------------------------------------------------*/ + +#include "tcuDefs.hpp" + +namespace tcu +{ +class Platform; +} + +tcu::Platform *createPlatform(void); + +#endif // _TCUMOBILEGLPLATFORM_HPP diff --git a/tools/cts/probe/.gitignore b/tools/cts/probe/.gitignore new file mode 100644 index 00000000..b5eab991 --- /dev/null +++ b/tools/cts/probe/.gitignore @@ -0,0 +1,2 @@ +mgprobe +*.o diff --git a/tools/cts/probe/mgprobe.c b/tools/cts/probe/mgprobe.c new file mode 100644 index 00000000..c2bbe123 --- /dev/null +++ b/tools/cts/probe/mgprobe.c @@ -0,0 +1,318 @@ +/* mgprobe - preflight gate for running a GL conformance suite against MobileGL + * from a bare adb-shell process (no APK, no Activity). + * + * Verifies, for one backend and one surface type, that MobileGL can hand out a + * GL 3.3 core context and that pixels read back correctly - both from the + * default framebuffer and from a user FBO. Run this before burning hours on a + * CTS run; it catches a broken device/library pairing in about a second. + * + * mgprobe --backend DirectGLES|DirectVulkan --surface pbuffer|imagereader + * [--lib /path/to/libMobileGL.so] + * + * Exit status: 0 if a context came up and FBO readback is correct, non-zero + * otherwise. Default-framebuffer readback is reported but does NOT gate, because + * DirectVulkan is known to return zeros there while FBO readback is sound. + * + * Build (NDK, arm64): + * $NDK/toolchains/llvm/prebuilt//bin/aarch64-linux-android26-clang \ + * -O1 -o mgprobe mgprobe.c -ldl -llog -landroid -lmediandk + */ +#include +#include +#include +#include +#include +#include +#include + +typedef void *EGLDisplay; +typedef void *EGLConfig; +typedef void *EGLSurface; +typedef void *EGLContext; +typedef int EGLint; +typedef unsigned int EGLBoolean; +typedef unsigned int EGLenum; +typedef void *EGLNativeDisplayType; +typedef void *EGLNativeWindowType; + +#define EGL_DEFAULT_DISPLAY ((EGLNativeDisplayType)0) +#define EGL_NO_CONTEXT ((EGLContext)0) +#define EGL_NO_SURFACE ((EGLSurface)0) +#define EGL_NONE 0x3038 +#define EGL_WIDTH 0x3057 +#define EGL_HEIGHT 0x3056 +#define EGL_RENDERABLE_TYPE 0x3040 +#define EGL_SURFACE_TYPE 0x3033 +#define EGL_WINDOW_BIT 0x0004 +#define EGL_PBUFFER_BIT 0x0001 +#define EGL_OPENGL_BIT 0x0008 +#define EGL_OPENGL_API 0x30A2 +#define EGL_RED_SIZE 0x3024 +#define EGL_GREEN_SIZE 0x3023 +#define EGL_BLUE_SIZE 0x3022 +#define EGL_ALPHA_SIZE 0x3021 +#define EGL_DEPTH_SIZE 0x3025 +#define EGL_STENCIL_SIZE 0x3026 +#define EGL_NATIVE_VISUAL_ID 0x302E +#define EGL_CONTEXT_MAJOR_VERSION 0x3098 +#define EGL_CONTEXT_MINOR_VERSION 0x30FB +#define EGL_CONTEXT_OPENGL_PROFILE_MASK 0x30FD +#define EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT 0x00000001 + +#define GL_VENDOR 0x1F00 +#define GL_RENDERER 0x1F01 +#define GL_VERSION 0x1F02 +#define GL_SHADING_LANGUAGE_VERSION 0x8B8C +#define GL_CONTEXT_PROFILE_MASK 0x9126 +#define GL_MAJOR_VERSION 0x821B +#define GL_MINOR_VERSION 0x821C +#define GL_COLOR_BUFFER_BIT 0x00004000 +#define GL_RGBA 0x1908 +#define GL_RGBA8 0x8058 +#define GL_UNSIGNED_BYTE 0x1401 +#define GL_TEXTURE_2D 0x0DE1 +#define GL_FRAMEBUFFER 0x8D40 +#define GL_COLOR_ATTACHMENT0 0x8CE0 +#define GL_FRAMEBUFFER_COMPLETE 0x8CD5 +#define GL_TEXTURE_MIN_FILTER 0x2801 +#define GL_TEXTURE_MAG_FILTER 0x2800 +#define GL_NEAREST 0x2600 + +typedef EGLDisplay (*P_getDisplay)(EGLNativeDisplayType); +typedef EGLBoolean (*P_initialize)(EGLDisplay, EGLint *, EGLint *); +typedef EGLBoolean (*P_bindAPI)(EGLenum); +typedef EGLBoolean (*P_chooseConfig)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *); +typedef EGLBoolean (*P_getConfigAttrib)(EGLDisplay, EGLConfig, EGLint, EGLint *); +typedef EGLSurface (*P_createWindowSurface)(EGLDisplay, EGLConfig, EGLNativeWindowType, const EGLint *); +typedef EGLSurface (*P_createPbufferSurface)(EGLDisplay, EGLConfig, const EGLint *); +typedef EGLContext (*P_createContext)(EGLDisplay, EGLConfig, EGLContext, const EGLint *); +typedef EGLBoolean (*P_makeCurrent)(EGLDisplay, EGLSurface, EGLSurface, EGLContext); +typedef EGLint (*P_getError)(void); + +typedef const unsigned char *(*P_glGetString)(unsigned int); +typedef void (*P_glGetIntegerv)(unsigned int, int *); +typedef void (*P_glClearColor)(float, float, float, float); +typedef void (*P_glClear)(unsigned int); +typedef void (*P_glFinish)(void); +typedef void (*P_glReadPixels)(int, int, int, int, unsigned int, unsigned int, void *); +typedef unsigned int (*P_glGetError)(void); +typedef void (*P_glGenTextures)(int, unsigned int *); +typedef void (*P_glBindTexture)(unsigned int, unsigned int); +typedef void (*P_glTexImage2D)(unsigned int, int, int, int, int, int, unsigned int, unsigned int, const void *); +typedef void (*P_glTexParameteri)(unsigned int, unsigned int, int); +typedef void (*P_glGenFramebuffers)(int, unsigned int *); +typedef void (*P_glBindFramebuffer)(unsigned int, unsigned int); +typedef void (*P_glFramebufferTexture2D)(unsigned int, unsigned int, unsigned int, unsigned int, int); +typedef unsigned int (*P_glCheckFramebufferStatus)(unsigned int); +typedef void (*P_glViewport)(int, int, int, int); + +static void *g_lib; +static void *S(const char *n) { return dlsym(g_lib, n); } + +static void on_image(void *ctx, AImageReader *r) { + (void)ctx; + AImage *img = NULL; + /* Drain the queue, or the producer blocks once maxImages are in flight. */ + if (AImageReader_acquireNextImage(r, &img) == AMEDIA_OK && img) AImage_delete(img); +} + +#define DIM 256 + +static int near8(unsigned got, int want, int tol) { + int d = (int)got - want; + return d <= tol && d >= -tol; +} + +int main(int argc, char **argv) { + const char *backend = "DirectGLES"; + const char *surface = "pbuffer"; + const char *libpath = "libMobileGL.so"; + + for (int i = 1; i < argc; ++i) { + if (!strcmp(argv[i], "--backend") && i + 1 < argc) backend = argv[++i]; + else if (!strcmp(argv[i], "--surface") && i + 1 < argc) surface = argv[++i]; + else if (!strcmp(argv[i], "--lib") && i + 1 < argc) libpath = argv[++i]; + else { + fprintf(stderr, "usage: %s [--backend DirectGLES|DirectVulkan]" + " [--surface pbuffer|imagereader] [--lib path]\n", argv[0]); + return 2; + } + } + setvbuf(stdout, NULL, _IONBF, 0); + + /* MobileGL parses its config from an ELF constructor, so the backend must be + * selected before the library is mapped. */ + setenv("MOBILEGL_BACKEND_TYPE", backend, 1); + printf("mgprobe backend=%s surface=%s lib=%s\n", backend, surface, libpath); + + int useWindow = !strcmp(surface, "imagereader"); + ANativeWindow *win = NULL; + AImageReader *reader = NULL; + if (useWindow) { + if (AImageReader_newWithUsage(DIM, DIM, AIMAGE_FORMAT_RGBA_8888, + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | + AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT, + 4, &reader) != AMEDIA_OK || !reader) { + printf("FAIL AImageReader_newWithUsage\n"); + return 3; + } + AImageReader_ImageListener l = {NULL, on_image}; + AImageReader_setImageListener(reader, &l); + if (AImageReader_getWindow(reader, &win) != AMEDIA_OK || !win) { + printf("FAIL AImageReader_getWindow\n"); + return 3; + } + } + + g_lib = dlopen(libpath, RTLD_NOW | RTLD_LOCAL); + if (!g_lib) { + printf("FAIL dlopen: %s\n", dlerror()); + return 4; + } + + P_getDisplay eglGetDisplay_ = (P_getDisplay)S("eglGetDisplay"); + P_initialize eglInitialize_ = (P_initialize)S("eglInitialize"); + P_bindAPI eglBindAPI_ = (P_bindAPI)S("eglBindAPI"); + P_chooseConfig eglChooseConfig_ = (P_chooseConfig)S("eglChooseConfig"); + P_getConfigAttrib eglGetConfigAttrib_ = (P_getConfigAttrib)S("eglGetConfigAttrib"); + P_createWindowSurface eglCreateWindowSurface_ = (P_createWindowSurface)S("eglCreateWindowSurface"); + P_createPbufferSurface eglCreatePbufferSurface_ = (P_createPbufferSurface)S("eglCreatePbufferSurface"); + P_createContext eglCreateContext_ = (P_createContext)S("eglCreateContext"); + P_makeCurrent eglMakeCurrent_ = (P_makeCurrent)S("eglMakeCurrent"); + P_getError eglGetError_ = (P_getError)S("eglGetError"); + + if (!eglGetDisplay_ || !eglInitialize_ || !eglChooseConfig_ || !eglCreateContext_ || !eglMakeCurrent_) { + printf("FAIL missing core EGL exports\n"); + return 5; + } + + EGLDisplay dpy = eglGetDisplay_(EGL_DEFAULT_DISPLAY); + EGLint vmaj = 0, vmin = 0; + if (!eglInitialize_(dpy, &vmaj, &vmin)) { + printf("FAIL eglInitialize err=0x%x\n", eglGetError_ ? eglGetError_() : 0); + return 6; + } + if (eglBindAPI_ && !eglBindAPI_(EGL_OPENGL_API)) { + printf("FAIL eglBindAPI(EGL_OPENGL_API) err=0x%x\n", eglGetError_ ? eglGetError_() : 0); + return 7; + } + + const EGLint cfgAttribs[] = { + EGL_SURFACE_TYPE, useWindow ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 24, EGL_STENCIL_SIZE, 8, + EGL_NONE}; + EGLConfig cfg = 0; + EGLint ncfg = 0; + if (!eglChooseConfig_(dpy, cfgAttribs, &cfg, 1, &ncfg) || ncfg < 1) { + printf("FAIL eglChooseConfig n=%d err=0x%x\n", ncfg, eglGetError_ ? eglGetError_() : 0); + return 8; + } + + EGLSurface surf; + if (useWindow) { + EGLint vis = 0; + if (eglGetConfigAttrib_ && eglGetConfigAttrib_(dpy, cfg, EGL_NATIVE_VISUAL_ID, &vis) && vis) + ANativeWindow_setBuffersGeometry(win, DIM, DIM, vis); + surf = eglCreateWindowSurface_(dpy, cfg, (EGLNativeWindowType)win, NULL); + } else { + const EGLint sa[] = {EGL_WIDTH, DIM, EGL_HEIGHT, DIM, EGL_NONE}; + surf = eglCreatePbufferSurface_(dpy, cfg, sa); + } + if (surf == EGL_NO_SURFACE) { + printf("FAIL create%sSurface err=0x%x\n", useWindow ? "Window" : "Pbuffer", + eglGetError_ ? eglGetError_() : 0); + return 9; + } + + const EGLint ctxAttribs[] = { + EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, + EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE}; + EGLContext ctx = eglCreateContext_(dpy, cfg, EGL_NO_CONTEXT, ctxAttribs); + if (ctx == EGL_NO_CONTEXT) { + printf("FAIL eglCreateContext(3.3 core) err=0x%x\n", eglGetError_ ? eglGetError_() : 0); + return 10; + } + /* MobileGL requires draw == read and rejects EGL_NO_SURFACE. */ + if (!eglMakeCurrent_(dpy, surf, surf, ctx)) { + printf("FAIL eglMakeCurrent err=0x%x\n", eglGetError_ ? eglGetError_() : 0); + return 11; + } + + P_glGetString glGetString_ = (P_glGetString)S("glGetString"); + P_glGetIntegerv glGetIntegerv_ = (P_glGetIntegerv)S("glGetIntegerv"); + P_glClearColor glClearColor_ = (P_glClearColor)S("glClearColor"); + P_glClear glClear_ = (P_glClear)S("glClear"); + P_glFinish glFinish_ = (P_glFinish)S("glFinish"); + P_glReadPixels glReadPixels_ = (P_glReadPixels)S("glReadPixels"); + P_glGetError glGetError_ = (P_glGetError)S("glGetError"); + P_glGenTextures glGenTextures_ = (P_glGenTextures)S("glGenTextures"); + P_glBindTexture glBindTexture_ = (P_glBindTexture)S("glBindTexture"); + P_glTexImage2D glTexImage2D_ = (P_glTexImage2D)S("glTexImage2D"); + P_glTexParameteri glTexParameteri_ = (P_glTexParameteri)S("glTexParameteri"); + P_glGenFramebuffers glGenFramebuffers_ = (P_glGenFramebuffers)S("glGenFramebuffers"); + P_glBindFramebuffer glBindFramebuffer_ = (P_glBindFramebuffer)S("glBindFramebuffer"); + P_glFramebufferTexture2D glFramebufferTexture2D_ = (P_glFramebufferTexture2D)S("glFramebufferTexture2D"); + P_glCheckFramebufferStatus glCheckFramebufferStatus_ = (P_glCheckFramebufferStatus)S("glCheckFramebufferStatus"); + P_glViewport glViewport_ = (P_glViewport)S("glViewport"); + + int major = -1, minor = -1, profile = -1; + glGetIntegerv_(GL_MAJOR_VERSION, &major); + glGetIntegerv_(GL_MINOR_VERSION, &minor); + glGetIntegerv_(GL_CONTEXT_PROFILE_MASK, &profile); + printf(" GL_VENDOR %s\n", (const char *)glGetString_(GL_VENDOR)); + printf(" GL_RENDERER %s\n", (const char *)glGetString_(GL_RENDERER)); + printf(" GL_VERSION %s\n", (const char *)glGetString_(GL_VERSION)); + printf(" GLSL %s\n", (const char *)glGetString_(GL_SHADING_LANGUAGE_VERSION)); + printf(" version %d.%d profile_mask 0x%x %s\n", major, minor, profile, + (profile & 1) ? "(core)" : "(NOT CORE)"); + + unsigned char px[4]; + + /* Default framebuffer. */ + glClearColor_(0.25f, 0.5f, 0.75f, 1.0f); + glClear_(GL_COLOR_BUFFER_BIT); + if (glFinish_) glFinish_(); + memset(px, 0, sizeof px); + glReadPixels_(DIM / 2, DIM / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px); + int defOk = near8(px[0], 64, 10) && near8(px[1], 128, 10) && near8(px[2], 191, 10); + printf(" default-FB readback (%u,%u,%u,%u) %s\n", px[0], px[1], px[2], px[3], + defOk ? "ok" : "BROKEN"); + + /* User FBO - this is what dEQP uses with --deqp-surface-type=fbo. */ + unsigned int tex = 0, fbo = 0; + glGenTextures_(1, &tex); + glBindTexture_(GL_TEXTURE_2D, tex); + glTexImage2D_(GL_TEXTURE_2D, 0, GL_RGBA8, DIM, DIM, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + glTexParameteri_(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri_(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glGenFramebuffers_(1, &fbo); + glBindFramebuffer_(GL_FRAMEBUFFER, fbo); + glFramebufferTexture2D_(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); + unsigned int fbst = glCheckFramebufferStatus_(GL_FRAMEBUFFER); + int fboOk = 0; + if (fbst == GL_FRAMEBUFFER_COMPLETE) { + glViewport_(0, 0, DIM, DIM); + glClearColor_(0.9f, 0.2f, 0.4f, 1.0f); + glClear_(GL_COLOR_BUFFER_BIT); + if (glFinish_) glFinish_(); + memset(px, 0, sizeof px); + glReadPixels_(DIM / 2, DIM / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px); + fboOk = near8(px[0], 230, 10) && near8(px[1], 51, 10) && near8(px[2], 102, 10); + printf(" user-FBO readback (%u,%u,%u,%u) %s\n", px[0], px[1], px[2], px[3], + fboOk ? "ok" : "BROKEN"); + } else { + printf(" user-FBO incomplete status=0x%x\n", fbst); + } + + unsigned glerr = glGetError_ ? glGetError_() : 0; + int ok = fboOk && (major > 3 || (major == 3 && minor >= 3)) && (profile & 1) && glerr == 0; + printf("%s backend=%s surface=%s default_fb=%s user_fbo=%s glerr=0x%x\n", + ok ? "PASS" : "FAIL", backend, surface, defOk ? "ok" : "broken", + fboOk ? "ok" : "broken", glerr); + + fflush(stdout); + /* MobileGL aborts in static teardown; leave before that runs. */ + _exit(ok ? 0 : 1); +} diff --git a/tools/cts/scripts/qpa_report.py b/tools/cts/scripts/qpa_report.py new file mode 100644 index 00000000..faa39091 --- /dev/null +++ b/tools/cts/scripts/qpa_report.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python +"""Summarise dEQP/glcts .qpa logs into a conformance pass rate. + +Handles the two ways a case can end in a .qpa: a normal +``#beginTestCaseResult``/``#endTestCaseResult`` pair carrying a +```` element, and ``#terminateTestCaseResult ``, +which is what the log contains when the process died partway through a case. +Cases that were started but never terminated (the run was killed) are reported +separately so a truncated chunk is never silently scored as a pass. + +Usage: + python qpa_report.py [ ...] [--json out.json] [--top N] +""" + +import argparse +import json +import os +import re +import sys +from collections import Counter, defaultdict + +# Khronos conformance treats these as non-failures: the test either passed or +# the implementation legitimately does not expose the feature under test. +NON_FAILURE = { + "Pass", + "NotSupported", + "QualityWarning", + "CompatibilityWarning", + "Waiver", +} + +# Statuses that indicate the case did not merely fail but destabilised the run. +HARD = {"Crash", "Timeout", "InternalError", "ResourceError", "DeviceHang"} + +CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)") +CASE_END = re.compile(r"^#endTestCaseResult") +CASE_TERM = re.compile(r"^#terminateTestCaseResult\s+(.*)") +RESULT = re.compile(r' 1 else case + + +def load_sidecar(paths, name): + """Case names run_cts.py recorded in one of its sidecar lists.""" + out = set() + for p in paths: + d = p if os.path.isdir(p) else os.path.dirname(p) + f = os.path.join(d, name) + if os.path.isfile(f): + with open(f, "r", encoding="utf-8") as fh: + out.update(l.strip() for l in fh if l.strip() and not l.strip().startswith("#")) + return out + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("paths", nargs="+") + ap.add_argument("--json", dest="json_out") + ap.add_argument("--top", type=int, default=25) + ap.add_argument("--label", default="") + args = ap.parse_args() + + files = collect(args.paths) + if not files: + print("no .qpa files found", file=sys.stderr) + return 2 + + # Later chunks may re-run a case; last result wins. + results = {} + for f in files: + for case, status in parse_qpa(f): + results[case] = status + + # A case the runner saw take the process down is a Crash, not merely an + # unterminated log entry - but a real result from a later retry wins. + for case in load_sidecar(args.paths, "crashed.txt"): + if results.get(case, "Incomplete") == "Incomplete": + results[case] = "Crash" + # Worse than a crash: these rebooted the device. + for case in load_sidecar(args.paths, "hung.txt"): + if results.get(case, "Incomplete") in ("Incomplete", "Crash"): + results[case] = "DeviceHang" + + # Cases excluded up front, and cases the run never reached, are not results. + # Report them separately so a partial run is never read as a complete one. + skipped = load_sidecar(args.paths, "skipped.txt") + unrun = load_sidecar(args.paths, "unrun.txt") - set(results) + + counts = Counter(results.values()) + total = len(results) + non_fail = sum(counts[s] for s in NON_FAILURE) + strict_pass = counts["Pass"] + failures = total - non_fail + + by_group_fail = defaultdict(int) + by_group_total = defaultdict(int) + for case, status in results.items(): + g = group_of(case) + by_group_total[g] += 1 + if status not in NON_FAILURE: + by_group_fail[g] += 1 + + label = f" [{args.label}]" if args.label else "" + print(f"=== glcts conformance summary{label} ===") + print(f"files parsed : {len(files)}") + print(f"cases with result : {total}") + print() + for status, n in counts.most_common(): + mark = " " if status in NON_FAILURE else " ! " + print(f"{mark}{status:<22} {n:>7} {100.0 * n / total:6.2f}%") + print() + if total: + print(f"conformance pass rate (Pass+NotSupported+warnings) : {100.0 * non_fail / total:6.2f}% ({non_fail}/{total})") + print(f"strict pass rate (Pass only) : {100.0 * strict_pass / total:6.2f}% ({strict_pass}/{total})") + print(f"failures : {failures}") + + if skipped or unrun: + print("\n--- NOT MEASURED (excluded from the rates above) ---") + if skipped: + print(f" quarantined up front : {len(skipped)}") + if unrun: + print(f" never reached : {len(unrun)}") + print(" The rates above cover only cases that produced a result.") + + if failures: + print(f"\n--- worst groups (of {len(by_group_total)}) ---") + worst = sorted(by_group_fail.items(), key=lambda kv: -kv[1])[: args.top] + for g, nf in worst: + nt = by_group_total[g] + print(f" {g:<52} {nf:>6}/{nt:<6} fail ({100.0 * nf / nt:5.1f}%)") + + if args.json_out: + with open(args.json_out, "w", encoding="utf-8") as fh: + json.dump( + { + "label": args.label, + "files": len(files), + "total": total, + "counts": dict(counts), + "non_failure": non_fail, + "strict_pass": strict_pass, + "failures": failures, + "pass_rate": (non_fail / total) if total else 0.0, + "strict_pass_rate": (strict_pass / total) if total else 0.0, + "results": results, + }, + fh, + indent=1, + ) + print(f"\nwrote {args.json_out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/cts/scripts/run_cts.py b/tools/cts/scripts/run_cts.py new file mode 100644 index 00000000..f827f9fb --- /dev/null +++ b/tools/cts/scripts/run_cts.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python +"""Drive a glcts run on a device, resuming across crashes. + +MobileGL crashes on some cases, and glcts takes the whole process down with it. +A single invocation would therefore stop at the first crash and leave most of +the suite unmeasured. This runner re-invokes glcts with only the cases that have +not produced a result yet, records each crashed case as "Crash", and repeats +until the list is exhausted, so one bad case costs one case rather than the run. + +Usage: + python run_cts.py --serial --backend DirectGLES|DirectVulkan \\ + --caselist --outdir [--device-dir /data/local/tmp/mgcts] +""" + +import argparse +import os +import re +import subprocess +import sys +import time + +CASE_START = re.compile(r"^#beginTestCaseResult\s+(\S+)") +CASE_END = re.compile(r"^#endTestCaseResult") +CASE_TERM = re.compile(r"^#terminateTestCaseResult\s+(.*)") + + +def adb(serial, *args, timeout=None): + try: + return subprocess.run(["adb", "-s", serial, *args], capture_output=True, text=True, timeout=timeout) + except subprocess.TimeoutExpired: + return subprocess.CompletedProcess(args, returncode=124, stdout="", stderr="adb timeout") + + +def device_alive(serial, timeout=30): + """True only if the device answers a trivial shell command. + + Distinguishes "glcts crashed" from "the device fell over". Without this a + dead device looks like every remaining case crashing, which silently turns a + broken run into a plausible-looking conformance number. + """ + r = adb(serial, "shell", "echo alive", timeout=timeout) + return r.returncode == 0 and "alive" in (r.stdout or "") + + +def wait_for_device(serial, attempts=20, delay=15): + for i in range(attempts): + if device_alive(serial): + return True + print(f"[run_cts] device {serial} unresponsive, waiting ({i + 1}/{attempts})") + time.sleep(delay) + return False + + +def mem_available_kb(serial): + r = adb(serial, "shell", "grep MemAvailable /proc/meminfo", timeout=30) + m = re.search(r"(\d+)", r.stdout or "") + return int(m.group(1)) if m else None + + +def completed_cases(qpa_path): + """Return (finished_case_names, last_started_case_or_None). + + A case that was started but never closed is the one the process died in. + """ + finished = [] + current = None + if not os.path.exists(qpa_path): + return finished, None + with open(qpa_path, "r", encoding="utf-8", errors="replace") as fh: + for line in fh: + m = CASE_START.match(line) + if m: + current = m.group(1) + continue + if current is not None and (CASE_END.match(line) or CASE_TERM.match(line)): + finished.append(current) + current = None + return finished, current + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--serial", required=True) + ap.add_argument("--backend", required=True, choices=["DirectGLES", "DirectVulkan"]) + ap.add_argument("--caselist", required=True) + ap.add_argument("--outdir", required=True) + ap.add_argument("--device-dir", default="/data/local/tmp/mgcts") + ap.add_argument("--surface", default="fbo", help="--deqp-surface-type value") + ap.add_argument("--max-rounds", type=int, default=4000) + ap.add_argument("--max-empty-streak", type=int, default=8, + help="abort after this many consecutive chunks that produce no log at all") + ap.add_argument("--min-mem-kb", type=int, default=400000, + help="pause when the device drops below this much available memory") + ap.add_argument("--chunk-timeout", type=int, default=900, + help="seconds before giving up on one glcts invocation (a GPU hang never returns)") + ap.add_argument("--skip-file", default=None, + help="file of case names to exclude, e.g. cases known to hang the device") + args = ap.parse_args() + + os.makedirs(args.outdir, exist_ok=True) + + with open(args.caselist, "r", encoding="utf-8") as fh: + remaining = [l.strip() for l in fh if l.strip() and not l.strip().startswith("#")] + + skipped = [] + if args.skip_file and os.path.isfile(args.skip_file): + with open(args.skip_file, "r", encoding="utf-8") as fh: + skip = {l.strip() for l in fh if l.strip() and not l.strip().startswith("#")} + skipped = [c for c in remaining if c in skip] + remaining = [c for c in remaining if c not in skip] + print(f"[run_cts] skipping {len(skipped)} case(s) from {args.skip_file}") + + total = len(remaining) + print(f"[run_cts] {args.backend} on {args.serial}: {total} cases") + + crashed = [] + hung = [] + done = set() + chunk = 0 + started = time.time() + empty_streak = 0 + + if not wait_for_device(args.serial): + print("[run_cts] device not responding before start; aborting", file=sys.stderr) + return 3 + + while remaining and chunk < args.max_rounds: + listfile = os.path.join(args.outdir, "remaining.txt") + with open(listfile, "w", encoding="utf-8", newline="\n") as fh: + fh.write("\n".join(remaining) + "\n") + + # Repeated process launches plus crash tombstones can drive the device + # into memory pressure; give it room rather than pushing it over. + mem = mem_available_kb(args.serial) + if mem is not None and mem < args.min_mem_kb: + print(f"[run_cts] low memory ({mem} kB available); pausing 30 s") + time.sleep(30) + + dev_list = f"{args.device_dir}/remaining.txt" + dev_qpa = f"{args.device_dir}/chunk.qpa" + push = adb(args.serial, "push", listfile, dev_list, timeout=120) + if push.returncode != 0: + print(f"[run_cts] push failed ({push.stderr.strip()}); treating as device trouble", + file=sys.stderr) + if not wait_for_device(args.serial): + print("[run_cts] ABORTING: device unreachable.", file=sys.stderr) + break + continue + adb(args.serial, "shell", f"rm -f {dev_qpa}", timeout=60) + + cmd = ( + f"cd {args.device_dir} && " + f"MOBILEGL_BACKEND_TYPE={args.backend} LD_LIBRARY_PATH=. " + f"./glcts --deqp-caselist-file={dev_list} " + f"--deqp-surface-type={args.surface} " + f"--deqp-terminate-on-device-lost=disable " + f"--deqp-log-images=disable --deqp-log-shader-sources=disable " + f"--deqp-log-filename={dev_qpa} > /dev/null 2>&1; echo RC=$?" + ) + run = adb(args.serial, "shell", cmd, timeout=args.chunk_timeout) + if run.returncode == 124: + print(f"[run_cts] chunk {chunk:04d} timed out after {args.chunk_timeout}s " + f"(likely a GPU hang)", file=sys.stderr) + + # Some cases hang the GPU hard enough to reboot the device. The log on + # /data/local/tmp survives that, so wait for the device to come back and + # pull it anyway rather than losing the whole chunk. + rebooted = False + if not device_alive(args.serial, timeout=30): + print(f"[run_cts] device went away during chunk {chunk:04d}; waiting for it", + file=sys.stderr) + if not wait_for_device(args.serial, attempts=40, delay=15): + print("[run_cts] ABORTING: device never came back. Results are incomplete; " + "do NOT treat the remaining cases as failures.", file=sys.stderr) + break + rebooted = True + print("[run_cts] device is back") + + local_qpa = os.path.join(args.outdir, f"chunk{chunk:04d}.qpa") + pull = adb(args.serial, "pull", dev_qpa, local_qpa, timeout=300) + if pull.returncode != 0 and rebooted: + time.sleep(10) + adb(args.serial, "pull", dev_qpa, local_qpa, timeout=300) + + finished, in_flight = completed_cases(local_qpa) + for c in finished: + done.add(c) + + progressed = len(finished) + if progressed > 0: + empty_streak = 0 + if in_flight is not None: + # The case that was open when the process (or the device) died. + if rebooted: + # It took the whole device down: quarantine it, or the next + # invocation walks straight back into it. + print(f"[run_cts] DEVICE HANG in {in_flight} - quarantining it") + hung.append(in_flight) + else: + crashed.append(in_flight) + done.add(in_flight) + progressed += 1 + elif progressed == 0: + # Nothing at all came back. Either the first remaining case takes + # the process down before the log is flushed, or the device died. + # Those look identical from here, so confirm the device is alive + # before blaming the test. + if not device_alive(args.serial): + print(f"[run_cts] device went away during chunk {chunk:04d}", file=sys.stderr) + if not wait_for_device(args.serial): + print("[run_cts] ABORTING: device never came back. Results are " + "incomplete; do NOT treat the remaining cases as crashes.", file=sys.stderr) + break + print("[run_cts] device recovered; retrying the same chunk") + continue + + empty_streak += 1 + if empty_streak >= args.max_empty_streak: + print(f"[run_cts] ABORTING: {empty_streak} consecutive chunks produced no output " + f"while the device stayed reachable. Something systemic is wrong; refusing " + f"to label the rest of the suite as crashes.", file=sys.stderr) + break + + victim = remaining[0] + print(f"[run_cts] no output at all; recording {victim} as Crash") + crashed.append(victim) + done.add(victim) + progressed = 1 + + remaining = [c for c in remaining if c not in done] + elapsed = time.time() - started + print( + f"[run_cts] chunk {chunk:04d}: +{progressed} (done {len(done)}/{total}, " + f"crashes {len(crashed)}, {elapsed / 60:.1f} min)" + ) + chunk += 1 + + with open(os.path.join(args.outdir, "crashed.txt"), "w", encoding="utf-8", newline="\n") as fh: + fh.write("\n".join(crashed) + ("\n" if crashed else "")) + + # Cases that rebooted the device. Feed this back in via --skip-file to avoid + # paying for the same reboot on the next run. + with open(os.path.join(args.outdir, "hung.txt"), "w", encoding="utf-8", newline="\n") as fh: + fh.write("\n".join(hung) + ("\n" if hung else "")) + if hung: + print(f"[run_cts] {len(hung)} case(s) hung the device (see hung.txt):") + for c in hung: + print(f" {c}") + + # Anything still in `remaining` was never measured. Record it so the report + # cannot quietly present a partial run as a complete one. + with open(os.path.join(args.outdir, "unrun.txt"), "w", encoding="utf-8", newline="\n") as fh: + fh.write("\n".join(remaining) + ("\n" if remaining else "")) + if skipped: + with open(os.path.join(args.outdir, "skipped.txt"), "w", encoding="utf-8", newline="\n") as fh: + fh.write("\n".join(skipped) + "\n") + + if remaining: + print(f"[run_cts] WARNING: {len(remaining)} cases were never run (see unrun.txt)", file=sys.stderr) + print(f"[run_cts] finished: {len(done)}/{total} cases, {len(crashed)} crashes, {chunk} invocations") + print(f"[run_cts] qpa chunks in {args.outdir}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/cts/scripts/sync_to_cts.py b/tools/cts/scripts/sync_to_cts.py new file mode 100644 index 00000000..de7d5db4 --- /dev/null +++ b/tools/cts/scripts/sync_to_cts.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +"""Copy the MobileGL dEQP platform port into a VK-GL-CTS checkout. + +The port is version-controlled here, in the MobileGL repo, so it survives a +throwaway CTS clone. This drops it into the places VK-GL-CTS expects: + + framework/platform/mobilegl/ <- platform sources + targets/mobilegl/mobilegl.cmake <- target definition (-DDEQP_TARGET=mobilegl) + +Usage: + python sync_to_cts.py +""" + +import os +import shutil +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +CTS_TOOLS = os.path.dirname(HERE) + +COPIES = [ + (os.path.join(CTS_TOOLS, "platform"), "framework/platform/mobilegl", None), + (os.path.join(CTS_TOOLS, "targets"), "targets/mobilegl", ["mobilegl.cmake", "ndk-modern.cmake"]), +] + + +def main(): + if len(sys.argv) != 2: + print(__doc__) + return 2 + cts = sys.argv[1] + if not os.path.isfile(os.path.join(cts, "CMakeLists.txt")): + print(f"error: {cts} does not look like a VK-GL-CTS checkout", file=sys.stderr) + return 1 + + for src, reldst, only in COPIES: + dst = os.path.join(cts, reldst) + os.makedirs(dst, exist_ok=True) + for name in sorted(os.listdir(src)): + if only is not None and name not in only: + continue + s = os.path.join(src, name) + if not os.path.isfile(s): + continue + shutil.copy2(s, os.path.join(dst, name)) + print(f" {reldst}/{name}") + + print("\nsynced. configure with -DDEQP_TARGET=mobilegl") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/cts/skills/README.md b/tools/cts/skills/README.md new file mode 100644 index 00000000..6b5831af --- /dev/null +++ b/tools/cts/skills/README.md @@ -0,0 +1,18 @@ +# MobileGL conformance-suite skills + +Task-focused skills for running Khronos conformance suites against MobileGL. +Each skill is a self-contained package, matching the layout used by +`tools/trace_replay/skills/`: + +- `SKILL.md` — the skill (frontmatter `name` + `description`, then the body). The + directory name equals the frontmatter `name`. +- `agents/openai.yaml` — OpenAI agent descriptor (`display_name`, + `short_description`, `default_prompt`). +- `scripts/` and/or `references/` — bundled tooling and supporting docs, when the + skill has them. + +## Skills + +| Skill | What it does | +| --- | --- | +| [gl-cts-on-mobilegl](gl-cts-on-mobilegl/SKILL.md) | Build VK-GL-CTS `glcts` as a standalone Android arm64 binary against MobileGL's own EGL, run KHR-GL33, and report a per-backend OpenGL 3.3 core conformance rate. | diff --git a/tools/cts/skills/gl-cts-on-mobilegl/SKILL.md b/tools/cts/skills/gl-cts-on-mobilegl/SKILL.md new file mode 100644 index 00000000..3a40b805 --- /dev/null +++ b/tools/cts/skills/gl-cts-on-mobilegl/SKILL.md @@ -0,0 +1,192 @@ +--- +name: gl-cts-on-mobilegl +description: Run the Khronos OpenGL CTS (VK-GL-CTS glcts, KHR-GL33) against MobileGL on an Android device and compute a per-backend conformance rate. Use when measuring OpenGL 3.3 core conformance for DirectGLES or DirectVulkan, building glcts for Android arm64, porting a dEQP tcu::Platform onto MobileGL, or triaging CTS failures, crashes, and cases that hang the device. +--- + +# OpenGL CTS on MobileGL (Android) + +## Overview + +`glcts` from VK-GL-CTS is built as a **standalone arm64 executable** and run from +`adb shell`. It reaches OpenGL only through `libMobileGL.so`, which supplies both +EGL and desktop GL, so a result is unambiguously MobileGL's and never the system +GL stack's. No APK and no Activity are involved. + +The port lives in this repository under `MobileGL/tools/cts/` and is copied into +a VK-GL-CTS checkout by `scripts/sync_to_cts.py`, so it survives a throwaway CTS +clone. + +Set up paths first: + +```sh +export MG= # do builds in a worktree, not the shared tree +export CTS= +export NDK="$ANDROID_HOME/ndk/27.3.13750724" +export SERIAL= +``` + +## Prerequisites + +- Android NDK r27 (the repo builds MobileGL with 27.3.13750724), CMake, Ninja, Python 3. +- A rooted-or-not Android device with `adb`; ~600 MB free under `/data/local/tmp`. +- **A device you can physically power-cycle.** Some cases hang the GPU hard + enough to reboot it — see "Cases that take the device down". +- On Windows, invoke `python`, not `python3`: the latter resolves to the + Microsoft Store alias stub and exits 49. + +## Step 1 — build libMobileGL.so + +Build in a git worktree (other agents share the main tree). A fresh worktree is +missing glslang's bundled SPIR-V Tools, which is a hard configure blocker +because `ENABLE_OPT` is forced on: + +```sh +cp -r /3rdparty/glslang/External/* "$MG/3rdparty/glslang/External/" +./gradlew -p "$MG/android-plugin" :app:assembleTraceRelease +``` + +The stripped library lands in +`android-plugin/app/build/intermediates/stripped_native_libs/traceRelease/.../arm64-v8a/libMobileGL.so`. + +## Step 2 — get VK-GL-CTS and its externals + +Use a **release tag**, not `main`, so the mustpass list — and therefore the +reported rate — is citable: + +```sh +git -C "$CTS" checkout opengl-cts-4.6.8.1 +cd "$CTS" && python external/fetch_sources.py +``` + +## Step 3 — build glcts for Android arm64 + +```sh +python "$MG/tools/cts/scripts/sync_to_cts.py" "$CTS" + +cmake -S "$CTS" -B build-cts-a64 -G Ninja \ + -DDEQP_TARGET=mobilegl -DDEQP_TARGET_TOOLCHAIN=ndk-modern \ + -DANDROID_NDK_PATH="$NDK" -DDE_ANDROID_API=26 -DANDROID_ABI=arm64-v8a \ + -DCMAKE_BUILD_TYPE=Release +ninja -C build-cts-a64 glcts +"$NDK"/toolchains/llvm/prebuilt/*/bin/llvm-strip build-cts-a64/external/openglcts/modules/glcts +``` + +Confirm the configure output says `DE_OS = DE_OS_ANDROID`, `DE_CPU = +DE_CPU_ARM_64` and `DEQP_ANDROID_BUILD = EXE`. Two things make that work and +both are easy to get wrong: + +- `DEQP_TARGET_TOOLCHAIN=ndk-modern` is required. dEQP includes `Defs.cmake` + *before* the target file, so a target cannot set `DE_OS` itself. Without the + toolchain hook the build mis-detects as `DE_OS_UNIX`/`x86_64` and dies on + `__assert_fail` (bionic has `__assert2`). +- The target sets `DEQP_ANDROID_EXE ON`. Otherwise dEQP builds the modules into + the `libdeqp.so` an APK would load and no `glcts` executable exists. + +`KHR-GL33` needs no ungating — the package registry registers it unconditionally; +only the `dEQP-*` packages are `#if DE_OS != DE_OS_ANDROID`. + +## Step 4 — deploy + +```sh +adb -s $SERIAL shell mkdir -p /data/local/tmp/mgcts +adb -s $SERIAL push build-cts-a64/external/openglcts/modules/glcts /data/local/tmp/mgcts/ +adb -s $SERIAL push build-cts-a64/external/openglcts/modules/gl_cts /data/local/tmp/mgcts/ +adb -s $SERIAL push /data/local/tmp/mgcts/ +adb -s $SERIAL shell chmod 755 /data/local/tmp/mgcts/glcts +``` + +## Step 5 — preflight + +Never start a multi-hour run without this. It proves the device/library pair +yields a 3.3 core context and that FBO readback is correct, in about a second: + +```sh +adb -s $SERIAL shell 'cd /data/local/tmp/mgcts && LD_LIBRARY_PATH=. ./mgprobe \ + --backend DirectVulkan --surface imagereader --lib ./libMobileGL.so' +``` + +Expect `PASS ... user_fbo=ok`. `default_fb=broken` on DirectVulkan is expected +and does not gate — see below. + +## Step 6 — run + +```sh +python "$MG/tools/cts/scripts/run_cts.py" \ + --serial $SERIAL --backend DirectGLES \ + --caselist .../mustpass/gl/khronos_mustpass/main/gl33-main.txt \ + --outdir runs/gles --skip-file runs/skip.txt +``` + +The runner re-invokes `glcts` with only the cases that have no result yet, so a +crash costs one case rather than the run. It distinguishes a crashed *case* from +a dead *device* by checking the device still answers a shell command — without +that check a dead device looks like every remaining case crashing, which yields +a completely bogus but plausible-looking conformance number. On a device reboot +it waits, re-pulls the partial `.qpa` (which survives on `/data/local/tmp`), +records the case that was open as `DeviceHang`, and quarantines it. + +## Step 7 — report + +```sh +python "$MG/tools/cts/scripts/qpa_report.py" runs/gles --label DirectGLES +``` + +Pass rate counts `Pass`, `NotSupported`, `QualityWarning`, `CompatibilityWarning` +and `Waiver` as non-failures, matching how Khronos scores a submission; the +strict rate counts only `Pass`. Quarantined and never-reached cases are reported +separately and excluded from the rates, so a partial run cannot read as a +complete one. + +## Required flags, and why + +| Flag | Why it is not optional | +| --- | --- | +| `--deqp-surface-type=fbo` | On DirectVulkan, `glReadPixels` from the **default framebuffer returns all zeros** with no GL error. dEQP verifies nearly everything through `glReadPixels`, so rendering to the surface scores DirectVulkan near zero for a reason unrelated to conformance. FBO readback is correct on both backends. Use it for **both** backends so the two numbers stay comparable. | +| `--deqp-terminate-on-device-lost=disable` | Defaults to *enable*, which calls `glGetGraphicsResetStatus()` after every case. That is GL 4.5 / `KHR_robustness`, absent from GL 3.3 core, so the pointer is null and the process segfaults on the first case. Desktop drivers expose the extension, which is why upstream never trips on it. | + +## Cases that take the device down + +Some cases hang the GPU hard enough that the device reboots or stops answering +adb entirely. Keep them in a `--skip-file`, and expect to find more: + +- `KHR-GL33.clip_distance.functional` — wedged an Adreno 750 tablet; it rebooted + and then stopped responding to adb altogether. +- `KHR-GL33.framebuffer_blit.multisampled_to_singlesampled_blit_color_config_test` + — rebooted an Adreno 830 phone after 862 cases, on DirectGLES. + +When a run dies, pull `/data/local/tmp/mgcts/chunk.qpa` — it survives the reboot, +and the last `#beginTestCaseResult` with no matching `#endTestCaseResult` names +the case that did it. + +## MobileGL constraints the port works around + +- **DirectVulkan cannot use an EGL pbuffer.** That path needs + `VK_EXT_headless_surface`, which Adreno's Android driver does not expose; it + fails inside `eglMakeCurrent`. The platform therefore gets a real + `ANativeWindow` from **`AImageReader`** — an ordinary BufferQueue producer that + `vkCreateAndroidSurfaceKHR` accepts, with no Activity. An `onImageAvailable` + listener must drain the queue or the producer blocks once `maxImages` buffers + are in flight and the next swap deadlocks. +- **`eglMakeCurrent` requires draw == read** and rejects `EGL_NO_SURFACE` with + `EGL_BAD_MATCH`, so dEQP's `surfaceless` platform cannot be used at all, and + `--deqp-surface-type=fbo` (which asks the platform for `SURFACETYPE_DONT_CARE`) + must still be given a real surface. +- **Every EGL call must go through the dynamically loaded library.** dEQP's + `surfaceless` platform mixes wrapper calls with globally linked `egl*` symbols; + copying that on Android silently reaches the system EGL and invalidates the + measurement. The `mobilegl` target links no `libEGL`/`libGLESv*` at all. +- **Desktop-GL configs need `EGL_OPENGL_BIT`.** The surfaceless port always asks + for an ES bit, which can never satisfy a GL 3.3 core context. +- MobileGL aborts during static teardown (`FORTIFY: pthread_mutex_lock called on + a destroyed mutex`) *after* the work is done; flush and `_exit()` in any small + tool, or its exit code and output are lost. + +## Contents + + platform/tcuMobileGLPlatform.{cpp,hpp} dEQP tcu::Platform for MobileGL + targets/mobilegl.cmake VK-GL-CTS target (-DDEQP_TARGET=mobilegl) + targets/ndk-modern.cmake NDK toolchain hook (sets DE_OS/DE_CPU) + probe/mgprobe.c preflight gate + scripts/sync_to_cts.py inject the port into a CTS checkout + scripts/run_cts.py crash- and reboot-resuming runner + scripts/qpa_report.py .qpa -> conformance rate diff --git a/tools/cts/skills/gl-cts-on-mobilegl/agents/openai.yaml b/tools/cts/skills/gl-cts-on-mobilegl/agents/openai.yaml new file mode 100644 index 00000000..c731da29 --- /dev/null +++ b/tools/cts/skills/gl-cts-on-mobilegl/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "OpenGL CTS on MobileGL (Android)" + short_description: "Build and run VK-GL-CTS KHR-GL33 against MobileGL and report per-backend conformance" + default_prompt: "Use $gl-cts-on-mobilegl to run the OpenGL 3.3 core CTS against MobileGL on my Android device and report the conformance rate for DirectGLES and DirectVulkan." diff --git a/tools/cts/targets/mobilegl.cmake b/tools/cts/targets/mobilegl.cmake new file mode 100644 index 00000000..021df7c1 --- /dev/null +++ b/tools/cts/targets/mobilegl.cmake @@ -0,0 +1,36 @@ +#------------------------------------------------------------------------- +# VK-GL-CTS target: MobileGL on Android +# +# Builds a standalone arm64 ELF that reaches OpenGL exclusively through +# libMobileGL.so, loaded at runtime. Nothing here links libEGL or libGLESv*: +# the whole point is that the system GL stack must not be reachable, so that a +# conformance result is unambiguously MobileGL's. +#------------------------------------------------------------------------- + +message("*** Using MobileGL target") + +set(DEQP_TARGET_NAME "MobileGL") + +# Build the modules as standalone executables instead of the libdeqp.so an APK +# would load. The suite runs from adb shell, with no Activity. +set(DEQP_ANDROID_EXE ON) + +# EGL comes from libMobileGL.so via the eglw dynamic wrapper, so the support +# flag is on but no import library is supplied. +set(DEQP_SUPPORT_EGL ON) +set(DEQP_EGL_LIBRARIES) +set(DEQP_GLES2_LIBRARIES) +set(DEQP_GLES3_LIBRARIES) + +set(TCUTIL_PLATFORM_SRCS + mobilegl/tcuMobileGLPlatform.cpp + mobilegl/tcuMobileGLPlatform.hpp + ) + +find_library(LOG_LIBRARY NAMES log) +find_library(ANDROID_LIBRARY NAMES android) +find_library(MEDIANDK_LIBRARY NAMES mediandk) + +# libmediandk supplies AImageReader, which is how a process with no Activity +# gets a real ANativeWindow. +list(APPEND TCUTIL_PLATFORM_LIBS ${ANDROID_LIBRARY} ${MEDIANDK_LIBRARY} ${LOG_LIBRARY}) diff --git a/tools/cts/targets/ndk-modern.cmake b/tools/cts/targets/ndk-modern.cmake new file mode 100644 index 00000000..cad1b3d1 --- /dev/null +++ b/tools/cts/targets/ndk-modern.cmake @@ -0,0 +1,61 @@ +#------------------------------------------------------------------------- +# drawElements CMake utilities +# ---------------------------- +# +# Copyright 2016 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +#------------------------------------------------------------------------- + +# Delegate most things to the NDK's cmake toolchain script + +if (NOT DEFINED ANDROID_NDK_PATH) + message(FATAL_ERROR "Please provide ANDROID_NDK_PATH") +endif () + +set(ANDROID_PLATFORM "android-${DE_ANDROID_API}") +set(ANDROID_STL c++_static) +set(ANDROID_CPP_FEATURES "rtti exceptions") + +include(${ANDROID_NDK_PATH}/build/cmake/android.toolchain.cmake) + +# The try_compile() used to verify the C/C++ compilers are sane tries to +# generate an executable, but doesn't seem to use the right compiler/linker +# options when cross-compiling, so it fails even when building an actual +# shared library or executable succeeds. +# +# I don't know why this doesn't affect simpler projects that use the NDK +# toolchain. +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) + +# Set variables used by other parts of dEQP's build scripts + +set(DE_OS "DE_OS_ANDROID") + +if (NOT DEFINED DE_COMPILER) + set(DE_COMPILER "DE_COMPILER_CLANG") +endif () + +if (ANDROID_ABI STREQUAL "x86") + set(DE_CPU "DE_CPU_X86") +elseif (ANDROID_ABI STREQUAL "armeabi" OR + ANDROID_ABI STREQUAL "armeabi-v7a") + set(DE_CPU "DE_CPU_ARM") +elseif (ANDROID_ABI STREQUAL "arm64-v8a") + set(DE_CPU "DE_CPU_ARM_64") +elseif (ANDROID_ABI STREQUAL "x86_64") + set(DE_CPU "DE_CPU_X86_64") +else () + message(FATAL_ERROR "Unknown ABI \"${ANDROID_ABI}\"") +endif ()