ci(render): Campaign V slice V9 - the Vulkan gate runs on lavapipe
The first CI job in this project's history that renders a frame.
The whole row rests on a decision V6g already made and paid for. When
section 5.5.8 cut set 0 from ten dynamic storage descriptors to four, four
was not merely under the RX 9070 XT's eight - it is Vulkan's guaranteed
minimum, so no conformant device can fail the layout. That is what makes a
software-device row possible at all. Every other requirement was then
checked against Mesa's lvp_device.c rather than assumed, and all seventeen
features the gate demands are true on lavapipe - including
samplerAnisotropy, which V7 made load-bearing eight commits ago and which a
software rasterizer would have been entirely within its rights to decline.
Three things had to exist before the job could:
1. The harness could not stop. VulkanBringUpHost presents until its window
closes, which is right at a desk and impossible in CI, where nothing ever
closes a window. ACDREAM_VULKAN_PROBE_FRAMES gives it a budget; unset or
malformed is zero, which keeps the interactive behaviour, so no existing
invocation changes. The budget never cuts the capture short - the loop
stays open until the screenshot has been attempted - because a run whose
entire product is a PNG must not be able to exit green with an empty
artifact directory. The decision is a pure static method, tested without
a window or a driver.
2. tools/compile-shaders.ps1 was Windows-only and nobody had noticed,
because nothing had ever run it anywhere else. It built its paths from
embedded 'src\AcDream.App\...' literals; a backslash is a separator on
Windows and an ordinary filename character everywhere else, so on Linux
that is one long nonexistent file name.
3. The report's jq paths were invisible to the compiler. Renaming a record
property or swapping the enum converter would have left every test green
and turned CI red on someone else's branch days later, with a failure
that reads like a driver problem. VulkanCapabilityReportContractTests
pins the exact strings the job greps and pins its packed-version
arithmetic against VulkanApiVersion's own unpacking.
The job, eleven steps: install lavapipe and Xvfb; record vulkaninfo as
evidence; publish linux-x64; run the Gpu.Vk tests on a second operating
system; probe the gate under a 24-bit Xvfb screen (the default is 8-bit,
which leaves the X11 WSI without a usable visual) and assert an accepting
verdict on a Cpu device at API >= 1.3 with a clean active probe; assert the
captured PNG is a real frame by IHDR dimensions and byte count; re-run with
ACDREAM_VULKAN_FORCE_UNSUPPORTED=timelineSemaphore and assert exit 4 with an
actionable refusal; recompile the shaders and compare. Artifacts upload on
always(), so a red run ships its own diagnosis.
The .spv step is what ties the committed binaries to their sources. The
existing App test hashes GLSL against the manifest, which catches "edited a
shader, forgot to recompile"; nothing caught a stale or hand-edited .spv.
Verified on Windows before shipping: 19/19 artifacts byte-identical to a
fresh compile, zero drift.
No GL-versus-Vulkan pixel compare, for two independent reasons recorded in
section 5.5.20: linux-graphical asserts exit 4, so there is no left-hand
side, and the probe renders synthetic scenes rather than the DAT world CI
cannot have. The two jobs now say something sharper than a pixel diff would
have - on the same software Mesa stack, GL is refused and Vulkan is accepted
and draws. Physical Linux GPU and Wayland rows stay deferred on the Slice L
precedent; no hosted runner offers either.
Gates: Release build green, zero errors. App tests 4,152 / 3 skipped against
a 4,134 / 3 baseline at this branch's base (9b7f4343) - eighteen new, all
from this slice. Workflow validated by a real YAML parse plus an Actions
schema check and bash -n over all nine extracted run blocks; no actionlint
was available locally and none was downloaded. The job itself has not run:
its first execution is the CI run this commit triggers, and the V9 row stays
partial until that is green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9b7f434376
commit
a13cff884f
8 changed files with 784 additions and 10 deletions
257
.github/workflows/headless-portability.yml
vendored
257
.github/workflows/headless-portability.yml
vendored
|
|
@ -21,6 +21,8 @@ on:
|
|||
- "tests/AcDream.Headless.Tests/**"
|
||||
- "tests/AcDream.App.Tests/**"
|
||||
- "tests/AcDream.UI.Abstractions.Tests/**"
|
||||
- "tools/ShaderCompiler/**"
|
||||
- "tools/compile-shaders.ps1"
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/headless-portability.yml"
|
||||
|
|
@ -41,6 +43,8 @@ on:
|
|||
- "tests/AcDream.Headless.Tests/**"
|
||||
- "tests/AcDream.App.Tests/**"
|
||||
- "tests/AcDream.UI.Abstractions.Tests/**"
|
||||
- "tools/ShaderCompiler/**"
|
||||
- "tools/compile-shaders.ps1"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
|
@ -202,3 +206,256 @@ jobs:
|
|||
test "$(jq -r '.Lifecycle.OwnedGlApiCount' "$report")" -eq 0
|
||||
test "$(jq -r '.Lifecycle.OwnedInputContextCount' "$report")" -eq 0
|
||||
jq -e '.SupportFailures | length > 0' "$report"
|
||||
|
||||
# Campaign V slice V9. The GL job above proves the unsupported-driver gate
|
||||
# fires on Mesa's software OpenGL; this one proves the Vulkan backend does the
|
||||
# opposite on Mesa's software Vulkan. lavapipe passes the capability gate that
|
||||
# llvmpipe-GL cannot, because mandatory GL_ARB_bindless_texture has no llvmpipe
|
||||
# implementation while every Vulkan feature acdream requires is core 1.3 or a
|
||||
# descriptor-indexing feature lavapipe implements. That makes this the first CI
|
||||
# job in the project's history that renders a frame.
|
||||
#
|
||||
# NOT DONE HERE, deliberately: a GL-versus-Vulkan pixel comparison. Two
|
||||
# independent reasons, either of which alone is disqualifying. First, the GL
|
||||
# job never produces a frame at all: it asserts exit code 4, so there is no
|
||||
# left-hand side. Second, even if llvmpipe-GL could run, 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 is V7's, on the developer machine, against the
|
||||
# DATs, with both clocks pinned. See plan section 5.5.20.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -619,7 +619,7 @@ tenth pair with no consumer at all; see the V6e report.
|
|||
| **V6m** ✅ | **Portal space, and V7's instrument**, reported in §5.5.18. Two commits: **1** `PortalTunnelPresentation`'s RHI arm — the last raw-GL world-adjacent renderer — as a backbuffer pass published on `IWorldPassScope`, clearing to retail's opaque portal-space black rather than loading it, plus the deletion of `NullLocalPlayerTeleportPresentation` (`59c6b2ae`); **2** `tools/run-backend-differential-gate.ps1` and `connected-backend-differential.route.txt`, with MSAA forced off on both launches, the repeat gate's desktop-witness guards, and **an interior EnvCell stop** — §5.1's durable fix for the campaign's oldest coverage gap (`a99f517e`). One smoke pair was run and is reported in full. | GL pixel gate 4.97e-05 (28 px) vs `280f3b3f` against a 3.55e-05 (20 px) same-commit control, band 9–31; App tests 4,132/3 and complete Release suite 9,195/5; GL connected `-Runs 3` at 3/3 on both columns; one validation-layer Vulkan run at 0 errors / 0 warnings; connected portal-tunnel, creature-appraisal and interior-EnvCell captures on BOTH backends, inspected in §5.5.18 |
|
||||
| **V7** ◐ **partially discharged — §5.5.19** | GL-versus-Vulkan differential: `tools/run-backend-differential-gate.ps1` (**built at V6m**), strict paired-PNG compare, divergences fixed in the Vulkan backend only, then lifecycle + R6 soak natively on Vulkan, one validation-layer-clean run, one RenderDoc capture. **Milestone: parity.** Starting distance, measured at V6m: 18.52% of the frame at the first stop. **What landed:** the world atlases' missing anisotropy (`ad5f8b68`, retail-anchored at `0x005a4230`), and two instrument pins the gate was silently missing — the cloud sheet's phase and, much larger, the Dereth clock, which had never actually been pinned by anything and was moving 22% of the frame between two captures 45 s apart *in the same run*. Offline GL-versus-Vulkan, both clocks pinned, is now **8.82e-04 below the tree band — inside the threshold**; the treeline is `AD-46`, proven not to be the depth class. **What did NOT land:** a passing connected stop (each carries a named phase exception), per-stop masks in the gate script, an aperture stop for the portal depth mask, the R6 soak on Vulkan, and the RenderDoc capture. | every differential checkpoint passes; both connected routes green on VK |
|
||||
| **V8** | Perf gate on the RX 9070 XT, uncapped, both backends, same route. | §2 acceptance table; parity is the floor |
|
||||
| **V9** | Linux + CI: X11/Wayland surfaces; a `linux-vulkan` job on lavapipe (probe accepts on a real 1.3 software device, a short real render under xvfb, forced-unsupported → exit 4, `.spv` freshness). Physical Linux GPU row deferred post-cutover, as for Slice L. | CI green including the new job |
|
||||
| **V9** ◐ **implemented, first CI run pending — §5.5.20** | Linux + CI: a `linux-vulkan` job on lavapipe (probe accepts on a real 1.3+ software device, a short real render under xvfb, forced-unsupported → exit 4, `.spv` freshness). **What landed:** the eleven-step job; `ACDREAM_VULKAN_PROBE_FRAMES`, without which the harness cannot terminate unattended; `tools/compile-shaders.ps1` made path-portable; and the report's jq contract pinned by App tests so a rename fails locally rather than in CI. lavapipe clears every gate requirement by source inspection, including the `samplerAnisotropy` V7 made load-bearing. **Deferred:** the physical Linux GPU row, post-cutover, as for Slice L; and Wayland, which no runner offers. **Not attempted, with cause:** a GL-versus-Vulkan pixel comparison — the GL job asserts exit 4 and so has no frame, and the probe renders synthetic scenes rather than the DAT world CI cannot have. | CI green including the new job |
|
||||
| **V10** | Cutover: Vulkan default, GL reachable by env var for one slice, gate scripts default to VK. | complete Release suite + retail expected PNGs **on VK** (baselines not regenerated) + both connected routes + **user visual sign-off** |
|
||||
| **V11** | GL deletion and closeout: delete `Gpu/Gl`, `OpenGLGraphicsDevice`, `ManagedGL*`, `GLSLShader`, `GLHelpers`, `GLStateScope`, `RenderStateCache`, `BindlessSupport`, `GraphicalGlFunctionProbe`, the GL branch in `GameWindow`, the ImGui project and Studio; drop the GL and (if the audit is clean) Chorizite packages; file the retained-UI dev-panels follow-up; swap CI assertions to VK; update the divergence register, architecture doc, code-structure doc, and rendering memory crib; re-measure memory. | complete Release suite + both connected routes + working-set re-measure |
|
||||
|
||||
|
|
@ -2746,6 +2746,115 @@ zero warnings**. Final: complete Release suite **9,195 passed / 5 skipped, zero
|
|||
failures** — no `AcDream.Content` flake this time; GL connected repeat gate
|
||||
`-Runs 3` at **3/3 RENDERED** on both the desktop witness and the client capture.
|
||||
|
||||
#### 5.5.20 V9 (2026-07-28): the first CI job that renders a frame
|
||||
|
||||
**The whole slice rests on one fact §5.5.8 already bought and paid for.** When
|
||||
V6g cut set 0 from ten dynamic storage descriptors to four, it did so on the rule
|
||||
that a dynamic descriptor is for ring-fed data whose offset moves and nothing
|
||||
else — and four is not merely under the RX 9070 XT's eight, it is *Vulkan's
|
||||
guaranteed minimum*. That decision is what makes a lavapipe row possible at all.
|
||||
Every other requirement was then checked against Mesa's
|
||||
`src/gallium/frontends/lavapipe/lvp_device.c` rather than assumed, and **every
|
||||
one of the seventeen features the gate demands is `true`**, including the two
|
||||
that looked most likely to bite:
|
||||
|
||||
- **`samplerAnisotropy`** — `.samplerAnisotropy = true`, an unconditional literal
|
||||
since 2021, not a pipe-cap query. This was the real risk: V7 made anisotropy
|
||||
load-bearing eight commits ago, and a software rasterizer declining to filter
|
||||
anisotropically would have been entirely reasonable of it.
|
||||
- **`textureCompressionBC`** — `true`, so the DAT surfaces upload without
|
||||
transcoding. (ETC2 and ASTC are `false`; the gate does not ask for them.)
|
||||
|
||||
`maxDescriptorSetStorageBuffersDynamic` is 500,000, `maxBoundDescriptorSets` 8,
|
||||
`maxPushConstantsSize` 256, timestamps supported, and the X11 WSI has a software
|
||||
non-SHM present path, so it works under Xvfb without DRI3. Two notes for whoever
|
||||
touches this next: lavapipe offers **only 1x and 4x** MSAA — the harness asks for
|
||||
4, which is fine, and 2x or 8x would not be — and it reports **API 1.4** on the
|
||||
Mesa 25.2 that Ubuntu 24.04 now ships while `conformanceVersion` stays 1.3, so
|
||||
the job asserts `>= 1.3` and never `== 1.3`.
|
||||
|
||||
**Three things had to be built before a job could exist.**
|
||||
|
||||
**1. The harness could not stop.** `VulkanBringUpHost.Present()` presents until
|
||||
its window closes, which is exactly right for a developer answering "does this
|
||||
machine pass?" at a desk and impossible in CI, where nothing ever closes a
|
||||
window. `ACDREAM_VULKAN_PROBE_FRAMES` gives it a frame budget; unset or malformed
|
||||
is zero, which keeps the interactive behaviour, so no existing invocation
|
||||
changes. The budget **never cuts the capture short** — the loop stays open until
|
||||
the screenshot has been attempted — because a run whose entire product is a PNG
|
||||
must not be able to exit green with an empty artifact directory. The decision is
|
||||
a pure static method so it is tested without a window or a driver.
|
||||
|
||||
**2. `tools/compile-shaders.ps1` was Windows-only** and nobody had noticed,
|
||||
because nothing had ever run it anywhere else. It composed its paths from
|
||||
embedded `'src\AcDream.App\Rendering\Shaders'` literals; a backslash is a path
|
||||
separator on Windows and an ordinary filename character everywhere else, so on
|
||||
Linux that is one long nonexistent file name. Now composed with
|
||||
`[System.IO.Path]::Combine`, and the `glslc` probe looks for the SDK's Linux
|
||||
layout as well as its Windows one.
|
||||
|
||||
**3. The report's jq paths were invisible to the compiler.** The job reads its
|
||||
verdict out of `graphical-capabilities-vulkan.json` with `jq`. Renaming a record
|
||||
property or swapping the enum converter would have left every existing test green
|
||||
and turned CI red on someone else's branch days later, with a failure that reads
|
||||
like a driver problem. `VulkanCapabilityReportContractTests` pins the exact
|
||||
strings — `"Cpu"`, `"X11"`, `SupportFailures`, `FunctionProbe.Failures`,
|
||||
`Features.TimelineSemaphore` — and pins the job's packed-version arithmetic
|
||||
against `VulkanApiVersion`'s own unpacking.
|
||||
|
||||
**The job, eleven steps.** Install lavapipe + loader + Xvfb; record
|
||||
`vulkaninfo --summary` as evidence; publish `linux-x64`; run the `Gpu.Vk` tests
|
||||
on a second operating system; **(a)** run the probe under
|
||||
`xvfb-run -s "-screen 0 1920x1080x24"` — 24-bit explicitly, because xvfb's
|
||||
default screen is 8-bit and leaves the X11 WSI without a usable visual — and
|
||||
assert an accepting verdict on a `Cpu` device at API >= 1.3 with a clean active
|
||||
probe; **(b)** assert the captured PNG is a real frame by IHDR dimensions and
|
||||
byte count; **(c)** re-run with `ACDREAM_VULKAN_FORCE_UNSUPPORTED=timelineSemaphore`
|
||||
and assert exit **4**, the forced feature recorded, a failure sentence naming it,
|
||||
and a refusal message that tells the operator where the report is; **(d)**
|
||||
recompile the shaders and compare. Every artifact uploads on `always()`, so a
|
||||
red run ships its own diagnosis.
|
||||
|
||||
**On (d), what it adds over the existing test.** `VulkanShaderManifestTests`
|
||||
re-hashes the GLSL against the manifest, which catches "edited a shader, forgot
|
||||
to recompile". Nothing tied the committed **binaries** to those sources, so a
|
||||
stale or hand-edited `.spv` would have shipped silently. The job recompiles and
|
||||
compares each `.spv` byte-for-byte plus the file set; the manifest is compared as
|
||||
parsed JSON rather than as bytes, because it is written with
|
||||
`Environment.NewLine` and a byte compare would report drift for the operating
|
||||
system rather than for the shaders. **Measured on Windows before shipping: 18
|
||||
`.spv` plus the manifest, 19/19 byte-identical to a fresh compile, zero drift.**
|
||||
Whether Linux shaderc agrees byte-for-byte with Windows shaderc at the same
|
||||
pinned Silk.NET 2.23.0 is the one thing this slice asserts without having
|
||||
observed; the job is the instrument that answers it, and a mismatch there is a
|
||||
real finding about toolchain determinism, not a threshold to relax.
|
||||
|
||||
**What was NOT done, and why — the pixel comparison.** The brief allowed a
|
||||
relaxed GL-versus-Vulkan smoke compare *if the GL job already produced a
|
||||
comparable frame*. It does not, for two independent reasons either of which is
|
||||
disqualifying. First, `linux-graphical` asserts **exit code 4**: llvmpipe-GL has
|
||||
no `GL_ARB_bindless_texture`, the gate refuses it, and there is no left-hand side
|
||||
to compare. Second, even given a GL frame, the probe harness renders the
|
||||
synthetic V6c/V6d verification scenes, not the world — and the world needs retail
|
||||
DATs that CI does not have and cannot be given. The real differential is V7's, on
|
||||
the developer machine, against the DATs, with both clocks pinned. The two jobs
|
||||
now say something sharper than a pixel diff would have: **on the same software
|
||||
Mesa stack, GL is refused and Vulkan is accepted and draws.**
|
||||
|
||||
**Also deferred.** The physical Linux GPU row, post-cutover, on the Slice L
|
||||
precedent — no hosted runner has a GPU, and Slice L's L1 checkpoint already
|
||||
records a supported physical AMD/NVIDIA driver row as its own first gate.
|
||||
Wayland, because no hosted runner offers a Wayland session; L1's immutable
|
||||
Win32/X11/Wayland selection is exercised on X11 here and on Windows natively.
|
||||
|
||||
**Gates.** Release build green, zero warnings. App tests **4,152 / 3 skipped**
|
||||
against a **4,134 / 3** baseline measured at this worktree's base commit
|
||||
(`9b7f4343`) — eighteen new, all from this slice. Workflow validated by a real
|
||||
YAML parse (YamlDotNet) plus a GitHub-Actions schema check and `bash -n` over
|
||||
every extracted `run` block: 9/9 clean, no `actionlint` available locally and
|
||||
none downloaded. **The job itself has not run**: its first execution is the CI
|
||||
run this commit triggers, and the row stays ◐ until that is green.
|
||||
|
||||
### 5.4 The null-target `BeginPass` divergence (V4c) — ✅ DISCHARGED at V6k
|
||||
|
||||
> **Closed 2026-07-28 by V6k commit 2 (`eb7e6b4e`); see §5.5.16.** The answer is
|
||||
|
|
@ -2819,11 +2928,11 @@ indistinguishable from the fork option (B) permits.
|
|||
| sRGB mismatch (global gamma shift) | Decided at V3 from the actual GL state; a mismatch fails every pixel at V7, so it cannot pass silently. |
|
||||
| MSAA sample positions differ across backends | Strict gates run MSAA off; MSAA on gets a relaxed (0.01) visual smoke; a register row lands at V11. |
|
||||
| ~15,000 lines of renderer churn destabilizing retail fidelity | CPU logic never forks; each port is self-differential on the still-shipping backend; V0 pins the contract so subagents never negotiate APIs; the architecture test prevents seam erosion. |
|
||||
| Driver matrix — only one physical GPU (RX 9070 XT) | Conservative universal feature floor; lavapipe in CI as a second real implementation; one validation-layer-clean run at V7; the physical Linux row is deferred exactly as Slice L deferred it. |
|
||||
| Driver matrix — only one physical GPU (RX 9070 XT) | Conservative universal feature floor; **lavapipe landed at V9 as a second real implementation** — §5.5.20 verified all seventeen required features against Mesa's source, and §5.5.8's four dynamic descriptors are Vulkan's guaranteed minimum; one validation-layer-clean run at V7; the physical Linux row is deferred exactly as Slice L deferred it. |
|
||||
| Swapchain lifecycle (resize, minimize, RDP) | Owned explicitly at V5 and exercised by the connected lifecycle gate. |
|
||||
| App tests breaking as renderers change signatures | `RecordingGpuDevice` ships at V0; each slice updates its renderers' test constructions in the same commit. |
|
||||
| Hidden Chorizite consumers | V1 builds the device root without Chorizite inheritance; V4h audits the remainder; the package drops at V11 only if that audit is clean. |
|
||||
| `.spv` staleness | Single GLSL source, committed `.spv`, regeneration script, and a CI hash-freshness check. |
|
||||
| `.spv` staleness | Single GLSL source, committed `.spv`, regeneration script, an App test hashing the GLSL against the manifest, and — **landed at V9** — a CI step that recompiles and compares every `.spv` byte-for-byte, which is what ties the committed binaries to those sources rather than merely tying the manifest to them. |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -137,6 +137,13 @@ internal sealed class VulkanBringUpHost : IDisposable
|
|||
/// <summary>
|
||||
/// The frame loop: record the verification scenes through the RHI, present,
|
||||
/// and capture one screenshot once the scene has settled.
|
||||
///
|
||||
/// <para>Campaign V slice V9: when <c>ACDREAM_VULKAN_PROBE_FRAMES</c> is
|
||||
/// positive the loop retires itself after that many presented frames instead
|
||||
/// of waiting for the window to close. CI has no one to close it. The budget
|
||||
/// never cuts the capture short — the loop stays open until the screenshot
|
||||
/// has been attempted — because an unattended run whose whole product is a
|
||||
/// PNG must not be able to exit without producing one.</para>
|
||||
/// </summary>
|
||||
private void Present()
|
||||
{
|
||||
|
|
@ -197,12 +204,59 @@ internal sealed class VulkanBringUpHost : IDisposable
|
|||
_log($"vulkan: screenshot request rejected: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
if (ShouldRetire(screenshots, screenshotRequested))
|
||||
{
|
||||
_log(
|
||||
$"vulkan: frame budget of {_options.VulkanCapabilityProbeFrames} " +
|
||||
"reached; closing the probe.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
device.WaitIdle();
|
||||
_log($"vulkan: presented {_frameSerial} RHI frame(s); shutting down.");
|
||||
}
|
||||
|
||||
private bool ShouldRetire(
|
||||
FrameScreenshotController? screenshots,
|
||||
bool screenshotRequested) =>
|
||||
ShouldRetire(
|
||||
_options.VulkanCapabilityProbeFrames,
|
||||
_frameSerial,
|
||||
screenshots is not null,
|
||||
screenshotRequested);
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V9. The bounded-run decision, pure so it can be tested
|
||||
/// without a window or a driver.
|
||||
/// </summary>
|
||||
/// <param name="frameBudget">
|
||||
/// <c>ACDREAM_VULKAN_PROBE_FRAMES</c>. Zero or negative means the
|
||||
/// interactive behaviour: run until the window closes.
|
||||
/// </param>
|
||||
/// <param name="presentedFrames">Frames presented so far.</param>
|
||||
/// <param name="capturesScreenshot">
|
||||
/// Whether an artifact directory was configured, and so whether this run owes
|
||||
/// a PNG.
|
||||
/// </param>
|
||||
/// <param name="screenshotRequested">Whether that capture has been attempted.</param>
|
||||
internal static bool ShouldRetire(
|
||||
int frameBudget,
|
||||
ulong presentedFrames,
|
||||
bool capturesScreenshot,
|
||||
bool screenshotRequested)
|
||||
{
|
||||
if (frameBudget <= 0)
|
||||
return false;
|
||||
if (presentedFrames < (ulong)frameBudget)
|
||||
return false;
|
||||
// A budget below the capture threshold would otherwise exit with an empty
|
||||
// artifact directory and a green step, which is the one outcome an
|
||||
// unattended render gate must never produce.
|
||||
return !capturesScreenshot || screenshotRequested;
|
||||
}
|
||||
|
||||
private void ReportTimings(VulkanGpuDevice device)
|
||||
{
|
||||
if (!device.Timers.IsSupported)
|
||||
|
|
|
|||
|
|
@ -62,7 +62,8 @@ public sealed record RuntimeOptions(
|
|||
RenderBackendKind RenderBackend,
|
||||
string? VulkanDeviceOverride,
|
||||
string? VulkanForcedUnsupportedFeature,
|
||||
bool VulkanCapabilityProbe)
|
||||
bool VulkanCapabilityProbe,
|
||||
int VulkanCapabilityProbeFrames)
|
||||
{
|
||||
/// <summary>
|
||||
/// Build options from the process environment. Used by
|
||||
|
|
@ -165,7 +166,15 @@ public sealed record RuntimeOptions(
|
|||
// diagnostic for "does this machine pass the Vulkan gate, and does
|
||||
// the backend draw?"; ignored on OpenGL.
|
||||
VulkanCapabilityProbe:
|
||||
IsExactlyOne(env("ACDREAM_VULKAN_PROBE")));
|
||||
IsExactlyOne(env("ACDREAM_VULKAN_PROBE")),
|
||||
// Campaign V slice V9: bound the probe harness to a frame budget so
|
||||
// it can run unattended. The harness otherwise presents until its
|
||||
// window closes, which is right for a developer answering "does this
|
||||
// machine pass?" at a desk and impossible for CI, where nothing ever
|
||||
// closes the window. Zero -- unset, unparseable, or an explicit 0 --
|
||||
// keeps the interactive behaviour, so no existing invocation changes.
|
||||
VulkanCapabilityProbeFrames:
|
||||
TryParseNonNegativeInt(env("ACDREAM_VULKAN_PROBE_FRAMES")) ?? 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
using AcDream.App.Rendering.Gpu.Vk;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Gpu.Vk;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V9: the probe harness's bounded-run decision.
|
||||
///
|
||||
/// <para>The harness presents until its window closes, which is right at a desk
|
||||
/// and impossible in CI — nothing there ever closes a window.
|
||||
/// <c>ACDREAM_VULKAN_PROBE_FRAMES</c> gives it a frame budget instead. Every
|
||||
/// other line of the harness needs a window and a driver; this decision does
|
||||
/// not, so it is tested here rather than left to the lavapipe job to discover.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class VulkanBringUpBudgetTests
|
||||
{
|
||||
/// <summary>
|
||||
/// The interactive default. An unset or malformed budget is zero, and zero
|
||||
/// must never retire the loop, or the developer diagnostic this harness
|
||||
/// exists for would close itself the moment it opened.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
public void ANonPositiveBudgetNeverRetires(int budget)
|
||||
{
|
||||
Assert.False(
|
||||
VulkanBringUpHost.ShouldRetire(
|
||||
budget,
|
||||
presentedFrames: 10_000,
|
||||
capturesScreenshot: true,
|
||||
screenshotRequested: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheLoopRunsUntilTheBudgetIsReached()
|
||||
{
|
||||
Assert.False(
|
||||
VulkanBringUpHost.ShouldRetire(
|
||||
frameBudget: 30,
|
||||
presentedFrames: 29,
|
||||
capturesScreenshot: false,
|
||||
screenshotRequested: false));
|
||||
Assert.True(
|
||||
VulkanBringUpHost.ShouldRetire(
|
||||
frameBudget: 30,
|
||||
presentedFrames: 30,
|
||||
capturesScreenshot: false,
|
||||
screenshotRequested: false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The invariant the CI render gate rests on: a run that owes a PNG cannot
|
||||
/// exit before the capture has been attempted. The harness captures at frame
|
||||
/// four, so a budget below that would otherwise exit with an empty artifact
|
||||
/// directory and a green step — the one outcome an unattended render gate
|
||||
/// must never produce.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ARunThatOwesAScreenshotStaysOpenUntilItHasBeenAttempted()
|
||||
{
|
||||
Assert.False(
|
||||
VulkanBringUpHost.ShouldRetire(
|
||||
frameBudget: 2,
|
||||
presentedFrames: 2,
|
||||
capturesScreenshot: true,
|
||||
screenshotRequested: false));
|
||||
Assert.True(
|
||||
VulkanBringUpHost.ShouldRetire(
|
||||
frameBudget: 2,
|
||||
presentedFrames: 4,
|
||||
capturesScreenshot: true,
|
||||
screenshotRequested: true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// With no artifact directory there is no PNG to wait for, so the budget is
|
||||
/// the only term.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ARunWithNoArtifactDirectoryRetiresOnTheBudgetAlone()
|
||||
{
|
||||
Assert.True(
|
||||
VulkanBringUpHost.ShouldRetire(
|
||||
frameBudget: 1,
|
||||
presentedFrames: 1,
|
||||
capturesScreenshot: false,
|
||||
screenshotRequested: false));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,190 @@
|
|||
using System;
|
||||
using System.Text.Json;
|
||||
using AcDream.App.Platform;
|
||||
using AcDream.App.Rendering.Gpu.Vk;
|
||||
using Silk.NET.Vulkan;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Gpu.Vk;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V9: the capability report is a CI contract, not just a
|
||||
/// diagnostic.
|
||||
///
|
||||
/// <para>The <c>linux-vulkan</c> job in
|
||||
/// <c>.github/workflows/headless-portability.yml</c> asserts the lavapipe run's
|
||||
/// verdict by reading <c>graphical-capabilities-vulkan.json</c> with <c>jq</c>.
|
||||
/// Those <c>jq</c> paths and the enum spellings they compare against are
|
||||
/// invisible to the compiler: renaming a record property or swapping an enum
|
||||
/// converter would leave every existing test green and turn CI red on a
|
||||
/// different branch, days later, with a failure that reads like a driver
|
||||
/// problem. These tests pin the exact strings the job depends on, so the rename
|
||||
/// fails here first and says why.</para>
|
||||
///
|
||||
/// <para>The record below is shaped like lavapipe deliberately — a CPU device
|
||||
/// on X11 reporting Vulkan 1.4 — because that is the device the job runs on.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class VulkanCapabilityReportContractTests
|
||||
{
|
||||
private static VulkanCapabilityRecord LavapipeShapedRecord(
|
||||
string? forcedUnsupportedFeature = null)
|
||||
{
|
||||
var record = new VulkanCapabilityRecord(
|
||||
DateTimeOffset.UnixEpoch,
|
||||
"linux-x64",
|
||||
GraphicalHostOperatingSystem.Linux,
|
||||
GraphicalDisplayProtocol.X11,
|
||||
GraphicalDisplayProtocol.X11,
|
||||
"Vulkan 1.4.0",
|
||||
"Vulkan 1.4.305",
|
||||
VulkanApiVersion.Make(1, 4, 305),
|
||||
"llvmpipe (LLVM 19.1.7, 256 bits)",
|
||||
"vendor 0x10005, device 0x0, driver 0.0.1",
|
||||
PhysicalDeviceType.Cpu,
|
||||
0,
|
||||
"automatic",
|
||||
RequestedDeviceOverride: null,
|
||||
ForcedUnsupportedFeature: null,
|
||||
AvailableDevices: [],
|
||||
InstanceExtensions: ["VK_KHR_surface", "VK_KHR_xlib_surface"],
|
||||
DeviceExtensions: ["VK_KHR_swapchain"],
|
||||
GraphicsQueueFamily: 0,
|
||||
PresentQueueFamily: 0,
|
||||
VulkanDeviceFeatureSupport.Complete,
|
||||
VulkanDeviceLimitSupport.Complete,
|
||||
VulkanFormatSupport.Complete,
|
||||
new VulkanSurfaceSupport(
|
||||
PresentSupported: true,
|
||||
SelectedFormat: Format.B8G8R8A8Unorm,
|
||||
SelectedColorSpace: ColorSpaceKHR.SpaceSrgbNonlinearKhr,
|
||||
SelectedPresentMode: PresentModeKHR.FifoKhr,
|
||||
SelectedImageCount: 3,
|
||||
SelectedWidth: 1280,
|
||||
SelectedHeight: 720,
|
||||
SupportsTransferSource: true,
|
||||
AvailableFormats: [Format.B8G8R8A8Unorm],
|
||||
AvailablePresentModes: [PresentModeKHR.FifoKhr]),
|
||||
new VulkanFunctionProbeResult(
|
||||
DeviceCreation: true,
|
||||
DescriptorIndexingLayout: true,
|
||||
PushConstantLayout: true,
|
||||
DynamicRenderingClear: true,
|
||||
TimelineSemaphoreWait: true,
|
||||
HostQueryReset: true,
|
||||
OffscreenReadback: true,
|
||||
Failures: []),
|
||||
SupportFailures: []);
|
||||
|
||||
record = VulkanCapabilityRequirements.Reevaluate(record);
|
||||
return VulkanCapabilityRequirements.ApplyForcedUnsupported(
|
||||
record,
|
||||
forcedUnsupportedFeature);
|
||||
}
|
||||
|
||||
private static JsonElement Report(string? forcedUnsupportedFeature = null)
|
||||
=> JsonDocument
|
||||
.Parse(
|
||||
VulkanCapabilityReportWriter.Serialize(
|
||||
LavapipeShapedRecord(forcedUnsupportedFeature)))
|
||||
.RootElement;
|
||||
|
||||
/// <summary>
|
||||
/// The passing run's assertions, one for one with the "Probe the Vulkan
|
||||
/// capability gate on lavapipe" step.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ThePassingRunCarriesEveryFieldTheCiJobReads()
|
||||
{
|
||||
JsonElement report = Report();
|
||||
|
||||
Assert.Equal(0, report.GetProperty("SupportFailures").GetArrayLength());
|
||||
Assert.Equal("X11", report.GetProperty("ActiveDisplayProtocol").GetString());
|
||||
// The exact spelling the job greps for. Silk.NET's enum member is Cpu;
|
||||
// a converter change that produced "CPU" or "4" would pass every other
|
||||
// test in this project and fail only in CI.
|
||||
Assert.Equal("Cpu", report.GetProperty("DeviceType").GetString());
|
||||
|
||||
JsonElement probe = report.GetProperty("FunctionProbe");
|
||||
Assert.Equal(0, probe.GetProperty("Failures").GetArrayLength());
|
||||
Assert.True(probe.GetProperty("DeviceCreation").GetBoolean());
|
||||
Assert.True(probe.GetProperty("OffscreenReadback").GetBoolean());
|
||||
|
||||
// Present and human-readable: the job prints these to the log so a
|
||||
// failure elsewhere still records which device ran.
|
||||
Assert.False(string.IsNullOrWhiteSpace(report.GetProperty("DeviceName").GetString()));
|
||||
Assert.False(string.IsNullOrWhiteSpace(report.GetProperty("DeviceApiVersion").GetString()));
|
||||
Assert.False(string.IsNullOrWhiteSpace(report.GetProperty("DriverInfo").GetString()));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The job unpacks the API version out of <c>DeviceApiVersionPacked</c> with
|
||||
/// jq arithmetic — dividing by 2^22 for the major and by 2^12 for the minor
|
||||
/// — rather than regexing the display string, which would break at 1.10.
|
||||
/// This asserts that arithmetic against the same packing the client uses.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ThePackedApiVersionUnpacksTheWayTheCiJobUnpacksIt()
|
||||
{
|
||||
uint packed = Report().GetProperty("DeviceApiVersionPacked").GetUInt32();
|
||||
|
||||
uint major = packed / 4194304;
|
||||
uint minor = packed % 4194304 / 4096;
|
||||
|
||||
Assert.Equal(VulkanApiVersion.Major(packed), major);
|
||||
Assert.Equal(VulkanApiVersion.Minor(packed), minor);
|
||||
Assert.True(major > 1 || (major == 1 && minor >= 3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The forced-unsupported run's assertions, one for one with the "Verify the
|
||||
/// forced-unsupported gate exits 4" step. The feature name is the literal
|
||||
/// the workflow passes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TheForcedUnsupportedRunCarriesEveryFieldTheCiJobReads()
|
||||
{
|
||||
JsonElement report = Report("timelineSemaphore");
|
||||
|
||||
Assert.Equal(
|
||||
"timelineSemaphore",
|
||||
report.GetProperty("ForcedUnsupportedFeature").GetString());
|
||||
Assert.False(
|
||||
report.GetProperty("Features").GetProperty("TimelineSemaphore").GetBoolean());
|
||||
|
||||
JsonElement failures = report.GetProperty("SupportFailures");
|
||||
Assert.True(failures.GetArrayLength() > 0);
|
||||
// The job matches on the feature name inside the failure sentence.
|
||||
Assert.Contains(
|
||||
failures.EnumerateArray(),
|
||||
failure => failure.GetString()?.Contains(
|
||||
"timelineSemaphore",
|
||||
StringComparison.Ordinal) == true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The operator-facing refusal names the report path. The job greps the log
|
||||
/// for the file name, because a gate that refuses without saying where the
|
||||
/// evidence is has failed at the only job it has on a machine nobody owns.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TheRefusalMessageNamesTheReportTheJobUploads()
|
||||
{
|
||||
string message = VulkanCapabilityGuard.FormatUnsupportedMessage(
|
||||
LavapipeShapedRecord("timelineSemaphore"),
|
||||
"/tmp/diagnostics/graphical-capabilities-vulkan.json");
|
||||
|
||||
Assert.Contains("graphical-capabilities-vulkan.json", message, StringComparison.Ordinal);
|
||||
Assert.Contains("timelineSemaphore", message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The report file name is what the workflow's VULKAN_REPORT path ends in.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TheReportFileNameIsTheOneTheWorkflowPathNames()
|
||||
{
|
||||
Assert.Equal(
|
||||
"graphical-capabilities-vulkan.json",
|
||||
VulkanCapabilityGuard.ReportFileName);
|
||||
}
|
||||
}
|
||||
|
|
@ -556,4 +556,54 @@ public sealed class RuntimeOptionsTests
|
|||
Env(new() { ["ACDREAM_VULKAN_FORCE_UNSUPPORTED"] = "timelineSemaphore" }))
|
||||
.VulkanForcedUnsupportedFeature);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V9. The frame budget is what lets the probe harness run
|
||||
/// unattended in CI. Zero is the interactive default, so every invocation
|
||||
/// that predates the slice keeps presenting until its window closes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void VulkanCapabilityProbeFrames_DefaultsToUnbounded()
|
||||
{
|
||||
Assert.Equal(
|
||||
0,
|
||||
RuntimeOptions.Parse(AnyDatDir, EmptyEnv()).VulkanCapabilityProbeFrames);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("1", 1)]
|
||||
[InlineData("30", 30)]
|
||||
[InlineData("0", 0)]
|
||||
public void VulkanCapabilityProbeFrames_ParsesANonNegativeBudget(
|
||||
string value,
|
||||
int expected)
|
||||
{
|
||||
Assert.Equal(
|
||||
expected,
|
||||
RuntimeOptions.Parse(
|
||||
AnyDatDir,
|
||||
Env(new() { ["ACDREAM_VULKAN_PROBE_FRAMES"] = value }))
|
||||
.VulkanCapabilityProbeFrames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A malformed budget falls back to the interactive default rather than to
|
||||
/// some invented number: a CI step that meant to bound the run and mistyped
|
||||
/// it should hang and be noticed, not silently capture at a frame count
|
||||
/// nobody asked for.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("-1")]
|
||||
[InlineData("many")]
|
||||
[InlineData("30.5")]
|
||||
public void VulkanCapabilityProbeFrames_RejectsMalformedValues(string value)
|
||||
{
|
||||
Assert.Equal(
|
||||
0,
|
||||
RuntimeOptions.Parse(
|
||||
AnyDatDir,
|
||||
Env(new() { ["ACDREAM_VULKAN_PROBE_FRAMES"] = value }))
|
||||
.VulkanCapabilityProbeFrames);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,14 @@ param(
|
|||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repo = Split-Path -Parent $PSScriptRoot
|
||||
# Campaign V slice V9: every path below is composed one segment at a time rather
|
||||
# than from an embedded 'a\b\c' literal. A backslash is a path separator on
|
||||
# Windows and an ordinary filename character everywhere else, so the embedded
|
||||
# form silently produced one long nonexistent file name on the Linux CI runner
|
||||
# that this slice's lavapipe job introduced.
|
||||
if (-not $ShadersDirectory) {
|
||||
$ShadersDirectory = Join-Path $repo 'src\AcDream.App\Rendering\Shaders'
|
||||
$ShadersDirectory = [System.IO.Path]::Combine(
|
||||
$repo, 'src', 'AcDream.App', 'Rendering', 'Shaders')
|
||||
}
|
||||
if (-not $OutputDirectory) {
|
||||
$OutputDirectory = Join-Path $ShadersDirectory 'spv'
|
||||
|
|
@ -67,8 +73,14 @@ if ($PreferSdk) {
|
|||
$glslc = $onPath.Source
|
||||
}
|
||||
elseif ($env:VULKAN_SDK) {
|
||||
$candidate = Join-Path $env:VULKAN_SDK 'Bin\glslc.exe'
|
||||
if (Test-Path $candidate) { $glslc = $candidate }
|
||||
# 'Bin/glslc.exe' on Windows, 'bin/glslc' on the SDK's Linux layout.
|
||||
$candidates = @(
|
||||
[System.IO.Path]::Combine($env:VULKAN_SDK, 'Bin', 'glslc.exe'),
|
||||
[System.IO.Path]::Combine($env:VULKAN_SDK, 'bin', 'glslc')
|
||||
)
|
||||
foreach ($candidate in $candidates) {
|
||||
if (Test-Path $candidate) { $glslc = $candidate; break }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,12 +96,15 @@ else {
|
|||
Write-Step 'no Vulkan SDK glslc found; using the managed Silk.NET.Shaderc compiler'
|
||||
}
|
||||
|
||||
$tool = Join-Path $repo 'tools\ShaderCompiler\ShaderCompiler.csproj'
|
||||
$tool = [System.IO.Path]::Combine(
|
||||
$repo, 'tools', 'ShaderCompiler', 'ShaderCompiler.csproj')
|
||||
Write-Step 'building the shader compiler'
|
||||
& dotnet build $tool -c Release --nologo -v q | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "Shader compiler build failed with exit code $LASTEXITCODE." }
|
||||
|
||||
$binary = Join-Path $repo 'tools\ShaderCompiler\bin\Release\net10.0\AcDream.Tools.ShaderCompiler.dll'
|
||||
$binary = [System.IO.Path]::Combine(
|
||||
$repo, 'tools', 'ShaderCompiler', 'bin', 'Release', 'net10.0',
|
||||
'AcDream.Tools.ShaderCompiler.dll')
|
||||
if (-not (Test-Path $binary)) { throw "Shader compiler not found at $binary." }
|
||||
|
||||
Write-Step "compiling $ShadersDirectory -> $OutputDirectory"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue