fix(render) #132: outdoor alpha drains follow retail walk order
Some checks failed
CI / linux-portable (push) Successful in 3m35s
CI / windows-gate (push) Failing after 6m46s
CI / release (push) Has been skipped

Retail draws every cell's objects inside LScape::draw's far-to-near walk
(DrawSortCell @0x005A17C0) and drains the one delayed-alpha list at the
DrawCells boundary AFTER the finished walk (@0x005A4872). Our outdoor
frame drained at the landscape-stage end and then drew punches, interior
shells, cell objects, and ALL dynamics — every one of those opaque
passes overwrote the already-composited flames (the reopened #132
candle class: "the door draws over the candle", creatures at openings).
Depth and barrier A/Bs were no-ops because the eraser is opaque color
painted after the drain.

Two retail-cited ordering corrections, outdoor-node roots only:

1. The stage-boundary drain is skipped and FlushLandscapeAlpha() runs
   after DrawDynamicsLast, where the frame's opaque world depth is
   complete — the one far-to-near list composites over everything,
   exactly like retail's boundary flush relative to its finished walk.
2. Before DrawExitPortalMasks, FlushLandscapeAlphaFartherThan(
   ExitPortalMaskBarrierDistance(...)) drains everything at or beyond
   the nearest cell whose exit-portal mask is about to write far-Z —
   retail DrawBuilding @0x0059F2A0 runs FlushAlphaList(0f) BEFORE its
   portal-only pass, so in the far-to-near walk nothing already drained
   can meet a punched aperture's falsified depth. Without this, the
   first correction let exterior waterfalls z-pass across punched
   apertures whose true landscape depth the punch erased (found live at
   the cathedral gate). Nearer content stays queued and legitimately
   composites in front of punched structures.

Interior roots keep the pre-clear stage-boundary drain unchanged.

User-gated live: Holtburg sign candle whole in front of the sign and
tower door at the aligned pose; cathedral waterfalls contained at every
camera zoom, inside and outside. Register row AP-236 retired (the
walk-order outcome reconstruction is complete; AP-34 remains the
umbrella for the CYpt-sort reconstruction itself). Filed #456 for the
separate occluded-distant-building/creature admission residual this
session diagnosed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-29 19:03:35 +02:00
parent 8acb544d44
commit e102fb363e
4 changed files with 148 additions and 3 deletions

View file

