diff --git a/docs/plans/2026-07-27-vulkan-campaign.md b/docs/plans/2026-07-27-vulkan-campaign.md index 3e17855d..3a55513b 100644 --- a/docs/plans/2026-07-27-vulkan-campaign.md +++ b/docs/plans/2026-07-27-vulkan-campaign.md @@ -617,8 +617,8 @@ tenth pair with no consumer at all; see the V6e report. | **V6k** ✅ | **Sky, viewports, and §5.4**, reported in §5.5.16. Two commits: **1** `SkyRenderer`'s RHI arm — V4f's content, two blend pipelines, `SkyParams` as a per-draw ring slice, the first Vulkan consumer of set 1 binding 4 — plus the retirement of the last interim `GlBindlessHandleTable` (`22aa2edc`); **2** `PrivateEntityViewportRenderer` onto `IGpuRenderTarget`, the deletion of §7.1's external-texture seam, and **§5.4's obligation discharged** (`eb7e6b4e`). **Particles did not land**: the pinned contract cannot express instanced vertex input, which is what both particle pipelines are built on. | GL pixel gate 4.43e-05 then 4.08e-05 (25 and 23 px, band 9–31), App tests 4,109/3, complete Release suite 9,172/5, GL connected `-Runs 3` at 3/3 on both columns per commit, one validation-layer Vulkan run at 0 errors / 0 warnings per commit, a seven-day-group GL sky comparison, a connected paperdoll capture, and a GL-versus-Vulkan inspection in §5.5.16 | | **V6l** ✅ | **Particles, the portal mask and the viewports**, reported in §5.5.17. Three commits, one per contract amendment: **1** instanced vertex input (`GpuVertexLayout` per-binding stride and input rate, `BindVertexBuffer(binding, …)`, `GpuVertexFormat.UInt1`) plus `ParticleRenderer`'s RHI arm, the standalone particle texture cache on both arms, and the stride-equals-the-uploaded-record gate for every RHI vertex layout (`b1ad1d48`); **2** the stencil dimension (`StencilTest` + `GpuStencilState` + `SetStencil`), `portal_depth` as a committed shader pair, `PortalDepthMaskRenderer`'s RHI arm, and the ambient stencil/colour-mask restore (`eced67d0`); **3** a layered sampled view per Vulkan render target, sample-count pipeline variants for `WbDrawDispatcher`, the composite texture cache on both arms, and the backend-derived viewport V orientation (`2e8b8b91`). | GL pixel gate per commit (3.20e-05, 2.31e-05, 3.55e-05; band 9–31 px), App tests 4,121/4,129/4,129 and complete Release suite 9,184/9,192/9,192, GL connected `-Runs 3` at 3/3 on both columns per commit, one validation-layer Vulkan run at 0 errors / 0 warnings per commit, and connected Vulkan paperdoll and particle captures inspected against GL | | **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 | +| **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** ◐ **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 | @@ -2732,8 +2732,11 @@ individually, because "phase" is not an excuse unless it is specific: 3. **Whether AD-46 is visible to a human.** It is 15% of the pixels in one band at an absolute tolerance of 2; nobody has yet looked at a Vulkan treeline beside a GL one and said whether they can tell. That is a user-stop question. -4. **The R6 soak and a RenderDoc capture natively on Vulkan**, both named in the - V7 row and neither run. +4. ~~**The R6 soak and a RenderDoc capture natively on Vulkan**, both named in the + V7 row and neither run.~~ **Half discharged at V8 (§5.5.21): the R6 soak ran + natively on Vulkan and passed with zero failures and a graceful exit. The + RenderDoc capture has a cause rather than an omission — RenderDoc is not + installed on this machine — and carries to V10.** **Gates.** Per commit: Release build green; App tests **4,133 / 3** then **4,134 / 3** against the 4,132/3 baseline (three new — the sky-phase parse, the @@ -2855,6 +2858,406 @@ 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.5.21 V8 (2026-07-28): the gate, and the instrument it needed first + +**The headline is a split verdict, and the split is not where anyone expected.** +Measured in three configurations on the same machine, the same build and the same +day: + +- **Stationary, light scene, 780-870 FPS** — Vulkan is 4.1x cheaper on GPU, + allocates 6.8x less and holds 7% less working set, and **costs 14.8% more CPU + per frame**. Two of seven floors missed. +- **Stationary, dense scene, 165-170 FPS, identical 21,024-entity world** — + **Vulkan wins every row**, CPU p50 included, and uses **18.5% less total process + CPU** by Windows' own accounting. +- **Canonical nine-stop route, identical world at all nine stops** — **Vulkan + wins every row**, renders **27.3% more frames** in the same 506 seconds and + halves GPU p99. + +The three do not contradict each other. The Vulkan-specific cost is **fixed per +frame**, so it dominates an almost-empty frame and disappears into a full one. +The recommendation is at the end; the measurement comes first, because V7's +lesson was that a number is worth what its instrument is worth. + +--- + +**0. The Vulkan arm had no instrument at all** (`00e1b321`). V6h wired the +Vulkan frame spine to `NullRenderFrameGpuMeasurement`, and that class's +`BeginFrame` is the **only** caller of `FrameProfiler.FrameBoundary`. So a Vulkan +run produced no `[frame-prof]` line, no CPU frame distribution, no +allocation-per-frame column, no frame-history CSV and no GPU sample. The R6 soak +waits on `[frame-prof]` boundaries to time its samples, so the campaign's own +performance vehicle could not be pointed at the backend V8 exists to judge. + +`VulkanFrameGpuMeasurement` closes it, and the bracket is deliberately the same +one GL uses — resource preparation, world scene and private presentation, not the +swapchain present — because two differently-bracketed numbers in one comparison +table are worse than none. Vulkan timestamps resolve two or three frames late, so +the sample carries the profiler frame index that ISSUED it, the pairing +`GpuFrameTimer` already performs internally on GL. `VulkanGpuTimerPool` gained +`TryTakeResolved`, which consumes what it reports: `TryResolve` reports the last +known value forever, which is right for a readout and wrong for a percentile. + +--- + +**1. The R6 soak is NOT the vehicle the §2 table was measured with, and using it +would have produced a false verdict against Vulkan.** This is the slice's most +important methodological finding and it was nearly missed. + +The founding numbers in §2 come from +`docs/research/2026-07-25-slice-g5-production-profile.md`, whose vehicle is +stated there in as many words: "a separate Release client ran uncapped with **no +UI probe or automation route; no automation artifact owner or +screenshot/current-path oracle; no developer tools; no WB diagnostic +comparison**; only the permanent frame profiler and the frame-history recorder." +The R6 soak has every one of those. Measured rather than assumed — one uncapped +GL soak run reported **CPU p50 7.3 ms, 2,531 KiB/frame allocated and a 1,709 MiB +working set** at a stationary checkpoint, against the ordinary profile's 1.13 ms, +77 KiB and 941 MiB on the same binary the same hour. + +And the bias is not symmetric. `VulkanGraphicsContext` arms +`retainBackbufferCapture` **exactly when `ACDREAM_AUTOMATION_ARTIFACT_DIR` is +set**, which makes every Vulkan frame copy the whole swapchain image into a +readback buffer (`RecordBackbufferCapture`). GL has no such per-frame cost: it +reads on demand. A soak-versus-soak table would therefore have charged Vulkan for +an instrument the gate itself switched on. That is V7's finding restated, and it +is why the numbers below come from the G5 vehicle. + +The G5 vehicle was reproduced with one addition — `ACDREAM_WORLD_TIME=0.5` and +`ACDREAM_DAY_GROUP=0`, V7's two instrument pins, so both arms light the same +scene the same way. **No probe script, no artifact directory, no developer tools +and no movement**: the client logs in where the character stands, settles for 100 +seconds, and the final 60 are the window. Every launch variable is disclosed in +each run's `report.json`, and what is absent from that list is as much of the +method as what is present. + +--- + +**2. Every run taken, and the conditions.** Physical console session (session 1, +2560x1440 at 240 Hz, RX 9070 XT, driver 2.0.395 / 32.0.31021.5001), Release, +windowed 1280x720, quality High, **4x MSAA on both arms** (confirmed in both +logs), uncapped (`ACDREAM_UNCAPPED_RENDER=1`; Vulkan selects +`PresentModeImmediateKhr`), live against local ACE, stationary at the Caul +plateau where the previous route left the character, final 60 seconds of a +100-second settle unless noted. Nothing is excluded from this list. + +| Run | Backend | CPU p50 | p95 | p99 | GPU p50 | p99 | alloc p50 | WS MiB | Priv MiB | FPS | +|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| gl-1 | GL | 1.127 | 1.256 | 1.390 | 0.651 | 0.707 | 77,664 | 941.4 | 1250.1 | 871.7 | +| gl-2 | GL | 1.130 | 1.269 | 1.441 | 0.658 | 0.717 | 77,704 | 944.0 | 1322.7 | 867.7 | +| gl-phase* | GL | 1.125 | 1.260 | 1.407 | 0.646 | 0.708 | 77,664 | 943.7 | 1340.4 | 872.1 | +| vk-1 | Vulkan | 1.319 | 1.427 | 1.543 | 0.159 | 0.189 | 11,440 | 880.0 | 1237.7 | 760.5 | +| vk-2* | Vulkan | 1.250 | 1.405 | 1.519 | 0.162 | 0.180 | 11,440 | 874.1 | 1233.6 | 778.0 | +| vk-3* | Vulkan | 1.314 | 1.409 | 1.512 | 0.159 | 0.179 | 11,400 | 870.0 | 1230.4 | 767.8 | +| vk-phase* | Vulkan | 1.273 | 1.432 | 1.546 | 0.160 | 0.185 | 11,440 | 880.3 | 1241.0 | 762.5 | +| vk-validation | Vulkan | 2.609 | 3.195 | 3.536 | 0.158 | 0.190 | 11,440 | 897.3 | 1268.3 | 371.8 | +| gl-dense† | GL | 5.934 | 6.972 | 8.867 | 1.673 | 1.858 | 82,016 | 1161.0 | 1476.0 | 165.2 | +| vk-dense† | Vulkan | 5.775 | 6.563 | 7.354 | 0.909 | 0.963 | 15,752 | 1106.6 | 1278.5 | 169.5 | + +`*` carried the temporary attribution probe described in item 4. It is not an +outlier filter: the probed Vulkan runs are FASTER than the unprobed one, so the +probe is not inflating anything and the 1.25-1.32 ms spread is run-to-run +variance. GL's spread over three runs is 1.125-1.130 ms, which is 0.4%. + +`†` is the **dense** stationary pair of item 3b — same spot and same camera as +the rows above it, but taken after the two soaks left the character in a Caul +ACE had grown to **21,024 entities**, so it is a different scene and belongs to +its own comparison rather than to the medians below. Both arms met the identical +population. + +`vk-validation` is the required validation-layer run, reported in the same table +rather than in a footnote because it is a real run. Its cost is the layer's: +**zero validation errors and zero validation warnings** over a complete connected +session, with the loader's own `Insert instance layer +"VK_LAYER_KHRONOS_validation"` line as proof the layer was inserted rather than +assumed. Measurement runs have it off. + +--- + +**3. The acceptance table, judged against today's GL rather than the founding +numbers.** Today's GL is faster than the founding profile on CPU and GPU and +heavier on memory, because the scene is Caul rather than Aerlinthe — so per +§2's own method requirement both are reported and the verdict is taken against +today's. + +| Dimension | Founding GL (G5, Aerlinthe) | GL today (median of 3) | Vulkan today (median of 4) | Floor | Verdict | +|---|---:|---:|---:|---|---| +| CPU frame p50 | 1.869 ms | **1.127 ms** | **1.294 ms** | <= GL today | **MISS**, +14.8% | +| CPU frame p99 | 2.484 ms | **1.407 ms** | **1.531 ms** | <= GL today | **MISS**, +8.8% | +| GPU frame p50 | 1.096 ms | **0.651 ms** | **0.160 ms** | <= GL today | **PASS**, -75.4% | +| GPU frame p99 | 1.136 ms | **0.708 ms** | **0.182 ms** | <= GL today | **PASS**, -74.3% | +| Working set | 652.1 MiB | **943.7 MiB** | **877.1 MiB** | <= GL today | **PASS**, -7.0% | +| Private set | 928.3 MiB | **1322.7 MiB** | **1235.7 MiB** | <= GL today | **PASS**, -6.6% | +| Frame-thread alloc p50 | 22,880 B | **77,664 B** | **11,440 B** | <= GL today | **PASS**, -85.3% | + +Two rows of that table deserve a note rather than a silent pass. **The founding +"~0 B/frame" row in §2 was never true of any whole-frame measurement**: the G5 +evidence it cites records 22,880 B/frame, and §2 appears to have borrowed the +figure from the `#250` zero-allocation UNIT tests, which assert zero bytes across +named hot paths and not across a frame. Judged as written, both backends fail a +0 B whole-frame criterion; judged as a comparison, Vulkan allocates 15% of what +GL does. And **memory is higher than the founding profile on both arms** for the +same scene reason as the CPU numbers, which is why the comparison is GL-today +versus Vulkan-today and not either against G5. + +--- + +**3b. The same stationary measurement in a DENSE scene, and it reverses the CPU +rows.** The two soaks left the character standing in a Caul that ACE had grown to +**21,024 entities**, so the ordinary-production profile was taken again on both +arms, same spot, same camera, same settings, populations identical to the entity +— everything as above except that the frame now contains a town instead of a +field. + +| Dimension | GL (21,024 ent) | Vulkan (21,024 ent) | | +|---|---:|---:|---| +| CPU frame p50 | 5.934 ms | **5.775 ms** | **-2.7%** | +| CPU frame p95 | 6.972 ms | **6.563 ms** | **-5.9%** | +| CPU frame p99 | 8.867 ms | **7.354 ms** | **-17.1%** | +| GPU frame p50 | 1.673 ms | **0.909 ms** | **-45.7%** | +| GPU frame p99 | 1.858 ms | **0.963 ms** | **-48.2%** | +| Frame-thread alloc p50 | 82,016 B | **15,752 B** | **-80.8%** | +| **Process CPU, whole window** | **1.246 cores** | **1.016 cores** | **-18.5%** | +| Working set | 1,161.0 MiB | **1,106.6 MiB** | **-4.7%** | +| Private set | 1,476.0 MiB | **1,278.5 MiB** | **-13.4%** | +| FPS | 165.2 | **169.5** | **+2.6%** | + +**Vulkan wins every row.** This is the cleanest comparison in the slice — one +stationary camera, one identical world, no route, no automation, no probe — and +it is the direct test of the explanation in item 4: the Vulkan-specific per-frame +cost is fixed, so it is 12% of an 1.13 ms frame and 2.5% of a 5.9 ms one, while +the GPU and allocation savings scale with the work. + +The process-CPU row is worth its own sentence because it comes from Windows +rather than from acdream's own instrument: **1.016 cores against 1.246**, an +18.5% reduction in total process CPU measured by `TotalProcessorTime` across the +same 60-second window. Nothing in that number passes through `FrameProfiler`. + +--- + +**4. Where the CPU difference is, measured rather than guessed.** A temporary +env-gated probe (`ACDREAM_VK_CPU_PROBE=1`) bracketed the frame spine's phases on +**both** arms and, on Vulkan, the four API calls that can block. It has been +stripped; it was one file plus five call sites in `RenderFrameOrchestrator`, four +in `VulkanGpuDevice` and three in `VulkanGpuPassEncoder`, and re-adding it is +twenty minutes if V10 wants it again. + +| Phase (mean ms/frame) | GL | Vulkan | delta | +|---|---:|---:|---:| +| open the GPU frame | 0.001 | 0.056 | **+0.055** | +| resource preparation | 0.003 | 0.002 | -0.001 | +| world scene | 0.737 | 0.828 | **+0.091** | +| private presentation (retained UI, viewports) | 0.229 | 0.148 | **-0.081** | +| close the GPU frame | 0.000 | 0.098 | **+0.098** | +| sum of phases | 0.970 | 1.132 | +0.162 | +| whole-frame CPU p50 | 1.125 | 1.273 | +0.148 | +| remainder outside the phases | 0.155 | 0.141 | -0.014 | + +The remainder matters and is easy to misread: **GL's `SwapBuffers` runs in the +Silk window loop after the render callback**, so it sits inside `cpu_ms` but +outside these brackets, while Vulkan's present sits inside "close the GPU frame". +The two remainders are within 0.014 ms of each other, which puts GL's whole +present at roughly that figure. + +The Vulkan-only breakdown says the rest: + +| Vulkan call, mean ms/frame | | +|---|---:| +| `vkQueuePresentKHR` | **0.070** | +| `vkQueueSubmit2` | 0.027 | +| timeline wait (`VulkanFrameFlightController.BeginFrame`) | 0.026 | +| `vkAcquireNextImageKHR` | 0.025 | +| descriptor resolve + bind + draw, **all 216 draws of the frame** | 0.031 | +| end-of-frame recording (barriers, upload record) | 0.001 | + +**So the campaign's named cost centre is not the problem.** §5.5.14 item 5 and +§5.5.18 item 4 both carried "bindings 4, 6, 7 and 8 cost a descriptor write per +draw" forward to V8 as the thing to fix if CPU were short. Measured, that whole +population — the arena resolve, the `vkUpdateDescriptorSets` round and the +`vkCmdBindDescriptorSets` for every one of the frame's ~216 draws — is **0.031 +ms, about 140 ns per draw and 2.4% of the frame**. Fixing it perfectly would +recover a fifth of the gap, and §5.5.14 already records that fixing it means +changing the shaders' indexing rather than the layout. **That item can be closed +as measured-and-not-worth-it rather than carried again.** + +What the gap actually is: **0.148 ms of per-frame WSI and synchronisation that +Vulkan requires and GL does not expose.** Present, submit, acquire and the +timeline wait total 0.148 ms against GL's ~0.014 ms of present, and none of the +four is a call acdream chooses to make. The world phase's remaining +0.091 ms is +the only part inside our code, and 0.031 ms of it is the descriptor population +above. + +--- + +**5. What was tried, and what was deliberately not.** Of the levers the V8 row +names: + +- **Coherent-versus-flush rings** — already taken. Every ring and staging + allocation is `HOST_VISIBLE | HOST_COHERENT` (`VulkanMemoryModel`), so there is + no per-allocation `vkFlushMappedMemoryRanges` to remove. +- **Submit consolidation** — already taken. One `vkQueueSubmit2` per frame, + measured at 0.027 ms. +- **Pipeline pre-warm coverage** — irrelevant to p50 by construction; every + pipeline is built at startup and the pipeline cache is reused from disk + (confirmed in the run logs). It could only move max, and max is 2.65-2.99 ms + on Vulkan against 2.48-2.77 ms on GL, which is the same population. +- **Descriptor writes per draw** — measured at 0.031 ms and left alone, above. +- **A fourth swapchain image / a third frame in flight** — considered and NOT + attempted. Acquire and the timeline wait cost 0.025 and 0.026 ms while the GPU + finishes its frame in **an eighth** of the CPU's time (0.160 ms against 1.294), + so nothing is actually waiting and those figures are the cost of making the + calls. A fourth image would buy nothing and cost real memory. Shipping it would + have been exactly the unproven optimization the slice brief forbids. + +**No change was made to the Vulkan backend in this slice beyond the instrument.** +That is the honest outcome of the attribution: there was nothing to fix that was +worth its risk, and inventing one to make a table go green would have been the +worse failure. + +--- + +**6. The route gates. The R6 soak has now run natively on Vulkan, which V7 left +outstanding** (`logs/connected-r6-soak-20260728-210728.report.json`): **PASS, zero +failures, exit code 0, graceful WM_CLOSE exit**, 506.6 s, all nine canonical +checkpoints in order with every reveal materialized/completed/observed, every +`streamingWork` backlog at zero and every named checkpoint artifact present. +Its warnings are the documented benign set — the 25 world-edge landblock misses, +19 DAT-driven VFX diagnostics, and ACE population drift at the Caul plateau +oracle. **A complete connected session on Vulkan: nine teleports, three +movement/jump/combat exercises, nine screenshots, and a clean shutdown.** + +**The same route was then run on GL the same hour** +(`logs/connected-r6-soak-20260728-211617.report.json`): **PASS, zero failures, +exit code 0, graceful exit**, 506.8 s against Vulkan's 506.6 s. Its warnings are +the same benign set. **And the two runs met an identical world**: 21,031 / +6,675 / 9,404 / ~6,660 / 6,675 / 10,383 / 21,031 / 6,675 / 21,031 entities at +the nine stops, the same number at every one. That makes them a matched pair +rather than two runs that happen to share a script. + +**Which produces the finding that most complicates this slice's verdict, and it +complicates it in Vulkan's favour.** Over the whole route, from the frame +histories: + +| Route-wide, 506 s, matched populations | GL | Vulkan | | +|---|---:|---:|---| +| Frames rendered | 30,378 | **38,683** | **+27.3%** | +| CPU p50 | 11.718 ms | **9.166 ms** | **-21.8%** | +| CPU p99 | 51.089 ms | **42.826 ms** | **-16.2%** | +| GPU p50 | 0.702 ms | **0.167 ms** | **-76.2%** | +| GPU p99 | 2.325 ms | **1.193 ms** | **-48.7%** | +| Allocation p50 | 4,318,088 B | **4,244,984 B** | -1.7% | +| Peak working set | 2,318.5 MiB | **2,295.4 MiB** | -1.0% | + +**On the canonical nine-stop route Vulkan is 21.8% FASTER on CPU p50, not 14.8% +slower.** The two results are not in conflict; they are the same fixed cost +divided by two very different frames. The stationary profile runs a light scene +at 780-870 FPS, where 0.148 ms of per-frame WSI is 12% of the budget. The route +moves, teleports, streams and stands in towns of ten and twenty thousand +entities at 11-45 FPS, where the same 0.148 ms is under 1% and Vulkan's cheaper +submission, cheaper GPU and lighter allocation carry the frame instead. + +**And this comparison is conservative in Vulkan's favour**, because it is taken +on the vehicle that charges Vulkan a full-resolution swapchain copy every frame +and charges GL nothing. Vulkan wins it anyway. + +Also worth reading twice: the route's GPU p99 is **2.325 ms on GL against 1.193 +on Vulkan**. Whatever headroom the campaign wanted for future content, that is +where it is. + +One observation worth keeping, because it bears on any future use of the soak +for numbers and because the matched pair above was luck as much as method: the +local ACE world is not a fixed fixture. An earlier aborted GL run met 6,675 +entities at Caul; the two runs compared above both met **21,031** there, forty +minutes later. A pair taken an hour apart would not have been comparable at all. +Check the populations before believing any two soak runs, and note that the +stationary profile above is immune to this because it never leaves one spot. + +--- + +**7. The RenderDoc capture the V7 list carried is still not taken**, and now with +a stated cause rather than an omission: **RenderDoc is not installed on this +machine** — not in either Program Files tree, not in `LOCALAPPDATA`, not on +`PATH`, and not in the uninstall registry, alongside neither Nsight nor PIX. It +carries to V10, which is the right place for it anyway: a capture of the backend +that is about to become the client is worth more than a capture of one that is +still dark. + +--- + +**8. Gates.** Release build green. App tests **4,152 passed / 3 skipped**, +exactly the pre-slice baseline, with no member of the `#250` family failing. +Strict GL offline pixel gate against `13c8733d`: **1.95e-05 (11 differing pixels +of 563,200)**, inside the documented 9-31 px band — **GL did not move**, which is +what the two shared files the instrument touches (`FrameProfiler`, +`FrameRootComposition`) owed. One connected Vulkan run with +`VK_LAYER_KHRONOS_validation` proven inserted by the loader: **zero errors, zero +warnings**. No divergence-register row is owed in either direction: the slice +adds diagnostic apparatus and changes no rendered pixel. + +--- + +**9. The verdict, and the recommendation.** + +**Floors met: 5 of 7 measured dimensions. CPU frame p50 and CPU frame p99 are +not met**, by 14.8% and 8.8%. + +**The campaign's absolute targets** (§2's middle column) tell a different and +sharper story. Vulkan **beats** the CPU p50 target of 1.60 ms at 1.294 ms and the +GPU p50 target of 1.00 ms at 0.160 ms, and **misses** the working-set target of +600 MiB at 877 MiB, the private-set target of 860 MiB at 1,236 MiB and the 0 B +allocation target at 11,440 B. But GL misses all three of those too, by more — +943 MiB, 1,323 MiB and 77,664 B — at this scene. **The absolute memory targets +were written from a measurement at Aerlinthe and are out of reach for either +backend at Caul**, which is a fact about the target, not about Vulkan. + +So the relative floor is the only meaningful test, and it was applied to three +independent configurations: + +| Configuration | Rows missed by Vulkan | +|---|---| +| Stationary, light scene (6,675 entities, 780-870 FPS) | **CPU p50, CPU p99** | +| Stationary, dense scene (21,024 entities, 165-170 FPS) | **none — Vulkan wins every row** | +| Canonical nine-stop route, matched world | **none — Vulkan wins every row** | + +**The only configuration in which Vulkan loses anything is a stationary field at +a frame rate no player will ever see.** Add a town to the frame, or move through +the world, and the sign flips on every dimension including total process CPU, +which Windows measures rather than acdream. + +**V8 therefore cannot declare a clean pass of the gate as literally written, and +would be misrepresenting the evidence if it declared a failure.** What the one +missing configuration is made of: + +- It is **0.148 ms/frame of required Vulkan WSI and synchronisation calls**, not + a defect, not a missing optimization, and not in acdream's code. +- It is a **fixed** cost, and that is the whole explanation. It is 12% of a + 1.13 ms frame, 2.5% of a 5.9 ms one, and under 1% of the route's dense-town + frames — measured, not modelled, in items 3b and 6. The GPU and allocation + savings scale with the work instead, which is why the sign flips. +- Against it stand: **every row on the dense stationary pair**, including 18.5% + less total process CPU as measured by Windows; **every row on the route**, + including 27.3% more frames and a halved GPU p99; a **75-46% GPU reduction** + and an **85-81% allocation reduction** in every configuration tested; the + compatibility case that started the campaign; and a zero-error validation run. + +**The recommendation is to proceed to V10, and to amend §2's acceptance table +rather than waive it.** A floor of "no worse than GL on any dimension" is met in +both configurations that resemble playing the game and missed only in one that +does not. The honest amendment names the scene and the pacing the floor is judged +at — and a natural choice is already sitting in the evidence: the dense +stationary pair and the nine-stop route, both of which Vulkan passes outright. +The opposite reading, that the light-scene rows are disqualifying, is available +and this slice has given it the same measurement space as the rest. **The call is +the user's, and this slice does not make it.** + +Three smaller corrections V8 recommends to §2 while it is open: the "~0 B/frame" +allocation row should say what it means (the `#250` unit-test criterion, or a +comparative frame-thread figure — it is not a whole-frame absolute); the table +should name its measurement vehicle, because the obvious candidate is the wrong +one and cost this slice an hour to establish; and the founding numbers should be +labelled with their scene, because Caul and Aerlinthe differ by more than the +entire GL-versus-Vulkan CPU gap. + ### 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