diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 516c7c8d..462dec5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,33 +41,29 @@ jobs: - name: Verify deterministic default theme icons run: dotnet run --project tools/Forma.IconPipeline/Forma.IconPipeline.csproj -- verify - # The Windows legs cannot run on the local fleet (no Windows runner exists here), but the branch ruleset requires those two - # contexts to succeed. Under the flag they are reported as successful with this placeholder instead of being left pending. - windows-legs-not-run: - name: ${{ matrix.runtime }} / windows-latest - if: vars.TRACE_CI_RUNNERS == 'self-hosted' - runs-on: ${{ vars.TRACE_CI_RUNNERS == 'self-hosted' && fromJSON('["self-hosted","Linux","X64"]') || 'ubuntu-latest' }} - strategy: - fail-fast: false - matrix: - runtime: [MonoGame, FNA] - steps: - - name: Report that the Windows leg did not run - run: echo "No Windows runner exists on the local machine; the ${{ matrix.runtime }} Windows leg is skipped under TRACE_CI_RUNNERS=self-hosted." - runtime: needs: lint name: ${{ matrix.runtime }} / ${{ matrix.os }} strategy: fail-fast: false matrix: - # Windows legs are skipped under TRACE_CI_RUNNERS=self-hosted: no Windows runner exists on the local machine. - os: ${{ fromJSON(vars.TRACE_CI_RUNNERS == 'self-hosted' && '["ubuntu-latest","macos-latest"]' || '["ubuntu-latest","windows-latest","macos-latest"]') }} runtime: [MonoGame, FNA] - # Under TRACE_CI_RUNNERS=self-hosted (docs/self-hosted-runners.md in Trace Arena) Linux legs run on the local x64 runner and - # macOS legs on the local macOS runner. The Linux steps that render through the Vulkan software renderer are skipped there: it - # crashes under x64 emulation ("rosetta error: mmap_anonymous_rw mmap failed"), and the native arm64 runner lacks the freetype native library. - runs-on: ${{ vars.TRACE_CI_RUNNERS == 'self-hosted' && (matrix.os == 'macos-latest' && fromJSON('["self-hosted","macOS","ARM64"]') || fromJSON('["self-hosted","Linux","X64","yiazmat-linux-x64-native"]')) || matrix.os }} + # The Windows legs always run on GitHub-hosted windows-latest: no Windows machine is part of the local fleet, and Forma + # is a public repository, where standard runners (Windows included) do not consume billed minutes. + os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-latest + selfHosted: '["self-hosted","Linux","X64","yiazmat-linux-x64-native"]' + - os: macos-latest + selfHosted: '["self-hosted","macOS","ARM64"]' + - os: windows-latest + selfHosted: '["windows-latest"]' + # Under TRACE_CI_RUNNERS=self-hosted (docs/self-hosted-runners.md in Trace Arena) the Linux legs run on the local native x64 + # runner and the macOS legs on the local macOS runner. The Windows legs stay on windows-latest either way (see the matrix). + # The Linux steps that render through the Vulkan software renderer are skipped on the self-hosted Linux runner: it crashes + # under x64 emulation ("rosetta error: mmap_anonymous_rw mmap failed"), and the native arm64 runner lacks the freetype native + # library. + runs-on: ${{ vars.TRACE_CI_RUNNERS == 'self-hosted' && fromJSON(matrix.selfHosted) || matrix.os }} env: FORMA_RUNTIME: ${{ matrix.runtime }} steps: @@ -108,7 +104,7 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends libegl1 libgl1 libgl1-mesa-dri libglx0 libglx-mesa0 mesa-vulkan-drivers vulkan-tools xvfb + sudo apt-get install -y --no-install-recommends libegl1 libgl1 libgl1-mesa-dri libglx0 libglx-mesa0 mesa-vulkan-drivers vulkan-tools xvfb x11-utils software_icd=$(find /opt/google/chrome -name 'vk_swiftshader_icd.json' -print -quit 2>/dev/null || true) software_icd=${software_icd:-$(find /usr/share/vulkan/icd.d -name 'lvp_icd*.json' -print -quit)} test -n "$software_icd" @@ -125,24 +121,24 @@ jobs: FNA3D_OPENGL_WINDOW_DEPTHSTENCILFORMAT: None run: | if [[ "$FORMA_RUNTIME" == "FNA" ]]; then - env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES SvgBackend=Skia FormaRuntime=FNA xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-catalog-smoke.sh - env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-quick-start.sh - env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' dotnet test tests/Forma.RenderTests/Forma.RenderTests.csproj --configuration Release -p:FormaRuntime=FNA --no-build --nologo - env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' dotnet run --project tests/Forma.SvgRenderSmoke/Forma.SvgRenderSmoke.csproj --configuration Release -p:FormaRuntime=FNA -p:SvgBackend=Skia + env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES SvgBackend=Skia FormaRuntime=FNA bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-catalog-smoke.sh + env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-quick-start.sh + env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' dotnet test tests/Forma.RenderTests/Forma.RenderTests.csproj --configuration Release -p:FormaRuntime=FNA --no-build --nologo + env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' dotnet run --project tests/Forma.SvgRenderSmoke/Forma.SvgRenderSmoke.csproj --configuration Release -p:FormaRuntime=FNA -p:SvgBackend=Skia else - xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' vulkaninfo --summary - SvgBackend=Skia FormaRuntime=MonoGame xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-catalog-smoke.sh - xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-quick-start.sh - xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' dotnet test tests/Forma.RenderTests/Forma.RenderTests.csproj --configuration Release -p:FormaRuntime=MonoGame --no-build --nologo - xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' dotnet run --project tests/Forma.SvgRenderSmoke/Forma.SvgRenderSmoke.csproj --configuration Release -p:FormaRuntime=MonoGame -p:SvgBackend=Skia + bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' vulkaninfo --summary + SvgBackend=Skia FormaRuntime=MonoGame bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-catalog-smoke.sh + bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-quick-start.sh + bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' dotnet test tests/Forma.RenderTests/Forma.RenderTests.csproj --configuration Release -p:FormaRuntime=MonoGame --no-build --nologo + bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' dotnet run --project tests/Forma.SvgRenderSmoke/Forma.SvgRenderSmoke.csproj --configuration Release -p:FormaRuntime=MonoGame -p:SvgBackend=Skia # MonoGame's DesktopVK backend aborts on llvmpipe over Xvfb on the self-hosted fleet, in both the catalog smoke and the # swapchain smoke: "Fatal : VkResult is c4653214 in vulkan/MGG_Vulkan.cpp:2226" followed by SIGSEGV (the backend prints # an uninitialized VkResult). A 1 GB /dev/shm did not change it, so both Vulkan steps stay on hosted CI. Everything # else of this leg (DesktopGL catalog smoke, quick start, render tests, SVG smoke) runs on the fleet, and the Vulkan # coverage there is `thorvg-linux`, which builds the ThorVG adapter and runs its tests on the native x64 server. if [[ -z "${FORMA_SKIP_VULKAN_SMOKE:-}" ]]; then - SvgBackend=Skia MonoGamePlatform=DesktopVK FormaNativeRuntime=true CatalogBackend=Vulkan xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-catalog-smoke.sh - MonoGamePlatform=DesktopVK FormaNativeRuntime=true xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' dotnet run --project tests/Forma.SvgRenderSmoke/Forma.SvgRenderSmoke.csproj --configuration Release -p:FormaRuntime=MonoGame -p:MonoGamePlatform=DesktopVK -p:FormaNativeRuntime=true -p:SvgBackend=Skia + SvgBackend=Skia MonoGamePlatform=DesktopVK FormaNativeRuntime=true CatalogBackend=Vulkan bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-catalog-smoke.sh + MonoGamePlatform=DesktopVK FormaNativeRuntime=true bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' dotnet run --project tests/Forma.SvgRenderSmoke/Forma.SvgRenderSmoke.csproj --configuration Release -p:FormaRuntime=MonoGame -p:MonoGamePlatform=DesktopVK -p:FormaNativeRuntime=true -p:SvgBackend=Skia else echo "Skipped on the self-hosted fleet: MonoGame DesktopVK crashes in vulkan/MGG_Vulkan.cpp:2226 on llvmpipe over Xvfb." fi @@ -156,7 +152,7 @@ jobs: SDL_AUDIODRIVER: dummy FNA3D_FORCE_DRIVER: OpenGL FNA3D_OPENGL_WINDOW_DEPTHSTENCILFORMAT: None - run: env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-fna-video-smoke.sh + run: env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-fna-video-smoke.sh - name: Compare Linux peer rendering if: runner.os == 'Linux' && matrix.runtime == 'MonoGame' shell: bash @@ -165,7 +161,7 @@ jobs: SDL_VIDEODRIVER: x11 FormaCatalogViewportWidth: 800 FormaCatalogViewportHeight: 480 - run: SvgBackend=Skia bash scripts/retry.sh xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-catalog-render-parity.sh + run: SvgBackend=Skia bash scripts/retry.sh bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' bash scripts/check-catalog-render-parity.sh - name: Validate Windows selected runtime if: runner.os == 'Windows' shell: bash @@ -304,7 +300,7 @@ jobs: - name: Install Linux graphics dependencies run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends libegl1 libgl1 libgl1-mesa-dri libglx0 libglx-mesa0 mesa-vulkan-drivers xvfb + sudo apt-get install -y --no-install-recommends libegl1 libgl1 libgl1-mesa-dri libglx0 libglx-mesa0 mesa-vulkan-drivers xvfb x11-utils software_icd=$(find /opt/google/chrome -name 'vk_swiftshader_icd.json' -print -quit 2>/dev/null || true) software_icd=${software_icd:-$(find /usr/share/vulkan/icd.d -name 'lvp_icd*.json' -print -quit)} test -n "$software_icd" @@ -317,7 +313,7 @@ jobs: # The committed font bytes belong to the GitHub-hosted image's font stack (on native x64 Linux they already differ # at the size field), so the fleet skips that one comparison and hosted CI keeps it. FORMA_SKIP_FONT_XNB_CMP: ${{ vars.TRACE_CI_RUNNERS == 'self-hosted' && '1' || '' }} - run: bash scripts/retry.sh env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' bash scripts/test-package-consumer.sh + run: bash scripts/retry.sh env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' bash scripts/test-package-consumer.sh - uses: actions/upload-artifact@v7 with: name: forma-peer-packages diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5afa9b5d..19a945cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,7 +67,7 @@ jobs: - name: Install Linux graphics dependencies run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends libegl1 libgl1 libgl1-mesa-dri libglx0 libglx-mesa0 xvfb + sudo apt-get install -y --no-install-recommends libegl1 libgl1 libgl1-mesa-dri libglx0 libglx-mesa0 xvfb x11-utils - name: Validate release tag if: startsWith(github.ref, 'refs/tags/') shell: bash @@ -89,7 +89,7 @@ jobs: env: LIBGL_ALWAYS_SOFTWARE: 1 SDL_VIDEODRIVER: x11 - run: xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' bash scripts/test-package-consumer.sh + run: bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' bash scripts/test-package-consumer.sh - name: Validate SVG package consumers run: bash scripts/test-svg-package-consumers.sh - name: Pack approved release manifest diff --git a/config/cspell-project-words.txt b/config/cspell-project-words.txt index 71c26ce2..4914f204 100644 --- a/config/cspell-project-words.txt +++ b/config/cspell-project-words.txt @@ -115,3 +115,4 @@ premake SIGSEGV swapchain yiazmat +xdpyinfo diff --git a/docs/continuous-integration.md b/docs/continuous-integration.md index 182e4bda..bf943f77 100644 --- a/docs/continuous-integration.md +++ b/docs/continuous-integration.md @@ -13,7 +13,7 @@ supported configuration for forks. | `parity` | `self-hosted, Linux, X64, yiazmat-linux-x64-emulated` | Sequential runtime builds; the step is wrapped in `scripts/retry.sh` because a transient `GenerateDepsFile` file lock can survive one build boundary. | | `runtime` (`ubuntu-latest` legs) | `self-hosted, Linux, X64, yiazmat-linux-x64-native` | Runs on the native x64 Linux server, where the Vulkan software renderer works. | | `runtime` (`macos-latest` legs) | `self-hosted, macOS, ARM64` | Native arm64 macOS. | -| `runtime` (`windows-latest` legs) | placeholder job | No Windows machine is part of the fleet; the job reports the leg as not run so the required checks still resolve. | +| `runtime` (`windows-latest` legs) | `windows-latest` | Always GitHub-hosted: no Windows machine is part of the fleet, and the repository is public, so these minutes are not billed. | | `thorvg-linux` | `self-hosted, Linux, X64, yiazmat-linux-x64-native` | Builds the ThorVG adapter and runs its tests inside the slot's own Docker daemon. | | `thorvg-macos`, `nativeaot` | `self-hosted, macOS, ARM64` | Native arm64 macOS. | | `package-consumers` | `self-hosted, Linux, X64, yiazmat-linux-x64-native` | Builds and inspects the shipped packages. | @@ -40,6 +40,13 @@ These are deliberate, documented limits — not silent skips: The emulated x64 pool runs at most two jobs at a time: the native x64 server carries the heavier Linux legs, and two emulated containers keep the host's Docker VM comfortable. +Every X program runs through `scripts/xvfb-run.sh`, not `xvfb-run` directly: `xvfb-run -a` hands the +display number to the payload as soon as Xvfb creates its socket, and Xvfb `2:21.1.12-1ubuntu1.9` +still needs a moment before it listens, so a client that connects immediately dies with +`XCB failed to connect to the X server due to error:1`. The wrapper waits for `xdpyinfo` to succeed +before starting the payload (4 of 5 plain `xvfb-run` runs failed in a fresh container on the server; +0 of 5 with the wrapper). + Every job exports `MSBUILDDISABLENODEREUSE=1`, `DOTNET_CLI_DO_NOT_USE_MSBUILD_SERVER=1` and `DOTNET_PROCESSOR_COUNT=2`: reused MSBuild nodes have held `*.deps.json` files across builds and broken the next `GenerateDepsFile`. Runs are serialized per working directory by the ephemeral diff --git a/scripts/check-runtime-svg-linux.sh b/scripts/check-runtime-svg-linux.sh index 0fd7eb34..17d5abd2 100755 --- a/scripts/check-runtime-svg-linux.sh +++ b/scripts/check-runtime-svg-linux.sh @@ -9,7 +9,7 @@ cd "$repository_root" if [[ "${FORMA_SKIP_APT:-0}" != "1" ]] && command -v apt-get >/dev/null 2>&1 && [[ "$(id -u)" == "0" ]]; then apt-get update - apt-get install -y --no-install-recommends libegl1 libfreetype6-dev libgl1 libgl1-mesa-dri libglx0 libglx-mesa0 libharfbuzz-dev mesa-vulkan-drivers vulkan-tools xvfb xauth + apt-get install -y --no-install-recommends libegl1 libfreetype6-dev libgl1 libgl1-mesa-dri libglx0 libglx-mesa0 libharfbuzz-dev mesa-vulkan-drivers vulkan-tools xvfb x11-utils xauth fi software_icd="$(find /usr/share/vulkan/icd.d -name 'lvp_icd*.json' -print -quit)" @@ -22,19 +22,19 @@ for runtime_name in MonoGame FNA; do -p:FormaRuntime="$runtime_name" --filter 'FullyQualifiedName~Svg' --nologo done -SDL_VIDEODRIVER=x11 xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' \ +SDL_VIDEODRIVER=x11 bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' \ dotnet run --project tests/Forma.SvgRenderSmoke/Forma.SvgRenderSmoke.csproj \ --configuration Release -p:FormaRuntime=MonoGame env -u VK_ICD_FILENAMES -u VK_DRIVER_FILES SDL_VIDEODRIVER=offscreen FNA3D_FORCE_DRIVER=OpenGL \ - xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' \ + bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' \ dotnet run --project tests/Forma.SvgRenderSmoke/Forma.SvgRenderSmoke.csproj \ --configuration Release -p:FormaRuntime=FNA VK_ICD_FILENAMES="$software_icd" VK_DRIVER_FILES="$software_icd" vulkaninfo --summary VK_ICD_FILENAMES="$software_icd" VK_DRIVER_FILES="$software_icd" SDL_VIDEODRIVER=x11 \ MonoGamePlatform=DesktopVK FormaNativeRuntime=true \ - xvfb-run -a -s '-screen 0 1440x900x24 +extension GLX' \ + bash scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' \ dotnet run --project tests/Forma.SvgRenderSmoke/Forma.SvgRenderSmoke.csproj \ --configuration Release -p:FormaRuntime=MonoGame -p:MonoGamePlatform=DesktopVK -p:FormaNativeRuntime=true diff --git a/scripts/xvfb-run.sh b/scripts/xvfb-run.sh new file mode 100755 index 00000000..cb43e710 --- /dev/null +++ b/scripts/xvfb-run.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Runs a command under a fresh X server, and returns only once the server accepts connections. +# +# `xvfb-run -a` hands the display number to the payload as soon as Xvfb creates its socket; Xvfb 2:21.1.12-1ubuntu1.9 then still needs a +# moment before it listens, so a client that connects immediately dies with +# XCB failed to connect to the X server due to error:1 +# (vulkaninfo, and SDL/GL programs in general). Waiting for a successful X query before starting the payload removes the race. +# Usage mirrors the part of xvfb-run this repository uses: +# +# scripts/xvfb-run.sh -s '-screen 0 1440x900x24 +extension GLX' COMMAND [ARGS...] +set -euo pipefail + +server_args="" +if [ "${1:-}" = "-s" ]; then + server_args=${2:?xvfb-run.sh: -s needs a value} + shift 2 +fi +if [ $# -eq 0 ]; then + echo "usage: xvfb-run.sh [-s SERVER_ARGS] COMMAND [ARGS...]" >&2 + exit 2 +fi +if ! command -v xdpyinfo >/dev/null 2>&1; then + echo "xvfb-run.sh: xdpyinfo is required (package x11-utils)" >&2 + exit 1 +fi + +# Xvfb starts in milliseconds on an idle machine and in a second or two under load; 15 s is the "something is actually wrong" limit. +wait_seconds=${XVFB_WAIT_SECONDS:-15} +args=(xvfb-run -a) +if [ -n "$server_args" ]; then + args+=(-s "$server_args") +fi + +exec "${args[@]}" bash -c ' + wait=$1 + shift + deadline=$((SECONDS + wait)) + while ! xdpyinfo >/dev/null 2>&1; do + if [ "$SECONDS" -ge "$deadline" ]; then + echo "xvfb-run.sh: the X server on ${DISPLAY:-?} did not accept connections within ${wait}s" >&2 + exit 1 + fi + sleep 0.1 + done + exec "$@" +' _ "$wait_seconds" "$@"