@ -257,7 +257,6 @@ research and is no longer active.
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
| AP-235 | **Filed 2026-08-25 at the Campaign CT4 fix round.** Retail resolves gender display text via `AppraisalSystem::InqGenderDisplayName @0x005b47c0` and heritage via `InqHeritageGroupDisplayName @0x005b4710`, both through the static `EnumMapper::GetString(uint32_t enumValue, uint32_t queryId, PStringBase<char>*) @0x0041ac40` overload — `DBObj::GetDIDByEnum(&did, enumValue, 1)` (master map `0x25000000` → category-1 sub-map `0x25000001``ClientEnumToID[0x10000001]`/`[0x10000002]` → EnumMapper DIDs `0x2200000A`/`0x2200000B`) — reading each id's `IdToStringMap` entry live, with heritage ids 2/5/0xd hardcoded to `"Gharu'ndim"`/`"Umbraen"`/`"Olthoi"` in place of the raw internal names `"Gharundim"`/`"Shadowbound"`/`"OlthoiAcid"`. `CharacterIdentityText.GenderDisplayName`/`HeritageGroupDisplayName` are hardcoded C# `switch` tables instead — a mechanism divergence (compile-time constant vs. live DAT read), not a content one: `CharacterPanelLiveDatTests.GenderHeritageDisplayNameTables_MatchTheRetailEnumMapperChain` (filed the same round) walks the live EnumMapper chain and asserts every table entry byte-exact, including the two entries (10 "Penumbraen", 12 "Olthoi") the CT4 review had flagged as unverified guesses — both are correct. | `src/AcDream.App/UI/Layout/CharacterIdentityText.cs` (`GenderDisplayName`, `HeritageGroupDisplayName`); `src/AcDream.App/UI/Layout/RetailAppraisalNameResolver.cs` (`ResolveHeritage` — CT5 fix round 2026-08-25 deleted its independent re-implementation of the same 2/5/13 overrides; it now delegates straight to `CharacterIdentityText.HeritageGroupDisplayName`, so this row's divergence has exactly ONE owner, not two) | `RetailDataIdResolver.Resolve` (`src/AcDream.Content/RetailDataIdResolver.cs`) already ports the generic two-level `GetDIDByEnum` chain (used today for layout/material DIDs); unifying gender/heritage onto it needs only `Resolve(dats, enumValue: 0x10000001u/0x10000002u, enumCategory: 1u)` plus an `EnumMapper.IdToStringMap` read — a live-DAT-only path with no bespoke traversal code to write, which is why the tables stayed hardcoded this round rather than porting live-read on the spot; CT5 is the natural landing slot since it already owns this same DAT-lookup family for the Titles page | A future DAT/game update that renames or reorders a heritage/gender enum entry would silently desync acdream's hardcoded tables from retail's live text with no build-time or runtime signal — the CT5 fix round retired the second-copy drift risk (`ResolveHeritage` now reads the same single table), but the core hardcoded-vs-live-DAT divergence itself remains open | `AppraisalSystem::InqGenderDisplayName @ 0x005B47C0`; `InqHeritageGroupDisplayName @ 0x005B4710`; `EnumMapper::GetString @ 0x0041AC40`; `DBObj::GetDIDByEnum @ 0x004153A0` |
| AP-236 | **Filed 2026-08-29 with the particle-composition Change 1 (unclipped once-per-stage particle submission).** Retail accumulates every translucent submission in ONE alpha list in far→near WALK order — `block_draw_list` is built viewer-block-first and `LScape::draw @0x00506330` iterates it reversed; `RenderDeviceD3D::DrawBlock @0x005A17C0` walks each block per cell — and `DrawBuilding @0x0059F2A0` calls `FlushAlphaList(0f)` (the float is a COUNT threshold, 0 = flush all, `@0x0059D2E0`) BEFORE its opaque passes, so only content from cells FARTHER along the walk has been inserted when a building flushes; a nearer emitter (candle in front of a Holtburg door) inserts later and composites after that door. acdream's batched landscape has no per-cell walk: scene-particle owners submit at a few fixed points (pre-building barrier, late stage, cell stage) into the CYpt-sorted `RetailAlphaQueue`, and the pre-building barrier drains the WHOLE queue — a nearer-than-building emitter already queued is composited early, then the building/late-stage opaques overpaint it (the reopened #132 candle/creature-at-opening class). | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (barrier/late/cell submission points); `src/AcDream.App/Rendering/RetailPViewPassExecutor.cs` (`FlushLandscapeAlpha`); `src/AcDream.App/Rendering/RetailAlphaQueue.cs` | Change 1 removed the invented per-slice clip-slot re-submission (particles are never view-clipped in retail); restoring the walk-order OUTCOME needs a farther-than-building partial drain at the pre-building barrier (planned Change 2), because the batched landscape cannot reproduce retail's insertion order directly and the queue's far→near CYpt sort is the established reconstruction (see AP-34) | A translucent effect NEARER than a building that is flushed at the pre-building barrier gets overpainted by that building's (or the late stage's) opaques — visible as the #132 candle/creature-at-opening class until Change 2 lands | `LScape::draw @0x00506330`; `RenderDeviceD3D::DrawBlock @0x005A17C0`; `RenderDeviceD3D::DrawBuilding @0x0059F2A0`; `D3DPolyRender::FlushAlphaList @0x0059D2E0`; `PView::DrawCells @0x005A4872` |
| AP-234 | **Filed 2026-08-23 at the #426 solid-face extraction fix.** Cell-wall (EnvCell/CellStruct) geometry approximates retail's "skip untextured subsets inside a cell interior" with the polygon's own `Stippling.NoPos` flag rather than resolving the Surface's own `Type` (`Base1Image`/`Base1ClipMap`) before the per-polygon draw decision — the same NoPos-vs-surface-type conflation #426 fixed for ordinary GfxObj extraction (`PrepareGfxObjMeshData`/`GfxObjMesh.Build`), deliberately LEFT in place here | `src/AcDream.Core/Meshing/CellMesh.cs:45`; `src/AcDream.Content/MeshExtractor.cs`'s `PrepareCellStructMeshData` `hasPos` gate carries the identical rule | Cells are the one retail context that genuinely skips untextured subsets (`DrawEnvCell`), so approximating "untextured" with NoPos is directionally correct for the common case — a solid-colour polygon always carries NoPos since it has no UVs to carry; resolving Surface.Type first would need a per-polygon dat lookup this code doesn't currently perform before the emit/skip decision | A textured polygon whose author left NoPos set (no positive UVs authored despite a real texture) would be wrongly skipped as if untextured, or an untextured polygon whose author left NoPos unset would wrongly draw — either edge case shows as a cell wall gaining or losing a face relative to retail | `RenderDeviceD3D::DrawEnvCell` @0x0059f170`D3DPolyRender::DrawMesh(..., arg4=1)`; `RetailUntexturedSurfacePolicy`/`RetailUntexturedSubsetPolicy` (`src/AcDream.Core/Meshing/RetailUntexturedSurfacePolicy.cs`) |
| AP-233 | **Filed 2026-08-23 at the Holtburg windmill fix (row owed since the R1-P5 sequencer cutover).** `AnimationSequencer.BuildBlendedFrame` blends each part between `floor(FrameNumber)` and the next frame in the playback direction using the retail slerp (`SlerpRetailClient`). Retail never blends animation frames: `CPartArray::UpdateParts` applies `CSequence::get_curr_animframe` = `get_part_frame(floor(frame_number))`, holding every authored 30 fps frame for its whole interval. Since 2026-08-23 the blend holds the boundary frame at BOTH ends of a node's window — including the cyclic seam — so a cycle's last→first transition is retail's hard cut, not a blend. | `src/AcDream.Core/Physics/AnimationSequencer.cs` (`BuildBlendedFrame`); tests `AnimationSequencerTests.Advance_LinkTailDoesNotBlendIntoLinkFrame0` (#61), `Advance_CyclicSeamHoldsLastFrameInsteadOfBlendingIntoFrame0` (windmill) | The blend only smooths between authored interior frames of one node; at every seam the pose is exactly retail's held frame. Authored cycles that loop by symmetry (the Holtburg windmill's 60-frame quarter turn, `0x0300061B`) or by design read identically at the seam; link tails hold their end pose (#61). The owner chose this over dropping the blend (retail's 30 fps stepping) on 2026-08-23. | Any two adjacent authored frames that are NOT meant to be traversed smoothly (a deliberate authored pop inside a node) would be smoothed where retail pops; none known. A per-frame hitch of one held 33 ms interval at each cycle seam is the price of the cut (1.5° on the windmill). | `CPartArray::UpdateParts @0x005190F0`; `CSequence::get_curr_animframe @0x00524970`; `CSequence::get_curr_frame_number @0x005249D0` |
| AP-232 | **Filed 2026-08-22 at Campaign VM slice VM1 (the #226 single-pass re-port; deviation introduced at `05970306`, row owed since then).** Retail's single-pass detail combine produces ONE pixel per subset whose OUTPUT alpha is stage 1's `MODULATE(TEXTURE, CURRENT)` (`D3DPolyRender::SetSurface @0x0059c4d0`, op at `0x0059c549`) — for a delayed-alpha (translucent) subset that product is the framebuffer blend weight. acdream draws the base subset with its own alpha, then a second `mesh_detail` draw weighted by `detail.a * instanceOpacity` under `SRCALPHA + INVSRCALPHA`. For OPAQUE subsets (base alpha 1) the two compose to exactly `lerp(base, detail, detail.a*opacity)` and, with both draws fogged, to retail's fog-after-combine pixel (identity pinned by `RetailDetailTextureContractTests`). For TRANSLUCENT building/EnvCell subsets the destination after the base draw is `mix(behind, foggedBase, baseAlpha)`, not `foggedBase`, so the detail weight differs from retail's single product. | `src/AcDream.App/Rendering/Shaders/mesh_detail.frag`; `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.Rhi.cs` (transparent interleave); `src/AcDream.App/Rendering/Wb/EnvCellRenderer.Rhi.cs` (transparent interleave) | Opaque subsets are the overwhelming majority of building shells and interior walls and are exact; translucent detail-bearing subsets (ClipMap/alpha/additive/inverse-alpha glass and grates) get a bounded weight difference that never exceeds the detail texture's own alpha (mean 0.132 on the live Dereth category texture). Collapsing to one draw would require the base pipelines to sample the detail texture, i.e. a second `mesh_modern` variant on the retail path. | A translucent building/EnvCell surface with the detail preference on reads visibly different from retail against a bright background. Separate from AP-34 (queue ORDER); this row is about the blend WEIGHT. | `D3DPolyRender::SetSurface @0x0059c4d0` (stage table), `RenderMeshSubset @0x0059ca10`; VM2 cdb note `docs/research/2026-08-22-vm2-retail-detail-path-cdb.md` |