docs(render): park the GL re-land of V4c/V4d, bring V6 forward

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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-28 07:14:41 +02:00
parent e8a4c1af3f
commit 78b0e14214

View file

@ -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 V4eV4h 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.
---