diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 1633705a..e9ad4b4b 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -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 diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index ca757b60..97dbeff7 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -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: V0–V2, V4a, V4b, V5, V6a–V6f. 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. --- diff --git a/docs/plans/2026-07-27-vulkan-campaign.md b/docs/plans/2026-07-27-vulkan-campaign.md index 650977ce..9216b9f8 100644 --- a/docs/plans/2026-07-27-vulkan-campaign.md +++ b/docs/plans/2026-07-27-vulkan-campaign.md @@ -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 V6b–V6d, 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