[Fix] (CI): merge Arch WSL runner and prefer it over hosted runners

This commit is contained in:
2026-10-05 09:16:06 -04:00
7 changed files with 573 additions and 273 deletions
@@ -0,0 +1,33 @@
name: Configure Test runner
description: Select the installed compiler and graphics stack without modifying the host
runs:
using: composite
steps:
- shell: bash
run: |
if [ "$RUNNER_ENVIRONMENT" = "self-hosted" ]; then
for tool in clang clang++ cmake ninja ccache; do command -v "$tool"; done
test -r /usr/share/vulkan/icd.d/dzn_icd.json
test -e /dev/dxg
{
echo 'MOBILEGL_CI_CC=clang'
echo 'MOBILEGL_CI_CXX=clang++'
echo 'MOBILEGL_CI_LLD=ld.lld'
echo 'MOBILEGL_CI_SOFTWARE_RENDERING=OFF'
echo 'VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/dzn_icd.json'
echo 'VK_DRIVER_FILES=/usr/share/vulkan/icd.d/dzn_icd.json'
echo 'EGL_PLATFORM=surfaceless'
echo 'LIBGL_ALWAYS_SOFTWARE=0'
echo 'GALLIUM_DRIVER=d3d12'
echo 'MESA_LOADER_DRIVER_OVERRIDE=d3d12'
} >> "$GITHUB_ENV"
else
{
echo 'MOBILEGL_CI_CC=clang-20'
echo 'MOBILEGL_CI_CXX=clang++-20'
echo 'MOBILEGL_CI_LLD=ld.lld-20'
echo 'MOBILEGL_CI_SOFTWARE_RENDERING=ON'
echo 'VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json'
echo 'VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json'
} >> "$GITHUB_ENV"
fi
+260 -264
View File
File diff suppressed because it is too large Load Diff
+138
View File
@@ -0,0 +1,138 @@
# MiniPC-8845 Arch WSL GPU Runner
Configuration recorded on 2026-10-05.
## Host
| Component | Configuration |
| --- | --- |
| Hostname | `MiniPC-8845` |
| Windows build | `10.0.26200.9445` |
| GPU | AMD Radeon 780M Graphics |
| Windows GPU driver | `32.0.12033.1030` |
| WSL version | `2.7.3.0` |
| Distribution | `archlinux`, Arch Linux on WSL 2 |
| Linux kernel | `6.6.114.1-microsoft-standard-WSL2` |
| Init system | systemd |
| WSL networking | Mirrored |
| GPU access | `/dev/dxg` and `/usr/lib/wsl/lib` |
## GitHub Actions Runner
| Setting | Value |
| --- | --- |
| Repository | `MobileGL-Dev/MobileGL` |
| Runner name | `minipc-8845-arch-wsl-gpu` |
| Runner version | `2.337.0`, automatic updates enabled |
| Labels | `self-hosted`, `Linux`, `X64`, `arch`, `wsl`, `gpu`, `gles`, `vulkan`, `d3d12` |
| Concurrent jobs | 1 |
| Linux account | `github-runner`, UID 1001, no sudo configuration |
| Installation directory | `/opt/actions-runner/mobilegl` |
| Work directory | `/opt/actions-runner/mobilegl/_work` |
| Environment file | `/etc/mobilegl-runner/gpu.env` |
Workflow runner selector:
```yaml
runs-on: [self-hosted, Linux, X64, arch, wsl, gpu, gles, vulkan]
```
## Graphics
| Component | Configuration |
| --- | --- |
| Mesa | `26.2.1-arch1.1` |
| OpenGL / GLES driver | Mesa D3D12 |
| Renderer | `D3D12 (AMD Radeon 780M Graphics)` |
| EGL | 1.5 |
| OpenGL | 4.6 |
| OpenGL ES | 3.1 |
| Vulkan driver | Mesa Dozen, translating Vulkan to D3D12 |
| Vulkan device API | 1.2.354 |
| Vulkan ICD | `/usr/share/vulkan/icd.d/dzn_icd.json` |
| Default EGL platform | Surfaceless |
Dozen reports a non-conformant Vulkan implementation. The exposed capabilities
are GLES 3.1 and Vulkan 1.2; GLES 3.2 and full Vulkan conformance are not available.
The runner service selects the Radeon GPU with these environment variables:
```sh
DISPLAY=:0
WAYLAND_DISPLAY=wayland-0
XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir
LD_LIBRARY_PATH=/usr/lib/wsl/lib
MESA_LOADER_DRIVER_OVERRIDE=d3d12
GALLIUM_DRIVER=d3d12
MESA_D3D12_DEFAULT_ADAPTER_NAME="AMD Radeon 780M Graphics"
VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/dzn_icd.json
EGL_PLATFORM=surfaceless
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
```
The installed GPU probe is `/usr/local/libexec/mobilegl-runner/gpu-smoke`.
Its GLES source is `/usr/local/libexec/mobilegl-runner/gles-smoke.c`, and the
recorded verification log is `/var/log/mobilegl-runner/gpu-smoke.log`.
## Build Tools
Installed tools include GCC, Clang 22, CMake, Ninja, ccache, LLD, libc++, Git LFS,
GitHub CLI, and EGL/GLES/Vulkan development headers.
## Service and Startup
| Setting | Value |
| --- | --- |
| systemd unit | `actions.runner.MobileGL-Dev-MobileGL.minipc-8845-arch-wsl-gpu.service` |
| Service drop-in | `/etc/systemd/system/actions.runner.MobileGL-Dev-MobileGL.minipc-8845-arch-wsl-gpu.service.d/gpu.conf` |
| Service startup | Enabled at WSL systemd boot |
| Restart policy | `Restart=always`, `RestartSec=15` |
| Startup checks | GPU device access and WSLg X11/Wayland sockets |
| Windows scheduled task | `MobileGL Arch WSL Runner` |
| Task trigger | Windows login by `Swung0x48` |
| Task behavior | Starts WSL in a hidden window and keeps it running |
| Task execution limit | Unlimited |
| Task retry policy | Every minute, up to 999 retries |
Automatic startup requires the Windows user to log in. The runner cannot execute
jobs while the host is asleep.
## Network
The runner uses the Windows Clash proxy through WSL mirrored networking:
```sh
http_proxy=http://127.0.0.1:7897
https_proxy=http://127.0.0.1:7897
no_proxy=localhost,127.0.0.1,::1
```
Clash must be running for the runner's configured GitHub connection to work.
## Main Test workflow
`test.yml` uses a small GitHub-hosted `select-runner` job to query this repository's
runner inventory. Every subsequent job uses its single decision, so artifacts built
against Arch's compiler/libc/Mesa cannot accidentally be consumed by Ubuntu jobs.
An online runner is preferred even when busy. Offline, unregistered, or mismatched
labels select `ubuntu-latest` for the whole run. Test failures do not cause fallback.
Configure repository Actions secret `RUNNER_STATUS_TOKEN` with a fine-grained PAT
restricted to this repository and **Administration: Read-only**. The ordinary
`GITHUB_TOKEN` cannot generally list repository runners. A missing/expired token or
API failure stops selection with an error: unknown availability is not treated as
an offline machine. The token is passed only to the GitHub-hosted selector.
Availability is sampled at workflow start. If the machine disconnects after
selection, GitHub queues its remaining jobs; rerunning the workflow while it is
offline chooses Ubuntu. GitHub Actions does not migrate already scheduled jobs
between runner types. Keeping one platform per run avoids incompatible artifacts.
The local composite setup action uses Arch's installed Clang/CMake and Dozen ICD.
Ubuntu package installation, swap provisioning, SDK download, and privileged core
pattern changes are skipped on this no-sudo host. Compiler caches are separated by
runner pool. Trace tests disable forced software-rendering environment overrides
on this machine; the Ubuntu fallback retains its existing Mesa software settings.
Hardware capability differences remain visible as test results, not fallback triggers.
The dedicated `wsl-gpu-smoke.yml` workflow has been removed; the host's installed
probe remains available to its operator.
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Choose one execution platform for the entire Test artifact dependency graph."""
import json
import os
import sys
import urllib.error
import urllib.request
NAME = "minipc-8845-arch-wsl-gpu"
LABELS = ["self-hosted", "Linux", "X64", "arch", "wsl", "gpu", "gles", "vulkan"]
def choose(runners):
for runner in runners:
labels = {label["name"].lower() for label in runner.get("labels", [])}
if runner.get("name") == NAME and set(map(str.lower, LABELS)) <= labels:
if runner.get("status") == "online":
# Busy is still available: queue behind its current job.
return LABELS, "arch-wsl", "preferred runner online (busy=%s)" % runner.get("busy", False)
return ["ubuntu-latest"], "github", "preferred runner offline, absent, or missing required labels"
def runner_inventory(repository, token):
if not token:
raise ValueError("Set RUNNER_STATUS_TOKEN with repository Administration: read; runner availability is unknown")
runners = []
page = 1
while True:
url = f"https://api.github.com/repos/{repository}/actions/runners?per_page=100&page={page}"
request = urllib.request.Request(url, headers={
"Authorization": "Bearer " + token,
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "MobileGL-test-runner-selector",
})
with urllib.request.urlopen(request, timeout=20) as response:
data = json.load(response)
batch = data["runners"]
runners.extend(batch)
if len(batch) < 100:
return runners
page += 1
def main():
try:
runners = runner_inventory(os.environ["GITHUB_REPOSITORY"], os.environ.get("RUNNER_STATUS_TOKEN", ""))
labels, pool, reason = choose(runners)
except (ValueError, KeyError, OSError, urllib.error.URLError) as error:
# An API/auth error is not evidence that the machine is offline. Do not
# silently send the whole matrix to GitHub when the status cannot be read.
print(f"::error::Cannot determine runner availability: {error}", file=sys.stderr)
return 1
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
output.write("runner=" + json.dumps(labels) + "\npool=" + pool + "\n")
print(f"Test runner: {pool}: {reason}")
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -44,7 +44,7 @@ def run_arm(build, out, case, forward):
if Path('/usr/share/glvnd/egl_vendor.d/50_mesa.json').exists():
env['__EGL_VENDOR_LIBRARY_FILENAMES'] = '/usr/share/glvnd/egl_vendor.d/50_mesa.json'
if Path('/usr/share/vulkan/icd.d/lvp_icd.json').exists():
env['VK_ICD_FILENAMES'] = '/usr/share/vulkan/icd.d/lvp_icd.json'
env.setdefault('VK_ICD_FILENAMES', '/usr/share/vulkan/icd.d/lvp_icd.json')
peer_env = dict(env, MOBILEGL_IPC_ROLE='server', MOBILEGL_IPC_DIAL='no',
MOBILEGL_IPC_LOG_FORWARD=str(int(forward)), MOBILEGL_LOG_FILE_PATH=str(out / 'peer.log'))
for name in ('MOBILEGL_TRANSPORT', 'MOBILEGL_IPC_SERVER_PATH', 'MOBILEGL_IPC_RING_MB', 'MOBILEGL_IPC_STAGE_MB'):
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
import contextlib
import importlib.util
import io
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
import urllib.error
spec = importlib.util.spec_from_file_location("selector", Path(__file__).with_name("select_test_runner.py"))
selector = importlib.util.module_from_spec(spec)
spec.loader.exec_module(selector)
def runner(status="online", busy=False):
return {"name": selector.NAME, "status": status, "busy": busy,
"labels": [{"name": label} for label in selector.LABELS]}
class SelectRunnerTest(unittest.TestCase):
def test_online_busy_and_idle_both_use_preferred(self):
for busy in (False, True):
labels, pool, _ = selector.choose([runner(busy=busy)])
self.assertEqual(labels, selector.LABELS)
self.assertEqual(pool, "arch-wsl")
def test_offline_or_absent_falls_back(self):
for rows in ([], [runner("offline")]):
self.assertEqual(selector.choose(rows)[:2], (["ubuntu-latest"], "github"))
def test_wrong_runner_or_missing_labels_cannot_queue_forever(self):
wrong = runner()
wrong["name"] = "another-machine"
missing = runner()
missing["labels"] = missing["labels"][:-1]
for rows in ([wrong], [missing]):
self.assertEqual(selector.choose(rows)[1], "github")
def test_inventory_reads_later_pages(self):
pages = [io.StringIO(json.dumps({"runners": [{}] * 100})),
io.StringIO(json.dumps({"runners": [runner()]}))]
with patch.object(selector.urllib.request, "urlopen", side_effect=pages) as request:
rows = selector.runner_inventory("owner/repo", "not-a-real-token")
self.assertEqual(len(rows), 101)
self.assertTrue(request.call_args.args[0].full_url.endswith("page=2"))
def test_auth_failure_does_not_masquerade_as_offline(self):
for error in (ValueError("missing token"), urllib.error.HTTPError("url", 403, "Forbidden", {}, None)):
with patch.dict(os.environ, {"GITHUB_REPOSITORY": "owner/repo"}), \
patch.object(selector, "runner_inventory", side_effect=error), \
contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(selector.main(), 1)
def test_outputs_are_valid_runs_on_json(self):
with tempfile.TemporaryDirectory() as root:
path = Path(root) / "outputs"
with patch.dict(os.environ, {"GITHUB_REPOSITORY": "owner/repo", "GITHUB_OUTPUT": str(path)}), \
patch.object(selector, "runner_inventory", return_value=[runner()]), \
contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(selector.main(), 0)
values = dict(line.split("=", 1) for line in path.read_text().splitlines())
self.assertEqual(json.loads(values["runner"]), selector.LABELS)
if __name__ == "__main__":
unittest.main()
+11 -8
View File
@@ -5,6 +5,8 @@ if(NOT UNIX)
return()
endif()
option(MOBILEGL_TRACE_SOFTWARE_RENDERING "Force Mesa software rendering for trace tests" ON)
set(MOBILEGL_TRACE_ROOT "${CMAKE_CURRENT_LIST_DIR}")
set(MOBILEGL_TRACE_SHARED_CPP_DIR "${CMAKE_SOURCE_DIR}/android-plugin/app/src/trace/cpp")
set(APITRACE_ROOT "${CMAKE_SOURCE_DIR}/3rdparty/apitrace")
@@ -445,15 +447,16 @@ function(add_trace_replay_test CASE_NAME BACKEND)
set(trace_case_labels "retrace\;retrace-${TRACE_CASE_VARIANT}")
string(TOLOWER "${trace_case_labels}" trace_case_labels)
endif()
if(BACKEND STREQUAL "DirectGLES")
set_tests_properties(${trace_case_name} PROPERTIES
ENVIRONMENT "EGL_PLATFORM=surfaceless;LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330"
LABELS "${trace_case_labels}")
else()
set_tests_properties(${trace_case_name} PROPERTIES
ENVIRONMENT "LIBGL_ALWAYS_SOFTWARE=1;MESA_GL_VERSION_OVERRIDE=3.3;MESA_GLSL_VERSION_OVERRIDE=330"
LABELS "${trace_case_labels}")
set(trace_case_environment "")
if(MOBILEGL_TRACE_SOFTWARE_RENDERING)
list(APPEND trace_case_environment LIBGL_ALWAYS_SOFTWARE=1
MESA_GL_VERSION_OVERRIDE=3.3 MESA_GLSL_VERSION_OVERRIDE=330)
endif()
if(BACKEND STREQUAL "DirectGLES")
list(APPEND trace_case_environment EGL_PLATFORM=surfaceless)
endif()
set_tests_properties(${trace_case_name} PROPERTIES
ENVIRONMENT "${trace_case_environment}" LABELS "${trace_case_labels}")
if(TRACE_CASE_IPC_CONTROL)
set_tests_properties(${trace_case_name} PROPERTIES RESOURCE_LOCK mobilegl-tcp-trace)
endif()