452 lines
19 KiB
YAML
452 lines
19 KiB
YAML
name: Headless portability
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- ".github/workflows/headless-portability.yml"
|
|
- "AcDream.slnx"
|
|
- "src/AcDream.Platform/**"
|
|
- "src/AcDream.Launcher.Core/**"
|
|
- "src/AcDream.Core/**"
|
|
- "src/AcDream.Core.Net/**"
|
|
- "src/AcDream.Content/**"
|
|
- "src/AcDream.Plugin.Abstractions/**"
|
|
- "src/AcDream.Runtime/**"
|
|
- "src/AcDream.Headless/**"
|
|
- "src/AcDream.App/**"
|
|
- "src/AcDream.UI.Abstractions/**"
|
|
- "tests/AcDream.Platform.Tests/**"
|
|
- "tests/AcDream.Launcher.Core.Tests/**"
|
|
- "tests/AcDream.Core.Tests/**"
|
|
- "tests/AcDream.Core.Net.Tests/**"
|
|
- "tests/AcDream.Content.Tests/**"
|
|
- "tests/AcDream.Runtime.Tests/**"
|
|
- "tests/AcDream.Headless.Tests/**"
|
|
- "tests/AcDream.App.Tests/**"
|
|
- "tests/AcDream.UI.Abstractions.Tests/**"
|
|
- "tests/Fixtures/campaign-la/**"
|
|
- "tools/ShaderCompiler/**"
|
|
- "tools/compile-shaders.ps1"
|
|
push:
|
|
paths:
|
|
- ".github/workflows/headless-portability.yml"
|
|
- "AcDream.slnx"
|
|
- "src/AcDream.Platform/**"
|
|
- "src/AcDream.Launcher.Core/**"
|
|
- "src/AcDream.Core/**"
|
|
- "src/AcDream.Core.Net/**"
|
|
- "src/AcDream.Content/**"
|
|
- "src/AcDream.Plugin.Abstractions/**"
|
|
- "src/AcDream.Runtime/**"
|
|
- "src/AcDream.Headless/**"
|
|
- "src/AcDream.App/**"
|
|
- "src/AcDream.UI.Abstractions/**"
|
|
- "tests/AcDream.Platform.Tests/**"
|
|
- "tests/AcDream.Launcher.Core.Tests/**"
|
|
- "tests/AcDream.Core.Tests/**"
|
|
- "tests/AcDream.Core.Net.Tests/**"
|
|
- "tests/AcDream.Content.Tests/**"
|
|
- "tests/AcDream.Runtime.Tests/**"
|
|
- "tests/AcDream.Headless.Tests/**"
|
|
- "tests/AcDream.App.Tests/**"
|
|
- "tests/AcDream.UI.Abstractions.Tests/**"
|
|
- "tests/Fixtures/campaign-la/**"
|
|
- "tools/ShaderCompiler/**"
|
|
- "tools/compile-shaders.ps1"
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
portable-headless:
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [windows-latest, ubuntu-latest]
|
|
runs-on: ${{ matrix.os }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Install .NET 10
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: "10.0.x"
|
|
|
|
# No apt step here on purpose. This job's whole claim is that the closure
|
|
# below is presentation-free: it builds Plugin.Abstractions, Core,
|
|
# Core.Net, Content, Runtime and Headless, runs their tests, and invokes
|
|
# the Headless CLI. Nothing in it opens a display, links GL, or calls
|
|
# xvfb-run, so an "install the graphical smoke dependencies" step here was
|
|
# both unnecessary and, being unconditional `sudo apt-get` in a
|
|
# two-operating-system matrix, fatal on the windows-latest leg (exit 127,
|
|
# `sudo: command not found`). The graphical jobs that do use xvfb-run and
|
|
# jq run only on ubuntu-latest and take both from the runner image.
|
|
- name: Build presentation-free closure
|
|
shell: pwsh
|
|
run: |
|
|
$projects = @(
|
|
"src/AcDream.Platform/AcDream.Platform.csproj",
|
|
"src/AcDream.Launcher.Core/AcDream.Launcher.Core.csproj",
|
|
"src/AcDream.Plugin.Abstractions/AcDream.Plugin.Abstractions.csproj",
|
|
"src/AcDream.Core/AcDream.Core.csproj",
|
|
"src/AcDream.Core.Net/AcDream.Core.Net.csproj",
|
|
"src/AcDream.Content/AcDream.Content.csproj",
|
|
"src/AcDream.Runtime/AcDream.Runtime.csproj",
|
|
"src/AcDream.Headless/AcDream.Headless.csproj"
|
|
)
|
|
foreach ($project in $projects) {
|
|
dotnet build $project -c Release
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
}
|
|
|
|
# AcDream.Core.Tests still contains historical App integration fixtures,
|
|
# so it is deliberately not a member of this no-App restore lane. Core is
|
|
# built directly above and exercised through every portable downstream
|
|
# test project below; the complete solution lane retains Core.Tests.
|
|
- name: Test presentation-free closure
|
|
shell: pwsh
|
|
run: |
|
|
$projects = @(
|
|
"tests/AcDream.Platform.Tests/AcDream.Platform.Tests.csproj",
|
|
"tests/AcDream.Launcher.Core.Tests/AcDream.Launcher.Core.Tests.csproj",
|
|
"tests/AcDream.Core.Net.Tests/AcDream.Core.Net.Tests.csproj",
|
|
"tests/AcDream.Content.Tests/AcDream.Content.Tests.csproj",
|
|
"tests/AcDream.Runtime.Tests/AcDream.Runtime.Tests.csproj",
|
|
"tests/AcDream.Headless.Tests/AcDream.Headless.Tests.csproj"
|
|
)
|
|
foreach ($project in $projects) {
|
|
dotnet test $project -c Release
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
}
|
|
|
|
- name: Verify CLI without connecting
|
|
shell: pwsh
|
|
run: |
|
|
dotnet run --project src/AcDream.Headless/AcDream.Headless.csproj -c Release -- --help
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
Set-Content -LiteralPath headless-k0.json -Value '{"version":1,"sessions":[]}'
|
|
dotnet run --project src/AcDream.Headless/AcDream.Headless.csproj -c Release -- validate --config headless-k0.json
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
|
|
linux-graphical:
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Install .NET 10
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: "10.0.x"
|
|
|
|
- name: Build and publish Linux graphical client
|
|
shell: pwsh
|
|
run: |
|
|
dotnet build src/AcDream.App/AcDream.App.csproj -c Release -r linux-x64
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
dotnet publish src/AcDream.App/AcDream.App.csproj `
|
|
-c Release `
|
|
-r linux-x64 `
|
|
--self-contained false `
|
|
-o artifacts/acdream-linux-x64
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
|
|
- name: Test portable graphical boundary
|
|
shell: pwsh
|
|
run: |
|
|
dotnet test `
|
|
tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj `
|
|
-c Release
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
dotnet test `
|
|
tests/AcDream.App.Tests/AcDream.App.Tests.csproj `
|
|
-c Release `
|
|
--filter "FullyQualifiedName~LinuxMonotonicFramePacingWaiterTests|FullyQualifiedName~LinuxPlatformBoundaryTests|FullyQualifiedName~GraphicalHostPlatformServicesTests|FullyQualifiedName~GraphicalLegacyConfigurationMigratorTests|FullyQualifiedName~GraphicalWindowBackendSelectionTests"
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
|
|
- name: Verify Linux package contract
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
root=artifacts/acdream-linux-x64
|
|
test -x "$root/AcDream.App"
|
|
test -f "$root/AcDream.App.dll"
|
|
test -f "$root/libglfw.so.3"
|
|
test -f "$root/libopenal.so"
|
|
test -f "$root/Rendering/Shaders/mesh_modern.vert"
|
|
test -f "$root/plugins/AcDream.Plugins.Smoke/AcDream.Plugins.Smoke.dll"
|
|
test -f "$root/plugins/AcDream.Plugins.Smoke/plugin.json"
|
|
|
|
test "$(grep -RIl --include='*.cs' 'LibraryImport(\"kernel32.dll\"' \
|
|
src/AcDream.App | wc -l)" -eq 1
|
|
grep -q 'WindowsHighResolutionFramePacingWaiter.cs' < <(
|
|
grep -RIl --include='*.cs' 'LibraryImport(\"kernel32.dll\"' \
|
|
src/AcDream.App)
|
|
! grep -RIn --include='*.cs' 'LocalApplicationData' \
|
|
src/AcDream.App src/AcDream.UI.Abstractions
|
|
! grep -RIn --include='*.cs' \
|
|
'WindowsHighResolutionFramePacingWaiter.Create()' \
|
|
src/AcDream.App \
|
|
--exclude='FramePacingWaiterFactory.cs' \
|
|
--exclude='WindowsHighResolutionFramePacingWaiter.cs'
|
|
|
|
# Campaign V slice V9 built this job to prove lavapipe (Mesa's software
|
|
# Vulkan) passes the capability gate: every Vulkan feature acdream requires
|
|
# is core 1.3 or a descriptor-indexing feature lavapipe implements. That made
|
|
# it the first CI job in the project's history to render a frame.
|
|
#
|
|
# Until Campaign V slice V11, linux-graphical (above) carried the mirror
|
|
# case: llvmpipe (Mesa's software OpenGL) FAILING the capability gate,
|
|
# because mandatory GL_ARB_bindless_texture has no llvmpipe implementation.
|
|
# V11 deleted the GL backend entirely, so there is no more GL capability
|
|
# gate for any driver to pass or fail — that job's "Verify actionable
|
|
# unsupported-driver gate" step went with it. "Verify the forced-unsupported
|
|
# gate exits 4" below is what now proves the exit-code-4 contract still
|
|
# fires, forcing an unsupported VULKAN feature instead.
|
|
#
|
|
# NOT DONE HERE, deliberately: a GL-versus-Vulkan pixel comparison. It was
|
|
# never viable in CI even before V11 — the probe harness renders synthetic
|
|
# verification scenes rather than the world, and the world needs retail DATs
|
|
# that CI does not have and cannot be given. The real GL-versus-Vulkan
|
|
# differential was V7's, on the developer machine, against the DATs, with
|
|
# both clocks pinned, before V11 deleted the GL arm it depended on.
|
|
linux-vulkan:
|
|
runs-on: ubuntu-latest
|
|
|
|
# Redirect the portable per-user roots into the workspace so the capability
|
|
# report, which the app writes to its own diagnostics directory rather than
|
|
# to a path a caller chooses, lands somewhere collectable. Slice L0 made
|
|
# these XDG-driven precisely so a host could place them.
|
|
env:
|
|
XDG_CONFIG_HOME: ${{ github.workspace }}/artifacts/xdg/config
|
|
XDG_DATA_HOME: ${{ github.workspace }}/artifacts/xdg/data
|
|
XDG_CACHE_HOME: ${{ github.workspace }}/artifacts/xdg/cache
|
|
VULKAN_REPORT: ${{ github.workspace }}/artifacts/xdg/cache/acdream/diagnostics/graphical-capabilities-vulkan.json
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Install .NET 10
|
|
uses: actions/setup-dotnet@v4
|
|
with:
|
|
dotnet-version: "10.0.x"
|
|
|
|
- name: Install lavapipe, the Vulkan loader and Xvfb
|
|
shell: bash
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends \
|
|
jq \
|
|
libvulkan1 \
|
|
mesa-vulkan-drivers \
|
|
vulkan-tools \
|
|
xauth \
|
|
xvfb
|
|
|
|
- name: Record the software Vulkan device
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p artifacts
|
|
# Evidence, not configuration: no ICD is forced, because on a runner
|
|
# with no GPU lavapipe is the only one the loader can find. If that
|
|
# ever stops being true the DeviceType assertion below turns red
|
|
# rather than silently measuring different hardware, which is the
|
|
# outcome this row wants; its whole point is that the gate passes on
|
|
# the weakest conformant device in existence.
|
|
ls -l /usr/share/vulkan/icd.d/ || true
|
|
vulkaninfo --summary 2>&1 | tee artifacts/vulkaninfo-summary.txt
|
|
|
|
- name: Build and publish the Linux graphical client
|
|
shell: pwsh
|
|
run: |
|
|
dotnet publish src/AcDream.App/AcDream.App.csproj `
|
|
-c Release `
|
|
-r linux-x64 `
|
|
--self-contained false `
|
|
-o artifacts/acdream-linux-x64
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
|
|
- name: Test the Vulkan backend's platform-independent decisions
|
|
shell: pwsh
|
|
run: |
|
|
dotnet test `
|
|
tests/AcDream.App.Tests/AcDream.App.Tests.csproj `
|
|
-c Release `
|
|
--filter "FullyQualifiedName~AcDream.App.Tests.Rendering.Gpu.Vk"
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
|
|
# (a) + (b): one run, two gates. The harness opens a real window, runs the
|
|
# capability gate against a real device, and presents the V6c/V6d
|
|
# verification scenes through the real RHI. ACDREAM_VULKAN_PROBE_FRAMES is
|
|
# what makes it terminate: nothing in CI ever closes a window.
|
|
- name: Probe the Vulkan capability gate on lavapipe
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
root=artifacts/acdream-linux-x64
|
|
out="$PWD/artifacts/vulkan-probe"
|
|
mkdir -p "$out"
|
|
|
|
# 24-bit depth explicitly: xvfb-run's default screen is 8-bit, which
|
|
# leaves the X11 WSI without a usable visual.
|
|
ACDREAM_RENDER_BACKEND=vulkan \
|
|
ACDREAM_VULKAN_PROBE=1 \
|
|
ACDREAM_VULKAN_PROBE_FRAMES=30 \
|
|
ACDREAM_AUTOMATION_ARTIFACT_DIR="$out" \
|
|
ACDREAM_DISPLAY_PROTOCOL=x11 \
|
|
ACDREAM_NO_AUDIO=1 \
|
|
xvfb-run -a -s "-screen 0 1920x1080x24" \
|
|
"$root/AcDream.App" /tmp/not-needed 2>&1 | tee artifacts/vulkan-probe.log
|
|
|
|
test -f "$VULKAN_REPORT"
|
|
cp "$VULKAN_REPORT" artifacts/vulkan-capabilities-pass.json
|
|
|
|
jq -r '"device: \(.DeviceName) (\(.DeviceType)), \(.DeviceApiVersion), \(.DriverInfo)"' \
|
|
"$VULKAN_REPORT"
|
|
|
|
# The gate accepted the device outright.
|
|
jq -e '.SupportFailures | length == 0' "$VULKAN_REPORT"
|
|
test "$(jq -r '.ActiveDisplayProtocol' "$VULKAN_REPORT")" = X11
|
|
# A software device, which is the whole point of this row.
|
|
test "$(jq -r '.DeviceType' "$VULKAN_REPORT")" = Cpu
|
|
# Vulkan 1.3 floor, unpacked from VK_MAKE_API_VERSION: major is bits
|
|
# 22+, minor is bits 12-21. Evaluate() already rejects anything lower,
|
|
# so this asserts the report agrees with the verdict rather than
|
|
# re-deriving it.
|
|
jq -e '
|
|
((.DeviceApiVersionPacked / 4194304) | floor) as $major
|
|
| (((.DeviceApiVersionPacked % 4194304) / 4096) | floor) as $minor
|
|
| $major > 1 or ($major == 1 and $minor >= 3)' "$VULKAN_REPORT"
|
|
# Advertisement is not evidence: the active probe created the device,
|
|
# built the descriptor layouts and a pipeline from committed .spv, drew
|
|
# an offscreen triangle and read the pixels back.
|
|
jq -e '.FunctionProbe.Failures | length == 0' "$VULKAN_REPORT"
|
|
jq -e '.FunctionProbe.DeviceCreation and .FunctionProbe.OffscreenReadback' \
|
|
"$VULKAN_REPORT"
|
|
|
|
- name: Assert the offline render produced a real frame
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
png=artifacts/vulkan-probe/vulkan-bringup.png
|
|
test -f "$png"
|
|
|
|
# PNG IHDR carries the dimensions at bytes 16..23, big-endian. Reading
|
|
# them proves the capture path returned a full backbuffer rather than a
|
|
# stub, without depending on a rasterizer's pixel values.
|
|
width=$(od -An -tu4 -j16 -N4 --endian=big "$png" | tr -d ' ')
|
|
height=$(od -An -tu4 -j20 -N4 --endian=big "$png" | tr -d ' ')
|
|
bytes=$(stat -c%s "$png")
|
|
echo "captured ${width}x${height}, ${bytes} bytes"
|
|
|
|
test "$width" -ge 640
|
|
test "$height" -ge 360
|
|
# A uniform-colour frame at this size encodes to a few kilobytes. This
|
|
# threshold is the "the scene actually drew" line, and is deliberately
|
|
# a byte count rather than a pixel baseline: lavapipe and any other
|
|
# rasterizer are free to disagree about shading, and nothing in CI has
|
|
# a reference frame to disagree with.
|
|
test "$bytes" -gt 8192
|
|
|
|
# (c) The gate's failure path, exercised on a device that actually
|
|
# supports everything, so the exit-code-4 contract is proven rather than
|
|
# assumed. Same knob slice V5 built for exactly this.
|
|
- name: Verify the forced-unsupported gate exits 4
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
root=artifacts/acdream-linux-x64
|
|
out="$PWD/artifacts/vulkan-forced"
|
|
mkdir -p "$out"
|
|
|
|
set +e
|
|
ACDREAM_RENDER_BACKEND=vulkan \
|
|
ACDREAM_VULKAN_PROBE=1 \
|
|
ACDREAM_VULKAN_PROBE_FRAMES=30 \
|
|
ACDREAM_VULKAN_FORCE_UNSUPPORTED=timelineSemaphore \
|
|
ACDREAM_AUTOMATION_ARTIFACT_DIR="$out" \
|
|
ACDREAM_DISPLAY_PROTOCOL=x11 \
|
|
ACDREAM_NO_AUDIO=1 \
|
|
xvfb-run -a -s "-screen 0 1920x1080x24" \
|
|
"$root/AcDream.App" /tmp/not-needed \
|
|
> artifacts/vulkan-forced.log 2>&1
|
|
code=$?
|
|
set -e
|
|
|
|
cat artifacts/vulkan-forced.log
|
|
test -f "$VULKAN_REPORT"
|
|
cp "$VULKAN_REPORT" artifacts/vulkan-capabilities-forced.json
|
|
|
|
test "$code" -eq 4
|
|
test "$(jq -r '.ForcedUnsupportedFeature' "$VULKAN_REPORT")" = timelineSemaphore
|
|
test "$(jq -r '.Features.TimelineSemaphore' "$VULKAN_REPORT")" = false
|
|
jq -e '.SupportFailures | length > 0' "$VULKAN_REPORT"
|
|
jq -e '.SupportFailures | any(test("timelineSemaphore"))' "$VULKAN_REPORT"
|
|
# The refusal must be actionable: the operator is told where the full
|
|
# report is, not merely that something was unsupported.
|
|
grep -q 'graphical-capabilities-vulkan.json' artifacts/vulkan-forced.log
|
|
|
|
# (d) The committed .spv are the only shaders the Vulkan backend ever
|
|
# loads. An App test already re-hashes the GLSL sources against the
|
|
# manifest, which catches "edited a shader, forgot to recompile". Nothing
|
|
# until now tied the committed BINARIES to those sources, so a stale or
|
|
# hand-edited .spv would have shipped silently. Recompiling here closes
|
|
# that, and does it on a second operating system.
|
|
- name: Verify the committed SPIR-V is fresh
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
committed=src/AcDream.App/Rendering/Shaders/spv
|
|
fresh="$PWD/artifacts/spv-fresh"
|
|
rm -rf "$fresh"
|
|
mkdir -p "$fresh"
|
|
|
|
pwsh tools/compile-shaders.ps1 -OutputDirectory "$fresh"
|
|
|
|
# The file SET first: a .spv present in one tree and not the other is
|
|
# drift the per-file compare would never visit.
|
|
diff <(cd "$committed" && ls -1 | sort) <(cd "$fresh" && ls -1 | sort)
|
|
|
|
drift=0
|
|
for path in "$committed"/*.spv; do
|
|
name=$(basename "$path")
|
|
if ! cmp -s "$path" "$fresh/$name"; then
|
|
echo "DRIFT: $name differs from a fresh compile"
|
|
echo " committed $(sha256sum "$path" | cut -d' ' -f1) $(stat -c%s "$path") bytes"
|
|
echo " fresh $(sha256sum "$fresh/$name" | cut -d' ' -f1) $(stat -c%s "$fresh/$name") bytes"
|
|
drift=$((drift + 1))
|
|
fi
|
|
done
|
|
|
|
# The manifest is compared as JSON rather than as bytes: it is written
|
|
# with Environment.NewLine, so a byte compare would report drift for
|
|
# the operating system rather than for the shaders.
|
|
diff <(jq -S . "$committed/shaders.manifest.json") \
|
|
<(jq -S . "$fresh/shaders.manifest.json")
|
|
|
|
if [ "$drift" -ne 0 ]; then
|
|
echo "$drift .spv artifact(s) are stale."
|
|
echo "Run tools/compile-shaders.ps1 and commit the result."
|
|
exit 1
|
|
fi
|
|
echo "all committed .spv match a fresh compile"
|
|
|
|
- name: Upload Vulkan evidence
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: linux-vulkan-evidence
|
|
if-no-files-found: warn
|
|
path: |
|
|
artifacts/vulkaninfo-summary.txt
|
|
artifacts/vulkan-probe.log
|
|
artifacts/vulkan-forced.log
|
|
artifacts/vulkan-capabilities-pass.json
|
|
artifacts/vulkan-capabilities-forced.json
|
|
artifacts/vulkan-probe/*.png
|