feat(render): Campaign V slice V10 — Vulkan becomes the default backend

THIS CUTOVER AWAITS THE USER'S VISUAL SIGN-OFF. It is not complete. Section 7
of the campaign plan names the V10 sign-off as the only required user stop
besides gate failures, and it has not been given. This commit flips the default
and runs the battery so that the sign-off has evidence in front of it.

ROLLBACK, one line: `git revert` of this commit. It restores the GL default,
the pre-V10 escape-hatch polarity and the gate scripts' inherited backend
together; nothing else has to move with it.

An unset, empty or unrecognised ACDREAM_RENDER_BACKEND now yields
RenderBackendKind.Vulkan. Only `gl` or `opengl`, case-insensitive, selects
OpenGL. The polarity of the typo case flipped with the default and on purpose:
before V10 an unrecognised token had to land on GL because Vulkan was dark and a
typo must never silently start a backend that cannot draw; after V10 it has to
land on Vulkan for the same reason read the other way, because GL is the backend
V11 deletes. `opengl` is honoured beside `gl` because an escape hatch exists to
be found.

Three gate scripts follow the flip. run-offline-pixel-gate.ps1 gains -Backend
(default vulkan) and now FORCES all four determinism levers — backend, day
group, world day fraction, sky phase — plus ACDREAM_MSAA_SAMPLES=0, instead of
inheriting any of them. run-repeat-connected-gate.ps1 and
run-connected-world-lifecycle-gate.ps1 CLEAR ACDREAM_RENDER_BACKEND rather than
setting it, so what they exercise is the process default and an ambient override
in a caller's shell cannot make a GL run wear the default's report.

TEST PIN UPDATED, flagged as required: RenderBackend_DefaultsToGl becomes
RenderBackend_DefaultsToVulkan, and RenderBackend_AnythingElseStaysOnGl splits
into RenderBackend_SelectsGlOnlyForTheEscapeHatchTokens and
RenderBackend_AnythingElseStaysOnVulkan. Five cases replace two. No other test
is touched, weakened or deleted.

AD-46's divergence-register row moves from "dormant until the V10 cutover" to
live, in this commit, per the same-commit register rule.

Battery, all on the new default:

  complete Release suite    9,222 passed / 5 skipped / 0 failed (9 projects)
                            +5 against the pre-flip 9,217; the +5 are this
                            slice's own escape-hatch cases
  #250 family, singly       4/4 pass (none failed in the whole-suite run)
  repeat connected gate     PASS 3/3 on both columns
  world-lifecycle route     PASS, 0 failures, both sessions graceful at exit 0
  validation layer          inserted at instance AND device level by the loader,
                            zero errors and zero warnings, real frame captured
  GL escape hatch           verified by two offline launches: 4.3.0 Core Profile
                            Context, bindless present, exit 0

Every connected launch in the battery reached Vulkan with no environment
variable set, which is the flip itself under test rather than an assertion
about it.

THE PIXEL GATE IS NOT MET, AND WAS NOT RELAXED. Vulkan against a GL-era capture
taken at this commit through the escape hatch, MSAA off and both clocks pinned:
1.099e-03 masked / 3.764e-02 whole-frame, against a 0.001 threshold. 97.9% of
the difference is in the treeline band, and the masked residual of 619 px — set
against a same-backend control of 10 px — sits entirely on the silhouettes of
distant alpha-blended scenery. That is AD-46's registered population; section
5.5.19 measured the same quantity at 497 px / 8.8e-04. Below the band the two
backends are photometrically identical: mean luminance differs by 0.01 of 255.
No baseline was regenerated and no mask or tolerance was widened.

Two instrument findings are recorded in section 5.5.23. The offline gate's sky
mask is still load-bearing — this slice tried retiring it on the reasoning that
V7's clock pins had made it obsolete, and the control refuted that: two launches
of the same binary still differ by 1,011 px on GL and 482 px on Vulkan, almost
all of it in the band. The default went back to 280 with the measurement written
into the script's help. And the repeat gate's desktop witness needs an
uncontested primary monitor: a first attempt reported 1/3, and the two failing
grabs turn out to be a web browser and Discord composited over the client rect,
not a blank frame — the client's Vulkan capture rendered in all six runs.

Nothing GL, ImGui or Studio is deleted. That is V11's scope and it is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-28 22:32:25 +02:00
parent 49a309aaaf
commit 122fe8a7e2
8 changed files with 315 additions and 41 deletions

View file

