diff --git a/docs/plans/2026-07-27-vulkan-campaign.md b/docs/plans/2026-07-27-vulkan-campaign.md index e2ce24cd..179d8469 100644 --- a/docs/plans/2026-07-27-vulkan-campaign.md +++ b/docs/plans/2026-07-27-vulkan-campaign.md @@ -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 interiors, particles, and the paperdoll/appraisal viewports, because the offline -scene is a fixed outdoor view with no camera control. Slices **V4e** (particles), -**V4f** (sky), and **V4g** (viewports) therefore carry a user gate in addition to -their automated one. +scene is a fixed outdoor view with no camera control. + +**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 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 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 are strictly sequential. The only permitted parallelism is V5 alongside V4d and/or V4f (fully disjoint files), and optionally V9's `.github`/`tools`-only