From 811fd82916c11b9941edc3b8d5e2371ff52738fc Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 09:13:09 +0200 Subject: [PATCH 01/12] =?UTF-8?q?compiler-rt:=20Install=20into=20clang?= =?UTF-8?q?=E2=80=99s=20resource=20directory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A standalone compiler-rt build does not set `COMPILER_RT_INSTALL_PATH`, so the runtimes are installed to `/usr/lib/linux/` instead of `/usr/lib/clang/22/lib/linux/`, and the headers and ignore lists to `/usr/include/sanitizer` and `/usr/share/*_ignorelist.txt`. clang only searches its resource directory, so no sanitizer can be linked: $ clang -print-runtime-dir (runtime dir is not present) $ clang -fsanitize=address t.c /usr/bin/ld: cannot find /usr/lib/clang/22/lib/x86_64-unknown-linux-gnu/libclang_rt.asan.a Pointing `-resource-dir` at a directory that symlinks `lib/linux` to `/usr/lib/linux`, makes the same command link, so the location is the only problem. Set `COMPILER_RT_INSTALL_PATH` to the resource directory `${LIBDIR}/clang/`, which is what the in-tree runtimes build does via `get_clang_resource_dir()`. Derive the major version from the package version instead of asking the installed clang with `clang -print-resource-dir`, so building against a clang of another version cannot silently install into the wrong directory. Assisted-by: claude-opus-5.5 (Claude Code) --- compiler-rt.be0 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler-rt.be0 b/compiler-rt.be0 index a0b80db52..f6394f370 100755 --- a/compiler-rt.be0 +++ b/compiler-rt.be0 @@ -1,6 +1,6 @@ #!/usr/bin/env beesh -# BEE_VERSION compiler-rt-22.1.8-0 +# BEE_VERSION compiler-rt-22.1.8-1 #SRCURL[0]="https://github.com/llvm/llvm-project/releases/download/llvmorg-${PKGVERSION}/llvm-project-${PKGVERSION}.src.tar.xz" SRCURL[0]="https://beehive.molgen.mpg.de/69065494ebcb1150b2a329c1b3db7584/llvm-project-22.1.8.src.tar.xz" @@ -20,9 +20,12 @@ sourcesubdir_append compiler-rt #} mee_configure() { + # A standalone build installs to ${PREFIX} by default, but clang only + # searches its resource directory ${LIBDIR}/clang/. bee_configure \ -DCMAKE_BUILD_TYPE=Release \ -DCOMPILER_RT_STANDALONE_BUILD=ON \ + -DCOMPILER_RT_INSTALL_PATH=${LIBDIR}/clang/${PKGVERSION%%.*} \ -DCOMPILER_RT_INCLUDE_TESTS=OFF \ -DLLVM_CMAKE_DIR=$(llvm-config --cmakedir) \ -DLLVM_CONFIG_PATH=$(command -v llvm-config) \ From 3fdabbd4be8730c83988be534dac4ec4e86711cb Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 09:13:25 +0200 Subject: [PATCH 02/12] llvm: Update version from 22.1.8 to 23.1.3 Assisted-by: claude-opus-5.5 (Claude Code) --- llvm.be0 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/llvm.be0 b/llvm.be0 index 681da7bbd..9a284267a 100755 --- a/llvm.be0 +++ b/llvm.be0 @@ -1,9 +1,9 @@ #!/usr/bin/env beesh -# BEE_VERSION llvm-22.1.8-0 +# BEE_VERSION llvm-23.1.3-0 #SRCURL[0]="https://github.com/llvm/llvm-project/releases/download/llvmorg-${PKGVERSION}/llvm-project-${PKGVERSION}.src.tar.xz" -SRCURL[0]="https://beehive.molgen.mpg.de/69065494ebcb1150b2a329c1b3db7584/llvm-project-22.1.8.src.tar.xz" +SRCURL[0]="https://beehive.molgen.mpg.de/d7e227fe471d80d5e2d411127a0123b1/llvm-project-23.1.3.src.tar.xz" # PATCHURL+=() From b4238f15ee8b303b1f8e89411f028751aab3f8f5 Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 09:13:25 +0200 Subject: [PATCH 03/12] clang: Update version from 22.1.8 to 23.1.3 Assisted-by: claude-opus-5.5 (Claude Code) --- clang.be0 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clang.be0 b/clang.be0 index 221a442be..825412e63 100755 --- a/clang.be0 +++ b/clang.be0 @@ -1,10 +1,10 @@ #!/usr/bin/env beesh -# BEE_VERSION clang-22.1.8-0 +# BEE_VERSION clang-23.1.3-0 #SRCURL[0]="https://github.com/llvm/llvm-project/releases/download/llvmorg-${PKGVERSION}/clang-${PKGVERSION}.src.tar.xz" #SRCURL[0]="https://github.com/llvm/llvm-project/releases/download/llvmorg-${PKGVERSION}/llvm-project-${PKGVERSION}.src.tar.xz" -SRCURL[0]="https://beehive.molgen.mpg.de/69065494ebcb1150b2a329c1b3db7584/llvm-project-22.1.8.src.tar.xz" +SRCURL[0]="https://beehive.molgen.mpg.de/d7e227fe471d80d5e2d411127a0123b1/llvm-project-23.1.3.src.tar.xz" # build_in_sourcedir From 6837810b054fc3c810d11e215cc2bf72f7caeeac Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 09:13:26 +0200 Subject: [PATCH 04/12] compiler-rt: Update version from 22.1.8 to 23.1.3 Assisted-by: claude-opus-5.5 (Claude Code) --- compiler-rt.be0 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler-rt.be0 b/compiler-rt.be0 index f6394f370..ef3f17a0c 100755 --- a/compiler-rt.be0 +++ b/compiler-rt.be0 @@ -1,9 +1,9 @@ #!/usr/bin/env beesh -# BEE_VERSION compiler-rt-22.1.8-1 +# BEE_VERSION compiler-rt-23.1.3-0 #SRCURL[0]="https://github.com/llvm/llvm-project/releases/download/llvmorg-${PKGVERSION}/llvm-project-${PKGVERSION}.src.tar.xz" -SRCURL[0]="https://beehive.molgen.mpg.de/69065494ebcb1150b2a329c1b3db7584/llvm-project-22.1.8.src.tar.xz" +SRCURL[0]="https://beehive.molgen.mpg.de/d7e227fe471d80d5e2d411127a0123b1/llvm-project-23.1.3.src.tar.xz" # PATCHURL+=() From b3c357bc05fa1f8d278dfecb6a364263fdd89f99 Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 09:13:26 +0200 Subject: [PATCH 05/12] lld: Update version from 22.1.8 to 23.1.3 Assisted-by: claude-opus-5.5 (Claude Code) --- lld.be0 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lld.be0 b/lld.be0 index 51f45c886..9124b3ecf 100755 --- a/lld.be0 +++ b/lld.be0 @@ -1,9 +1,9 @@ #!/usr/bin/env beesh -# BEE_VERSION lld-22.1.8-0 +# BEE_VERSION lld-23.1.3-0 #SRCURL[0]="https://github.com/llvm/llvm-project/releases/download/llvmorg-${PKGVERSION}/llvm-project-${PKGVERSION}.src.tar.xz" -SRCURL[0]="https://beehive.molgen.mpg.de/69065494ebcb1150b2a329c1b3db7584/llvm-project-22.1.8.src.tar.xz" +SRCURL[0]="https://beehive.molgen.mpg.de/d7e227fe471d80d5e2d411127a0123b1/llvm-project-23.1.3.src.tar.xz" # PATCHURL+=() From 339158a8df0c1de90da0b5a777d0522823380934 Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 09:13:33 +0200 Subject: [PATCH 06/12] spirv-llvm-translator: Update version from 22.1.6 to 23.1.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The translator’s major version has to match LLVM’s, and 23.1.2 requires LLVM 23.1.0 (`BASE_LLVM_VERSION`). The SPIRV-Headers commit it pins, 575b6512, is already contained in spirv-headers 1.4.357.0. Assisted-by: claude-opus-5.5 (Claude Code) --- spirv-llvm-translator.be0 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spirv-llvm-translator.be0 b/spirv-llvm-translator.be0 index 47e2e0b50..75947f6d8 100755 --- a/spirv-llvm-translator.be0 +++ b/spirv-llvm-translator.be0 @@ -1,9 +1,9 @@ #!/usr/bin/env beesh -# BEE_VERSION spirv-llvm-translator-22.1.6-0 +# BEE_VERSION spirv-llvm-translator-23.1.2-0 #SRCURL[0]="https://github.com/KhronosGroup/SPIRV-LLVM-Translator/archive/v${PKGVERSION}/SPIRV-LLVM-Translator-${PKGVERSION}.tar.gz" -SRCURL[0]="https://beehive.molgen.mpg.de/4e302a4bab696f81369211c2b6204ba1/SPIRV-LLVM-Translator-22.1.6.tar.gz" +SRCURL[0]="https://beehive.molgen.mpg.de/2f163a32cb716012c3f19bb9a754a0e4/SPIRV-LLVM-Translator-23.1.2.tar.gz" # PATCHURL+=() From a0a15ef1b23e28995f65528cc8755d14f460fccd Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 09:36:11 +0200 Subject: [PATCH 07/12] mesa-libclc: Add version 22.1.8.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Description](https://gitlab.freedesktop.org/karolherbst/mesa-libclc): > This is a fork of LLVM's libclc applying some fixes of a known good > state. > > […] Its main purpose is to function as a reliable base mesa can use > and fix issues that won't be addressed upstream. It also functions as > a version pin, so that unexpected changes upstream won't cause > regressions as the interface between the OpenCL runtimes using libclc > and libclc itself is not stable. Since 26.2.0, Mesa looks for `mesa-libclc.pc` first, and falls back to upstream’s `libclc.pc` only if it is missing. rusticl warns if it does not load the fork. The fallback stops working with LLVM 23: libclc 23.1.3 builds one target per configuration (`LIBCLC_TARGETS_TO_BUILD` is gone), installs `share/clc//libclc.{spv,bc}`, and no longer ships `libclc.pc` or the `spirv{,64}-mesa3d-.spv` files Mesa loads. The fork builds only these two SPIR-V targets, and installs them to `/usr/share/mesa-clc/` with `/usr/share/pkgconfig/mesa-libclc.pc`, so it does not conflict with the libclc package. Model the bee-file on `libclc.be0` and `lld.be0`. Tag 22.1.8.3 is based on libclc 22, and the fork’s CI builds it with clang 22, but `LIBCLC_MIN_LLVM` is 22.1.0, so it is meant to be buildable with LLVM 23, too. The fork has an `llvm_23` branch, but no tag for it yet. Assisted-by: claude-opus-5.5 (Claude Code) --- mesa-libclc.be0 | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100755 mesa-libclc.be0 diff --git a/mesa-libclc.be0 b/mesa-libclc.be0 new file mode 100755 index 000000000..a38c82a2a --- /dev/null +++ b/mesa-libclc.be0 @@ -0,0 +1,36 @@ +#!/usr/bin/env beesh + +# BEE_VERSION mesa-libclc-22.1.8.3-0 + +# Mesa's fork of libclc, pinned to a state that passes the OpenCL CTS with +# rusticl. Mesa prefers it (mesa-libclc.pc) over upstream libclc (libclc.pc). +#SRCURL[0]="https://gitlab.freedesktop.org/karolherbst/mesa-libclc/-/archive/${PKGVERSION}/mesa-libclc-${PKGVERSION}.tar.gz" +SRCURL[0]="https://beehive.molgen.mpg.de/b643e5da5335d299f06b65790ad8e277/mesa-libclc-22.1.8.3.tar.gz" + +# PATCHURL+=() + +# build_in_sourcedir + +#mee_extract() { +# bee_extract "${@}" +#} + +#mee_patch() { +# bee_patch "${@}" +#} + +mee_configure() { + bee_configure \ + -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_CMAKE_DIR=$(llvm-config --cmakedir) \ + -Wno-dev \ + -G Ninja +} + +mee_build() { + start_cmd ninja -v -C ${B} ${BEE_NINJAFLAGS} "${@}" +} + +mee_install() { + DESTDIR=${D} start_cmd ninja -C ${B} install "${@}" +} From facf17f94cb0cbcc26733e1d5f3d1df49b815306 Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 09:49:10 +0200 Subject: [PATCH 08/12] libclc: Drop package with no users left Since 26.2.0, Mesa builds against its fork mesa-libclc, which is now packaged, and no other bee-file refers to libclc. Porting libclc to LLVM 23 would also not be a version bump: libclc 23.1.3 builds one target per configuration (`LIBCLC_TARGETS_TO_BUILD` is gone), so the eleven targets of the recipe would need eleven configurations, and it no longer ships `libclc.pc`. Assisted-by: claude-opus-5.5 (Claude Code) --- libclc.be0 | 36 ------------------------------------ 1 file changed, 36 deletions(-) delete mode 100755 libclc.be0 diff --git a/libclc.be0 b/libclc.be0 deleted file mode 100755 index 7dcbcb1fb..000000000 --- a/libclc.be0 +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env beesh - -# BEE_VERSION libclc-22.1.8-0 - -# Since LLVM 19 upstream only publishes the monorepo tarball, so libclc is -# built from the llvm-project source subdirectory, like clang and compiler-rt. -#SRCURL[0]="https://github.com/llvm/llvm-project/releases/download/llvmorg-${PKGVERSION}/libclc-${PKGVERSION}.src.tar.xz" -#SRCURL[0]="https://github.com/llvm/llvm-project/releases/download/llvmorg-${PKGVERSION}/llvm-project-${PKGVERSION}.src.tar.xz" -SRCURL[0]="https://beehive.molgen.mpg.de/69065494ebcb1150b2a329c1b3db7584/llvm-project-22.1.8.src.tar.xz" - -# PATCHURL+=() - -# build_in_sourcedir - -sourcesubdir_append libclc - -#mee_extract() { -# bee_extract "${@}" -#} - -#mee_patch() { -# bee_patch "${@}" -#} - -mee_configure() { - bee_configure \ - -DLIBCLC_TARGETS_TO_BUILD="amdgcn--;amdgcn-amd-amdhsa;clspv--;clspv64--;r600--;nvptx64--;nvptx64--nvidiacl;nvptx64-nvidia-cuda;amdgcn-mesa-mesa3d;spirv-mesa3d-;spirv64-mesa3d-" -} - -#mee_build() { -# start_cmd ninja -v -C ${B} ${BEE_NINJAFLAGS} "${@}" -#} - -#mee_install() { -# DESTDIR=${D} start_cmd ninja -C ${B} install "${@}" -#} From af340bc92058e5f78028e88bfb3eaaf41713f5fb Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 09:13:33 +0200 Subject: [PATCH 09/12] mesalib: Update version from 26.2.2 to 26.2.4 Assisted-by: claude-opus-5.5 (Claude Code) --- mesalib.be0 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mesalib.be0 b/mesalib.be0 index 47f562876..3dc0e9fdf 100755 --- a/mesalib.be0 +++ b/mesalib.be0 @@ -1,6 +1,6 @@ #!/usr/bin/env beesh -# BEE_VERSION mesalib-26.2.2-0 +# BEE_VERSION mesalib-26.2.4-0 ## this file was created by bee init and should be executed to build a ## bee-package. (Additional hints are located at the end of this file.) @@ -10,7 +10,7 @@ ## downloaded. Version variables may be used to simplify reuse of this bee-file. #SRCURL[0]="https://archive.mesa3d.org/mesa-${PKGVERSION}.tar.xz" -SRCURL[0]="https://beehive.molgen.mpg.de/4f9e0c96c913cdd9e8998453e248714e/mesa-26.2.2.tar.xz" +SRCURL[0]="https://beehive.molgen.mpg.de/d8ee81c058acce4f7ce7e5be1917ed2e/mesa-26.2.4.tar.xz" ############################################################################### ## Add URLs/pathes to patch files to the PATCHURL array. From a630724f178312c10eb1e6faa69000c77d4e10a8 Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 15:18:27 +0200 Subject: [PATCH 10/12] smoke-tests: Add llvm-smoke-test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit llvm, clang, compiler-rt, lld and spirv-llvm-translator are built as separate standalone packages, so each can be fine on its own and still not fit the others. compiler-rt 22.1.8-0 installed its runtimes to `/usr/lib/linux`, where clang never looks, and nothing noticed until `-fsanitize=address` failed to link. So compile, link and run small programs through every package: the versions agree (including the libLLVM llvm-spirv links), clang finds its resource directory and compiler-rt, C and C++ with exceptions, lld also with ThinLTO, compiler-rt's builtins, ASan, UBSan and TSan catching what they should, profiling with llvm-profdata and llvm-cov, and OpenCL C to SPIR-V and back with llvm-spirv. `--prefix` takes image directories or `.bee.tar.bz2` packages and mounts them read-only over `/usr` in a user namespace, so packages can be checked before installing them, without root. clang derives its resource directory from where its binary is, so compiler-rt has to appear below the same `/usr`. ASan and TSan reserve terabytes of address space, which hosts with `vm.overcommit_memory=2`, like the MXQ nodes and servers, refuse. There they are only linked, and that is reported as a warning. On machdoch, with the installed LLVM 22.1.8 (lld not installed): FAIL missing: clang 22.1.8, llvm-config 22.1.8, ld.lld missing, llvm-spirv (libLLVM) 22.1 FAIL no compiler-rt runtimes in clang's runtime directory (clang -print-runtime-dir: (runtime dir is not present)) OK C OK C++ with exceptions […] FAIL ASan: clang failed: /usr/bin/ld: cannot find /usr/lib/clang/22/lib/x86_64-unknown-linux-gnu/libclang_rt.asan_static.a: No such file or directory and with the 23.1.3 packages of this branch: $ TMPDIR=/dev/shm scripts/smoke-tests/llvm-smoke-test --prefix …/llvm-23.1.3-0.x86_64.bee.tar.bz2 \ --prefix …/clang-23.1.3-0.x86_64.bee.tar.bz2 --prefix …/compiler-rt-23.1.3-0.x86_64.bee.tar.bz2 \ --prefix …/lld-23.1.3-0.x86_64.bee.tar.bz2 --prefix …/spirv-llvm-translator-23.1.2-0.x86_64.bee.tar.bz2 OK clang 23.1.3, llvm-config 23.1.3, ld.lld 23.1.3, llvm-spirv (libLLVM) 23.1 OK runtime directory /usr/lib/clang/23/lib/linux holds 76 compiler-rt files OK C OK C++ with exceptions OK lld links and runs, .comment names LLD 23 OK ThinLTO with lld OK compiler-rt builtins (--rtlib=compiler-rt) OK ASan reports heap-buffer-overflow OK UBSan reports signed integer overflow OK TSan reports data race OK profiling and coverage: compiler-rt profile, llvm-profdata, llvm-cov OK OpenCL C to SPIR-V and back with llvm-spirv, spirv-val accepts it PASS (0 failed, 0 warned) Assisted-by: claude-opus-5.5 (Claude Code) --- scripts/smoke-tests/llvm-smoke-test | 407 ++++++++++++++++++++++++++++ 1 file changed, 407 insertions(+) create mode 100755 scripts/smoke-tests/llvm-smoke-test diff --git a/scripts/smoke-tests/llvm-smoke-test b/scripts/smoke-tests/llvm-smoke-test new file mode 100755 index 000000000..252e4c854 --- /dev/null +++ b/scripts/smoke-tests/llvm-smoke-test @@ -0,0 +1,407 @@ +#! /usr/bin/env python3 + +# Smoke-test the installed LLVM toolchain: llvm, clang, compiler-rt, lld and +# spirv-llvm-translator. +# +# scripts/smoke-tests/llvm-smoke-test +# TMPDIR=/dev/shm scripts/smoke-tests/llvm-smoke-test \ +# --prefix /src/mariux/beeroot/packages/llvm-23.1.3-0.x86_64.bee.tar.bz2 \ +# --prefix /src/mariux/beeroot/packages/clang-23.1.3-0.x86_64.bee.tar.bz2 \ +# --prefix /src/mariux/beeroot/packages/compiler-rt-23.1.3-0.x86_64.bee.tar.bz2 \ +# --prefix /src/mariux/beeroot/packages/lld-23.1.3-0.x86_64.bee.tar.bz2 \ +# --prefix /src/mariux/beeroot/packages/spirv-llvm-translator-23.1.2-0.x86_64.bee.tar.bz2 +# +# The five packages are built separately, standalone, against each other, so +# each can be fine on its own and still not fit the others: compiler-rt 22.1.8-0 +# installed its runtimes to /usr/lib/linux, where clang never looks, and +# nothing noticed until `-fsanitize=address' failed to link. So this compiles, +# links and runs small programs through every package: +# +# - the versions agree: clang, llvm-config, ld.lld and the libLLVM that +# llvm-spirv links (the translator's major version has to match LLVM's), +# - clang finds its resource directory, headers and compiler-rt runtimes, +# - C and C++ (libstdc++, exceptions) with the default linker, +# - lld, also with ThinLTO, and compiler-rt's builtins via --rtlib, +# - AddressSanitizer, UndefinedBehaviorSanitizer and ThreadSanitizer catch +# what they should, +# - profiling and coverage with llvm-profdata and llvm-cov, +# - OpenCL C to SPIR-V and back with llvm-spirv, as Mesa's rusticl does. +# +# --prefix (repeatable) takes the image directory of a build, or a .bee.tar.bz2 +# package, which is unpacked below $TMPDIR. They are mounted read-only over /usr +# in a user namespace, so packages can be checked before they are installed, +# without root: clang derives its resource directory from where its binary is, +# so compiler-rt has to show up below the same /usr as clang. +# +# Failures exit non-zero. ASan and TSan reserve terabytes of address space, which +# hosts with vm.overcommit_memory=2 (MXQ nodes and servers) refuse; there the +# programs are only linked, and that is reported as WARN. + +import argparse +import os +import re +import shlex +import shutil +import subprocess +import sys +import tempfile + +# How --prefix survives the re-exec into the mount namespace. +PREFIX_VARIABLE = "LLVM_SMOKE_TEST_PREFIX" + +SOURCES = { + "hello.c": r""" +#include +int main(void) { puts("hello"); return 0; } +""", + "hello.cc": r""" +#include +#include +#include +int main() { + try { + throw std::runtime_error("hello"); + } catch (const std::exception &e) { + std::cout << std::string(e.what()) << std::endl; + } + return 0; +} +""", + "lto-main.c": r""" +#include +int twice(int); +int main(void) { printf("hello %d\n", twice(21)); return 0; } +""", + "lto-twice.c": r""" +int twice(int x) { return 2 * x; } +""", + "asan.c": r""" +#include +int main(void) { int *p = malloc(4 * sizeof *p); int x = p[4]; free(p); return x; } +""", + "ubsan.c": r""" +#include +int main(int argc, char **argv) { int x = INT_MAX; x += argc; return x == 0; } +""", + "tsan.c": r""" +#include +static int counter; +static void *work(void *arg) { for (int i = 0; i < 100000; i++) counter++; return arg; } +int main(void) { + pthread_t a, b; + pthread_create(&a, 0, work, 0); + pthread_create(&b, 0, work, 0); + pthread_join(a, 0); + pthread_join(b, 0); + return 0; +} +""", + "profile.c": r""" +#include +static int odd(int x) { return x & 1; } +int main(int argc, char **argv) { printf("%d\n", odd(argc) ? 1 : 2); return 0; } +""", + "kernel.cl": r""" +kernel void k(global float *out) { + size_t i = get_global_id(0); + out[i] = sqrt((float)i) + clamp((float)i, 1.0f, 2.0f); +} +""", +} + +status = {"fail": 0, "warn": 0} + + +def report(state, message): + if state != "ok": + status[state] += 1 + print("%-5s %s" % (state.upper(), message)) + + +def run(command, **kwargs): + """Run command, capturing its output, never raising for its exit status.""" + try: + return subprocess.run(command, capture_output=True, text=True, + timeout=600, **kwargs) + except (OSError, subprocess.TimeoutExpired) as error: + return subprocess.CompletedProcess(command, 127, "", str(error)) + + +def last_line(process): + """The line of the output that says what went wrong.""" + lines = (process.stderr or process.stdout).strip().splitlines() + for line in lines: + # The linker says why before clang says that it failed. + if re.search(r"error|cannot find|undefined reference", line) and \ + "linker command failed" not in line: + return line.strip() + return lines[-1] if lines else "exit status %d" % process.returncode + + +# ---------------------------------------------------------------- --prefix + +def unpack(items, workdir): + """Return the directories to overlay, unpacking package archives.""" + lowers = [] + for number, item in enumerate(items): + if os.path.isdir(item): + path = os.path.abspath(item.rstrip("/")) + elif os.path.isfile(item): + path = os.path.join(workdir, str(number)) + os.mkdir(path) + print(" unpacking %s" % item, flush=True) + process = subprocess.run(["tar", "-C", path, "-xf", item], + capture_output=True, text=True) + if process.returncode: + sys.exit("cannot unpack %s: %s" % (item, process.stderr)) + else: + sys.exit("no such directory or package: %s" % item) + if ":" in path or "," in path: + sys.exit("cannot overlay %s: overlayfs separates layers by ':'" + " and options by ','" % path) + if not os.path.isdir(path + "/usr"): + sys.exit("%s has no usr/" % item) + lowers.append(path) + return lowers + + +def run_in_overlay(lowers): + """Run this script again with lowers mounted over /usr.""" + layers = ":".join([lower + "/usr" for lower in lowers] + ["/usr"]) + setup = "set -e\nmount -t overlay overlay -o lowerdir=%s /usr\nexec \"$@\"" \ + % shlex.quote(layers) + environment = dict(os.environ) + environment[PREFIX_VARIABLE] = ":".join(lowers) + command = ["unshare", "--user", "--map-root-user", "--mount", + "sh", "-c", setup, "sh", sys.executable, + os.path.abspath(__file__)] + sys.argv[1:] + try: + return subprocess.run(command, env=environment).returncode + except OSError as error: + sys.exit("cannot re-run in a mount namespace (%s); install the" + " packages and run without --prefix" % error) + + +# ----------------------------------------------------------------- checks + +def major(version): + return version.split(".")[0] if version else None + + +def check_versions(): + """Return clang's major version, after comparing it with the others.""" + clang = re.search(r"clang version (\S+)", run(["clang", "--version"]).stdout) + clang = clang.group(1) if clang else None + llvm = run(["llvm-config", "--version"]).stdout.strip() or None + lld = re.search(r"LLD (\S+)", run(["ld.lld", "--version"]).stdout) + lld = lld.group(1) if lld else None + spirv = None + path = shutil.which("llvm-spirv") + if path: + linked = re.search(r"libLLVM\.so\.(\S+)", run(["ldd", path]).stdout) + spirv = linked.group(1) if linked else None + found = {"clang": clang, "llvm-config": llvm, "ld.lld": lld, + "llvm-spirv (libLLVM)": spirv} + summary = ", ".join("%s %s" % (name, version or "missing") + for name, version in found.items()) + if not clang: + report("fail", "no clang: " + summary) + return None + if None in found.values(): + report("fail", "missing: " + summary) + elif all(major(version) == major(clang) for version in found.values()): + report("ok", summary) + else: + report("fail", "versions disagree: " + summary) + return major(clang) + + +def check_resource_dir(version): + resource = run(["clang", "-print-resource-dir"]).stdout.strip() + expected = "/usr/lib/clang/%s" % version + if resource != expected: + report("fail", "resource directory is %s, not %s" % (resource, expected)) + if not os.path.exists(resource + "/include/stddef.h"): + report("fail", "no %s/include/stddef.h: clang's headers are missing" + % resource) + runtime = run(["clang", "-print-runtime-dir"]).stdout.strip() + runtimes = os.listdir(runtime) if os.path.isdir(runtime) else [] + if any(name.startswith("libclang_rt.asan") for name in runtimes): + report("ok", "runtime directory %s holds %d compiler-rt files" + % (runtime, len(runtimes))) + else: + report("fail", "no compiler-rt runtimes in clang's runtime directory" + " (clang -print-runtime-dir: %s)" % runtime) + + +def build(name, command, directory): + """Run a compile command; report and return False if it fails.""" + process = run(command, cwd=directory) + if process.returncode: + report("fail", "%s: %s failed: %s" + % (name, " ".join(command[:1]), last_line(process))) + return False + return True + + +def check_program(name, command, directory, expect="hello", binary="./a.out"): + if not build(name, command, directory): + return + process = run([binary], cwd=directory) + if process.returncode or expect not in process.stdout: + report("fail", "%s: %s printed %r, exit status %d" + % (name, binary, process.stdout.strip(), process.returncode)) + else: + report("ok", name) + + +def check_lld(version, directory): + name = "lld links and runs" + if not build(name, ["clang", "-fuse-ld=lld", "-o", "lld", "hello.c"], + directory): + return + comment = run(["readelf", "-p", ".comment", "lld"], cwd=directory).stdout + if "LLD %s." % version not in comment: + report("fail", "%s: .comment names no LLD %s" % (name, version)) + return + process = run(["./lld"], cwd=directory) + if process.returncode or "hello" not in process.stdout: + report("fail", "%s: ./lld exit status %d" % (name, process.returncode)) + else: + report("ok", "%s, .comment names LLD %s" % (name, version)) + + +def strict_overcommit(): + try: + with open("/proc/sys/vm/overcommit_memory") as sysctl: + return sysctl.read().strip() == "2" + except OSError: + return False + + +def check_sanitizer(name, flags, source, expect, directory): + output = name.lower() + if not build(name, ["clang", "-g"] + flags + ["-o", output, source], + directory): + return + if strict_overcommit() and name != "UBSan": + report("warn", "%s: linked, but not run: vm.overcommit_memory=2 refuses" + " its shadow memory" % name) + return + process = run(["./" + output], cwd=directory) + if expect in process.stderr: + report("ok", "%s reports %s" % (name, expect)) + else: + report("fail", "%s: expected %r, got exit status %d: %s" + % (name, expect, process.returncode, last_line(process))) + + +def check_profile(directory): + name = "profiling and coverage" + if not build(name, ["clang", "-fprofile-instr-generate", + "-fcoverage-mapping", "-o", "profile", "profile.c"], + directory): + return + steps = ([["./profile"], {"LLVM_PROFILE_FILE": "profile.profraw"}], + [["llvm-profdata", "merge", "-o", "profile.profdata", + "profile.profraw"], {}], + [["llvm-cov", "report", "./profile", + "-instr-profile=profile.profdata"], {}]) + for command, environment in steps: + process = run(command, cwd=directory, + env=dict(os.environ, **environment)) + if process.returncode: + report("fail", "%s: %s failed: %s" + % (name, command[0], last_line(process))) + return + if "TOTAL" in process.stdout: + report("ok", "%s: compiler-rt profile, llvm-profdata, llvm-cov" % name) + else: + report("fail", "%s: llvm-cov report printed no TOTAL" % name) + + +def check_spirv(directory): + name = "OpenCL C to SPIR-V" + if not shutil.which("llvm-spirv"): + report("fail", "%s: no llvm-spirv" % name) + return + if not build(name, ["clang", "-c", "-target", "spir64-unknown-unknown", + "-cl-std=CL2.0", "-emit-llvm", "-Xclang", + "-finclude-default-header", "-o", "kernel.bc", + "kernel.cl"], directory): + return + if not build(name, ["llvm-spirv", "kernel.bc", "-o", "kernel.spv"], + directory): + return + with open(os.path.join(directory, "kernel.spv"), "rb") as spirv: + if spirv.read(4) != b"\x03\x02\x23\x07": + report("fail", "%s: kernel.spv has no SPIR-V magic number" % name) + return + if shutil.which("spirv-val") and \ + not build(name, ["spirv-val", "kernel.spv"], directory): + return + if build(name, ["llvm-spirv", "-r", "kernel.spv", "-o", "back.bc"], + directory) and \ + build(name, ["llvm-dis", "back.bc"], directory): + report("ok", "%s and back with llvm-spirv%s" + % (name, ", spirv-val accepts it" + if shutil.which("spirv-val") else "")) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--prefix", action="append", default=[], + metavar="DIR|PACKAGE", + help="test the image directory DIR of a build, or the" + " .bee.tar.bz2 PACKAGE, instead of what is" + " installed; may be repeated") + arguments = parser.parse_args() + + if arguments.prefix and PREFIX_VARIABLE not in os.environ: + workdir = tempfile.mkdtemp(prefix="llvm-smoke-") + try: + return run_in_overlay(unpack(arguments.prefix, workdir)) + finally: + shutil.rmtree(workdir, ignore_errors=True) + if PREFIX_VARIABLE in os.environ: + print(" testing, over /usr: %s" + % os.environ[PREFIX_VARIABLE].replace(":", " ")) + + version = check_versions() + if not version: + return 1 + directory = tempfile.mkdtemp(prefix="llvm-smoke-") + try: + for name, text in SOURCES.items(): + with open(os.path.join(directory, name), "w") as source: + source.write(text) + check_resource_dir(version) + check_program("C", ["clang", "hello.c"], directory) + check_program("C++ with exceptions", ["clang++", "hello.cc"], directory) + check_lld(version, directory) + check_program("ThinLTO with lld", + ["clang", "-flto=thin", "-fuse-ld=lld", "-O2", + "lto-main.c", "lto-twice.c"], directory, + expect="hello 42") + check_program("compiler-rt builtins (--rtlib=compiler-rt)", + ["clang", "--rtlib=compiler-rt", "--unwindlib=libgcc", + "hello.c"], directory) + check_sanitizer("ASan", ["-fsanitize=address"], "asan.c", + "heap-buffer-overflow", directory) + check_sanitizer("UBSan", ["-fsanitize=undefined"], "ubsan.c", + "signed integer overflow", directory) + check_sanitizer("TSan", ["-fsanitize=thread"], "tsan.c", + "data race", directory) + check_profile(directory) + check_spirv(directory) + finally: + shutil.rmtree(directory, ignore_errors=True) + + print("%s (%d failed, %d warned)" + % ("FAIL" if status["fail"] else "PASS", status["fail"], + status["warn"])) + return 1 if status["fail"] else 0 + + +if __name__ == "__main__": + sys.exit(main()) From 0807035eca4ee5c502afc08bdf1a1e13e4e40d9f Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 15:18:28 +0200 Subject: [PATCH 11/12] smoke-tests: Add mesa-smoke-test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A version bump of llvm, clang, spirv-llvm-translator or mesa-libclc can break a Mesa driver without breaking mesalib's build. So check what a desktop or compute user hits: every Mesa library resolves its shared libraries; OpenGL ES with llvmpipe and with the hardware driver, each drawing a shaded triangle and comparing the pixels, so llvmpipe's LLVM JIT compiles and runs a shader; GLX on an Xvfb of its own; a device on every GPU of each Mesa Vulkan driver; and an OpenCL kernel calling libclc builtins on every rusticl device, warning when rusticl does not find Mesa's libclc fork. Vulkan and OpenCL go through ctypes, as hosts like machdoch have neither the Vulkan loader nor its headers: the Vulkan driver is asked for its entry points directly, through the interface the loader itself uses. EGL, Vulkan and OpenCL are restricted to Mesa's own vendor files, so NVIDIA's proprietary driver, where installed, is not what gets tested. OpenCL loads `/usr/lib/libOpenCL.so.1` by path: the /pkg python wrapper puts `/pkg/cuda-12.8.1-0/lib` on the library path on machdoch, and the Khronos loader there found no platform: FAIL OpenCL: no OpenCL platform `--prefix` works like llvm-smoke-test's, over `/usr` and `/etc`. On machdoch (Intel CometLake GT2), with the installed mesalib 26.2.2 and upstream libclc 22.1.8, OpenCL is broken: WARN OpenCL: rusticl uses upstream libclc, not Mesa's fork (install mesa-libclc) FAIL OpenCL Mesa Intel(R) UHD Graphics (CML GT2) (OpenCL 3.1): build failed: Internal compilation error: nir_shader not fully linked FAIL OpenCL llvmpipe (LLVM 22.1.8, 256 bits) (OpenCL 3.1): build failed: Internal compilation error: nir_shader not fully linked and with the packages of this branch everything passes: $ TMPDIR=/dev/shm scripts/smoke-tests/mesa-smoke-test --prefix …/llvm-23.1.3-0.x86_64.bee.tar.bz2 \ --prefix …/clang-23.1.3-0.x86_64.bee.tar.bz2 --prefix …/spirv-llvm-translator-23.1.2-0.x86_64.bee.tar.bz2 \ --prefix …/mesa-libclc-22.1.8.3-0.x86_64.bee.tar.bz2 --prefix …/mesalib-26.2.4-0.x86_64.bee.tar.bz2 OK 7 Mesa libraries resolve their shared libraries, using libLLVM.so.23.1, libLLVMSPIRVLib.so.23.1, libclang-cpp.so.23.1 OK mesa-libclc provides its SPIR-V libraries in /usr/share/mesa-clc OK OpenGL ES (software) draws a shaded triangle: llvmpipe (LLVM 23.1.3, 256 bits), OpenGL ES 3.2 Mesa 26.2.4 OK OpenGL ES (hardware) draws a shaded triangle: Mesa Intel(R) UHD Graphics (CML GT2), OpenGL ES 3.2 Mesa 26.2.4 OK GLX on Xvfb: llvmpipe (LLVM 23.1.3, 256 bits), 4.6 (Core Profile) Mesa 26.2.4 OK Vulkan libvulkan_intel.so: Intel(R) UHD Graphics (CML GT2), Intel open-source Mesa driver Mesa 26.2.4, Vulkan 1.4.354, device created OK Vulkan libvulkan_radeon.so: loads, drives no GPU of this host OK OpenCL Mesa Intel(R) UHD Graphics (CML GT2) (OpenCL 3.1) runs a kernel calling libclc builtins OK OpenCL llvmpipe (LLVM 23.1.3, 256 bits) (OpenCL 3.1) runs a kernel calling libclc builtins PASS (0 failed, 0 warned) Assisted-by: claude-opus-5.5 (Claude Code) --- scripts/smoke-tests/mesa-smoke-test | 749 ++++++++++++++++++++++++++++ 1 file changed, 749 insertions(+) create mode 100755 scripts/smoke-tests/mesa-smoke-test diff --git a/scripts/smoke-tests/mesa-smoke-test b/scripts/smoke-tests/mesa-smoke-test new file mode 100755 index 000000000..cc27884b2 --- /dev/null +++ b/scripts/smoke-tests/mesa-smoke-test @@ -0,0 +1,749 @@ +#! /usr/bin/env python3 + +# Smoke-test an installed Mesa: OpenGL, Vulkan and OpenCL (rusticl). +# +# scripts/smoke-tests/mesa-smoke-test +# scripts/smoke-tests/mesa-smoke-test --rusticl-enable llvmpipe,radeonsi +# TMPDIR=/dev/shm scripts/smoke-tests/mesa-smoke-test \ +# --prefix /src/mariux/beeroot/packages/llvm-23.1.3-0.x86_64.bee.tar.bz2 \ +# --prefix /src/mariux/beeroot/packages/clang-23.1.3-0.x86_64.bee.tar.bz2 \ +# --prefix /src/mariux/beeroot/packages/spirv-llvm-translator-23.1.2-0.x86_64.bee.tar.bz2 \ +# --prefix /src/mariux/beeroot/packages/mesa-libclc-22.1.8.3-0.x86_64.bee.tar.bz2 \ +# --prefix /src/mariux/beeroot/packages/mesalib-26.2.4-0.x86_64.bee.tar.bz2 +# +# mesalib.be0 links Mesa against llvm, clang, spirv-llvm-translator and +# mesa-libclc, and a version bump of any of them can break a driver without +# breaking the build. So this checks what a desktop and a compute user would hit: +# +# - every Mesa library resolves its shared libraries (ldd), +# - OpenGL ES with the software rasterizer (llvmpipe) and with the hardware +# driver, each by drawing a shaded triangle into a framebuffer and comparing +# the pixels; llvmpipe compiles that shader with LLVM's JIT, +# - GLX on an Xvfb of its own, the path Xfce takes, +# - each Mesa Vulkan driver: create an instance and a device on every GPU it +# drives, talking to the driver directly, as hosts need not have the loader, +# - OpenCL: build and run a kernel calling libclc builtins on every rusticl +# device, and complain when rusticl does not find Mesa's libclc fork. +# +# The EGL, Vulkan and OpenCL checks are restricted to Mesa's own vendor files, so +# that NVIDIA's proprietary driver, where installed, is not what gets tested. +# +# --prefix (repeatable) takes the image directory of a build, or a .bee.tar.bz2 +# package, which is unpacked below $TMPDIR. They are mounted read-only over /usr +# (and /etc) in a user namespace, so packages can be checked before they are +# installed, without root; the drivers find each other only under the paths +# compiled into them, which LD_LIBRARY_PATH alone does not cover. +# +# Failures of what the packages provide exit non-zero; what depends on the +# machine (no GPU, no render node, no Xvfb) is reported as WARN. + +import argparse +import ctypes +import glob +import json +import math +import os +import shlex +import shutil +import subprocess +import sys +import tempfile +import time + +# How --prefix survives the re-exec into the mount namespace. +PREFIX_VARIABLE = "MESA_SMOKE_TEST_PREFIX" + +# Mesa's own vendor files, see the comment at the top. +EGL_VENDOR = "/usr/share/glvnd/egl_vendor.d/50_mesa.json" +RUSTICL_ICD = "/etc/OpenCL/vendors/rusticl.icd" +OPENCL_LOADER = "/usr/lib/libOpenCL.so.1" +VULKAN_ICD_DIRS = ("/usr/share/vulkan/icd.d", "/etc/vulkan/icd.d") + +# Logged by rusticl (src/gallium/frontends/rusticl/core/device.rs). +LIBCLC_MISSING = "Libclc failed to load" +LIBCLC_UPSTREAM = "Patched Mesa libclc not detected" + +status = {"fail": 0, "warn": 0} + + +def report(state, message): + if state != "ok": + status[state] += 1 + print("%-5s %s" % (state.upper(), message)) + + +# ---------------------------------------------------------------- --prefix + +def unpack(items, workdir): + """Return the directories to overlay, unpacking package archives.""" + lowers = [] + for number, item in enumerate(items): + if os.path.isdir(item): + path = os.path.abspath(item.rstrip("/")) + elif os.path.isfile(item): + path = os.path.join(workdir, str(number)) + os.mkdir(path) + print(" unpacking %s" % item, flush=True) + process = subprocess.run(["tar", "-C", path, "-xf", item], + capture_output=True, text=True) + if process.returncode: + sys.exit("cannot unpack %s: %s" % (item, process.stderr)) + else: + sys.exit("no such directory or package: %s" % item) + if ":" in path or "," in path: + sys.exit("cannot overlay %s: overlayfs separates layers by ':'" + " and options by ','" % path) + lowers.append(path) + return lowers + + +def run_in_overlay(lowers): + """Run this script again with lowers mounted over /usr and /etc.""" + setup = ["set -e"] + for directory in ("usr", "etc"): + layers = [lower + "/" + directory for lower in lowers + if os.path.isdir(lower + "/" + directory)] + if layers: + setup.append("mount -t overlay overlay -o lowerdir=%s /%s" + % (shlex.quote(":".join(layers + ["/" + directory])), + directory)) + setup.append('exec "$@"') + environment = dict(os.environ) + environment[PREFIX_VARIABLE] = ":".join(lowers) + command = ["unshare", "--user", "--map-root-user", "--mount", + "sh", "-c", "\n".join(setup), "sh", sys.executable, + os.path.abspath(__file__)] + sys.argv[1:] + try: + return subprocess.run(command, env=environment).returncode + except OSError as error: + sys.exit("cannot re-run in a mount namespace (%s); install the" + " packages and run without --prefix" % error) + + +# --------------------------------------------------------------- children +# +# Every check that loads a driver runs in a child process of this script, so +# that each gets its own environment, a crash fails one check instead of the +# run, and what the driver logs to stderr can be looked at. A child prints one +# line of JSON. + +def child(kind, environment=None, timeout=300): + """Run check kind in a child; return (result or None, stderr).""" + env = dict(os.environ) + env.update(environment or {}) + try: + process = subprocess.run( + [sys.executable, os.path.abspath(__file__), "--child", kind], + env=env, capture_output=True, text=True, timeout=timeout) + except subprocess.TimeoutExpired: + return None, "timed out after %d s" % timeout + for line in reversed(process.stdout.splitlines()): + if line.startswith("{"): + return json.loads(line), process.stderr + return None, (process.stderr.strip().splitlines() or + ["exit status %d" % process.returncode])[-1] + + +def child_gl(): + """Draw a shaded triangle with OpenGL ES 3 on EGL's surfaceless platform.""" + c_int, c_uint, c_void_p = ctypes.c_int, ctypes.c_uint, ctypes.c_void_p + egl = ctypes.CDLL("libEGL.so.1") + gl = ctypes.CDLL("libGLESv2.so.2") + egl.eglGetPlatformDisplay.restype = c_void_p + egl.eglGetPlatformDisplay.argtypes = [c_uint, c_void_p, c_void_p] + egl.eglInitialize.argtypes = [c_void_p, c_void_p, c_void_p] + egl.eglChooseConfig.argtypes = [c_void_p, c_void_p, c_void_p, c_int, + c_void_p] + egl.eglCreateContext.restype = c_void_p + egl.eglCreateContext.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] + egl.eglMakeCurrent.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] + egl.eglGetError.restype = c_int + gl.glGetString.restype = ctypes.c_char_p + gl.glCreateShader.restype = c_uint + gl.glCreateProgram.restype = c_uint + gl.glShaderSource.argtypes = [c_uint, c_int, c_void_p, c_void_p] + gl.glVertexAttribPointer.argtypes = [c_uint, c_int, c_uint, ctypes.c_ubyte, + c_int, c_void_p] + gl.glReadPixels.argtypes = [c_int, c_int, c_int, c_int, c_uint, c_uint, + c_void_p] + gl.glBindAttribLocation.argtypes = [c_uint, c_uint, ctypes.c_char_p] + + def fail(message): + return {"error": "%s (EGL error 0x%x, GL error 0x%x)" + % (message, egl.eglGetError(), gl.glGetError())} + + display = egl.eglGetPlatformDisplay(0x31DD, None, None) # SURFACELESS_MESA + if not display or not egl.eglInitialize(display, None, None): + return fail("eglInitialize failed on the surfaceless platform") + # EGL_SURFACE_TYPE 0: any config, the context renders into an FBO. + attributes = (c_int * 5)(0x3033, 0, 0x3040, 0x0040, 0x3038) + config, count = c_void_p(), c_int() + if not egl.eglChooseConfig(display, attributes, ctypes.byref(config), 1, + ctypes.byref(count)) or count.value < 1: + return fail("no OpenGL ES 3 config") + egl.eglBindAPI(0x30A0) # EGL_OPENGL_ES_API + context = egl.eglCreateContext(display, config, None, + (c_int * 3)(0x3098, 3, 0x3038)) + if not context or not egl.eglMakeCurrent(display, None, None, context): + return fail("cannot make an OpenGL ES 3 context current") + + size = 64 + framebuffer, renderbuffer = c_uint(), c_uint() + gl.glGenFramebuffers(1, ctypes.byref(framebuffer)) + gl.glBindFramebuffer(0x8D40, framebuffer) + gl.glGenRenderbuffers(1, ctypes.byref(renderbuffer)) + gl.glBindRenderbuffer(0x8D41, renderbuffer) + gl.glRenderbufferStorage(0x8D41, 0x8058, size, size) # GL_RGBA8 + gl.glFramebufferRenderbuffer(0x8D40, 0x8CE0, 0x8D41, renderbuffer) + if gl.glCheckFramebufferStatus(0x8D40) != 0x8CD5: + return fail("framebuffer incomplete") + + sources = { + 0x8B31: b"attribute vec2 pos;\n" + b"void main() { gl_Position = vec4(pos, 0.0, 1.0); }\n", + 0x8B30: b"precision highp float;\n" + b"void main() {\n" + b" gl_FragColor = vec4(gl_FragCoord.xy / 64.0,\n" + b" 0.5 + 0.5 * sin(gl_FragCoord.x), 1.0);\n" + b"}\n", + } + program = gl.glCreateProgram() + for kind, text in sources.items(): + shader = gl.glCreateShader(kind) + pointer = ctypes.c_char_p(text) + gl.glShaderSource(shader, 1, ctypes.byref(pointer), None) + gl.glCompileShader(shader) + compiled = c_int() + gl.glGetShaderiv(shader, 0x8B81, ctypes.byref(compiled)) + if not compiled.value: + return fail("shader does not compile") + gl.glAttachShader(program, shader) + gl.glBindAttribLocation(program, 0, b"pos") + gl.glLinkProgram(program) + linked = c_int() + gl.glGetProgramiv(program, 0x8B82, ctypes.byref(linked)) + if not linked.value: + return fail("program does not link") + gl.glUseProgram(program) + + triangle = (ctypes.c_float * 6)(-1, -1, 3, -1, -1, 3) + gl.glViewport(0, 0, size, size) + gl.glEnableVertexAttribArray(0) + gl.glVertexAttribPointer(0, 2, 0x1406, 0, 0, triangle) + gl.glDrawArrays(0x0004, 0, 3) + pixels = (ctypes.c_ubyte * (size * size * 4))() + gl.glReadPixels(0, 0, size, size, 0x1908, 0x1401, pixels) + if gl.glGetError(): + return fail("drawing failed") + + wrong = 0 + for y in range(size): + for x in range(size): + want = ((x + .5) / size, (y + .5) / size, + .5 + .5 * math.sin(x + .5)) + got = pixels[(y * size + x) * 4:(y * size + x) * 4 + 3] + if any(abs(g - w * 255) > 3 for g, w in zip(got, want)): + wrong += 1 + return {"renderer": gl.glGetString(0x1F01).decode(), + "version": gl.glGetString(0x1F02).decode(), + "wrong": wrong, "pixels": size * size} + + +class VkApplicationInfo(ctypes.Structure): + _fields_ = [("sType", ctypes.c_uint32), ("pNext", ctypes.c_void_p), + ("pApplicationName", ctypes.c_char_p), + ("applicationVersion", ctypes.c_uint32), + ("pEngineName", ctypes.c_char_p), + ("engineVersion", ctypes.c_uint32), + ("apiVersion", ctypes.c_uint32)] + + +class VkInstanceCreateInfo(ctypes.Structure): + _fields_ = [("sType", ctypes.c_uint32), ("pNext", ctypes.c_void_p), + ("flags", ctypes.c_uint32), + ("pApplicationInfo", ctypes.POINTER(VkApplicationInfo)), + ("enabledLayerCount", ctypes.c_uint32), + ("ppEnabledLayerNames", ctypes.c_void_p), + ("enabledExtensionCount", ctypes.c_uint32), + ("ppEnabledExtensionNames", ctypes.c_void_p)] + + +class VkDeviceQueueCreateInfo(ctypes.Structure): + _fields_ = [("sType", ctypes.c_uint32), ("pNext", ctypes.c_void_p), + ("flags", ctypes.c_uint32), + ("queueFamilyIndex", ctypes.c_uint32), + ("queueCount", ctypes.c_uint32), + ("pQueuePriorities", ctypes.POINTER(ctypes.c_float))] + + +class VkDeviceCreateInfo(ctypes.Structure): + _fields_ = [("sType", ctypes.c_uint32), ("pNext", ctypes.c_void_p), + ("flags", ctypes.c_uint32), + ("queueCreateInfoCount", ctypes.c_uint32), + ("pQueueCreateInfos", ctypes.POINTER(VkDeviceQueueCreateInfo)), + ("enabledLayerCount", ctypes.c_uint32), + ("ppEnabledLayerNames", ctypes.c_void_p), + ("enabledExtensionCount", ctypes.c_uint32), + ("ppEnabledExtensionNames", ctypes.c_void_p), + ("pEnabledFeatures", ctypes.c_void_p)] + + +class VkPhysicalDeviceProperties(ctypes.Structure): + # Only the head is read; the tail (limits, sparse properties: 532 bytes) + # just has to fit. + _fields_ = [("apiVersion", ctypes.c_uint32), + ("driverVersion", ctypes.c_uint32), + ("vendorID", ctypes.c_uint32), ("deviceID", ctypes.c_uint32), + ("deviceType", ctypes.c_uint32), + ("deviceName", ctypes.c_char * 256), + ("pipelineCacheUUID", ctypes.c_uint8 * 16), + ("tail", ctypes.c_uint64 * 128)] + + +class VkPhysicalDeviceProperties2(ctypes.Structure): + _fields_ = [("sType", ctypes.c_uint32), ("pNext", ctypes.c_void_p), + ("properties", VkPhysicalDeviceProperties)] + + +class VkPhysicalDeviceDriverProperties(ctypes.Structure): + _fields_ = [("sType", ctypes.c_uint32), ("pNext", ctypes.c_void_p), + ("driverID", ctypes.c_uint32), + ("driverName", ctypes.c_char * 256), + ("driverInfo", ctypes.c_char * 256), + ("conformanceVersion", ctypes.c_uint8 * 4)] + + +def child_vulkan(): + """Create a device on every GPU of the driver named in $MESA_SMOKE_ICD. + + Without the loader the driver is asked for its entry points directly, + through the interface the loader itself uses (vk_icdGetInstanceProcAddr). + """ + driver = ctypes.CDLL(os.environ["MESA_SMOKE_ICD"]) + negotiated = ctypes.c_uint32(5) + if hasattr(driver, "vk_icdNegotiateLoaderICDInterfaceVersion"): + driver.vk_icdNegotiateLoaderICDInterfaceVersion( + ctypes.byref(negotiated)) + gipa = driver.vk_icdGetInstanceProcAddr + gipa.restype = ctypes.c_void_p + gipa.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + + def function(instance, name, *argtypes): + address = gipa(instance, name) + if not address: + raise RuntimeError("no %s" % name.decode()) + return ctypes.CFUNCTYPE(ctypes.c_int32, *argtypes)(address) + + application = VkApplicationInfo(sType=0, pApplicationName=b"mesa-smoke-test", + apiVersion=(1 << 22) | (3 << 12)) + info = VkInstanceCreateInfo(sType=1, + pApplicationInfo=ctypes.pointer(application)) + instance = ctypes.c_void_p() + result = function(None, b"vkCreateInstance", ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p)( + ctypes.byref(info), None, ctypes.byref(instance)) + if result: + return {"error": "vkCreateInstance returned %d" % result} + enumerate_devices = function(instance, b"vkEnumeratePhysicalDevices", + ctypes.c_void_p, ctypes.c_void_p, + ctypes.c_void_p) + properties2 = function(instance, b"vkGetPhysicalDeviceProperties2", + ctypes.c_void_p, ctypes.c_void_p) + create_device = function(instance, b"vkCreateDevice", ctypes.c_void_p, + ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p) + destroy_device = function(instance, b"vkDestroyDevice", ctypes.c_void_p, + ctypes.c_void_p) + count = ctypes.c_uint32(16) + handles = (ctypes.c_void_p * 16)() + enumerate_devices(instance, ctypes.byref(count), handles) + devices = [] + for handle in handles[:count.value]: + driver_properties = VkPhysicalDeviceDriverProperties(sType=1000196000) + properties = VkPhysicalDeviceProperties2( + sType=1000059001, + pNext=ctypes.cast(ctypes.pointer(driver_properties), + ctypes.c_void_p)) + properties2(handle, ctypes.byref(properties)) + priority = ctypes.c_float(1) + queue = VkDeviceQueueCreateInfo(sType=2, queueCount=1, + pQueuePriorities=ctypes.pointer(priority)) + create = VkDeviceCreateInfo(sType=3, queueCreateInfoCount=1, + pQueueCreateInfos=ctypes.pointer(queue)) + device = ctypes.c_void_p() + result = create_device(handle, ctypes.byref(create), None, + ctypes.byref(device)) + if not result: + destroy_device(device, None) + api = properties.properties.apiVersion + devices.append({ + "name": properties.properties.deviceName.decode(), + "driver": "%s %s" % (driver_properties.driverName.decode(), + driver_properties.driverInfo.decode()), + "api": "%d.%d.%d" % (api >> 22, (api >> 12) & 0x3ff, api & 0xfff), + "create": result}) + return {"interface": negotiated.value, "devices": devices} + + +KERNEL = b""" +kernel void k(global float *out) { + size_t i = get_global_id(0); + float x = (float)i; + out[i] = sqrt(x) + sin(x) + clamp(x, 1.0f, 2.0f) + (float)popcount((uint)i); +} +""" + + +def child_opencl(): + """Build and run KERNEL on every device of the rusticl platform.""" + # By path: the /pkg python wrapper can put CUDA's lib/ on the library path, + # and the Khronos ICD loader there finds no rusticl on machdoch. + cl = ctypes.CDLL(OPENCL_LOADER) + c_void_p, c_uint, c_size_t = ctypes.c_void_p, ctypes.c_uint, ctypes.c_size_t + for name, restype in (("clCreateContext", c_void_p), + ("clCreateCommandQueueWithProperties", c_void_p), + ("clCreateProgramWithSource", c_void_p), + ("clCreateBuffer", c_void_p), + ("clCreateKernel", c_void_p)): + getattr(cl, name).restype = restype + cl.clGetDeviceIDs.argtypes = [c_void_p, ctypes.c_uint64, c_uint, c_void_p, + c_void_p] + cl.clGetPlatformInfo.argtypes = [c_void_p, c_uint, c_size_t, c_void_p, + c_void_p] + cl.clGetDeviceInfo.argtypes = [c_void_p, c_uint, c_size_t, c_void_p, + c_void_p] + cl.clCreateContext.argtypes = [c_void_p, c_uint, c_void_p, c_void_p, + c_void_p, c_void_p] + cl.clCreateCommandQueueWithProperties.argtypes = [c_void_p, c_void_p, + c_void_p, c_void_p] + cl.clCreateProgramWithSource.argtypes = [c_void_p, c_uint, c_void_p, + c_void_p, c_void_p] + cl.clBuildProgram.argtypes = [c_void_p, c_uint, c_void_p, c_void_p, + c_void_p, c_void_p] + cl.clGetProgramBuildInfo.argtypes = [c_void_p, c_void_p, c_uint, c_size_t, + c_void_p, c_void_p] + cl.clCreateBuffer.argtypes = [c_void_p, ctypes.c_uint64, c_size_t, + c_void_p, c_void_p] + cl.clCreateKernel.argtypes = [c_void_p, ctypes.c_char_p, c_void_p] + cl.clSetKernelArg.argtypes = [c_void_p, c_uint, c_size_t, c_void_p] + cl.clEnqueueNDRangeKernel.argtypes = [c_void_p, c_void_p, c_uint, c_void_p, + c_void_p, c_void_p, c_uint, c_void_p, + c_void_p] + cl.clEnqueueReadBuffer.argtypes = [c_void_p, c_void_p, c_uint, c_size_t, + c_size_t, c_void_p, c_uint, c_void_p, + c_void_p] + + def text(query, handle, key): + buffer = ctypes.create_string_buffer(4096) + query(handle, key, 4096, buffer, None) + return buffer.value.decode(errors="replace").strip() + + platforms = (c_void_p * 8)() + count = c_uint() + if cl.clGetPlatformIDs(8, platforms, ctypes.byref(count)) or not count.value: + return {"error": "no OpenCL platform"} + results = [] + for platform in platforms[:count.value]: + if text(cl.clGetPlatformInfo, platform, 0x0902) != "rusticl": + continue + devices = (c_void_p * 8)() + found = c_uint() + if cl.clGetDeviceIDs(platform, 0xFFFFFFFF, 8, devices, + ctypes.byref(found)): + found.value = 0 + for device in devices[:found.value]: + entry = {"name": text(cl.clGetDeviceInfo, device, 0x102B), + "version": text(cl.clGetDeviceInfo, device, 0x102F)} + results.append(entry) + handle = c_void_p(device) + error = ctypes.c_int() + context = cl.clCreateContext(None, 1, ctypes.byref(handle), None, + None, ctypes.byref(error)) + queue = cl.clCreateCommandQueueWithProperties( + context, device, None, ctypes.byref(error)) + source = ctypes.c_char_p(KERNEL) + program = cl.clCreateProgramWithSource( + context, 1, ctypes.byref(source), None, ctypes.byref(error)) + if cl.clBuildProgram(program, 1, ctypes.byref(handle), None, None, + None): + log = ctypes.create_string_buffer(8192) + cl.clGetProgramBuildInfo(program, device, 0x1183, 8192, log, + None) + entry["error"] = "build failed: " + \ + log.value.decode(errors="replace").strip() + continue + n = 1024 + buffer = cl.clCreateBuffer(context, 1 << 1, n * 4, None, + ctypes.byref(error)) + kernel = cl.clCreateKernel(program, b"k", ctypes.byref(error)) + memory = c_void_p(buffer) + cl.clSetKernelArg(kernel, 0, ctypes.sizeof(memory), + ctypes.byref(memory)) + work = c_size_t(n) + out = (ctypes.c_float * n)() + if cl.clEnqueueNDRangeKernel(queue, kernel, 1, None, + ctypes.byref(work), None, 0, None, + None) or \ + cl.clEnqueueReadBuffer(queue, buffer, 1, 0, n * 4, out, 0, None, + None): + entry["error"] = "running the kernel failed" + continue + wrong = 0 + for i in range(n): + want = math.sqrt(i) + math.sin(i) + min(max(i, 1), 2) + \ + bin(i).count("1") + if abs(out[i] - want) > 1e-3 * max(1, abs(want)): + wrong += 1 + entry["wrong"], entry["values"] = wrong, n + return {"devices": results} + + +CHILDREN = {"gl": child_gl, "vulkan": child_vulkan, "opencl": child_opencl} + + +# ----------------------------------------------------------------- checks + +def check_libraries(): + """Every Mesa library has to resolve its shared libraries. + + Starts from the entry points and follows the libgallium they load, rather + than globbing for it: with --prefix the overlay still shows the installed, + older libgallium-.so, which nothing uses any more. + """ + queue = sorted(glob.glob("/usr/lib/libvulkan_*.so") + + glob.glob("/usr/lib/dri/*_dri.so")[:1] + + ["/usr/lib/libEGL_mesa.so.0", "/usr/lib/libGLX_mesa.so.0", + "/usr/lib/libRusticlOpenCL.so.1"]) + checked, llvm, broken = [], set(), False + while queue: + library = queue.pop(0) + if library in checked: + continue + checked.append(library) + if not os.path.exists(library): + report("warn", "%s is not installed" % library) + continue + output = subprocess.run(["ldd", library], capture_output=True, + text=True).stdout + for line in output.splitlines(): + fields = line.split() + if "not found" in line: + report("fail", "%s: %s not found" % (library, fields[0])) + broken = True + elif fields and fields[0].startswith("libgallium-") and \ + len(fields) > 2: + queue.append(fields[2]) + elif fields and fields[0].startswith(("libLLVM.so", "libclang-cpp.so", + "libLLVMSPIRVLib.so")): + llvm.add(fields[0]) + if not broken: + report("ok", "%d Mesa libraries resolve their shared libraries, using %s" + % (len(checked), ", ".join(sorted(llvm)) or "no LLVM")) + + +def check_libclc(): + """Mesa's libclc fork, which mesalib finds through mesa-libclc.pc.""" + pc = "/usr/share/pkgconfig/mesa-libclc.pc" + if not os.path.exists(pc): + report("warn", "mesa-libclc is not installed (no %s)" % pc) + return + with open(pc) as pkgconfig: + variables = dict(line.strip().split("=", 1) for line in pkgconfig + if "=" in line and ":" not in line.split("=")[0]) + directory = variables.get("libexecdir", "") + missing = [name for name in ("spirv-mesa3d-.spv", "spirv64-mesa3d-.spv") + if not os.path.exists(os.path.join(directory, name))] + if missing: + report("fail", "mesa-libclc: %s missing in %s" + % (", ".join(missing), directory)) + else: + report("ok", "mesa-libclc provides its SPIR-V libraries in %s" + % directory) + + +def check_gl(): + environment = {"__EGL_VENDOR_LIBRARY_FILENAMES": EGL_VENDOR, + "EGL_PLATFORM": "surfaceless"} + for label, extra in (("software", {"LIBGL_ALWAYS_SOFTWARE": "1"}), + ("hardware", {})): + result, stderr = child("gl", dict(environment, **extra)) + if not result or "error" in result: + state = "fail" if label == "software" else "warn" + report(state, "OpenGL ES (%s): %s" + % (label, result["error"] if result else stderr)) + continue + name = "%s, %s" % (result["renderer"], result["version"]) + if label == "hardware" and "llvmpipe" in result["renderer"]: + report("warn", "OpenGL ES (hardware): no hardware driver, got %s" + " (no render node?)" % name) + elif result["wrong"]: + report("fail", "OpenGL ES (%s) %s: %d of %d pixels wrong" + % (label, name, result["wrong"], result["pixels"])) + else: + report("ok", "OpenGL ES (%s) draws a shaded triangle: %s" + % (label, name)) + + +def start_xvfb(): + """Return (display, process) for a private X server, or (None, None).""" + if not shutil.which("Xvfb") or not shutil.which("xdpyinfo"): + return None, None + for number in range(77, 97): + if os.path.exists("/tmp/.X%d-lock" % number): + continue + display = ":%d" % number + process = subprocess.Popen( + ["Xvfb", display, "-nolisten", "tcp", "-screen", "0", "640x480x24"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + for _ in range(50): + time.sleep(.2) + if process.poll() is not None: + break + if subprocess.run(["xdpyinfo", "-display", display], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL).returncode == 0: + return display, process + process.kill() + return None, None + + +def check_glx(): + if not shutil.which("glxinfo"): + report("warn", "GLX: no glxinfo") + return + display, xvfb = start_xvfb() + if not display: + report("warn", "GLX: cannot start Xvfb") + return + try: + environment = dict(os.environ, DISPLAY=display, + __GLX_VENDOR_LIBRARY_NAME="mesa") + output = subprocess.run(["glxinfo", "-B"], env=environment, + capture_output=True, text=True, + timeout=60).stdout + finally: + xvfb.terminate() + xvfb.wait() + fields = dict(line.strip().split(": ", 1) for line in output.splitlines() + if ": " in line) + renderer = fields.get("OpenGL renderer string") + if fields.get("direct rendering") == "Yes" and renderer: + report("ok", "GLX on Xvfb: %s, %s" + % (renderer, fields.get("OpenGL core profile version string"))) + else: + report("fail", "GLX on Xvfb: no direct rendering context\n%s" % output) + + +def vulkan_drivers(): + """Mesa's Vulkan drivers, from the ICD manifests.""" + drivers = {} + for directory in VULKAN_ICD_DIRS: + for manifest in sorted(glob.glob(directory + "/*.json")): + try: + with open(manifest) as file: + library = json.load(file)["ICD"]["library_path"] + except (OSError, ValueError, KeyError) as error: + report("fail", "%s: %s" % (manifest, error)) + continue + if os.path.basename(library).startswith("libvulkan_"): + drivers[os.path.basename(library)] = library + return drivers + + +def check_vulkan(): + drivers = vulkan_drivers() + if not drivers: + report("warn", "Vulkan: no Mesa driver installed") + return + devices = 0 + for name, library in sorted(drivers.items()): + result, stderr = child("vulkan", {"MESA_SMOKE_ICD": library}) + if not result or "error" in result: + report("fail", "Vulkan %s: %s" + % (name, result["error"] if result else stderr)) + continue + if not result["devices"]: + report("ok", "Vulkan %s: loads, drives no GPU of this host" % name) + for device in result["devices"]: + devices += 1 + line = "Vulkan %s: %s, %s, Vulkan %s" % ( + name, device["name"], device["driver"], device["api"]) + if device["create"]: + report("fail", "%s: vkCreateDevice returned %d" + % (line, device["create"])) + else: + report("ok", line + ", device created") + if not devices: + report("warn", "Vulkan: no GPU of this host is driven by Mesa") + + +def check_opencl(drivers): + if not os.path.exists(RUSTICL_ICD): + report("fail", "OpenCL: %s is not installed" % RUSTICL_ICD) + return + result, stderr = child("opencl", {"OCL_ICD_VENDORS": RUSTICL_ICD, + "RUSTICL_ENABLE": drivers}) + if not result or "error" in result: + report("fail", "OpenCL: %s" % (result["error"] if result else stderr)) + return + if LIBCLC_MISSING in stderr: + report("fail", "OpenCL: rusticl finds no libclc") + elif LIBCLC_UPSTREAM in stderr: + report("warn", "OpenCL: rusticl uses upstream libclc, not Mesa's fork" + " (install mesa-libclc)") + if not result["devices"]: + report("warn", "OpenCL: rusticl offers no device for %s" % drivers) + for device in result["devices"]: + line = "OpenCL %s (%s)" % (device["name"], device["version"]) + if "error" in device: + report("fail", "%s: %s" % (line, device["error"])) + elif device["wrong"]: + report("fail", "%s: %d of %d results wrong" + % (line, device["wrong"], device["values"])) + else: + report("ok", "%s runs a kernel calling libclc builtins" % line) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--prefix", action="append", default=[], + metavar="DIR|PACKAGE", + help="test the image directory DIR of a build, or the" + " .bee.tar.bz2 PACKAGE, instead of what is" + " installed; may be repeated") + parser.add_argument("--rusticl-enable", default="llvmpipe,iris,radeonsi", + metavar="DRIVERS", + help="RUSTICL_ENABLE, the Gallium drivers to offer as" + " OpenCL devices (default: %(default)s)") + parser.add_argument("--child", choices=sorted(CHILDREN), + help=argparse.SUPPRESS) + arguments = parser.parse_args() + + if arguments.child: + print(json.dumps(CHILDREN[arguments.child]())) + return 0 + + if arguments.prefix and PREFIX_VARIABLE not in os.environ: + workdir = tempfile.mkdtemp(prefix="mesa-smoke-") + try: + return run_in_overlay(unpack(arguments.prefix, workdir)) + finally: + shutil.rmtree(workdir, ignore_errors=True) + if PREFIX_VARIABLE in os.environ: + print(" testing, over /usr and /etc: %s" + % os.environ[PREFIX_VARIABLE].replace(":", " ")) + + check_libraries() + check_libclc() + check_gl() + check_glx() + check_vulkan() + check_opencl(arguments.rusticl_enable) + + print("%s (%d failed, %d warned)" + % ("FAIL" if status["fail"] else "PASS", status["fail"], + status["warn"])) + return 1 if status["fail"] else 0 + + +if __name__ == "__main__": + sys.exit(main()) From 47a18e6d6b7c394cfb5fbdbceca8d42e7fba0d64 Mon Sep 17 00:00:00 2001 From: Paul Menzel Date: Wed, 14 Oct 2026 18:20:15 +0200 Subject: [PATCH 12/12] smoke-tests: Draw with OpenGL ES on every EGL device MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a host with two GPUs, the hardware OpenGL ES check draws only on the one EGL’s surfaceless platform picks by default. On maleficent that is the Intel GPU, so radeonsi goes unchecked unless the caller knows to set DRI_PRIME=1: $ ./scripts/smoke-tests/mesa-smoke-test OK OpenGL ES (software) draws a shaded triangle: llvmpipe (LLVM 23.1.3, 256 bits), OpenGL ES 3.2 Mesa 26.2.4 OK OpenGL ES (hardware) draws a shaded triangle: Mesa Intel(R) HD Graphics 630 (KBL GT2), OpenGL ES 3.2 Mesa 26.2.4 $ DRI_PRIME=1 ./scripts/smoke-tests/mesa-smoke-test OK OpenGL ES (software) draws a shaded triangle: llvmpipe (LLVM 23.1.3, 256 bits), OpenGL ES 3.2 Mesa 26.2.4 OK OpenGL ES (hardware) draws a shaded triangle: AMD Radeon Pro WX 5100 Graphics (radeonsi, polaris10, ACO, DRM 3.64, 6.18.51.mx64.498), OpenGL ES 3.2 Mesa 26.2.4 So list the devices with EGL_EXT_device_enumeration and draw on each through EGL_EXT_platform_device, as the Vulkan and OpenCL checks already do for their devices. Mesa’s software device replaces LIBGL_ALWAYS_SOFTWARE for llvmpipe, and each GPU is reported with its render node. A GPU that cannot be set up (no display, config or context) is still only a warning, as Mesa need not have a driver for it, for example for an NVIDIA GPU on its proprietary driver. Once the context is current, a failure on a GPU (shader compilation, drawing, wrong pixels) now fails the run instead of warning, as it is the driver that is broken then. Assisted-by: claude-opus-5.5 (Claude Code) --- scripts/smoke-tests/mesa-smoke-test | 109 +++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 20 deletions(-) diff --git a/scripts/smoke-tests/mesa-smoke-test b/scripts/smoke-tests/mesa-smoke-test index cc27884b2..4017ef51c 100755 --- a/scripts/smoke-tests/mesa-smoke-test +++ b/scripts/smoke-tests/mesa-smoke-test @@ -16,9 +16,9 @@ # breaking the build. So this checks what a desktop and a compute user would hit: # # - every Mesa library resolves its shared libraries (ldd), -# - OpenGL ES with the software rasterizer (llvmpipe) and with the hardware -# driver, each by drawing a shaded triangle into a framebuffer and comparing -# the pixels; llvmpipe compiles that shader with LLVM's JIT, +# - OpenGL ES on every device EGL lists, the software rasterizer (llvmpipe) +# and each GPU, by drawing a shaded triangle into a framebuffer and +# comparing the pixels; llvmpipe compiles that shader with LLVM's JIT, # - GLX on an Xvfb of its own, the path Xfce takes, # - each Mesa Vulkan driver: create an instance and a device on every GPU it # drives, talking to the driver directly, as hosts need not have the loader, @@ -144,8 +144,56 @@ def child(kind, environment=None, timeout=300): ["exit status %d" % process.returncode])[-1] +def egl_devices(egl): + """Return EGL's devices (EGL_EXT_device_enumeration), or raise.""" + egl.eglQueryString.restype = ctypes.c_char_p + egl.eglQueryString.argtypes = [ctypes.c_void_p, ctypes.c_int] + extensions = (egl.eglQueryString(None, 0x3055) or b"").split() # EXTENSIONS + for extension in (b"EGL_EXT_device_enumeration", b"EGL_EXT_platform_device"): + if extension not in extensions: + raise RuntimeError("EGL lacks %s" % extension.decode()) + egl.eglGetProcAddress.restype = ctypes.c_void_p + egl.eglGetProcAddress.argtypes = [ctypes.c_char_p] + query_devices = ctypes.CFUNCTYPE( + ctypes.c_uint, ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)( + egl.eglGetProcAddress(b"eglQueryDevicesEXT")) + devices, count = (ctypes.c_void_p * 16)(), ctypes.c_int() + if not query_devices(16, devices, ctypes.byref(count)): + raise RuntimeError("eglQueryDevicesEXT failed") + return devices[:count.value] + + +def child_egl_devices(): + """List EGL's devices: the software one, and a DRM node for each GPU.""" + egl = ctypes.CDLL("libEGL.so.1") + try: + devices = egl_devices(egl) + except RuntimeError as error: + return {"error": str(error)} + query_string = ctypes.CFUNCTYPE(ctypes.c_char_p, ctypes.c_void_p, + ctypes.c_int)( + egl.eglGetProcAddress(b"eglQueryDeviceStringEXT")) + result = [] + for index, device in enumerate(devices): + extensions = (query_string(device, 0x3055) or b"").split() + node = None + if b"EGL_EXT_device_drm_render_node" in extensions: + node = query_string(device, 0x3377) # DRM_RENDER_NODE_FILE_EXT + if not node and b"EGL_EXT_device_drm" in extensions: + node = query_string(device, 0x3233) # DRM_DEVICE_FILE_EXT + result.append({"index": index, + "software": b"EGL_MESA_device_software" in extensions, + "node": node.decode() if node else None}) + return {"devices": result} + + def child_gl(): - """Draw a shaded triangle with OpenGL ES 3 on EGL's surfaceless platform.""" + """Draw a shaded triangle with OpenGL ES 3 on EGL device + $MESA_SMOKE_EGL_DEVICE (EGL_EXT_platform_device), into a framebuffer object. + + An error before the context is current carries "setup", as it may just + mean that Mesa has no driver for the device. + """ c_int, c_uint, c_void_p = ctypes.c_int, ctypes.c_uint, ctypes.c_void_p egl = ctypes.CDLL("libEGL.so.1") gl = ctypes.CDLL("libGLESv2.so.2") @@ -168,24 +216,29 @@ def child_gl(): c_void_p] gl.glBindAttribLocation.argtypes = [c_uint, c_uint, ctypes.c_char_p] - def fail(message): + def fail(message, setup=False): return {"error": "%s (EGL error 0x%x, GL error 0x%x)" - % (message, egl.eglGetError(), gl.glGetError())} + % (message, egl.eglGetError(), gl.glGetError()), + "setup": setup} - display = egl.eglGetPlatformDisplay(0x31DD, None, None) # SURFACELESS_MESA + try: + device = egl_devices(egl)[int(os.environ["MESA_SMOKE_EGL_DEVICE"])] + except (RuntimeError, IndexError) as error: + return fail("no such EGL device (%s)" % error, setup=True) + display = egl.eglGetPlatformDisplay(0x313F, device, None) # DEVICE_EXT if not display or not egl.eglInitialize(display, None, None): - return fail("eglInitialize failed on the surfaceless platform") + return fail("eglInitialize failed", setup=True) # EGL_SURFACE_TYPE 0: any config, the context renders into an FBO. attributes = (c_int * 5)(0x3033, 0, 0x3040, 0x0040, 0x3038) config, count = c_void_p(), c_int() if not egl.eglChooseConfig(display, attributes, ctypes.byref(config), 1, ctypes.byref(count)) or count.value < 1: - return fail("no OpenGL ES 3 config") + return fail("no OpenGL ES 3 config", setup=True) egl.eglBindAPI(0x30A0) # EGL_OPENGL_ES_API context = egl.eglCreateContext(display, config, None, (c_int * 3)(0x3098, 3, 0x3038)) if not context or not egl.eglMakeCurrent(display, None, None, context): - return fail("cannot make an OpenGL ES 3 context current") + return fail("cannot make an OpenGL ES 3 context current", setup=True) size = 64 framebuffer, renderbuffer = c_uint(), c_uint() @@ -497,7 +550,8 @@ def child_opencl(): return {"devices": results} -CHILDREN = {"gl": child_gl, "vulkan": child_vulkan, "opencl": child_opencl} +CHILDREN = {"egl-devices": child_egl_devices, "gl": child_gl, + "vulkan": child_vulkan, "opencl": child_opencl} # ----------------------------------------------------------------- checks @@ -561,20 +615,35 @@ def check_libclc(): def check_gl(): - environment = {"__EGL_VENDOR_LIBRARY_FILENAMES": EGL_VENDOR, - "EGL_PLATFORM": "surfaceless"} - for label, extra in (("software", {"LIBGL_ALWAYS_SOFTWARE": "1"}), - ("hardware", {})): - result, stderr = child("gl", dict(environment, **extra)) + """Draw on every EGL device; DRI_PRIME would pick only one GPU.""" + environment = {"__EGL_VENDOR_LIBRARY_FILENAMES": EGL_VENDOR} + listing, stderr = child("egl-devices", environment) + if not listing or "error" in listing: + report("fail", "OpenGL ES: cannot list EGL devices: %s" + % (listing["error"] if listing else stderr)) + return + devices = listing["devices"] + if not any(device["software"] for device in devices): + report("fail", "OpenGL ES: EGL lists no software device (llvmpipe)") + if not any(not device["software"] for device in devices): + report("warn", "OpenGL ES (hardware): EGL lists no GPU" + " (no render node?)") + for device in devices: + label = ("software" if device["software"] else "hardware, %s" + % (device["node"] or "EGL device %d" % device["index"])) + result, stderr = child("gl", dict(environment, MESA_SMOKE_EGL_DEVICE= + str(device["index"]))) if not result or "error" in result: - state = "fail" if label == "software" else "warn" + # A GPU Mesa has no driver for, like NVIDIA's with its own driver. + setup = result and result.get("setup") + state = "warn" if setup and not device["software"] else "fail" report(state, "OpenGL ES (%s): %s" % (label, result["error"] if result else stderr)) continue name = "%s, %s" % (result["renderer"], result["version"]) - if label == "hardware" and "llvmpipe" in result["renderer"]: - report("warn", "OpenGL ES (hardware): no hardware driver, got %s" - " (no render node?)" % name) + if not device["software"] and "llvmpipe" in result["renderer"]: + report("warn", "OpenGL ES (%s): no hardware driver, got %s" + % (label, name)) elif result["wrong"]: report("fail", "OpenGL ES (%s) %s: %d of %d pixels wrong" % (label, name, result["wrong"], result["pixels"]))