---
name: linux-gl-cts-on-mobilegl
description: Run the Khronos OpenGL CTS (VK-GL-CTS glcts) against MobileGL on desktop Linux with no GPU and no device, and report a per-backend conformance rate for Espryt (DirectGLES) and Magma (DirectVulkan). Use when measuring or iterating on the conformance of one test group, when a fix needs a before/after number, or when neither an Android device nor a Windows GPU box is available.
---

# OpenGL CTS on MobileGL (desktop Linux)

## Overview

`glcts` is built as an ordinary x86-64 host executable that reaches OpenGL only
through `libMobileGL.so`, using the `mobilegl-desktop` VK-GL-CTS target and the
`tcu::Platform` port in `tools/cts/platform/`. Nothing links `libGL` or `libEGL`,
so a result is unambiguously MobileGL's.

Both backends run headless on software rendering, so this needs no GPU at all:

- **Espryt** (`DirectGLES`) drives Mesa's OpenGL ES through the system EGL.
- **Magma** (`DirectVulkan`) runs on lavapipe, whose `VK_EXT_headless_surface`
  is what the desktop platform port's pbuffer path requires.

Always report the two backends **separately**. They are different
implementations of the same front end, they fail different cases, and a single
combined number hides which one a change moved.

## Prerequisites

```sh
sudo apt-get install -y ninja-build cmake libvulkan-dev \
    libegl1-mesa-dev libgles2-mesa-dev mesa-vulkan-drivers
```

`mesa-vulkan-drivers` is what installs lavapipe; without it DirectVulkan has no
ICD and `eglInitialize` fails inside the backend. A C++23 toolchain is required
(GCC 13+, or Clang 20+ — Clang 18 defines `__cpp_concepts` as 201907L, which
switches libstdc++'s `<expected>` off and the build fails in
`MG_Util/ShaderTranspiler/Types.h`).

```sh
export MG=<path-to-MobileGL-worktree>
export CTS=<path-to-VK-GL-CTS-checkout>
```

## Step 1 — build libMobileGL.so

```sh
git -C "$MG" submodule update --init --recursive
python3 "$MG/3rdparty/glslang/update_glslang_sources.py"   # SPIRV-Tools; ENABLE_OPT is forced on

cmake -S "$MG" -B "$MG/build-linux" -G Ninja -DCMAKE_BUILD_TYPE=Release \
      -DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
      -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
cmake --build "$MG/build-linux" --parallel "$(nproc)"
```

Add `-DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=FALSE` when iterating. The
Release configuration turns LTO on, which makes every relink cost minutes for no
behavioural difference; a conformance number is identical either way.

## Step 2 — get VK-GL-CTS and build glcts

Use a release tag so the mustpass list, and therefore the reported rate, is
citable.

```sh
git -C "$CTS" checkout opengl-cts-4.6.8.1
python3 "$CTS/external/fetch_sources.py"

python3 "$MG/tools/cts/scripts/sync_to_cts.py" "$CTS"
git -C "$CTS" apply "$MG/tools/cts/patches/0001-fbo-color-texture-attachment.patch"

cmake -S "$CTS" -B "$CTS/build-cts" -G Ninja -DDEQP_TARGET=mobilegl-desktop \
      -DCMAKE_BUILD_TYPE=Release \
      -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
ninja -C "$CTS/build-cts" glcts
```

Confirm the configure output says `*** Using MobileGL desktop target`. Budget a
couple of hours for the `glcts` link on a small machine; it is a one-time cost
that ccache makes cheap afterwards.

If `fetch_sources.py` dies with `HTTP Error 403` it is an egress policy blocking
the GitHub archive downloads (zlib, libpng), not a broken checkout: `git clone`
still works, so clone the package at the tag the script pins into
`external/<pkg>/src` by hand — for libpng also copy
`scripts/pnglibconf.h.prebuilt` to `src/pnglibconf.h`, which is what the
script's post-extract step does.

## Step 3 — run, once per backend

