diff --git a/docs/plans/2026-07-27-vulkan-campaign.md b/docs/plans/2026-07-27-vulkan-campaign.md index 54dc7bf6..f62a878c 100644 --- a/docs/plans/2026-07-27-vulkan-campaign.md +++ b/docs/plans/2026-07-27-vulkan-campaign.md @@ -576,6 +576,7 @@ tenth pair with no consumer at all; see the V6e report. | **V6h** ✅ | **The Vulkan composition host**, specified by §5.5.9 and reported in §5.5.10. `ACDREAM_RENDER_BACKEND=vulkan` runs the real `GameWindow` composition — DAT load, streaming, camera, entity table, session, and the real retained `UiHost` through the RHI — with no world renderers. Three seams: the already-generic platform acquisition now publishes a `GameWindowGraphics`; `VulkanHostInputCameraCompositionFactory` is the host-phase fork; the frame root gains a Vulkan arm. `VulkanBringUpHost` is reduced to the capability-probe harness over the extracted `VulkanGraphicsContext`. | offline Vulkan launch reaching the real composition with the client's own UI captured, one validation-layer run at 0 errors / 0 warnings, converging ownership ledger, App tests 4,075/3, complete Release suite 9,138/5, GL offline pixel gate 1.78e-05 | | **V6i** ✅ | **The world arm's prerequisites**, in two parts. **V6i-1** (`df6e2a79`, reported in §5.5.12) closed §5.5.8's one-binding-two-buffers hazard with one descriptor-set pair per renderer scope, derived from the descriptor state rather than declared, and measured the ordered remainder list the world arm still needed. **V6i-2** (reported in §5.5.13) took items 1, 2 and 6 of that list in three gated commits: **1** the `TerrainClip` descriptor-set fix plus set 1's missing bindings 2 and 4, proven by `spirv-dis` and now gated by a SPIR-V-reading contract test (`f7344758`); **2** world texture CREATION crosses to `IGpuTexture` — `IWorldTextureArray` over `TextureAtlasManager`/`ManagedGLTextureArray`, `TerrainAtlas`'s second construction path, and `ICompositeTextureArrayBackend`'s RHI arm — with the Vulkan arm exercised at startup (`c8d0f70b`); **3** `IMeshPipelineDevice` decouples `ObjectMeshManager`/`WbMeshAdapter` from `OpenGLGraphicsDevice`. **Items 3–5 — the submission arms, `RetailPViewPassExecutor`, and the pass-structure merge — are the next slice's.** | pixel gate per commit (3.02e-05, 3.20e-05, 1.60e-05 vs `0ca802cd`), App tests 4,109/3 and complete Release suite 9,172/5, 3/3 desktop-witness connected run at commits 2 and 3, one validation-layer Vulkan run per commit | | **V6i-3** | **The mesh pipeline runs on both arms, and the Vulkan frame gets a world pass**, reported in §5.5.14. Two commits: **1** `GlobalMeshBuffer` takes `GL?` and publishes `VertexStore`/`IndexStore`, `ObjectMeshManager`'s `RequireGl` narrows to the unreachable legacy upload, `VulkanMeshPipelineDevice` is `IMeshPipelineDevice`'s second implementation, and `NullWbMeshAdapter` is deleted (`fe8abacf`); **2** the clear merges into the world pass with `Store = Resolve` and descriptor sets bind at draw time (`887de4ae`). **The world renderers' submission arms and `RetailPViewPassExecutor` did NOT land** — §5.5.14 enumerates what they still need. | pixel gate per commit (4.44e-05, 3.73e-05 vs `579e0b7f`), App tests 4,112/3, one 3/3 desktop-witness connected run at HEAD, one validation-layer Vulkan run per commit, and a bit-identical (0/921,600) Vulkan capture across the pass merge | +| **V6j** ✅ | **The world arm, whole**, reported in §5.5.15. Two commits: **1** `VulkanViewportMapping` stops inverting the front face — the world arm is the mapping's first culling consumer and measured that the inversion culls terrain outright and turns every closed shell inside-out (`81fe5e1b`); **2** the three renderers' RHI submission arms as a SECOND arm per §5.5.6, both pass executors made backend-neutral behind `IWorldPassSurface`, `VulkanWorldPassScope` publishing the frame's one pass, `WorldFrameSections` carrying the three frame-global sections, and the composition that reaches them (`f84eef32`). **`ACDREAM_RENDER_BACKEND=vulkan` renders Dereth**: terrain, blending, roads, water, statics, scenery and the retained UI. Sky stays fog until V4f. | GL pixel gate 5.50e-05 vs `847f14ae` with a characterised 12–31 px noise distribution, App tests 4,112/3, complete Release suite 9,175/5, GL connected `-Runs 3` at 3/3 on both columns, one validation-layer Vulkan run at 0 errors / 0 warnings, and the world PNG inspected in §5.5.15 | | **V7** | GL-versus-Vulkan differential: `tools/run-backend-differential-gate.ps1`, 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.** | 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 | @@ -1857,6 +1858,134 @@ submission arms and `RetailPViewPassExecutor`, which §5.5.12 item 3 established is on the critical path because the offline gate's scene has buildings and therefore takes the PView route. +#### 5.5.15 V6j (2026-07-28): the world arm lands, and Dereth draws on Vulkan + +§5.5.14's remainder — the three renderers' submission arms, +`RetailPViewPassExecutor`, and the frame-global sections — is done. +**`ACDREAM_RENDER_BACKEND=vulkan` renders the offline scene: terrain with blended +textures and road overlays, the water edge, static world meshes, procedural +scenery, and the complete retained UI**, from the same camera as the GL capture +(`artifacts/v6j-vk2` against `artifacts/v6j-head2`). Sky is still the atmosphere +fog clear, because `SkyRenderer` is raw GL until V4f. + +Three predecessors stopped at this unit under §7.1 rule 3. It landed as one slice +in two commits, because the rule is satisfied at SLICE granularity: the first +commit's piece is unreachable on its own and the second wires it and exercises it. + +**1. The winding inversion was wrong, and nothing had ever asked** (`81fe5e1b`). +`VulkanViewportMapping` has inverted the front face since V6c, on the standard +negative-viewport argument. Every Vulkan consumer through V6i declares +`Cull = GpuCullMode.None`, so the mapping had never decided a fragment. The world +arm is its first culling consumer and falsified it twice on one frame: terrain — +the one single-sided surface acdream draws, `FrontFace(Ccw)` + `Cull(Back)` per +`ACRender::landPolysDraw`'s eye-side predicate — vanished entirely while issuing +190 multi-draw commands against 625 loaded landblocks, and every closed building +shell rendered inside-out with its front wall culled and its interior beams +visible through the gap. The identity mapping restores both at once. Two +independent surfaces, one change. The viewport flip itself is untouched and still +correct; what goes is the claim that a winding inversion travels with it. + +**Worth generalising**: this is the third defect this campaign has found in a path +that compiled, validated clean, and had a test — because the test asserted the +behaviour rather than the requirement, and no consumer exercised it. §5.5.12's +descriptor layouts and §5.5.13's `TerrainClip` set were the first two. + +**2. The world arm** (`f84eef32`). V4c's and V4d-2's content returns as a SECOND +arm, per §5.5.6's option (B): the GL arm issues the same statements in the same +order, and the encoder arm lives in three `.Rhi.cs` partials entered by one branch +per submission site. Three differences from V4c, each because the tree moved — +no binding-9 table (V4t put the slot on the device; Vulkan binds set 2), pipelines +carry the device's sample count, and **no renderer opens a pass**. + +`VulkanWorldScenePhase` opens the frame's one backbuffer pass, publishes the +encoder on `VulkanWorldPassScope` for exactly the span of the inner +`WorldSceneRenderer`, and every renderer borrows it — §5.5.14 item 1's shape, +built as specified. The three frame-global sections are PUBLISHED on +`WorldFrameSections` and each renderer binds them inside the pass after its own +binds (item 2). The interior depth clear is `vkCmdClearAttachments` through the +scope (item 3); clip distances are no-ops (item 4); the collision-wireframe +`DebugLineRenderer` is composed null (item 7). + +**Both pass executors became backend-neutral rather than gaining twins.** That is +the one place this slice diverged from §5.5.14's sketch, and it is why the slice +was tractable. Everything those two classes do is delegation to a renderer except +four concerns — the clip-frame publication, the doorway scissor, +`gl_ClipDistance` enablement and the interior depth clear — so those moved behind +`IWorldPassSurface` and retail's ordering, which is what the classes are actually +for, is written once for both backends. `RetailPViewPassExecutor` was on the +critical path precisely because the gate's scene has buildings (§5.5.12 item 3); +it now serves both arms unchanged. + +**The A8 question is answered.** §5.5.13 and §5.5.14 asked what happens to the +Phase A8 `CullMode.Landblock → None` override the moment world materials draw on +a second backend. It carried over verbatim and is still load-bearing: +`EnvCellRenderer`'s Vulkan arm applies the same rewrite, for the same reason, and +the offline scene has no interior to disprove it with. The override is therefore +neither vindicated nor retired by this slice — it is now a divergence with two +consumers instead of one, and the dungeon pass in §5.1's checklist is what will +settle it. + +**Gates.** Release build green. App tests **4,112 / 3 skips** — the unchanged +baseline; complete Release suite **9,175 / 5**. Strict GL offline pixel gate +against `847f14ae`: **5.50e-05** (31 differing pixels of 563,200), inside the +documented 9–31 band and 18x under the threshold. Characterised rather than +accepted, because 31 is the band's top: + +``` +cross-commit (847f14ae vs V6j): 21, 29, 31 +same-commit (V6j vs V6j): 12, 20 +maximumChannelDelta, all pairs: 46-52 +``` + +The max delta is 46–52 in every comparison INCLUDING the pure same-commit +controls, so the few large-delta pixels are a property of the capture. A +cross-commit pair at 21 against a same-commit pair at 20 is not what a systematic +shift looks like — a real one floors every cross-commit comparison above every +same-commit one. GL connected `tools/run-repeat-connected-gate.ps1 -Runs 3`: +**3/3 RENDERED** on the desktop witness and 3/3 on the client capture. One offline +Vulkan run with `VK_LAYER_KHRONOS_validation` **proven inserted by the loader**: +**zero validation errors, zero warnings**, a captured world frame, graceful close. +`tools/run-offline-vulkan-capture.ps1` is the new script that produces it. + +**The Dereth PNG, inspected honestly.** Right: terrain geometry, terrain blending +and its T-code transitions, road overlays, the water edge and its shoreline, +static world meshes with correct silhouettes and closed shells, procedural +scenery, per-instance lighting, the fog colour, and the complete retained UI +including the radar. The frame matches the GL capture's framing and content +element for element below the horizon. + +Wrong, and each already owned by a later slice: **the sky is flat fog** (V4f — +`SkyRenderer` is raw GL, so there is no dome, no sun, no cloud sheet, no horizon +band); **no particles** (V4e); **no paperdoll or appraisal viewport and no portal +depth mask** (V4g); **no GPU frame timings** (`NullRenderFrameGpuMeasurement` +until V4h). None of these is a defect in this slice — they are the renderers it +deliberately does not compose. + +**The V7 defect list — what a differential must not be run against yet.** + +1. **Sky, particles, viewports, portal mask** — absent, not wrong. V4e/V4f/V4g. +2. **§5.4's `Target: null` divergence is still live on GL.** V7 must not run until + V4g/V4h remove it, or it surfaces as an entire viewport on the wrong surface — + unchanged from §5.4's own warning, and now more binding because both backends + draw a world. +3. **`EnvCellRenderer`'s Vulkan arm is unproven by anything automated.** The + offline scene has no interior. This is V4c's exact debt, restated on the new + backend. +4. **The deferred-alpha path and the doorway scissor are unexercised** in the + offline scene: no alpha queue collects and no doorway slice is assembled, so + `DrawPreparedAlphaBatch`'s Vulkan arm and `RhiWorldPassSurface.BeginScissor` + have run zero times. +5. **MSAA must be forced off for the differential**, as §5.1 already states — + sample positions are not specified across implementations, and the world + pipelines now genuinely differ in sample count from the UI's. +6. **Bindings 4, 6, 7 and 8 cost a descriptor write per draw** (§5.5.14 item 5). + Correct and bounded, and a V8 perf item rather than a correctness one, but the + arena's `ScopeCount` is the number to watch. + +**What this slice does NOT claim.** It has not run connected on Vulkan, it has not +been seen by the user, and no dungeon, portal, paperdoll or spell effect has been +drawn on it. The milestone "full game frame on Vulkan" needs V4e–V4g behind it. + ### 5.4 The null-target `BeginPass` divergence (V4c) — must be undone at V6 V4c had to stop GL's `BeginPass` from binding framebuffer 0 when a pass declares