diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md index 02a532c9..b1db5639 100644 --- a/docs/architecture/acdream-architecture.md +++ b/docs/architecture/acdream-architecture.md @@ -1135,8 +1135,11 @@ explicit Campaign OVERHAUL divergences, not accepted retail contracts: retail's `DrawLandCell` then `DrawSortCell` interleave for each land cell; - depth clear/seal events are emitted for zero-outside-view roots and do not consume retail's persistent previous-call `uint16 portalsDrawnCount` gate; -- `RetailAlphaQueue` distance-sorts one shared queue, whereas retail owns two - FIFO mesh lists and drains clip before alpha at exact barriers. +- ordinary object and particle parts now receive retail's stable per-cell + authored-CYpt order before entering the two FIFO mesh lists, which drain + CLIP before ALPHA at exact barriers. The retained differences are AP-241 + (opaque/row-5 GPU channels), AP-242 (equal cross-source tie ordinal), and + AP-243 (retail's >50 m shared-cell-key optimization). The exact target and evidence are recorded in `docs/research/2026-09-01-overhaul/oh1-retail-world-contract.md`; OH5, OH6, and diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 4cb3a2f4..bf875870 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -220,7 +220,7 @@ readiness/requeue adaptation. See --- -## 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) +## 3. Documented approximation (AP) — 162 active rows (AP-241/AP-242/AP-243 filed 2026-09-04 at S4-c3a; 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; @@ -333,6 +333,9 @@ research and is no longer active. | AP-238 | **S4-c2 (2026-09-04); NARROWED FURTHER by fix rounds 1/2 and the owner-authorized repair — renumbered from the colliding id "AP-236" (that id was already filed AND retired on `main`; ids are never reused).** `RetailAlphaQueue` is retail's independent CLIP/ALPHA FIFO pair. The repaired production leaf excludes every non-additive transparent EnvCell subset from its preceding opaque draw, retains each real batch's `RetailSurfaceMask`, and routes canonical `0x08` plus legal positive-stipple `0x09` to CLIP with row-3 `overrideClipmap=false`; `0x02` routes to ALPHA. EnvCell CLIP now uses `SetSurface`'s exact `ONE/INVSRCALPHA`, depth-test/write-on state and `GREATER_EQUAL` reference (100/255 paletted, 200/255 DDS); detail-active row 1 keeps that CLIP base/reference before its detail contribution. A mixed cell can contribute to both lists without cross-feeding. The only remaining residual is token granularity: acdream coalesces to at most ONE token per `(cell,list)` and replays a list-filtered subset view, where retail appends one entry per surface subset. | `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs` (`SubmitOrDrawTransparentCellShell`, `EnvCellAlphaDrawSource`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` / `.Rhi.cs` (`BatchBelongsToPass`, retained-mask state grouping, filtered `RenderTransparentOrdered`); `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs` (`ObjectRenderBatch.RetailSurfaceMask`); `src/AcDream.App/Rendering/RetailAlphaQueue.cs` | Per-list replay preserves the exact subset class/state and the queue's adjacent-only batching preserves interleaving around other sources; the approximation exists only within one cell/list when several same-list subsets are represented by one token. | At a genuine depth-overlap, collapsing multiple same-list subsets to one `(cell,list)` token can visibly change compositing if other translucent content should fall between those subsets in retail's FIFO. | `RenderDeviceD3D::DrawEnvCell` 0x0059F170; `D3DPolyRender::DrawMesh` 0x0059D4A0; `D3DPolyRender::AddMeshToAlphaList` 0x0059C230; `D3DPolyRender::SetSurface` 0x0059C4D0 (paired binary: blend ids 2/6 @0x0059C72A, refs `0x64`/`0xC8` at globals 0x00820D8C/0x00820D90, func 7, depth-write byte 1 @0x0059C866); canonical installed-DAT case `F4180104` surface `08000BFF` mask `0x08` | | AP-239 | **Filed 2026-09-04 (S4-c2); renumbered from the colliding id "AP-237" at fix round 1.** `RetailAlphaMeshRouter.MaskFromTranslucencyKind` reconstructs retail's `ConstructMesh` routing mask from acdream's already-collapsed `TranslucencyKind`. A raw `Surface.Type` with Translucent(0x10) and Base1ClipMap(0x04), but no Alpha/InvAlpha/Additive bit, collapses to `AlphaBlend` and reconstructs as mask `0x02` (ALPHA), while retail retains mask `0x08` (CLIP). `GroupKey` does not retain the raw ClipMap bit required to distinguish the two origins. The original cloud example was disproven; a 2026-09-04 scan found 27 real `Type=0x14` surfaces among 6,152, including surface `0x08000015` referenced by GfxObj `0x010001EC`. | `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` (`MaskFromTranslucencyKind`); `src/AcDream.Core/Meshing/TranslucencyKind.cs` (`FromSurfaceType`) | The affected surface keeps the same blend state, but choosing the wrong FIFO changes compositing order; at a genuine overlap that order difference can be visibly different. | A translucent+clipmap surface (e.g. GfxObj `0x010001EC`) interleaves with ALPHA-list content instead of CLIP-list content and can composite differently at a depth overlap. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority); `D3DPolyRender::SetSurface` 0x0059C4D0 | | AP-240 | **Filed 2026-09-04 (S4-c2 fix round 1, M2); narrowed at fix round 2 and the owner-authorized repair.** `WbDrawDispatcher.IsOpaque(t) => t == Opaque \|\| t == ClipMap` keeps clip-mapped GfxObj/scenery/building subsets out of `_translucentDraws`/`SubmitWalkAlphaInstance`/`SubmitToAlphaQueue`; retail gives those subsets mask `0x08` and appends them to CLIP. They instead draw immediately on acdream's `OpaqueAlphaToCoverage` pipeline. CLIP has two genuine production feeder families outside this residual: clip-mapped mesh particles and exact-mask EnvCell subsets (canonical `F4180104` / surface `08000BFF`), with EnvCell now proven through the real leaf and exact `SetSurface` CLIP state. The residual is only ordinary GfxObj/scenery/building content; no ordinary-Wb A2C behavior changed in this repair. | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` (`IsOpaque`); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs` (`RouteTransparentBatch`); `src/AcDream.App/Rendering/ParticleRenderer.cs`; `src/AcDream.App/Rendering/RetailAlphaMeshRouter.cs` | The cutout itself remains present, but immediate opaque-path placement differs from retail's deferred FIFO order. Changing it is a VisualMaster-era pipeline decision beyond this bounded chunk. | At an overlap with deferred CLIP content from particles or EnvCells, clip-mapped GfxObj/scenery/building edges can visibly composite in the wrong relative order. | `D3DPolyRender::ConstructMesh` 0x0059DFA0 (mask priority, row 3 → CLIP); `D3DPolyRender::DrawMesh` 0x0059D4A0; `D3DPolyRender::AddMeshToAlphaList` 0x0059C230 | +| AP-241 | **Filed 2026-09-04 at Campaign OVERHAUL S4-c3a.** Retail walks one cell's opaque/cutout object parts and row-5 immediate particle parts through one distance-sorted `CShadowPart::draw` sequence. Acdream reconstructs the same authored per-part CYpt order, but ordinary opaque/cutout objects first enter the ordered Vulkan stream while row-5 particles draw from `ParticleRenderer.PrepareForCellAlpha` at the later cell-alpha event. Those are separate Vulkan submission channels rather than one literal interleaved part loop. | `src/AcDream.App/Rendering/Walk/WalkStaticStreamPopulator.cs` (`PopulateCellObjects`); `src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs` (`SubmitCellAlpha`); `src/AcDream.App/Rendering/ParticleRenderer.cs` (`PrepareForCellAlpha`) | Preserving the established opaque stream and S4-c2 row-5 fixed-state path keeps this chunk bounded; merging their GPU command channels would change ordered-stream ownership and pipeline scheduling beyond the delayed-alpha repair. | Coplanar opaque/cutout object and row-5 particle geometry can choose a different first depth writer than retail even though delayed alpha order is exact. | `RenderDeviceD3D::DrawObjCellForDummies` 0x005A0760; `CShadowPart::insertion_sort` 0x006B5130; `D3DPolyRender::DrawMesh` 0x0059D4A0 row 5 | +| AP-242 | **Filed 2026-09-04 at Campaign OVERHAUL S4-c3a.** Retail's one physical `shadow_part_list` supplies a single insertion ordinal for equal-CYpt object and particle parts. Acdream has separate object membership and particle owners, so no exact shared registration ordinal exists. The production merge uses a deterministic stable source tie: ordinary object records precede particle records when their retained squared CYpt keys are exactly equal; order within each source remains authored/stable. | `src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs` (`SubmitCellAlpha`, strict `>` particle comparison); `src/AcDream.App/Rendering/ParticleSubmissionOrdering.cs` (`PreparedParticleAlphaSubmission`) | Inventing a common ordinal would claim evidence acdream does not retain. The deterministic object-first tie is explicit, stable, and confined to exact floating-point equality. | Exactly coincident translucent object/particle parts can composite in the opposite order from retail when retail's unrepresented physical insertion ordinal put the particle first. | `CPhysicsObj::add_particle_shadow_to_cell` 0x00514A70; `CPartArray::AddPartsShadow` 0x00517E40; `CShadowPart::insertion_sort` 0x006B5130..0x006B519D | +| AP-243 | **Filed 2026-09-04 at Campaign OVERHAUL S4-c3a after paired-binary correction.** Acdream always computes the per-part authored `SortCenter` key for a cell turn. Retail does that only in `UpdateObjCell`'s near branch. When cell viewer distance is greater than 50 m, retail normalizes the cell heading and calls `CPhysicsObj::UpdateViewerDistance(obj, cellDistance, heading)`, propagating the same cell CYpt/heading to every part; equal keys then retain shadow-list insertion order. | `src/AcDream.App/Rendering/Walk/WalkStaticStreamPopulator.cs` (`PopulateCellObjects`) | Porting `UpdateObjCell`'s >50 m cell-distance optimization also requires the cell heading/distance inputs and belongs with a later LOD/distance chunk. Always using the already-authored per-part center is deterministic and more spatially exact, but not mechanism-identical. | In far cells, acdream may reorder parts within that cell by their individual centers where retail gives them one shared key and preserves stable insertion order. Cell boundaries and FIFO-list behavior remain unchanged. | `RenderDeviceD3D::UpdateObjCell` 0x005A0690, paired bytes 0x005A06B7..0x005A0720 (`test ah,0x41; jne` near branch); `CPhysicsObj::UpdateViewerDistance` 0x0050F340; `CPartArray::UpdateViewerDistance` 0x00518290 | | 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 9dd10462..fc8ae21d 100644 --- a/docs/architecture/worldbuilder-inventory.md +++ b/docs/architecture/worldbuilder-inventory.md @@ -400,11 +400,14 @@ renderers classify translucent mesh batches correctly, but they have no live retail `CPartCell`/`CShadowPart` list and render particles in a separate batcher. `src/AcDream.App/Rendering/RetailAlphaQueue.cs` is therefore an acdream-owned runtime seam above the extracted mesh pipeline. During the main -world frame, `WbDrawDispatcher` and `ParticleRenderer` submit transparent -GfxObj subsets and scene particles into one stable far-to-near stream keyed by -the transformed DAT `SortCenter`; only adjacent compatible entries may batch. +world frame, `WalkFrameDriver` reconstructs retail's stable far-to-near order +inside each object-cell turn from transformed authored DAT `SortCenter`, merges +the cell's `WbDrawDispatcher` GfxObj subsets with prepared `ParticleRenderer` +records, then appends the result to retail's two FIFO lists (CLIP/ALPHA). +It never performs one scope-global distance sort; only adjacent compatible +entries may batch at drain. Billboard particle textures are resident bindless `sampler2DArray` handles in -the per-instance vertex ABI, so different textures preserve that sorted order +the per-instance vertex ABI, so different textures preserve that per-cell order inside one instanced draw; only a DAT blend-mode boundary splits the run. This keeps dense particle fields from becoming one Vulkan draw per alternating texture. `WalkFrameDriver` drains the landscape and world scopes at the exact diff --git a/docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md b/docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md index 0a81e8b7..f644c24d 100644 --- a/docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md +++ b/docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md @@ -1349,72 +1349,86 @@ register count, allowed files, and every gate claim. A failed lens gets one bounded fix contract; a third fix round stops and is written up. Dual PASS lands c3a; the lead then writes c3b's deletion-only contract from that code. -## 19. S4-c3a stop record — owner attempts 3–5 exhausted (2026-09-04) +### 18.7 Implementation result (scratch implementation, 2026-09-04) -**STOPPED UNLANDED.** The clean scratch worktree -`C:\Users\erikn\source\repos\acdream\.claude\worktrees\s4-c3a-impl` -ends at `359061b8299dd77a2335935868b739b8295b9b0c`. Nothing from c3a was -cherry-picked to the campaign branch; the campaign renderer remains the -reviewed S4-c2 stack through `252886e84`, with this §18 contract at -`5c9da6fc2`. c3b is locked and G3/G4 remain unpassed. +The bounded c3a implementation now builds one combined registry view for each +ordinary outdoor/interior object-cell turn, transforms each GfxObj's authored +`SortCenter`, applies a stable descending per-cell insertion sort, and derives +both opaque/cutout commands and delayed object records from that same order. +The real particle leaf prepares but does not expose delayed records; the cell +event merges their retained keys with the object's range before either source +appends to the unchanged CLIP/ALPHA FIFO lists. Row-5 immediate routing, +capacity-drop/source rollback, barriers, and building-shell ownership are +unchanged. Exact equal object/particle keys use the disclosed object-first tie. -The complete unlanded scratch stack is: +The paired-binary correction is recorded as AP-243: retail's +`UpdateObjCell` greater-than-50 m branch at `0x005A06B7..0x005A0720` gives +all parts one cell CYpt/heading through `CPhysicsObj::UpdateViewerDistance` +and therefore retains insertion order, while this bounded port always uses +the more exact per-part authored center. AP-241/AP-242 record the two other +narrow residuals. The active AP header and physical table are pinned at 162 +rows, with exactly one row for each AP-241/AP-242/AP-243. -1. `44e2bc227b` — initial c3a implementation; -2. `b6bf6c131` — lead fix-round-1 contract; -3. `eea5793d2` — fix round 1; -4. `14397b14c` — lead fix-round-2 contract, explicitly the last round; -5. `359061b82` — fix round 2. +Lead-approved process-scope clarification: the real-path integration also +changes exactly `src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs` +and `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs`. The literal +`{Walk,Wb}/` production-file list omitted the existing leaf adapter and the +thin `PrepareCellParticleAlpha` forwarder required to reach the expressly +allowed `ParticleRenderer`; relocating that bridge would create an artificial +seam. These two files contain only the interface adaptation and forwarder—no +router, queue, mask, pipeline, reference, depth, or flush behavior. The lead +approved this minimum real-path reachability delta before commit. It is a +contract-scope clarification, not a retail behavior deviation, so it files no +additional AP row. -Attempt 3 failed the retail lens because billboard/mode-2–5 particles keyed -CYpt from their rendered/AABB center instead of the GfxObj-authored -`sort_center`; the same lens found one stale static-record-order comment. -Fix round 1 separated authored and visual centers, applied particle scale and -orientation to the authored point, made prepared payload reservation lazy, -rolled back false/exception append exactly once, bounded preparation to 3,000 -CLIP plus 3,000 ALPHA candidates, and corrected the prose. +Clean-state return matrix (no graphical client was launched): -Attempt 4 failed the retail re-review on one universal edge: a successfully -decoded real GfxObj with no Surface DID was replaced by -`ParticleGfxInfo.Default`, discarding its authored center. The installed scan -found zero practical exposure (2,051 emitter records / 360 referenced GfxObjs, -all with a nonzero first Surface DID), but retail's CYpt read is surface- -independent. Fix round 2 preserved the decoded record and made only texture -acquisition conditional; it also corrected the synthetic-fixture evidence -wording. +- Initial solution `--no-restore` build was invalid setup evidence: 42 + projects lacked `project.assets.json` (`NETSDK1004`). One solution restore + followed; the official Release solution build then succeeded with 0 + warnings and 0 errors. +- Shader descriptor/manifest/SPIR-V validation: 32/32. No shader source, + checked-in SPIR-V, manifest, Content model, serializer, or package format + changed. +- Focused queue/router/particle/Wb/walk/EnvCell/PView production filter: + 210/210. The three real warmed routes (EnvCell whole-leaf SubmitRhi, + immediate mesh particle RHI, and the new driver/particle/dispatcher merge) + pass 3/3 and each asserts 0 managed bytes. +- Official one-shot 14-assembly hermetic gate: 16,743 passed / 0 skipped / + 0 failed. Evidence: + `%TEMP%/acdream-s4-c3a-impl-20260904-2015`. +- InstalledDat with explicit `C:\Turbine\Asheron's Call`: 385 passed / 10 + failed / 1 skipped. The failures are exactly TowerAscent, the two #383 + layout sweeps, #458 `Oh_doorway_still_first_frame_diff`, and the six + retained `AlphaFlushCounts_*`; all six paired `AlphaFlushSites_*` pass. + Evidence: `%TEMP%/acdream-s4-c3a-installed-20260904-2017`. +- `git diff --check`: pass. -Attempt 5's final retail lens **PASSED**. It reconfirmed the paired executable -and PDB, `CPhysicsPart::UpdateViewerDistance @0x0050E030`, stable descending -per-cell order, combined static/dynamic membership, the real object/particle -merge, building separation, unchanged two-list FIFO/router/row-5 behavior, -and AP-241/AP-242/AP-243 truth/count in the scratch stack. Its focused -reproduction passed 117/117. +Observed mutation first failures (every mutation was reversed with a patch +before the return matrix): -The final production/gate-honesty lens found no production, lifecycle, -allocation, scope, register, or retained-memory defect. It nevertheless -returned **FAIL** on one mandatory evidence error. The initial candidate's -mutation ledger says renaming AP-241 first fails `Assert.Single`. The unchanged -pin actually computes `rowIndex == -1` and first fails the earlier AP-section -boundary `Assert.True` in `WalkStaticStreamPopulatorTests.cs`; `Assert.Single` -is reached later. The other eighteen mutation claims match their actual first -assertions. Correcting that sentence would be a third c3a fix round, forbidden -by plan §5, packet §18.6, and the owner's explicit attempt limit. Therefore no -evidence-only correction, landing, graphical gate, or c3b contract follows. - -Final read-only evidence on `359061b82`: - -- Release solution: 0 warnings / 0 errors; -- focused §19 lane: 95/95; no-surface target: 1/1; final retail superset: - 117/117; -- AP boundary/count pin: 1/1; physical and unique AP counts: 162; -- real allocation pins: 2/2 at 0 managed bytes; -- shader/manifest/SPIR-V: 32/32; -- one-shot hermetic artifact: 16,751 passed / 0 skipped / 0 failed at - `%TEMP%\acdream-s4-c3a-fix1-20260904-2215`; -- inclusive InstalledDat artifact: 385 passed / 10 documented failures / 1 - skip at `%TEMP%\acdream-s4-c3a-fix1-installed-inclusive-20260904-2222`; -- `git diff --check`: PASS; scratch HEAD clean; no graphical client launched. - -Resume only after an explicit owner process change. The minimum mechanical -correction is known, but the stop rule exists precisely to prevent a green -renderer from bypassing a failed evidence contract. +1. Reverse the CYpt comparator: `PopulateCellObjects_UsesAuthoredSortCenterAndStableFarToNearOrder` + expected `[202,101]`, actual `[101,202]` at the first sequence assertion. +2. Move equal keys left (`>=`): `PopulateCellObjects_EqualCyptRetainsEntityPartAndSubsetOrder` + expected `[11,12,21,22]`, actual `[22,21,12,11]`. +3. Restore static/dynamic blocks: `PopulateCellObjects_InterleavesStaticAndDynamicOpaquePartsByCypt` + expected `[2,3,1]`, actual `[3,1,2]`. +4. Replace authored SortCenter with entity translation: the authored-center + test first expected `[202,101]`, actual `[101,202]`. +5. Restore the old object-then-particle tail: `CellTurn_RealParticlePreparationMergesWithObjectAlphaByCypt` + expected source order `[Wb,Particle,Wb,Particle]`, actual + `[Wb,Wb,Particle,Particle]` at position 1. +6. Sort all staged objects scope-globally before replay: + `SeparateCellTurnsRemainCellMajorWhenLaterCellIsFarther` first expected + model X `5`, actual `50`. +7. Restore a dead optional camera parameter on `SubmitWalkAlphaInstance`: + `WorldAlphaCyptContract_RetainsTheKeyWithoutDeadCameraSubmitThreading` + first expected parameter count 2, actual 3. +8. Reinsert “distance-sorts one shared queue” into the inventory: + `WorldAlphaCyptDocumentationAndRegister_PinPerCellTruthAndThreeResiduals` + first failed `Assert.DoesNotContain` on that exact phrase. +9. Rename AP-241 away: that same truth pin first failed `Assert.Single` + because no AP-241 row matched. +10. Allocate one object in the real cell-merge path: + `ProductionCellObjectParticleMerge_WarmedPathAllocatesZeroBytes` first + expected 0, actual 3,072 managed bytes. diff --git a/src/AcDream.App/Rendering/ParticleRenderer.cs b/src/AcDream.App/Rendering/ParticleRenderer.cs index cfd289bd..536f2531 100644 --- a/src/AcDream.App/Rendering/ParticleRenderer.cs +++ b/src/AcDream.App/Rendering/ParticleRenderer.cs @@ -18,8 +18,10 @@ namespace AcDream.App.Rendering; /// /// Instanced renderer for retail particle emitters. Scene particles submit to /// while a world frame is active so their -/// compositing order is shared with ordinary translucent GfxObj parts. Sky and -/// sealed off-screen passes retain their independent immediate path. +/// compositing order is merged by authored CYpt with ordinary translucent +/// GfxObj parts at each owning cell turn before either source enters the two +/// FIFO lists. Sky and sealed off-screen passes retain their independent +/// immediate path. /// public sealed unsafe partial class ParticleRenderer : IDisposable { @@ -163,6 +165,7 @@ public sealed unsafe partial class ParticleRenderer : IDisposable private readonly List _meshDrawListScratch = new(64); private readonly List _meshRunScratch = new(64); private readonly List _submissionScratch = new(128); + private readonly List _preparedCellAlphaScratch = new(128); private readonly List _scopedEmitterScratch = new(64); private readonly List _deferredAlpha = new(128); private DeferredParticleDraw[] _preparedAlpha = new DeferredParticleDraw[256]; @@ -315,6 +318,105 @@ public sealed unsafe partial class ParticleRenderer : IDisposable FinishDraw(camera, renderPass); } + /// + /// S4-c3a production cell-shadow seam. Builds the same real billboard and + /// mesh records as , orders particle parts by + /// their authored CYpt key, executes row-5 immediate draws at this exact + /// cell turn, and returns only delayed records without appending them. + /// The walk driver merges those retained keys with ordinary object parts + /// before calling . + /// + internal ReadOnlySpan PrepareForCellAlpha( + ICamera camera, + Vector3 cameraWorldPos, + ParticleRenderPass renderPass, + uint cellId, + uint clipSlot = 0) + { + _preparedCellAlphaScratch.Clear(); + if (camera is null) + return CollectionsMarshal.AsSpan(_preparedCellAlphaScratch); + + _particles.CopyRenderableEmittersInCell(renderPass, cellId, _scopedEmitterScratch); + if (_scopedEmitterScratch.Count == 0) + return CollectionsMarshal.AsSpan(_preparedCellAlphaScratch); + + Matrix4x4.Invert(camera.View, out Matrix4x4 invView); + Vector3 cameraRight = Vector3.Normalize(new Vector3(invView.M11, invView.M12, invView.M13)); + Vector3 cameraUp = Vector3.Normalize(new Vector3(invView.M21, invView.M22, invView.M23)); + BuildDrawLists( + cameraWorldPos, + renderPass, + cameraRight, + cameraUp, + emitterFilter: null, + _scopedEmitterScratch, + clipSlot); + + if (_submissionScratch.Count == 0) + return CollectionsMarshal.AsSpan(_preparedCellAlphaScratch); + + bool defers = renderPass == ParticleRenderPass.Scene && _alphaQueue?.IsCollecting == true; + if (!defers) + { + DrawOrdered(camera); + return CollectionsMarshal.AsSpan(_preparedCellAlphaScratch); + } + + ParticleSubmissionOrdering.Sort(_submissionScratch); + Matrix4x4 viewProjection = camera.View * camera.Projection; + RetailAlphaQueue queue = _alphaQueue!; + for (int i = 0; i < _submissionScratch.Count; i++) + { + ParticleSubmission submission = _submissionScratch[i]; + TranslucencyKind translucency = submission.Kind == ParticleSubmissionKind.Mesh + ? _meshDrawListScratch[submission.DrawIndex].Batch.Translucency + : default; + uint colorArgb = submission.Kind == ParticleSubmissionKind.Mesh + ? _meshDrawListScratch[submission.DrawIndex].Instance.ColorArgb + : default; + RetailAlphaMeshDecision decision = RouteParticleSubmission( + submission.Kind, translucency, colorArgb); + + if (decision.Action is RetailAlphaMeshAction.Append + or RetailAlphaMeshAction.AppendClipAndImmediate) + { + _dispatchDeferredParticle = submission.Kind == ParticleSubmissionKind.Billboard + ? new DeferredParticleDraw( + submission.Kind, + _drawListScratch[submission.DrawIndex], + default, + viewProjection) + : new DeferredParticleDraw( + submission.Kind, + default, + _meshDrawListScratch[submission.DrawIndex], + viewProjection); + int token = ReserveDispatchDeferredParticle(); + _preparedCellAlphaScratch.Add(new PreparedParticleAlphaSubmission( + queue, + decision.List, + _alphaSource, + token, + decision.OverrideClipmap, + submission.DistanceSq, + submission.Sequence)); + } + + if (decision.Action is RetailAlphaMeshAction.Immediate + or RetailAlphaMeshAction.AppendClipAndImmediate) + { + _drawImmediateParticle( + viewProjection, + submission.Kind, + submission.DrawIndex, + opaqueDepthState: decision.Action == RetailAlphaMeshAction.Immediate); + } + } + + return CollectionsMarshal.AsSpan(_preparedCellAlphaScratch); + } + private void FinishDraw(ICamera camera, ParticleRenderPass renderPass) { if (_submissionScratch.Count == 0) @@ -738,11 +840,10 @@ public sealed unsafe partial class ParticleRenderer : IDisposable Matrix4x4 model = Matrix4x4.CreateScale(particle.Size) * Matrix4x4.CreateFromQuaternion(orientation) * Matrix4x4.CreateTranslation(particle.Position); - // S4-c2: RetailAlphaOrdering.ComputeViewerDistance is deleted (the - // alpha queue is FIFO now), but this distanceSq is still needed by - // the UNRELATED non-deferred immediate-draw path's own local sort - // (ParticleSubmissionOrdering.Sort, ParticleRenderer.Rhi.cs) — - // inlined here rather than resurrecting the deleted helper. + // The queue remains FIFO. S4-c3a retains this authored part-center + // distance for the owning cell's object/particle pre-append merge; + // the unrelated non-deferred immediate path uses the same key for its + // own local ParticleSubmissionOrdering.Sort. Vector3 worldSortCenter = Vector3.Transform(renderData.SortCenter, model); float distanceSq = Vector3.DistanceSquared(worldSortCenter, cameraWorldPosition); var instance = new MeshParticleInstance( diff --git a/src/AcDream.App/Rendering/ParticleSubmissionOrdering.cs b/src/AcDream.App/Rendering/ParticleSubmissionOrdering.cs index 889555d9..8b75a845 100644 --- a/src/AcDream.App/Rendering/ParticleSubmissionOrdering.cs +++ b/src/AcDream.App/Rendering/ParticleSubmissionOrdering.cs @@ -16,6 +16,27 @@ internal readonly record struct ParticleSubmission( float DistanceSq, int Sequence); +/// +/// One scene-particle alpha record prepared at its owning cell turn but not +/// yet appended. S4-c3a lets WalkFrameDriver merge this retained CYpt +/// key with ordinary object parts before either source enters retail's FIFO +/// list. The source payload is reserved during preparation; remains the sole visibility edge and +/// still owns capacity-drop cleanup by registering the source before a drop. +/// +internal readonly record struct PreparedParticleAlphaSubmission( + RetailAlphaQueue Queue, + RetailAlphaList List, + IRetailAlphaDrawSource Source, + int Token, + bool OverrideClipmap, + float DistanceSq, + int Sequence) +{ + internal void Append() => + Queue.TryAppend(List, Source, Token, OverrideClipmap); +} + /// /// Shared ordering for the two retail particle geometry paths. Transparent /// particles are submitted back-to-front; creation/enumeration order is the diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs index 3abfda1c..c2cccb4a 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs @@ -89,9 +89,9 @@ internal sealed partial class RetailPViewPassExecutor /// S3 chunk 3 fix round 1 (F2): the production /// IWalkFrameLeafRenderer.HasRenderableEmittersInCell wiring — - /// both and - /// draw through - /// ParticleRenderPass.Scene, so this asks that SAME pass. + /// both walk particle-prepare leaves use + /// ParticleRenderPass.Scene, so this asks that SAME pass before + /// either leaf constructs its real delayed records. internal bool HasWalkRenderableEmittersInCell(uint cellId) => _particles?.HasRenderableEmittersInCell(ParticleRenderPass.Scene, cellId) ?? false; @@ -271,11 +271,11 @@ internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer public void ClearInteriorDepth() => _clearInteriorDepth(); - public void DrawStaticParticles(uint cellId) => - _passes.DrawLandscapeStaticParticles(_frame, cellId); + public ReadOnlySpan PrepareStaticParticles(uint cellId) => + _passes.PrepareCellParticleAlpha(_frame, cellId); - public void DrawCellParticles(uint cellId) => - _passes.DrawCellParticles(_frame, cellId); + public ReadOnlySpan PrepareCellParticles(uint cellId) => + _passes.PrepareCellParticleAlpha(_frame, cellId); public int DrawExitSeals() => _drawExitSeals(); diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs index 7a5d1159..b6d185e3 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs @@ -555,6 +555,21 @@ internal sealed partial class RetailPViewPassExecutor : IEnvCellImmediateDrawSin } } + internal ReadOnlySpan PrepareCellParticleAlpha( + RetailPViewFrameInput frame, + uint cellId) + { + if (_particles is null || _particleRenderer is null) + return ReadOnlySpan.Empty; + + return _particleRenderer.PrepareForCellAlpha( + frame.Camera, + frame.CameraWorldPosition, + ParticleRenderPass.Scene, + cellId, + clipSlot: 0); + } + public void ClearInteriorDepth() { _surface.ClearInteriorDepth(); diff --git a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs index 4282f6bc..8e1c5c7e 100644 --- a/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs +++ b/src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs @@ -42,6 +42,15 @@ internal readonly record struct WalkFrameStaticRecords( /// internal interface IWalkFrameWorldData { + /// The exact combined ordinary-object membership for one indoor + /// cell, preserving the registry's single static/dynamic insertion + /// sequence for S4-c3a's per-part stable CYpt sort. + WalkFrameStaticRecords GetCellObjects(uint cellId); + + /// The exact combined ordinary-object membership for one + /// landscape cell; see . + WalkFrameStaticRecords GetOutdoorObjects(uint cellId); + /// An indoor PView::DrawCells flood cell's (or a building /// look-in's) static content — RenderProjectionClass.IndoorCellStatic. WalkFrameStaticRecords GetCellStatics(uint cellId); @@ -113,8 +122,8 @@ internal interface IWalkFrameLeafRenderer /// pipeline-state citation. A /// is NOT one of them (F10: the ordered stream it draws is opaque-only, /// depth test AND write on, no blending); the F2 particle-turn exception - /// still holds (a genuinely empty / turn neither submits nor flushes). This is + /// still holds (a genuinely empty / turn neither submits nor flushes). This is /// order-preserving by construction: the batch's GPU submission point is /// always the SAME point the unbatched terrain draws would have occupied /// (immediately before the next real submission), so pixel output is @@ -133,10 +142,10 @@ internal interface IWalkFrameLeafRenderer /// name="cellId"/> has any renderable emitter right now — production /// wires this to ParticleSystem.HasRenderableEmittersInCell with /// the SAME ParticleRenderPass.Scene pass both and - /// already draw through. asks this - /// BEFORE flushing the pending terrain batch (or calling either particle - /// leaf) so a genuinely empty particle turn submits nothing and does not + /// cref="PrepareStaticParticles"/> and + /// prepare through. asks this BEFORE + /// flushing the pending terrain batch (or preparing either particle leaf) + /// so a genuinely empty particle turn submits nothing and does not /// force an otherwise-unneeded terrain flush. bool HasRenderableEmittersInCell(uint cellId); @@ -157,12 +166,12 @@ internal interface IWalkFrameLeafRenderer /// (0x00514a70) draws an emitter at its own current cell's object turn /// regardless of its attached owner's registry membership, so this is a /// cell lookup, not an owner union. - void DrawStaticParticles(uint cellId); + ReadOnlySpan PrepareStaticParticles(uint cellId); /// Submits one indoor cell's own-cell particle emitters at that /// cell's own object-list turn. Meshes have already entered the ordered /// stream before this leaf event. - void DrawCellParticles(uint cellId); + ReadOnlySpan PrepareCellParticles(uint cellId); /// PView::DrawCells @0x005a4840's gated full depth clear /// (pc:432731-432732) between the outside stage and the interior root's @@ -347,7 +356,7 @@ internal enum WalkFrameEventKind : byte /// . ExitSeals, - /// — + /// — /// ONE landscape (land) cell's () /// own-cell particle emitters, emitted AT ITS OWN WALK TURN. Retail's /// falls containment is positional: an outdoor emitter's polys join the @@ -368,7 +377,7 @@ internal enum WalkFrameEventKind : byte /// owns one shadow in its OWN current cell, never its parent's). StaticParticles, - /// — + /// — /// is the cell. Fires at EVERY /// visited interior cell's own object-list turn regardless of whether /// that cell has any visible static/dynamic owner record: an emitter @@ -481,11 +490,23 @@ internal readonly struct WalkFrameEvent internal static WalkFrameEvent AlphaBarrier() => new(WalkFrameEventKind.AlphaBarrier, 0, 0, 0f, null); - internal static WalkFrameEvent LandscapeCellParticles(uint cellId) => - new(WalkFrameEventKind.StaticParticles, 0, cellId, 0f, null); + internal static WalkFrameEvent LandscapeCellParticles( + uint cellId, int alphaExclusiveEnd, bool includeParticles) => + new( + WalkFrameEventKind.StaticParticles, + alphaExclusiveEnd, + cellId, + includeParticles ? 1f : 0f, + null); - internal static WalkFrameEvent CellParticles(uint cellId) => - new(WalkFrameEventKind.CellParticles, 0, cellId, 0f, null); + internal static WalkFrameEvent CellParticles( + uint cellId, int alphaExclusiveEnd, bool includeParticles) => + new( + WalkFrameEventKind.CellParticles, + alphaExclusiveEnd, + cellId, + includeParticles ? 1f : 0f, + null); internal static WalkFrameEvent LandscapeFlush() => new(WalkFrameEventKind.LandscapeFlush, 0, 0, 0f, null); @@ -1138,7 +1159,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource _alphaSubmissions[alphaCursor]; _dispatcher.SubmitWalkAlphaInstance( in batch, - _cameraWorldPosition, _viewProjection); } break; @@ -1205,18 +1225,16 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource // current cell, drawn at that cell's object turn // regardless of any owner's registry membership — // this is a cell lookup, not an owner union. - if (_leafRenderer.HasRenderableEmittersInCell(e.CellId)) - { - FlushPendingTerrainBatch(); - _leafRenderer.DrawStaticParticles(e.CellId); - } + SubmitCellAlpha( + e, + ref alphaCursor, + staticParticleTurn: true); break; case WalkFrameEventKind.CellParticles: - if (_leafRenderer.HasRenderableEmittersInCell(e.CellId)) - { - FlushPendingTerrainBatch(); - _leafRenderer.DrawCellParticles(e.CellId); - } + SubmitCellAlpha( + e, + ref alphaCursor, + staticParticleTurn: false); break; } } @@ -1231,13 +1249,62 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource _events.Clear(); _markPositions.Clear(); _alphaSubmissions.Clear(); - _pendingTerrainBatch.Clear(); + _pendingTerrainBatch.Clear(); _alphaSubmitMark = 0; _readyToReplay = false; _ctx = null; } } + /// + /// S4-c3a: completes one retail object-cell turn. Object delayed subsets + /// are already in stable far→near part order. Real particle records are + /// prepared only now, then merged by their retained CYpt before either + /// source appends to the two untouched FIFO queue lists. Object wins an + /// exactly equal cross-source key; AP-242 records that deterministic + /// source tie because acdream has no shared physical registration + /// ordinal. The event always drains its object range, including a cell + /// with no particle emitters. + /// + private void SubmitCellAlpha( + in WalkFrameEvent e, + ref int alphaCursor, + bool staticParticleTurn) + { + ReadOnlySpan particles = + ReadOnlySpan.Empty; + bool includeParticles = e.FloatArg != 0f; + if (includeParticles && _leafRenderer.HasRenderableEmittersInCell(e.CellId)) + { + // Preserve the existing particle-turn terrain boundary: a row-5 + // immediate mesh may draw during preparation. + FlushPendingTerrainBatch(); + particles = staticParticleTurn + ? _leafRenderer.PrepareStaticParticles(e.CellId) + : _leafRenderer.PrepareCellParticles(e.CellId); + } + + int objectEnd = e.IntArg; + int particleIndex = 0; + while (alphaCursor < objectEnd || particleIndex < particles.Length) + { + bool takeParticle = particleIndex < particles.Length + && (alphaCursor >= objectEnd + || particles[particleIndex].DistanceSq + > _alphaSubmissions[alphaCursor].SortDistanceSq); + if (takeParticle) + { + particles[particleIndex++].Append(); + } + else + { + WbDrawDispatcher.WalkClassifiedBatch batch = + _alphaSubmissions[alphaCursor++]; + _dispatcher.SubmitWalkAlphaInstance(in batch, _viewProjection); + } + } + } + /// S3 chunk 3 fix round 1 (F2), round 2 (F10): submits the /// pending terrain batch as ONE /// call, if it holds anything, and clears it. Called immediately before @@ -1446,23 +1513,18 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource + "view set — the walk and draw driver are desynchronized."); } VisitedLandscapeCellIds.Add(cellId); - WalkFrameStaticRecords records = _worldData.GetOutdoorStatics(cellId); - WalkFrameStaticRecords dynamics = _worldData.GetOutdoorDynamics(cellId); - _populator.PopulateOutdoorStatics( - _stream, cellId, records.Records, records.TupleLandblockId, - _cameraWorldPosition, _viewProjection, - this, _landscapeViewRouteIndex, - alphaSubmissions: _alphaSubmissions); - _populator.PopulateCellDynamics( - _stream, cellId, dynamics.Records, dynamics.TupleLandblockId, - _cameraWorldPosition, _viewProjection, - this, _landscapeViewRouteIndex, - alphaSubmissions: _alphaSubmissions); - if (_alphaSubmissions.Count != _alphaSubmitMark) - { - MarkIfGrown(); - MarkAlphaIfGrown(); - } + WalkFrameStaticRecords records = _worldData.GetOutdoorObjects(cellId); + _populator.PopulateCellObjects( + _stream, + WalkDrawStage.OutdoorStatic, + cellId, + records.Records, + records.TupleLandblockId, + _cameraWorldPosition, + _viewProjection, + this, + _landscapeViewRouteIndex, + _alphaSubmissions); // FW4 (the #132 positional invariant): this cell's emitters submit // AT THIS TURN, so nearer buildings' pre-punch barriers drain them // against still-true depth — see WalkFrameEventKind.StaticParticles. @@ -1473,7 +1535,12 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource // has any visible static/dynamic owner record (Campaign OVERHAUL S2 // chunk 6 — a suspended/hidden owner's emitter must still show). MarkIfGrown(); - _events.Add(WalkFrameEvent.LandscapeCellParticles(cellId)); + bool includeParticles = _cellParticleTurnsDrawnThisFrame.Add(cellId); + _alphaSubmitMark = _alphaSubmissions.Count; + _events.Add(WalkFrameEvent.LandscapeCellParticles( + cellId, + _alphaSubmitMark, + includeParticles)); } /// S4-c2: retail RenderDeviceD3D::DrawBlock @0x005a17c0's @@ -1837,13 +1904,19 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource // The shell loop captured the exact live portal_view for this cell. // Static and dynamic object lists reuse that same route: retail's // DrawEnvCell and DrawObjCell consume the same PortalList. - WalkFrameStaticRecords records = _worldData.GetCellStatics(cellId); - _populator.PopulateCell( - _stream, stage, cellId, records.Records, records.TupleLandblockId, - _cameraWorldPosition, _viewProjection, - this, viewRouteIndex, _alphaSubmissions); + WalkFrameStaticRecords records = _worldData.GetCellObjects(cellId); + _populator.PopulateCellObjects( + _stream, + stage, + cellId, + records.Records, + records.TupleLandblockId, + _cameraWorldPosition, + _viewProjection, + this, + viewRouteIndex, + _alphaSubmissions); MarkIfGrown(); - MarkAlphaIfGrown(); if (stage == WalkDrawStage.LookInStatic) { @@ -1851,19 +1924,6 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource LookInCells.Add(cellId); } - WalkFrameStaticRecords dynamics = _worldData.GetCellDynamics(cellId); - _populator.PopulateCellDynamics( - _stream, - cellId, - dynamics.Records, - dynamics.TupleLandblockId, - _cameraWorldPosition, - _viewProjection, - this, - viewRouteIndex, - alphaSubmissions: _alphaSubmissions); - MarkIfGrown(); - MarkAlphaIfGrown(); // Fires for every cell turn regardless of whether that cell has any // visible static/dynamic owner record (Campaign OVERHAUL S2 chunk 6 — // a suspended/hidden owner's emitter must still show), but ONCE per @@ -1873,8 +1933,12 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource // frame (a chamber reached through two portals). The same frame-scoped // set that dedupes the cell SHELL dedupes the particle turn (chunk 6 // review F1). - if (_cellParticleTurnsDrawnThisFrame.Add(cellId)) - _events.Add(WalkFrameEvent.CellParticles(cellId)); + bool includeParticles = _cellParticleTurnsDrawnThisFrame.Add(cellId); + _alphaSubmitMark = _alphaSubmissions.Count; + _events.Add(WalkFrameEvent.CellParticles( + cellId, + _alphaSubmitMark, + includeParticles)); } private void CaptureCellViews(uint cellId) diff --git a/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs b/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs index dddce624..da038bbb 100644 --- a/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs +++ b/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs @@ -83,10 +83,12 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData private readonly Dictionary _cellCache = new(); private readonly Dictionary _cellDynamicCache = new(); + private readonly Dictionary _cellObjectsCache = new(); private readonly Dictionary _facilityShadowProbeSignatures = new(); private readonly Dictionary> _shellsByAnchor = new(); private readonly Dictionary _outdoorMaterialized = new(); private readonly Dictionary _outdoorDynamicsMaterialized = new(); + private readonly Dictionary _outdoorObjectsMaterialized = new(); private readonly Dictionary _shellMaterialized = new(); // Campaign OVERHAUL S2 chunk 5: every distinct entity id counted into @@ -179,8 +181,10 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData _tupleLandblockId = tupleLandblockId; _cellCache.Clear(); _cellDynamicCache.Clear(); + _cellObjectsCache.Clear(); _outdoorMaterialized.Clear(); _outdoorDynamicsMaterialized.Clear(); + _outdoorObjectsMaterialized.Clear(); _shellMaterialized.Clear(); _arenaLength = 0; UnregisteredRenderMembershipCount = 0; @@ -252,6 +256,16 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData return records; } + public WalkFrameStaticRecords GetCellObjects(uint cellId) + { + if (_cellObjectsCache.TryGetValue(cellId, out WalkFrameStaticRecords cached)) + return cached; + WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: null); + EmitFacilityShadowProbe(records.Records, cellId, "combined"); + _cellObjectsCache[cellId] = records; + return records; + } + public WalkFrameStaticRecords GetCellDynamics(uint cellId) { if (_cellDynamicCache.TryGetValue(cellId, out WalkFrameStaticRecords cached)) @@ -285,6 +299,20 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData return records; } + public WalkFrameStaticRecords GetOutdoorObjects(uint cellId) + { + if (_outdoorObjectsMaterialized.TryGetValue( + cellId, + out WalkFrameStaticRecords cached)) + { + return cached; + } + + WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: null); + _outdoorObjectsMaterialized[cellId] = records; + return records; + } + /// /// Campaign OVERHAUL S2 chunk 5: one cell id's borrowed retail render /// membership. Reads @@ -303,7 +331,7 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData /// entity into every crossed cell under its own true id, so no /// additional indoor/outdoor dispatch is needed here. /// - private WalkFrameStaticRecords ResolveCellView(uint cellId, bool dynamic) + private WalkFrameStaticRecords ResolveCellView(uint cellId, bool? dynamic) { IReadOnlyList entries = _shadows.GetRetailPartEntriesInCell(cellId); @@ -335,7 +363,8 @@ internal sealed class WalkProductionWorldData : IWalkFrameWorldData } if (record.EntityPayload.IsBuildingShell) continue; // buildings draw at their own shell turn. - if (IsDynamicProjectionClass(record.ProjectionClass) != dynamic) + if (dynamic.HasValue + && IsDynamicProjectionClass(record.ProjectionClass) != dynamic.Value) continue; if (written == _cellViewScratch.Length) diff --git a/src/AcDream.App/Rendering/Walk/WalkStaticStreamPopulator.cs b/src/AcDream.App/Rendering/Walk/WalkStaticStreamPopulator.cs index 54d543cf..67ac60db 100644 --- a/src/AcDream.App/Rendering/Walk/WalkStaticStreamPopulator.cs +++ b/src/AcDream.App/Rendering/Walk/WalkStaticStreamPopulator.cs @@ -18,14 +18,11 @@ namespace AcDream.App.Rendering.Walk; /// shell over terrain) — and retail resolves those by first-drawn-wins in /// ITS walk/cell-content order, not by any texture/material grouping. So /// this populator appends one per (entity, -/// opaque batch) in the SAME order the caller's records span presents -/// them — never material-grouped, never re-sorted. Translucent batches go to -/// the SAME the classic/packed paths already -/// use (global far→near re-sort still applies; walk-order submission only -/// improves retail's submission-order tie-break fidelity for coincident -/// distances — WbDrawDispatcher.DeferTransparentGroups's own doc -/// comment cites the same CShadowPart::insertion_sort stability this -/// preserves). +/// opaque batch) in the per-cell stable far→near part order reconstructed +/// from authored GfxObj SortCenter. Opaque/cutout and delayed projections +/// derive from that same order. Translucent batches then enter the SAME two +/// FIFO lists as particles; the queues never +/// perform a scope-global sort. /// /// Reads no retained scene state itself: every method takes the target /// and a caller-supplied span of already @@ -40,6 +37,12 @@ internal sealed class WalkStaticStreamPopulator private readonly WbDrawDispatcher _dispatcher; private readonly List _batchScratch = new(); private readonly List _selectionScratch = new(); + private readonly List _cellBatchScratch = new(); + + private readonly record struct CellBatch( + WbDrawDispatcher.WalkClassifiedBatch Batch, + WalkDrawStage Stage, + uint LocalEntityId); internal WalkStaticStreamPopulator(WbDrawDispatcher dispatcher) { @@ -147,6 +150,120 @@ internal sealed class WalkStaticStreamPopulator } } + /// + /// S4-c3a: classifies the cell's combined static/dynamic registry view, + /// computes every subset's CYpt from its authored part sort center, + /// performs retail's stable descending insertion sort, and projects the + /// same order to opaque commands and delayed records. Equal keys retain + /// registry/part/subset order because only a strictly farther record is + /// moved left. + /// + /// AP-243: this always-authored per-part key is deliberately more + /// exact than retail's greater-than-50 m branch. Retail + /// UpdateObjCell @0x005A06B7..0x005A0720 gives every part the + /// shared cell CYpt/heading there, preserving insertion order; acquiring + /// that cell-distance/heading LOD input is outside this bounded repair. + /// + internal void PopulateCellObjects( + OrderedDrawStream stream, + WalkDrawStage staticStage, + uint cellId, + ReadOnlySpan records, + uint tupleLandblockId, + Vector3 cameraWorldPosition, + Matrix4x4 viewProjection, + IWalkLookInViewSource? views, + int viewRouteIndex, + List alphaSubmissions) + { + ArgumentNullException.ThrowIfNull(stream); + ArgumentNullException.ThrowIfNull(alphaSubmissions); + _cellBatchScratch.Clear(); + + for (int recordIndex = 0; recordIndex < records.Length; recordIndex++) + { + ref readonly RenderProjectionRecord record = ref records[recordIndex]; + bool liveDynamic = record.ProjectionClass is RenderProjectionClass.LiveDynamicRoot + or RenderProjectionClass.EquippedChild; + WalkDrawStage stage = liveDynamic ? WalkDrawStage.Dynamic : staticStage; + _batchScratch.Clear(); + _selectionScratch.Clear(); + _dispatcher.ClassifyEntityForWalk( + in record, + tupleLandblockId, + _batchScratch, + _selectionScratch, + liveDynamic, + views, + viewRouteIndex, + cellId, + diagnosticViewProjection: viewProjection); + + for (int batchIndex = 0; batchIndex < _batchScratch.Count; batchIndex++) + { + WbDrawDispatcher.WalkClassifiedBatch batch = _batchScratch[batchIndex]; + Vector3 worldSortCenter = Vector3.Transform(batch.LocalSortCenter, batch.Transform); + batch = batch with + { + SortDistanceSq = Vector3.DistanceSquared( + worldSortCenter, cameraWorldPosition), + }; + _cellBatchScratch.Add(new CellBatch( + batch, + stage, + record.Source.LocalEntityId)); + } + + for (int selectionIndex = 0; selectionIndex < _selectionScratch.Count; selectionIndex++) + { + WbDrawDispatcher.WalkClassifiedSelectionPart part = + _selectionScratch[selectionIndex]; + _dispatcher.PublishWalkSelectionPart(in part); + } + } + + StableSortCellBatches(_cellBatchScratch); + for (int i = 0; i < _cellBatchScratch.Count; i++) + { + CellBatch item = _cellBatchScratch[i]; + WbDrawDispatcher.WalkClassifiedBatch batch = item.Batch; + if (batch.IsOpaque) + { + int commandIndex = stream.Count; + stream.Append(new OrderedDrawCommand( + batch.Key, batch.Transform, item.Stage, cellId, batch.ClipSlot, + batch.Lights, batch.IndoorFlag, batch.Alpha, + batch.SelectionLighting, batch.DetailCategory)); + _dispatcher.ProbeFacilityStairCommandAppended( + commandIndex, + item.LocalEntityId, + cellId, + item.Stage, + in batch); + } + else + { + alphaSubmissions.Add(batch); + } + } + } + + private static void StableSortCellBatches(List batches) + { + for (int i = 1; i < batches.Count; i++) + { + CellBatch value = batches[i]; + int insertion = i; + while (insertion > 0 + && value.Batch.SortDistanceSq > batches[insertion - 1].Batch.SortDistanceSq) + { + batches[insertion] = batches[insertion - 1]; + insertion--; + } + batches[insertion] = value; + } + } + private void ClassifyAndAppend( OrderedDrawStream stream, WalkDrawStage stage, @@ -195,7 +312,7 @@ internal sealed class WalkStaticStreamPopulator if (alphaSubmissions is null) { _dispatcher.SubmitWalkAlphaInstance( - in batch, cameraWorldPosition, viewProjection); + in batch, viewProjection); } else { diff --git a/src/AcDream.App/Rendering/Wb/GroupKey.cs b/src/AcDream.App/Rendering/Wb/GroupKey.cs index e5283094..7a7d8981 100644 --- a/src/AcDream.App/Rendering/Wb/GroupKey.cs +++ b/src/AcDream.App/Rendering/Wb/GroupKey.cs @@ -20,7 +20,10 @@ namespace AcDream.App.Rendering.Wb; /// key's VALUE never orders anything: groups are enumerated in the dictionary's /// insertion order and sorted by cull mode then camera distance /// (CompareOpaqueSubmissionOrder / CompareTransparentSubmissionOrder), -/// and the delayed-alpha path sorts by viewer distance then submission ordinal. +/// while the normal-world delayed-alpha path orders authored part centers +/// stably within each cell before appending to two FIFO lists. The grouped +/// fallback restores submission ordinal after material grouping; it is not a +/// scope-global world-alpha distance sort. /// The key reaches only equality, hashing, and the scene-digest fingerprints. /// /// Campaign VM VM6 review fix round: joined diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs index 32ed5467..995dd83f 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.WalkClassify.cs @@ -146,8 +146,10 @@ public sealed partial class WbDrawDispatcher /// (the authored GfxObj sort center — the same value /// InstanceGroup.LocalSortCenters carries per instance today; S4-c2 /// deleted its one alpha-ordering consumer, RetailAlphaOrdering.ComputeViewerDistance, - /// but the field itself stays: InstanceGroup.LocalSortCenters is a - /// general per-instance record this type mirrors, not an alpha-only one). + /// but S4-c3a restores its retail use: WalkStaticStreamPopulator + /// transforms it through the part model and retains the resulting + /// squared CYpt key in before the per-cell + /// stable sort. The key feeds two FIFO alpha lists; neither queue sorts. /// internal readonly record struct WalkClassifiedBatch( GroupKey Key, @@ -159,7 +161,8 @@ public sealed partial class WbDrawDispatcher Vector2 SelectionLighting, uint DetailCategory, bool IsOpaque, - Vector3 LocalSortCenter); + Vector3 LocalSortCenter, + float SortDistanceSq = 0f); /// /// One retail-picking part surfaced by @@ -889,10 +892,8 @@ public sealed partial class WbDrawDispatcher /// internal void SubmitWalkAlphaInstance( in WalkClassifiedBatch batch, - Vector3 cameraWorldPosition, Matrix4x4 viewProjection) { - _ = cameraWorldPosition; // S4-c2: retail's queues are FIFO, not distance-sorted. RetailAlphaQueue queue = _alphaQueue ?? throw new InvalidOperationException( "SubmitWalkAlphaInstance requires an active RetailAlphaQueue."); diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 952eb46b..3dcfbb04 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -47,9 +47,12 @@ namespace AcDream.App.Rendering.Wb; /// DrawElementsIndirectCommand. Per-frame ring allocations carry instance /// matrices (binding 0), per-group batch metadata/texture-table slots (binding /// 1), and the indirect draw commands. Opaque world groups remain MDI-batched. -/// Transparent world instances enter so ordinary -/// GfxObj parts and particles share retail's stable far-to-near stream; sealed -/// off-screen consumers retain the immediate transparent MDI path. +/// Transparent world instances enter after +/// WalkFrameDriver reconstructs retail's stable far-to-near order +/// independently for each object-cell turn from authored part SortCenter. +/// Object and particle records are merged before they append to retail's two +/// FIFO lists; sealed off-screen consumers retain the immediate transparent +/// MDI path. /// /// /// @@ -2419,14 +2422,11 @@ public sealed partial class WbDrawDispatcher : IDisposable throw new InvalidOperationException( "One retail alpha scope cannot combine different view-projection matrices."); - // Retail CShadowPart::insertion_sort (0x006B5130) is stable: - // equal-CYpt parts keep the order in which the cell submitted them. // Material grouping is an acdream batching detail and must not become - // that tiebreak. Reconstruct the original draw-local instance order - // before handing entries to the queue — S4-c2: retail's own queue is - // FIFO (D3DPolyRender::AddMeshToAlphaList never sorts), so preserving - // this walk-order sequence IS the ordering, not a sort key for a - // later comparator. + // a tiebreak. Reconstruct original draw-local submission order before + // handing entries to retail's FIFO queue. The normal-world walk path + // does not use this grouped route: WalkStaticStreamPopulator performs + // the per-cell stable CYpt sort before its per-instance submissions. _alphaFingerprintScratch.Clear(); foreach (InstanceGroup group in _translucentDraws) { diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs index 660ec636..e09fdb3c 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkFrameDriverTests.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Collections; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Numerics; @@ -12,6 +13,7 @@ using AcDream.App.Rendering.Walk; using AcDream.App.Tests.Rendering.Gpu; using AcDream.Content; using AcDream.Core.Meshing; +using AcDream.Core.Vfx; using AcDream.Core.World; using DatReaderWriter; using DatReaderWriter.DBObjs; @@ -130,9 +132,17 @@ public sealed partial class WalkFrameDriverTests alpha.Flush(RetailAlphaFlushSite.SortCellExit, 0.75f); } - public void DrawStaticParticles(uint cellId) => log.Add($"PARTICLES:{cellId:x8}"); + public ReadOnlySpan PrepareStaticParticles(uint cellId) + { + log.Add($"PARTICLES:{cellId:x8}"); + return ReadOnlySpan.Empty; + } - public void DrawCellParticles(uint cellId) => log.Add($"CELL-PARTICLES:{cellId:x8}"); + public ReadOnlySpan PrepareCellParticles(uint cellId) + { + log.Add($"CELL-PARTICLES:{cellId:x8}"); + return ReadOnlySpan.Empty; + } } private sealed class RecordingTrace(List log) : IWalkFrameDriverTrace @@ -141,6 +151,39 @@ public sealed partial class WalkFrameDriverTests log.Add($"FLUSH:{commandCount}:{string.Join(',', stages.Distinct())}"); } + private sealed class ProductionParticleLeaf( + ParticleSystem particles, + ParticleRenderer renderer, + ICamera camera, + Vector3 cameraWorldPosition) : IWalkFrameLeafRenderer + { + public void DrawSky() { } + public void DrawLandCellBatch( + IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells) { } + public bool HasRenderableEmittersInCell(uint cellId) => + particles.HasRenderableEmittersInCell(ParticleRenderPass.Scene, cellId); + public void DrawCellShell(uint cellId) { } + public ReadOnlySpan PrepareStaticParticles(uint cellId) => + renderer.PrepareForCellAlpha( + camera, cameraWorldPosition, ParticleRenderPass.Scene, cellId); + public ReadOnlySpan PrepareCellParticles(uint cellId) => + renderer.PrepareForCellAlpha( + camera, cameraWorldPosition, ParticleRenderPass.Scene, cellId); + public void ClearInteriorDepth() { } + public void FlushLandscape() { } + public int DrawExitSeals() => 0; + public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) { } + public void AlphaBarrier() { } + public void FlushSortCellExit() { } + } + + private sealed class IdentityCamera : ICamera + { + public Matrix4x4 View => Matrix4x4.Identity; + public Matrix4x4 Projection => Matrix4x4.Identity; + public float Aspect { get; set; } = 1f; + } + private sealed class FakeWorldData : IWalkFrameWorldData { public readonly Dictionary CellStaticsByCell = new(); @@ -153,12 +196,22 @@ public sealed partial class WalkFrameDriverTests public WalkFrameStaticRecords GetCellStatics(uint cellId) => CellStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty); + public WalkFrameStaticRecords GetCellObjects(uint cellId) => + Combine( + CellStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty), + CellDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty)); + public WalkFrameStaticRecords GetCellDynamics(uint cellId) => CellDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty); public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) => OutdoorStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty); + public WalkFrameStaticRecords GetOutdoorObjects(uint cellId) => + Combine( + OutdoorStaticsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty), + OutdoorDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty)); + public WalkFrameStaticRecords GetOutdoorDynamics(uint cellId) => OutdoorDynamicsByCell.GetValueOrDefault(cellId, WalkFrameStaticRecords.Empty); @@ -167,6 +220,25 @@ public sealed partial class WalkFrameDriverTests public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building) => WorldTransformByBuilding.GetValueOrDefault(building, Matrix4x4.Identity); + + private static WalkFrameStaticRecords Combine( + WalkFrameStaticRecords first, + WalkFrameStaticRecords second) + { + if (second.Records.Count == 0) + return first; + RenderProjectionRecord[] dynamicRecords = second.Records + .Select(record => record with + { + ProjectionClass = RenderProjectionClass.LiveDynamicRoot, + }) + .ToArray(); + if (first.Records.Count == 0) + return new WalkFrameStaticRecords(dynamicRecords, second.TupleLandblockId); + return new WalkFrameStaticRecords( + first.Records.Concat(dynamicRecords).ToArray(), + first.TupleLandblockId); + } } // ── The walk-level test context (interior flood + building portal pass) ─ @@ -1001,7 +1073,9 @@ public sealed partial class WalkFrameDriverTests new[] { "ALPHA", "PUNCH:4@v0", "SHELL:00000104", - "FLUSH:1:LookInStatic", "FLUSH:1:Dynamic", + // S4-c3a: one cell shadow list, so static + dynamic are one + // sorted stream segment rather than two artificial blocks. + "FLUSH:2:LookInStatic,Dynamic", "CELL-PARTICLES:00000104", "FLUSH:1:BuildingShell", }, @@ -1805,6 +1879,205 @@ public sealed partial class WalkFrameDriverTests } } + [Fact] + public void CellTurn_RealParticlePreparationMergesWithObjectAlphaByCypt() + { + using var fx = new DispatcherFixture(); + const uint cellId = 0x8C040005u; + const ulong objectGfx = 0x0200_0C31UL; + InjectRenderData(fx.Manager, objectGfx, MakeFlatMesh( + MakeBatch(0x08100C31u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1))); + var worldData = new FakeWorldData(); + worldData.OutdoorStaticsByCell[cellId] = new WalkFrameStaticRecords( + new[] + { + MakeRecord(1, 0, new Vector3(20, 0, 0), + [new MeshRef((uint)objectGfx, Matrix4x4.Identity)]), + MakeRecord(2, 0, new Vector3(40, 0, 0), + [new MeshRef((uint)objectGfx, Matrix4x4.Identity)]), + }, + 0x8C04u); + + var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42)); + EmitterDesc desc = new() + { + DatId = 0x32000C31u, + Type = AcDream.Core.Vfx.ParticleType.Still, + MaxParticles = 1, + InitialParticles = 1, + LifetimeMin = 100f, + LifetimeMax = 100f, + StartAlpha = 1f, + EndAlpha = 1f, + }; + int particle30 = particles.SpawnEmitter(desc, new Vector3(30, 0, 0)); + // Equal to object 1: AP-242's deterministic source tie is object-first. + int particle20 = particles.SpawnEmitter(desc, new Vector3(20, 0, 0)); + particles.UpdateEmitterOwnerCell(particle30, cellId); + particles.UpdateEmitterOwnerCell(particle20, cellId); + using var renderer = new ParticleRenderer( + fx.Device, + fx.FrameLifetime, + fx.Scope, + particles, + meshAdapter: fx.MeshAdapter, + alphaQueue: fx.AlphaQueue); + var leaf = new ProductionParticleLeaf( + particles, renderer, new IdentityCamera(), Vector3.Zero); + var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData); + var ctx = new TestContext(); + IWalkEventSink sink = driver; + + using DrawScope draw = fx.BeginDraw(beginAlpha: true); + renderer.BeginFrame(frameSlot: 0); + driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero); + var activeViews = new WalkPortalView(); + WalkCopyView.AppendFullViewportQuad( + activeViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight); + sink.OnLandscapeViews(activeViews); + sink.OnLandscapeCellTurn(cellId); + driver.EndFrame(); + driver.Replay(draw.Frame, draw.Pass); + + List entries = QueueAlphaEntries(fx.AlphaQueue); + Assert.Equal(4, entries.Count); + Assert.Equal( + [ + typeof(WbDrawDispatcher), + typeof(ParticleRenderer), + typeof(WbDrawDispatcher), + typeof(ParticleRenderer), + ], + entries.Select(static entry => entry.Source.GetType().DeclaringType)); + + fx.AlphaQueue.AbortFrame(); + } + + [Fact] + public void SeparateCellTurnsRemainCellMajorWhenLaterCellIsFarther() + { + using var fx = new DispatcherFixture(); + const uint firstCell = 0x8C040005u; + const uint secondCell = 0x8C040006u; + const ulong objectGfx = 0x0200_0C32UL; + InjectRenderData(fx.Manager, objectGfx, MakeFlatMesh( + MakeBatch(0x08100C32u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1))); + var worldData = new FakeWorldData(); + worldData.OutdoorStaticsByCell[firstCell] = new WalkFrameStaticRecords( + new[] { MakeRecord(1, 0, new Vector3(5, 0, 0), + [new MeshRef((uint)objectGfx, Matrix4x4.Identity)]) }, + 0x8C04u); + worldData.OutdoorStaticsByCell[secondCell] = new WalkFrameStaticRecords( + new[] { MakeRecord(2, 0, new Vector3(50, 0, 0), + [new MeshRef((uint)objectGfx, Matrix4x4.Identity)]) }, + 0x8C04u); + var log = new List(); + var leaf = new RecordingLeafRenderer(log, fx.AlphaQueue); + leaf.CellsWithoutEmitters.UnionWith([firstCell, secondCell]); + var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData); + var ctx = new TestContext(); + IWalkEventSink sink = driver; + + using DrawScope draw = fx.BeginDraw(beginAlpha: true); + driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero); + var activeViews = new WalkPortalView(); + WalkCopyView.AppendFullViewportQuad( + activeViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight); + sink.OnLandscapeViews(activeViews); + sink.OnLandscapeCellTurn(firstCell); + sink.OnLandscapeCellTurn(secondCell); + driver.EndFrame(); + driver.Replay(draw.Frame, draw.Pass); + + Assert.Equal(2, QueueAlphaEntries(fx.AlphaQueue).Count); + IList payload = (IList)typeof(WbDrawDispatcher).GetField( + "_deferredAlpha", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(fx.Dispatcher)!; + Matrix4x4 firstModel = (Matrix4x4)payload[0]!.GetType().GetProperty("Model")! + .GetValue(payload[0])!; + Matrix4x4 secondModel = (Matrix4x4)payload[1]!.GetType().GetProperty("Model")! + .GetValue(payload[1])!; + Assert.Equal(5f, firstModel.M41); + Assert.Equal(50f, secondModel.M41); + + fx.AlphaQueue.AbortFrame(); + } + + [Fact] + public void ProductionCellObjectParticleMerge_WarmedPathAllocatesZeroBytes() + { + using var fx = new DispatcherFixture(); + const uint cellId = 0x8C040007u; + const ulong objectGfx = 0x0200_0C33UL; + InjectRenderData(fx.Manager, objectGfx, MakeFlatMesh( + MakeBatch(0x08100C33u, TranslucencyKind.AlphaBlend, 0, 0, 3, 1))); + var worldData = new FakeWorldData(); + worldData.OutdoorStaticsByCell[cellId] = new WalkFrameStaticRecords( + new[] { MakeRecord(1, 0, new Vector3(40, 0, 0), + [new MeshRef((uint)objectGfx, Matrix4x4.Identity)]) }, + 0x8C04u); + + var particles = new ParticleSystem(new EmitterDescRegistry(), new Random(42)); + int handle = particles.SpawnEmitter( + new EmitterDesc + { + DatId = 0x32000C33u, + Type = AcDream.Core.Vfx.ParticleType.Still, + MaxParticles = 1, + InitialParticles = 1, + LifetimeMin = 100f, + LifetimeMax = 100f, + StartAlpha = 1f, + EndAlpha = 1f, + }, + new Vector3(20, 0, 0)); + particles.UpdateEmitterOwnerCell(handle, cellId); + using var renderer = new ParticleRenderer( + fx.Device, + fx.FrameLifetime, + fx.Scope, + particles, + meshAdapter: fx.MeshAdapter, + alphaQueue: fx.AlphaQueue); + var leaf = new ProductionParticleLeaf( + particles, renderer, new IdentityCamera(), Vector3.Zero); + var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData); + var ctx = new TestContext(); + IWalkEventSink sink = driver; + var activeViews = new WalkPortalView(); + WalkCopyView.AppendFullViewportQuad( + activeViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight); + + using DrawScope draw = fx.BeginDraw(); + fx.Dispatcher.BeginFrame(frameSlot: 0); + renderer.BeginFrame(frameSlot: 0); + fx.Device.RecordingEnabled = false; + + void RunCellTurn() + { + fx.AlphaQueue.BeginFrame(); + driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero); + sink.OnLandscapeViews(activeViews); + sink.OnLandscapeCellTurn(cellId); + driver.EndFrame(); + driver.Replay(draw.Frame, draw.Pass); + fx.AlphaQueue.AbortFrame(); + } + + long allocated = ZeroAllocationProbe.MeasureWarmed( + RunCellTurn, + batchSize: 128, + warmupBatches: 2, + samples: 4); + + Assert.Equal(0, allocated); + } + + private static List QueueAlphaEntries(RetailAlphaQueue queue) => + (List)typeof(RetailAlphaQueue).GetField( + "_alpha", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(queue)!; + // ── 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 @@ -2217,6 +2490,8 @@ public sealed partial class WalkFrameDriverTests public ObjectMeshManager Manager => _meshAdapter.MeshManager!; + public WbMeshAdapter MeshAdapter => _meshAdapter; + public DrawScope BeginDraw(bool beginAlpha = false) { if (beginAlpha) diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs index 17121936..c0b03a0a 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkStaticStreamPopulatorTests.cs @@ -120,6 +120,11 @@ public sealed class WalkStaticStreamPopulatorTests private static ObjectRenderData MakeFlatMesh(params ObjectRenderBatch[] batches) => new() { Batches = new List(batches) }; + private static ObjectRenderData MakeSortedMesh( + Vector3 sortCenter, + params ObjectRenderBatch[] batches) => + new() { SortCenter = sortCenter, Batches = new List(batches) }; + // ── Reflection seam: ObjectMeshManager owns no test-injection API, and // driving real GPU/GfxObj upload for a unit test is out of this stage's // scope — ObjectRenderData/ObjectRenderBatch are plain settable classes, @@ -135,6 +140,224 @@ public sealed class WalkStaticStreamPopulatorTests dict[id] = data; } + [Fact] + public void PopulateCellObjects_UsesAuthoredSortCenterAndStableFarToNearOrder() + { + using var fx = new DispatcherFixture(); + const ulong nearOrigin = 0x0100_0C01UL; + const ulong farAuthoredCenter = 0x0100_0C02UL; + InjectRenderData(fx.Manager, nearOrigin, MakeSortedMesh( + Vector3.Zero, + MakeBatch(101, TranslucencyKind.AlphaBlend, 101, 0, 3, 1))); + InjectRenderData(fx.Manager, farAuthoredCenter, MakeSortedMesh( + new Vector3(10, 0, 0), + MakeBatch(202, TranslucencyKind.AlphaBlend, 202, 0, 3, 2))); + + RenderProjectionRecord[] records = + [ + MakeRecord(1, 0, new Vector3(5, 0, 0), [new MeshRef((uint)nearOrigin, Matrix4x4.Identity)]), + MakeRecord(2, 0, Vector3.Zero, [new MeshRef((uint)farAuthoredCenter, Matrix4x4.Identity)]), + ]; + var alpha = new List(); + var stream = new OrderedDrawStream(); + var populator = new WalkStaticStreamPopulator(fx.Dispatcher); + + populator.PopulateCellObjects( + stream, + WalkDrawStage.CellStatic, + 0x8C040112u, + records, + 0x8C04u, + Vector3.Zero, + Matrix4x4.Identity, + views: null, + viewRouteIndex: -1, + alpha); + + Assert.Empty(stream.Keys); + Assert.Equal([202u, 101u], alpha.Select(static batch => batch.Key.FirstIndex)); + Assert.Equal([100f, 25f], alpha.Select(static batch => batch.SortDistanceSq)); + } + + [Fact] + public void PopulateCellObjects_EqualCyptRetainsEntityPartAndSubsetOrder() + { + using var fx = new DispatcherFixture(); + const ulong setup = 0x1000_0C10UL; + const ulong firstPart = 0x0100_0C11UL; + const ulong secondPart = 0x0100_0C12UL; + InjectRenderData(fx.Manager, firstPart, MakeSortedMesh( + Vector3.Zero, + MakeBatch(11, TranslucencyKind.AlphaBlend, 11, 0, 3, 1), + MakeBatch(12, TranslucencyKind.AlphaBlend, 12, 0, 3, 2))); + InjectRenderData(fx.Manager, secondPart, MakeSortedMesh( + Vector3.Zero, + MakeBatch(21, TranslucencyKind.AlphaBlend, 21, 0, 3, 3), + MakeBatch(22, TranslucencyKind.AlphaBlend, 22, 0, 3, 4))); + InjectRenderData(fx.Manager, setup, new ObjectRenderData + { + IsSetup = true, + SetupParts = + [ + (firstPart, Matrix4x4.CreateTranslation(10, 0, 0)), + (secondPart, Matrix4x4.CreateTranslation(0, 10, 0)), + ], + }); + RenderProjectionRecord record = MakeRecord( + 10, 0, Vector3.Zero, [new MeshRef((uint)setup, Matrix4x4.Identity)]); + var alpha = new List(); + + new WalkStaticStreamPopulator(fx.Dispatcher).PopulateCellObjects( + new OrderedDrawStream(), + WalkDrawStage.CellStatic, + 0x8C040112u, + [record], + 0x8C04u, + Vector3.Zero, + Matrix4x4.Identity, + views: null, + viewRouteIndex: -1, + alpha); + + Assert.Equal([11u, 12u, 21u, 22u], alpha.Select(static batch => batch.Key.FirstIndex)); + Assert.All(alpha, static batch => Assert.Equal(100f, batch.SortDistanceSq)); + } + + [Fact] + public void PopulateCellObjects_InterleavesStaticAndDynamicOpaquePartsByCypt() + { + using var fx = new DispatcherFixture(); + const ulong nearStatic = 0x0100_0C21UL; + const ulong farDynamic = 0x0100_0C22UL; + const ulong middleStatic = 0x0100_0C23UL; + InjectRenderData(fx.Manager, nearStatic, MakeFlatMesh( + MakeBatch(1, TranslucencyKind.Opaque, 1, 0, 3, 1))); + InjectRenderData(fx.Manager, farDynamic, MakeFlatMesh( + MakeBatch(2, TranslucencyKind.Opaque, 2, 0, 3, 2))); + InjectRenderData(fx.Manager, middleStatic, MakeFlatMesh( + MakeBatch(3, TranslucencyKind.Opaque, 3, 0, 3, 3))); + RenderProjectionRecord dynamic = MakeRecord( + 2, 0, new Vector3(30, 0, 0), [new MeshRef((uint)farDynamic, Matrix4x4.Identity)]) + with { ProjectionClass = RenderProjectionClass.LiveDynamicRoot }; + RenderProjectionRecord[] records = + [ + MakeRecord(1, 0, new Vector3(10, 0, 0), [new MeshRef((uint)nearStatic, Matrix4x4.Identity)]), + dynamic, + MakeRecord(3, 0, new Vector3(20, 0, 0), [new MeshRef((uint)middleStatic, Matrix4x4.Identity)]), + ]; + var stream = new OrderedDrawStream(); + + new WalkStaticStreamPopulator(fx.Dispatcher).PopulateCellObjects( + stream, + WalkDrawStage.CellStatic, + 0x8C040112u, + records, + 0x8C04u, + Vector3.Zero, + Matrix4x4.Identity, + views: null, + viewRouteIndex: -1, + []); + + Assert.Equal([2u, 3u, 1u], stream.Keys.Select(static key => key.FirstIndex)); + Assert.Equal( + [WalkDrawStage.Dynamic, WalkDrawStage.CellStatic, WalkDrawStage.CellStatic], + stream.Stages); + } + + [Fact] + public void WorldAlphaCyptContract_RetainsTheKeyWithoutDeadCameraSubmitThreading() + { + Type batchType = typeof(WbDrawDispatcher).GetNestedType( + "WalkClassifiedBatch", BindingFlags.NonPublic) + ?? throw new InvalidOperationException("WalkClassifiedBatch was not found."); + PropertyInfo sortDistance = batchType.GetProperty("SortDistanceSq") + ?? throw new InvalidOperationException("SortDistanceSq was not retained."); + Assert.Equal(typeof(float), sortDistance.PropertyType); + + MethodInfo submit = typeof(WbDrawDispatcher).GetMethod( + "SubmitWalkAlphaInstance", BindingFlags.NonPublic | BindingFlags.Instance) + ?? throw new InvalidOperationException("SubmitWalkAlphaInstance was not found."); + ParameterInfo[] parameters = submit.GetParameters(); + Assert.Equal(2, parameters.Length); + Assert.Equal("batch", parameters[0].Name); + Assert.Equal(typeof(Matrix4x4), parameters[1].ParameterType); + Assert.DoesNotContain(parameters, static parameter => parameter.ParameterType == typeof(Vector3)); + + string root = FindRepoRoot(); + string populator = File.ReadAllText(Path.Combine( + root, "src", "AcDream.App", "Rendering", "Walk", "WalkStaticStreamPopulator.cs")); + Assert.Contains("batch.LocalSortCenter, batch.Transform", populator); + Assert.Contains("SortDistanceSq = Vector3.DistanceSquared", populator); + Assert.Contains("value.Batch.SortDistanceSq >", populator); + Assert.Contains("AP-243", populator); + Assert.Contains("greater-than-50 m", populator); + Assert.Contains("0x005A06B7..0x005A0720", populator); + } + + [Fact] + public void WorldAlphaCyptDocumentationAndRegister_PinPerCellTruthAndThreeResiduals() + { + string root = FindRepoRoot(); + string inventory = File.ReadAllText(Path.Combine( + root, "docs", "architecture", "worldbuilder-inventory.md")); + string architecture = File.ReadAllText(Path.Combine( + root, "docs", "architecture", "acdream-architecture.md")); + string registerPath = Path.Combine( + root, "docs", "architecture", "retail-divergence-register.md"); + string register = File.ReadAllText(registerPath); + + Assert.Contains("per-cell", inventory, StringComparison.OrdinalIgnoreCase); + Assert.Contains("two FIFO", inventory, StringComparison.Ordinal); + Assert.DoesNotContain( + "distance-sorts one shared queue", inventory, StringComparison.OrdinalIgnoreCase); + Assert.Contains("per-cell", architecture, StringComparison.OrdinalIgnoreCase); + Assert.Contains("two FIFO", architecture, StringComparison.Ordinal); + Assert.DoesNotContain( + "one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase); + + Assert.Contains("162 active rows", register, StringComparison.Ordinal); + int apSectionStart = register.IndexOf( + "## 3. Documented approximation (AP)", StringComparison.Ordinal); + int apSectionEnd = register.IndexOf( + "## 4. Temporary stopgap (TS)", StringComparison.Ordinal); + Assert.True(apSectionStart >= 0, "AP section heading must exist."); + Assert.True(apSectionEnd > apSectionStart, "TS heading must follow the AP section."); + foreach (string id in new[] { "AP-241", "AP-242", "AP-243" }) + { + int rowIndex = register.IndexOf($"| {id} |", StringComparison.Ordinal); + Assert.True( + rowIndex > apSectionStart && rowIndex < apSectionEnd, + $"{id} row must be inside the AP table before the TS section; " + + $"rowIndex={rowIndex}, AP=[{apSectionStart},{apSectionEnd})."); + } + Assert.Single(File.ReadLines(registerPath), static line => + line.StartsWith("| AP-241 |", StringComparison.Ordinal)); + Assert.Single(File.ReadLines(registerPath), static line => + line.StartsWith("| AP-242 |", StringComparison.Ordinal)); + Assert.Single(File.ReadLines(registerPath), static line => + line.StartsWith("| AP-243 |", StringComparison.Ordinal)); + Assert.Equal( + 162, + File.ReadLines(registerPath).Count(static line => + line.StartsWith("| AP-", StringComparison.Ordinal))); + Assert.Contains("greater than 50 m", register, StringComparison.Ordinal); + Assert.Contains("0x005A06B7..0x005A0720", register, StringComparison.Ordinal); + } + + private static string FindRepoRoot() + { + string? directory = AppContext.BaseDirectory; + while (directory is not null) + { + if (File.Exists(Path.Combine(directory, "AcDream.slnx"))) + return directory; + directory = Directory.GetParent(directory)?.FullName; + } + + throw new DirectoryNotFoundException("Could not locate AcDream.slnx."); + } + // ── Deliverable 1: ClassifyEntityForWalk data equivalence ───────────── [Fact] @@ -573,13 +796,12 @@ public sealed class WalkStaticStreamPopulatorTests var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend, FoliageFlags: 0); Vector3 localSortCenter = new(1, 2, 3); Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6); - var cameraWorldPosition = Vector3.Zero; var batch = new WbDrawDispatcher.WalkClassifiedBatch( key, model, ClipSlot: 7, WbDrawDispatcher.InstanceLightSet.Disabled, IndoorFlag: 1, Alpha: 0.5f, SelectionLighting: new Vector2(0.25f, 0.75f), DetailCategory: 1, IsOpaque: false, LocalSortCenter: localSortCenter); - fx.Dispatcher.SubmitWalkAlphaInstance(in batch, cameraWorldPosition, Matrix4x4.Identity); + fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Matrix4x4.Identity); Assert.Equal(1, fx.AlphaQueue.PendingCount); Assert.Equal(1, fx.AlphaQueue.AlphaCount); @@ -614,7 +836,6 @@ public sealed class WalkStaticStreamPopulatorTests WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(detailCategory: 1u); on.Dispatcher.SubmitWalkAlphaInstance( in building, - Vector3.Zero, Matrix4x4.Identity); Assert.Equal(0, on.AlphaQueue!.PendingCount); @@ -636,7 +857,6 @@ public sealed class WalkStaticStreamPopulatorTests WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(detailCategory: 1u); off.Dispatcher.SubmitWalkAlphaInstance( in building, - Vector3.Zero, Matrix4x4.Identity); Assert.Equal(1, off.AlphaQueue!.PendingCount); @@ -656,7 +876,6 @@ public sealed class WalkStaticStreamPopulatorTests WbDrawDispatcher.WalkClassifiedBatch batch = AlphaWalkBatch(detailCategory: 0u); ordinary.Dispatcher.SubmitWalkAlphaInstance( in batch, - Vector3.Zero, Matrix4x4.Identity); Assert.Equal(1, ordinary.AlphaQueue!.PendingCount); @@ -750,11 +969,11 @@ public sealed class WalkStaticStreamPopulatorTests key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10)); - fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Vector3.Zero, Matrix4x4.Identity); + fx.Dispatcher.SubmitWalkAlphaInstance(in batch, Matrix4x4.Identity); Assert.Throws(() => fx.Dispatcher.SubmitWalkAlphaInstance( - in batch, Vector3.Zero, Matrix4x4.CreateTranslation(1, 0, 0))); + in batch, Matrix4x4.CreateTranslation(1, 0, 0))); fx.AlphaQueue.AbortFrame(); } diff --git a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs index 7a31a6ef..c755c137 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs @@ -218,8 +218,10 @@ public sealed partial class WalkTraceConformanceTests IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells) { } public bool HasRenderableEmittersInCell(uint cellId) => false; public void DrawCellShell(uint cellId) { } - public void DrawStaticParticles(uint cellId) { } - public void DrawCellParticles(uint cellId) { } + public ReadOnlySpan PrepareStaticParticles(uint cellId) => + ReadOnlySpan.Empty; + public ReadOnlySpan PrepareCellParticles(uint cellId) => + ReadOnlySpan.Empty; public void ClearInteriorDepth() { } public void FlushLandscape() => RecordFlush(RetailAlphaFlushSite.LandscapeFlush, 0f); public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) { } @@ -275,6 +277,8 @@ public sealed partial class WalkTraceConformanceTests /// query returns the shared empty record. private sealed class EmptyAlphaDepthWorldData : IWalkFrameWorldData { + public WalkFrameStaticRecords GetCellObjects(uint cellId) => WalkFrameStaticRecords.Empty; + public WalkFrameStaticRecords GetOutdoorObjects(uint cellId) => WalkFrameStaticRecords.Empty; public WalkFrameStaticRecords GetCellStatics(uint cellId) => WalkFrameStaticRecords.Empty; public WalkFrameStaticRecords GetCellDynamics(uint cellId) => WalkFrameStaticRecords.Empty; public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) => WalkFrameStaticRecords.Empty; @@ -475,16 +479,14 @@ public sealed partial class WalkTraceConformanceTests /// 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. + /// capture's real content volume; (2) AP-238 coalesces transparent + /// EnvCell work to one token per (cell,list), whereas retail + /// appends each contributing subset; (3) AP-239/AP-240 change + /// CLIP-vs-ALPHA membership for the named ordinary-Wb surface cases. + /// Ordinary Wb and particle paths otherwise append per real subset; the + /// former broad "per instance versus per subset" explanation was false. + /// See the S4 packet's §9/§10/§11 subsections for the full per-pose + /// sequences. /// private void RunAlphaFlushCountsGate(string fixtureName) {