```sh
cd "$MG"
for BACKEND in DirectGLES DirectVulkan; do
  python3 tools/cts/scripts/run_cts_local.py --backend "$BACKEND" \
      --glcts "$CTS/build-cts/external/openglcts/modules/glcts" \
      --lib   build-linux/libMobileGL.so \
      --caselist cases.txt --outdir "runs/${BACKEND}" \
      --env EGL_PLATFORM=surfaceless
  python3 tools/cts/scripts/qpa_report.py "runs/${BACKEND}" --label "$BACKEND"
done
```

`cases.txt` is any subset of a mustpass list. For one group, filter the list
rather than running the whole suite:

```sh
grep direct_state_access \
  "$CTS"/external/openglcts/data/gl_cts/data/mustpass/gl/khronos_mustpass/main/gl45-main.txt \
  > cases.txt
```

`run_cts_local.py` re-invokes `glcts` with only the cases that have no result
yet, so a crash costs one case rather than the run, and it records the case that
was open when the process died as `Crash`. `qpa_report.py` scores `Pass`,
`NotSupported` and the warning statuses as non-failures, the way Khronos scores
a submission, and reports the strict `Pass`-only rate alongside.

## Required flags, and why

| Flag | Why it is not optional |
| --- | --- |
| `--env EGL_PLATFORM=surfaceless` | For DirectGLES. With no `/dev/dri` node Mesa's EGL fails `eglInitialize` on the default display, and MobileGL surfaces that as `EGL_BAD_ALLOC` out of `eglCreatePbufferSurface` — which points at the wrong call entirely. Harmless for DirectVulkan, so pass it to both. |
| `--deqp-surface-type=fbo` (the runner's default) | DirectVulkan reads back zeros from the **default** framebuffer. dEQP verifies nearly everything through `glReadPixels`, so rendering to the surface scores Magma near zero for a reason unrelated to conformance. Use it for both backends so the two numbers stay comparable. |
| `--deqp-terminate-on-device-lost=disable` (supplied by the runner) | Its default calls `glGetGraphicsResetStatus()` after every case. That is GL 4.5 / `KHR_robustness`, absent from what MobileGL exports, so the pointer is null and the process segfaults on the first case. |

## What this environment does and does not tell you

Reproducible here, and MobileGL's own rather than a driver quirk:

- DirectVulkan's default-framebuffer readback returns zeros; a user FBO,
  renderbuffer- or texture-attached, is correct on both backends. This is the
  same defect the Android runs work around, so it can be debugged without a
  phone.

Different from a real GPU, so do not read conformance into it:

- lavapipe supports renderbuffer formats Adreno reports as unsupported, so the
  Android runs see `NotSupported` where these do not, and vice versa.
- Backend limits differ. `GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT` is 16 on llvmpipe
  and on lavapipe; a device that reports 1 will not exercise the same paths.
- Everything is a software rasterizer, so a case that fails only under real
  timing or real tiling will not fail here.

## Reference results: KHR-GL45.direct_state_access

`opengl-cts-4.6.8.1`, the 371 `direct_state_access` cases of the `gl45-main`
mustpass list, Mesa 25.2.8, `--deqp-surface-type=fbo`.

| backend | renderer | conformance | strict Pass | Fail | InternalError | Crash |
| --- | --- | ---: | ---: | ---: | ---: | ---: |
| DirectGLES | Espryt | **82.48%** | 74.93% | 57 | 8 | 0 |
| DirectVulkan | Magma | **73.58%** | 73.32% | 87 | 8 | 3 |

## Contents

    platform/tcuMobileGLPlatform.{cpp,hpp}   dEQP tcu::Platform for MobileGL
    targets/mobilegl-desktop.cmake           VK-GL-CTS target (-DDEQP_TARGET=mobilegl-desktop)
    scripts/sync_to_cts.py                   inject the port into a CTS checkout
    scripts/run_cts_local.py                 crash-resuming local-host runner
    scripts/qpa_report.py                    .qpa -> conformance rate