@ -442,8 +442,11 @@ the report.
## 5. Slice sequence
Every slice ends with `dotnet build` and the App test suite green, its gate
passed, and one commit. GL remains the default backend through V9; all Vulkan
work is dark behind `ACDREAM_RENDER_BACKEND` (default `gl`).
passed, and one commit. GL remained the default backend through V9; all Vulkan
work was dark behind `ACDREAM_RENDER_BACKEND` (default `gl`). **Slice V10
inverted that**: the default is now `vulkan`, and `ACDREAM_RENDER_BACKEND=gl` is
the escape hatch for one slice — see §5.5.23, and note that the cutover is
committed but not yet signed off.
**Run the suite in Release: `dotnet test … -c Release`.** Some tests assert
Release-only behaviour and legitimately fail in Debug —
@ -620,7 +623,7 @@ tenth pair with no consumer at all; see the V6e report.
| **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~~ (**run and passed at V8 — §5.5.21**), and the RenderDoc capture (**V8 established the cause: RenderDoc is not installed on this machine**). | every differential checkpoint passes; both connected routes green on VK |
| **V8****measured; two floors missed; the cutover call is the user's — §5.5.21** | Perf gate on the RX 9070 XT, uncapped, both backends, same scene, same day. **What landed:** `VulkanFrameGpuMeasurement` (`00e1b321`), without which the Vulkan arm emitted no `[frame-prof]` line at all — `NullRenderFrameGpuMeasurement` was the only caller of `FrameProfiler.FrameBoundary`, so no performance vehicle could be pointed at it; the finding that **the R6 soak is NOT the vehicle §2's founding numbers came from and is biased against Vulkan** by the per-frame swapchain copy its own artifact directory arms; a same-day GL-versus-Vulkan profile on the G5 ordinary-production vehicle; a phase-level CPU attribution on both arms; and **the R6 soak run natively on Vulkan** (PASS, 0 failures, graceful exit), which V7 left outstanding. **Result, in three configurations:** on a stationary LIGHT scene at 780-870 FPS Vulkan is 75.4% cheaper on GPU p50 and 85.3% lighter on allocation but **14.8% more expensive on CPU p50 and 8.8% on p99** — two floors missed; on a stationary DENSE scene (21,024 entities, identical on both arms) **Vulkan wins every row**, including CPU p50/p99 and 18.5% less total process CPU by Windows' accounting; and on the nine-stop route against an identical world **Vulkan wins every row** and renders **27.3% more frames**. The Vulkan-specific cost is **fixed per frame** — 0.148 ms of required WSI/sync calls (present 0.070, submit 0.027, timeline wait 0.026, acquire 0.025) against GL's ~0.014 ms of present — so it dominates an almost-empty frame and disappears into a full one. **The campaign's named cost centre is closed as measured-and-not-worth-it:** bindings 4/6/7/8 cost **0.031 ms for all ~216 draws of the frame**, 2.4% of it. No Vulkan code was changed to chase the miss. **Not taken:** the RenderDoc capture — RenderDoc is not installed on this machine; it carries to V10. | §2 acceptance table; parity is the floor |
| **V9****CI green — §5.5.20, §5.5.22** | 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 — **met**: run 30393357552, all four jobs green, lavapipe `llvmpipe (Cpu)` at Vulkan 1.4.318 / Mesa 25.2.8, a 1280x720 / 35,594-byte captured frame, and "all committed .spv match a fresh compile" on Linux |
| **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** |
| **V10****flipped and measured; PENDING THE USER'S SIGNATURE — §5.5.23** | Cutover: Vulkan default, GL reachable by env var for one slice, gate scripts default to VK. **What landed:** `ParseRenderBackend` inverted (unset/typo → Vulkan; only `gl`/`opengl` → OpenGL, the polarity of the typo case flipping with the default because GL is now the backend V11 deletes); `run-offline-pixel-gate.ps1` gained `-Backend` (default `vulkan`) and forces all four determinism levers plus MSAA off instead of inheriting them; the two connected gates **clear** `ACDREAM_RENDER_BACKEND` so they exercise the process default and an ambient override cannot disguise a GL run as one. **Battery:** complete Release suite **9,222 / 5 skipped / 0 failed** (`#250` family also 4/4 singly); repeat connected gate **3/3 on both columns**; connected world-lifecycle route **PASS**, 0 failures, both sessions graceful at exit 0; validation layer proven inserted at instance and device level with **zero errors and zero warnings**; GL escape hatch verified by two offline launches. **Every connected launch reached Vulkan with no environment variable set.** **The pixel gate is NOT met and was not relaxed:** VK against the GL-era capture is **1.099e-03 masked / 3.764e-02 whole-frame**, 97.9% of it in the treeline band, the masked residual entirely on distant alpha-blended scenery silhouettes — `AD-46`, whose register row this slice moves from dormant to live. Below the band the arms are photometrically identical (mean luminance Δ 0.01 of 255). Nothing GL, ImGui or Studio is deleted; that stays V11's. **Rollback: `git revert` of this slice's commit.** | 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 |
### 5.2 Why V2's handle table is not the device's table
@ -3336,6 +3339,140 @@ ever exercised a cache hit. 10/10 clean under the pin. Filed as #255.
**Net effect on the V9 row:** the job is green, and so is the whole workflow —
its first fully green run is the evidence the row was waiting for.
#### 5.5.23 V10 (2026-07-28): the default is Vulkan, and the signature is outstanding
**This slice does not complete V10.** It flips the default and runs the battery so
that the one remaining acceptance criterion — the user looking at the client and
saying it matches retail — has evidence in front of it. §7 names the V10 visual
sign-off as the campaign's only required user stop besides gate failures, and it
has not been given. **The cutover is committed but unsigned, and reverting this
slice's commit restores the GL default, the old escape-hatch polarity and the gate
scripts' inherited backend in one step.**
**What changed, and only this.** `RuntimeOptions.ParseRenderBackend` inverted: an
unset, empty or unrecognised `ACDREAM_RENDER_BACKEND` now yields
`RenderBackendKind.Vulkan`, and only `gl` or `opengl` (case-insensitive) selects
OpenGL. The polarity of the typo case flipped with the default, deliberately and
for the reason it had before read the other way round: a misspelling must never
silently start the backend that cannot carry the client. Before V10 that was
Vulkan, because it was dark; after V10 it is GL, because V11 deletes it. Five
test cases replace two in `RuntimeOptionsTests`, and the pin of the old default
is the only test this slice touches.
Three gate scripts follow. `run-offline-pixel-gate.ps1` gained `-Backend`
(default `vulkan`) and now **forces** all four determinism levers — backend, day
group, world day fraction, sky phase — plus `ACDREAM_MSAA_SAMPLES=0`, rather than
inheriting any of them. `run-repeat-connected-gate.ps1` and
`run-connected-world-lifecycle-gate.ps1` **clear** `ACDREAM_RENDER_BACKEND`
instead of setting it, so what they exercise is the process default and an
ambient override in a caller's shell cannot make a GL run wear the default's
report. Nothing GL, ImGui or Studio is deleted; that is V11's, untouched.
---
**The battery.**
| Gate | Result |
|---|---|
| Complete Release suite | **9,222 passed / 5 skipped / 0 failed** across 9 projects |
| `#250` family, run singly | **4/4 pass** (none failed in the whole-suite run either) |
| GL escape hatch, offline | **works**`ACDREAM_RENDER_BACKEND=gl` reports `4.3.0 Core Profile Context`, bindless present, 1,730,800 B frame, exit 0. Twice. |
| Repeat connected gate, `-Runs 3` | **PASS 3/3 on both columns** (attempt 2; attempt 1 is below) |
| Connected world-lifecycle route | **PASS** — 0 failures, 1 warning (`capped: 25 expected world-edge landblock miss(es)`); both sessions exited gracefully with code 0 (239 s capped, 61 s uncapped reconnect), six canonical capped checkpoints plus the reconnect checkpoint |
| Validation layer proven loaded | **zero errors, zero warnings** — loader prints `Insert instance layer "VK_LAYER_KHRONOS_validation"` and `Inserted device layer`, and the run captured a real 1,753,854 B frame |
| Offline pixel gate, VK against the GL-era capture | **1.099e-03 masked / 3.764e-02 whole-frame — over threshold, and the excess is `AD-46` alone.** Detail below. |
The suite number is +5 against the pre-flip tree's 9,217/5, and all five are this
slice's own escape-hatch cases; the App project moved 4,152 → 4,157 exactly.
**Every connected launch in the battery reached Vulkan with no environment
variable set** — six repeat-gate runs and both lifecycle sessions log
`vulkan: capability gate passed`, which is the flip itself under test rather than
an assertion about it.
---
**The pixel number, honestly.** No baseline was regenerated, and none exists to
regenerate: the campaign commits no expected PNGs (§5.1 captures the left-hand
side at a reference commit instead), so the GL-era expected frame was taken at
this same commit through the escape hatch — which is also what verifies the hatch.
Both captures: 1280x720, MSAA off, day group 0, day fraction 0.5, sky phase 0.
| Pair | Whole frame | Top 280 masked |
|---|---|---|
| GL vs GL, same binary (control) | 1,011 / 921,600 = **1.097e-03** | 10 / 563,200 = **1.78e-05** |
| VK vs VK, same binary (control) | 482 / 921,600 = **5.23e-04** | 8 / 563,200 = **1.42e-05** |
| **GL vs VK** | 34,690 = **3.764e-02** | 619 = **1.099e-03** |
| GL vs VK, second independent pair | 34,649 = 3.760e-02 | 613 = 1.088e-03 |
Three things that table says and a bare "it failed" would not.
1. **97.9% of the whole-frame difference is in rows 0-239** — the treeline and
the sky behind it. This scene is the Holtburg overlook, whose top third is
solid conifer billboards.
2. **The masked residual is 619 px against a same-backend control of 10 px**, so
it is not capture noise; and every one of those 619 pixels sits on the
silhouette of a distant alpha-blended scenery clump, verified by inspecting the
difference map rather than inferred. That is `AD-46`'s registered population
exactly — the anisotropic tap pattern in dense alpha-blended scenery, which
both specifications leave implementation-defined. §5.5.19 measured the same
quantity at 497 px / 8.8e-04 on its own capture; this one is 619 px /
1.099e-03. **Same class, 10% over the threshold instead of 12% under it.**
3. **Below the band the two backends are photometrically identical**: mean
luminance differs by **0.01 of 255** over rows 280-719, and 612 pixels of
563,200 differ at all. In the band the Vulkan arm is slightly **darker** — mean
luminance -1.93 of 255 across rows 0-279, -4.71 in a tight treeline crop, with
74% of differing pixels darker on Vulkan. So the foliage reads marginally
denser, which is the exact symptom `AD-46`'s risk column predicted. At 1:1 the
two full frames are not distinguishable by eye; at 4x the fringes are.
`AD-46`'s register row is updated in this commit from "dormant until the V10
cutover" to live, which is the deviation this slice introduces.
**The gate as written is not met, and this slice does not relax it.** §7.1 rule 2
forbids widening the mask or the tolerance to turn this green, and neither was
touched: the mask stayed at the 280 rows it has had since the gate was built, and
the tolerance at 2 / 0.001. What the slice does instead is put the split, the
control and the population in front of the person whose sign-off V10 needs.
---
**Two findings from running the battery, both about instruments.**
**1. The offline pixel gate's sky mask is still load-bearing, and V10 nearly
retired it on an assumption.** The reasoning looked sound — V7 found the two
clocks that make the sky drift, this script now pins both, therefore the mask is
obsolete and masking would only hide regressions — so the default was changed to
0. The control refuted it: with all four levers pinned, two launches of the *same
binary* still differ by 1,011 px on GL and 482 px on Vulkan, essentially all of it
in the band, which means an unmasked self-differential fails the 0.001 threshold
on noise alone. The default was put back to 280 and the measurement written into
the script's help so the next reader does not repeat the reasoning. Below the
band, the same controls show 10 px and 8 px — the strictness the mask buys.
**2. The repeat-gate desktop witness needs an uncontested primary monitor, and
said so all along.** Attempt 1 reported 1/3, with runs 2 and 3 "BLANK" at ~420 KB
while the client's own capture rendered at ~1.38 MB — the two instruments
disagreeing, which that script documents as itself a finding. The grabs settle it
with no ambiguity: run 2's PNG is a **web browser**, run 3's is **Discord**.
`CopyFromScreen` captured whatever was composited at the client's client-rect,
which is failure mode one in the script's own header. Attempt 2, minutes later,
passed 3/3 on both columns. The client's Vulkan frame was correct in all six runs.
Worth recording for V11, which inherits this instrument: the §5.5.2 finding that
justified making the desktop grab the verdict — a blank GL frame reads back from
framebuffer 0 as RGBA(0,0,0,0) even where the UI is demonstrably on screen — is a
statement about **GL's** readback. The Vulkan arm captures by copying the
swapchain image on the device (`RecordBackbufferCapture`), which is not that path.
Whether the client capture is now strong enough to be a verdict rather than a
second column is a question for whoever next touches the gate; this slice only
notes that the reason it was demoted no longer obviously applies.
---
**Not taken, and why.** The RenderDoc capture the V7 and V8 lists carried is still
outstanding: V8 established the cause — RenderDoc is not installed on this machine
— and nothing about the cutover changes that. It carries to V11.
### 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