docs(render): close V6f's shader obligation; record why the Vulkan world path is blocked

Two things to write down after slice V6f.

The obligation §5 recorded against V6e is discharged. terrain_modern was "the one
production pair still not Vulkan-expressible"; its three blockers - the two loose
matrices, the loose tiling array, and the GL-only sampler-from-handle
construction - are gone, and 8/9 pairs now compile. The ninth is `mesh`, which
the plan already records as having no consumer at all, so every shader acdream
actually draws with is Vulkan-expressible. V6f gets its own slice row and its own
line in the user-gate debt table: terrain through a doorway clip region is the
one terrain path the offline gate cannot see, and it now has a second UBO binding
beside the clip block, so a bind-order mistake would surface exactly there.

The larger entry is §5.5.7, which records a measurement rather than an opinion.
§5.5.6 selected option (B) - V4c/V4d's content returning as the Vulkan world path
behind a fork at the thin submission seam - and V6f set out to build that fork.
It cannot be built yet, for a reason the plan had not stated: the Vulkan path
constructs no game state at all. GameWindow.Run returns at :695, before
Window.Create and therefore before OnLoad, which is the only caller of the
composition pipeline. A capture confirms it (artifacts/vk-world/): what the
Vulkan backend draws today is V6c's verification scene and V6d's generated UI
sprite, correctly and completely, and nothing else. A backend-selected fork would
therefore have a GL arm that runs and a Vulkan arm nothing can reach - the
unexercised second path §3.1 and §7.1 exist to prevent.

Worse for sequencing, the parked V4c/V4d code could not drive Vulkan even if it
were reached: it binds GL bindless handles as a storage buffer because §5.3
deferred the real port to V4t, and GroupKey carries the raw ulong. V4t is a hard
prerequisite, and it rewrites exactly the code the fork's Vulkan arm would
contain. Landing the fork first means writing that arm twice.

One validation-layer run is recorded with it, and it found two defects that
outlive the slice, both pre-existing and both on the path any world frame takes.
The pipeline layout declares all ten storage bindings as STORAGE_BUFFER_DYNAMIC
against a device limit of eight - the pinned binding model meeting a real limit,
wanting a decision rather than a patch. And any depth-off pipeline in a pass that
carries depth declares VK_FORMAT_UNDEFINED where the attachment's real format is
required. Also noted: the render-target-view-in-table usage V6f was told to
expect did NOT fire, so it should be re-checked rather than carried forward as
known-and-accepted.

The section closes with the recommended order - composition host, the validation
defects, V4t, then the fork - and with a cheaper intermediate milestone worth
considering: terrain, water and sky only, for which V6f's work is the whole
shader prerequisite.

Also: the roadmap's Campaign V paragraph gains a shipped-so-far line, and #250
gains a third test of the same class. One full App run during this slice reported
2,752 bytes against an expected 0 in
CurrentRenderSceneOracleTests.SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing,
on a diff that touches only GLSL and terrain's uniform plumbing; it passed alone
and in four other full runs of the same binary.

Documentation only - no code, no gates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-28 10:22:57 +02:00
parent 30e94da607
commit f8dbe2ee4a
3 changed files with 113 additions and 8 deletions

View file

@ -109,6 +109,11 @@ tree:
- `AcDream.App.Tests.UI.UiDatFontTests.InstanceMeasureWidth_ReusesGlyphTableWithoutAllocating`
- `AcDream.App.Tests.Rendering.RenderFrameProductTests.WarmProductBuildAndBorrowAllocateNothing`
- `AcDream.App.Tests.Rendering.CurrentRenderSceneOracleTests.SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing`
(added 2026-07-28 during Campaign V slice V6f: one full run reported 2,752
bytes against an expected 0, on a diff that touches only GLSL and the terrain
renderer's uniform plumbing. It passed in isolation and in four other full
runs of the same binary, so it is the same class rather than a new defect.)
Measured over six consecutive Release runs of the App suite on an unmodified
tree: four passed 3,846/3, and two failed with exactly one failure — a different

View file

