docs(render): close Campaign V slice V4c and pin its two obligations

V4c verified independently: offline pixel gate passing at 26 differing pixels of 563,200 against a 17-pixel same-commit control, App suite 3,844/3, 8 files, contract untouched, no tests removed, encoding clean.

Two things the slice surfaced are now written down rather than left in a report. First, GL BeginPass had to stop binding framebuffer 0 for a null target, because the viewport and portal renderers bind their own FBO before calling the dispatcher - correct today, but it makes GL diverge from the contract, and Vulkan must honour a null target literally as the swapchain image. V4h has to restore it or the V7 differential will show an entire viewport rendering to the wrong surface. Second, the offline gate exercised the dispatcher hard and EnvCellRenderer not at all, so dungeon interiors are half of V4c and remain unproven; the accumulated user-gate debt across V2c, V4c and the upcoming V4e/V4f/V4g is now tabulated with the connected route that clears it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-27 20:58:07 +02:00
parent f353fb53f8
commit cb0182a06c

View file

@ -485,9 +485,23 @@ fog, and the entire retained UI (vitals, spell bar, toolbar, chat, radar).
**Not covered — these still need a user visual gate:** sky (masked), EnvCell **Not covered — these still need a user visual gate:** sky (masked), EnvCell
interiors, particles, and the paperdoll/appraisal viewports, because the offline interiors, particles, and the paperdoll/appraisal viewports, because the offline
scene is a fixed outdoor view with no camera control. Slices **V4e** (particles), scene is a fixed outdoor view with no camera control.
**V4f** (sky), and **V4g** (viewports) therefore carry a user gate in addition to
their automated one. **Accumulated user-gate debt.** Each of these landed with its automated gate green
but part of its surface unproven. They should be checked together, in one connected
session, rather than one at a time:
| Slice | What the offline gate could not prove |
|---|---|
| V2c | Particle texture-index migration — no particles in the captured scene |
| V4c | **`EnvCellRenderer` — zero EnvCell activity in the capture.** Dungeon interiors are half of that slice and are entirely unproven. Also the paperdoll/appraisal/portal-tunnel views, which is precisely what §5.4's `BeginPass` change protects |
| V4e | Particles (again) |
| V4f | Sky — deliberately masked for determinism |
| V4g | Paperdoll and appraisal viewports, portal transit |
The connected route to run: a dungeon interior, a portal transit, the paperdoll and
an item-appraisal window, a spell cast with visible particle effects, and a dawn or
dusk sky.
MSAA is left at the quality preset for GL-versus-GL self-differentials, where it MSAA is left at the quality preset for GL-versus-GL self-differentials, where it
is deterministic. The V7 GL-versus-Vulkan differential must force MSAA off, is deterministic. The V7 GL-versus-Vulkan differential must force MSAA off,
@ -572,6 +586,34 @@ as well, and the `SceneLighting` UBO (binding 1) by terrain and the four viewpor
portal renderers. GL binding points are global, so the safe move while those consumers portal renderers. GL binding points are global, so the safe move while those consumers
are still raw GL is to leave both bound as they are and convert them with the spine. are still raw GL is to leave both bound as they are and convert them with the spine.
### 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
`Target: null`. The reason is sound: `PrivateEntityViewportRenderer` and
`PortalTunnelPresentation` bind their own offscreen FBO and *then* call
`WbDrawDispatcher.Draw`, so forcing framebuffer 0 would have redirected the
paperdoll, appraisal and portal-tunnel views to the backbuffer. The offline gate
would never have caught it — none of those surfaces appear in its scene.
**But this makes GL's `BeginPass` diverge from the contract it implements.**
`GpuColorAttachment` documents `Target: null` as "the backbuffer," and the Vulkan
backend *must* honour that literally: a null target is the acquired swapchain
image (or the multisampled scratch that resolves into it), and there is no
ambient "currently bound framebuffer" for it to inherit instead.
So this is a **GL-only transitional behaviour, correct today and wrong at V6.**
Two obligations follow:
1. **V4g** ports those renderers onto `IGpuRenderTarget`, at which point they
declare their target explicitly and the inheritance is no longer needed.
2. **V4h** restores GL `BeginPass` to binding the declared target, once the spine
owns framebuffer management and every consumer names its own. The Vulkan
backend is written against the contract, never against this divergence.
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 **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 are strictly sequential. The only permitted parallelism is V5 alongside V4d
and/or V4f (fully disjoint files), and optionally V9's `.github`/`tools`-only and/or V4f (fully disjoint files), and optionally V9's `.github`/`tools`-only