From c49b4a30133da55d74ed4f3f5fa81e58c27d79f0 Mon Sep 17 00:00:00 2001 From: Alex Garcia Gil Date: Sat, 29 Aug 2026 17:58:15 +1200 Subject: [PATCH 1/4] feat(angle): add desktop ANGLE runtime builds Co-Authored-By: Claude --- .github/workflows/build-angle.yml | 172 ++++++++++++++++++ .gitignore | 3 + AGENTS.md | 4 +- CLAUDE.md | 3 + README.md | 10 +- build-angle.py | 289 ++++++++++++++++++++++++++++++ tests/test-angle.cpp | 188 +++++++++++++++++++ 7 files changed, 665 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/build-angle.yml create mode 100644 build-angle.py create mode 100644 tests/test-angle.cpp diff --git a/.github/workflows/build-angle.yml b/.github/workflows/build-angle.yml new file mode 100644 index 0000000..e34679b --- /dev/null +++ b/.github/workflows/build-angle.yml @@ -0,0 +1,172 @@ +name: Build ANGLE +run-name: Build ANGLE +on: + pull_request: + paths: + - '.github/workflows/build-angle.yml' + - 'build-angle.py' + - 'tests/test-angle.cpp' + workflow_dispatch: + inputs: + angle_revision: + description: 'ANGLE git revision to build' + required: false + type: string + default: '107da744f62a319b3c6851694740d9ebf247d048' + release_suffix: + description: 'Build suffix for the release tag. Defaults to the run number.' + required: false + type: string + default: '' + skip_release: + description: 'Skip creating release' + required: false + type: boolean + default: false + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + ANGLE_REVISION: ${{ inputs.angle_revision || '107da744f62a319b3c6851694740d9ebf247d048' }} + +jobs: + build-angle: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + platform: linux + arch: x64 + - os: ubuntu-24.04-arm + platform: linux + arch: arm64 + - os: macos-15 + platform: mac + arch: arm64 + - os: macos-15-intel + platform: mac + arch: x64 + + runs-on: ${{ matrix.os }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install Linux build dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libdrm-dev \ + libgbm-dev \ + libwayland-dev \ + libwayland-egl-backend-dev \ + libx11-dev \ + libx11-xcb-dev \ + libxcb1-dev \ + libxext-dev \ + libxfixes-dev \ + libxi-dev \ + libxrandr-dev \ + mesa-vulkan-drivers \ + pkg-config + + - name: Build ANGLE + run: | + python3 build-angle.py ${{ matrix.platform }} \ + -archs "${{ matrix.arch }}" \ + -revision "$ANGLE_REVISION" \ + -out build + + - name: Validate ANGLE runtime + shell: bash + run: | + package_dir="build/angle-${{ matrix.platform }}-${{ matrix.arch }}" + test_binary="build/test-angle-${{ matrix.platform }}-${{ matrix.arch }}" + c++ -std=c++17 \ + -I"$package_dir/include" \ + tests/test-angle.cpp \ + -L"$package_dir/lib" \ + -lEGL -lGLESv2 \ + -o "$test_binary" + + if [[ "$RUNNER_OS" == "Linux" ]]; then + icd_path=$(find /usr/share/vulkan/icd.d -name 'lvp_icd*.json' -print -quit) + test -n "$icd_path" + VK_ICD_FILENAMES="$icd_path" \ + LD_LIBRARY_PATH="$package_dir/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + "$test_binary" + else + DYLD_LIBRARY_PATH="$package_dir/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" \ + "$test_binary" + fi + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: angle-${{ matrix.platform }}-${{ matrix.arch }} + path: build/angle-${{ matrix.platform }}-${{ matrix.arch }}/ + + create-release: + needs: build-angle + if: ${{ github.event_name == 'workflow_dispatch' && !inputs.skip_release }} + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: downloaded_artifacts + + - name: Package release + run: | + mkdir -p release + for dir in downloaded_artifacts/*/; do + name=$(basename "$dir") + (cd "$dir" && zip -r "../../release/${name}.zip" .) + done + + - name: Resolve release metadata + id: metadata + shell: bash + run: | + echo "revision_short=${ANGLE_REVISION:0:12}" >> "$GITHUB_OUTPUT" + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + tag_name: angle-${{ steps.metadata.outputs.revision_short }}-${{ inputs.release_suffix || github.run_number }} + name: ANGLE ${{ steps.metadata.outputs.revision_short }} Build ${{ inputs.release_suffix || github.run_number }} + body: | + Automated ANGLE shared runtime builds for MystralNative WebGL 2. + + **Revision:** `${{ env.ANGLE_REVISION }}` + + ## Platforms + - Linux x64 and arm64: Vulkan backend with X11 and Wayland support + - macOS arm64 and x64: Metal backend + + ## Package layout + - `include/EGL`, `include/GLES2`, `include/GLES3`, `include/KHR` + - `lib/libEGL.so` and `lib/libGLESv2.so` on Linux + - `lib/libEGL.dylib` and `lib/libGLESv2.dylib` on macOS + - `LICENSE.angle` and `ANGLE_REVISION` + + ## Runtime requirements + - Linux: system Vulkan loader, Vulkan driver, and XCB runtime; Wayland runtime for Wayland presentation + - macOS: Metal-capable macOS host + files: release/*.zip + draft: false + prerelease: false diff --git a/.gitignore b/.gitignore index 04a4a71..3f13f71 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ example/build-* monitor_build.py third_party/swc-static/target/ +# Python bytecode +__pycache__/ + # Test binaries and outputs tests/test_moshi tests/*.wav diff --git a/AGENTS.md b/AGENTS.md index 84c72d7..0a68d5c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,8 @@ This file describes how AI coding agents can effectively work with this reposito ## Repository Context -This is a **build automation repository** for cross-platform static libraries. The primary artifacts are: -- Pre-built libraries (`.a`, `.lib` files) +This is a **build automation repository** for cross-platform native libraries. The primary artifacts are: +- Pre-built static and shared libraries (`.a`, `.lib`, `.so`, `.dylib` files) - Headers - GitHub Actions workflows for CI/CD diff --git a/CLAUDE.md b/CLAUDE.md index cb07730..121ac36 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,6 +23,7 @@ This repository (mystralengine/library-builder) provides Python scripts and GitH | `build-swc.py` | Build SWC TypeScript compiler | macOS (arm64, x86_64), Linux, Windows | | `build-moshi.py` | Build Moshi/Mimi speech AI codec | macOS (arm64, Metal), Linux (x64), Windows (x64) | | `build-quiche.py` | Build quiche (QUIC + HTTP/3) for WebTransport | macOS (arm64, x86_64), Linux (x64), Windows (x64), iOS (device + sim), Android (arm64/armv7/x64) | +| `build-angle.py` | Build ANGLE EGL/OpenGL ES shared runtimes | macOS (arm64, x86_64), Linux (x64, arm64) | ## Build Commands @@ -41,6 +42,8 @@ python3 build-skia.py win # Windows x64 (static CRT) python3 build-skia.py linux # Linux x64 python3 build-skia.py wasm # WebAssembly python3 build-skia.py xcframework # Apple XCFramework (macOS + iOS) +python3 build-angle.py linux -archs x64 # ANGLE Vulkan runtime +python3 build-angle.py mac -archs arm64 # ANGLE Metal runtime # Options python3 build-skia.py -config Debug # Debug build (default: Release) diff --git a/README.md b/README.md index 5a11320..538d9f5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Mystral Engine Library Builder -This repository provides automated builds of static libraries for [Mystral Engine](https://github.com/mystralengine) dependencies. +This repository provides automated builds of native libraries for [Mystral Engine](https://github.com/mystralengine) dependencies. **Currently supported:** - [Skia](https://skia.org/) - 2D graphics library with Dawn/Graphite support @@ -9,10 +9,10 @@ This repository provides automated builds of static libraries for [Mystral Engin - [libuv](https://libuv.org/) - Async I/O event loop (non-blocking network, file, timers). - [Draco](https://github.com/google/draco) - Mesh compression (native glTF Draco decoding). - [quiche](https://github.com/cloudflare/quiche) - QUIC + HTTP/3, the native backend for the WebTransport API. Built with the `ffi` feature plus a small patch exposing WebTransport SETTINGS; bundles BoringSSL. +- [ANGLE](https://chromium.googlesource.com/angle/angle) - EGL and OpenGL ES shared runtimes for WebGL 2. Linux packages use Vulkan with X11 and Wayland; macOS packages use Metal. **Planned:** - V8 - JavaScript engine. Currently using older build from https://github.com/kuoruan/libv8 -- ANGLE - OpenGL ES to other backends - no good cross platform solution yet (individual repos w/ Windows & Linux builds, but no cross platform support). **Dependencies with existing prebuilt libraries** - Dawn - https://github.com/google/dawn/releases - Google / Chrome's WebGPU implementation. Note however that this doesn't have iOS releases, while the Skia build above does have a Dawn iOS release associated. @@ -61,8 +61,14 @@ python3 build-skia.py win -crt dynamic # Windows x64 (dynamic CRT) python3 build-skia.py linux # Linux x64 python3 build-skia.py wasm # WebAssembly python3 build-skia.py xcframework # Apple XCFramework + +# Build ANGLE WebGL 2 runtimes (must run on the target operating system) +python3 build-angle.py linux -archs x64 # Vulkan, X11, and Wayland +python3 build-angle.py mac -archs arm64 # Metal ``` +ANGLE packages are written to `build/angle--`. Linux runtimes use the host's Vulkan loader, Vulkan driver, and XCB runtime; Wayland presentation additionally uses the system Wayland runtime libraries. The ANGLE workflow builds each package and verifies a headless triangle render with pixel readback before publishing it. + ### Options ```bash diff --git a/build-angle.py b/build-angle.py new file mode 100644 index 0000000..99bd22e --- /dev/null +++ b/build-angle.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +"""Build portable ANGLE EGL and OpenGL ES runtime packages. + +The packages contain ANGLE's public headers plus the shared EGL and GLESv2 +libraries used by MystralNative's optional WebGL 2 implementation. + +Supported platforms: Linux and macOS. +Source: https://chromium.googlesource.com/angle/angle +""" + +import argparse +import os +import platform as host_platform +import shutil +import subprocess +import sys +from pathlib import Path + +ANGLE_REPO = "https://chromium.googlesource.com/angle/angle" +ANGLE_REVISION = "107da744f62a319b3c6851694740d9ebf247d048" +DEPOT_TOOLS_REPO = ( + "https://chromium.googlesource.com/chromium/tools/depot_tools.git" +) +DEPOT_TOOLS_REVISION = "f70835271105ca56d2cd5382a0118152bc2bdeea" + + +def git_revision(value): + if len(value) != 40 or any( + character not in "0123456789abcdefABCDEF" for character in value + ): + raise argparse.ArgumentTypeError( + "revision must be a full 40-character Git commit" + ) + return value.lower() + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Build ANGLE shared runtime libraries" + ) + parser.add_argument("platform", choices=["linux", "mac"]) + parser.add_argument( + "-archs", help="Target architectures (comma separated)", default="x64" + ) + parser.add_argument("-config", choices=["Release", "Debug"], default="Release") + parser.add_argument("-out", help="Output directory", default="build") + parser.add_argument( + "-revision", + type=git_revision, + help="ANGLE git revision", + default=ANGLE_REVISION, + ) + return parser.parse_args() + + +def run_command(command, cwd=None, env=None): + print(f"Running: {' '.join(str(part) for part in command)}", flush=True) + try: + subprocess.check_call(command, cwd=cwd, env=env) + except subprocess.CalledProcessError as error: + print(f"Command failed with exit code {error.returncode}") + sys.exit(error.returncode) + + +def require_host(target_platform): + host = host_platform.system() + expected = "Linux" if target_platform == "linux" else "Darwin" + if host != expected: + print( + f"Error: {target_platform} ANGLE builds require a {expected} host " + f"(current host: {host})" + ) + sys.exit(1) + + +def ensure_git_checkout(directory, repository, revision): + if not (directory / ".git").exists(): + if directory.exists(): + shutil.rmtree(directory) + directory.parent.mkdir(parents=True, exist_ok=True) + run_command( + [ + "git", + "clone", + "--filter=blob:none", + "--no-checkout", + repository, + str(directory), + ] + ) + + run_command(["git", "fetch", "--depth", "1", "origin", revision], cwd=directory) + run_command(["git", "checkout", "--force", "FETCH_HEAD"], cwd=directory) + + +def ensure_sources(root_dir, revision): + third_party_dir = root_dir / "third_party" + depot_tools_dir = third_party_dir / "depot_tools" + source_dir = third_party_dir / "angle" + + ensure_git_checkout(depot_tools_dir, DEPOT_TOOLS_REPO, DEPOT_TOOLS_REVISION) + ensure_git_checkout(source_dir, ANGLE_REPO, revision) + + gclient_config = """solutions = [ + { + 'name': '.', + 'url': 'https://chromium.googlesource.com/angle/angle', + 'managed': False, + 'custom_deps': {}, + 'custom_vars': { + 'checkout_angle_cl_deps': False, + 'checkout_angle_dawn_deps': False, + 'checkout_angle_internal': False, + 'checkout_angle_mesa': False, + 'checkout_angle_partition_alloc': False, + 'checkout_angle_perfetto': False, + 'checkout_angle_restricted_traces': False, + 'checkout_extra_traces': False, + }, + }, +] +""" + (source_dir / ".gclient").write_text(gclient_config) + + env = os.environ.copy() + env["DEPOT_TOOLS_UPDATE"] = "0" + env["PATH"] = f"{depot_tools_dir}{os.pathsep}{env.get('PATH', '')}" + run_command( + [str(depot_tools_dir / "ensure_bootstrap")], + cwd=depot_tools_dir, + env=env, + ) + run_command( + [str(depot_tools_dir / "gclient"), "sync", "--no-history"], + cwd=source_dir, + env=env, + ) + return source_dir, depot_tools_dir, env + + +def normalize_arch(platform, arch): + if arch in ("x64", "x86_64"): + return "x64" + if arch in ("arm64", "aarch64"): + return "arm64" + print(f"Error: unsupported {platform} architecture: {arch}") + sys.exit(1) + + +def gn_args(target_platform, arch, config): + args = { + "angle_build_tests": False, + "angle_enable_cl": False, + "angle_enable_commit_id": False, + "angle_enable_gl": False, + "angle_enable_perfetto": False, + "angle_enable_null": False, + "angle_enable_swiftshader": False, + "angle_enable_wgpu": False, + "angle_has_frame_capture": False, + "clang_use_chrome_plugins": False, + "is_component_build": False, + "is_debug": config == "Debug", + "is_official_build": False, + "symbol_level": 0, + "target_cpu": arch, + "target_os": target_platform, + "use_remoteexec": False, + } + if target_platform == "linux": + args.update( + { + "angle_enable_metal": False, + "angle_enable_vulkan": True, + "angle_use_custom_libvulkan": False, + "angle_use_wayland": True, + "angle_use_x11": True, + "use_sysroot": True, + } + ) + else: + args.update( + { + "angle_enable_metal": True, + "angle_enable_vulkan": False, + "angle_use_wayland": False, + "angle_use_x11": False, + } + ) + + values = [] + for key, value in sorted(args.items()): + if isinstance(value, bool): + encoded = "true" if value else "false" + elif isinstance(value, int): + encoded = str(value) + else: + encoded = f'"{value}"' + values.append(f"{key}={encoded}") + return " ".join(values) + + +def build_arch(source_dir, depot_tools_dir, env, target_platform, arch, config): + if target_platform == "linux": + run_command( + [ + "python3", + str(source_dir / "build/linux/sysroot_scripts/install-sysroot.py"), + f"--arch={arch}", + ], + cwd=source_dir, + env=env, + ) + + output_dir = source_dir / "out" / f"{target_platform}-{arch}-{config.lower()}" + run_command( + [ + str(depot_tools_dir / "gn"), + "gen", + str(output_dir), + f"--args={gn_args(target_platform, arch, config)}", + ], + cwd=source_dir, + env=env, + ) + run_command( + [ + str(depot_tools_dir / "autoninja"), + "-C", + str(output_dir), + "libEGL", + "libGLESv2", + ], + cwd=source_dir, + env=env, + ) + return output_dir + + +def copy_headers(source_dir, package_dir): + include_dir = package_dir / "include" + if include_dir.exists(): + shutil.rmtree(include_dir) + include_dir.mkdir(parents=True) + for name in ("EGL", "GLES2", "GLES3", "KHR"): + shutil.copytree(source_dir / "include" / name, include_dir / name) + shutil.copy2(source_dir / "LICENSE", package_dir / "LICENSE.angle") + + +def copy_runtime(output_dir, package_dir, target_platform): + lib_dir = package_dir / "lib" + lib_dir.mkdir(parents=True, exist_ok=True) + suffix = ".dylib" if target_platform == "mac" else ".so" + for name in (f"libEGL{suffix}", f"libGLESv2{suffix}"): + source = output_dir / name + if not source.exists(): + print(f"Error: expected ANGLE runtime not found: {source}") + sys.exit(1) + shutil.copy2(source, lib_dir / name) + + +def main(): + args = parse_args() + require_host(args.platform) + + root_dir = Path(__file__).parent.absolute() + build_dir = Path(args.out).absolute() + source_dir, depot_tools_dir, env = ensure_sources(root_dir, args.revision) + + archs = [ + normalize_arch(args.platform, arch.strip()) for arch in args.archs.split(",") + ] + for arch in archs: + print(f"Building ANGLE for {args.platform} {arch} ({args.config})") + output_dir = build_arch( + source_dir, depot_tools_dir, env, args.platform, arch, args.config + ) + package_dir = build_dir / f"angle-{args.platform}-{arch}" + if package_dir.exists(): + shutil.rmtree(package_dir) + package_dir.mkdir(parents=True) + copy_headers(source_dir, package_dir) + copy_runtime(output_dir, package_dir, args.platform) + (package_dir / "ANGLE_REVISION").write_text(f"{args.revision}\n") + print(f"Packaged ANGLE at {package_dir}") + + +if __name__ == "__main__": + main() diff --git a/tests/test-angle.cpp b/tests/test-angle.cpp new file mode 100644 index 0000000..3e0a1eb --- /dev/null +++ b/tests/test-angle.cpp @@ -0,0 +1,188 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace { + +bool checkShader(GLuint shader) { + GLint compiled = GL_FALSE; + glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); + if (compiled == GL_TRUE) { + return true; + } + + GLint length = 0; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); + std::string log(static_cast(length), '\0'); + glGetShaderInfoLog(shader, length, nullptr, log.data()); + std::cerr << "Shader compilation failed: " << log << std::endl; + return false; +} + +} // namespace + +int main() { + auto getPlatformDisplay = reinterpret_cast( + eglGetProcAddress("eglGetPlatformDisplayEXT")); + if (!getPlatformDisplay) { + std::cerr << "eglGetPlatformDisplayEXT is unavailable" << std::endl; + return 1; + } + +#if defined(__APPLE__) + constexpr EGLint renderer = EGL_PLATFORM_ANGLE_TYPE_METAL_ANGLE; +#else + constexpr EGLint renderer = EGL_PLATFORM_ANGLE_TYPE_VULKAN_ANGLE; +#endif + const EGLint displayAttributes[] = { + EGL_PLATFORM_ANGLE_TYPE_ANGLE, + renderer, +#if !defined(__APPLE__) + EGL_PLATFORM_ANGLE_NATIVE_PLATFORM_TYPE_ANGLE, + EGL_PLATFORM_SURFACELESS_MESA, +#endif + EGL_NONE, + }; + EGLDisplay display = getPlatformDisplay( + EGL_PLATFORM_ANGLE_ANGLE, EGL_DEFAULT_DISPLAY, displayAttributes); + if (display == EGL_NO_DISPLAY || !eglInitialize(display, nullptr, nullptr)) { + std::cerr << "ANGLE display initialization failed: 0x" << std::hex + << eglGetError() << std::endl; + return 2; + } + + const EGLint configAttributes[] = { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT_KHR, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_NONE, + }; + EGLConfig config = nullptr; + EGLint configCount = 0; + if (!eglChooseConfig(display, configAttributes, &config, 1, &configCount) || + configCount == 0) { + std::cerr << "ANGLE framebuffer selection failed" << std::endl; + eglTerminate(display); + return 3; + } + + const EGLint contextAttributes[] = { + EGL_CONTEXT_CLIENT_VERSION, 3, + EGL_CONTEXT_WEBGL_COMPATIBILITY_ANGLE, EGL_TRUE, + EGL_ROBUST_RESOURCE_INITIALIZATION_ANGLE, EGL_TRUE, + EGL_NONE, + }; + EGLContext context = eglCreateContext( + display, config, EGL_NO_CONTEXT, contextAttributes); + const EGLint surfaceAttributes[] = { + EGL_WIDTH, 64, + EGL_HEIGHT, 64, + EGL_NONE, + }; + EGLSurface surface = eglCreatePbufferSurface( + display, config, surfaceAttributes); + if (context == EGL_NO_CONTEXT || surface == EGL_NO_SURFACE || + !eglMakeCurrent(display, surface, surface, context)) { + std::cerr << "ANGLE context creation failed: 0x" << std::hex + << eglGetError() << std::endl; + if (surface != EGL_NO_SURFACE) { + eglDestroySurface(display, surface); + } + if (context != EGL_NO_CONTEXT) { + eglDestroyContext(display, context); + } + eglTerminate(display); + return 4; + } + + const char *rendererName = reinterpret_cast( + glGetString(GL_RENDERER)); + if (!rendererName || std::strstr(rendererName, "ANGLE") == nullptr) { + std::cerr << "Unexpected renderer: " + << (rendererName ? rendererName : "unavailable") << std::endl; + return 5; + } + std::cout << "Renderer: " << rendererName << std::endl; + + constexpr const char *vertexSource = R"GLSL(#version 300 es +in vec2 position; +void main() { + gl_Position = vec4(position, 0.0, 1.0); +} +)GLSL"; + constexpr const char *fragmentSource = R"GLSL(#version 300 es +precision highp float; +out vec4 color; +void main() { + color = vec4(1.0, 0.0, 0.0, 1.0); +} +)GLSL"; + + const GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); + glShaderSource(vertexShader, 1, &vertexSource, nullptr); + glCompileShader(vertexShader); + const GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); + glShaderSource(fragmentShader, 1, &fragmentSource, nullptr); + glCompileShader(fragmentShader); + if (!checkShader(vertexShader) || !checkShader(fragmentShader)) { + return 6; + } + + const GLuint program = glCreateProgram(); + glAttachShader(program, vertexShader); + glAttachShader(program, fragmentShader); + glLinkProgram(program); + GLint linked = GL_FALSE; + glGetProgramiv(program, GL_LINK_STATUS, &linked); + if (linked != GL_TRUE) { + std::cerr << "Shader program linking failed" << std::endl; + return 7; + } + glUseProgram(program); + + constexpr std::array vertices = { + 0.0f, 0.8f, -0.8f, -0.8f, 0.8f, -0.8f, + }; + GLuint buffer = 0; + glGenBuffers(1, &buffer); + glBindBuffer(GL_ARRAY_BUFFER, buffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices.data(), + GL_STATIC_DRAW); + const GLint position = glGetAttribLocation(program, "position"); + glEnableVertexAttribArray(static_cast(position)); + glVertexAttribPointer(static_cast(position), 2, GL_FLOAT, GL_FALSE, + 0, nullptr); + + glViewport(0, 0, 64, 64); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glDrawArrays(GL_TRIANGLES, 0, 3); + glFinish(); + + std::array pixel = {}; + glReadPixels(32, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel.data()); + std::cout << "Center pixel: " << static_cast(pixel[0]) << ", " + << static_cast(pixel[1]) << ", " + << static_cast(pixel[2]) << ", " + << static_cast(pixel[3]) << std::endl; + + eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + eglDestroySurface(display, surface); + eglDestroyContext(display, context); + eglTerminate(display); + + if (pixel[0] < 200 || pixel[1] > 20 || pixel[2] > 20 || pixel[3] < 200) { + std::cerr << "ANGLE rendered an unexpected center pixel" << std::endl; + return 8; + } + return 0; +} From ae492caeb7f841a3bdf4b3654f1bec25df4733fb Mon Sep 17 00:00:00 2001 From: Alex Garcia Gil Date: Sun, 30 Aug 2026 19:22:53 +1200 Subject: [PATCH 2/4] fix(angle): make macOS runtime packages relocatable Co-Authored-By: Claude Code --- .github/workflows/build-angle.yml | 16 ++++++++++++++-- README.md | 2 +- build-angle.py | 8 +++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-angle.yml b/.github/workflows/build-angle.yml index e34679b..008e6a3 100644 --- a/.github/workflows/build-angle.yml +++ b/.github/workflows/build-angle.yml @@ -94,10 +94,17 @@ jobs: run: | package_dir="build/angle-${{ matrix.platform }}-${{ matrix.arch }}" test_binary="build/test-angle-${{ matrix.platform }}-${{ matrix.arch }}" + link_options=() + if [[ "$RUNNER_OS" == "macOS" ]]; then + link_options+=( + "-Wl,-rpath,@executable_path/angle-${{ matrix.platform }}-${{ matrix.arch }}/lib" + ) + fi c++ -std=c++17 \ -I"$package_dir/include" \ tests/test-angle.cpp \ -L"$package_dir/lib" \ + "${link_options[@]}" \ -lEGL -lGLESv2 \ -o "$test_binary" @@ -108,8 +115,13 @@ jobs: LD_LIBRARY_PATH="$package_dir/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ "$test_binary" else - DYLD_LIBRARY_PATH="$package_dir/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" \ - "$test_binary" + otool -D "$package_dir/lib/libEGL.dylib" | \ + grep -Fxq '@rpath/libEGL.dylib' + otool -D "$package_dir/lib/libGLESv2.dylib" | \ + grep -Fxq '@rpath/libGLESv2.dylib' + codesign --verify --strict "$package_dir/lib/libEGL.dylib" + codesign --verify --strict "$package_dir/lib/libGLESv2.dylib" + "$test_binary" fi - name: Upload artifact diff --git a/README.md b/README.md index 538d9f5..2102da6 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ python3 build-angle.py linux -archs x64 # Vulkan, X11, and Wayland python3 build-angle.py mac -archs arm64 # Metal ``` -ANGLE packages are written to `build/angle--`. Linux runtimes use the host's Vulkan loader, Vulkan driver, and XCB runtime; Wayland presentation additionally uses the system Wayland runtime libraries. The ANGLE workflow builds each package and verifies a headless triangle render with pixel readback before publishing it. +ANGLE packages are written to `build/angle--`. Linux runtimes use the host's Vulkan loader, Vulkan driver, and XCB runtime; Wayland presentation additionally uses the system Wayland runtime libraries. macOS libraries use relocatable `@rpath` install names and are ad-hoc signed after packaging. The ANGLE workflow builds each package and verifies a headless triangle render with pixel readback before publishing it. ### Options diff --git a/build-angle.py b/build-angle.py index 99bd22e..9c170b7 100644 --- a/build-angle.py +++ b/build-angle.py @@ -256,7 +256,13 @@ def copy_runtime(output_dir, package_dir, target_platform): if not source.exists(): print(f"Error: expected ANGLE runtime not found: {source}") sys.exit(1) - shutil.copy2(source, lib_dir / name) + destination = lib_dir / name + shutil.copy2(source, destination) + if target_platform == "mac": + run_command( + ["install_name_tool", "-id", f"@rpath/{name}", str(destination)] + ) + run_command(["codesign", "--force", "--sign", "-", str(destination)]) def main(): From 593ad86cc69efd6d486b84222091ab32642755d4 Mon Sep 17 00:00:00 2001 From: Alex Garcia Gil Date: Mon, 31 Aug 2026 16:49:46 +1200 Subject: [PATCH 3/4] fix(angle): validate arm64 cross-builds Co-Authored-By: Claude Code --- .github/workflows/build-angle.yml | 88 +++++++++++++++++++++---------- tests/test-angle.cpp | 2 +- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/.github/workflows/build-angle.yml b/.github/workflows/build-angle.yml index 008e6a3..eca3e11 100644 --- a/.github/workflows/build-angle.yml +++ b/.github/workflows/build-angle.yml @@ -43,7 +43,7 @@ jobs: - os: ubuntu-24.04 platform: linux arch: x64 - - os: ubuntu-24.04-arm + - os: ubuntu-24.04 platform: linux arch: arm64 - os: macos-15 @@ -82,6 +82,12 @@ jobs: mesa-vulkan-drivers \ pkg-config + - name: Set up arm64 emulation + if: matrix.platform == 'linux' && matrix.arch == 'arm64' + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + - name: Build ANGLE run: | python3 build-angle.py ${{ matrix.platform }} \ @@ -94,34 +100,60 @@ jobs: run: | package_dir="build/angle-${{ matrix.platform }}-${{ matrix.arch }}" test_binary="build/test-angle-${{ matrix.platform }}-${{ matrix.arch }}" - link_options=() - if [[ "$RUNNER_OS" == "macOS" ]]; then - link_options+=( - "-Wl,-rpath,@executable_path/angle-${{ matrix.platform }}-${{ matrix.arch }}/lib" - ) - fi - c++ -std=c++17 \ - -I"$package_dir/include" \ - tests/test-angle.cpp \ - -L"$package_dir/lib" \ - "${link_options[@]}" \ - -lEGL -lGLESv2 \ - -o "$test_binary" - - if [[ "$RUNNER_OS" == "Linux" ]]; then - icd_path=$(find /usr/share/vulkan/icd.d -name 'lvp_icd*.json' -print -quit) - test -n "$icd_path" - VK_ICD_FILENAMES="$icd_path" \ - LD_LIBRARY_PATH="$package_dir/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ - "$test_binary" + + if [[ "$RUNNER_OS" == "Linux" && "${{ matrix.arch }}" == "arm64" ]]; then + docker run --rm --platform linux/arm64 \ + -v "$PWD:/workspace" -w /workspace ubuntu:24.04 bash -c ' + set -euo pipefail + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y g++ libxcb1 mesa-vulkan-drivers + package_dir="build/angle-linux-arm64" + test_binary="build/test-angle-linux-arm64" + c++ -std=c++17 \ + -I"$package_dir/include" \ + tests/test-angle.cpp \ + -L"$package_dir/lib" \ + -lEGL -lGLESv2 \ + -o "$test_binary" + icd_path=$(find /usr/share/vulkan/icd.d \ + -name "lvp_icd*.json" -print -quit) + test -n "$icd_path" + VK_ICD_FILENAMES="$icd_path" \ + LD_LIBRARY_PATH="$package_dir/lib" \ + "$test_binary" + ' else - otool -D "$package_dir/lib/libEGL.dylib" | \ - grep -Fxq '@rpath/libEGL.dylib' - otool -D "$package_dir/lib/libGLESv2.dylib" | \ - grep -Fxq '@rpath/libGLESv2.dylib' - codesign --verify --strict "$package_dir/lib/libEGL.dylib" - codesign --verify --strict "$package_dir/lib/libGLESv2.dylib" - "$test_binary" + link_options=() + if [[ "$RUNNER_OS" == "macOS" ]]; then + link_options+=( + "-Wl,-rpath,@executable_path/angle-${{ matrix.platform }}-${{ matrix.arch }}/lib" + ) + fi + c++ -std=c++17 \ + -I"$package_dir/include" \ + tests/test-angle.cpp \ + -L"$package_dir/lib" \ + "${link_options[@]}" \ + -lEGL -lGLESv2 \ + -o "$test_binary" + + if [[ "$RUNNER_OS" == "Linux" ]]; then + icd_path=$(find /usr/share/vulkan/icd.d \ + -name 'lvp_icd*.json' -print -quit) + test -n "$icd_path" + VK_ICD_FILENAMES="$icd_path" \ + LD_LIBRARY_PATH="$package_dir/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + "$test_binary" + else + otool -D "$package_dir/lib/libEGL.dylib" | \ + grep -Fxq '@rpath/libEGL.dylib' + otool -D "$package_dir/lib/libGLESv2.dylib" | \ + grep -Fxq '@rpath/libGLESv2.dylib' + codesign --verify --strict "$package_dir/lib/libEGL.dylib" + codesign --verify --strict "$package_dir/lib/libGLESv2.dylib" + "$test_binary" + fi fi - name: Upload artifact diff --git a/tests/test-angle.cpp b/tests/test-angle.cpp index 3e0a1eb..0ee50fc 100644 --- a/tests/test-angle.cpp +++ b/tests/test-angle.cpp @@ -50,7 +50,7 @@ int main() { EGL_NONE, }; EGLDisplay display = getPlatformDisplay( - EGL_PLATFORM_ANGLE_ANGLE, EGL_DEFAULT_DISPLAY, displayAttributes); + EGL_PLATFORM_ANGLE_ANGLE, nullptr, displayAttributes); if (display == EGL_NO_DISPLAY || !eglInitialize(display, nullptr, nullptr)) { std::cerr << "ANGLE display initialization failed: 0x" << std::hex << eglGetError() << std::endl; From 14b0185431da45228065f9d048b521994bf7daf8 Mon Sep 17 00:00:00 2001 From: Alex Garcia Gil Date: Mon, 31 Aug 2026 17:09:33 +1200 Subject: [PATCH 4/4] fix(angle): test macOS x64 under Rosetta Co-Authored-By: Claude Code --- .github/workflows/build-angle.yml | 17 +++++++++++++++-- README.md | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-angle.yml b/.github/workflows/build-angle.yml index eca3e11..7049af3 100644 --- a/.github/workflows/build-angle.yml +++ b/.github/workflows/build-angle.yml @@ -49,7 +49,7 @@ jobs: - os: macos-15 platform: mac arch: arm64 - - os: macos-15-intel + - os: macos-15 platform: mac arch: x64 @@ -88,6 +88,10 @@ jobs: with: platforms: arm64 + - name: Set up Rosetta + if: matrix.platform == 'mac' && matrix.arch == 'x64' + run: sudo softwareupdate --install-rosetta --agree-to-license + - name: Build ANGLE run: | python3 build-angle.py ${{ matrix.platform }} \ @@ -124,13 +128,18 @@ jobs: "$test_binary" ' else + compile_options=() link_options=() if [[ "$RUNNER_OS" == "macOS" ]]; then link_options+=( "-Wl,-rpath,@executable_path/angle-${{ matrix.platform }}-${{ matrix.arch }}/lib" ) + if [[ "${{ matrix.arch }}" == "x64" ]]; then + compile_options+=("-arch" "x86_64") + fi fi c++ -std=c++17 \ + "${compile_options[@]}" \ -I"$package_dir/include" \ tests/test-angle.cpp \ -L"$package_dir/lib" \ @@ -152,7 +161,11 @@ jobs: grep -Fxq '@rpath/libGLESv2.dylib' codesign --verify --strict "$package_dir/lib/libEGL.dylib" codesign --verify --strict "$package_dir/lib/libGLESv2.dylib" - "$test_binary" + if [[ "${{ matrix.arch }}" == "x64" ]]; then + arch -x86_64 "$test_binary" + else + "$test_binary" + fi fi fi diff --git a/README.md b/README.md index 2102da6..da1b445 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ python3 build-angle.py linux -archs x64 # Vulkan, X11, and Wayland python3 build-angle.py mac -archs arm64 # Metal ``` -ANGLE packages are written to `build/angle--`. Linux runtimes use the host's Vulkan loader, Vulkan driver, and XCB runtime; Wayland presentation additionally uses the system Wayland runtime libraries. macOS libraries use relocatable `@rpath` install names and are ad-hoc signed after packaging. The ANGLE workflow builds each package and verifies a headless triangle render with pixel readback before publishing it. +ANGLE packages are written to `build/angle--`. Linux runtimes use the host's Vulkan loader, Vulkan driver, and XCB runtime; Wayland presentation additionally uses the system Wayland runtime libraries. macOS libraries use relocatable `@rpath` install names and are ad-hoc signed after packaging. The macOS x64 package is cross-built on Apple Silicon and render-tested under Rosetta because GitHub's Intel runner does not expose Metal. The ANGLE workflow builds each package and verifies a headless triangle render with pixel readback before publishing it. ### Options