@ -32,7 +32,13 @@ are deleted at V11; re-homing the dev panels onto the retained UI is a tracked
follow-up. Targets versus the GL baseline (520 FPS, CPU/GPU p50 1.869/1.096 ms,
652 MiB working set): CPU p50 ≤ 1.60 ms, GPU p50 ≤ 1.00 ms, working set
≤ 600 MiB, 0 B/frame managed allocation — with parity on all four as the cutover
floor.
floor. Shipped so far: V0V2, V4a, V4b, V5, V6aV6f. V4c/V4d were reverted and
their GL re-land is closed — an AMD GL driver defect, established by a
cross-vendor 10/10 on NVIDIA — so their content returns as the Vulkan world path
(plan §5.5.5§5.5.6). V6f made every production shader Vulkan-expressible and
then measured that world path: it is blocked behind a Vulkan composition host,
three validation-layer defects, and V4t's texture stack, all recorded with
evidence in plan §5.5.7.
---

View file

@ -513,6 +513,7 @@ session, rather than one at a time:
| V4g | Paperdoll and appraisal viewports, portal transit |
| V6d | The paperdoll/appraisal viewport sprite. It is the one retained-UI texture the gate's scene never draws, and V6d changed how every UI texture is sampled — from a bound texture unit to a table slot. The seam that registers it (`GlGpuDevice.RegisterExternalColorTexture`) is unchanged and its handle is now simply encoded rather than resolved back to a GL name, but that path is unproven by anything automated. Check it with the dungeon/portal pass above rather than on its own |
| V6e | **Particles, again — and now sky.** The particle half is V2c's and V4e's debt restated: the offline scene draws no particles, so nothing automated saw the varying retype or the ACDREAM_TEXTURE_NONE sentinel. The sky half is new and larger: V6e moved a dozen loose uniforms into a `SkyParams` uniform buffer and moved the sky's texture from a bound unit-0 texture-plus-sampler to a bindless (texture, wrap) table slot, and the gate masks the sky band for determinism. What WAS checked, and should be read as bounding the risk rather than closing it: a base-versus-head offline capture at **all seven day groups**, matching in gradient, cloud sheet, horizon band and fog on every one — including day group 2's salmon cloud band and day group 6's green band, which exercise texture sampling, tint, blend and fog together — plus 3/3 RENDERED on the desktop-witness repeat gate. **What remains unproven is pixel-exactness and the parts of the dome the fixed outdoor camera cannot see**: the sun and moon (additive surfaces high in the sky) and the rain cylinder, which is the one sky mesh that surrounds the camera and the one whose REPEAT wrap mode is most visible. Stand outside at dawn or dusk, and stand in rain |
| V6f | **Terrain seen through a doorway clip region.** The offline gate covers terrain heavily — blending, road overlays and the water edge are most of the frame, and every one of those samples goes through `terrainTiling()`, so the std140 stride and the new `ACDREAM_SAMPLE_ARRAY` reads are well proven. What it cannot see is the one terrain path with its own binding: the clip UBO at binding 2, exercised when terrain is viewed through a doorway. Binding 3 now sits beside it and is rebound per draw, so a bind-order mistake would show exactly there. Check it with the dungeon/doorway pass above |
**The user confirmed on 2026-07-27 that the local ACE server is always available
and they will verify visually on request.** That converts this table from deferred
@ -556,17 +557,21 @@ because sample positions are not specified across implementations.
| **V4h****re-sequenced — §5.5.5** | Frame-spine formalization: pass executors emit real declared `BeginPass`/`EndPass` (clears and framebuffer management move out of the spine and into pass load/store ops), flight/screenshot/resize/profiler move onto the RHI, `OpenGLGraphicsDevice`'s live role retires, Chorizite consumers are audited, and the architecture test lands. **Milestone: seam complete.** | pixel + connected lifecycle + R6 soak + complete Release suite + interim perf (RHI-on-GL CPU p50 ≤ 1.95 ms) |
| **V5** ✅ | Vulkan bring-up, dark: `ACDREAM_RENDER_BACKEND`, surface/instance/device/queues/swapchain, the capability record/probe/guard with the exit-4 contract, a clear-colour loop with screenshot and clean shutdown. | VK boots to clear on the RX 9070 XT; forced-unsupported knob → exit 4 |
| **V6** | Vulkan RHI backend, dark, four sequential commits: **a** ✅ allocator/buffers/staging/rings/timeline (`fb9c6693`); **b** ✅ textures/BC mips/samplers/descriptor table/render targets/MSAA resolve (`9eae4963`); **c**`.spv` toolchain, pipelines, pipeline cache, negative viewport, push constants, timestamps, readback, debug names (`234fe91d`); **d** ✅ first production renderers — `TextRenderer` and `DebugLineRenderer` on both backends, the colour-format contract amendment, and the retained UI drawn on Vulkan; **e** ✅ every remaining production shader crosses the dialect — `mesh_modern` (`935f4dc3`), both particle pairs (`602bc9dd`) and `sky` — leaving 8/9 pairs compiling to SPIR-V. **Milestone deferred:** "full game frame on Vulkan" is not reachable while V4c/V4d are parked and the world renderers plus `TextureCache` are still raw GL, so V6 delivers the backend, the two renderers that can use it today, and the shaders the Vulkan world path will be built on. | per-commit build + tests; V6d additionally pixel-gates GL and captures a Vulkan UI frame; V6e pixel-gates GL per commit and adds a seven-day-group sky comparison plus a 3-run desktop-witness gate |
| **V6f** ✅ | `terrain_modern` crosses the dialect, in three separately gated commits: **1** `uView`/`uProjection``uViewProjection` (`5e13b45f`); **2** `uTexTiling[36]` → the `UniformTerrainTiling` std140 buffer (`fac09407`); **3** the atlas reads → `ACDREAM_SAMPLE_ARRAY` (`30e94da6`). **Every production shader acdream draws with is now Vulkan-expressible.** The slice also measured the Vulkan world path and found it blocked — see §5.5.7. | pixel gate per commit (3.91e-05, 3.73e-05, 2.13e-05; cumulative 3.91e-05 against `7faaaa34`), App tests, one 3/3 desktop-witness connected run, one validation-layer Vulkan run |
**The one production pair still not Vulkan-expressible after V6e is `terrain_modern`**,
blocked on exactly the two things V4d was going to do: `uView`/`uProjection` are
two loose `mat4` uniforms (128 bytes — they cannot both fit the 96-byte push
block, which is why V4d's first sub-commit converged them into one
`uViewProjection` on its own pixel gate), and `uTexTiling[36]` is the 144-byte
array `UniformTerrainTiling` was reserved for. V6e left it alone because
**The one production pair still not Vulkan-expressible after V6e was
`terrain_modern`**, blocked on exactly the two things V4d was going to do:
`uView`/`uProjection` are two loose `mat4` uniforms (128 bytes — they cannot both
fit the 96-byte push block, which is why V4d's first sub-commit converged them
into one `uViewProjection` on its own pixel gate), and `uTexTiling[36]` is the
144-byte array `UniformTerrainTiling` was reserved for. V6e left it alone because
converging the matrices moves a multiply from per-vertex GPU to a CPU multiply —
a real numeric change that the plan requires be attributable on its own gate,
and one that belongs to whoever re-lands V4d's content rather than to a shader
dialect slice. `mesh` is a tenth pair with no consumer at all; see the V6e report.
dialect slice. **V6f closed all three** (the third was the frag's GL-only
`sampler2DArray(handle)` construction), so 8/9 pairs now compile. `mesh` is a
tenth pair with no consumer at all; see the V6e report.
| **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 |
@ -1022,6 +1027,95 @@ consumer is proven only offline, and V4c's diff sits un-landed on a branch for
the duration. Both were accepted as the price of turning an inference into a
measurement — and V6 is work the campaign has to do regardless.
#### 5.5.7 V6f (2026-07-28): the fork cannot be built yet, and why
§5.5.6 selected option (B) — "the RHI world path ships on Vulkan", V4c/V4d's
content returning as the Vulkan world path behind a fork at the thin submission
seam. V6f set out to build that fork and measured, instead, that **there is
nothing for it to select between on the Vulkan side.** The finding is recorded
here so the next slice inherits it rather than rediscovering it.
**The Vulkan path constructs no game state at all.** `GameWindow.Run` branches at
`GameWindow.cs:683` and `return`s at `:695`*before* `Window.Create`, before
`_windowCallbacks.Attach()`, and therefore before `OnLoad`, which is the sole
caller of `GameWindowCompositionPipeline.Run`. On Vulkan **not one composition
phase executes**: no DAT loading, no `LandblockStreamer`, no camera, no entity
table, no world renderer. `VulkanBringUpHost` is a second `main()` that opens its
own window and presents `VulkanRhiScene` + `VulkanRetainedUiScene`, both of which
document themselves as synthetic. A V6f capture confirms it visually
(`artifacts/vk-world/`): checkerboard mip quads, a gradient sphere, two debug
polylines and a generated UI sprite with system-font glyphs. Correct, and not
Dereth.
So a backend-selected fork inside `WbDrawDispatcher` / `EnvCellRenderer` /
`TerrainModernRenderer` would today have a GL arm that runs and a Vulkan arm that
nothing can reach. That is ~2,000 lines of duplicated submission code with no
consumer and no gate — precisely the "unexercised second path" shape §3.1 and
§7.1 rule 3 exist to prevent.
**And the parked V4c/V4d code could not drive Vulkan even if reached.** V4c binds
its texture table as an ordinary storage buffer of packed `GL_ARB_bindless_texture`
`uvec2` handles (`wb-texture-table`) because §5.3 deferred the real port to V4t.
`ObjectRenderBatch.BindlessTextureHandle` is a raw `ulong`, and `GroupKey` — the
bucketing key V4c is forbidden to change — carries it. On Vulkan that buffer is
meaningless: the table is set 2's opaque descriptor array. **V4t is a hard
prerequisite, not a parallel track**, and it is ~4,400 lines across `TextureCache`,
`CompositeTextureArrayCache`, `ManagedGLTextureArray`, `TerrainAtlas` and
`BindlessSupport`, plus retyping three records that fan out into five renderers
and one cache-validity comparison. Landing the fork before V4t means writing the
RHI world path twice, because V4t rewrites exactly the code the fork's Vulkan arm
would contain.
**Two defects the validation layer found, both pre-existing V6bV6d, both
blocking the world path.** One full run with `VK_LAYER_KHRONOS_validation`
(`artifacts/vk-world/client.log`) reported seven distinct VUIDs:
1. **`VUID-VkPipelineLayoutCreateInfo-descriptorType-03032`** and
**`-pSetLayouts-03040`** — "sum of dynamic storage buffer bindings among all
stages (10) exceeds device `maxDescriptorSetStorageBuffersDynamic` limit (8)."
`VulkanPipelineLayouts.cs:101,108` declares all `GpuBindingModel.StorageBindingCount`
= 10 storage bindings as `StorageBufferDynamic`, and the RX 9070 XT allows 8.
**This is the pinned binding model meeting a real device limit, and the world
path is the consumer that needs all ten bindings.** It wants a decision, not a
patch: make the rarely-rebound bindings non-dynamic, or split them across sets.
It fires today only because the verification scene builds the same layout.
2. **`VUID-vkCmdDraw-dynamicRenderingUnusedAttachments-08914` / `-08917`** — the
`vk-scene-line` pipeline declares `depthAttachmentFormat = VK_FORMAT_UNDEFINED`
while the pass carries a `D32_SFLOAT_S8_UINT` depth attachment.
`VulkanGpuPipeline.cs:184,187` sets the format only when
`Depth.Test || Depth.Write`, so any depth-off pipeline in a pass that *has*
depth is malformed. Debug lines are depth-off; so is a lot of UI.
3. **`VUID-vkCmdBeginRendering-pRenderingInfo-09588` / `-09590` / `-09592`** —
`vk-backbuffer-depth` and `vk-backbuffer-msaa-color` are in
`VK_IMAGE_LAYOUT_UNDEFINED` at `vkCmdBeginRendering`. A missing first-use
layout transition on the backbuffer attachments.
Worth recording: the render-target-view-in-table usage from V6c that §5.5.6's
brief expected did **not** fire in this run. Either it needs the paperdoll path
the bring-up host never exercises, or it is not a validation error. Do not carry
it forward as a known-and-accepted item without re-checking.
**Recommended sequencing.** The fork is real and still the plan; it simply comes
after its prerequisites, in this order:
1. **A Vulkan composition host** — a slice the plan has never scoped. Either
`GameWindow`'s composition becomes backend-parameterised (most of V4h) or the
Vulkan host gains a world, which must borrow the same CPU owners rather than
fork them.
2. **The three validation defects above**, since every one of them is on the path
any world frame takes.
3. **V4t**, the texture stack. Nothing world-shaped can sample a texel on Vulkan
until `GpuTextureSlot` replaces the `ulong` bindless handle end to end.
4. **Then** V4c/V4d's content returns as the Vulkan arm of the fork, behind a
construction-time backend selection at the submission seam, with the GL arm
untouched.
A cheaper intermediate milestone exists and is worth considering: **terrain only**
on Vulkan — steps 1, 2, a partial 3 and V4d's plumbing — renders terrain, water
and sky with no scenery or statics, and would be the first real evidence the
Vulkan world path works. V6f's shader work is the whole of that path's shader
prerequisite.
### 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