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:
Erik 2026-07-28 20:10:01 +02:00
parent 9b7f434376
commit a13cff884f
8 changed files with 784 additions and 10 deletions

View file

@ -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 931; 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. |
---