From 9ccb61a8ec65cb446566c07ba47c35bf8fe9eed1 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 4 Sep 2026 07:30:25 +0200 Subject: [PATCH] fix(render): S4 chunk 2 round 1 - particle throw, detail-off immediate draws, EnvCell FIFO, gate honesty Fix round 1 of the two the plan allows for S4-c2 (048d5b12f), addressing every item M1-M8/A1-A8 from the campaign branch's packet section 11. M1 (BLOCKING): ParticleRenderer.DeferToRetailAlphaQueue threw whenever a mesh-particle batch's TranslucencyKind.Opaque classification (mask 0x00) reached a non-Append DrawMesh row - a real, reachable outcome since TryAppendMeshDraws applies no IsOpaque-style pre-filter unlike WbDrawDispatcher's path. Deleted both throws (ParticleRenderer.cs, WbDrawDispatcher.cs SubmitToAlphaQueue), extracted the router-input derivation into internal static ParticleRenderer.RouteParticleSubmission (directly testable), and implemented rows 1/5's immediate draw by reusing the existing per-instance billboard/mesh draw code (new DrawImmediateParticleSubmissionRhi, no new renderer, no sort per A8). materialHasAlpha (row 4) is derived from CMaterial::CheckAlphaValues @0x005396a0 (Ghidra-verified 2026-09-04): has_alpha=0 only when Ambient.a==Diffuse.a==Specular.a==Emissive.a==1.0f, else 1 - and CMaterial::SetTranslucencySimple @0x005396f0 (reached from CPhysicsPart::SetTranslucency <- Particle::Update @0x0051c290's per-frame alpha-fade) sets all four channels to (1f - translucency) uniformly, so has_alpha == "this particle's current alpha isn't 1.0" - already carried in ColorArgb's top byte (Lerp(StartAlpha,EndAlpha,tLife), ParticleSystem.cs 1039-1041/1453). M3: SubmitToAlphaQueue now takes isBuildingShell + viewProjection and feeds Row 1's detailSurfaceActive gate for translucent building-shell instances (retail DrawBuilding @0x0059f2a0 installs building detail before its own shell subsets reach DrawMesh). Row 1 (detail on) draws the instance immediately at its own walk-stream AlphaSubmitMark via new DrawImmediateAlphaInstance/DrawImmediateAlphaInstanceRhi (reusing WriteDeferredAlphaEntrySlot + the drain's pipeline/detail code for slot 0); never touches _deferredAlpha/the queue for pure-Immediate submissions (fixes a latent leak: previously reserving a _deferredAlpha slot for every candidate meant a frame with ALL-immediate content never registered a source, so ResetAlphaSubmissions never ran). M3c: DrawPreparedAlphaBatchRhi's own per-command hasDetail/DrawBuildingDetailRangeRhi branch is deleted - detail-eligible content no longer reaches the drain (spec: delayed replay is always detailEnabled=0). New AD-120 register row for the "in place -> at the entity's own stream mark" granularity adaptation this introduces. M6: RetailPViewPassExecutor.SubmitOrDrawTransparentCellShell routes an EnvCell's transparent shell through the SAME RetailAlphaMeshRouter table (mask 0x02, detailSurfaceActive = EnvCellRenderer.TransparentDetailEnabled) instead of a hand-coded branch. Detail on: unchanged immediate draw. Detail off: appends ONE token per CELL via new EnvCellAlphaDrawSource (internal, delegate-based so it's unit-testable without a GPU harness), replayed through EnvCellRenderer.RenderTransparentOrdered at the next flush - RenderTransparentOrdered already replayed a cell's batch on demand, so no new deferred-replay abstraction was needed (the original landing's claim that one was required is corrected in the packet). M4: RetailAlphaEntry.IsFirstForList deleted. Retail's per-DrawMesh- invocation "first for this list" flag is trivially true for every subset retail ever appends (DrawMesh @0x0059d4a0 sets both flags true at entry, 0059d4cc/0059d4d0, clearing each independently after ITS OWN first append within that call, 0059d5ef) - the captures confirm it (new=1 on 9,685/10,556 AM lines, impossible under "first since last drain"). acdream's deleted isFirstForList = target.Count == 0 computed the wrong quantifier. One acdream append already IS one DrawMesh call, so the field carried no information and its own pin was vacuous (feedback_every_new_pin_must_be_ shown_to_fail) - deleted rather than kept as an always-true no-op. M5: split all six AlphaFlushTranscript_* KnownFailure Facts into live AlphaFlushSites_* (site+threshold sequence only, content-independent, unconditional pass) and AlphaFlushCounts_* (KnownFailure, full tuple incl. drained counts) sharing one RunAlphaFlushTranscriptReplay helper. Rewrote the KnownFailure reason honestly: THREE independent causes, not "harness content only" - (1) zero live mesh content, (2) acdream appends one entry per INSTANCE vs retail's one per SUBSET per DrawMesh call, (3) acdream's CLIP list is structurally empty for ordinary content (M2). Added ONE live valve pin (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAtThe ExactBoundary, a Theory over 2250/2249) that goes through WalkFrameDriver. Replay's own SortCellExit arm, not RetailAlphaQueue.Flush called directly. M2/A2/M8c (register): ~~AP-34~~ restored as struck-through RETIRED (it had been deleted outright, not struck, breaking the register's own convention) citing residuals AP-238/AP-239/AP-240. The chunk-2 commit's "AP-236"/ "AP-237" collided with an id ALREADY filed-and-retired on main by an unrelated #132 fix (docs/ISSUES.md:19976) - ids are never reused, so both are renumbered: AP-236->AP-238 (EnvCell; rewritten per M6 - granularity-only residual now, "never through the shared queue" is retired), AP-237->AP-239 (mask-reconstruction gap; the dangling "AP-236b" citation in RetailAlphaMeshRouter.cs never matched either filed id and is corrected to AP-239). New AP-240 filed for M2: WbDrawDispatcher.IsOpaque filters ClipMap out upstream, so ordinary clip-mapped content never reaches retail's CLIP FIFO (draws immediately on the pre-existing OpaqueAlphaToCoverage pipeline instead) - no pipeline change this round, per the contract. AP section header recounted honestly: 159 active (was stale at 161); AD section 92 (was 91, +AD-120). M7: AP-239's only cited example (cloud GfxObj 0x01004C35, surface 0x08000023) is DISPROVEN - decoded Type=0x10114 has alpha-family bits that win in BOTH retail's real priority and acdream's reconstruction, so it never actually diverges. A 2026-09-04 scan of all 6,152 Portal-dat Surfaces found 27 real Translucent+Base1ClipMap-no-alpha-family instances (Type=0x14); cross-referencing GfxObjs found surface 0x08000015 on GfxObj 0x010001EC - cited in place of the disproven cloud example. M8: (a) RenderDeviceD3D::DrawBlock is @0x005a17c0 per symbols.json, not @0x005a18d0 (that is its per-land-cell loop head) - fixed across RetailAlphaQueue.cs, RetailPViewPassExecutor.cs, WalkEvents.cs, WalkFrameDriver.cs (x3), RetailFrameWalk.cs. (b) RetailAlphaQueue.cs's four FlushAlphaList addresses were labelled "return site" but are the CALL instruction's own address (x86 CALL rel32 is 5 bytes) - relabelled "call at X (returns to X+5)". (d) worldbuilder-inventory.md:420's AP-34 citation updated. A1: RetailPViewRenderer.cs's outdoor-root passes.FlushLandscapeAlpha() call (labelled LandscapeFlush) is deleted - retail's real LandscapeFlush (PView::DrawCells @0x005a4840's own FlushAlphaList(0f) @0x005a4872) sits strictly inside if (outside_view.view_count > 0) on the INTERIOR PView; an outdoor root never reaches it (terrace-edge capture: zero 005a4877 FL lines). Verified no opaque draw intervenes between this deletion point and RetailAlphaQueue.EndFrame()'s own RenderNormalMode pass-end flush (WorldSceneRenderer.cs: DrawPostWorldParticles's PView branch is an explicit no-op; only diagnostic wireframes follow) - #132's "drain after the finished opaque world" invariant is kept by that flush alone. A4: RetailAlphaQueue's ApplyScratchRetention call sites passed the ENTRY count as the SOURCE-count argument too (observedClip + observedAlpha twice), so _sources/_sourceDrawOffsets never shrank toward the real source count. Fixed to capture _sources.Count before Clear(). A5/A6/A7: WalkStaticStreamPopulatorTests.cs's stale "same viewer distance" banner corrected; two off-by-one entry counts in the packet's section 10 sequences corrected (foundry-deep x10->x11, holtburg-doorway-still x5->x6); WalkAlphaDepthTrace.cs's "new=" comment explains why IsNew is retained but never compared (M4's finding makes a per-subset comparison meaningless at acdream's per-instance granularity); RetailAlphaMeshRouterTests.cs's IndependentReferenceRoute (never actually independently-shaped) renamed to RestatedBranchTableRoute with an honest doc comment, and the unverified "flipped in turn and failed every time" claim deleted. A3 was resolved as a side effect of M1's SubmitToAlphaQueue doc-comment rewrite (the wrong "0x02/0x03, 0x04/0x05, 0x08/0x09" enumeration no longer exists); RetailAlphaMeshRouter.cs's own MaskFromTranslucencyKind comment updated in the same spirit (M1's particle-site Opaque-reachability finding). Mutation checks (each applied to a scratch-restored copy, confirmed failing, then byte-exact reverted - diffed against a /tmp backup after restore): - A1 (RetailPViewRendererTests.DrawInside_NeverCallsFlushLandscapeAlphaDirectly): restoring the deleted "if (ctx.RootCell.IsOutdoorNode) passes.FlushLandscapeAlpha();" line reintroduces a direct RetailPViewPassExecutor.FlushLandscapeAlpha call in DrawInside's compiled body. Actual failure: "Assert.DoesNotContain() Failure: Filter matched in collection ... CompiledCall { ... Target = Void FlushLandscapeAlpha() }". - A4 (RetailAlphaQueueTests.RetainedSourceCapacity_ConvergesToTheReal SourceCountNotTheEntryCount): reverting both ApplyScratchRetention calls to (observedClip + observedAlpha, observedClip + observedAlpha) - with 100 distinct sources in the spike frame (so _sources.Capacity grows to 128, past its initial 4) followed by three low-demand frames of 10 entries via ONE repeated source - makes sourceTarget compute from 10 (Math.Max(4,10*2)=20) instead of 1 (Math.Max(4,1*2)=4). Actual failure: "Expected the retained source-array capacity to converge toward the real source count (1), but it stayed at 20". (A single-source-only spike does NOT discriminate this bug - Math.Min(sourceTarget, _sources.Capacity) clamps both formulas to the same unchanged initial 4 - hence the 100-source setup.) - M5 valve (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAt TheExactBoundary, 2250 case): flipping RetailAlphaQueue.Flush's no-op comparison from "<" to "<=" (S4-c1/c2's own boundary mutation) makes the 2250-entries case a no-op instead of draining. Actual failure: "Assert.Equal() Failure: Values differ Expected: 0 Actual: 2250". - M6 (EnvCellAlphaDrawSourceTests, both Facts): adding an immediate renderTransparentOrdered(new[]{cellId}) call inside EnvCellAlphaDrawSource.AddPendingCellId (simulating the M1/M3 "drew immediately instead of deferring" bug class) makes PendingCellToken_IsDrainedAtTheFlushAndNeverBeforeIt fail with "Assert.Empty() Failure: Collection was not empty Collection: [[4660]]" and ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombined Drain fail with 'Expected: string[] ["cell:256", "particle:7", "cell:512"] Actual: List ["cell:256", "cell:512", "cell:256", "particle:7", "cell:512"]'. - M1 (ParticleRendererRouteTests.OpaqueClassifiedMeshBatch_WithMaterialAlpha _RoutesToAlphaAppend): hardcoding materialHasAlpha to false inside RouteParticleSubmission makes the Opaque-mask+has-alpha case route Immediate instead of Append. Actual failure: "Assert.Equal() Failure: Values differ Expected: Append Actual: Immediate". Gates (real output lines): - dotnet build AcDream.slnx -c Release -> "Build succeeded. 0 Warning(s) 0 Error(s)". - Hermetic lane (--filter "Lane!=InstalledDat&Lane!=PreparedPackage& Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux& Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure") -> "Passed! - Failed: 0, Passed: 6864, Skipped: 0, Total: 6864". - InstalledDat lane (ACDREAM_RUN_INSTALLED_DAT_TESTS=1, ACDREAM_DAT_DIR="C:/Users/erikn/Documents/Asheron's Call", --filter "Lane=InstalledDat") -> "Failed: 10, Passed: 255, Skipped: 1, Total: 266" - the exact 4 pre-existing failures (TowerAscent_StaircaseStaysConeVisible_EveryStep, LayoutImporterMediaBearingChildSweepTests, LayoutImporterInvisibleSweep Tests, Oh_doorway_still_first_frame_diff) plus the six AlphaFlushCounts_* KnownFailure Facts, nothing else; all six AlphaFlushSites_* Facts pass inside this same lane (the whole WalkTraceConformanceTests partial class carries Lane=InstalledDat, so they cannot run hermetically). - Shader classes (VulkanShaderDescriptorContractTests, VulkanShaderManifestTests, RenderPackSpirvValidatorTests) -> "Passed! - Failed: 0, Passed: 32, Skipped: 0, Total: 32". - RetailAlphaQueueTests, RetailAlphaMeshRouterTests, RetailFrameWalkTests, WalkFrameDriverTests, ParticleRenderer*Tests, EnvCellRenderer*Tests (combined filter) -> "Passed! - Failed: 0, Passed: 120, Skipped: 0, Total: 120". - Register collision grep: AP-238/AP-239/AP-240/AD-120 each count exactly 1; ~~AP-34~~ count exactly 1; live totals AP=159, AD=92. Not done / deferred: none - all M1-M8/A1-A8 items landed; A8 was a no-action note. Co-Authored-By: Claude Fable 5.1 --- .../retail-divergence-register.md | 11 +- docs/architecture/worldbuilder-inventory.md | 6 +- .../Rendering/ParticleRenderer.Rhi.cs | 50 +++++ src/AcDream.App/Rendering/ParticleRenderer.cs | 130 +++++++++--- .../Rendering/RetailAlphaMeshRouter.cs | 47 +++-- src/AcDream.App/Rendering/RetailAlphaQueue.cs | 76 ++++--- .../RetailPViewPassExecutor.WalkLeaf.cs | 8 +- .../Rendering/RetailPViewPassExecutor.cs | 107 +++++++++- .../Rendering/RetailPViewRenderer.cs | 39 +++- .../Rendering/Walk/RetailFrameWalk.cs | 5 +- src/AcDream.App/Rendering/Walk/WalkEvents.cs | 4 +- .../Rendering/Walk/WalkFrameDriver.cs | 14 +- .../Rendering/Wb/EnvCellRenderer.Rhi.cs | 14 ++ .../Rendering/Wb/WbDrawDispatcher.Rhi.cs | 132 ++++++++---- .../Wb/WbDrawDispatcher.WalkClassify.cs | 8 +- .../Rendering/Wb/WbDrawDispatcher.cs | 194 ++++++++++++------ .../Rendering/EnvCellAlphaDrawSourceTests.cs | 92 +++++++++ .../Rendering/ParticleRendererRouteTests.cs | 76 +++++++ .../Rendering/RetailAlphaMeshRouterTests.cs | 38 ++-- .../Rendering/RetailAlphaQueueTests.cs | 104 ++++++---- .../Rendering/RetailPViewPassExecutorTests.cs | 39 ++++ .../Rendering/RetailPViewRendererTests.cs | 56 +++++ .../Rendering/Walk/WalkAlphaDepthTrace.cs | 12 +- .../Rendering/Walk/WalkFrameDriverTests.cs | 69 +++++++ .../Walk/WalkStaticStreamPopulatorTests.cs | 7 +- ...ceConformanceTests.AlphaDepthTranscript.cs | 168 ++++++++++----- 26 files changed, 1180 insertions(+), 326 deletions(-) create mode 100644 tests/AcDream.App.Tests/Rendering/EnvCellAlphaDrawSourceTests.cs create mode 100644 tests/AcDream.App.Tests/Rendering/ParticleRendererRouteTests.cs create mode 100644 tests/AcDream.App.Tests/Rendering/RetailPViewRendererTests.cs diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index f33614dc..d760231b 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -67,7 +67,7 @@ accepted-divergence entries (#96, #49, #50). --- -## 2. Adaptation (AD) — 91 active rows (AD-119 filed 2026-09-03 at Campaign OVERHAUL v2 S4 chunk 1 (S4-c1 C2) — the portal-depth color path substitutes a `ColorWrite=false` write mask for retail's zero-source-alpha `SRCALPHA`/`INVSRCALPHA` blend, a provably pixel-identical no-op either way; AD-117 filed 2026-09-03 at the Campaign OVERHAUL S2 review fix round — three residual Contract A/B approximations the S2 retail-lens review named (visual-AABB circumsphere cheap reject, part rows published into unloaded neighbour cells, the unported `state & 0x1000` particle branch) — its original item 1, the render-only destination-cell move rule, was VERIFIED the same night as retail's own zero-sphere `CObjCell::find_cell_list` 0x0052b4e0 mechanism and is not a deviation; AD-116 filed 2026-09-03 at Campaign OVERHAUL S2 chunk 5 — `WalkProductionWorldData`'s borrowed per-cell view contributes NO cell for an entity the registry has flooded but the presentation scene cannot resolve yet (the deleted parent-cell/root-position fallbacks are gone), counted once per distinct entity per frame in `UnregisteredRenderMembershipCount`; AD-115 filed 2026-08-25 at Campaign AS slice AS2 review fix round (F16) — `BuildCharacterTitleDisplay` clears the Profession element (`0x10000151`) when neither Int 261 CharacterTitleId nor String 5 Template resolves, where retail never clears `0x10000150`/`51`/`52` anywhere and would instead show the PREVIOUS target's stale title; AD-114 filed 2026-08-25 at Campaign AS slice AS2, owner-ruled ("we animate it, and I like it") — the examination window's preview clone tracks the assessed creature's live current animated pose every frame, where retail's clone plays its own private `CreatureMode` cycle decoupled from the live target's actual motion; AD-113 filed 2026-08-25 at Campaign CT slice CT-GF1 — `UiMenu`'s inline-drawn popup opts out of the new client-wide ancestor-clip default (`ExpandsClipForPopup`), standing in for retail's separate top-level popup region; AD-112 filed 2026-08-23 with the sky default-script port — camera-anchored synthetic script owners instead of retail's sky-cell physics objects; AD-110 filed 2026-08-17 at the entry/exit presentation round — the in-world logoff's single confirmed-echo handoff edge versus retail's two independent ExecuteLogOff/CharacterList edges, and the Tunnel-hold tail; AD-74 RETIRED 2026-08-17 at the same round — the Exit to Character Selection "behaves as Exit Game" adaptation is deleted: the confirmed grounded exit now runs the REAL retail flow (0xF653 request, server LogOut motion, 3 s hold, reverse wormhole, return to the live-connection character-select screen via LiveSessionController.CompleteCharacterLogOff), and the previously-missing indicator-bar grounded gate now runs retail's shared three-way branch; AD-109 filed 2026-08-17 at the entry/exit presentation round — the click-armed login tunnel: the wormhole presentation + enter cue now begin at the character-select Enter click instead of retail's black CreatePlayer wait, USER-DIRECTED; AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 RETIRED 2026-08-28 — #386's named-retail message trace confirmed the vendor popup is content-sized and installed-DAT property 0x79 hides its disabled scrollbar; both behaviors are now ported; AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice; AD-111 (renumbered from a parallel-round AD-109 collision) filed 2026-08-17 at the systemic escape-normalization round — the appraisal report's wire-domain literal- +## 2. Adaptation (AD) — 92 active rows (AD-120 filed 2026-09-04 at the S4-c2 fix round 1 (M3) — a translucent building-shell instance under building detail draws immediately at its own walk-stream alpha-submission mark rather than "in place" mid-mesh-call, since acdream's opaque instances are stream-batched and retail's mesh call has no equivalent; AD-119 filed 2026-09-03 at Campaign OVERHAUL v2 S4 chunk 1 (S4-c1 C2) — the portal-depth color path substitutes a `ColorWrite=false` write mask for retail's zero-source-alpha `SRCALPHA`/`INVSRCALPHA` blend, a provably pixel-identical no-op either way; AD-117 filed 2026-09-03 at the Campaign OVERHAUL S2 review fix round — three residual Contract A/B approximations the S2 retail-lens review named (visual-AABB circumsphere cheap reject, part rows published into unloaded neighbour cells, the unported `state & 0x1000` particle branch) — its original item 1, the render-only destination-cell move rule, was VERIFIED the same night as retail's own zero-sphere `CObjCell::find_cell_list` 0x0052b4e0 mechanism and is not a deviation; AD-116 filed 2026-09-03 at Campaign OVERHAUL S2 chunk 5 — `WalkProductionWorldData`'s borrowed per-cell view contributes NO cell for an entity the registry has flooded but the presentation scene cannot resolve yet (the deleted parent-cell/root-position fallbacks are gone), counted once per distinct entity per frame in `UnregisteredRenderMembershipCount`; AD-115 filed 2026-08-25 at Campaign AS slice AS2 review fix round (F16) — `BuildCharacterTitleDisplay` clears the Profession element (`0x10000151`) when neither Int 261 CharacterTitleId nor String 5 Template resolves, where retail never clears `0x10000150`/`51`/`52` anywhere and would instead show the PREVIOUS target's stale title; AD-114 filed 2026-08-25 at Campaign AS slice AS2, owner-ruled ("we animate it, and I like it") — the examination window's preview clone tracks the assessed creature's live current animated pose every frame, where retail's clone plays its own private `CreatureMode` cycle decoupled from the live target's actual motion; AD-113 filed 2026-08-25 at Campaign CT slice CT-GF1 — `UiMenu`'s inline-drawn popup opts out of the new client-wide ancestor-clip default (`ExpandsClipForPopup`), standing in for retail's separate top-level popup region; AD-112 filed 2026-08-23 with the sky default-script port — camera-anchored synthetic script owners instead of retail's sky-cell physics objects; AD-110 filed 2026-08-17 at the entry/exit presentation round — the in-world logoff's single confirmed-echo handoff edge versus retail's two independent ExecuteLogOff/CharacterList edges, and the Tunnel-hold tail; AD-74 RETIRED 2026-08-17 at the same round — the Exit to Character Selection "behaves as Exit Game" adaptation is deleted: the confirmed grounded exit now runs the REAL retail flow (0xF653 request, server LogOut motion, 3 s hold, reverse wormhole, return to the live-connection character-select screen via LiveSessionController.CompleteCharacterLogOff), and the previously-missing indicator-bar grounded gate now runs retail's shared three-way branch; AD-109 filed 2026-08-17 at the entry/exit presentation round — the click-armed login tunnel: the wormhole presentation + enter cue now begin at the character-select Enter click instead of retail's black CreatePlayer wait, USER-DIRECTED; AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 RETIRED 2026-08-28 — #386's named-retail message trace confirmed the vendor popup is content-sized and installed-DAT property 0x79 hides its disabled scrollbar; both behaviors are now ported; AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice; AD-111 (renumbered from a parallel-round AD-109 collision) filed 2026-08-17 at the systemic escape-normalization round — the appraisal report's wire-domain literal- -to-line-break shaping, which retail's `ItemExamineUI::AddItemInfo @0x004AC050` does not do (wire text appends verbatim; the escape decode retail runs at `StringInfo` resolution now lives at our string source, `DatStringResolver` → `RetailStringEscapes`); AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 filed 2026-08-13 at the #385 dropdown fix — the vendor category dropdown keeps G5's fixed 6-row scrollable window although its authored popup ListBox is edge-docked, the condition that arms retail's `RecalculatePopupSize` size-to-content resize; classification UNCLEAR pending a retail side-by-side (ISSUES #386); AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice) Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate @@ -112,6 +112,7 @@ readiness/requeue adaptation. See | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| | AD-119 | **Filed 2026-09-03 at Campaign OVERHAUL v2 S4 chunk 1 (S4-c1 C2; `docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md` §6 R3).** Retail's portal-depth draws (`D3DPolyRender::DrawPortalPolyInternal` @0x0059bc90, the `BLEND_SRCALPHA`/`BLEND_INVSRCALPHA` `SetBlendFunction` call) keep color writes ENABLED with a zero-source-alpha `SRCALPHA`/`INVSRCALPHA` blend — every OTHER piece of R3's state (`DEPTHTEST_ALWAYS`, depth write on, `CULLMODE_NONE`, no stencil) is ported exactly. acdream instead disables the color-write mask outright on the SAME pipeline (`ColorWrite = false` alongside `Blend = GpuBlendMode.None`) and `portal_depth.frag`'s `main()` writes no color output at all — a write-mask substituting for a zero-alpha blend. | `src/AcDream.App/Rendering/PortalDepthMaskRenderer.Rhi.cs:92,100` (`CreatePortalPipeline`'s `Blend`/`ColorWrite` fields); `src/AcDream.App/Rendering/Shaders/portal_depth.frag` (empty `main()`, no color output) | Retail's blend equation is `dst' = src*srcAlpha + dst*(1-srcAlpha)`; with `srcAlpha` fixed at 0 this collapses to `dst' = dst` for every fragment regardless of its RGB — the destination color buffer is left byte-identical either way. A write mask reaches the SAME outcome (the destination is never touched) through a structurally simpler path — no blend-unit work per fragment, no fragment color output to author or keep in sync with a "must stay zero" alpha invariant — so the two are pixel-identical, not merely usually-equivalent. | None expected: the equivalence is provable from the blend algebra above, not measured, so no capture, transcript, or visual gate can distinguish the two. The write mask is in fact the SAFER of the two going forward — a future edit that gives `portal_depth.frag` a real color output (e.g. an authored debug tint) still writes nothing under today's mask, where a ported zero-alpha blend would depend on that same edit remembering to keep alpha at exactly 0. | `D3DPolyRender::DrawPortalPolyInternal` @0x0059bc90 (`SetBlendFunction(BLEND_SRCALPHA, BLEND_INVSRCALPHA, BLENDOP_ADD)`, `SetDepthBufferMode(DEPTHTEST_ALWAYS, ...)`, `SetCullMode(CULLMODE_NONE)`); `PortalDepthMaskRenderer.Rhi.cs` | +| AD-120 | **Filed 2026-09-04 at the S4-c2 fix round 1 (M3).** Retail's `DrawMesh` row 1 immediate draw happens IN PLACE, inside the mesh draw call itself, the instant `curr_detail_surface != null` is observed for a translucent building-shell subset — there is no separate "later" point at which it could draw. acdream's walk stream-batches ORDINARY opaque instances into one deferred submission, so a translucent building-shell instance under building detail cannot draw "in place" in that same sense; `WbDrawDispatcher.SubmitToAlphaQueue`'s `RetailAlphaMeshAction.Immediate` branch instead draws it via `DrawImmediateAlphaInstance`/`DrawImmediateAlphaInstanceRhi` at the entity's own `AlphaSubmitMark` — the point `WalkFrameDriver.Replay` reaches that instance's alpha submission within the walk's existing turn order (after the entity's building/land-cell turn, same relative position an ordinary deferred instance would occupy). | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`SubmitToAlphaQueue`, `DrawImmediateAlphaInstance`); `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs` (`DrawImmediateAlphaInstanceRhi`) | The walk's turn order already places a building's alpha-eligible instances at the correct relative position among that turn's other content (retail draws building shells within `DrawBuilding`, itself ordered exactly where the walk visits that building) — moving the draw from "mid-mesh-call" to "at the turn's alpha submission point" changes WHEN within one turn it executes, never WHICH turn, so it still lands between the same neighboring opaque/translucent content retail's own turn order would bracket it with. | A translucent building-shell instance under detail could theoretically composite differently than retail if some OTHER alpha content is submitted between the entity's own opaque stream mark and its alpha submission mark within the SAME turn — no such intervening content exists in the current walk (an entity's alpha submission immediately follows its own classification), so this is a structural note, not an observed symptom. | `D3DPolyRender::DrawMesh` @0x0059d4a0 (row 1); `RenderDeviceD3D::DrawBuilding` @0x0059f2a0 (`curr_detail_surface` install/clear) | | AD-117 | **Filed 2026-09-03 at the Campaign OVERHAUL S2 review fix round.** Three residual Contract A/B readings (the row's original item 1 — a render-only owner's no-cell-array SetPosition commit republishing at its destination cell alone, `ShadowObjectRegistry.RefreshPositionRows` — was verified statically the same night as retail's own mechanism: `CObjCell::find_cell_list` 0x0052b4e0 with `num_sphere == 0` adds only the current cell (interior `add_cell` at 0x0052b563; outdoor `CLandCell::add_all_outside_cells` 0x00533630 `arg2 <= 0` branch) and skips the transit walk (`arg2 != 0` gate at 0x0052b576); it is a port, not a deviation). (2) `ShadowShapeBuilder.FromStaticRenderParts` uses the visual-AABB circumsphere as the per-portal cheap-reject sphere for a part with no physics BSP, where retail uses `gfxobj->physics_sphere` else `drawing_sphere` (pc:310147-310152) — strictly larger, so it can only WIDEN membership. (3) `PublishRetailPartEntries` publishes part rows into every CELLARRAY id, including an unloaded neighbour cell `CEnvCell::find_transit_cells` added with a null owner, where retail's `add_shadows_to_cells` (pc:282850) zeroes that shadow's cell and skips `AddPartsShadow` until the cell loads. (4) PORTED at Campaign OVERHAUL S2 chunk 6: an emitter now owns exactly one draw membership in its own current cell via `ParticleSystem`'s per-pass cell index (`CopyRenderableEmittersInCell`), matching `add_particle_shadow_to_cell` 0x00514a70's own-cell-only, no-clip-planes rule and drawn at that cell's own walk turn independent of its attached owner's registry membership. The residual is architectural, not behavioral: the membership index lives in `ParticleSystem` rather than as a `ShadowObjectRegistry` row, because an emitter is not a `CPhysicsObj` in acdream and never registers with the shadow registry at all. Two behavioral residuals remain: acdream's emitter cell is its OWNER's pose cell (`ParticleEmitter.OwnerCellId` ← `EntityEffectPoseRegistry.CellId`), where retail's emitter is a standalone `CPhysicsObj` (`makeParticleObject` 0x00512640) with its own position and cell; and retail re-evaluates membership per emission (`ParticleEmitter::EmitParticle` → `CPhysicsObj::AddPartToShadowCells` at 0x0051d126), which acdream does not — a caster in a doorway draws the cloud at the caster's cell turn only. | `src/AcDream.Core/Physics/ShadowObjectRegistry.cs` (`RefreshPositionRows` render-only branch, `PublishRetailPartEntries`), `src/AcDream.Core/Physics/ShadowShapeBuilder.cs` (`FromStaticRenderParts` non-BSP arm), `src/AcDream.Core/Vfx/ParticleSystem.cs` (per-pass cell index, item 4) | (2) A cheap reject that fires less often admits a superset; the admitting tests (`Plane::intersect_box`, `box_intersects_cell`) are ported exactly. (3) The extra rows are unreachable by the walk until the cell is resident and `RefloodLandblock` converges them at hydration. (4) `ParticleSystem`'s cell index is maintained at every point renderable state or `OwnerCellId` changes (`RefreshRenderableIndex`, `UpdateEmitterOwnerCell`), so an emitter's draw membership always matches its live cell regardless of its attached owner's suspended/hidden registry state. | (2) A decorative non-BSP part admitted to a neighbouring cell retail's cheap reject would have dropped — a draw clipped by that cell's portal planes, at worst a sliver. (3) A one-frame draw into a cell that just hydrated before its reflood ran. (4) None behavioral — a code-location note only: if particle emitters are ever modeled as registry-backed physics objects, this cell index should be retired in favor of a genuine `ShadowObjectRegistry` row rather than kept as a parallel mechanism. | `CPhysicsObj::SetPositionInternal` 0x00515330 (pc:283530-283541), `CPhysicsObj::add_shadows_to_cells` 0x00514ae0 (pc:282837-282875), `CEnvCell::find_transit_cells` 0x0052cae0 (pc:310147-310217), `CPhysicsObj::add_particle_shadow_to_cell` 0x00514a70, `CObjCell::find_cell_list` 0x0052b4e0. | | AD-116 | **Filed 2026-09-03 at Campaign OVERHAUL S2 chunk 5 (consumer cutover).** `WalkProductionWorldData.ResolveCellView`'s borrowed per-cell view treats an entity the registry HAS flooded into its retail CELLARRAY (so `ShadowObjectRegistry.GetRetailPartEntriesInCell` names it) but whose `RenderProjectionRecord` `RenderSceneQuery.TryGetByLocalEntityId` cannot resolve yet as contributing to NO cell for that frame — it is silently skipped rather than falling back to its authored parent cell or an outdoor root-position cell (both deleted this chunk). Every distinct entity id this happens for in one frame is counted once in `WalkProductionWorldData.UnregisteredRenderMembershipCount` and, when nonzero, reported by one print-only `[walk-membership]` line at the start of the next `BeginFrame`, gated on `RenderingDiagnostics.ProbeFacilityStairsEnabled`. | `src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs` (`ResolveCellView`, `UnregisteredRenderMembershipCount`, the `BeginFrame` diagnostic line) | Retail has no such gap at all: `CEnvCell::init_static_objects` installs the CELLARRAY before a static is ever drawable, and Contract B's collision (`shadow_object_list`) and render (`shadow_part_list`) products are ONE transaction, so they can never race. acdream's registry (the physics publisher) and its presentation scene (the projection journal) are two independently incremental pipelines fed off the same Create/appearance edge, so a transient one-frame window where the registry runs first is possible during streaming — the same class of race AD-49's residency reasoning already accepts for `CellTransit`'s own outdoor seed. Contributing NOTHING for that one frame matches retail's own rule ("an object not yet in a cell is not drawn") more closely than the deleted parent-cell/root-position fallbacks did, which could draw an object at a cell its real CELLARRAY does not actually include. | If the presentation journal's apply cadence ever falls more than one frame behind the registry's registration (not merely a same-frame ordering race), an entity would stay missing for several consecutive frames instead of appearing on the very next one — `UnregisteredRenderMembershipCount` staying nonzero across consecutive frames (not a single one-frame spike) is the signal that this row's "transient" premise has broken and needs re-investigation, not a widened fallback. | `CEnvCell::init_static_objects`; `CPartArray::AddPartsShadow` 0x00517e40 (`docs/research/2026-09-01-overhaul/oh1-construction-landscape-contract.md` Contract B) | | AD-115 | **Filed 2026-08-25 at Campaign AS slice AS2 review fix round (F16), classification: intentional.** `AppraisalUiController.BuildCharacterTitleDisplay` composes examination element `0x10000151` (Profession/title): when Int 261 `CharacterTitleId` is absent/unresolvable AND String 5 `Template` is also absent, it returns an empty string, and `ClearCreatureText` has already blanked the element for this `ApplyCreature` call, so the element stays cleared. Retail never clears `0x10000150`/`0x10000151`/`0x10000152` anywhere — neither `CharExamineUI::Show @0x004AB5D0` nor `BasicCreatureExamineUI::Init @0x004AB9C0` writes an empty string to those elements — so in this exact case retail would keep showing the PREVIOUS assessed target's title text on screen instead of clearing it. | `src/AcDream.App/UI/Layout/AppraisalUiController.cs` (`BuildCharacterTitleDisplay`, `ClearCreatureText`) | Deliberate improvement over retail's quirk: a stale leftover title from a prior target reads as more confusing/wrong to a player than a blank line for the current one; review F16 (2026-08-25) accepted the clear-on-no-source behavior as intentional. | None expected — this is a deliberate, reviewed divergence, not a game-feel regression; a future retail-faithfulness audit assuming `0x10000151` always mirrors retail's persistent stale-text behavior would be surprised to see it clear instead when the current target's title can't be resolved. | `CharExamineUI::Show @0x004AB5D0`; `BasicCreatureExamineUI::Init @0x004AB9C0` | @@ -219,7 +220,7 @@ readiness/requeue adaptation. See --- -## 3. Documented approximation (AP) — 161 active rows (AP-234 RETIRED 2026-09-02 at OH2/S1 chunk B — retail's exact CellStruct surface/subset construction (`CellStructSideCandidates`, source-surface-index subsets, and the built-EnvCell `(Surface.Type & 6) != 0` admission test) replaces the NoPos-based approximation, ported from `D3DPolyRender::ConstructMesh` @0x0059DFA0, `RenderDeviceD3D::DrawEnvCell` @0x0059F170, and `D3DPolyRender::DrawMesh` @0x0059D4A0 (docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md), with chunk-A/B test evidence; AP-202 RETIRED 2026-08-26 by #446 — retail PFile `.keymap` Load/Save/startup/shutdown persistence is now live; AP-235 filed 2026-08-25 at the Campaign CT4 fix round — `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# switches instead of a live `EnumMapper` read; AP-233 filed 2026-08-23 at the Holtburg windmill fix — the render-side inter-frame animation blend, now holding the boundary frame at every seam; AP-232 filed 2026-08-22 at Campaign VM VM1 — the #226 two-draw detail blend weight on TRANSLUCENT subsets versus retail's single stage-1 output alpha; AP-185 RETIRED 2026-08-20 — `RetailWindowLockPresentationController` now swaps all eight imported locked/live chrome blocks, hides live-only floating-chat and SmartBox grips, suppresses only the nine-slice grip overlay, and applies the current lock before a late-mounted window's first `OnShown`; the radar's persistent B7/B8 semantic face is pinned against pointer-state clobber and covered by a real-fixture draw cycle; AP-231 filed 2026-08-16 at the Campaign CC gate round 1 closeout Group 2 — the Skills page formula-connector-text approximation in `ComposeFormula`, see the row's own text for the full disclosure of what is byte-verified versus best-derived; AP-213 RETIRED 2026-08-16 at the Campaign CC gate round 1 closeout Group 2 — the remaining flat-list-vs-four-bucket-sorted-model half is now ported: `ChargenSkillDetail`/`ChargenSkillFormula` (Core) thread `SkillBase.MinLevel`/`Description`/`Formula` from the global SkillTable through `ChargenOptions.TryGetSkillDetail` (`ChargenTableReader.Project` populates it, live-DAT-pinned at 38 entries — 23 MinLevel<=1/15 MinLevel==2, matching the Batch F investigation's own recorded finding exactly), and `CharacterCreationSkillsPage` now groups every costable skill into `SkillBucket` (Specialized/Trained/UseableUntrained/UnuseableUntrained, `UpdateSkillEntry`'s own `iMinlevel <= 1` test), sorts each bucket alphabetically (`InsertEntrySorted`'s `wcscmp`, ported as `string.CompareOrdinal`), and builds one `Templates[0]` header row per bucket ahead of that bucket's `Templates[1]` skill rows — `DoSkillRecords`'s own unconditional 4-header-then-populate build order. A level change re-buckets the row (detected per-refresh against each row's own cached bucket, then a full rebuild — the observable placement matches retail's incremental single-row `InsertEntrySorted` move without reproducing its internal mechanism, a documented and harmless substitution). 3 new fixture tests (`SkillsPage_BucketHeaders_AlwaysBuildAllFour_InRetailOrder`, `SkillsPage_UntrainedSkill_BucketsByMinLevel`, `SkillsPage_AdvancingASkill_MovesItsRowIntoTheNewBucket`) plus 1 new live-DAT test (`InstalledSkillTable_GlobalSkillDetails_MinLevelDistributionMatchesCostCoverage`); AP-216/AP-217 RETIRED 2026-08-16 at the Campaign CC gate round 1 closeout Group 1 — both rows' STOPPED items are now landed: `CharacterCreationUiController.AppearancePalSetSource`/`AppearanceClothingTableSource`/`AppearancePaletteColorSource` wire a DAT-backed `ChargenAppearanceCatalog` into the Appearance page from `LivePresentationComposition` (mirroring the existing `AppearancePreviewControl` seam), and `UiButton`/`UiDatElement` both gained a per-instance `Tint` property threaded into every existing `DrawSprite` call they make; `CharacterCreationAppearancePage` now sets `Tint` directly on each swatch button and the GradCircle element instead of layering a flat-fill `ChargenSwatchColorTile` overlay on top (that class is deleted) — a genuine multiplicative sprite tint on the widget's OWN authored art, matching retail's `SurfaceWindow::BlitAndColor(..., Blit_Multiply, color)` exactly rather than approximating it with an opaque rectangle. Both fixture test suites (`CharacterCreationAppearancePageSwatchColorTests`, 8 tests) and the live-DAT color pins (`ChargenAppearanceCatalogColorTests`) pass unchanged against the new mechanism; AP-218 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-6) — `gmCGAppearancePage::Update`'s heritage-flavored static Hair/Eyes/Skin spin caption (`ID_CharGen_HairStyle`/`_Eyes`/`_Skin`, Gearknight `GearText_*`, Olthoi/OlthoiAcid `OlthoiText_*`) is now ported verbatim by `RefreshSpinCaptions`, replacing the prior ordinal substitution outright — see AP-215's own rewritten row for what remains open (the icon-thumbnail gap, restated); recount at this same edit: the row count this header carried before Batch B was already one LOW relative to the physical table (Batch A's own ending state: header said 164, the physical table already held 165 rows — verified by direct count against that commit) — a pre-existing drift this edit corrects to the counted total, not an artifact of Batch B's own net change (F12 correction, gate round 1 closeout, 2026-08-16: this note originally said "one high", the inverted direction — the header was UNDER-counting, not over-counting); AP-222 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch B fix (GF-11b) — the Appearance spins' current-part highlight and the Town buttons' Normal-to-white caption swap both port retail's actual mechanism (per-state label color/outline commit off the REQUESTED retail state id, independent of art-media availability — `UiButton.SetPerStateLabelStyle`/`ComputeRequestedStateId`), closing the row's own "not yet resolved which side is wrong" question: NEITHER client's spin ART changes (no Highlight media exists on either), but BOTH clients' spin TEXT does, matching retail's `SetState(1)`/`SetState(6)` property commit exactly (live-DAT-measured 218,167,85 -> 255,221,131, outline off -> on); AP-215 NARROWED the same batch (GF-9) — item 1 (the swatch-selection substitution) is RETIRED now that the real companion-overlay mechanism (`SetColor`'s `m_tColorWheel[...][0x10][iCurColor*7]->SetVisible`) is ported (`CharacterCreationAppearancePage`'s nine `SwatchOverlayIds`), leaving only item 2 (the icon-less style-spin ordinal label) open; AP-230 filed 2026-08-16 at the Campaign CC gate round 1 Batch A fix (GF-13) — the chargen-scoped-vs-general-importer-wide honor split for dat property 0x3B (Invisible: `UIElement::OnSetAttribute` case 8 hides an element), with the general client-wide honor deferred as its own visual gate (docs/ISSUES.md #408, 1,083 elements affected); AP-213 NARROWED the same gate round (GF-5) — the Skills page's click-to-advance/double-click-retreat single-button substitution is RETIRED now that the real per-row `pSkillUpButton`/`pSkillDownButton` arrows are wired to retail's own plain-click dispatch, leaving open only the flat-list-vs-four-bucket-sorted-model half; AP-229 filed 2026-08-16 at the Campaign CC CC7 review-fix round, F1 — the screen-layering divergence: retail's `UIFlow::UseNewMode` destroys/reconstructs the current UI framework on every mode switch where acdream's CC7 keeps both `CharacterManagementUiController` and `CharacterCreationUiController` mounted for the whole lifetime and only reveals/occludes them; AP-228 filed 2026-08-16 at the CC5 re-review residual round (R4) — the Summary listbox's skill-row KEY source, same divergence class as AP-226 filed the same round, a few retail lines away; AP-227 filed 2026-08-16 at the same review-fix round, F9 — an empty Summary name-field commit calls `SetName("")` (clearing the state), where retail's own NUL-inclusive length gate leaves `CharGenState.name` UNCHANGED for that specific case; AP-226 filed 2026-08-16 at the Campaign CC CC5 review-fix round, F11 — the Summary page's DAT-sourced labels versus retail's static `pcProfessions`/`pcGender`/`pcHeritage`/`pcTown` tables, including the non-human-heritage-renders-bare-"Heritage: " retail quirk; AP-225 RETIRED the same round, F6 — the reviewer re-derived `gmCGSummaryPage::ListenToElementMessage @0x0047bf40`'s length check and proved the 32-vs-33 threshold this row flagged as "not fully certain" does NOT exist: the compared length is NUL-inclusive (an empty field's length is 1, matching AP-226's own F11/F9 finding), so `length > 0x21` is EXACTLY `visibleChars > 32` — acdream's `MaxNameLength = 32` was always byte-correct, not merely internally-consistent; AP-223/AP-224 filed 2026-08-15 at Campaign CC slice CC5 — the acdream-only `HeritageOrGenderUnset` Finish refusal and the Summary listbox's two-bucket (Specialized/Trained only) skill-list narrowing (AP-224 corrected 2026-08-16 at the same review-fix round, F3 — its "template mechanism ported exactly" claim was FALSE as shipped, now fixed and true again, see its own row); AP-214 RETIRED the same slice — `RandomizeCharacter` is now ported and wired at the screen-open edge, closing the honest-blank-open gap it recorded; AP-212 NARROWED the same slice — the Appearance/Summary Random-button primitives are now real faithful ports, not uniform-pick approximations, leaving only Heritage/Profession/Town (still uniform-pick) and Skills (still unported) open; AP-222 filed 2026-08-15 at the re-review of Campaign CC CC6b-MOUNT fix commit `d2a71152` (N2) — the current-part spin highlight is a measured no-op for all nine spins, no Highlight media authored on any of them; AP-221 filed the same re-review (R2) — the chargen preview's one-shot-composition-vs-retryable-coordinator binding gap; AP-217 rewritten and AP-220 tightened the same re-review (R3 corrects the GradCircle from a dead click target to unported paint-art; N1 narrows the Gearknight-exit wording to non-Olthoi); AP-216..AP-220 filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round, F2 — DoColorSpots swatch-art, the inert GradCircle, spin-caption/heritage-swap loss, the Skin-spin MoveTo reposition, and the Gearknight-boundary randomize calls; AP-215 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Appearance page's swatch-highlight (`UiButton.Selected` vs retail's separate overlay toggle) and icon-less style-spin ordinal-label substitutions; AP-214 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — retail's `gmCharGenMainUI` ctor rolls a full `RandomizeCharacter` BEFORE any page constructs, so retail's chargen screen is never actually blank on open (and the Appearance page's own gender-flip-on-init always fires against a real gender); acdream opens honestly blank instead, closing out the campaign plan's risk item 5; AP-212/AP-213 filed 2026-08-15 at Campaign CC slice CC4 — the Random button's uniform-pick approximation of retail's three unported randomize algorithms, and the Skills page's flat-listbox simplification of retail's four-bucket sorted skill model; AP-211 filed 2026-08-15 at the Campaign CC slice CC3 review-fix round — the client-side roster-vs-slotCount refusal in `RuntimeCharacterCreationState.TryBeginFinish` has no retail counterpart at that layer, retail enforces the cap in char-select UI instead; AP-207..AP-210 filed 2026-08-15 at Campaign CC slice CC3 — the FitTemplateToCharacter FPU-unrecoverable auto-detect skip, the shared-ClothingColors-list color-count approximation, the classID DAT-DID-lookup placeholder, and the ApplyTemplate atomic-replace-vs-per-attribute-guard simplification; AP-205 filed 2026-08-11 at Campaign OP gate 4 (#381) — the Apply/Reset/Defaults footer's opaque backing field is a genuine acdream synthesis with no authored retail counterpart; ~~AP-201~~ RETIRED 2026-08-11 at the Campaign OP gate-3 fix round — `UiScrollablePanel` now keeps a straddling row visible and CLIPS it to the viewport (`ClipsChildren` → `UiRenderContext.PushClip`, which existed by then), replacing the whole-row cull this row recorded; the user-observed symptom (the Chat tab's per-window filter blocks vanishing into a void at the DEFAULT scroll offset) closed issue #371; AP-203 RETIRED 2026-08-26 by #446 — all 306 installed-DAT rows now have distinct identities and concrete consumers; AP-204 corrected and RETIRED the same day — exact capture/conflict/button semantics now follow named retail; AP-202 RETIRED 2026-08-26 by #446 — retail `.keymap` file interchange and profile lifetime now ship; AP-200 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Chat Font Face/Size menu rows are store-only, distinct fields from the existing live `ChatSettings.FontSize`; AP-199 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Sound Features menu, Interface Sound trio, and Play Sound Only When Active are store-only (the Interface trio cites AP-174's existing "retail's own dead knob" finding); AP-198 filed 2026-08-11 at Campaign OP slice OP6, row count reconciled at the OP6 rework round (2026-08-11, review N1) — the Config tab's TEN Graphics/Rendering-Quality-family rows (including Screen Brightness, its own field as of the S2 fix) are store-only, the Vulkan+one-aggregate-QualityPreset renderer having no per-feature knobs; AP-197 filed 2026-08-11 at the OP4 review-fix round (SF-1/S4) — Display Timestamps hardcodes retail's constructor-default format string instead of the per-character GenericQualitiesData key-1 override the parser reads and discards; ~~AP-196~~ RETIRED 2026-08-11 at Campaign OP slice OP9 — originally filed at the OP4 review-fix round (MUST-FIX 3 / blast M2) for the Group-C re-point's observable-default changes (ViewCombatTarget true→false) and the PARTIAL GameplaySettings retirement (AutoTarget/AutoRepeatAttack/ViewCombatTarget deleted, the other five kept as write-behind mirrors); OP9 deleted `GameplaySettings` outright (all 13 remaining members were already re-pointed to the server-bit seam at OP4), closing the write-behind-mirror gap for good — see its retirement note below; AP-195 RETIRED 2026-08-11 at Campaign OP slice OP5 — ported both halves left open at OP2 re-review closure: the ALL-set LED media swap (`UiButton.FaceFileOverride`, driven by the block-level `P0x10000082`/`P0x10000083` sprites now threaded through `ElementInfo`/`DatWidgetFactory`) and the `CreateChildren` self-sizing tail (`UiCheckboxBitfield64.Height` grows with `_contentHeight` per row; the ENCLOSING page ListBox reflows around the block's FINAL size via the new `UiTemplateListBox.AddPrebuiltRow`, reusing the ListBox's own stacking exactly as the row's own disposition menu allowed, rather than a third stacking path); AP-194 filed 2026-08-10 at Campaign OP slice OP1 — the GetDefaultOptionValue vs constructor-default disagreement for ConfirmVolatileRareUse/ShowHelm/ShowCloak (see the row below); AP-193 filed 2026-08-10 at Campaign OP slice OP1 — the 0x34 HearPKDeathMessages id/mask mapping is ACE-sourced (see the row below); AP-192 filed 2026-08-10 at the Campaign CH round-5 polish (S2) — authored outline `0x21`/`0x22` now reaches every text-bearing widget, but only at the element's effective-default state; per-STATE outline switching (dialog/character/combat buttons author `0x21` in state `0x3` only) is not ported; AP-191 filed 2026-08-10 at Campaign CH round 4 items 1+2 — the chat transcript's missing tag-colour (`0x1D`, green) and tag-font (`0x1C`) are deferred, needing a per-run tag concept `UiText.Line` does not have yet; AP-184 RETIRED 2026-08-10 at Campaign CH round 4 — the three PARTIAL `/help` group topics (channels/chatting/commands) are now COMPLETE verbatim listings, `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290` fully decoded (the "vftable slot" operands are the same pooled/mislabeled-data artifact as AP-186's own precedent, not real vtable dispatch — reading the function's own disassembly for the `push imm32` preceding each constructor call resolves them), closing ISSUES.md #364 (full retirement note later in this same list, at its own "AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09..." entry); AP-113 RETIRED 2026-08-10 at the consolidated-review round, SHOULD-FIX 3/1 byproduct — DoLifestone's own bad-args refusal text is now byte-recovered, see its retirement note below; AP-183 and AP-186 RETIRED 2026-08-10 by issue #363's interface-text seam — see their retirement notes below; AP-190 filed 2026-08-10 at Campaign CH slice CH6c — window opacity now fades every RetailWindowManager window on retail's focus-driven Default/Active mechanism, not just ChatInterface-derived ones, and ships gmMainChatUI's 1.0/1.0 default as the ONE shared default across every registered window (fixed from the original 0.5/1.0 base-ChatInterface value, per the row's own REWORDED (2)) instead of applying it only to ChatInterface-derived windows, retiring AP-40 (the prior "opacity is fixed at 0.75, no focus transition" row) in the same commit; AP-189 filed 2026-08-10 at the CH6a/b REJECT-review rework, SHOULD-FIX 5 — acdream's ONE shared 500-entry/200-line-display-tail chat log gives every window a shallower EFFECTIVE per-window scrollback depth than retail's own per-window 10,000-line log, though the accumulate-while-closed and independent-per-window-scroll BEHAVIORS are both correctly reproduced; AP-188 filed 2026-08-10 at Campaign CH slice CH6b — a floating chat window's chat entry always sends on the Say channel because the floaty LayoutDesc authors no talk-focus menu and acdream does not (yet) share the main window's currently-selected channel across all five chat-window instances; AP-187 filed 2026-08-10 at Campaign CH slice CH6b — the four floating chat windows' text-type filters persist in local `settings.json` only (`ChatSettings.ChatWindow1..4Filter`), with no analog to retail's server-side `0x1000008C` GameplayOptions blob, so a character's floaty filter customization does not travel between acdream installs or round-trip to/from a retail client sharing the same character; AP-186 RETIRED 2026-08-10, issue #363's interface-text seam — `ChatVM` now carries an `OnInterfaceText` hook (`Action?`) the App-layer composition wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)`, exactly fix shape (a) this row's own filing proposed; `ChatCommandRouter`'s two local-presentation fallbacks (`RetailCommandHelpTable.UnknownCommand` and the degenerate-prefix "Unknown command: {verb}." refusal) now call `ShowInterfaceText` and reach the SpewBox, with a null-fallback into the chat log (still tagged `ClientLocal`) for hosts that never wire the hook (headless has no `ChatVM` at all). Closes ISSUES.md #367; AP-185 filed 2026-08-10 at Campaign CH slice CH6a — the chat window's UiLocked border-art cosmetic swap is unported, see the row for detail; AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09 at Campaign CH user-gate round 2, item 3, recording that three of the seven retail `/help` group-topic listings (channels/chatting/commands) remained PARTIAL because their detail text is built in full or in part by `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which the filing believed "not decodable with confidence from a static string sweep" because Binary Ninja renders its three internal string operands as dereferences of unrelated vtable slots (`&ClientCommunicationSystem::\`vftable'.RecvNotice_StartBarberNotice` etc.). That belief was WRONG — the same pooled/mislabeled-data artifact this register already documented elsewhere (AP-113's retirement note) applies here too: reading the function's own disassembly for the `push imm32` immediately preceding each `PStringBase::PStringBase` constructor call (rather than trusting BN's line-grouped rendering, which hides the true instruction order) resolves all three operands directly — `"@"` + a one-character tag sliced from a shared wide literal `U"fvpca"`/`U"mh,."` (a wide string read through a narrow `char*` truncates at the first zero high byte, the "hack" retail's own function name calls out) + `" - Sends a broadcast to your "` + `ChannelSystem::GetChannelName`'s own literal switch-table result + `".\n"`. `ChannelsGroupDetail` (entirely 6 such calls), `ChattingGroupDetail` (6 more, plus a `HelpReply@0x00577A50` Summary-branch quirk that unconditionally emits reply+pr+mr together — read directly, not assumed), and `CommandsGroupDetail` (`HelpAllGroup`, a straight-line concatenation of every other group's Detail branch plus a handful of its own short one-liners, including a CONFIRMED retail saveui/loadui duplicate) are now COMPLETE verbatim listings, matching the four (death/status/text/allegiances) the original filing already had. See `RetailCommandHelpTable`'s class remarks and `RetailCommandHelpTableTests` for the full per-line address citations. Round 2 item 2 also deletes `PortalWaitNoticeController` (the dedicated centered-overlay presentation the user reported was the wrong retail surface) and reroutes the portal-space wait-cue notice through the same `AddText`/SpewBox chokepoint every other on-screen interface-text site uses — AP-178's open SpewBox position/extent/font/colour questions now cover this notice too, since its separate controller and consts are gone; no new row was needed for the surface mismatch itself, since it was never separately registered (`PortalWaitNoticeController`'s own doc comment asserted "not a chat message" as an accepted design, not a flagged divergence). AP-150 RETIRED 2026-08-09 at Campaign CH user-gate round 1, item D (#329) — `PortalTunnelPresentation.TickRotation` now emits `"In Portal Space - Please Wait..."` unconditionally on every rotation-segment expiry, exactly matching `gmSmartBoxUI::UseTime`'s `else`-arm at 0x004D6FCD, instead of gating on `_waitCueVisible`, which only ever went true after the invented 5-second `RuntimeWorldTransitState.RetailWaitCueDelay` hold; `RetailWaitCueDelay`/`ObserveWait`/`SetWaitCue` remain as `LocalPlayerTeleportController`'s own hold-delay telemetry (`RuntimePortalSnapshot.WaitCueShown`) but no longer gate the on-screen cue, so they are not a residual of this row — closes issue #329; AP-183 RETIRED 2026-08-10, issue #363 — every named site now routes through the `ChatVM.ShowInterfaceText`/`OnInterfaceText` seam (see AP-186's retirement note) at its correct retail type: `DoStupidChannelHack` ("You must specify the text you wish to say!", newly wired — the six legacy channel verbs previously fell through `ChatInputParser.Parse`'s pure `return null` with no message at all), `DoChannelList`/`On`/`Off` ("Please specify the channel name.", reclassified), `DoAllegiance` ("Please see @help Allegiance...", reclassified), `DoHouseAvailableList` (reclassified AND corrected to retail's own "Please see @help hslist for more information on how to use this command" string, replacing the acdream-synthesized "Usage: /hslist " fallback — verified `acclient_2013_pseudo_c.txt:381481`/`1029383`), and `DoReply` ("Someone must @tell you first!", newly wired for the message-but-no-last-teller branch only — bare `/r` with no message at all is a separate retail branch, deliberately still unported). `DoSpeaker`/`DoEndurance`/`DoTitle` are untouched, confirmed still correct at `0x00`. The generic bad-args fallback (`ChatCommandRouter.Submit`'s catalog dispatch) now resolves `WeenieErrorMessages.Resolve(0x026u, null)` ("That is not a valid command.", the exact port of `DoCommand @0x0057E46D`'s `HandleFailureEvent(0x26)`) instead of synthesizing a `"Usage: {Usage}"` line — cross-checked against five decompiled handlers (`DoDie` plus the four above), all `0x1A`, confirming the uniform routing decision; AP-182 filed 2026-08-09 at Campaign CH slice CH4, corrected at the CH4 REJECT-review (nit 11) — `@title` is wired to a pure no-op (the value is neither stored nor consumed anywhere) and also omits `DoTitle`'s three local failure messages; recount at the CH3 Opus review corrected a pre-existing off-by-one; AP-181 filed 2026-08-09, Campaign CH slice CH3 — the local chat spam throttle (`IsMessageSpam`) has no acdream port. AP-178 NARROWED 2026-08-09 at the CH2 REJECT-review rework NIT 3, wording corrected at the CH2 re-review nits pass (`docs/plans/2026-08-09-chat-parity-campaign.md`, nits 1/2/6) — the original `dats.Portal` pass used an id source that was not Portal's own (`dats.Portal.GetAllIdsOfType()` is empty for this type), so it established nothing about Portal either way; extending a correctly-paired sweep to `dats.Local` FOUND the SpewBox element there; extent (`450×72`) and `MaxConcurrentItems` (`4`, not the code-default `1`) are now AUTHORED, leaving absolute screen position, colour, AND vertical content flow (now TOP-aligned, acdream's own invention pending measurement) open. AP-180 filed 2026-08-09 at the CH2 REJECT-review rework — `RuntimeCommunicationState.AddText`'s `windowId` parameter is accepted but not consumed, so retail's dual-destination echo (a `0x1A` message with a non-zero `windowId` lands in both the SpewBox and its originating chat window) is unimplemented; latent today since every production caller passes `windowId = 0`. AP-177/AP-178/AP-179 filed 2026-08-09, Campaign CH slice CH2 (interface text / SpewBox) — AP-177 records the invented 5-second SpewBox line lifetime (retail's real timeout is keystone-owned and unmeasured); AP-178's original filing recorded the invented SpewBox screen position/extent/font/colour/MaxConcurrentItems after `SpewBoxLayoutDumpDiagnostic`'s Portal-only sweep found zero elements of class 0x10000016 — see the NARROWED note above for the corrected finding; AP-179 is the OnCombatLine half of the RETIRED AP-176 split out to its own row. AP-176 RETIRED the same day — the WeenieErrorMessages full 344-row `HandleFailureEvent` port (`WeenieErrorMessages.Resolve`) replaces the single-stand-in-`LogTextType` approximation that row recorded for `ChatLog.OnWeenieError`. AP-175 filed 2026-08-09, Campaign CH slice CH1 — PopUpString renders as a chat-log line instead of retail's modal dialog; AP-39 updated the same day — chat coloring is now retail's exact 34-value `LogTextType` table, not a synthetic per-`ChatKind` approximation of it. AP-173 and AP-174 filed 2026-08-08, Campaign A slice A2 — AP-173 expresses retail's ±15 dB DirectSound pan as an OpenAL azimuth by inverting the constant-power pan law, since AL exposes no per-channel gain for a mono source; AP-174 records acdream's extra master volume knob on top of retail's three, folded into retail's single master multiply so the −50 dB cutoff and dB quantisation move with it. AP-172 and AP-171 filed 2026-08-08, #354 spell-bar drag-reorder fix — the favorite-bar reorder gesture defers its own list rebuild for the drag's duration so `UiRoot`'s drag-cancel safety net cannot destroy the in-flight cell, compensating the drop-time target index for the resulting stale sibling numbering; final positions and the wire pair are retail-exact, only the mid-drag visual reflow timing differs. AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own `PublicWeenieDesc._stackSize` is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of `92ea3977` (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container` [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency `m_last_sale` simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (`VendorUiController.cs`, `VendorState.cs`, `GameEventWiring.cs`, `RetailUiRuntime.cs`); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global `ACCWeenieObject::selectedID`, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's `m_last_sale`-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a `GetObjectSplitSize`-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the `maxReach` distance pre-filter is DELETED rather than re-centred, because retail has none: `CObjCell::find_obj_collisions` @0x0052b750 walks the cell's shadow list and calls `CPhysicsObj::FindObjCollisions` unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the `find_bbox_cell_list` port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's `maxReach` distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, **with its direction corrected**. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because `BuildFloodSpheres` carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — `ShadowShape.BoundsCenter`, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one `CPartArray::GetSortingSphere` where acdream floods from every Sphere shape, and acdream's cylinder flood ignores `CylHeight`. AP-152 RETIRED 2026-08-06, one day after it was filed: `ShadowShapeBuilder.FromSetup` now dispatches BSP-first instead of unioning, and `ShadowObjectRegistry.BuildFloodSpheres` now applies `calc_cross_cells`' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: `Transition.BspOnlyDispatch` had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while `BuildFloodSpheres` approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's `CPhysicsObj::FindObjCollisions` dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (`CPhysicsObj::FindObjCollisions` 0x0050f050 exits at `0x0050f22f je 0x50f31b` returning the seeded OK_TS, and `CPartArray::GetRadius`/`GetHeight` are absent from its whole call set), so the invented `setup.Radius` cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit `CurrentCellId` measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's `installPlacementFrame: true, clearParent: true` literals no longer exist; `InboundPhysicsStateController.TryApplyPosition` now computes both flags PRE-MERGE from `(disposition, hasAnimations(old))`, which is exactly `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`'s own `ApplyPlacementFrameBeforeRouting`/`UnparentBeforeRouting` rows (false/false on the Gate A force row, `!HasAnimations`/true on every accepted non-force route). Retail decides both writes BEFORE `MoveOrTeleport` is consulted — Gate A @0x0045400C returns @0x0045409D ahead of `unset_parent` @0x00454129 and the `HasAnims` `SetPlacementFrame` gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: `InboundPhysicsStateControllerTests` — `ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame`, `ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame`, `ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment`, and the 12-row `MergedPrePlacementFieldsMatchTheClassifiedRouteFlags` matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — `TryPublishPlace` now publishes the local player's Place through `LocalPlayerShadowSynchronizer.SyncPose`, the same publisher ordinary per-tick movement uses, instead of a direct `LocalPlayerShadowState.Set` that never touched `PhysicsEngine.ShadowObjects`; AP-1 RETIRED 2026-08-05, C5a deletion sweep — `PhysicsEngine.Resolve`/`ResolvePlacement`/`HasCellSurface` deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses `UsePositionFromServer` (`autonomy_level != 2`) where retail's actual gate, `SendMovementEvent`, is `autonomy_level != 0`; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; `ConstraintManager::adjust_offset` only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client `Airborne` flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at `PhysicsBody.InContact`, retail's literal `transient_state & 1` test at `InterpolationManager::adjust_offset` @0x00555D52 (bit 0 = `CONTACT_TS`, acclient.h:3690), while leaving `Airborne` and all five of its `!Body.OnWalkable` writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, `teleport_hook`'s call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the `WithdrawalRestored` receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) +## 3. Documented approximation (AP) — 159 active rows (recounted honestly 2026-09-04 at the S4-c2 fix round 1 — the prior "161" was stale; AP-240 filed 2026-09-04 at the S4-c2 fix round 1 (M2) — clip-mapped GfxObj/scenery/building subsets draw immediately outside retail's CLIP FIFO, `WbDrawDispatcher.IsOpaque` filtering them out before they ever reach the alpha queue; AP-34 RETIRED 2026-09-04 at the S4-c2 fix round 1 — its residuals continue as the renumbered AP-238/AP-239 (both were filed as the colliding/dangling "AP-236"/"AP-237"/"AP-236b" ids the S4-c2 chunk 2 commit introduced; ids are never reused, so the fix round renumbered them and restored AP-34's own row as struck-through rather than deleted outright — see A2/M8c); AP-234 RETIRED 2026-09-02 at OH2/S1 chunk B — retail's exact CellStruct surface/subset construction (`CellStructSideCandidates`, source-surface-index subsets, and the built-EnvCell `(Surface.Type & 6) != 0` admission test) replaces the NoPos-based approximation, ported from `D3DPolyRender::ConstructMesh` @0x0059DFA0, `RenderDeviceD3D::DrawEnvCell` @0x0059F170, and `D3DPolyRender::DrawMesh` @0x0059D4A0 (docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md), with chunk-A/B test evidence; AP-202 RETIRED 2026-08-26 by #446 — retail PFile `.keymap` Load/Save/startup/shutdown persistence is now live; AP-235 filed 2026-08-25 at the Campaign CT4 fix round — `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# switches instead of a live `EnumMapper` read; AP-233 filed 2026-08-23 at the Holtburg windmill fix — the render-side inter-frame animation blend, now holding the boundary frame at every seam; AP-232 filed 2026-08-22 at Campaign VM VM1 — the #226 two-draw detail blend weight on TRANSLUCENT subsets versus retail's single stage-1 output alpha; AP-185 RETIRED 2026-08-20 — `RetailWindowLockPresentationController` now swaps all eight imported locked/live chrome blocks, hides live-only floating-chat and SmartBox grips, suppresses only the nine-slice grip overlay, and applies the current lock before a late-mounted window's first `OnShown`; the radar's persistent B7/B8 semantic face is pinned against pointer-state clobber and covered by a real-fixture draw cycle; AP-231 filed 2026-08-16 at the Campaign CC gate round 1 closeout Group 2 — the Skills page formula-connector-text approximation in `ComposeFormula`, see the row's own text for the full disclosure of what is byte-verified versus best-derived; AP-213 RETIRED 2026-08-16 at the Campaign CC gate round 1 closeout Group 2 — the remaining flat-list-vs-four-bucket-sorted-model half is now ported: `ChargenSkillDetail`/`ChargenSkillFormula` (Core) thread `SkillBase.MinLevel`/`Description`/`Formula` from the global SkillTable through `ChargenOptions.TryGetSkillDetail` (`ChargenTableReader.Project` populates it, live-DAT-pinned at 38 entries — 23 MinLevel<=1/15 MinLevel==2, matching the Batch F investigation's own recorded finding exactly), and `CharacterCreationSkillsPage` now groups every costable skill into `SkillBucket` (Specialized/Trained/UseableUntrained/UnuseableUntrained, `UpdateSkillEntry`'s own `iMinlevel <= 1` test), sorts each bucket alphabetically (`InsertEntrySorted`'s `wcscmp`, ported as `string.CompareOrdinal`), and builds one `Templates[0]` header row per bucket ahead of that bucket's `Templates[1]` skill rows — `DoSkillRecords`'s own unconditional 4-header-then-populate build order. A level change re-buckets the row (detected per-refresh against each row's own cached bucket, then a full rebuild — the observable placement matches retail's incremental single-row `InsertEntrySorted` move without reproducing its internal mechanism, a documented and harmless substitution). 3 new fixture tests (`SkillsPage_BucketHeaders_AlwaysBuildAllFour_InRetailOrder`, `SkillsPage_UntrainedSkill_BucketsByMinLevel`, `SkillsPage_AdvancingASkill_MovesItsRowIntoTheNewBucket`) plus 1 new live-DAT test (`InstalledSkillTable_GlobalSkillDetails_MinLevelDistributionMatchesCostCoverage`); AP-216/AP-217 RETIRED 2026-08-16 at the Campaign CC gate round 1 closeout Group 1 — both rows' STOPPED items are now landed: `CharacterCreationUiController.AppearancePalSetSource`/`AppearanceClothingTableSource`/`AppearancePaletteColorSource` wire a DAT-backed `ChargenAppearanceCatalog` into the Appearance page from `LivePresentationComposition` (mirroring the existing `AppearancePreviewControl` seam), and `UiButton`/`UiDatElement` both gained a per-instance `Tint` property threaded into every existing `DrawSprite` call they make; `CharacterCreationAppearancePage` now sets `Tint` directly on each swatch button and the GradCircle element instead of layering a flat-fill `ChargenSwatchColorTile` overlay on top (that class is deleted) — a genuine multiplicative sprite tint on the widget's OWN authored art, matching retail's `SurfaceWindow::BlitAndColor(..., Blit_Multiply, color)` exactly rather than approximating it with an opaque rectangle. Both fixture test suites (`CharacterCreationAppearancePageSwatchColorTests`, 8 tests) and the live-DAT color pins (`ChargenAppearanceCatalogColorTests`) pass unchanged against the new mechanism; AP-218 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-6) — `gmCGAppearancePage::Update`'s heritage-flavored static Hair/Eyes/Skin spin caption (`ID_CharGen_HairStyle`/`_Eyes`/`_Skin`, Gearknight `GearText_*`, Olthoi/OlthoiAcid `OlthoiText_*`) is now ported verbatim by `RefreshSpinCaptions`, replacing the prior ordinal substitution outright — see AP-215's own rewritten row for what remains open (the icon-thumbnail gap, restated); recount at this same edit: the row count this header carried before Batch B was already one LOW relative to the physical table (Batch A's own ending state: header said 164, the physical table already held 165 rows — verified by direct count against that commit) — a pre-existing drift this edit corrects to the counted total, not an artifact of Batch B's own net change (F12 correction, gate round 1 closeout, 2026-08-16: this note originally said "one high", the inverted direction — the header was UNDER-counting, not over-counting); AP-222 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch B fix (GF-11b) — the Appearance spins' current-part highlight and the Town buttons' Normal-to-white caption swap both port retail's actual mechanism (per-state label color/outline commit off the REQUESTED retail state id, independent of art-media availability — `UiButton.SetPerStateLabelStyle`/`ComputeRequestedStateId`), closing the row's own "not yet resolved which side is wrong" question: NEITHER client's spin ART changes (no Highlight media exists on either), but BOTH clients' spin TEXT does, matching retail's `SetState(1)`/`SetState(6)` property commit exactly (live-DAT-measured 218,167,85 -> 255,221,131, outline off -> on); AP-215 NARROWED the same batch (GF-9) — item 1 (the swatch-selection substitution) is RETIRED now that the real companion-overlay mechanism (`SetColor`'s `m_tColorWheel[...][0x10][iCurColor*7]->SetVisible`) is ported (`CharacterCreationAppearancePage`'s nine `SwatchOverlayIds`), leaving only item 2 (the icon-less style-spin ordinal label) open; AP-230 filed 2026-08-16 at the Campaign CC gate round 1 Batch A fix (GF-13) — the chargen-scoped-vs-general-importer-wide honor split for dat property 0x3B (Invisible: `UIElement::OnSetAttribute` case 8 hides an element), with the general client-wide honor deferred as its own visual gate (docs/ISSUES.md #408, 1,083 elements affected); AP-213 NARROWED the same gate round (GF-5) — the Skills page's click-to-advance/double-click-retreat single-button substitution is RETIRED now that the real per-row `pSkillUpButton`/`pSkillDownButton` arrows are wired to retail's own plain-click dispatch, leaving open only the flat-list-vs-four-bucket-sorted-model half; AP-229 filed 2026-08-16 at the Campaign CC CC7 review-fix round, F1 — the screen-layering divergence: retail's `UIFlow::UseNewMode` destroys/reconstructs the current UI framework on every mode switch where acdream's CC7 keeps both `CharacterManagementUiController` and `CharacterCreationUiController` mounted for the whole lifetime and only reveals/occludes them; AP-228 filed 2026-08-16 at the CC5 re-review residual round (R4) — the Summary listbox's skill-row KEY source, same divergence class as AP-226 filed the same round, a few retail lines away; AP-227 filed 2026-08-16 at the same review-fix round, F9 — an empty Summary name-field commit calls `SetName("")` (clearing the state), where retail's own NUL-inclusive length gate leaves `CharGenState.name` UNCHANGED for that specific case; AP-226 filed 2026-08-16 at the Campaign CC CC5 review-fix round, F11 — the Summary page's DAT-sourced labels versus retail's static `pcProfessions`/`pcGender`/`pcHeritage`/`pcTown` tables, including the non-human-heritage-renders-bare-"Heritage: " retail quirk; AP-225 RETIRED the same round, F6 — the reviewer re-derived `gmCGSummaryPage::ListenToElementMessage @0x0047bf40`'s length check and proved the 32-vs-33 threshold this row flagged as "not fully certain" does NOT exist: the compared length is NUL-inclusive (an empty field's length is 1, matching AP-226's own F11/F9 finding), so `length > 0x21` is EXACTLY `visibleChars > 32` — acdream's `MaxNameLength = 32` was always byte-correct, not merely internally-consistent; AP-223/AP-224 filed 2026-08-15 at Campaign CC slice CC5 — the acdream-only `HeritageOrGenderUnset` Finish refusal and the Summary listbox's two-bucket (Specialized/Trained only) skill-list narrowing (AP-224 corrected 2026-08-16 at the same review-fix round, F3 — its "template mechanism ported exactly" claim was FALSE as shipped, now fixed and true again, see its own row); AP-214 RETIRED the same slice — `RandomizeCharacter` is now ported and wired at the screen-open edge, closing the honest-blank-open gap it recorded; AP-212 NARROWED the same slice — the Appearance/Summary Random-button primitives are now real faithful ports, not uniform-pick approximations, leaving only Heritage/Profession/Town (still uniform-pick) and Skills (still unported) open; AP-222 filed 2026-08-15 at the re-review of Campaign CC CC6b-MOUNT fix commit `d2a71152` (N2) — the current-part spin highlight is a measured no-op for all nine spins, no Highlight media authored on any of them; AP-221 filed the same re-review (R2) — the chargen preview's one-shot-composition-vs-retryable-coordinator binding gap; AP-217 rewritten and AP-220 tightened the same re-review (R3 corrects the GradCircle from a dead click target to unported paint-art; N1 narrows the Gearknight-exit wording to non-Olthoi); AP-216..AP-220 filed 2026-08-15 at the Campaign CC CC6b-MOUNT review fix round, F2 — DoColorSpots swatch-art, the inert GradCircle, spin-caption/heritage-swap loss, the Skin-spin MoveTo reposition, and the Gearknight-boundary randomize calls; AP-215 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Appearance page's swatch-highlight (`UiButton.Selected` vs retail's separate overlay toggle) and icon-less style-spin ordinal-label substitutions; AP-214 filed 2026-08-15 at Campaign CC slice CC6b-MOUNT — retail's `gmCharGenMainUI` ctor rolls a full `RandomizeCharacter` BEFORE any page constructs, so retail's chargen screen is never actually blank on open (and the Appearance page's own gender-flip-on-init always fires against a real gender); acdream opens honestly blank instead, closing out the campaign plan's risk item 5; AP-212/AP-213 filed 2026-08-15 at Campaign CC slice CC4 — the Random button's uniform-pick approximation of retail's three unported randomize algorithms, and the Skills page's flat-listbox simplification of retail's four-bucket sorted skill model; AP-211 filed 2026-08-15 at the Campaign CC slice CC3 review-fix round — the client-side roster-vs-slotCount refusal in `RuntimeCharacterCreationState.TryBeginFinish` has no retail counterpart at that layer, retail enforces the cap in char-select UI instead; AP-207..AP-210 filed 2026-08-15 at Campaign CC slice CC3 — the FitTemplateToCharacter FPU-unrecoverable auto-detect skip, the shared-ClothingColors-list color-count approximation, the classID DAT-DID-lookup placeholder, and the ApplyTemplate atomic-replace-vs-per-attribute-guard simplification; AP-205 filed 2026-08-11 at Campaign OP gate 4 (#381) — the Apply/Reset/Defaults footer's opaque backing field is a genuine acdream synthesis with no authored retail counterpart; ~~AP-201~~ RETIRED 2026-08-11 at the Campaign OP gate-3 fix round — `UiScrollablePanel` now keeps a straddling row visible and CLIPS it to the viewport (`ClipsChildren` → `UiRenderContext.PushClip`, which existed by then), replacing the whole-row cull this row recorded; the user-observed symptom (the Chat tab's per-window filter blocks vanishing into a void at the DEFAULT scroll offset) closed issue #371; AP-203 RETIRED 2026-08-26 by #446 — all 306 installed-DAT rows now have distinct identities and concrete consumers; AP-204 corrected and RETIRED the same day — exact capture/conflict/button semantics now follow named retail; AP-202 RETIRED 2026-08-26 by #446 — retail `.keymap` file interchange and profile lifetime now ship; AP-200 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Chat Font Face/Size menu rows are store-only, distinct fields from the existing live `ChatSettings.FontSize`; AP-199 filed 2026-08-11 at Campaign OP slice OP6 — the Config tab's Sound Features menu, Interface Sound trio, and Play Sound Only When Active are store-only (the Interface trio cites AP-174's existing "retail's own dead knob" finding); AP-198 filed 2026-08-11 at Campaign OP slice OP6, row count reconciled at the OP6 rework round (2026-08-11, review N1) — the Config tab's TEN Graphics/Rendering-Quality-family rows (including Screen Brightness, its own field as of the S2 fix) are store-only, the Vulkan+one-aggregate-QualityPreset renderer having no per-feature knobs; AP-197 filed 2026-08-11 at the OP4 review-fix round (SF-1/S4) — Display Timestamps hardcodes retail's constructor-default format string instead of the per-character GenericQualitiesData key-1 override the parser reads and discards; ~~AP-196~~ RETIRED 2026-08-11 at Campaign OP slice OP9 — originally filed at the OP4 review-fix round (MUST-FIX 3 / blast M2) for the Group-C re-point's observable-default changes (ViewCombatTarget true→false) and the PARTIAL GameplaySettings retirement (AutoTarget/AutoRepeatAttack/ViewCombatTarget deleted, the other five kept as write-behind mirrors); OP9 deleted `GameplaySettings` outright (all 13 remaining members were already re-pointed to the server-bit seam at OP4), closing the write-behind-mirror gap for good — see its retirement note below; AP-195 RETIRED 2026-08-11 at Campaign OP slice OP5 — ported both halves left open at OP2 re-review closure: the ALL-set LED media swap (`UiButton.FaceFileOverride`, driven by the block-level `P0x10000082`/`P0x10000083` sprites now threaded through `ElementInfo`/`DatWidgetFactory`) and the `CreateChildren` self-sizing tail (`UiCheckboxBitfield64.Height` grows with `_contentHeight` per row; the ENCLOSING page ListBox reflows around the block's FINAL size via the new `UiTemplateListBox.AddPrebuiltRow`, reusing the ListBox's own stacking exactly as the row's own disposition menu allowed, rather than a third stacking path); AP-194 filed 2026-08-10 at Campaign OP slice OP1 — the GetDefaultOptionValue vs constructor-default disagreement for ConfirmVolatileRareUse/ShowHelm/ShowCloak (see the row below); AP-193 filed 2026-08-10 at Campaign OP slice OP1 — the 0x34 HearPKDeathMessages id/mask mapping is ACE-sourced (see the row below); AP-192 filed 2026-08-10 at the Campaign CH round-5 polish (S2) — authored outline `0x21`/`0x22` now reaches every text-bearing widget, but only at the element's effective-default state; per-STATE outline switching (dialog/character/combat buttons author `0x21` in state `0x3` only) is not ported; AP-191 filed 2026-08-10 at Campaign CH round 4 items 1+2 — the chat transcript's missing tag-colour (`0x1D`, green) and tag-font (`0x1C`) are deferred, needing a per-run tag concept `UiText.Line` does not have yet; AP-184 RETIRED 2026-08-10 at Campaign CH round 4 — the three PARTIAL `/help` group topics (channels/chatting/commands) are now COMPLETE verbatim listings, `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290` fully decoded (the "vftable slot" operands are the same pooled/mislabeled-data artifact as AP-186's own precedent, not real vtable dispatch — reading the function's own disassembly for the `push imm32` preceding each constructor call resolves them), closing ISSUES.md #364 (full retirement note later in this same list, at its own "AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09..." entry); AP-113 RETIRED 2026-08-10 at the consolidated-review round, SHOULD-FIX 3/1 byproduct — DoLifestone's own bad-args refusal text is now byte-recovered, see its retirement note below; AP-183 and AP-186 RETIRED 2026-08-10 by issue #363's interface-text seam — see their retirement notes below; AP-190 filed 2026-08-10 at Campaign CH slice CH6c — window opacity now fades every RetailWindowManager window on retail's focus-driven Default/Active mechanism, not just ChatInterface-derived ones, and ships gmMainChatUI's 1.0/1.0 default as the ONE shared default across every registered window (fixed from the original 0.5/1.0 base-ChatInterface value, per the row's own REWORDED (2)) instead of applying it only to ChatInterface-derived windows, retiring AP-40 (the prior "opacity is fixed at 0.75, no focus transition" row) in the same commit; AP-189 filed 2026-08-10 at the CH6a/b REJECT-review rework, SHOULD-FIX 5 — acdream's ONE shared 500-entry/200-line-display-tail chat log gives every window a shallower EFFECTIVE per-window scrollback depth than retail's own per-window 10,000-line log, though the accumulate-while-closed and independent-per-window-scroll BEHAVIORS are both correctly reproduced; AP-188 filed 2026-08-10 at Campaign CH slice CH6b — a floating chat window's chat entry always sends on the Say channel because the floaty LayoutDesc authors no talk-focus menu and acdream does not (yet) share the main window's currently-selected channel across all five chat-window instances; AP-187 filed 2026-08-10 at Campaign CH slice CH6b — the four floating chat windows' text-type filters persist in local `settings.json` only (`ChatSettings.ChatWindow1..4Filter`), with no analog to retail's server-side `0x1000008C` GameplayOptions blob, so a character's floaty filter customization does not travel between acdream installs or round-trip to/from a retail client sharing the same character; AP-186 RETIRED 2026-08-10, issue #363's interface-text seam — `ChatVM` now carries an `OnInterfaceText` hook (`Action?`) the App-layer composition wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)`, exactly fix shape (a) this row's own filing proposed; `ChatCommandRouter`'s two local-presentation fallbacks (`RetailCommandHelpTable.UnknownCommand` and the degenerate-prefix "Unknown command: {verb}." refusal) now call `ShowInterfaceText` and reach the SpewBox, with a null-fallback into the chat log (still tagged `ClientLocal`) for hosts that never wire the hook (headless has no `ChatVM` at all). Closes ISSUES.md #367; AP-185 filed 2026-08-10 at Campaign CH slice CH6a — the chat window's UiLocked border-art cosmetic swap is unported, see the row for detail; AP-184 RETIRED 2026-08-10 at Campaign CH round 4, closing ISSUES.md #364 — filed 2026-08-09 at Campaign CH user-gate round 2, item 3, recording that three of the seven retail `/help` group-topic listings (channels/chatting/commands) remained PARTIAL because their detail text is built in full or in part by `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`, which the filing believed "not decodable with confidence from a static string sweep" because Binary Ninja renders its three internal string operands as dereferences of unrelated vtable slots (`&ClientCommunicationSystem::\`vftable'.RecvNotice_StartBarberNotice` etc.). That belief was WRONG — the same pooled/mislabeled-data artifact this register already documented elsewhere (AP-113's retirement note) applies here too: reading the function's own disassembly for the `push imm32` immediately preceding each `PStringBase::PStringBase` constructor call (rather than trusting BN's line-grouped rendering, which hides the true instruction order) resolves all three operands directly — `"@"` + a one-character tag sliced from a shared wide literal `U"fvpca"`/`U"mh,."` (a wide string read through a narrow `char*` truncates at the first zero high byte, the "hack" retail's own function name calls out) + `" - Sends a broadcast to your "` + `ChannelSystem::GetChannelName`'s own literal switch-table result + `".\n"`. `ChannelsGroupDetail` (entirely 6 such calls), `ChattingGroupDetail` (6 more, plus a `HelpReply@0x00577A50` Summary-branch quirk that unconditionally emits reply+pr+mr together — read directly, not assumed), and `CommandsGroupDetail` (`HelpAllGroup`, a straight-line concatenation of every other group's Detail branch plus a handful of its own short one-liners, including a CONFIRMED retail saveui/loadui duplicate) are now COMPLETE verbatim listings, matching the four (death/status/text/allegiances) the original filing already had. See `RetailCommandHelpTable`'s class remarks and `RetailCommandHelpTableTests` for the full per-line address citations. Round 2 item 2 also deletes `PortalWaitNoticeController` (the dedicated centered-overlay presentation the user reported was the wrong retail surface) and reroutes the portal-space wait-cue notice through the same `AddText`/SpewBox chokepoint every other on-screen interface-text site uses — AP-178's open SpewBox position/extent/font/colour questions now cover this notice too, since its separate controller and consts are gone; no new row was needed for the surface mismatch itself, since it was never separately registered (`PortalWaitNoticeController`'s own doc comment asserted "not a chat message" as an accepted design, not a flagged divergence). AP-150 RETIRED 2026-08-09 at Campaign CH user-gate round 1, item D (#329) — `PortalTunnelPresentation.TickRotation` now emits `"In Portal Space - Please Wait..."` unconditionally on every rotation-segment expiry, exactly matching `gmSmartBoxUI::UseTime`'s `else`-arm at 0x004D6FCD, instead of gating on `_waitCueVisible`, which only ever went true after the invented 5-second `RuntimeWorldTransitState.RetailWaitCueDelay` hold; `RetailWaitCueDelay`/`ObserveWait`/`SetWaitCue` remain as `LocalPlayerTeleportController`'s own hold-delay telemetry (`RuntimePortalSnapshot.WaitCueShown`) but no longer gate the on-screen cue, so they are not a residual of this row — closes issue #329; AP-183 RETIRED 2026-08-10, issue #363 — every named site now routes through the `ChatVM.ShowInterfaceText`/`OnInterfaceText` seam (see AP-186's retirement note) at its correct retail type: `DoStupidChannelHack` ("You must specify the text you wish to say!", newly wired — the six legacy channel verbs previously fell through `ChatInputParser.Parse`'s pure `return null` with no message at all), `DoChannelList`/`On`/`Off` ("Please specify the channel name.", reclassified), `DoAllegiance` ("Please see @help Allegiance...", reclassified), `DoHouseAvailableList` (reclassified AND corrected to retail's own "Please see @help hslist for more information on how to use this command" string, replacing the acdream-synthesized "Usage: /hslist " fallback — verified `acclient_2013_pseudo_c.txt:381481`/`1029383`), and `DoReply` ("Someone must @tell you first!", newly wired for the message-but-no-last-teller branch only — bare `/r` with no message at all is a separate retail branch, deliberately still unported). `DoSpeaker`/`DoEndurance`/`DoTitle` are untouched, confirmed still correct at `0x00`. The generic bad-args fallback (`ChatCommandRouter.Submit`'s catalog dispatch) now resolves `WeenieErrorMessages.Resolve(0x026u, null)` ("That is not a valid command.", the exact port of `DoCommand @0x0057E46D`'s `HandleFailureEvent(0x26)`) instead of synthesizing a `"Usage: {Usage}"` line — cross-checked against five decompiled handlers (`DoDie` plus the four above), all `0x1A`, confirming the uniform routing decision; AP-182 filed 2026-08-09 at Campaign CH slice CH4, corrected at the CH4 REJECT-review (nit 11) — `@title` is wired to a pure no-op (the value is neither stored nor consumed anywhere) and also omits `DoTitle`'s three local failure messages; recount at the CH3 Opus review corrected a pre-existing off-by-one; AP-181 filed 2026-08-09, Campaign CH slice CH3 — the local chat spam throttle (`IsMessageSpam`) has no acdream port. AP-178 NARROWED 2026-08-09 at the CH2 REJECT-review rework NIT 3, wording corrected at the CH2 re-review nits pass (`docs/plans/2026-08-09-chat-parity-campaign.md`, nits 1/2/6) — the original `dats.Portal` pass used an id source that was not Portal's own (`dats.Portal.GetAllIdsOfType()` is empty for this type), so it established nothing about Portal either way; extending a correctly-paired sweep to `dats.Local` FOUND the SpewBox element there; extent (`450×72`) and `MaxConcurrentItems` (`4`, not the code-default `1`) are now AUTHORED, leaving absolute screen position, colour, AND vertical content flow (now TOP-aligned, acdream's own invention pending measurement) open. AP-180 filed 2026-08-09 at the CH2 REJECT-review rework — `RuntimeCommunicationState.AddText`'s `windowId` parameter is accepted but not consumed, so retail's dual-destination echo (a `0x1A` message with a non-zero `windowId` lands in both the SpewBox and its originating chat window) is unimplemented; latent today since every production caller passes `windowId = 0`. AP-177/AP-178/AP-179 filed 2026-08-09, Campaign CH slice CH2 (interface text / SpewBox) — AP-177 records the invented 5-second SpewBox line lifetime (retail's real timeout is keystone-owned and unmeasured); AP-178's original filing recorded the invented SpewBox screen position/extent/font/colour/MaxConcurrentItems after `SpewBoxLayoutDumpDiagnostic`'s Portal-only sweep found zero elements of class 0x10000016 — see the NARROWED note above for the corrected finding; AP-179 is the OnCombatLine half of the RETIRED AP-176 split out to its own row. AP-176 RETIRED the same day — the WeenieErrorMessages full 344-row `HandleFailureEvent` port (`WeenieErrorMessages.Resolve`) replaces the single-stand-in-`LogTextType` approximation that row recorded for `ChatLog.OnWeenieError`. AP-175 filed 2026-08-09, Campaign CH slice CH1 — PopUpString renders as a chat-log line instead of retail's modal dialog; AP-39 updated the same day — chat coloring is now retail's exact 34-value `LogTextType` table, not a synthetic per-`ChatKind` approximation of it. AP-173 and AP-174 filed 2026-08-08, Campaign A slice A2 — AP-173 expresses retail's ±15 dB DirectSound pan as an OpenAL azimuth by inverting the constant-power pan law, since AL exposes no per-channel gain for a mono source; AP-174 records acdream's extra master volume knob on top of retail's three, folded into retail's single master multiply so the −50 dB cutoff and dB quantisation move with it. AP-172 and AP-171 filed 2026-08-08, #354 spell-bar drag-reorder fix — the favorite-bar reorder gesture defers its own list rebuild for the drag's duration so `UiRoot`'s drag-cancel safety net cannot destroy the in-flight cell, compensating the drop-time target index for the resulting stale sibling numbering; final positions and the wire pair are retail-exact, only the mid-drag visual reflow timing differs. AP-170 filed 2026-08-08, grand-gate finding G3 — an out-of-range vendor Use now arms on arrival instead of sending immediately, because the user's local ACE server polls for the player to actually reach use range before opening the shop panel and a too-early Use is silently lost; AP-169 filed 2026-08-08, grand-gate finding G2 — the vendor toolbar split-slider resolver falls back to the packed shop-supply-count field when the item's own `PublicWeenieDesc._stackSize` is absent, because the user's local ACE server never populates the latter for a browse-list item; AP-167/AP-168 filed 2026-08-09 at the Opus review of `92ea3977` (findings F1/F6) — Buy All's container-vs-item slot classification approximates retail's bitfield/capacity test with `ItemType.Container` [AP-168], and SellSingleItem's non-empty-container refusal branch is not ported [AP-167]; AP-164 RETIRED the same review (finding F4) — BF_RETAINED is now checked end to end; AP-162 NARROWED the same review (finding F1) — Buy All's four client-side pre-send guards are now ported, leaving only the single-item TryBuy path without one; AP-161 gains a REVIEW CORRECTIONS paragraph the same review (findings F1-F13) summarizing the rest as bug fixes to already-claimed behavior, not new divergences. AP-164/AP-165/AP-166 filed 2026-08-09 at Slice 6b/6c (staging+sell arc) — InqAcceptability's non-sellable bitfield is unmodeled [AP-164], the Buy-side stackable-removal-amount test substitutes DescStackSize for retail's _maxStackSize [AP-165], and the Buying/Selling tabs' own purse/count text plus the cross-panel pending-sell inventory highlight are unwired [AP-166]; AP-161 NARROWED the same day — the row's last vendor-specific residual (Buying/Selling tabs render but carry no data binding) CLOSES now that both tabs are fully wired (staging, drag-to-sell, InqAcceptability gating, Sell 0x0060, the X-close confirmation), leaving only the two long-standing PRE-EXISTING residuals (dropdown arrow-cap glyph, alt-currency m_last_sale simplification) plus the three new AP-164/165/166 residuals just filed; AP-162 EXTENDED the same day — the same no-client-pre-check omission now also covers the batched "Buy All" path (TryBuyAll), not just the single-item TryBuy. AP-162/AP-163 filed 2026-08-09 at Slice 6.3 (buy arc) — no client-side Buy affordability/capacity pre-check [AP-162] and the shop-item guid-collision skip-not-clobber policy [AP-163]; AP-161 NARROWED the same day — the private-selection and unwired-examine residuals CLOSE at Slice 6.1/6.2, leaving only the dropdown arrow-cap glyph and the alt-currency `m_last_sale` simplification, plus a confirmed-absent-from-retail note on double-click-to-buy. AP-161 REWRITTEN 2026-08-09 at the Slice 5.4 review (findings F1-F8) — the popup-never-rendered, wrong-quantity-price, no-auto-select, dropped-icon-layer, stale-category-on-vendor-switch, and unguarded-Apply-fanout bugs the review found are fixed (`VendorUiController.cs`, `VendorState.cs`, `GameEventWiring.cs`, `RetailUiRuntime.cs`); the row now records only the four consciously-deferred residuals it still owns (private per-panel selection vs. retail's global `ACCWeenieObject::selectedID`, the unwired shop-item examine route, the dropdown button-face arrow-cap glyph, and the alt-currency held-amount's `m_last_sale`-free simplification). AP-110's "retail-correct per-unit prices" phrasing is corrected the same day to "quantity-correct pricing" — the OLD phrase mischaracterized what retail even shows (a `GetObjectSplitSize`-quantity price, not literally one unit) independent of whether the code was buggy. AP-161 filed 2026-08-09 at Slice 5.4 (vendor browse panel) — the authored "Buying"/"Selling" tabs render and switch pages but carry no data binding, per contract decision 8's required successor to AP-110's narrowing; AP-110 NARROWED the same day — "vendor" is retired from its absent-panels list now that the "Items" browse tab is user-reachable. AP-160 filed 2026-08-07 at Slice 5.3 — the client-local vendor-panel distance watcher closes on plain 3D center distance instead of retail/ACE's cylinder-gap distance, because Runtime has no per-entity collision radius/height source outside the App-layer's Setup-cylinder resolver. AP-158 RETIRED 2026-08-06 by the #333 fix, closing #337 — the `maxReach` distance pre-filter is DELETED rather than re-centred, because retail has none: `CObjCell::find_obj_collisions` @0x0052b750 walks the cell's shadow list and calls `CPhysicsObj::FindObjCollisions` unconditionally. The row's predicted symptom was observed live at Neftet before it was fixed — a tall prop AP-156 had just placed correctly still not blocking, plus jumps sinking into the mesh and corpses falling through. Perf measured, not assumed: at the live-maximum 38 in-cell candidates 10.61 µs → 16.68 µs per resolve. AP-159 filed 2026-08-06 at the #334 fix — the INDOOR half of AP-156’s traversal residual is all that remains of it; the outdoor half is CLOSED by the `find_bbox_cell_list` port, and AP-156’s RISK COLUMN IS CORRECTED at the same commit: it recorded the residual as “extra broadphase candidates, never a missed one”, which generalised the indoor direction to the whole row and is exactly why #334 — a MISSED one, and a user-observed loss of collision on landblock-spanning formations — sat inside it unnoticed. AP-158 filed 2026-08-06 at the AP-156 fix review — the shadow broadphase's `maxReach` distance pre-filter is acdream's own invention with NO retail counterpart, and it measures from the part origin, so it can discard a genuine contact for exactly the off-centre parts AP-156 just placed correctly; issue #333. AP-156 CORRECTED at the same review: its population was understated — 172 is AP-152's DISPATCH population, not AP-156's CONTAINMENT population. AP-155 NARROWED and AP-156/AP-157 filed 2026-08-06 at the AP-152 retail-conformance review. AP-155 bundled two divergences with different code paths, populations and gates under one id; its flood half is now AP-156, **with its direction corrected**. AP-155(b) recorded the BSP flood approximation as OVER-inclusive and used that direction as the reason the residual was safe to defer; measured over the installed DAT it was UNDER-inclusive for 428 of the 530 BSP-bearing Setups (the AP-156 fix review corrected the originally-recorded '170 of 172'), because `BuildFloodSpheres` carried each physics-BSP part's root bounding-sphere RADIUS while discarding that sphere's own ORIGIN and centring it on the part origin. That is the #98/#168 class, and for 43 Setups the post-AP-152 flood was strictly smaller than the pre-AP-152 one. AP-156 records the correction and the fix — `ShadowShape.BoundsCenter`, filled from the same resolver that supplies the radius, plus the retirement of the 10-sphere clamp on a branch where retail has none — and keeps open only the sphere-vs-portal TRAVERSAL approximation. AP-157 is the previously unregistered third-branch substitution: retail floods from one `CPartArray::GetSortingSphere` where acdream floods from every Sphere shape, and acdream's cylinder flood ignores `CylHeight`. AP-152 RETIRED 2026-08-06, one day after it was filed: `ShadowShapeBuilder.FromSetup` now dispatches BSP-first instead of unioning, and `ShadowObjectRegistry.BuildFloodSpheres` now applies `calc_cross_cells`' own BSP → cylsphere → sorting-sphere order. Four statements in the row were false and are corrected in its retirement text — most importantly its predicted symptom, "catching on a doorway sill", which could not have been occurring: `Transition.BspOnlyDispatch` had already made the extra primitive inert at collision-query time since 2026-05-25. The live half was CELL MEMBERSHIP, the #98/#168 symptom class, which had no such guard. AP-153/AP-154/AP-155 filed at that retirement — retail's dispatch flag is cached once at part-array construction where acdream's gate is live [AP-153]; acdream's query-time guard takes a CLIENT-DERIVED flag off the WIRE and never derives it, an undeclared dependency on ACE reading the same DAT bit [AP-154]; and the static publication paths emit a Setup Sphere as a height-capped Cylinder while `BuildFloodSpheres` approximates retail's bounding BOX with bounding SPHERES [AP-155, whose flood-priority half is closed by the same commit]. AP-152 filed 2026-08-06 at the AP-22 retirement — the LIVE collision path emits Setup primitives and per-part physics-BSP shapes additively where retail's `CPhysicsObj::FindObjCollisions` dispatches exclusively; 172 of 5,935 installed Setups are affected, including BSP doors, so it needs its own visual gate and was deliberately not folded into the AP-22 commit; the count is unchanged because AP-22 retired in the same commit. AP-22 RETIRED 2026-08-06 — retail synthesizes no shape for a shapeless object (`CPhysicsObj::FindObjCollisions` 0x0050f050 exits at `0x0050f22f je 0x50f31b` returning the seeded OK_TS, and `CPartArray::GetRadius`/`GetHeight` are absent from its whole call set), so the invented `setup.Radius` cylinder was deleted rather than re-derived; the row's site list named one file that never contained the fallback and omitted the two that did, one of them the headless-only copy, and its "rare decorative props" risk described an unreachable branch — 0 of 5,935 installed Setups can satisfy the guard. AP-150/AP-151 filed 2026-08-06 at the #280 dual review — the wait cue's five-second arming is acdream's own and not retail's trigger [AP-150], and the reveal gate is materially stricter than retail's DAT-residency prefetch predicate on the mesh-build/GPU-upload axis [AP-151], the opposite asymmetry from AP-149; AP-149 filed 2026-08-05 at the #280 portal-prefetch fix — the reveal gate's outer ring accepts terrain-only publication where retail requires LandBlockInfo and every building EnvCell; the fix closes the reveal-window/visible-window ratio, not this residual; AP-148 filed 2026-08-05 at the C5b closeout — acdream's local-player Gate A requires the wire TELEPORT_TS to be EQUAL where retail requires only that it not be OLDER, verified by disassembly against the PDB-paired binary after two review rounds read the Binary Ninja tautology and missed it; AP-147 filed 2026-08-05 at the C5b architecture review, finding D3 — the accepted-Position delta stream's cardinality change and its torn intermediate; AP-138 amended at the same review — C5b staled its route-2 first-submit `CurrentCellId` measurement; AP-131 RETIRED 2026-08-05, C5b, closing #275 — the steady-state merge's `installPlacementFrame: true, clearParent: true` literals no longer exist; `InboundPhysicsStateController.TryApplyPosition` now computes both flags PRE-MERGE from `(disposition, hasAnimations(old))`, which is exactly `RuntimeAuthoritativePositionRouteClassifier.ClassifyAcceptedPosition`'s own `ApplyPlacementFrameBeforeRouting`/`UnparentBeforeRouting` rows (false/false on the Gate A force row, `!HasAnimations`/true on every accepted non-force route). Retail decides both writes BEFORE `MoveOrTeleport` is consulted — Gate A @0x0045400C returns @0x0045409D ahead of `unset_parent` @0x00454129 and the `HasAnims` `SetPlacementFrame` gate @0x00454137 — so the flags need no route, no player distance and no signature change. The row's predicted symptoms are gone: an animated entity's ordinary Position no longer installs a placement frame retail skips, and a ForcePosition no longer unparents. Evidence: `InboundPhysicsStateControllerTests` — `ApplyOnAnimatedEntity_NeverInstallsTheWirePlacementFrame`, `ApplyOnNonAnimatedEntity_InstallsTheWirePlacementFrame`, `ForcePositionOnParentedLocalPlayer_RetainsTheParentAttachment`, and the 12-row `MergedPrePlacementFieldsMatchTheClassifiedRouteFlags` matrix which uses the production classifier as its oracle rather than re-encoding the table; all four sabotage-verified in both directions. The row's "the legacy caller is deleted at the production cutover" framing was overtaken: the caller was CORRECTED, not deleted, and remains the only production Position wire caller; AP-145 RETIRED 2026-08-05, C5a commit 1, closing #318 — `TryPublishPlace` now publishes the local player's Place through `LocalPlayerShadowSynchronizer.SyncPose`, the same publisher ordinary per-tick movement uses, instead of a direct `LocalPlayerShadowState.Set` that never touched `PhysicsEngine.ShadowObjects`; AP-1 RETIRED 2026-08-05, C5a deletion sweep — `PhysicsEngine.Resolve`/`ResolvePlacement`/`HasCellSurface` deleted outright, zero production callers, so "production zero-delta routes remain on the legacy resolver" is now structurally false; AP-146 filed 2026-08-05, #319 fix — the local player's canonical cell is written only at login/inbound-Position/teleport, not per ordinary-movement tick as retail's SetPositionInternal does; #319's fix makes a player-parented child inherit exactly this coarseness, stale-but-equal to the parent, not a new staleness class; follow-up filed as issue #320; AP-144 filed 2026-08-05, C4 route 3 round 3 (R7) — the portal-arrival movement-event send reuses `UsePositionFromServer` (`autonomy_level != 2`) where retail's actual gate, `SendMovementEvent`, is `autonomy_level != 0`; the two agree everywhere except level 1, which no production caller can reach today; AP-142/AP-143 filed 2026-08-04, C4 route 7 — the parented-child single-field cell model (id/pointer collapse, zero-not-stale removal propagation, same-cell tick-loop subsumption) and the headless parent-realize drive's skipped holding-location validation; AP-141 filed 2026-08-04, C4 route 5, NARROWED 2026-08-04 at the round-2 delta review — the far-branch StopInterpolating clause was wrong for the adopted-body case (it is now ported there) and the row's language now distinguishes "never armed" from "never re-anchored"; CORRECTED 2026-08-04 at the round-3 delta review — the risk column's "would drag the body toward a stale anchor" claim was itself wrong (the leash anchor is write-only; `ConstraintManager::adjust_offset` only brakes, never pulls) and is retracted; every half remains test-gated only, since ACE never sends a missile UpdatePosition; AP-140 filed AND RETIRED 2026-08-04 — filed at the Bug B Opus review because the two accepted-Position routing gates read the client `Airborne` flag, i.e. walkability, where retail's free-flight predicate is CONTACT, and Bug B had just turned "in contact, not on walkable ground" from unreachable into ordinary; retired the same day by pointing both gates at `PhysicsBody.InContact`, retail's literal `transient_state & 1` test at `InterpolationManager::adjust_offset` @0x00555D52 (bit 0 = `CONTACT_TS`, acclient.h:3690), while leaving `Airborne` and all five of its `!Body.OnWalkable` writers untouched — the narrow shape the row itself pinned. A remote sliding on a steep face now interpolates as retail does instead of snapping at UpdatePosition cadence; AP-139 filed 2026-08-04, Bug B remote steep-contact slide — the interpolation-queue clear on the landing edge, carried over from the deleted hand-rolled remote landing block; AP-81 narrowed the same day by that fix, which retired its whole GRAVITY half; AP-87 annotated the same day — its predicted symptom was observed live and then fixed at the source, with the row's own thresholds and conditions deliberately unchanged; AP-138 filed 2026-08-04, C4 route 4b-2 dual Opus review, parts (1) and (2) rewritten the same day at the DELTA review — the far snap's refusable-placement residual: store_position only on the outcomes that never reached the engine, the two quiescence parks made restorable at the source, with the rollback gated on the cell it actually restores into, rather than refused by a pre-flight that structurally cannot see them, and the leash not armed through a superseded incarnation; AP-137 filed 2026-08-04, C4 route 4b-2 and rewritten the same day at that review, `teleport_hook`'s call list completed at the delta review — the acdream-only null/rejected/cell-less leftover arm, what the deleted duplicated 96 m/4 m constant pairs actually computed, and the vacuous headless satisfaction; AP-136 filed 2026-08-04, C4 route 4b-1 review, NARROWED 2026-08-04 at the C4 route 4b-2 delta review and AMENDED 2026-08-04 by the cancelled-park presentation rollback (the row's "restored visible" claim covered only the CANONICAL half; the presentation half was never rolled back, which left a parked-then-cancelled remote that stops moving invisible in the world AND absent from the radar for the rest of the session — a defect, now fixed by the `WithdrawalRestored` receipt, with the selection residual filed as AD-63) — a cancelled lost-cell park re-shows the entity where retail keeps it hidden until cell load, and the rollback's scope now covers the two placement-side quiescence parks whenever the cell it restores into is not itself quiescing — round 4 (2026-08-04) applies that same test a second time at RESTORE time, because a retained park's rollback lands a packet later; AP-135 filed 2026-08-03, C4 route 4a — the airborne no-op's retained acdream bookkeeping; the stated total was 2 rows stale before that filing and is now a literal count of this section; AP-130/AP-131/AP-132 filed 2026-08-02, continuation-executor slice; AP-5 retired 2026-07-31 at Campaign P Slice 2A — every successful `step_down` now performs retail's final `PLACEMENT_INSERT`; AP-3/AP-4 retired 2026-07-31 at Campaign P Slice 1B — `transitional_insert` and `edge_slide` now preserve retail's valid-contact early return and Branch-1-first order; AP-127 retired 2026-07-31 by #268 — the complete augmentation chain is shared by character UI and Runtime movement; AP-30 retired 2026-07-30 by the movement parity audit — retail Frame::is_equal genuinely uses the 0.0002 epsilon [byte-confirmed], so the row recorded a NON-divergence; acdream already matches; AP-129 narrowed 2026-07-30 at the P4 Opus review fix — `CanMoveInto`/`RestrictionDB::IsAllowedIn` are now ported and fed end-to-end (CreateObject HouseOwner/HouseRestrictions/Monarch tail fields + live `House_UpdateRestrictions 0x0248`, resolved through `PhysicsEngine.Objects`), retiring the original "CanMoveInto entirely unmodeled, unconditional fail-closed" gap the row described — the review was triggered by `RestrictionObjPrevalenceInspectionTests` showing 103,766 of 729,888 installed EnvCells (the whole housing estate) carry a baked `RestrictionObj`, so the unconditional fail-closed default would have locked every house for every player including its own owner; AP-10 retired 2026-07-30 at Campaign P Slice P4 — restored retail's 0.1 m dry-corner water sink-in, full suite green proving the sticky-bit no-regression argument; AP-71 retired same slice — `check_entry_restrictions` ported at the head of the indoor `FindEnvCollisions` branch, `CellPhysics.RestrictionObj` wired from the DAT-baked `EnvCell` field in both the dev and production caching paths; AP-128 filed 2026-07-30 at the P3 Opus review — PK-timer clock basis; AP-25 retired 2026-07-30 at Campaign P Slice P1 — the vitae/enchantment-aware run/jump skill chain; AP-7 retired 2026-07-30 at Campaign P Slice P2 — `calc_friction`'s threshold ported to retail's confirmed 0.25f; its still-open cos(10°)-vs-0.99999536f Sledding constant question moved to AD-55) **2026-08-28 count correction:** #408 retired AP-230 by moving property `0x3B` visibility into the shared importer and every retained stateful widget; @@ -328,8 +329,10 @@ research and is no longer active. | AP-27 | PlayerDescription trailer: GameplayOptions skipped by a 4-byte-aligned heuristic scan for a valid inventory parse; options blob captured opaque, never decoded (retail decodes + applies UI options) | `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs:69` | Variable-length opaque blobs; mirrors holtburger's heuristics; follow-up issue extends when panels consume those sections | An options blob that coincidentally parses as a valid inventory (or inventory not landing at EOF) yields wrong/empty inventory+equipped at login; retail-persisted UI options silently ignored | ACE GameEventPlayerDescription.WriteEventBody; holtburger events.rs:195-218 | | ~~AP-28~~ | **RETIRED 2026-08-08 (Campaign A slice A2).** The three picked AL parameters and the gain-driven eviction are both gone. `RetailSoundMixer` now carries the byte-decoded retail curve — `g = dist < 5 ? vol : 25·vol/dist²`, clamped to 1, ONE master multiply, `db = ceil(20·log10 g)`, and a hard −50 dB no-allocate floor (audible radius ≈94.2 m at unity) — with pan as retail's `−15·sin(Δbearing)` in whole decibels and a 5-metre integer deadzone. Every AL source is source-relative with `AL_ROLLOFF_FACTOR = 0` and the global distance model is `None`, so AL contributes no attenuation of its own; the old `InverseDistanceClamped` ref-2 m curve was inverse FIRST power (`2/d`), quieter than retail up close and far louder at range with no cutoff at all. Voice eviction now compares the DAT-authored float priority strictly-less in ring order per `SoundManager::PlaySoundInternal` @ `0x0054FEC0` (the row's old `FUN_00550ad0` citation was wrong — that address is inside an `IntrusiveHashTable` constructor). The residual pan-LAW approximation is AP-173; retail's own `s_bPlaySoundOnlyWhenActive` gate is TS-64. | retired | — | — | `SoundManager::GetAttenuation @ 0x00550020`; `SoundManager::PlaySoundInternal @ 0x00550170` and `@ 0x0054FEC0`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` | | AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter | -| AP-236 | **S4-c2 (2026-09-04): AP-34's distance-sort residual is CLOSED — retired, not narrowed.** `RetailAlphaQueue` is now retail's own two independent FIFO lists (CLIP/ALPHA, capacity 3000 each, `D3DPolyRender::AddMeshToAlphaList`'s exact append-only/capacity-drop behavior), routed by `RetailAlphaMeshRouter`'s port of `DrawMesh`'s branch table and drained at retail's four normal-world `FlushAlphaList` sites (`DrawBuilding`/`DrawBlock`/`PView::DrawCells`/`RenderNormalMode`) under retail's exact no-op threshold rule. No acdream-only sort remains anywhere on the alpha path. The ONE residual AP-34 documented that this chunk does NOT fix: `EnvCellRenderer` transparent shell batches still draw immediately in their own per-cell MDI-indirect call, never through the shared queue — wiring EnvCell's batched multi-draw-range submission into the per-subset FIFO token protocol (`IRetailAlphaDrawSource`) is a substantial, separate architecture change (EnvCell has no per-subset deferred-replay abstraction today), out of this bounded chunk's scope and deliberately deferred rather than attempted without visual verification. | `src/AcDream.App/Rendering/EnvCellRenderer.Rhi.cs` (the `renderPass == WbRenderPass.Transparent` MDI-indirect block); `src/AcDream.App/Rendering/RetailAlphaQueue.cs`; `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` | EnvCell's per-cell transparent batch is homogeneously mask 0x02 (ALPHA-family; EnvCell shells never carry a ClipMap bit in this path) so, when a future chunk wires it in, the WHOLE per-cell batch can be ONE atomic FIFO token — coarser than retail's per-subset entries but consistent with how WbDrawDispatcher/ParticleRenderer already submit at instance granularity, not per-D3D-surface-subset. Until then, EnvCell's own `detailEnabled` gate already matches retail's row-1 "detail active → immediate" case exactly; only the "detail off → append instead of draw-now" branch is missing. | An EnvCell's transparent shell (glass, water, grates) with no environment detail texture set composites in strict draw-call order relative to ordinary GfxObj/particle content instead of interleaving through the shared FIFO — a narrow-overlap ordering difference, not a blend/visual one (same class of risk AP-34 originally carried, now scoped to exactly this one renderer). | `RenderDeviceD3D::DrawEnvCell` 0x0059F170; `D3DPolyRender::AddMeshToAlphaList` 0x0059C230; `D3DPolyRender::FlushAlphaList` 0x0059D2E0 | -| AP-237 | **Filed 2026-09-04 (S4-c2).** `RetailAlphaMeshRouter.MaskFromTranslucencyKind` reconstructs retail's `ConstructMesh` queue-routing mask (spec: alpha-family bits always win; else ClipMap; else Translucent) from acdream's already-collapsed `TranslucencyKind`, which was built for a DIFFERENT priority chain (`TranslucencyKindExtensions.FromSurfaceType`'s blend-STATE selection: a "Translucent override" step checks Translucent+ClipMap-without-alpha-family FIRST and calls it `AlphaBlend`). A raw `Surface.Type` with Translucent(0x10) and Base1ClipMap(0x04) bits but no Alpha/InvAlpha/Additive bit (observed: cloud GfxObj `0x01004C35`) therefore reconstructs to mask 0x02 (routes to ALPHA) here, while retail's real `ConstructMesh` mask is 0x08 (routes to CLIP) — `GroupKey` does not retain the raw ClipMap bit needed to distinguish the two `AlphaBlend` origins, and threading it through `GfxObjSubMesh`/`GroupKey`/every mesh-classification call site was judged out of this bounded chunk's scope. | `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` (`MaskFromTranslucencyKind`'s own doc comment); `src/AcDream.Core/Meshing/TranslucencyKind.cs` (`FromSurfaceType`'s Translucent-override step) | Compositing-ORDER only (which FIFO list a subset enters), never a blend-mode or visual difference — the affected surface still renders alpha-blended exactly as retail's `SetSurface` configures it; only its position in the shared drain order can differ at a narrow overlap with other CLIP-list content. Extremely narrow: the only known example client-wide is the cloud GfxObj. | A translucent+clipmap surface (clouds being the only known instance) interleaves with ordinary ALPHA-list content instead of CLIP-list content at a rare compositing overlap. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority); `D3DPolyRender::SetSurface` 0x0059C4D0 (the Translucent-override blend selection acdream's `TranslucencyKind` already matches) | +| ~~AP-34~~ | **RETIRED 2026-09-04 (S4-c2 fix round 1) — residuals AP-238 / AP-239 / AP-240.** Historical: a single acdream-only distance-sorted alpha queue stood in for retail's two independent per-`DrawMesh`-invocation FIFO lists (CLIP/ALPHA, capacity 3000 each) drained at retail's four normal-world `FlushAlphaList` sites (`DrawBuilding`/`DrawBlock`/`PView::DrawCells`/`RenderNormalMode`). S4-c2 (`048d5b12f`) replaced it with `RetailAlphaQueue`'s exact two-list FIFO port of `D3DPolyRender::AddMeshToAlphaList`'s append-only/capacity-drop behavior, routed by `RetailAlphaMeshRouter`'s port of `DrawMesh`'s five-row branch table, under retail's exact no-op threshold rule — no acdream-only sort remains anywhere on the alpha path. | retired | — | — | `D3DPolyRender::AddMeshToAlphaList` 0x0059C230; `D3DPolyRender::FlushAlphaList` 0x0059D2E0; `D3DPolyRender::DrawMesh` 0x0059D4A0 | +| AP-238 | **S4-c2 (2026-09-04); NARROWED FURTHER at the S4-c2 fix round 1 (2026-09-04) — renumbered from the colliding id "AP-236" (that id was already filed AND retired on `main` by an unrelated #132 fix; ids are never reused, per A2).** One of TWO residuals AP-34's closure left open (the other is AP-240's CLIP-list-for-ordinary-content gap) — NOT "the ONE residual" as originally claimed. `RetailAlphaQueue` is retail's own two independent FIFO lists (CLIP/ALPHA), routed by `RetailAlphaMeshRouter`'s port of `DrawMesh`'s branch table and drained at retail's four normal-world `FlushAlphaList` sites. Fix round 1 (M6) then wired EnvCell's transparent shell into that SAME table: `RetailPViewPassExecutor.SubmitOrDrawTransparentCellShell` routes through `RetailAlphaMeshRouter.Route` with the shell's mask (always 0x02, alpha-family) — detail ON takes Row 1 (immediate, unchanged from before); detail OFF now takes Row 3 (Append) instead of drawing immediately, appending ONE token per CELL via the new `EnvCellAlphaDrawSource`, replayed through `EnvCellRenderer.RenderTransparentOrdered` at the queue's next flush. The residual is now PURELY the token GRANULARITY, not "never through the shared queue" (that claim is retired) — no "substantial separate architecture change" was needed; `RenderTransparentOrdered` already replayed a cell's transparent batch on demand. | `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs` (`SubmitOrDrawTransparentCellShell`, `EnvCellAlphaDrawSource`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` (`TransparentDetailEnabled`); `src/AcDream.App/Rendering/RetailAlphaQueue.cs`; `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` | EnvCell's per-cell transparent batch is homogeneously mask 0x02 (EnvCell shells never carry a ClipMap bit in this path), so one atomic per-cell FIFO token is consistent with how WbDrawDispatcher/ParticleRenderer already submit at instance granularity, not per-D3D-surface-subset; a particle or ordinary GfxObj instance appended between two cell tokens keeps its own position (the queue's adjacent-only batching invariant) — only interleaving BETWEEN two subsets of the SAME cell's own batch is unavailable, and nothing needs that today. | A single EnvCell's transparent shell (glass, water, grates) with multiple distinct subsets composites those subsets in strict draw-call order RELATIVE TO EACH OTHER (no visual change from before this round); the coarser per-cell token only matters if a future feature needs to interleave other content between two subsets of that SAME cell's own batch. | `RenderDeviceD3D::DrawEnvCell` 0x0059F170; `D3DPolyRender::AddMeshToAlphaList` 0x0059C230; `D3DPolyRender::FlushAlphaList` 0x0059D2E0 | +| AP-239 | **Filed 2026-09-04 (S4-c2); renumbered from the colliding id "AP-237" at the S4-c2 fix round 1 (2026-09-04, A2) — ids are never reused; the dangling "AP-236b" citation at `RetailAlphaMeshRouter.cs` (which never matched either filed id) is corrected to this one in the same fix.** `RetailAlphaMeshRouter.MaskFromTranslucencyKind` reconstructs retail's `ConstructMesh` queue-routing mask (spec: alpha-family bits always win; else ClipMap; else Translucent) from acdream's already-collapsed `TranslucencyKind`, which was built for a DIFFERENT priority chain (`TranslucencyKindExtensions.FromSurfaceType`'s blend-STATE selection: a "Translucent override" step checks Translucent+ClipMap-without-alpha-family FIRST and calls it `AlphaBlend`). A raw `Surface.Type` with Translucent(0x10) and Base1ClipMap(0x04) bits but no Alpha/InvAlpha/Additive bit therefore reconstructs to mask 0x02 (routes to ALPHA) here, while retail's real `ConstructMesh` mask is 0x08 (routes to CLIP) — `GroupKey` does not retain the raw ClipMap bit needed to distinguish the two `AlphaBlend` origins, and threading it through `GfxObjSubMesh`/`GroupKey`/every mesh-classification call site was judged out of this bounded chunk's scope. **S4-c2 fix round 1 (M7, 2026-09-04): the original cited example was DISPROVEN and replaced.** Cloud GfxObj `0x01004C35`'s surface `0x08000023` is `Type=0x10114` (ADDITIVE\|ALPHA\|TRANSLUCENT\|BASE1_CLIPMAP) — its alpha-family bits win in BOTH retail's real priority and this reconstruction, so it reproduces IDENTICALLY (mask 0x02 either way) and never demonstrated the gap. A 2026-09-04 scan of every Portal-dat `Surface` (6,152 scanned) found 27 REAL instances of Translucent+Base1ClipMap with no alpha-family bit (`Type=0x14`); cross-referencing the Portal-dat `GfxObj` table found surface `0x08000015` referenced by GfxObj `0x010001EC` — a real, confirmed instance, not the disproven cloud one. | `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` (`MaskFromTranslucencyKind`'s own doc comment); `src/AcDream.Core/Meshing/TranslucencyKind.cs` (`FromSurfaceType`'s Translucent-override step) | Compositing-ORDER only (which FIFO list a subset enters), never a blend-mode or visual difference — the affected surface still renders alpha-blended exactly as retail's `SetSurface` configures it; only its position in the shared drain order can differ at a narrow overlap with other CLIP-list content. 27 of 6,152 scanned surfaces (2026-09-04) carry this shape — narrow, but not as narrow as "only the cloud." | A translucent+clipmap surface (e.g. GfxObj `0x010001EC`) interleaves with ordinary ALPHA-list content instead of CLIP-list content at a rare compositing overlap. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority); `D3DPolyRender::SetSurface` 0x0059C4D0 (the Translucent-override blend selection acdream's `TranslucencyKind` already matches) | +| AP-240 | **Filed 2026-09-04 (S4-c2 fix round 1, M2).** `WbDrawDispatcher.IsOpaque(t) => t == Opaque \|\| t == ClipMap` (`WbDrawDispatcher.cs`) keeps every clip-mapped subset out of `_translucentDraws`/`SubmitWalkAlphaInstance`/`SubmitToAlphaQueue` entirely; retail's `ConstructMesh` gives clip-mapped surfaces mask 0x08 → `DrawMesh` row 3 → the CLIP list, and the S4-c2 capture evidence (six `*.alphadepth.log` files) shows 4,183 of 10,556 `AM` lines on CLIP (e.g. `(DrawBuilding,0,158,452)`: 158 real CLIP entries acdream's ordinary-content path reports as 0). Clip-mapped GfxObj/scenery/building subsets instead draw IMMEDIATELY on the `OpaqueAlphaToCoverage` pipeline (the pre-S4/VisualMaster-era translucency decision — not a pipeline change this round) and never enter retail's CLIP FIFO at all; the only CLIP-list feeders left after S4-c2 are clip-mapped MESH PARTICLES (M1's fix: `ParticleRenderer` applies no `IsOpaque`-style pre-filter, so a mesh particle classified `TranslucencyKind.ClipMap` genuinely reaches mask 0x08 and can append to CLIP). | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`IsOpaque`); `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` (`MaskClipMap` — the only reachable feeder is `ParticleRenderer`) | Depth-writing alpha-to-coverage cutouts composite correctly regardless of draw order for the overwhelming majority of clip-mapped content (doors, windows, foliage) — the divergence is compositing ORDER at a narrow overlap with genuine CLIP-list content (clip-mapped particles), not a missing/wrong cutout. Changing this is a VisualMaster-era pipeline decision, out of this bounded chunk's scope. | Clip-mapped GfxObj/scenery/building content composites in draw-call order relative to other opaque-pipeline content instead of interleaving through retail's deferred CLIP drain with clip-mapped particles/EnvCell content — edges differ only at a narrow overlap, never a missing cutout. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority, row 3 → CLIP); `D3DPolyRender::DrawMesh` 0x0059D4A0 (row 3); `D3DPolyRender::AddMeshToAlphaList` 0x0059C230 | | AP-36 | Dungeon streaming gate triggers on the player's CURRENT cell being a sealed EnvCell (`CurrCell.IsEnv && !SeenOutside`), an approximation of ACE's full landblock `IsDungeon` (all-heights-zero + NumCells>0 + Buildings.Count==0). The retail BEHAVIOR (a dungeon loads no adjacent landblocks) is faithful — only the runtime TRIGGER is the cheap cell predicate instead of classifying the center landblock. **#135 pre-collapse:** at login/teleport the same collapse is triggered EARLY (the instant the streaming center is recentered onto the spawn/dest cell) via `IsSealedDungeonCell` reading the EnvCell **dat** `SeenOutside` flag — because the physics `CurrCell` is null until placement, which waits for hydration; without the early trigger the full 25×25 ocean-grid window loads then unloads (the ~30 s login FPS ramp). **#215 cell identity:** the pre-collapse/recenter decision compares the player's current `Position.objcell_id` landblock with the received destination `objcell_id`; it never reconstructs the source from XYZ because dungeon frame origins may be negative. **#145/#138 teleport-hold suppression:** during a teleport arrival HOLD the player is unplaced, so `CurrCell` is the frozen SOURCE cell, not the destination; the gate is suppressed for the hold (`DungeonStreamingGate.Compute(isTeleportHold:true)` → not-inside-dungeon) so a teleport OUT of a dungeon follows the destination (the PortalSpace observer pin) and `ExitDungeonExpand`s, instead of re-pinning streaming onto the source dungeon (which left the outdoor destination un-hydrated → 600-frame readiness timeout → force-snap to ocean — the #145 "second teleport does nothing" + #138 incomplete-world) | `src/AcDream.App/Streaming/TeleportLandblockTransition.cs` (source/destination cell-ID classification) + `src/AcDream.App/Streaming/DungeonStreamingGate.cs` (`Compute` — per-frame predicate + teleport-hold suppression) + `src/AcDream.App/World/LiveEntityHydrationPorts.cs` (`LiveEntityWorldOriginCoordinator.TryInitialize` — login pre-collapse) + `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`OnPosition` — first accepted canonical Position) + `GameWindow:AimTeleportDestination`/`IsSealedDungeonCell` (teleport pre-collapse and DAT predicate) + `src/AcDream.App/Streaming/StreamingController.cs` (collapse/expand/`PreCollapseToDungeon`) | The predicate is already computed for sun/sky gating (playerInsideCell) and exactly matches for sealed dungeons vs windowed building interiors (SeenOutside=true → not gated); no landblock re-classification needed. The dat-flag read is the same `EnvCellFlags.SeenOutside` the hydrated `ObjCell.SeenOutside` is built from (`EnvCell.cs:72`/`PhysicsDataCache.cs:224`), so the pre-collapse decision matches the eventual per-frame gate exactly. The cell-ID comparison matches retail's complete `Position` flow. | A dungeon cell that reports SeenOutside (an entrance cell open to the surface) briefly un-collapses and re-streams the window; a hypothetical windowless building back-room (IsEnv && !SeenOutside but HasBuildings) would wrongly collapse its outdoor neighbors; a sealed-dungeon entrance cell that is itself SeenOutside is simply MISSED by the early trigger and falls back to the existing late collapse (no worse than before #135) | ACE `LandblockManager.GetAdjacentIDs` (dungeons→empty) Landblock.cs:577-582; `IsDungeon` Landblock.cs:1264-1277; retail `SmartBox::TeleportPlayer` 0x00453910 | | AP-43 | Per-object torch (point/spot) lighting AND sun are both gated on the OBJECT's own cell via the same `IndoorObjectReceivesTorches(ParentCellId)` predicate (`(id & 0xFFFF) >= 0x0100`): indoor objects (EnvCell-parented) get torches + NO sun; outdoor objects get the SUN + ambient + NO torches. This is the faithful per-draw port of retail's `useSunlight` gate — `DrawMeshInternal` (0x0059f398) calls `minimize_object_lighting` only `if (Render::useSunlight == 0)`, and `PView::DrawCells` (0x005a4840) calls `useSunlightSet(1)` (0x005a485a) for the outdoor stage and `useSunlightSet(0)` (0x005a49f3) for the interior-cell stage. **#142 (2026-06-20):** the sun gate is now PER-INSTANCE in the shader (binding=6 `instanceIndoor[]` flag in `mesh_modern.vert`, filled by `AppendCurrentLightSet`) — it was previously a per-FRAME global keyed on the PLAYER cell (`UpdateSunFromSky`). The per-frame global is retained for sealed dungeons (correctly kills the sun frame-wide when no sky is visible). **Residual:** the `ebp_2` second seen-outside test in `CellManager::ChangePosition` (0x004559B0) is unaudited — unclear whether it changes the ambient/sun regime for a subset of cells. No observed behavioral impact in tested cells. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`IndoorObjectReceivesTorches`, `ComputeEntityLightSet`, `AppendCurrentLightSet`, `_instIndoorSsbo`/`_indoorData`/`InstanceGroup.IndoorFlags`); `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` (binding=6 `instanceIndoor[]` gate on sun loop); per-frame sun `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameEnvironmentPreparation.UpdateSunFromSky`) | Torches: outdoor objects never torch-lit (exact retail). Sun: indoor objects (furniture, NPCs, player in a windowed building) never sun-lit (exact retail per-stage). Ambient: per-player-cell regime unchanged (exact retail `ChangePosition`). | The `ebp_2` unaudited test in `ChangePosition` could affect a narrow class of cells (entrance cells? sub-cells with special flags?) — no symptom observed; audit it if a lighting edge case arises in an unusual cell type | `useSunlight` gate `DrawMeshInternal` 0x0059f398; `useSunlightSet` 0x0054d450; per-stage `PView::DrawCells` 0x005a4840 (`useSunlightSet(1)` 0x005a485a / `useSunlightSet(0)` 0x005a49f3); `minimize_object_lighting` 0x0054d480; `CellManager::ChangePosition` 0x004559B0 (ambient + seen_outside) | | AP-35 | Point/spot lights are now PER-VERTEX Gouraud (`pointContribution` ~line 153 of `mesh_modern.vert`) matching retail's `SetStaticLightingVertexColors` bake path. Half-Lambert wrap (`(1/1.5)·(N·D + 0.5·d)`) AND norm distance attenuation (`distsq>1 ? distsq·d : d`) ARE ported (A7 Fix A, `aa94ced`). Point-light sum clamped to [0,1] on its own accumulator before adding ambient+sun (A7 Fix D D-1, mirrors retail's per-vertex bake clamp). CPU oracle: `src/AcDream.Core/Lighting/LightBake.cs`, locked by `tests/AcDream.Core.Tests/Lighting/LightBakeConformanceTests.cs`. **Residual (two parts):** (a) acdream lights in-shader each frame (per-frame GPU evaluate); retail bakes into the vertex buffer ONCE — an architecture/performance difference; the wrap + norm + clamp formula is the same, but bake-once is cheaper for static geometry; (b) acdream's `SelectForObject` keeps only the 8 NEAREST reaching point/spot lights per object/cell (`MaxLightsPerObject=8`, see AP-16), whereas retail's bake sums ALL reaching static lights per vertex — a surface reached by >8 point lights is dimmer in acdream than retail's bake result (rare in practice; a room has a handful of torches) | `src/AcDream.App/Rendering/Shaders/mesh_modern.vert` (`pointContribution` ~line 153; wrap ~line 163; norm ~line 167; point-sum clamp line 210) | Per-vertex Gouraud + wrap + norm + clamp all match retail. The two residuals are: (a) per-frame GPU vs bake-once — architecture/perf only; (b) 8-light cap dimming when >8 lights reach one surface — rare. `LightInfoLoader.cs:81` folds static_light_factor 1.3 into Range | (a) A new frame-time consumer bypassing `accumulateLights` would need to replicate the wrap + norm formula; per-frame GPU re-evaluate has higher per-frame cost than bake for static geometry. (b) A densely lit scene (>8 torches reaching one wall) renders dimmer than retail — see AP-16 for the 8-cap ownership | `calc_point_light` 0x0059c8b0 (line 0x0059c9a2 ramp; 0x0059c925 wrap); `SetStaticLightingVertexColors` 0x0059cfe0; static_light_factor 0x00820e24 | diff --git a/docs/architecture/worldbuilder-inventory.md b/docs/architecture/worldbuilder-inventory.md index e1552fef..9dd10462 100644 --- a/docs/architecture/worldbuilder-inventory.md +++ b/docs/architecture/worldbuilder-inventory.md @@ -416,8 +416,10 @@ mesh decoder, or second scene graph was introduced. Retail anchors: `RenderDeviceD3D::DrawObjCellForDummies` `0x005A0760`, `CShadowPart::insertion_sort` `0x006B5130`, `D3DPolyRender::AddMeshToAlphaList` `0x0059C230`, and -`D3DPolyRender::FlushAlphaList` `0x0059D2E0`. The modern per-cell-order and -EnvCell-shell residual is tracked explicitly as AP-34. WorldBuilder does not +`D3DPolyRender::FlushAlphaList` `0x0059D2E0`. AP-34 is RETIRED (S4-c2 fix +round 1, 2026-09-04) — retail's own two-list FIFO alpha queue now ports the +per-cell-order mechanism directly; the EnvCell-shell granularity residual +continues as AP-238 (see the register). WorldBuilder does not choose visible cells, portal slices, or cross-cell order: it receives the walk's ordered records and GPU clip slots and performs asset preparation plus order-preserving Vulkan batching only. diff --git a/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs b/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs index f4870ff9..5e65f138 100644 --- a/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs +++ b/src/AcDream.App/Rendering/ParticleRenderer.Rhi.cs @@ -370,6 +370,56 @@ public sealed unsafe partial class ParticleRenderer } } + /// + /// S4-c2 fix round 1 (M1c): DrawMesh rows 1/5's "render + /// immediately; enqueue nothing" outcome for ONE particle submission — + /// reuses the exact same per-instance billboard/mesh draw code + /// uses for its own runs (no new renderer), + /// applied to a single-element run so no sort is introduced (A8: retail's + /// immediate RenderMeshSubset draws in mesh/submission order — + /// this draws exactly this submission, right where DrawMesh was + /// called, never re-ordered against its neighbors). + /// + private void DrawImmediateParticleSubmissionRhi( + Matrix4x4 viewProjection, + ParticleSubmissionKind kind, + int drawIndex) + { + IGpuPassEncoder encoder = _scope!.RequireEncoder(); + IGpuFrame frame = RequireRhiFrame(); + + if (kind == ParticleSubmissionKind.Billboard) + { + ParticleDraw draw = _drawListScratch[drawIndex]; + _runScratch.Clear(); + _runScratch.Add(draw.Instance); + DrawInstancesRhi(encoder, frame, _runScratch, viewProjection, draw.Key.Additive); + return; + } + + GlobalMeshBuffer? global = _meshAdapter?.MeshManager?.GlobalBuffer; + if (!MeshParticlesAvailable || global is null) + return; + + MeshParticleDraw meshDraw = _meshDrawListScratch[drawIndex]; + if (_meshInstanceScratch.Length < 1) + _meshInstanceScratch = new MeshParticleGpuInstance[256]; + WriteMeshGpuInstance(ref _meshInstanceScratch[0], meshDraw.Instance); + GpuRingAllocation instances = WriteVertexRing( + frame, + _meshInstanceScratch.AsSpan(0, 1)); + DrawMeshBatchRhi( + encoder, + frame, + global, + meshDraw.Batch, + viewProjection, + instances.Buffer, + instances.OffsetBytes, + instanceCount: 1, + firstInstance: 0); + } + private void DrawInstancesRhi( IGpuPassEncoder encoder, IGpuFrame frame, diff --git a/src/AcDream.App/Rendering/ParticleRenderer.cs b/src/AcDream.App/Rendering/ParticleRenderer.cs index 868bd618..f32264b2 100644 --- a/src/AcDream.App/Rendering/ParticleRenderer.cs +++ b/src/AcDream.App/Rendering/ParticleRenderer.cs @@ -316,6 +316,64 @@ public sealed unsafe partial class ParticleRenderer : IDisposable DrawOrdered(camera); } + /// + /// S4-c2 fix round 1 (M1): pure router-input derivation for one particle + /// submission, extracted so it is directly unit-testable without a + /// GPU/mesh-manager harness. A billboard's blend mode (Additive bool) is + /// either the Additive or the plain Alpha surface bit — both live in + /// retail's alpha-family union (0x00010300) and construct the SAME queue + /// mask (0x02); billboards never carry a ClipMap bit (that is a mesh/ + /// UV-texture concept a generated particle quad has no equivalent of), + /// so they always route to ALPHA. A mesh-particle batch's + /// CAN be + /// (mask 0x00) — unlike WbDrawDispatcher's ordinary translucent- + /// GfxObj path, TryAppendMeshDraws appends every batch with + /// IndexCount>0 regardless of translucency classification, so + /// mask 0x00 is a real, reachable input here. + /// + /// M1(b): retail's has_alpha (CMaterial::CheckAlphaValues + /// @0x005396a0 — Ghidra-verified 2026-09-04: has_alpha=0 only when + /// Ambient.a==Diffuse.a==Specular.a==Emissive.a==1.0f, else has_alpha=1) + /// is driven, for a particle, by CMaterial::SetTranslucencySimple + /// @0x005396f0 — the only production writer reached from a particle's + /// live translucency (CPhysicsPart::SetTranslucency <- + /// Particle::Update @0x0051c290's own per-frame alpha-fade + /// interpolation) — which sets ALL FOUR alpha channels to the SAME + /// 1f - translucency value and then calls CheckAlphaValues. So + /// has_alpha for a particle's material is exactly "this particle's + /// CURRENT alpha isn't 1.0 (fully opaque)". acdream already carries that + /// exact interpolated value — Lerp(p.StartAlpha, p.EndAlpha, tLife), + /// baked into the top byte of by + /// Color32 (ParticleSystem.cs:1039-1041,1453) — so no new state is + /// needed. Billboards never reach row 4 (their mask 0x02 always + /// satisfies row 3), so only matters for + /// mesh particles. + /// + /// This "Scene" pass never draws during the Sky leaf (sky/off- + /// screen particles use the independent DrawOrdered path entirely, never + /// this method) and never installs a detail surface; MultiPassAlpha + /// stays false (Must Not: no environment override) — Row 2 is therefore + /// provably unreachable here. Rows 1/5 (Immediate) ARE reachable for an + /// Opaque-classified mesh-particle batch with no material alpha. + /// + internal static RetailAlphaMeshDecision RouteParticleSubmission( + ParticleSubmissionKind kind, TranslucencyKind meshTranslucency, uint meshColorArgb) + { + bool isMesh = kind == ParticleSubmissionKind.Mesh; + byte mask = kind == ParticleSubmissionKind.Billboard + ? RetailAlphaMeshRouter.MaskAlphaFamily + : RetailAlphaMeshRouter.MaskFromTranslucencyKind(meshTranslucency); + bool materialHasAlpha = isMesh && ((meshColorArgb >> 24) & 0xFFu) != 0xFFu; + + return RetailAlphaMeshRouter.Route( + currentlyDrawingSky: false, + delayMask: RetailAlphaMeshRouter.DefaultDelayMask, + detailSurfaceActive: false, + multiPassAlpha: false, + subsetMask: mask, + materialHasAlpha: materialHasAlpha); + } + private void DeferToRetailAlphaQueue(ICamera camera) { RetailAlphaQueue queue = _alphaQueue!; @@ -323,16 +381,6 @@ public sealed unsafe partial class ParticleRenderer : IDisposable for (int i = 0; i < _submissionScratch.Count; i++) { ParticleSubmission submission = _submissionScratch[i]; - // A billboard's blend mode (Additive bool) is either the - // Additive or the plain Alpha surface bit — both live in - // retail's alpha-family union (0x00010300) and construct the - // SAME queue mask (0x02); billboards never carry a ClipMap bit - // (that is a mesh/UV-texture concept a generated particle quad - // has no equivalent of), so they always route to ALPHA. - byte mask = submission.Kind == ParticleSubmissionKind.Billboard - ? RetailAlphaMeshRouter.MaskAlphaFamily - : RetailAlphaMeshRouter.MaskFromTranslucencyKind( - _meshDrawListScratch[submission.DrawIndex].Batch.Translucency); DeferredParticleDraw deferred = submission.Kind == ParticleSubmissionKind.Billboard ? new DeferredParticleDraw( @@ -346,31 +394,49 @@ public sealed unsafe partial class ParticleRenderer : IDisposable _meshDrawListScratch[submission.DrawIndex], viewProjection); - int token = _deferredAlpha.Count; - _deferredAlpha.Add(deferred); + RetailAlphaMeshDecision decision = submission.Kind == ParticleSubmissionKind.Billboard + ? RouteParticleSubmission(submission.Kind, default, default) + : RouteParticleSubmission( + submission.Kind, + _meshDrawListScratch[submission.DrawIndex].Batch.Translucency, + _meshDrawListScratch[submission.DrawIndex].Instance.ColorArgb); - // S4-c2: same unreachable-branch reasoning as - // WbDrawDispatcher.SubmitToAlphaQueue — this "Scene" pass never - // draws during the Sky leaf (sky/off-screen particles use the - // independent DrawOrdered path entirely, never this method) and - // never installs a detail surface; MultiPassAlpha stays false. - RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route( - currentlyDrawingSky: false, - delayMask: RetailAlphaMeshRouter.DefaultDelayMask, - detailSurfaceActive: false, - multiPassAlpha: false, - subsetMask: mask, - materialHasAlpha: false); - if (decision.Action != RetailAlphaMeshAction.Append) + switch (decision.Action) { - throw new InvalidOperationException( - "Scene particle submissions never draw during the Sky leaf or with a " - + "detail surface installed, and MultiPassAlpha stays false — DrawMesh's " - + $"row 1/2/4/5 branches are unreachable here; got {decision.Action}."); - } + case RetailAlphaMeshAction.Append: + { + int token = _deferredAlpha.Count; + _deferredAlpha.Add(deferred); + // Capacity overflow (spec §5): dropped, no recovery. + queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap); + break; + } - // Capacity overflow (spec §5): dropped, no recovery. - queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap); + case RetailAlphaMeshAction.Immediate: + // M1(c): rows 1/5 — draw NOW, through the same + // per-instance draw code DrawOrderedRhi uses for one + // submission (no new renderer). A8: no sort — retail's + // immediate RenderMeshSubset draws in mesh/submission + // order, so this draws exactly this submission, right + // where DrawMesh was called. + DrawImmediateParticleSubmissionRhi(viewProjection, submission.Kind, submission.DrawIndex); + break; + + case RetailAlphaMeshAction.AppendClipAndImmediate: + { + // Row 2: provably unreachable at this call site + // (MultiPassAlpha stays false), but a router row is a + // data-driven outcome, never an invariant to crash on + // (feedback_retail_dispatch_is_data_driven) — handled in + // full, exactly as spec §4 row 2 describes: append to + // CLIP AND ALSO draw immediately. + int token = _deferredAlpha.Count; + _deferredAlpha.Add(deferred); + queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap); + DrawImmediateParticleSubmissionRhi(viewProjection, submission.Kind, submission.DrawIndex); + break; + } + } } } diff --git a/src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs b/src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs index b567c0ed..633995e5 100644 --- a/src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs +++ b/src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs @@ -95,27 +95,44 @@ internal static class RetailAlphaMeshRouter /// spec's queue-routing mask from that already-collapsed classification. /// and /// are pre-filtered as "opaque" by WbDrawDispatcher.IsOpaque and - /// never actually reach a call site that invokes this — both map - /// defensively rather than throwing. + /// never reach THAT call site (both map defensively there rather than + /// throwing) — but S4-c2 fix round 1 (M1) found a SECOND call site, + /// ParticleRenderer.TryAppendMeshDraws/DeferToRetailAlphaQueue, + /// which applies NO such pre-filter: a mesh-particle batch's + /// classification is a real, + /// reachable input there, mapping to mask 0x00 — a data-driven + /// outcome the particle site now handles (immediate draw, or ALPHA via + /// the material-alpha fallback), never an invariant to assume away. /// - /// Known gap (register row AP-236b): + /// Known gap (register row AP-239, formerly filed as the + /// dangling "AP-236b" — corrected S4-c2 fix round 1, A2/M8c): /// 's /// "Translucent override" (raw Surface.Type has BOTH the /// Translucent (0x10) AND Base1ClipMap (0x04) bits, but no alpha-family - /// bit — e.g. cloud GfxObj 0x01004C35) resolves to - /// , discarding the raw ClipMap - /// bit. Retail's ConstructMesh would still see that ClipMap bit - /// and produce mask 0x08 (routes to CLIP); this reconstruction, - /// having only the collapsed , produces - /// mask 0x02 (routes to ALPHA) instead. GroupKey does not - /// carry the raw SurfaceType needed to distinguish the two - /// AlphaBlend origins, and plumbing it through would touch + /// bit) resolves to , discarding + /// the raw ClipMap bit. Retail's ConstructMesh would still see + /// that ClipMap bit and produce mask 0x08 (routes to CLIP); this + /// reconstruction, having only the collapsed , + /// produces mask 0x02 (routes to ALPHA) instead. GroupKey + /// does not carry the raw SurfaceType needed to distinguish the + /// two AlphaBlend origins, and plumbing it through would touch /// GfxObjSubMesh/GroupKey/every mesh-classification call - /// site — out of this chunk's bounded scope. The consequence is + /// site — out of this chunk's bounded scope. S4-c2 fix round 1 (M7) + /// DISPROVED this row's original cited example: cloud GfxObj + /// 0x01004C35's surface 0x08000023 is + /// Type=0x10114 (ADDITIVE|ALPHA|TRANSLUCENT|BASE1_CLIPMAP) — its + /// alpha-family bits win in BOTH retail's real priority AND this + /// reconstruction, so it reproduces identically (mask 0x02 either + /// way) and never demonstrated the gap at all. A 2026-09-04 scan of + /// every Portal-dat Surface (6,152 scanned) found 27 REAL + /// instances of Translucent+Base1ClipMap with no alpha-family bit + /// (Type=0x14) — e.g. surface 0x08000015, referenced by + /// GfxObj 0x010001EC — confirming the gap is real, just not via + /// the disproven cloud example. The consequence remains /// compositing-ORDER only (a queue-selection difference, not a blend/ - /// visual difference): a translucent+clipmap subset (in practice, only - /// clouds) can interleave with ordinary ALPHA-list content at a narrow - /// overlap instead of retail's CLIP-list interleave. + /// visual difference): an affected subset can interleave with ordinary + /// ALPHA-list content at a narrow overlap instead of retail's CLIP-list + /// interleave. /// internal static byte MaskFromTranslucencyKind(TranslucencyKind kind) => kind switch { diff --git a/src/AcDream.App/Rendering/RetailAlphaQueue.cs b/src/AcDream.App/Rendering/RetailAlphaQueue.cs index 9bc8fd71..7458c825 100644 --- a/src/AcDream.App/Rendering/RetailAlphaQueue.cs +++ b/src/AcDream.App/Rendering/RetailAlphaQueue.cs @@ -33,24 +33,32 @@ internal enum RetailAlphaList : byte /// Retail's four normal-world D3DPolyRender::FlushAlphaList /// (@0x0059d2e0) call sites (OH1 contract §7), each confirmed 2026-09-04 /// against docs/research/named-retail/symbols.json — the capture's -/// return address lies inside the named function's range: +/// return address lies inside the named function's range. S4-c2 fix round 1 +/// (M8b): every address below is the CALL INSTRUCTION's own address, not a +/// "return site" (the earlier landing's mislabel) — x86 CALL rel32 is 5 +/// bytes, so the actual return address is the call address + 5 (see +/// WalkAlphaDepthTrace.cs's own "ret" doc comment for the same +/// distinction on the capture side). /// /// RenderDeviceD3D::DrawBuilding -/// @0x0059f2a0, return site 0x0059f30b, threshold 0f. +/// @0x0059f2a0, call at 0x0059f30b (returns to 0x0059f310), threshold +/// 0f. /// — acdream's own name for -/// RenderDeviceD3D::DrawBlock @0x005a18d0's per-land-cell -/// FlushAlphaList(::flush), return site 0x005a1a07, threshold 0.75f +/// RenderDeviceD3D::DrawBlock @0x005a17c0's (per-land-cell loop head +/// @0x005a18d0) own FlushAlphaList(::flush), call at 0x005a1a07 +/// (returns to 0x005a1a0c), threshold 0.75f /// (the immutable global ::flush). There is no retail symbol to /// borrow for the walk's own leaf-event name — the walk expresses this turn /// as OnSortCellTurn/OnLandscapeCellTurn, not a "DrawBlock" /// hook. /// PView::DrawCells @0x005a4840, -/// return site 0x005a4872, threshold 0f. +/// call at 0x005a4872 (returns to 0x005a4877), threshold 0f. /// SmartBox::RenderNormalMode -/// @0x00453aa0, return site 0x00453b8b, threshold 0f. +/// @0x00453aa0, call at 0x00453b8b (returns to 0x00453b90), threshold +/// 0f. /// /// A fifth retail caller (the private CreatureMode::Render -/// @0x004529d0, return site 0x00452bf0) is out of the normal-world walk this +/// @0x004529d0, call at 0x00452bf0) is out of the normal-world walk this /// enum covers (OH1 contract §7) and has no acdream call site. /// internal enum RetailAlphaFlushSite @@ -84,20 +92,32 @@ internal interface IRetailAlphaDrawSource /// /// One retail AlphaListEntry. mirrors /// retail's per-entry clip byte (SetSurface's alpha-blend-vs- -/// clip-test arm selector); mirrors retail's -/// per-entry new byte (true only for the first entry appended to its -/// list since the list was last drained — retail captures the current -/// material/object matrix only for that entry). Neither drives observable -/// acdream behavior today: Vulkan's per-batch material/matrix binding makes -/// retail's per-entry material-capture optimization a no-op here (see -/// 's class doc comment), but both are stored -/// for fidelity and are directly test-observable. +/// clip-test arm selector). +/// +/// S4-c2 fix round 1 (M4): retail's per-entry new +/// ("first-for-this-list") byte is NOT modeled here. Retail's quantifier is +/// per-DrawMesh-INVOCATION: DrawMesh @0x0059d4a0 sets both +/// list flags true at ENTRY (0059d4cc arg3 = 1; 0059d4d0 var_c = 1) +/// and clears each independently after its OWN first append to that list +/// WITHIN THAT CALL (0059d5ef if (var_4_1 == 0) var_c = 0; else arg3 = +/// 0;) — one retail subset can append to only one list once, so this +/// flag is trivially true for every subset retail ever appends; the +/// captures confirm it (10,556 AM lines, new=1 on 9,685 of +/// them — impossible under "first since the last drain", which acdream's +/// deleted isFirstForList = target.Count == 0 computed instead). One +/// acdream append already IS one full DrawMesh invocation (one +/// instance, one call), so the truthful port of an always-true flag with no +/// acdream reader (Vulkan's per-batch material/matrix binding makes +/// retail's per-entry material-capture optimization a no-op here — see +/// 's class doc comment) is to not carry it at +/// all, rather than keep a field whose value can never vary and whose only +/// possible test is therefore vacuous +/// (feedback_every_new_pin_must_be_shown_to_fail). /// internal readonly record struct RetailAlphaEntry( IRetailAlphaDrawSource Source, int Token, - bool OverrideClipmap, - bool IsFirstForList); + bool OverrideClipmap); /// /// Frame-scoped port of retail's shared D3DPolyRender alpha lists. @@ -199,9 +219,10 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame /// at , return and the /// subset is DROPPED — no recovery, no fallback draw (spec §5). The /// caller ('s decision) supplies - /// ; IsFirstForList is computed - /// here from whether was empty before this - /// append. + /// . Retail's per-entry "first for + /// this list" flag is not modeled — see 's + /// own doc comment (M4) for why it is trivially true for every subset + /// retail ever appends and therefore carries no information here. /// internal bool TryAppend( RetailAlphaList list, @@ -219,8 +240,7 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame if (target.Count >= ListCapacity) return false; - bool isFirstForList = target.Count == 0; - target.Add(new RetailAlphaEntry(source, token, overrideClipmap, isFirstForList)); + target.Add(new RetailAlphaEntry(source, token, overrideClipmap)); RegisterSource(source); return true; } @@ -313,6 +333,11 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame { int observedClip = _clip.Count; int observedAlpha = _alpha.Count; + // S4-c2 fix round 1 (A4): the real SOURCE count, captured before + // Clear() — passing the entry count here (HEAD~1's regression) + // means _sources/_sourceDrawOffsets never shrink, since the + // entry count is always >= the source count. + int observedSources = _sources.Count; for (int i = 0; i < _sources.Count; i++) { try @@ -327,7 +352,7 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame _sources.Clear(); _clip.Clear(); _alpha.Clear(); - ApplyScratchRetention(observedClip + observedAlpha, observedClip + observedAlpha); + ApplyScratchRetention(observedClip + observedAlpha, observedSources); } if (drawFailure is not null) @@ -401,11 +426,14 @@ internal sealed class RetailAlphaQueue : IWorldSceneAlphaFrame { int observedClip = _clip.Count; int observedAlpha = _alpha.Count; + // S4-c2 fix round 1 (A4): see DrainAndReset's own comment — the + // real source count, captured before Clear(). + int observedSources = _sources.Count; _sources.Clear(); _clip.Clear(); _alpha.Clear(); IsCollecting = false; - ApplyScratchRetention(observedClip + observedAlpha, observedClip + observedAlpha); + ApplyScratchRetention(observedClip + observedAlpha, observedSources); } if (failures is { Count: > 0 }) diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs index f4e1d980..3abfda1c 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs @@ -204,7 +204,6 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer private Action _clearInteriorDepth = null!; private Func _drawExitSeals = null!; private readonly HashSet _singleCellScratch = new(); - private readonly List _singleCellListScratch = new(); internal WalkProductionLeafRenderer( RetailPViewPassExecutor passes, @@ -261,9 +260,10 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer _passes.DrawOpaqueCellShells(_singleCellScratch); if (_passes.CellHasTransparentShell(cellId)) { - _singleCellListScratch.Clear(); - _singleCellListScratch.Add(cellId); - _passes.DrawTransparentCellShellsOrdered(_singleCellListScratch); + // S4-c2 fix round 1 (M6): drawn now (with detail) or appended to + // the retail alpha queue for later replay (without detail) — + // see SubmitOrDrawTransparentCellShell's own doc comment. + _passes.SubmitOrDrawTransparentCellShell(cellId); } } diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs index 5285313f..0c8241ef 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs @@ -164,6 +164,106 @@ public RetailPViewPassExecutor( public void DrawTransparentCellShellsOrdered(IReadOnlyList cellIds) => _envCells.RenderTransparentOrdered(cellIds); + /// + /// S4-c2 fix round 1 (M6, contract C4): retail DrawEnvCell + /// @0x0059f1c2 installs the environment detail surface (may be null) + /// before DrawMesh @0x0059f212 and clears it @0x0059f21a — detail + /// ON makes the subset take Row 1 (immediate, WITH detail — + /// 's own doc + /// comment); detail OFF takes Row 3 (append; mask 0x02 alpha-family for + /// an EnvCell transparent shell surface). Routed through the SAME table + /// every other alpha submitter uses so the decision is the table's, not + /// a hand-coded branch. + /// already replays a cell's transparent batch on demand, so the queue + /// side () needs no new deferred- + /// replay abstraction — one token per CELL (coarser than retail's own + /// per-subset entries; AP-236's residual documents that granularity gap). + /// + internal void SubmitOrDrawTransparentCellShell(uint cellId) + { + RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route( + currentlyDrawingSky: false, + delayMask: RetailAlphaMeshRouter.DefaultDelayMask, + detailSurfaceActive: _envCells.TransparentDetailEnabled, + multiPassAlpha: false, + subsetMask: RetailAlphaMeshRouter.MaskAlphaFamily, + materialHasAlpha: false); + + if (decision.Action == RetailAlphaMeshAction.Immediate) + { + _singleCellListScratch.Clear(); + _singleCellListScratch.Add(cellId); + _envCells.RenderTransparentOrdered(_singleCellListScratch); + return; + } + + // Row 2 (AppendClipAndImmediate) needs MultiPassAlpha, fixed false + // above — provably unreachable, same reasoning as + // WbDrawDispatcher.SubmitToAlphaQueue's ordinary-content case. + _envCellAlphaSource ??= new EnvCellAlphaDrawSource(_envCells.RenderTransparentOrdered); + int token = _envCellAlphaSource.AddPendingCellId(cellId); + _alpha.TryAppend(decision.List, _envCellAlphaSource, token, decision.OverrideClipmap); + } + + private EnvCellAlphaDrawSource? _envCellAlphaSource; + + private readonly List _singleCellListScratch = new(1); + + /// + /// The queue-facing half of : + /// retains the cell ids submitted this frame in append order and replays + /// a drained slice through + /// (production wiring) — one token per cell, never per subset (M6's + /// documented granularity residual). 's + /// "only adjacent same-source entries batch" invariant means a particle + /// or ordinary GfxObj instance appended BETWEEN two cell tokens keeps + /// its own position in the combined drain — this source never + /// re-orders across another entry. Depends on a delegate rather than + /// the concrete type (marked + /// , not ) so + /// EnvCellAlphaDrawSourceTests can pin the queue mechanics + /// without standing up a GPU-backed renderer. + /// + internal sealed class EnvCellAlphaDrawSource(Action> renderTransparentOrdered) + : IRetailAlphaDrawSource + { + private readonly List _pendingCellIds = new(); + private readonly List _preparedCellIds = new(); + private readonly List _drawScratch = new(); + + /// Returns this cell's token — its index into + /// at the moment it was added. + internal int AddPendingCellId(uint cellId) + { + int token = _pendingCellIds.Count; + _pendingCellIds.Add(cellId); + return token; + } + + public void PrepareAlphaDraws(ReadOnlySpan tokens) + { + _preparedCellIds.Clear(); + for (int i = 0; i < tokens.Length; i++) + _preparedCellIds.Add(_pendingCellIds[tokens[i]]); + } + + public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) + { + if (drawCount <= 0) + return; + _drawScratch.Clear(); + for (int i = 0; i < drawCount; i++) + _drawScratch.Add(_preparedCellIds[firstPreparedDraw + i]); + renderTransparentOrdered(_drawScratch); + } + + public void ResetAlphaSubmissions() + { + _pendingCellIds.Clear(); + _preparedCellIds.Clear(); + } + } + /// /// S3 chunk 1 fix round 2 (§11.6 H1): retail draws the weather pass /// EXACTLY ONCE per frame — GameSky::Draw @0x00506ff0 with @@ -347,9 +447,10 @@ public RetailPViewPassExecutor( internal void FlushBuildingAlpha() => _alpha.Flush(RetailAlphaFlushSite.DrawBuilding, 0f); - /// RenderDeviceD3D::DrawBlock @0x005a18d0's per-land-cell - /// FlushAlphaList(::flush) @0x005a1a07, the immutable global - /// 0.75f pressure valve (OH1 contract §7 site 2). + /// RenderDeviceD3D::DrawBlock @0x005a17c0's (per-land-cell + /// loop head @0x005a18d0) own FlushAlphaList(::flush) @0x005a1a07, + /// the immutable global 0.75f pressure valve (OH1 contract §7 site + /// 2). internal void FlushSortCellExitAlpha() => _alpha.Flush(RetailAlphaFlushSite.SortCellExit, 0.75f); diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 290fee08..d47641e7 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -473,16 +473,35 @@ internal sealed class RetailPViewRenderer ClearWalkFrameBindings(); } - // OUTDOOR root: the LScape-boundary alpha drain deferred from the - // landscape stage runs HERE, after punches, interior shells, cell - // objects, and the dynamics pass — the frame's complete opaque - // world. Retail's walk draws all of those before its boundary - // flush (LScape::draw includes every cell's objects, - // DrawSortCell 0x005A17C0), so this is the same one-list far→near - // composite over finished depth; draining at the stage end instead - // let every later opaque mesh overwrite the flames (#132). - if (ctx.RootCell.IsOutdoorNode) - passes.FlushLandscapeAlpha(); + // S4-c2 fix round 1 (A1): the call that used to sit HERE + // (`passes.FlushLandscapeAlpha()`, an outdoor-root-only drain + // labelled LandscapeFlush) is DELETED — it was mislabelled. + // Retail's LandscapeFlush (`PView::DrawCells` @0x005a4840's own + // `FlushAlphaList(0f)` @0x005a4872) sits strictly INSIDE + // `if (outside_view.view_count > 0)` on the INTERIOR PView + // (WalkEvents.cs's own `OnInteriorFloodDrawTurn` doc comment); + // an outdoor root never reaches that branch at all — confirmed + // by the terrace-edge capture (zero `005a4877` FL lines) and by + // `RetailAlphaQueue`'s own flush-site enum, whose + // `LandscapeFlush` member cites that exact address. Retail's + // real outdoor-root drains are `DrawBlock`'s per-cell 0.75 valve + // (`WalkFrameEventKind.SortCellExit`, already wired) and + // `SmartBox::RenderNormalMode`'s own `FlushAlphaList(0f)` + // @0x00453b8b at the pass end — which acdream already performs + // unconditionally in `RetailAlphaQueue.EndFrame()` (called from + // `WorldSceneRenderer.CompleteWorldFrame()`). Between this + // method's `return result;` below and that `EndFrame()` call, + // `WorldSceneRenderer.Render` performs zero further opaque-world + // draws for a PView-rooted frame — `DrawPostWorldParticles`'s + // own PView branch is an explicit no-op ("already drew inside + // LScape::draw... replaying here would be too late"), and the + // diagnostics that follow only emit collision wireframes (a + // debug-only probe, off by default) — so #132's "drain after + // the finished opaque world" invariant is kept by the + // RenderNormalMode flush alone, and `DrawUnattachedSceneParticles` + // below joins the SAME final drain retail's own outdoor root + // would produce (never an extra LandscapeFlush call retail + // never makes there). // Interior-cell UNATTACHED emitters (spell ground effects and // swirls anchored in EnvCells) draw in this final world scope — diff --git a/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs index 942fc545..d880ac47 100644 --- a/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs +++ b/src/AcDream.App/Rendering/Walk/RetailFrameWalk.cs @@ -240,8 +240,9 @@ public sealed class RetailFrameWalk block.SideCellCount, cellIndex); - // S4-c2: RenderDeviceD3D::DrawBlock @0x005a18d0's own - // FlushAlphaList(::flush) @0x005a1a07 call, immediately after + // S4-c2: RenderDeviceD3D::DrawBlock @0x005a17c0's (per-land-cell + // loop head @0x005a18d0) own FlushAlphaList(::flush) + // @0x005a1a07 call, immediately after // DrawSortCell under the SAME alwaysDrawObjects||cellInView // gate that already wraps this whole loop body (the early // `continue` above) — see OnSortCellExit's own doc comment. diff --git a/src/AcDream.App/Rendering/Walk/WalkEvents.cs b/src/AcDream.App/Rendering/Walk/WalkEvents.cs index 42f2bc52..7f1ae7a6 100644 --- a/src/AcDream.App/Rendering/Walk/WalkEvents.cs +++ b/src/AcDream.App/Rendering/Walk/WalkEvents.cs @@ -155,8 +155,8 @@ public interface IWalkEventSink /// that cell's turn (the /// object-list turn, itself after the building's own turn) and before /// the loop moves to the next cell's — - /// RenderDeviceD3D::DrawBlock @0x005a18d0's own - /// FlushAlphaList(::flush) @0x005a1a07 call, gated by the exact + /// RenderDeviceD3D::DrawBlock @0x005a17c0's (per-land-cell loop + /// head @0x005a18d0) own FlushAlphaList(::flush) @0x005a1a07 call, gated by the exact /// same alwaysDrawObjects || cellInView condition as /// DrawSortCell itself (Ghidra-verified 2026-09-04: retail /// re-evaluates the identical condition for the flush call, immediately diff --git a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs index cf0365aa..4282f6bc 100644 --- a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs +++ b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs @@ -238,8 +238,9 @@ internal interface IWalkFrameLeafRenderer /// synthetic viewer-distance threshold. void AlphaBarrier(); - /// S4-c2: retail RenderDeviceD3D::DrawBlock @0x005a18d0's - /// per-land-cell FlushAlphaList(::flush) @0x005a1a07 — the + /// S4-c2: retail RenderDeviceD3D::DrawBlock @0x005a17c0's + /// (per-land-cell loop head @0x005a18d0) own FlushAlphaList(::flush) + /// @0x005a1a07 — the /// immutable global 0.75f pressure valve, inert below 2250 entries in /// either list (a no-op the overwhelming majority of the time at /// today's scene complexity) but pinned exact by the capacity/threshold @@ -377,8 +378,8 @@ internal enum WalkFrameEventKind : byte CellParticles, /// S4-c2: — - /// retail RenderDeviceD3D::DrawBlock @0x005a18d0's per-land-cell - /// FlushAlphaList(0.75f). + /// retail RenderDeviceD3D::DrawBlock @0x005a17c0's (per-land-cell + /// loop head @0x005a18d0) own FlushAlphaList(0.75f). SortCellExit, } @@ -1475,8 +1476,9 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource _events.Add(WalkFrameEvent.LandscapeCellParticles(cellId)); } - /// S4-c2: retail RenderDeviceD3D::DrawBlock @0x005a18d0's - /// per-land-cell FlushAlphaList(::flush) @0x005a1a07 (the 0.75f + /// S4-c2: retail RenderDeviceD3D::DrawBlock @0x005a17c0's + /// (per-land-cell loop head @0x005a18d0) own FlushAlphaList(::flush) + /// @0x005a1a07 (the 0.75f /// pressure valve) — fires once per admitted land-block cell, at the SAME /// gate /// already fires under (RetailFrameWalk.DrawLandscape calls this diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs index dc4842ee..5524019b 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs @@ -32,6 +32,20 @@ public sealed unsafe partial class EnvCellRenderer private readonly TerrainAtlas.RetailDetailTextureBinding _environmentDetail; private readonly Func _buildingDetailEnabled; + /// + /// S4-c2 fix round 1 (M6): the SAME detailEnabled test the + /// transparent-shell MDI path (line ~254 below) already evaluates at + /// draw time — retail DrawEnvCell @0x0059f1c2 installs the + /// environment detail surface (may be null) before DrawMesh + /// @0x0059f212 and clears it @0x0059f21a, so a non-null environment + /// detail is what makes an eligible transparent subset take Row 1 + /// (immediate). reads this to + /// decide whether a cell's transparent shell draws now or is appended + /// to the retail alpha queue for later replay. + /// + internal bool TransparentDetailEnabled => + RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _environmentDetail); + /// /// The RHI arm's constructor. It also completes Initialize's job: the /// five pipelines ARE this renderer's program, so there is no second step diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs index 47051004..1c26d668 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs @@ -587,21 +587,25 @@ public sealed unsafe partial class WbDrawDispatcher checked((uint)(count * DrawCommandStride))); } - private void DrawPreparedAlphaBatchRhi( + /// + /// The push-constant/pipeline/storage-binding setup 's + /// drain and 's single- + /// instance immediate draw (S4-c2 fix round 1, M3) share — the only + /// difference between the two call sites is + /// (the drain reads the deferred scope's own _deferredAlphaViewProjection; + /// an immediate draw uses whatever the CURRENT frame's viewProjection + /// is) and whether a building-detail overlay pass follows. + /// + private GpuPushConstants BindAlphaDrawState( + IGpuPassEncoder encoder, + IGpuFrame frame, GlobalMeshBuffer mesh, - int firstPreparedDraw, - int drawCount) + Matrix4x4 viewProjection, + out MeshPipelineSet pipelines) { - if (_alphaCommands.Buffer is null) - return; - - IWorldPassScope scope = _scope!; - IGpuPassEncoder encoder = scope.RequireEncoder(); - IGpuFrame frame = RequireRhiFrame(); - var pushConstants = new GpuPushConstants { - ViewProjection = _deferredAlphaViewProjection, + ViewProjection = viewProjection, DrawIdOffset = 0, LightingMode = 0, RenderPass = 1, @@ -612,7 +616,7 @@ public sealed unsafe partial class WbDrawDispatcher ParamB = 0f, }; - MeshPipelineSet pipelines = PipelinesFor( + pipelines = PipelinesFor( encoder, frame, out DirectionalShadowFrameBinding shadowBinding); @@ -635,13 +639,34 @@ public sealed unsafe partial class WbDrawDispatcher GpuBindingModel.StorageInstanceDetailCategory, _alphaDetailCategory); AcDream.App.Rendering.WorldFrameSectionBinding.BindClipRegions( - encoder, scope.Sections, frame); + encoder, _scope!.Sections, frame); AcDream.App.Rendering.WorldFrameSectionBinding.BindSceneLighting( - encoder, scope.Sections, frame); + encoder, _scope!.Sections, frame); + return pushConstants; + } + + /// + /// S4-c2 fix round 1 (M3c): delayed replay is ALWAYS detailEnabled=0 + /// (OH1 contract §4/§5 — FlushAlphaList's replay calls + /// RenderMeshSubset(..., 0, entry.clip, ...)) because every + /// detail-eligible subset now takes Row 1 immediate instead + /// (SubmitToAlphaQueue's detailSurfaceActive gate) and + /// never reaches this drain at all — so this loop batches purely by + /// blend kind, with no per-command detail-category branch left. + /// + private void DrawPreparedAlphaBatchRhi( + GlobalMeshBuffer mesh, + int firstPreparedDraw, + int drawCount) + { + if (_alphaCommands.Buffer is null) + return; + + IGpuPassEncoder encoder = _scope!.RequireEncoder(); + IGpuFrame frame = RequireRhiFrame(); + GpuPushConstants pushConstants = BindAlphaDrawState( + encoder, frame, mesh, _deferredAlphaViewProjection, out MeshPipelineSet pipelines); - bool detailEnabled = RetailDetailTextureContract.ShouldRender( - _buildingDetailEnabled(), - _buildingDetail); if (firstPreparedDraw < 0 || drawCount < 0 || firstPreparedDraw > _preparedAlphaInstanceCount - drawCount) @@ -650,25 +675,14 @@ public sealed unsafe partial class WbDrawDispatcher nameof(firstPreparedDraw), "The prepared-alpha draw range exceeds its uploaded instance/category payload."); } - ReadOnlySpan usedDetailCategories = - _detailCategoryData.AsSpan(0, _preparedAlphaInstanceCount); + int runStart = firstPreparedDraw; int preparedEnd = firstPreparedDraw + drawCount; while (runStart < preparedEnd) { TranslucencyKind blend = _deferredAlphaKinds[runStart]; - bool hasDetail = detailEnabled - && CommandContainsDetailCategory( - _indirectCommands[runStart], - usedDetailCategories); int runEnd = runStart + 1; - while (runEnd < preparedEnd - && !hasDetail - && _deferredAlphaKinds[runEnd] == blend - && (!detailEnabled - || !CommandContainsDetailCategory( - _indirectCommands[runEnd], - usedDetailCategories))) + while (runEnd < preparedEnd && _deferredAlphaKinds[runEnd] == blend) runEnd++; // ApplyRetailBlend's three cases are three pipelines, including the @@ -682,23 +696,55 @@ public sealed unsafe partial class WbDrawDispatcher _alphaCommands.OffsetBytes, runStart, runEnd - runStart); - - if (hasDetail) - { - DrawBuildingDetailRangeRhi( - encoder, - mesh, - pipelines.RetailDetailTransparent, - ref pushConstants, - _alphaCommands.Buffer!, - _alphaCommands.OffsetBytes, - runStart, - 1); - } runStart = runEnd; } } + /// + /// S4-c2 fix round 1 (M3b): DrawMesh row 1's "render immediately; + /// enqueue nothing" outcome for ONE translucent building-shell instance + /// under building detail — bind the blend pipeline, one indirect draw, + /// then the detail pass, reusing and + /// (the same code + /// used to use) for the single + /// slot just + /// prepared. The caller only reaches this method when + /// detailSurfaceActive was true, so the detail pass always + /// applies here — the drain's own per-command detail CHECK is gone + /// (M3c) precisely because detail-eligible content no longer reaches it. + /// + private void DrawImmediateAlphaInstanceRhi( + GlobalMeshBuffer mesh, TranslucencyKind blend, Matrix4x4 viewProjection) + { + if (_alphaCommands.Buffer is null) + return; + + IGpuPassEncoder encoder = _scope!.RequireEncoder(); + IGpuFrame frame = RequireRhiFrame(); + GpuPushConstants pushConstants = BindAlphaDrawState( + encoder, frame, mesh, viewProjection, out MeshPipelineSet pipelines); + + BindPipelineWithMesh(encoder, PipelineForBlend(pipelines, blend), mesh); + encoder.SetPushConstants(in pushConstants); + DrawIndirectRangeRhi( + encoder, + ref pushConstants, + _alphaCommands.Buffer!, + _alphaCommands.OffsetBytes, + startCommand: 0, + commandCount: 1); + + DrawBuildingDetailRangeRhi( + encoder, + mesh, + pipelines.RetailDetailTransparent, + ref pushConstants, + _alphaCommands.Buffer!, + _alphaCommands.OffsetBytes, + firstCommand: 0, + commandCount: 1); + } + private void DrawImmediateTransparentRhi( IGpuPassEncoder encoder, GlobalMeshBuffer mesh, diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs index 1b6b5de5..32ed5467 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs @@ -903,10 +903,10 @@ public sealed partial class WbDrawDispatcher throw new InvalidOperationException( "One retail alpha scope cannot combine different view-projection matrices."); - int token = _deferredAlpha.Count; - _deferredAlpha.Add(new DeferredAlphaInstance( + var candidate = new DeferredAlphaInstance( batch.Key, batch.Transform, batch.ClipSlot, batch.Lights, - batch.IndoorFlag, batch.DetailCategory, batch.Alpha, batch.SelectionLighting)); - SubmitToAlphaQueue(queue, batch.Key.Translucency, token); + batch.IndoorFlag, batch.DetailCategory, batch.Alpha, batch.SelectionLighting); + SubmitToAlphaQueue( + queue, batch.Key.Translucency, in candidate, batch.DetailCategory == 1u, viewProjection); } } diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 955c1882..952eb46b 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -2445,8 +2445,7 @@ public sealed partial class WbDrawDispatcher : IDisposable { InstanceGroup group = entry.Group; int i = entry.InstanceIndex; - int token = _deferredAlpha.Count; - _deferredAlpha.Add(new DeferredAlphaInstance( + var candidate = new DeferredAlphaInstance( ToKey(group), group.Matrices[i], group.Slots[i], @@ -2454,8 +2453,9 @@ public sealed partial class WbDrawDispatcher : IDisposable group.IndoorFlags[i], group.DetailCategories[i], group.Opacities[i], - group.SelectionLighting[i])); - SubmitToAlphaQueue(queue, group.Translucency, token); + group.SelectionLighting[i]); + SubmitToAlphaQueue( + queue, group.Translucency, in candidate, group.DetailCategories[i] == 1u, viewProjection); } } @@ -2466,40 +2466,71 @@ public sealed partial class WbDrawDispatcher : IDisposable /// clip-map subsets are filtered out upstream by IsOpaque and /// never reach _translucentDraws/this path at all — see /// 's own doc - /// comment). Neither call site ever draws during the Sky leaf or with a - /// building/environment detail surface installed (ordinary GfxObj/ - /// particle paths never install one — OH1 contract §6), and + /// comment). Neither call site ever draws during the Sky leaf and /// MultiPassAlpha stays false (no environment override — Must - /// Not). Under those fixed inputs and , - /// every reachable mask (0x02/0x03, 0x04/0x05, 0x08/0x09) intersects the - /// delay mask, so DrawMesh's row 3 always fires — rows 1, 2, 4, - /// and 5 are unreachable here and would indicate a real routing bug if - /// ever hit. + /// Not) — Row 2 is provably unreachable. Under + /// the only + /// reachable mask here is 0x02 (alpha-family), which always intersects + /// the delay mask, so an ordinary (non-building-shell) instance always + /// takes Row 3 (Append). + /// + /// S4-c2 fix round 1 (M3): feeds + /// Row 1's curr_detail_surface != null gate — retail's + /// DrawBuilding @0x0059f2a0 installs the building detail surface + /// BEFORE its own shell subsets reach DrawMesh + /// (0059f2eb curr_detail_surface = building_detail_surface), so a + /// translucent building-shell instance takes Row 1 (immediate, WITH + /// detail) whenever building detail is on. This IS therefore a real, + /// data-driven outcome — not an invariant to throw on + /// (feedback_retail_dispatch_is_data_driven) — handled below by drawing + /// the instance immediately instead of appending it. /// private void SubmitToAlphaQueue( - RetailAlphaQueue queue, TranslucencyKind kind, int token) + RetailAlphaQueue queue, + TranslucencyKind kind, + in DeferredAlphaInstance candidate, + bool isBuildingShell, + Matrix4x4 viewProjection) { byte mask = RetailAlphaMeshRouter.MaskFromTranslucencyKind(kind); + bool detailSurfaceActive = isBuildingShell + && RetailDetailTextureContract.ShouldRender(_buildingDetailEnabled(), _buildingDetail); RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route( currentlyDrawingSky: false, delayMask: RetailAlphaMeshRouter.DefaultDelayMask, - detailSurfaceActive: false, + detailSurfaceActive: detailSurfaceActive, multiPassAlpha: false, subsetMask: mask, materialHasAlpha: false); - if (decision.Action != RetailAlphaMeshAction.Append) + if (decision.Action == RetailAlphaMeshAction.Immediate) { - throw new InvalidOperationException( - "Ordinary translucent GfxObj/particle submissions never install a detail " - + "surface or draw during the Sky leaf, and MultiPassAlpha stays false — " - + $"DrawMesh's row 1/2/4/5 branches are unreachable here; got {decision.Action}."); + // M3(b): row 1 — draw NOW, at this instance's own turn, and + // never touch _deferredAlpha/the queue at all (so a frame whose + // translucent content is ENTIRELY immediate — e.g. every visible + // shell drawing with building detail on — still leaves nothing + // for the queue to clear; only Append/AppendClipAndImmediate, + // below, ever grow _deferredAlpha). + DrawImmediateAlphaInstance(in candidate, viewProjection); + return; } + int token = _deferredAlpha.Count; + _deferredAlpha.Add(candidate); // Capacity overflow (spec §5): TryAppend returns false and the // subset is DROPPED. The reserved _deferredAlpha token simply never // gets prepared/drawn — no recovery, matching retail exactly. queue.TryAppend(decision.List, _alphaSource, token, decision.OverrideClipmap); + + if (decision.Action == RetailAlphaMeshAction.AppendClipAndImmediate) + { + // Row 2: provably unreachable here (MultiPassAlpha stays false), + // but handled in full rather than asserted against — exactly as + // spec §4 row 2 describes: append to CLIP AND ALSO draw + // immediately. + DrawImmediateAlphaInstance(in candidate, viewProjection); + } + } private sealed class AlphaSubmissionOrderComparer : @@ -2518,6 +2549,59 @@ public sealed partial class WbDrawDispatcher : IDisposable left.SubmissionOrder.CompareTo(right.SubmissionOrder); } + /// + /// Writes ONE into the CPU staging + /// arrays at — the per-entry body + /// 's drain loop and + /// 's single-instance immediate + /// path (S4-c2 fix round 1, M3) both share, so the two paths can never + /// disagree on how a becomes GPU + /// data. Caller ensures capacity first. + /// + private void WriteDeferredAlphaEntrySlot(int slot, in DeferredAlphaInstance entry) + { + WriteMatrix(_instanceData, slot * 16, entry.Model); + _clipSlotData[slot] = entry.ClipSlot; + _indoorData[slot] = entry.Indoor; + _detailCategoryData[slot] = entry.DetailCategory; + _alphaData[slot] = entry.Opacity; + _selectionLightingData[slot] = entry.SelectionLighting; + int lightOffset = slot * LightManager.MaxLightsPerObject; + entry.Lights.CopyTo(_lightSetData, lightOffset); + + GroupKey key = entry.Key; + _batchData[slot] = new BatchData + { + // Campaign V slice V2: table slot, not the raw handle. + TextureIndex = key.TextureSlot.Index, + TextureLayer = key.TextureLayer, + // DrawMesh invokes RenderMeshSubset with detail enabled for + // every built-mesh material subset while curr_detail_surface + // is installed. The per-instance category still rejects + // ordinary objects in mesh_detail. Review fix round 2 (F2): + // bit 0 (the #226 built-mesh marker) is unconditional here — + // every delayed-alpha entry IS a built-mesh alpha submission + // — but the previous hardcoded `1` silently dropped bits 1/2 + // (Campaign VM VM6 foliage classification). A trunk instance + // promoted into the alpha-blend group mid-fade (translucency + // < 1 forces AlphaBlend even for an otherwise-Opaque trunk — + // see ClassifyBatches's #188 promotion) must keep swaying + // through the deferred-alpha replay instead of going rigid + // for the duration of its fade. + Flags = 1u | key.FoliageFlags, + }; + _indirectCommands[slot] = new DrawElementsIndirectCommand + { + Count = (uint)key.IndexCount, + InstanceCount = 1, + FirstIndex = key.FirstIndex, + BaseVertex = key.BaseVertex, + BaseInstance = (uint)slot, + }; + _drawCullModes[slot] = key.CullMode; + _deferredAlphaKinds[slot] = key.Translucency; + } + private void PrepareDeferredAlphaDraws(ReadOnlySpan tokens) { if (tokens.Length == 0) @@ -2530,49 +2614,7 @@ public sealed partial class WbDrawDispatcher : IDisposable int count = tokens.Length; EnsureDeferredAlphaCapacity(count); for (int i = 0; i < count; i++) - { - DeferredAlphaInstance entry = _deferredAlpha[tokens[i]]; - WriteMatrix(_instanceData, i * 16, entry.Model); - _clipSlotData[i] = entry.ClipSlot; - _indoorData[i] = entry.Indoor; - _detailCategoryData[i] = entry.DetailCategory; - _alphaData[i] = entry.Opacity; - _selectionLightingData[i] = entry.SelectionLighting; - int lightOffset = i * LightManager.MaxLightsPerObject; - entry.Lights.CopyTo(_lightSetData, lightOffset); - - GroupKey key = entry.Key; - _batchData[i] = new BatchData - { - // Campaign V slice V2: table slot, not the raw handle. - TextureIndex = key.TextureSlot.Index, - TextureLayer = key.TextureLayer, - // DrawMesh invokes RenderMeshSubset with detail enabled for - // every built-mesh material subset while curr_detail_surface - // is installed. The per-instance category still rejects - // ordinary objects in mesh_detail. Review fix round 2 (F2): - // bit 0 (the #226 built-mesh marker) is unconditional here — - // every delayed-alpha entry IS a built-mesh alpha submission - // — but the previous hardcoded `1` silently dropped bits 1/2 - // (Campaign VM VM6 foliage classification). A trunk instance - // promoted into the alpha-blend group mid-fade (translucency - // < 1 forces AlphaBlend even for an otherwise-Opaque trunk — - // see ClassifyBatches's #188 promotion) must keep swaying - // through the deferred-alpha replay instead of going rigid - // for the duration of its fade. - Flags = 1u | key.FoliageFlags, - }; - _indirectCommands[i] = new DrawElementsIndirectCommand - { - Count = (uint)key.IndexCount, - InstanceCount = 1, - FirstIndex = key.FirstIndex, - BaseVertex = key.BaseVertex, - BaseInstance = (uint)i, - }; - _drawCullModes[i] = key.CullMode; - _deferredAlphaKinds[i] = key.Translucency; - } + WriteDeferredAlphaEntrySlot(i, _deferredAlpha[tokens[i]]); // One upload per source per sorted alpha scope. RetailAlphaQueue later // draws contiguous ranges from this immutable prepared payload; it must @@ -2584,6 +2626,34 @@ public sealed partial class WbDrawDispatcher : IDisposable PrepareRhiAlphaSections(count); } + /// + /// S4-c2 fix round 1 (M3b): DrawMesh row 1's "render immediately; + /// enqueue nothing" outcome for ONE translucent building-shell instance + /// — retail draws the subset in place inside the mesh; the walk's + /// opaque instances are stream-batched, so "in place" becomes "at the + /// entity's own stream mark" (a register row records this granularity + /// adaptation — see SubmitToAlphaQueue's caller). Reuses + /// and + /// for slot 0, then + /// for the actual pipeline/ + /// detail draw. Never touches _deferredAlpha or the queue — this + /// instance never enters a list — and never overlaps a real queue drain + /// in time (a drain only replays previously-APPENDED tokens at a LATER + /// flush site; this call happens at the instance's own submission, + /// strictly before any such flush). + /// + private void DrawImmediateAlphaInstance(in DeferredAlphaInstance entry, Matrix4x4 viewProjection) + { + GlobalMeshBuffer? global = _meshAdapter.MeshManager?.GlobalBuffer; + if (global is null || !MeshSourceReady()) + return; + + EnsureDeferredAlphaCapacity(1); + WriteDeferredAlphaEntrySlot(0, in entry); + PrepareRhiAlphaSections(1); + DrawImmediateAlphaInstanceRhi(global, entry.Key.Translucency, viewProjection); + } + private void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) { if (drawCount <= 0) diff --git a/tests/AcDream.App.Tests/Rendering/EnvCellAlphaDrawSourceTests.cs b/tests/AcDream.App.Tests/Rendering/EnvCellAlphaDrawSourceTests.cs new file mode 100644 index 00000000..fc8ed406 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/EnvCellAlphaDrawSourceTests.cs @@ -0,0 +1,92 @@ +using AcDream.App.Rendering; + +namespace AcDream.App.Tests.Rendering; + +/// +/// S4-c2 fix round 1 (M6, contract C4): +/// is the queue-facing half of an EnvCell transparent shell's detail-off +/// submission — one token per CELL, replayed through a caller-supplied +/// delegate (production wiring: +/// ) +/// at whatever flush site the shared drains +/// at. These tests exercise the REAL production class end-to-end against a +/// real queue — only the render destination is faked (a delegate recording +/// what it was asked to draw), so the GPU-backed EnvCellRenderer +/// never needs to be constructed to pin the queue mechanics. +/// +public sealed class EnvCellAlphaDrawSourceTests +{ + /// Mutation check: if SubmitOrDrawTransparentCellShell + /// drew the cell immediately instead of appending it when detail is off + /// (the M1/M3 bug class this chunk fixes elsewhere), the drawn list + /// would already contain an entry BEFORE Flush runs — the + /// Assert.Empty(drawn) line fails against that mutation. + [Fact] + public void PendingCellToken_IsDrainedAtTheFlushAndNeverBeforeIt() + { + var drawn = new List>(); + var source = new RetailPViewPassExecutor.EnvCellAlphaDrawSource( + cells => drawn.Add(cells.ToArray())); + var queue = new RetailAlphaQueue(); + + queue.BeginFrame(); + int token = source.AddPendingCellId(0x1234u); + Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, token, false)); + + Assert.Empty(drawn); + + queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f); + + IReadOnlyList singleDraw = Assert.Single(drawn); + Assert.Equal(new uint[] { 0x1234u }, singleDraw); + queue.EndFrame(); + } + + /// Two cell tokens from the SAME , + /// with an unrelated source's entry appended between them, must still + /// produce TWO separate single-cell draw calls around the interposed + /// entry — 's "only adjacent + /// same-source entries batch" invariant (the queue never groups across + /// another entry, which is what keeps compositing order exact). Mutation + /// check: an implementation that draws every prepared cell id in one + /// call regardless of the requested (first, count) range would + /// print both cell ids together on EACH of the two + /// DrawPreparedAlphaBatch invocations instead of once each — the + /// exact sequence assertion below fails against that mutation. + [Fact] + public void ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombinedDrain() + { + var log = new List(); + var cellSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource( + cells => log.Add($"cell:{string.Join(',', cells)}")); + var particleSource = new RecordingSource("particle", log); + var queue = new RetailAlphaQueue(); + + queue.BeginFrame(); + int cell1Token = cellSource.AddPendingCellId(0x100u); + Assert.True(queue.TryAppend(RetailAlphaList.Alpha, cellSource, cell1Token, false)); + Assert.True(queue.TryAppend(RetailAlphaList.Alpha, particleSource, 7, false)); + int cell2Token = cellSource.AddPendingCellId(0x200u); + Assert.True(queue.TryAppend(RetailAlphaList.Alpha, cellSource, cell2Token, false)); + queue.EndFrame(); + + Assert.Equal(new[] { "cell:256", "particle:7", "cell:512" }, log); + } + + private sealed class RecordingSource(string name, List log) : IRetailAlphaDrawSource + { + private int[] _prepared = []; + + public void PrepareAlphaDraws(ReadOnlySpan tokens) => _prepared = tokens.ToArray(); + + public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) + { + for (int i = 0; i < drawCount; i++) + log.Add($"{name}:{_prepared[firstPreparedDraw + i]}"); + } + + public void ResetAlphaSubmissions() + { + } + } +} diff --git a/tests/AcDream.App.Tests/Rendering/ParticleRendererRouteTests.cs b/tests/AcDream.App.Tests/Rendering/ParticleRendererRouteTests.cs new file mode 100644 index 00000000..7ca79a5c --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/ParticleRendererRouteTests.cs @@ -0,0 +1,76 @@ +using AcDream.App.Rendering; +using AcDream.Core.Meshing; + +namespace AcDream.App.Tests.Rendering; + +/// +/// S4-c2 fix round 1 (M1, blocking): +/// is the pure router-input derivation the blocking fix extracted from +/// DeferToRetailAlphaQueue so it is directly testable without a GPU/ +/// mesh-manager harness. Before this round, a mesh-particle batch classified +/// (mask 0x00) reaching DrawMesh +/// row 5 (Immediate) hit an unconditional throw in the render loop — these +/// tests pin the two outcomes the fix made reachable. +/// +public sealed class ParticleRendererRouteTests +{ + /// M1(d) pin 1: an Opaque-classified mesh-particle batch (mask + /// 0x00) with a non-fully-opaque current alpha (top ColorArgb byte != + /// 0xFF, i.e. materialHasAlpha true) routes to ALPHA (Append) — retail's + /// row 4 material-alpha fallback — not Immediate. Mutation check: + /// reverting to the pre-fix code (which threw whenever the decision + /// wasn't Append) would not distinguish this from pin 2 below at all + /// (both would just throw); reverting ONLY the materialHasAlpha + /// derivation to a hardcoded `false` makes this assertion fail because + /// the decision becomes Immediate instead of Append. + [Fact] + public void OpaqueClassifiedMeshBatch_WithMaterialAlpha_RoutesToAlphaAppend() + { + // Top byte (alpha) = 0x80 -> not 0xFF -> materialHasAlpha = true. + const uint colorArgbWithAlpha = 0x80FFFFFFu; + + RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission( + ParticleSubmissionKind.Mesh, TranslucencyKind.Opaque, colorArgbWithAlpha); + + Assert.Equal(RetailAlphaMeshAction.Append, decision.Action); + Assert.Equal(RetailAlphaList.Alpha, decision.List); + } + + /// M1(d) pin 2: the SAME Opaque-classified mesh-particle batch + /// with a fully-opaque current alpha (top ColorArgb byte == 0xFF, i.e. + /// materialHasAlpha false) draws immediately and never enters a list — + /// retail's row 5 fallthrough. Mutation check: this is the EXACT case + /// that threw before the fix (mask 0x00, materialHasAlpha false -> + /// row 5 Immediate -> the old "rows 1/2/4/5 unreachable" guard fired); + /// reverting the throw's removal reproduces + /// InvalidOperationException here, which this test would report + /// as a failure (an unhandled exception) rather than an assertion + /// mismatch. + [Fact] + public void OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_RoutesImmediate() + { + const uint fullyOpaqueColorArgb = 0xFFFFFFFFu; + + RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission( + ParticleSubmissionKind.Mesh, TranslucencyKind.Opaque, fullyOpaqueColorArgb); + + Assert.Equal(RetailAlphaMeshAction.Immediate, decision.Action); + } + + /// A billboard submission always carries the alpha-family mask + /// (0x02) regardless of the (unused) mesh parameters, and therefore + /// always satisfies row 3 (Append, ALPHA) under the fixed default delay + /// mask — never Immediate, never CLIP. Mutation check: swapping + /// for + /// in the billboard + /// branch would route this to the CLIP list instead of ALPHA. + [Fact] + public void Billboard_AlwaysRoutesToAlphaAppend() + { + RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission( + ParticleSubmissionKind.Billboard, default, default); + + Assert.Equal(RetailAlphaMeshAction.Append, decision.Action); + Assert.Equal(RetailAlphaList.Alpha, decision.List); + } +} diff --git a/tests/AcDream.App.Tests/Rendering/RetailAlphaMeshRouterTests.cs b/tests/AcDream.App.Tests/Rendering/RetailAlphaMeshRouterTests.cs index 8c3cba35..79c87960 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailAlphaMeshRouterTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailAlphaMeshRouterTests.cs @@ -152,20 +152,25 @@ public sealed class RetailAlphaMeshRouterTests } /// - /// Full brute-force sweep over every (mask 0x00-0x09) x sky x detail x - /// multipass x hasAlpha cell at the fixed default delay mask (0x0E), - /// checked against an INDEPENDENTLY-shaped reference derivation (a - /// switch-driven truth table, not a copy of 's - /// own if-chain) so a control-flow slip in one is unlikely to survive in - /// the other with the same wrong answer. Mutation check: flipping any - /// single row's condition in Route (e.g. row 3's != 0 to - /// == 0) changes that row's outcome for roughly half the 160 - /// cells, which this sweep catches immediately (verified by hand during - /// S4-c2 development: each of the five rows' conditions was flipped in - /// turn and this test failed every time). + /// S4-c2 fix round 1 (A7): this was documented as an "INDEPENDENTLY- + /// shaped reference derivation (a switch-driven truth table, not a copy + /// of 's own if-chain)" — false; + /// below is the SAME five-row + /// if-chain shape with different local variable names, not a switch or + /// lookup table. It is renamed and redocumented honestly rather than + /// rewritten into a literal 160-row table (spec §4's five rows over ten + /// masks × sky × detail × multipass × hasAlpha) — the per-row + /// Theory above already carries one + /// mutation text per row boundary; this brute-force sweep's real value + /// is catching a copy-paste/off-by-one divergence BETWEEN the two + /// restatements (a typo in one that the other doesn't share), not an + /// independent verification of the spec itself. The claim that "each of + /// the five rows' conditions was flipped in turn and this test failed + /// every time" is deleted — that hand-verification was never re-run and + /// is not reproduced in this round's evidence. /// [Fact] - public void Route_MatchesIndependentTruthTableAcrossEveryCell() + public void Route_MatchesRestatedBranchTableAcrossEveryCell() { byte[] masks = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]; bool[] bothBools = [false, true]; @@ -180,7 +185,7 @@ public sealed class RetailAlphaMeshRouterTests { RetailAlphaMeshDecision actual = RetailAlphaMeshRouter.Route( sky, RetailAlphaMeshRouter.DefaultDelayMask, detail, multipass, mask, hasAlpha); - RetailAlphaMeshDecision expected = IndependentReferenceRoute( + RetailAlphaMeshDecision expected = RestatedBranchTableRoute( sky, RetailAlphaMeshRouter.DefaultDelayMask, detail, multipass, mask, hasAlpha); bool matches = actual.Action == expected.Action @@ -198,9 +203,10 @@ public sealed class RetailAlphaMeshRouterTests Assert.True(mismatches == 0, $"{mismatches} mismatches; first: {firstMismatch}"); } - /// Independent re-derivation of spec §4's table using a - /// switch/lookup shape rather than the production if-chain. - private static RetailAlphaMeshDecision IndependentReferenceRoute( + /// Re-derivation of spec §4's table in independently-named + /// local variables (NOT a switch/lookup shape — see this Fact's own doc + /// comment above, A7). + private static RetailAlphaMeshDecision RestatedBranchTableRoute( bool sky, byte delayMask, bool detail, bool multipass, byte mask, bool hasAlpha) { bool row1 = sky || delayMask == 0 || detail; diff --git a/tests/AcDream.App.Tests/Rendering/RetailAlphaQueueTests.cs b/tests/AcDream.App.Tests/Rendering/RetailAlphaQueueTests.cs index 5fe39529..6ecb5de6 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailAlphaQueueTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailAlphaQueueTests.cs @@ -311,42 +311,6 @@ public sealed class RetailAlphaQueueTests queue.AbortFrame(); } - /// The first entry appended to a list after it was last drained - /// is flagged IsFirstForList; later entries in the same - /// uninterrupted run are not. Mutation check: always setting the flag - /// true (or always false) fails this exact sequence assertion. - [Fact] - public void TryAppend_FlagsOnlyTheFirstEntrySinceTheLastDrain() - { - var log = new List(); - var source = new RecordingSource("alpha", log); - var queue = new RetailAlphaQueue(); - FieldInfo alphaField = typeof(RetailAlphaQueue).GetField( - "_alpha", BindingFlags.NonPublic | BindingFlags.Instance)!; - - queue.BeginFrame(); - queue.TryAppend(RetailAlphaList.Alpha, source, 1, false); - queue.TryAppend(RetailAlphaList.Alpha, source, 2, false); - - // Inspect BEFORE flushing: two entries in the SAME list snapshot, - // discriminating true (first) from false (second) — checking only - // the post-flush single-survivor list (as an earlier draft of this - // test did) is vacuous, since a one-element list is trivially - // "first" whether or not the flag logic is correct. - var beforeFlush = (List)alphaField.GetValue(queue)!; - Assert.Equal(2, beforeFlush.Count); - Assert.True(beforeFlush[0].IsFirstForList); - Assert.False(beforeFlush[1].IsFirstForList); - - queue.Flush(RetailAlphaFlushSite.RenderNormalMode, 0f); - queue.TryAppend(RetailAlphaList.Alpha, source, 3, false); - - var afterDrain = (List)alphaField.GetValue(queue)!; - Assert.Single(afterDrain); - Assert.True(afterDrain[0].IsFirstForList); - queue.AbortFrame(); - } - [Fact] public void RetainedScratchConvergesAfterAOneScopeSpike() { @@ -371,6 +335,74 @@ public sealed class RetailAlphaQueueTests Assert.Equal(0, queue.PendingCount); } + /// + /// S4-c2 fix round 1 (A4): DrainAndReset/AbortFrame used to + /// pass the ENTRY count as ApplyScratchRetention's SOURCE-count + /// argument too (ApplyScratchRetention(observedClip + observedAlpha, + /// observedClip + observedAlpha)). A single spike frame first + /// registers 100 DISTINCT sources (one entry each) so _sources' + /// own List-growth capacity climbs well past its initial 4 — otherwise + /// Math.Min(sourceTarget, _sources.Capacity) clamps ANY + /// sourceTarget down to that unchanged initial 4 and the two + /// formulas become indistinguishable, which is why a single-source + /// spike does not discriminate this bug. Three low-demand frames then + /// each resubmit 10 entries through the SAME ONE source (matching the + /// three consecutive observations + /// requires before it recommends shrinking) — at the moment the shrink + /// fires, the ENTRY count (10) and the real SOURCE count (1) genuinely + /// diverge. Mutation check: reverting to + /// ApplyScratchRetention(observedClip + observedAlpha, observedClip + + /// observedAlpha) makes sourceTarget compute from 10 + /// (Math.Max(4, 10*2)=20, clamped by the now-≥20 _sources.Capacity + /// to 20) instead of from 1 (Math.Max(4, 1*2)=4) — the actual + /// observed capacity under that mutation is 20, and the upper-bound + /// assertion below fails against it. + /// + [Fact] + public void RetainedSourceCapacity_ConvergesToTheRealSourceCountNotTheEntryCount() + { + const int budgetBytes = 128 * 1024; + var manySources = new CountingSource[100]; + for (int i = 0; i < manySources.Length; i++) + manySources[i] = new CountingSource(); + var queue = new RetailAlphaQueue(budgetBytes); + FieldInfo sourcesField = typeof(RetailAlphaQueue).GetField( + "_sources", BindingFlags.NonPublic | BindingFlags.Instance)!; + + // 8,192 entries (enough to also push the ENTRY capacity itself past + // its low-demand budget, matching RetainedScratchConvergesAfterAOneScopeSpike's + // own spike size) spread across the 100 distinct sources, so BOTH + // _sources' own capacity AND the entry-side capacity are in their + // post-spike high-water state together. + queue.BeginFrame(); + for (int i = 0; i < 8_192; i++) + queue.TryAppend(RetailAlphaList.Alpha, manySources[i % manySources.Length], i, false); + queue.EndFrame(); + + var sourcesAfterSpike = (List)sourcesField.GetValue(queue)!; + Assert.True( + sourcesAfterSpike.Capacity > 8, + "Test setup check: the 100-distinct-source spike must grow _sources' own capacity " + + $"past its initial 4 (observed {sourcesAfterSpike.Capacity}) — otherwise the " + + "Math.Min clamp below hides the bug regardless of which formula runs."); + + CountingSource repeatedSource = manySources[0]; + for (int i = 0; i < 3; i++) + { + queue.BeginFrame(); + for (int j = 0; j < 10; j++) + queue.TryAppend(RetailAlphaList.Alpha, repeatedSource, j, false); + queue.EndFrame(); + } + + var sources = (List)sourcesField.GetValue(queue)!; + Assert.True( + sources.Capacity <= 8, + "Expected the retained source-array capacity to converge toward the real source " + + $"count (1), but it stayed at {sources.Capacity} — the entry count (10), not the " + + "source count (1), must have driven ApplyScratchRetention's second argument."); + } + [Fact] public void AbortFrame_DiscardsPayloadAndAllowsTheNextFrameToRender() { diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs index 75513f23..61c0a188 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs @@ -8,6 +8,7 @@ using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Gpu.Vk; using AcDream.App.Rendering.Sky; using AcDream.App.Rendering.Walk; +using AcDream.App.Rendering.Wb; using AcDream.App.Tests.Architecture; using AcDream.App.Tests.Rendering.Gpu; @@ -158,6 +159,44 @@ public sealed class RetailPViewPassExecutorTests call => call.Target.DeclaringType == typeof(WalkTranscriptDump)); } + /// + /// S4-c2 fix round 1 (M6, contract C4): a cell's transparent shell's + /// immediate-vs-append decision must come from the SAME + /// table every other alpha submitter + /// uses — never a hand-coded if (detailEnabled) branch that + /// bypasses it. Pins the STRUCTURE: + /// is read to feed 's own + /// detailSurfaceActive argument BEFORE the route call, and the + /// route call precedes BOTH possible outcomes + /// ( for + /// Immediate, for Append). + /// Mutation check: deleting the Route call and branching directly + /// on TransparentDetailEnabled instead removes the + /// call entirely — RequiredCallIndex + /// fails its index >= 0 assertion for the route call. + /// + [Fact] + public void SubmitOrDrawTransparentCellShell_RoutesThroughTheSharedTableBeforeEitherOutcome() + { + MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod( + "SubmitOrDrawTransparentCellShell", + BindingFlags.Instance | BindingFlags.NonPublic)!; + IReadOnlyList calls = CompiledCallGraph.Read(method); + + int detailProbe = RequiredCallIndex( + calls, typeof(EnvCellRenderer), "get_TransparentDetailEnabled"); + int route = RequiredCallIndex( + calls, typeof(RetailAlphaMeshRouter), nameof(RetailAlphaMeshRouter.Route)); + int renderImmediate = RequiredCallIndex( + calls, typeof(EnvCellRenderer), nameof(EnvCellRenderer.RenderTransparentOrdered)); + int append = RequiredCallIndex( + calls, typeof(RetailAlphaQueue), nameof(RetailAlphaQueue.TryAppend)); + + Assert.True(detailProbe < route); + Assert.True(route < renderImmediate); + Assert.True(route < append); + } + /// /// S3 chunk 4 (§10.2): the former per-outside-view-slice loop /// (the walk's own screen-space terrain-clip writer + its per-frame diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewRendererTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewRendererTests.cs new file mode 100644 index 00000000..e5182373 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/RetailPViewRendererTests.cs @@ -0,0 +1,56 @@ +using System.Reflection; +using AcDream.App.Rendering; +using AcDream.App.Tests.Architecture; + +namespace AcDream.App.Tests.Rendering; + +public sealed class RetailPViewRendererTests +{ + /// + /// S4-c2 fix round 1 addendum (A1): retail's LandscapeFlush + /// (PView::DrawCells @0x005a4840's own FlushAlphaList(0f) + /// @0x005a4872) only ever fires on an INTERIOR PView, strictly inside + /// if (outside_view.view_count > 0) — an outdoor root never + /// reaches it (terrace-edge's capture: zero 005a4877 FL lines). + /// In production the interior case reaches + /// through the + /// walk driver's LandscapeFlush leaf (FlushWalkLandscape, a + /// SEPARATE method the driver calls during Replay) — never as a + /// direct call compiled into 's + /// own body. The outdoor-root direct call that used to sit inline here + /// (mislabelled LandscapeFlush) is deleted; retail's real outdoor-root + /// drains are the DrawBlock 0.75 valve (already wired via + /// WalkFrameEventKind.SortCellExit) and + /// SmartBox::RenderNormalMode's pass-end flush + /// (, called from + /// WorldSceneRenderer.CompleteWorldFrame). + /// + /// Mutation check: reverting the deletion (restoring + /// if (ctx.RootCell.IsOutdoorNode) passes.FlushLandscapeAlpha();) + /// reintroduces a direct call to + /// inside + /// 's compiled body — the + /// Assert.DoesNotContain below fails against that mutation. + /// + [Fact] + public void DrawInside_NeverCallsFlushLandscapeAlphaDirectly() + { + MethodInfo drawInside = typeof(RetailPViewRenderer).GetMethod( + "DrawInside", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!; + IReadOnlyList calls = CompiledCallGraph.Read(drawInside); + + Assert.DoesNotContain( + calls, + call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor) + && call.Target.Name == nameof(RetailPViewPassExecutor.FlushLandscapeAlpha)); + + // The interior-cell unattached-emitter draw beside the deleted call + // must remain — this pin only removes the mislabelled flush, not + // the particle submission next to it. + Assert.Contains( + calls, + call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor) + && call.Target.Name == nameof(RetailPViewPassExecutor.DrawUnattachedSceneParticles)); + } +} diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkAlphaDepthTrace.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkAlphaDepthTrace.cs index 663c4ef7..bba935dd 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkAlphaDepthTrace.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkAlphaDepthTrace.cs @@ -87,9 +87,17 @@ public static class WalkAlphaDepthTrace // own printf. listSel nonzero selects CLIP // (alphaedMeshCountClip/List); zero selects ALPHA // (alphaedMeshCountAlpha/List) — Ghidra-verified 2026-09-04, see - // RetailAlphaList's own doc comment. "new" is the per-entry + // RetailAlphaList's own doc comment. "new" is retail's per-entry // first-for-list flag (param_4); "clip" (param_5, unused here) - // is overrideClipmap. + // is overrideClipmap. S4-c2 fix round 1 (M4/A6): IsNew is parsed + // and RETAINED purely as a captured/parsed FACT (fidelity to the + // capture's own format) but is never compared against acdream's + // own routing — M4 established that flag is trivially true for + // EVERY subset retail appends (one DrawMesh invocation owns one + // subset per list), so a per-subset comparison would be + // meaningless at acdream's coarser per-INSTANCE granularity (the + // same content-volume mismatch M5's KnownFailure count gate + // documents) — no dead-parse-as-evidence claim is made here. Match am = AmPattern.Match(line); if (am.Success) { diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs index c20e53da..660ec636 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs @@ -1736,6 +1736,75 @@ public sealed partial class WalkFrameDriverTests Assert.Single(leaf.LandCellBatches)); } + /// + /// S4-c2 fix round 1 (M5): the ONE live pin exercising the valve THIS + /// CHUNK added, through 's own + /// WalkFrameEventKind.SortCellExit arm (not + /// called directly — + /// RetailAlphaQueueTests already has that boundary). The queue is + /// preloaded directly (a dummy source, never real mesh content — this + /// pin proves the VALVE's threshold behavior, not classification, which + /// is covered elsewhere) with entries sitting in ALPHA when the walk's + /// own DrawBlock-equivalent land-cell turn reaches its + /// FlushAlphaList(0.75f) call. Mutation check: forcing + /// 's no-op comparison to + /// <= instead of < (S4-c1/c2's own boundary mutation) + /// makes the 2250 case read PendingCount == 2250 (a no-op) + /// instead of 0 — the first assertion below fails against that + /// mutation. + /// + [Theory] + [InlineData(2250, 0)] + [InlineData(2249, 2249)] + public void SortCellExit_ValveDrainsThroughReplayAtTheExactBoundary( + int preloadedCount, int expectedPendingAfter) + { + using var fx = new DispatcherFixture(); + var log = new List(); + var leaf = new RecordingLeafRenderer(log, fx.AlphaQueue); + leaf.CellsWithoutEmitters.UnionWith(CoarseLandscapeBuckets(0xF4180000u)); + var ctx = new TestContext(); + var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData()); + var walk = new RetailFrameWalk(); + var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] }; + var block = new WalkLandBlock + { + LandblockId = 0xF4180000u, SideCellCount = 1, MaxZ = 10f, MinZ = 0f, + }; + block.EnsureCellArrays(); + landscape.Blocks[0] = block; + var dummySource = new DummyAlphaSource(); + + using DrawScope draw = fx.BeginDraw(beginAlpha: true); + for (int i = 0; i < preloadedCount; i++) + { + Assert.True(fx.AlphaQueue.TryAppend(RetailAlphaList.Alpha, dummySource, i, false)); + } + + driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero); + walk.DrawLandscape(landscape, OneDegenerateView(), ctx, driver); + driver.EndFrame(); + driver.Replay(draw.Frame, draw.Pass); + + Assert.Equal(new[] { preloadedCount }, leaf.AlphaPendingAtSortCellExit); + Assert.Equal(expectedPendingAfter, fx.AlphaQueue.PendingCount); + } + + private sealed class DummyAlphaSource : IRetailAlphaDrawSource + { + public void PrepareAlphaDraws(ReadOnlySpan tokens) + { + } + + public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount) + { + } + + public void ResetAlphaSubmissions() + { + } + } + // ── F4(b) (S3 chunk 3 fix round 1 §9.6): an interior root with one // surviving exit view — the SAME fixture as // RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs index 6b009ec6..bd17cfaa 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs @@ -551,8 +551,11 @@ public sealed class WalkStaticStreamPopulatorTests Assert.Equal(record.Transform.LocalToWorld, call.LocalToWorld); } - // ── SubmitWalkAlphaInstance: same viewer distance + per-instance data as - // DeferTransparentGroups, through the REAL RetailAlphaQueue. ─────────── + // ── SubmitWalkAlphaInstance: same per-instance data and router decision + // as DeferTransparentGroups, through the REAL RetailAlphaQueue — S4-c2 + // fix round 1 (A5): this banner's stale "same viewer distance" text is + // corrected; the FIFO cutover deleted viewer distance from the alpha + // path entirely. ──────────────────────────────────────────────────── /// S4-c2: retail's queues are FIFO, not distance-sorted — this /// pins the routing decision instead (an AlphaBlend batch's constructed diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs index 48f6e101..7a31a6ef 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs @@ -368,33 +368,21 @@ public sealed partial class WalkTraceConformanceTests } /// - /// S4-c2 gate G-c2: extends this file's own PM/PC transcript gate from - /// depth events to the AM/FL alpha-queue stream. Reuses the EXACT SAME - /// replay harness (: no live - /// GfxObj/particle mesh content, matching this file's own established - /// PM/PC pattern) with a real wired - /// through 's flush hooks. - /// - /// What this DOES prove, unconditionally (no KnownFailure - /// needed): the SEQUENCE of (site, threshold) the walk drives the - /// queue through — which of the four normal-world sites fires, how many - /// times, in what order — is purely structural (a function of cell/ - /// building/land-cell traversal, never of mesh content), so it is - /// checked here as an unconditional pass/fail. - /// - /// What this CANNOT prove with this harness: the drained - /// entry count PER LIST. This gate's queue carries zero live content - /// (same design choice as the PM/PC gate it extends), so every 0f- - /// threshold flush observes count (0, 0) — always a real drain (0 is - /// never < 0), matching retail's own always-drains behavior at - /// threshold 0f, but with a DIFFERENT count than the capture's real - /// content volume. A capture pose whose expected transcript has any - /// nonzero drained count therefore diverges on the count dimension by - /// harness design, not by a routing bug — see this Fact's own - /// [Trait("Status","KnownFailure")] and the S4-c2 packet write-up - /// (S4 packet §9) for the full per-pose sequences. + /// S4-c2 gate G-c2, split by S4-c2 fix round 1 (M5): extends this file's + /// own PM/PC transcript gate from depth events to the AM/FL alpha-queue + /// stream. Reuses the EXACT SAME replay harness + /// (: no live GfxObj/particle mesh + /// content, matching this file's own established PM/PC pattern) with a + /// real wired through + /// 's flush hooks. Returns BOTH the expected + /// and actual transcripts so and + /// can assert their own dimension + /// over the SAME replay logic without duplicating the harness setup. /// - private void RunAlphaFlushTranscriptGate(string fixtureName) + private static ( + IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> Expected, + IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> Actual) + RunAlphaFlushTranscriptReplay(string fixtureName) { IReadOnlyList poseFrames = WalkOracleTrace.Load(OhCaptureRoot, fixtureName); Assert.NotEmpty(poseFrames); @@ -447,34 +435,74 @@ public sealed partial class WalkTraceConformanceTests leaf.ActualFlushes.Add( (RetailAlphaFlushSite.RenderNormalMode, 0f, clipBeforeFinal, alphaBeforeFinal)); - IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> actual = - leaf.ActualFlushes; + return (expected, leaf.ActualFlushes); + } + + /// + /// S4-c2 fix round 1 (M5): the LIVE half of gate G-c2 — the SEQUENCE of + /// (site, threshold) the walk drives the queue through (which of the + /// four normal-world sites fires, how many times, in what order) is + /// purely structural (a function of cell/building/land-cell traversal, + /// never of mesh content), so it is a real, unconditional pass/fail with + /// no KnownFailure tag — a future site-sequence regression is now + /// visible in the hermetic/InstalledDat lanes instead of being buried + /// under the (expected-red) count dimension. + /// + private void RunAlphaFlushSitesGate(string fixtureName) + { + ( + IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> expected, + IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> actual + ) = RunAlphaFlushTranscriptReplay(fixtureName); bool siteSequenceMatches = expected .Select(e => (e.Site, e.Threshold)) .SequenceEqual(actual.Select(a => (a.Site, a.Threshold))); - // The site+threshold sequence is a REAL, unconditional pass/fail — - // purely structural, never content-dependent. Assert.True( siteSequenceMatches, $"{fixtureName}: flush SITE sequence diverges (content-independent — this must " + $"match unconditionally) — expected {FormatFlushSites(expected)}, " + $"actual {FormatFlushSites(actual)}"); + } - // The full tuple (including per-list drained counts) is the part - // this harness's zero-content design cannot prove — recorded here - // as a real, evaluated assertion (never weakened), expected to - // diverge on the count dimension for any pose with real content; + /// + /// S4-c2 fix round 1 (M5): the KnownFailure half of gate G-c2 — the + /// drained entry count PER LIST can never match by construction, for + /// THREE independent reasons (not just "harness content only", the + /// prior write-up's incomplete claim): (1) this harness carries no live + /// GfxObj/particle mesh content at all (), + /// so every 0f-threshold flush observes count (0, 0) — always a real + /// drain (0 is never < 0), matching retail's own always-drains + /// behavior at threshold 0f, but with a different count than the + /// capture's real content volume; (2) even WITH content, acdream appends + /// one queue entry per INSTANCE, where retail's AddMeshToAlphaList + /// appends one entry per SUBSET per DrawMesh call — a single + /// multi-subset instance inflates retail's count relative to acdream's; + /// (3) acdream's CLIP list is structurally empty for ordinary content + /// (M2's new register row) — WbDrawDispatcher.IsOpaque filters + /// clip-mapped subsets out before they ever reach a submit call, so + /// acdream's CLIP count reads 0 wherever retail's capture shows nonzero + /// CLIP entries. See the S4 packet's §9/§10/§11 subsections for the + /// full per-pose sequences. + /// + private void RunAlphaFlushCountsGate(string fixtureName) + { + ( + IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> expected, + IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> actual + ) = RunAlphaFlushTranscriptReplay(fixtureName); + + // Recorded as a real, evaluated assertion (never weakened), expected + // to diverge on the count dimension for any pose with real content — // callers gate on Status=KnownFailure, not on this passing. bool fullMatches = expected.SequenceEqual(actual); Assert.True( fullMatches, - $"{fixtureName}: flush transcript diverges on drained COUNT only (site+threshold " - + "sequence above already verified exact) — this hermetic replay carries no live " - + "GfxObj/particle content (EmptyAlphaDepthWorldData, matching this file's own " - + $"PM/PC design), so every count reads (0,0) — expected {FormatFlushCounts(expected)}, " - + $"actual {FormatFlushCounts(actual)}"); + $"{fixtureName}: flush transcript diverges on drained COUNT only (the site+threshold " + + "sequence is separately verified exact by AlphaFlushSites_*) — see this method's " + + $"own doc comment for the three reasons this can never match — expected " + + $"{FormatFlushCounts(expected)}, actual {FormatFlushCounts(actual)}"); } private static string FormatFlushSites( @@ -487,41 +515,67 @@ public sealed partial class WalkTraceConformanceTests ", ", events.Select(e => $"({e.Site},thresh={e.Threshold},clip={e.DrainedClip},alpha={e.DrainedAlpha})")) + "]"; - /// S4-c2 gate G-c2. See 's - /// own doc comment for what this Fact does and does not prove; tagged - /// KnownFailure because this hermetic harness carries no live mesh - /// content, so the drained-count dimension diverges from the capture's - /// real content volume by design — the S4 packet's §9 subsection quotes - /// both sequences per pose. + /// S4-c2 fix round 1 (M5): the live half — see + /// 's own doc comment. No + /// KnownFailure tag: this dimension is content-independent and must + /// match unconditionally. + [Fact] + public void AlphaFlushSites_CathedralArrival_MatchesRetailFrame2() + => RunAlphaFlushSitesGate("cathedral-arrival.alphadepth"); + + /// S4-c2 fix round 1 (M5): the KnownFailure half — see + /// 's own doc comment for the three + /// independent reasons the drained COUNT can never match by + /// construction. [Fact] [Trait("Status", "KnownFailure")] - public void AlphaFlushTranscript_CathedralArrival_MatchesRetailFrame2() - => RunAlphaFlushTranscriptGate("cathedral-arrival.alphadepth"); + public void AlphaFlushCounts_CathedralArrival_MatchesRetailFrame2() + => RunAlphaFlushCountsGate("cathedral-arrival.alphadepth"); + + [Fact] + public void AlphaFlushSites_CathedralLeak_MatchesRetailFrame2() + => RunAlphaFlushSitesGate("cathedral-leak.alphadepth"); [Fact] [Trait("Status", "KnownFailure")] - public void AlphaFlushTranscript_CathedralLeak_MatchesRetailFrame2() - => RunAlphaFlushTranscriptGate("cathedral-leak.alphadepth"); + public void AlphaFlushCounts_CathedralLeak_MatchesRetailFrame2() + => RunAlphaFlushCountsGate("cathedral-leak.alphadepth"); + + [Fact] + public void AlphaFlushSites_CathedralStairArch_MatchesRetailFrame2() + => RunAlphaFlushSitesGate("cathedral-stair-arch.alphadepth"); [Fact] [Trait("Status", "KnownFailure")] - public void AlphaFlushTranscript_CathedralStairArch_MatchesRetailFrame2() - => RunAlphaFlushTranscriptGate("cathedral-stair-arch.alphadepth"); + public void AlphaFlushCounts_CathedralStairArch_MatchesRetailFrame2() + => RunAlphaFlushCountsGate("cathedral-stair-arch.alphadepth"); + + [Fact] + public void AlphaFlushSites_FoundryDeep_MatchesRetailFrame2() + => RunAlphaFlushSitesGate("foundry-deep.alphadepth"); [Fact] [Trait("Status", "KnownFailure")] - public void AlphaFlushTranscript_FoundryDeep_MatchesRetailFrame2() - => RunAlphaFlushTranscriptGate("foundry-deep.alphadepth"); + public void AlphaFlushCounts_FoundryDeep_MatchesRetailFrame2() + => RunAlphaFlushCountsGate("foundry-deep.alphadepth"); + + [Fact] + public void AlphaFlushSites_HoltburgDoorwayStill_MatchesRetailFrame2() + => RunAlphaFlushSitesGate("holtburg-doorway-still.alphadepth"); [Fact] [Trait("Status", "KnownFailure")] - public void AlphaFlushTranscript_HoltburgDoorwayStill_MatchesRetailFrame2() - => RunAlphaFlushTranscriptGate("holtburg-doorway-still.alphadepth"); + public void AlphaFlushCounts_HoltburgDoorwayStill_MatchesRetailFrame2() + => RunAlphaFlushCountsGate("holtburg-doorway-still.alphadepth"); + + [Fact] + public void AlphaFlushSites_TerraceEdge_MatchesRetailFrame2() + => RunAlphaFlushSitesGate("terrace-edge.alphadepth"); [Fact] [Trait("Status", "KnownFailure")] - public void AlphaFlushTranscript_TerraceEdge_MatchesRetailFrame2() - => RunAlphaFlushTranscriptGate("terrace-edge.alphadepth"); + public void AlphaFlushCounts_TerraceEdge_MatchesRetailFrame2() + => RunAlphaFlushCountsGate("terrace-edge.alphadepth"); private static string FormatPm(IReadOnlyList<(int Mode, int CounterBefore)> events) => "[" + string.Join(", ", events.Select(e => $"(mode={e.Mode},counterBefore={e.CounterBefore})")) + "]";