From 78b0e1421412e05aa822f4ed7cae9c74d9f459cd Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 28 Jul 2026 07:14:41 +0200 Subject: [PATCH] docs(render): park the GL re-land of V4c/V4d, bring V6 forward MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the decision that §5.5.4 recommended and the V5 bring-up now makes actionable: option (C). No further GL-side attempt is made to re-land V4c or V4d until the same ported world path has been measured on Vulkan. The grounds are the three investigation sections read together. A blank run rasterizes its first world frame correctly - frame 43's occlusion counters, 1,692,830 terrain and 317,561 entity samples, byte-match the parent build - and then one irreversible event kills every GPU→CPU return channel at once: readbacks come back RGBA(0,0,0,0) over UI pixels the desktop witness shows on screen, a guarded glGetQueryObject deadlocks the render thread inside the driver, and a GPU-timeline query-buffer write never lands on its sentinel. Present and fences keep running at 5.5 ms throughout, on NO_ERROR from glGetError and a clean glGetGraphicsResetStatus across 1,814 samples. Five mechanisms are falsified and four independent instrument faults have turned up, all of them below the API, all on one driver on one GPU. That is not a shape any further GL-side bisect is well placed to resolve, so the document now states the decision rule rather than leaving option (C) as a recommendation. If the identical RHI world path renders correctly on Vulkan on this GPU, the driver defect is proven and option (B) is adopted deliberately: GL keeps the legacy world path through V10 as a documented, scoped exception to §3.1's no-fork rule, confined to the thin submission seam. If it fails on Vulkan too, the trigger is in code we own and the hunt resumes against a much smaller haystack. Two knock-on edits keep the plan self-consistent rather than leaving the reorder stated in one place and contradicted in another. The slice table marks V4c and V4d parked, V4t and V4e-V4h re-sequenced pending the verdict, and V5 shipped. §5.4's sequencing invariants no longer claim V0→V4h is strictly sequential, and they now carry the consequence that matters: V6 arrives before V4g and V4h, so the Vulkan backend honours the contract's literal null target while GL still carries the transitional inheritance. That makes §5.4's two removal obligations more binding, not less, and V7's differential must not run until the removal has happened - otherwise it would surface the divergence as a viewport rendering to the wrong surface, which post-decision is indistinguishable from the fork option (B) permits. Co-Authored-By: Claude Fable 5 --- docs/plans/2026-07-27-vulkan-campaign.md | 117 ++++++++++++++++++++--- 1 file changed, 105 insertions(+), 12 deletions(-) diff --git a/docs/plans/2026-07-27-vulkan-campaign.md b/docs/plans/2026-07-27-vulkan-campaign.md index 7e9528f4..c148e100 100644 --- a/docs/plans/2026-07-27-vulkan-campaign.md +++ b/docs/plans/2026-07-27-vulkan-campaign.md @@ -545,14 +545,14 @@ because sample positions are not specified across implementations. | **V3** | Clip-space and sRGB audit: verify every projection producer is [0,1] convention, confirm clip-plane derivation, record the sRGB swapchain decision and the depth-precision divergence class here. | pixel gate + connected lifecycle | | **V4a** | `TextRenderer` (three fence-buffered VBO sets → ring allocations), `BitmapFont`, `DebugLineRenderer`, the UI RenderSurface upload path, `UiViewport`'s texture handoff. | pixel gate (UI-heavy checkpoints) | | **V4b** | `GlobalMeshBuffer` + `ObjectMeshManager` onto `IGpuBuffer`; arena, LRU and ledger logic untouched. | pixel gate | -| **V4c** | **The large one.** `WbDrawDispatcher` + `EnvCellRenderer`: per-frame uploads → rings, MDI brackets → pipelines + `MultiDrawIndexedIndirect`, loose uniforms → push constants, timer scopes. `RetailAlphaQueue` and all bucketing untouched. **Narrowed after the V4c scouting report — see §5.3.** | pixel gate at several checkpoints + connected lifecycle | -| **V4t** | **World texture stack** (added 2026-07-27, see §5.3): `TextureCache`, `CompositeTextureArrayCache`, `ManagedGLTextureArray`, `TerrainAtlas` and `ObjectMeshManager`'s material path onto `IGpuTexture`/`IGpuSampler`; retype `GroupKey`, `CachedBatch` and `ObjectRenderBatch` from `ulong` bindless handle to `GpuTextureSlot`; retire the interim per-renderer handle tables for V4c, V4d and V4e at once. | pixel gate | -| **V4d** | `TerrainModernRenderer` only — **`TerrainAtlas` belongs to V4t** with the rest of the texture stack. Two sub-commits: first the `uView`/`uProjection` → `uViewProjection` shader convergence on its own pixel gate (it moves a matrix product from per-vertex GPU to a CPU multiply, so its rounding effect must be attributable alone), then the plumbing. Terrain has no GPU timer to port — its diagnostics use a CPU `Stopwatch`. | pixel gate per sub-commit | -| **V4e** | `ParticleRenderer` (after V4c — shared alpha-queue contract). | pixel gate (particle-heavy checkpoint) | -| **V4f** | `SkyRenderer` + weather. | pixel gate (dawn/dusk, day group pinned) | -| **V4g** | `PrivateEntityViewportRenderer` → `IGpuRenderTarget`; `PortalDepthMaskRenderer` + `PortalTunnelPresentation` → stencil/depth-mask pipelines. | pixel gate incl. paperdoll and portal transit | -| **V4h** | 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 | +| **V4c** ⏸ **PARKED — §5.5.5** | **The large one.** `WbDrawDispatcher` + `EnvCellRenderer`: per-frame uploads → rings, MDI brackets → pipelines + `MultiDrawIndexedIndirect`, loose uniforms → push constants, timer scopes. `RetailAlphaQueue` and all bucketing untouched. **Narrowed after the V4c scouting report — see §5.3.** | pixel gate at several checkpoints + connected lifecycle | +| **V4t** ↻ **re-sequenced — §5.5.5** | **World texture stack** (added 2026-07-27, see §5.3): `TextureCache`, `CompositeTextureArrayCache`, `ManagedGLTextureArray`, `TerrainAtlas` and `ObjectMeshManager`'s material path onto `IGpuTexture`/`IGpuSampler`; retype `GroupKey`, `CachedBatch` and `ObjectRenderBatch` from `ulong` bindless handle to `GpuTextureSlot`; retire the interim per-renderer handle tables for V4c, V4d and V4e at once. | pixel gate | +| **V4d** ⏸ **PARKED — §5.5.5** | `TerrainModernRenderer` only — **`TerrainAtlas` belongs to V4t** with the rest of the texture stack. Two sub-commits: first the `uView`/`uProjection` → `uViewProjection` shader convergence on its own pixel gate (it moves a matrix product from per-vertex GPU to a CPU multiply, so its rounding effect must be attributable alone), then the plumbing. Terrain has no GPU timer to port — its diagnostics use a CPU `Stopwatch`. | pixel gate per sub-commit | +| **V4e** ↻ **re-sequenced — §5.5.5** | `ParticleRenderer` (after V4c — shared alpha-queue contract). | pixel gate (particle-heavy checkpoint) | +| **V4f** ↻ **re-sequenced — §5.5.5** | `SkyRenderer` + weather. | pixel gate (dawn/dusk, day group pinned) | +| **V4g** ↻ **re-sequenced — §5.5.5** | `PrivateEntityViewportRenderer` → `IGpuRenderTarget`; `PortalDepthMaskRenderer` + `PortalTunnelPresentation` → stencil/depth-mask pipelines. | pixel gate incl. paperdoll and portal transit | +| **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, three sequential commits: **a** allocator/buffers/staging/rings/timeline; **b** textures/BC mips/samplers/descriptor table/render targets/MSAA resolve; **c** `.spv` toolchain, pipelines, pipeline cache, negative viewport, push constants, timestamps, readback, debug names. **Milestone: full game frame on Vulkan.** | per-commit build + tests; VK renders world, UI, paperdoll, portals | | **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 | @@ -907,6 +907,85 @@ second column), and no re-land attempt should be made before the (C) measurement because a 10/10 pass on this machine cannot distinguish a fix from the defect's ordinary ~1-in-5 quiet streak. +#### 5.5.5 Decision (2026-07-28): the GL re-land of V4c/V4d is PARKED, V6 is brought forward + +Option (C) of §5.5.4 is adopted. **No further GL-side attempt is made to re-land +V4c or V4d until the Vulkan world path has been measured.** V5 landed at +`e8a4c1af` — the capability gate passes on the RX 9070 XT, Vulkan 1.4.349, AMD +driver 2.0.395, with a `B8G8R8A8Unorm` swapchain presenting the clear colour and +the forced-unsupported knob returning exit 4. V6 follows immediately; V4c and V4d +stay un-landed on their branches meanwhile. + +**Grounds.** The evidence in §5.5.1–§5.5.3 converges on one shape, and it is not +the shape of an application bug. + +- **The world is drawn correctly, once.** §5.5.3's frame 43 reports 1,692,830 + terrain samples and 317,561 entity samples on V4c and **byte-matches the parent + build's first world frame on both counters**. Whatever goes wrong is not a + rasterization difference; the ported path draws exactly what the working path + draws. +- **Then one irreversible event kills every GPU→CPU return channel at once.** + From that frame on, `glReadPixels` of framebuffer 0 returns RGBA(0,0,0,0) even + over UI pixels the desktop witness plainly shows on screen; a guarded + `glGetQueryObject` read never returns and deadlocks the render thread inside + the driver; and a GPU-timeline `GL_QUERY_BUFFER` write never lands, leaving the + pre-filled sentinel untouched. The transition is sharp, total, and permanent — + 924 consecutive dead frames in run 1. +- **Meanwhile the channels that carry no result keep working.** Present and fence + signalling continue at a steady 5.5 ms for ~3,700 frames. The GPU is running + the frame; it has stopped *reporting*. The common factor is direction, not + subsystem — the UI query dies alongside the world queries, so §5.5.2's + "shared 3-D state" framing is superseded. +- **Every API-level explanation has been eliminated.** `glGetError` is clean + throughout, and `glGetGraphicsResetStatus` returned `NO_ERROR` on **1,814 + samples across four blank runs**. Five mechanisms are falsified: the ring's + `glBufferSubData` hazard (§5.5.1, and condition 1 landed at `8dec163f` without + fixing it), a capture-FBO binding leak, reveal ordering, shared 3-D state + (depth plane bit-identical, camera sane, clip distances forced off changed + nothing, no context reset), and CPU-side visibility (3,331 statics dispatched + on blank frames). +- **Four independent instrument faults, all below the API, all on one driver.** + The multisampled `glReadPixels` (§5.5.1), the in-process capture that cannot + see the presented surface (§5.5.2), the deadlocking CPU query read and the + never-executed GPU-timeline query write (§5.5.3). A perfectly legal occlusion + query makes the failure *worse*. All of it on AMD 26.6.4, RX 9070 XT — one + driver, one GPU, no second data point. + +A context that loses its readback, its query results, and its ability to answer a +query without blocking — simultaneously, on a clean reset status — has failed in +a way not expressible in the API. Continuing to bisect the V4c diff (option A) +costs roughly three hours of connected machine time per round, pins the user's +machine, and has five falsified mechanisms behind it. + +**Decision rule.** The same ported world path running on Vulkan on the same GPU +is the decisive discriminator, and it yields a verdict in one measurement instead +of a multi-hour bisect: + +- **Clean on Vulkan ⇒ the driver defect is proven.** Option (B) is then adopted + deliberately rather than inferred. GL keeps the legacy raw-GL world path + through to V10 as a **documented, scoped exception to §3.1's no-fork rule, + confined to the thin submission seam** — the world renderers, not the contract. + The consequences must be carried explicitly: V4h's "seam complete" milestone no + longer means "nothing raw-GL remains," and V7's differential compares a raw-GL + world against an RHI world rather than one contract against two backends, which + weakens it exactly where it is most valuable. +- **Fails on Vulkan too ⇒ the trigger is ours.** The defect is then in code we + own, and (A) becomes worth its cost because the haystack is far smaller: a + fault reproducing on both backends is a property of the ported path itself, not + of a driver's GL stack. + +**Sequencing.** V5 and V6 execute next, in that order. **V4t and V4e–V4h are +re-sequenced after the verdict**, not before it — V4h in particular cannot be +specified until it is known whether "nothing raw-GL remains" is still reachable. +V4a and V4b are landed and unaffected. §5.5's re-land conditions remain binding +on any eventual V4c/V4d re-land, including the §5.5.4 amendment that the gate +asserts on the desktop witness. + +**What this costs.** V5 and V6 are written against an RHI whose world-path +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.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 @@ -935,10 +1014,24 @@ If V4h lands without removing it, the GL and Vulkan backends will disagree about what a null target means, and the V7 differential will surface it as an entire viewport rendering to the wrong surface. -**Sequencing invariants.** The app ships on GL until V10. V0→V1→V2→V3→V4a…V4h -are strictly sequential. The only permitted parallelism is V5 alongside V4d -and/or V4f (fully disjoint files), and optionally V9's `.github`/`tools`-only -work alongside V8. While V4c runs, nothing else touches `Rendering/Wb`. +**Sequencing invariants.** The app ships on GL until V10. The original +V0→V1→V2→V3→V4a…V4h chain was strictly sequential, with the only permitted +parallelism being V5 alongside V4d and/or V4f (fully disjoint files) and +optionally V9's `.github`/`tools`-only work alongside V8; while V4c runs, nothing +else touches `Rendering/Wb`. + +**Amended by §5.5.5 (2026-07-28).** V4c/V4d are parked and V5→V6 execute next, so +that chain no longer holds past V4b. Two consequences land on *this* section. +First, V6 now arrives **before** V4g and V4h, so the Vulkan backend is written +against the contract's literal `Target: null` — the acquired swapchain image — +while GL still carries the transitional inheritance described above. That is +tolerable only because the two backends are never live in the same process, and +it makes obligations 1 and 2 above *more* binding, not less: whichever slice +finally lands V4g/V4h still owes the removal, and until then the divergence is +load-bearing on the GL side alone. Second, V7's differential must not be run +until that removal has happened, or it will surface the divergence as an entire +viewport rendering to the wrong surface — which, post-§5.5.5, would be +indistinguishable from the fork option (B) permits. ---