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:
setenv("MOBILEGL_BACKEND_TYPE", "DirectGLES"|"DirectVulkan")before the library is mapped — MobileGL parses its configuration from an ELF constructor.dlopen("libMobileGL.so"), thendlsymtheegl*andgl*entry points. MobileGL exports 45 EGL symbols and the desktop GL functions directly;eglGetProcAddressresolves the same set.eglBindAPI(EGL_OPENGL_API), choose a config withEGL_RENDERABLE_TYPE = EGL_OPENGL_BIT, theneglCreateContextwithEGL_CONTEXT_OPENGL_PROFILE_MASK = EGL_CONTEXT_OPENGL_CORE_PROFILE_BITand major/minor3/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
eglMakeCurrentrequires draw == read and rejectsEGL_NO_SURFACEwithEGL_BAD_MATCH. dEQP'ssurfacelessplatform is therefore unusable, which is why this port supplies its owntcu::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.qpalog 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.