From 45f58ac72bf53c87b1f834f34fa733a0cdb74aab Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 28 Jul 2026 19:44:33 +0200 Subject: [PATCH] docs(render): record V7 - the instrument was measuring itself, and AD-46 Plan section 5.5.19, the V7 slice row, two rows in the section 5.1 uncovered table, and one divergence-register row. No product code changes. WHAT V7 TURNED OUT TO BE. Three of V6m's four numbers were taken through an instrument that was not holding the world still. The route pinned the Dereth clock by pressing AcdreamCycleTimeOfDay, whose mechanism is the transient /time override that SyncFromServer clears -- so ACE un-pinned it seconds into every run this campaign has taken. Two captures 45 s apart at ONE stop on ONE backend differ in 22.3% of the frame because the sun keeps moving. Pinning it (commit 2) took the interior stop from 12.16% to 0.78% on its own. THE THREE LEADS, ANSWERED. Lead 3 was WRONG and the section says so. 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 and its entire remaining difference map is the player character -- the EnvCell's walls, floor, doorway and per-cell ambient are black. EnvCellRenderer's Vulkan arm has its numeric pair, V6m's defect-list item 2 is discharged, and no route change was needed or made. Lead 2 is closed by pinning the cloud phase rather than masking the band, so the gate keeps the sky under strict comparison. Lead 1 is half fix, half finding. The residual was predominantly the anisotropy gap (commit 1). What survives is one population -- dense alpha-blended distant scenery -- and isolating it needed a better instrument than the connected route. THE INSTRUMENT V7 RECOMMENDS FORWARD. An offline GL-versus-Vulkan pair, which is just the two existing capture scripts run with ACDREAM_WORLD_TIME and ACDREAM_SKY_PHASE_SECONDS set in the invoking shell. No session, no server, no entities, no camera settle, no wandering NPCs, and unattended: GL vs GL same commit (control) 1,966 px 2.13e-03 VK vs VK same commit (control) 1,039 px 1.13e-03 GL vs VK whole frame 28,807 px 3.13e-02 GL vs VK everything below the tree band (rows 280+) 497 px 8.82e-04 Terrain, blending, roads, the water edge, fog, statics, scenery below the horizon and the entire retained UI are at parity, inside the campaign's 0.001 threshold. AD-46, FILED WITH ITS REFUTATIONS RATHER THAN ITS THEORY. The treeline band is an anisotropic tap-pattern divergence between AMD's GL and Vulkan drivers. Three competing explanations were tested and refuted, and section 4.7's predicted class is one of them: - not a sub-pixel offset -- an integer shift search finds (0, 0); - not sharpness or LOD scale -- high-frequency energy matches within 5%; - NOT DEPTH PRECISION. 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 by 3% (28,807 -> 27,852). The experiment was reverted. The one pre-approved divergence class is not what this is, and the row says so rather than borrowing its approval. - It IS anisotropy, and there is no knob left: 41,509 differing pixels in the band at anisotropy 1, 22,266 at 16, which is GL's value and retail's. THE VERDICT TABLE. Full route, both backends, tolerance 2, MSAA off, day group 0, world time 0.5, sky phase 0 (artifacts/v7-diff-c2): holtburg_town 26,330 px 2.86e-02 EXCEPTION -- phase + AD-46 facility_hub_interior 7,176 px 7.79e-03 EXCEPTION -- phase aerlinthe_island 62,892 px 6.82e-02 EXCEPTION -- AD-46 + dark floor No stop passes and none of the three exceptions is a renderer defect; each is named individually in the section, because "phase" is not an excuse unless it is specific. Aerlinthe's is partly the instrument rather than either renderer: the scene's mean luminance is 28/255 and half its differing pixels are exactly delta 3, one step over a tolerance that is absolute rather than relative. Changing that tolerance is not V7's call. CARRIED FORWARD, recorded in the section 5.1 table and the V7 row: a passing connected stop needs authored per-stop masks that the gate script does not have (it still has only the global -MaskTopPixels, deliberately defaulted to 0); the portal depth mask has now gone three slices without drawing a pixel in an automated run, and HouseExitWalkReplayTests names the cheapest target for it (the Holtburg corner building, cell 0xA9B40170); whether AD-46 is visible to a human is a user-stop question nobody has asked yet; and the R6 soak and RenderDoc capture on Vulkan were not run. Gates for this commit: docs only, so the code gates of commits 1 and 2 stand. Complete Release suite 9,195 passed / 5 skipped with zero failures, and the GL connected repeat gate at 3/3 RENDERED on both the desktop witness and the client capture, both taken at this tree. Co-Authored-By: Claude Fable 5 --- .../retail-divergence-register.md | 3 +- docs/plans/2026-07-27-vulkan-campaign.md | 216 +++++++++++++++++- 2 files changed, 217 insertions(+), 2 deletions(-) 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