diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index ec9d5533..d0e40991 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -62,10 +62,11 @@ accepted-divergence entries (#96, #49, #50). --- -## 2. Adaptation (AD) — 38 rows (AD-11 retired 2026-07-23 — exact low-bit ItemUses predicate; AD-31 retired 2026-07-15 — the DAT-authored portal-space viewport replaces the black transit cover) +## 2. Adaptation (AD) — 39 rows (AD-11 retired 2026-07-23 — exact low-bit ItemUses predicate; AD-31 retired 2026-07-15 — the DAT-authored portal-space viewport replaces the black transit cover) | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| +| AD-46 | **Vulkan-arm only, dormant until the V10 cutover.** Dense alpha-blended distant scenery (the treeline) resolves differently on the Vulkan backend than on GL, by about 15% of the pixels in the band, even though both arms now request identical sampler state — trilinear, clamp-and-repeat, and the device's maximum anisotropy. What remains is the anisotropic TAP PATTERN, which both the GL and Vulkan specifications leave implementation-defined, and AMD's two drivers do not agree. | `src/AcDream.App/Rendering/Wb/WorldTextureArray.cs` (`RhiWorldTextureArray.WorldArrayAnisotropy`); measured in plan §5.5.19 | Not assumed — narrowed by measurement, on an offline capture with no session, no entities and both clocks pinned. Anisotropy 1 → 41,509 differing pixels in the tree band; anisotropy 16 (GL's value, and retail's `m_D3DCaps.MaxAnisotropy`) → 22,266, and the rest of the frame falls to 497 px of 563,200, i.e. 8.8e-04, inside the campaign's 0.001 threshold. The residual is not a sub-pixel shift (an integer shift search finds none), not a sharpness change (high-frequency energy matches within 5%), and not depth precision (forcing Vulkan's window-depth range to GL's compressed [0.5, 1] moved it by 3%). Monotone improvement toward GL's own anisotropy with no knob left is what makes it a driver property rather than a bug. | If it is NOT the tap pattern, some other Vulkan sampler or alpha-path difference is hiding behind this row, and it will present after cutover as distant foliage that shimmers or reads denser than retail's. The class is confined to alpha-blended dense overlap: opaque terrain, roofs, walls, water, statics, the character and the whole retained UI are inside threshold. | `RenderDeviceD3D::SetDefaultD3DStates @ 0x005a3800`, whose `SetSamplerState(stage, 0xA /* D3DSAMP_MAXANISOTROPY */, m_D3DCaps.MaxAnisotropy)` at `0x005a4230` is the value both arms now request | | AD-38 | Outgoing teleport viewports retire when retail's quantized animation level exceeds the last captured visible level 1022 (index 96), suppressing levels 1023/1024 up to 20.2 ms before retail's literal `elapsed >= 1.0` state edge. Incoming fades retain the exact timer. | `src/AcDream.Core/World/TeleportAnimSequencer.cs` (`OutgoingViewportReachedTerminalProjection`) | An uncapped 2000 FPS pass can publish the finite tunnel at levels 1023/1024 even though the paired 2013 retail capture switches viewports after 1022. The table-level cutover preserves the captured visible viewport ordering without throttling the application. | Exit sound, viewport replacement, and logout tunnel entry can occur at most two easing-table quanta (about 20.2 ms) earlier than retail's logical timer. | `UIGlobals::GetAnimLevel @ 0x004EE540`; `gmSmartBoxUI::UseTime @ 0x004D6E30`; paired retail/acdream captures documented in `docs/research/2026-07-15-retail-portal-space-pseudocode.md` | | AD-1 | Lost-cell machinery replaced by recoverable outdoor demote (**#107** safety net) + outdoor-restore `max(terrainZ, z)` under-terrain lift; retail goes `GotoLostCell` | `src/AcDream.Core/Physics/PhysicsEngine.cs:553` (+ :808) | acdream has no lost-cell state machine; outdoor landcell is the recoverable equivalent; the #107 auto-entry hold should make the demote branch unreachable | Gap in the hold → player committed to outdoor terrain inside/under a building (fake-grounded spawn, fall-through); a legit below-heightmap server restore is silently lifted — upward warp vs server | `GotoLostCell` pc:283418; `SetPositionInternal` 0x00515bd0, pc:283892-283945 | | AD-2 | Async readiness gates replace retail's synchronous destination cell load. **#229 refinement (2026-07-20):** login and F751 portal-space exit now share `WorldRevealReadinessBarrier`, so neither path can expose the normal viewport until the same render-publication, composite-texture, and collision domains converge. A hydratable indoor claim requires its owning Near-tier static/EnvCell mesh set, destination composites, and exact EnvCell physics (`IsSpawnCellReady`); an outdoor claim requires those render domains plus terrain/collision residency for the required Near ring. Hard-recenter generations and tier-aware completion application prevent stale overlapping loads/unloads or Far/Near jobs from opening or erasing the gate; mesh upload remains separate from balanced landblock ownership. Claims beyond NumCells still take the loud unhydratable-placement path. `RuntimeWorldTransitState` owns the shared reveal generation, accepted readiness, transit correlation, and exact generation/cell-scoped host-acknowledgement suffix. `WorldRevealCoordinator` is a graphical adapter holding only App resource receipts; normalized Runtime checkpoints observe ownership without defining another readiness path. **Slice E3 refinement (2026-07-24):** the same generation now publishes an immediate `WorldGenerationQuiescence` edge: old-world drawing/spatial queries, simulation/effect clocks, reconciliation, targeting, and 3-D audio stop while retained physical teardown advances through metered cursors and destination network/UI/streaming/readiness remain live. **Slice E4 refinement (2026-07-24):** accepted render/physics/static publication may span update frames through retained exact cursors, but reveal still consumes only the completed spatial/render-ready generation; building and EnvCell snapshots remain invisible until complete and the final spatial identity swap stays observer-atomic. **Slice E5 refinement (2026-07-24):** the reveal generation owns one exact destination reservation across every typed budget dimension. Stale completion cannot consume or clear its replacement, and hydratable incomplete content is never force-revealed; portal transit retains the DAT tunnel and centered retail wait cue until readiness converges. The hold→materialize→regain-control lifecycle remains owned by `TeleportAnimSequencer`. | `src/AcDream.Runtime/World/RuntimeWorldTransitState.cs`; `src/AcDream.App/Streaming/WorldRevealCoordinator.cs`; `src/AcDream.App/Streaming/WorldGenerationQuiescence.cs`; `src/AcDream.App/Streaming/WorldRevealReadinessBarrier.cs`; `src/AcDream.App/Streaming/StreamingOriginRecenterCoordinator.cs`; `src/AcDream.App/Streaming/LandblockPresentationPipeline.cs`; `src/AcDream.App/Streaming/StreamingController.cs`; `src/AcDream.App/Rendering/PortalTunnelPresentation.cs`; `src/AcDream.App/UI/PortalWaitNoticeController.cs`; `src/AcDream.App/Streaming/GpuWorldState.cs` (`IsRenderReady`); `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs`; `src/AcDream.Core/Physics/PhysicsEngine.cs` (`IsSpawnCellReady`, `IsNeighborhoodTerrainResident`) | This is the asynchronous equivalent of retail leaving `SmartBox::position_update_complete` false while `CellManager::blocking_for_cells` is set: neither initial login nor portal arrival may reveal or continue simulating an old/partial collision world, a terrain-only Far shell, or a published-but-not-drawable GPU landblock. Indoor does not require a terrain heightmap, only the owning render landblock and exact EnvCell. | Gate opens early → grey/untextured first login or portal reveal, free-fall, wrong-cell rooting, missing scenery, or a still-active old generation; predicate never satisfies (streamer/DAT/upload failure) → login remains behind the world render gate, while portal transit remains in the authored tunnel and presents the centered wait cue after five seconds. | `SmartBox::UseTime` 0x00455410; `gmSmartBoxUI::UseTime` 0x004D6E30; `gmSmartBoxUI::EndTeleportAnimation` 0x004D65A0 | diff --git a/docs/plans/2026-07-27-vulkan-campaign.md b/docs/plans/2026-07-27-vulkan-campaign.md index ee55cd67..28761f41 100644 --- a/docs/plans/2026-07-27-vulkan-campaign.md +++ b/docs/plans/2026-07-27-vulkan-campaign.md @@ -542,6 +542,7 @@ session, rather than one at a time: | V6k | **The sky band and the paperdoll — both CHECKED rather than banked, and recorded here so the next slice does not re-open them.** Commit 1 carries a seven-day-group before-and-after comparison on GL (V6e's method) because the gate masks the sky; commit 2 carries a connected run that presses `ToggleInventoryPanel` and captures the doll through the new render target (`artifacts/v6k-paperdoll`). **What remains uncovered:** the creature-appraisal viewport, which is the same class and the same code as the paperdoll but was not itself driven; and the sun, moon and rain cylinder, which V6e already filed and a fixed outdoor camera at one time of day still cannot see | | V6l | **The Vulkan paperdoll and a particle effect — both CHECKED rather than banked, and both connected, because the offline gate reaches neither.** The doll capture (`artifacts/v6l-vk-paperdoll3` against `artifacts/v6l-gl-paperdoll`) is also the no-regression check for making the viewport's V orientation backend-derived. The particle capture (`artifacts/v6l-vk-poi` against `artifacts/v6l-gl-poi`) is Holtburg's forge plume and glint field, cropped 4× at `artifacts/crop-vk-glow.png` / `crop-gl-glow.png`. **What remains uncovered:** the creature-appraisal viewport, which is the same class and the same code as the paperdoll but was again not itself driven — V6k's half-discharge, carried forward unchanged; and the portal depth mask, which drew no pixel in either capture because neither run entered a building aperture. Check it with the dungeon/doorway pass above | | V6m | **The portal tunnel, the creature-appraisal viewport and an interior EnvCell — all three CHECKED, connected, on BOTH backends, because the offline gate reaches none of them.** `artifacts/v6m-gl-tunnel` versus `artifacts/v6m-vk-tunnel`: ten transit frames, the examination window over a Brown Rabbit, and the Facility Hub's interior. **This discharges V6k's and V6l's carried appraisal-viewport half-discharge** — the view that shares the paperdoll's class and code has now been driven itself. **What remains uncovered:** the portal DEPTH MASK still drew no pixel, because none of these runs stood in a building aperture either; and the interior pair is eyes-on only, because the indoor spring-arm camera settled to different distances in the two runs (31.2% differing, a route defect rather than a renderer one — see §5.5.18) | +| V7 | **The portal depth mask, for the third slice running, and now a numeric interior instead.** V7 fixed the interior stop by pinning the world clock rather than by touching the route (§5.5.19), so `facility_hub_interior` has a numeric GL-versus-Vulkan pair at 7.79e-03 whose entire residual is the player character — the EnvCell itself is clean, which discharges V6m's item 2. **What remains uncovered is unchanged and now three slices old:** no automated run has stood in a building aperture, so `PortalDepthMaskRenderer` has still never drawn a pixel outside an eyes-on session. `HouseExitWalkReplayTests` names the cheapest target — the Holtburg corner building, cell `0xA9B40170` — whose exit door it already resolves from the DAT | | 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 @@ -616,7 +617,7 @@ tenth pair with no consumer at all; see the V6e report. | **V6k** ✅ | **Sky, viewports, and §5.4**, reported in §5.5.16. Two commits: **1** `SkyRenderer`'s RHI arm — V4f's content, two blend pipelines, `SkyParams` as a per-draw ring slice, the first Vulkan consumer of set 1 binding 4 — plus the retirement of the last interim `GlBindlessHandleTable` (`22aa2edc`); **2** `PrivateEntityViewportRenderer` onto `IGpuRenderTarget`, the deletion of §7.1's external-texture seam, and **§5.4's obligation discharged** (`eb7e6b4e`). **Particles did not land**: the pinned contract cannot express instanced vertex input, which is what both particle pipelines are built on. | GL pixel gate 4.43e-05 then 4.08e-05 (25 and 23 px, band 9–31), App tests 4,109/3, complete Release suite 9,172/5, GL connected `-Runs 3` at 3/3 on both columns per commit, one validation-layer Vulkan run at 0 errors / 0 warnings per commit, a seven-day-group GL sky comparison, a connected paperdoll capture, and a GL-versus-Vulkan inspection in §5.5.16 | | **V6l** ✅ | **Particles, the portal mask and the viewports**, reported in §5.5.17. Three commits, one per contract amendment: **1** instanced vertex input (`GpuVertexLayout` per-binding stride and input rate, `BindVertexBuffer(binding, …)`, `GpuVertexFormat.UInt1`) plus `ParticleRenderer`'s RHI arm, the standalone particle texture cache on both arms, and the stride-equals-the-uploaded-record gate for every RHI vertex layout (`b1ad1d48`); **2** the stencil dimension (`StencilTest` + `GpuStencilState` + `SetStencil`), `portal_depth` as a committed shader pair, `PortalDepthMaskRenderer`'s RHI arm, and the ambient stencil/colour-mask restore (`eced67d0`); **3** a layered sampled view per Vulkan render target, sample-count pipeline variants for `WbDrawDispatcher`, the composite texture cache on both arms, and the backend-derived viewport V orientation (`2e8b8b91`). | GL pixel gate per commit (3.20e-05, 2.31e-05, 3.55e-05; band 9–31 px), App tests 4,121/4,129/4,129 and complete Release suite 9,184/9,192/9,192, GL connected `-Runs 3` at 3/3 on both columns per commit, one validation-layer Vulkan run at 0 errors / 0 warnings per commit, and connected Vulkan paperdoll and particle captures inspected against GL | | **V6m** ✅ | **Portal space, and V7's instrument**, reported in §5.5.18. Two commits: **1** `PortalTunnelPresentation`'s RHI arm — the last raw-GL world-adjacent renderer — as a backbuffer pass published on `IWorldPassScope`, clearing to retail's opaque portal-space black rather than loading it, plus the deletion of `NullLocalPlayerTeleportPresentation` (`59c6b2ae`); **2** `tools/run-backend-differential-gate.ps1` and `connected-backend-differential.route.txt`, with MSAA forced off on both launches, the repeat gate's desktop-witness guards, and **an interior EnvCell stop** — §5.1's durable fix for the campaign's oldest coverage gap (`a99f517e`). One smoke pair was run and is reported in full. | GL pixel gate 4.97e-05 (28 px) vs `280f3b3f` against a 3.55e-05 (20 px) same-commit control, band 9–31; App tests 4,132/3 and complete Release suite 9,195/5; GL connected `-Runs 3` at 3/3 on both columns; one validation-layer Vulkan run at 0 errors / 0 warnings; connected portal-tunnel, creature-appraisal and interior-EnvCell captures on BOTH backends, inspected in §5.5.18 | -| **V7** | GL-versus-Vulkan differential: `tools/run-backend-differential-gate.ps1` (**built at V6m**), 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.** Starting distance, measured at V6m: 18.52% of the frame at the first stop, 1.17% with the sky band and the animated portal masked, with the 2-D retained UI already inside the threshold on its own. | every differential checkpoint passes; both connected routes green on VK | +| **V7** ◐ **partially discharged — §5.5.19** | GL-versus-Vulkan differential: `tools/run-backend-differential-gate.ps1` (**built at V6m**), 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.** Starting distance, measured at V6m: 18.52% of the frame at the first stop. **What landed:** the world atlases' missing anisotropy (`ad5f8b68`, retail-anchored at `0x005a4230`), and two instrument pins the gate was silently missing — the cloud sheet's phase and, much larger, the Dereth clock, which had never actually been pinned by anything and was moving 22% of the frame between two captures 45 s apart *in the same run*. Offline GL-versus-Vulkan, both clocks pinned, is now **8.82e-04 below the tree band — inside the threshold**; the treeline is `AD-46`, proven not to be the depth class. **What did NOT land:** a passing connected stop (each carries a named phase exception), per-stop masks in the gate script, an aperture stop for the portal depth mask, the R6 soak on Vulkan, and the RenderDoc capture. | 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 | | **V10** | Cutover: Vulkan default, GL reachable by env var for one slice, gate scripts default to VK. | complete Release suite + retail expected PNGs **on VK** (baselines not regenerated) + both connected routes + **user visual sign-off** | @@ -2532,6 +2533,219 @@ connected Vulkan session end to end with a graceful exit. threshold, and the chat-panel measurement rules the 2-D path out. This is V7's first lead and it is a real one. +#### 5.5.19 V7 (2026-07-28): the instrument was measuring itself + +**The headline is not a renderer fix. Three of V6m's four numbers were taken +through an instrument that was not holding the world still, and the largest +single improvement in this slice came from noticing that.** + +**0. A transplanted test fix** (`a7529a97`). `81427cd4` from +`worktree-agent-aa0684dee99776743` serializes the ten App test classes that share +`CameraDiagnostics`, `RenderingDiagnostics.ProbeFlapEnabled` and `Console.Out`. +Its base predates the campaign, so its `docs/ISSUES.md` entry claimed `#251` — +already taken on this branch — and was renumbered to **`#252`** in the conflict +resolution, subject included. App suite verified at **4,132 / 3**. It also +de-flakes `Issue181WallPressEquilibriumTests`, which V7 leans on. + +--- + +**1. The world atlases were sampled without anisotropy on Vulkan** +(`ad5f8b68`). `RhiWorldTextureArray` — the only `IWorldTextureArray` the Vulkan +arm ever constructs — registered its clamp and repeat slots with +`GpuSamplerDescription.WorldClamp`/`WorldRepeat` as written, which carry +`MaxAnisotropy: 1`. The GL arm asks for the driver's own +`GL_MAX_TEXTURE_MAX_ANISOTROPY` twice over, on the image and again on the two +sampler objects its resident bindless handles are built from. V6i-2 knew it was +asking for 1 and left a comment saying the slice that draws through these arrays +is the one that can gate a filtering change; that slice was V6j and the gate is +here. + +**Retail settles it rather than the GL arm settling it.** +`RenderDeviceD3D::SetDefaultD3DStates` (`0x005a3800`) loops all sixteen sampler +stages and issues `SetSamplerState(stage, 0xA, this->m_D3DCaps.MaxAnisotropy)` at +`0x005a4230`. `0xA` is `D3DSAMP_MAXANISOTROPY` and the argument is the device's +reported cap, not a setting. "As much anisotropy as this device has" is retail's +rule; asking for 1 diverged from retail as well as from GL, and no register row +is owed in either direction. + +The fix requests a ceiling rather than reading a limit back, because §3.3 is +frozen and has no anisotropy field — and does not need one: `VulkanGpuSampler` +already clamps to `maxSamplerAnisotropy`, which Vulkan guarantees is at least 16 +wherever `samplerAnisotropy` is supported. **Measured worth**, on the offline +capture below: the tree band halves (41,509 → 22,266 differing pixels) and +everything below it drops by two thirds (1,491 → 497). The roof shingles of both +Holtburg cottages, which had been dense hatching across the whole surface, go +black in the difference map; high-frequency energy on that roof matches GL at a +ratio of 0.999. + +**2. The sky has two clocks, and the second one was pinned rather than masked** +(`ad5f8b68`). `ACDREAM_DAY_GROUP` pins the day group; the cloud sheet does not +read that clock at all — `SkyRenderer` accumulates `TexVelocityX/Y` against +`DateTime.UtcNow` minus its own construction time, by design, because retail's +clouds drift with real time regardless of the date. `ACDREAM_SKY_PHASE_SECONDS` +replaces that elapsed value with a fixed one, off by default. Rows 0–32 of the +Holtburg pair went from 23,090 differing pixels to 1,211. **The alternative was +`-MaskTopPixels`, which would have permanently blinded the campaign's strictest +instrument to the entire sky.** See §5.1. + +**3. The world clock was never pinned at all, and that was most of the number** +(`1f25a609`). The route opened by pressing `AcdreamCycleTimeOfDay` three times. +The mechanism underneath is `WorldTimeService.SetDebugTime`, and +`SyncFromServer` clears it — deliberately, since that setter is the `/time` +slash command and there is a test pinning exactly that behaviour +(`WorldTimeDebugTests.SyncFromServer_ClearsDebugOverride`). ACE sends TimeSync +every few seconds. **The clock was therefore un-pinned again long before the +route reached its first stop, on every run this campaign has taken, V6m's smoke +pair included.** And the Dereth clock does not only move the sky; it moves the +sun, so it moves the directional term of every lit surface in the scene. + +Measured rather than argued — a probe route captured each stop twice, 45 seconds +apart, in the same run on the same backend: + +| Arm | Stop | capture 1 vs capture 2 | +|---|---|---| +| GL | Holtburg | 205,772 px — **22.33%** | +| Vulkan | Holtburg | 218,732 px — **23.73%** | +| GL | Facility Hub | 108,795 px — **11.81%** | +| Vulkan | Facility Hub | 130,206 px — **14.13%** | + +One backend, one stop, nothing moving, and a fifth of the frame changes while you +watch. `WorldTimeService.PinnedDayFraction` (`ACDREAM_WORLD_TIME` → +`RuntimeOptions.PinnedWorldDayFraction` → `WorldEnvironmentController`) outranks +both the server clock and `SetDebugTime` and survives every sync; the Runtime +environment owner is session-scoped, so one write outlives every teleport and +reveal. Values outside [0, 1) are rejected rather than clamped. The gate forces +0.5 on both launches and the route's presses are deleted. + +**The three-stop route, before and after that one change:** + +| Stop | before | after | +|---|---|---| +| `holtburg_town` | 9.05% (83,438 px) | **2.86%** (26,330 px) | +| `facility_hub_interior` | 12.16% (112,075 px) | **0.78%** (7,176 px) | +| `aerlinthe_island` | 23.09% (212,824 px) | **6.82%** (62,892 px) | + +--- + +**The three leads V6m handed forward, answered.** + +**Lead 3 was wrong, and it is worth saying so plainly.** V6m recorded the +interior stop as a route defect on the theory that the indoor spring-arm camera +settles to different distances in two runs. It does not. The interior was lit +differently because the sun had moved; with the sun held still the stop drops by +a factor of fifteen to **0.78%**, and its entire remaining difference map is the +*player character* — the armour highlights and the collar. The brick walls, the +floor, the doorway, the per-cell ambient, the chat panel, the radar and the +toolbar are black. **`EnvCellRenderer`'s Vulkan arm now has its numeric pair**, +and no route change was needed or made. No aperture stop was added either: the +portal depth mask remains undrawn by anything automated, and it is carried +forward rather than claimed. + +**Lead 2 is closed by pinning rather than masking** — item 2 above. + +**Lead 1 is half fix and half finding.** The "silhouette-edge residual" was +predominantly the anisotropy gap, which is fixed. What survives is a single +population — **dense alpha-blended distant scenery, the treeline** — and it took +a better instrument to isolate. + +**The offline GL-versus-Vulkan pair is that instrument, and V7 recommends it to +V8 and V10.** Both capture scripts already exist; setting `ACDREAM_WORLD_TIME` +and `ACDREAM_SKY_PHASE_SECONDS` in the shell that invokes them makes the pair +strictly comparable, and the offline scene has **no session, no server, no +entities, no camera settle and no wandering NPCs** — so a difference is the +renderer or it is nothing. It also runs unattended. + +| Pair (offline, both clocks pinned, MSAA off) | Differing px | Fraction | +|---|---|---| +| GL vs GL, same commit (control) | 1,966 | 2.13e-03 | +| Vulkan vs Vulkan, same commit (control) | 1,039 | 1.13e-03 | +| **GL vs Vulkan, whole frame** | 28,807 | 3.13e-02 | +| **GL vs Vulkan, everything below the tree band (rows 280–720)** | **497** | **8.82e-04 — inside 0.001** | + +**Terrain, terrain blending, roads, the water edge, fog, statics, scenery below +the horizon and the entire retained UI are at parity.** Both controls sit in the +tree band too, which is the tell. + +**What the treeline is, and what it is not.** Four hypotheses were tested and +three were refuted: + +- **Not a sub-pixel offset.** An integer shift search over the band finds the + best alignment at (0, 0). +- **Not a sharpness or LOD-scale difference.** High-frequency energy (mean + absolute neighbour difference) matches within 5% in the band and within 2% + everywhere else. +- **Not depth precision** — and this one matters, because §4.7 predicted the + z-fight class would present here. Forcing the Vulkan viewport's window-depth + range to `[0.5, 1.0]`, which reproduces GL's compressed mapping *exactly*, moved + the whole-frame number from 28,807 to 27,852: **3%**. The experiment was + reverted. The pre-approved divergence class is not what this is. +- **It IS anisotropic filtering, and there is no knob left.** Anisotropy 1 gives + 41,509 in the band; anisotropy 16 — GL's value, and retail's — gives 22,266. + Monotone improvement toward GL's own setting, with both arms now requesting + identical sampler state, leaves the anisotropic TAP PATTERN, which both the GL + and Vulkan specifications leave implementation-defined and AMD's two drivers do + not agree about. It is the same kind of thing as §6's MSAA sample-position row. + +That is **`AD-46`** in the divergence register, filed as an adaptation with the +measurements above as its justification and its refutations. It is dormant until +V10 makes Vulkan the client. + +**The V7 verdict table** (`artifacts/v7-diff-c2`, full route, both backends, +tolerance 2, fraction 0.001, MSAA off, day group 0, world time 0.5, sky phase 0): + +| Stop | Differing px | Fraction | Max Δ | Verdict | +|---|---|---|---|---| +| `holtburg_town` | 26,330 | 2.86e-02 | 255 | **EXCEPTION** — phase + AD-46 | +| `facility_hub_interior` | 7,176 | 7.79e-03 | 255 | **EXCEPTION** — phase | +| `aerlinthe_island` | 62,892 | 6.82e-02 | 255 | **EXCEPTION** — AD-46 + dark-scene floor | + +**No stop passes, and none of the three exceptions is a renderer defect.** Named +individually, because "phase" is not an excuse unless it is specific: + +- `holtburg_town` — the animated portal beside the stop (a scrolling texture, the + same class as an emitter's age), two wandering NPCs, a chimney smoke plume, the + vitals readouts (stamina and mana genuinely regenerate at different rates across + two logins minutes apart), and the AD-46 treeline on the left. The terrain, + roofs, walls, road, chat panel, toolbar and radar are black in the map. +- `facility_hub_interior` — the local player's idle pose and its lighting, and + nothing else. **This is the cleanest evidence in the slice**: an EnvCell + interior, drawn by the Vulkan arm, whose walls and floor and doorway differ in + no pixel worth naming. +- `aerlinthe_island` — AD-46 shrubbery plus a floor that the instrument imposes + rather than the renderer: the scene's mean luminance is **28/255**, and **half + of its differing pixels are exactly delta 3**, one step over a tolerance that is + absolute rather than relative. A relative tolerance would report this stop very + differently, and changing the pinned tolerance is not V7's call. + +**What V7 did not reach**, carried to V8 and to the user's visual session: + +1. **A pass at any connected stop.** Getting one needs the route to stand + somewhere with no creature, no emitter and no animated portal in frame, and the + local player is unavoidable in a chase camera. The realistic shape is an + authored per-stop mask in the gate script, which does not exist yet: the script + still has only the global `-MaskTopPixels`, deliberately defaulted to 0. +2. **The portal depth mask still has never drawn a pixel in an automated run.** + V6l and V6m both carried this; so does V7. It needs a stop standing in a + building aperture. `HouseExitWalkReplayTests` names a usable target — the + Holtburg corner building, cell `0xA9B40170`, whose exit door the test resolves + from the DAT — and that is the cheapest path when someone picks it up. +3. **Whether AD-46 is visible to a human.** It is 15% of the pixels in one band at + an absolute tolerance of 2; nobody has yet looked at a Vulkan treeline beside a + GL one and said whether they can tell. That is a user-stop question. +4. **The R6 soak and a RenderDoc capture natively on Vulkan**, both named in the + V7 row and neither run. + +**Gates.** Per commit: Release build green; App tests **4,133 / 3** then +**4,134 / 3** against the 4,132/3 baseline (three new — the sky-phase parse, the +day-fraction range check, and the two `WorldTimeService` pin assertions in +`AcDream.Core.Tests`); GL offline pixel gate against the pre-slice tree at +**2.31e-05** (13 px) and **2.66e-05** (15 px), both inside the documented 9–31 +band, so **GL did not move**; one offline Vulkan run per commit with +`VK_LAYER_KHRONOS_validation` **proven inserted by the loader** at **zero errors, +zero warnings**. Final: complete Release suite **9,195 passed / 5 skipped, zero +failures** — no `AcDream.Content` flake this time; GL connected repeat gate +`-Runs 3` at **3/3 RENDERED** on both the desktop witness and the client capture. + ### 5.4 The null-target `BeginPass` divergence (V4c) — ✅ DISCHARGED at V6k > **Closed 2026-07-28 by V6k commit 2 (`eb7e6b4e`); see §5.5.16.** The answer is