checkpoint: preserve user-gated FW closeout fixes

This commit is contained in:
Erik 2026-08-31 08:27:37 +02:00
parent a2f2eb7d78
commit b8befded8b
22 changed files with 1005 additions and 198 deletions

View file

@ -1096,34 +1096,47 @@ devtools, and screenshots; then closes the GPU-flight transaction in
> `TerrainModernRenderer` (terrain), fed by the unified PView stack. This is the
> authoritative current draw model; the 2026-05-31 reset handoff is historical.
**One visibility owner.** `RetailPViewRenderer.DrawInside` is the production
world gate. Its root is the collided camera/viewer cell, or the synthetic
outdoor cell adaptation; the player's current cell separately owns sunlight
and indoor lighting. A null root exists only for login/debug/streaming-gap
fallback frames. `RetailPViewRenderer` is the one authoritative PView owner and
product family: it builds the main frame, deliberate per-building exterior
floods for the synthetic outdoor root, and separate interior-root look-in
frames. No second per-frame ACME visibility BFS competes with that family.
**One visibility owner.** `RetailFrameWalk`, driven once per frame by
`WalkFrameDriver.Collect`, is the production authority for rooting, portal
recursion, visible cells/buildings, per-view admission, and draw order. Its root
is the collided camera/viewer cell or retail's synthetic outdoor cell; the
player's current cell separately owns sunlight and indoor lighting. A null root
exists only for login/debug/streaming-gap fallback frames. The classes named
`RetailPViewRenderer` and `RetailPViewPassExecutor` remain composition/pass
facades around that one walk; they do not build a second visibility product.
`PortalVisibilityBuilder` is retained only by tests and research diagnostics and
has no production caller. `CellVisibility` does not decide frame visibility.
**Current draw discipline.** Outside-view slices draw sky, terrain, and outdoor
statics first. Interior-root building look-ins then punch all entry apertures
before drawing their shell and contents. The landscape shared-alpha scope
flushes before the root-specific depth boundary. Interior roots perform the
conditional depth clear and then write true-depth exit seals; the synthetic
outdoor root retains world depth and writes far-Z building-entry punches.
Opaque EnvCell shells, immediate far-to-near transparent EnvCell shells, cell
statics and their particles, then the surviving main-stage dynamics and their
particles follow. Look-in and outside-stage dynamics intentionally draw in
their earlier landscape phases. The final world shared-alpha scope flushes
before private portal/paperdoll viewports and UI.
**Current draw discipline.** The walk records one ordered event stream for
landscape cells, building portals, EnvCell shells, statics, dynamics, particles,
depth boundaries, and alpha barriers. `WalkFrameDriver` replays those events in
retail order through the Vulkan leaf renderers. Interior roots preserve retail's
conditional depth clear and true-depth exit seals; the synthetic outdoor root
retains world depth and stamps far-Z entry punches. World translucents and scene
particles share `RetailAlphaQueue` and drain at the walk's exact barriers.
Private portal/paperdoll viewports and retained UI execute only after the world
stream finishes. Projectiles are ordinary live-entity draws, never a separate
global pass.
The modern renderer intentionally does not hard-clip every shell or entity to
the accumulated polygon. It combines PView admission and viewcone checks with
retail's punch/seal depth discipline; terrain/outside slices use the bounded
clip-plane/scissor adaptation. World Wb translucents and Scene particles share
one stable far-to-near `RetailAlphaQueue`; EnvCell transparent shells and
private viewports remain immediate. Projectiles are ordinary live-entity draws,
never a separate global pass.
**Portal-slice clipping is part of admission, not a later approximation.** Each
walk visit retains the exact authored pixel-space portal polygon. The frame
adapter converts it once to a Vulkan GPU clip slot, and the cell shell, statics,
and dynamics admitted by that visit draw through that same slot. A content
drawing sphere may reject a slice before submission, but an accepted slice
never emits the whole mesh unclipped. The same cell can therefore draw more
than once through distinct apertures, exactly as retail's `viewconeCheck` plus
`portal_view` loop requires. Particles remain unclipped at submission because
retail contains them positionally through owner-cell ordering, opaque depth,
and alpha barriers. The complete visibility answer is the union of the walk's
visited EnvCells and visited landscape land cells. EnvCell batch preparation
consumes only the former; particle, light, and shadow visibility consume the
union. Omitting the landscape half disables every outdoor emitter even though
its owner still reaches the draw stream.
WorldBuilder contributes DAT decoding, mesh preparation, residency, and Vulkan
batch mechanics only; it makes no visibility or ordering decision. Forced merge
breaks at walk event boundaries preserve retail order while compatible draws
inside a leaf may still use order-preserving MDI merging.
Retail anchors are `SmartBox::RenderNormalMode @ 0x00453AA0`,
`PView::DrawInside @ 0x005A5860`, `PView::DrawCells @ 0x005A4840`,

View file

@ -325,7 +325,6 @@ research and is no longer active.
| AP-27 | PlayerDescription trailer: GameplayOptions skipped by a 4-byte-aligned heuristic scan for a valid inventory parse; options blob captured opaque, never decoded (retail decodes + applies UI options) | `src/AcDream.Core.Net/Messages/PlayerDescriptionParser.cs:69` | Variable-length opaque blobs; mirrors holtburger's heuristics; follow-up issue extends when panels consume those sections | An options blob that coincidentally parses as a valid inventory (or inventory not landing at EOF) yields wrong/empty inventory+equipped at login; retail-persisted UI options silently ignored | ACE GameEventPlayerDescription.WriteEventBody; holtburger events.rs:195-218 |
| ~~AP-28~~ | **RETIRED 2026-08-08 (Campaign A slice A2).** The three picked AL parameters and the gain-driven eviction are both gone. `RetailSoundMixer` now carries the byte-decoded retail curve — `g = dist < 5 ? vol : 25·vol/dist²`, clamped to 1, ONE master multiply, `db = ceil(20·log10 g)`, and a hard 50 dB no-allocate floor (audible radius ≈94.2 m at unity) — with pan as retail's `15·sin(Δbearing)` in whole decibels and a 5-metre integer deadzone. Every AL source is source-relative with `AL_ROLLOFF_FACTOR = 0` and the global distance model is `None`, so AL contributes no attenuation of its own; the old `InverseDistanceClamped` ref-2 m curve was inverse FIRST power (`2/d`), quieter than retail up close and far louder at range with no cutoff at all. Voice eviction now compares the DAT-authored float priority strictly-less in ring order per `SoundManager::PlaySoundInternal` @ `0x0054FEC0` (the row's old `FUN_00550ad0` citation was wrong — that address is inside an `IntrusiveHashTable` constructor). The residual pan-LAW approximation is AP-173; retail's own `s_bPlaySoundOnlyWhenActive` gate is TS-64. | retired | — | — | `SoundManager::GetAttenuation @ 0x00550020`; `SoundManager::PlaySoundInternal @ 0x00550170` and `@ 0x0054FEC0`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` |
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter |
| AP-33 | Interior-root look-in cells (**#124** sub-pass) draw their statics + DYNAMICS + emitters WHOLE — no per-part/per-object viewcone check; retail viewconeCheck's each vs the installed view (the **#131** portal closure: a server object in a look-in cell drew nowhere — dynamics-last culls cells absent from the main cone, and post-seal it z-fails anyway) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawBuildingLookIns`) | The main viewcone has no entries for look-in cells; over-include is the safe direction (z-correct, repainted outside apertures by the root's shells); look-in cell counts are small (~1-3 cells) | A few wasted draws on content outside the doorway region (repainted); no under-draw direction remains | `viewconeCheck` 0x0054c250; nested `DrawCells` objects pc:432878 |
| AP-34 | The world now shares one delayed alpha queue across Wb GfxObj/Setup entities and scene particles and drains it at retail's landscape/final boundaries. Residual: the modern reconstruction uses one stable scope-global CYpt sort rather than retail's per-`CPartCell` `CShadowPart` sort followed by cell traversal; `EnvCellRenderer` transparent shell batches also remain immediate and outside this queue | `src/AcDream.App/Rendering/RetailAlphaQueue.cs`; `RetailPViewPassExecutor.cs` (`FlushLandscapeAlpha`); `Rendering/Wb/WbDrawDispatcher.cs`; `ParticleRenderer.cs` | The mandatory modern renderer no longer owns retail `CPartCell` shadow lists. The shared queue restores the material consequence that motivated the port—particles and ordinary translucent parts can interleave—without rebuilding a second scene graph; stable sequence retains authored order on equal CYpt | Transparent objects from different cells can exchange order at a narrow overlap compared with retail cell traversal; an alpha-blended EnvCell shell cannot interleave with a particle or Wb entity, so those rare overlaps can still overpaint differently | `RenderDeviceD3D::DrawObjCellForDummies` 0x005A0760; `CShadowPart::insertion_sort` 0x006B5130; `D3DPolyRender::FlushAlphaList` 0x0059D2E0; `PView::DrawCells` 0x005A4840 |
| 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) |
@ -510,7 +509,6 @@ research and is no longer active.
| TS-59 | No outbound Flow report (retail emits a 6-byte bytes-received+interval header whenever the inbound remote interval advances). | `src/AcDream.Core.Net/Transport/ReliableTransport.cs` (interval clock present; no Flow emission) | ACE parses the Flow header and has no handler (PacketHeaderOptional.cs:117-124); the standalone unsequenced form would trip the watermark hole. Retail itself never consumes inbound Flow and has no throttle (`WireRoomLeft` is a folded return-1). | A future server that rate-adapts on client Flow reports sees nothing. | `SharedNet::ProcessNewRemoteInterval @ 0x00543A80`; `ClientFlowQueue::WireRoomLeft @ 0x0052C1C0` (folded) |
| TS-60 | No 140 s dead-link declaration or referral auto-reconnect in the transport; a silent server is only visible through `LinkStatusSnapshot.SecondsSinceLastPacket` (presentational). | `src/AcDream.Core.Net/Transport/ReliableTransport.cs`; `src/AcDream.Core.Net/WorldSession.cs` (`BuildLinkStatus`) | The input (seconds since last inbound) is already exposed; session lifecycle/reconnect is Runtime's ownership domain and deserves its own campaign rather than a transport-embedded side effect. Every ACE transport death is silence, so nothing server-side depends on the client reacting at 140 s. | A dead link idles until the user acts; no automatic recall/referral reconnect where retail would attempt one. | `ClientNet::ProcessConnection @ 0x00545450` tail (the two 140.0 literals) |
| TS-61 | A UDP send failure burns the reliable sequence and its ISAAC word (the encode commits before `_net.Send`); retail keeps the sealed packet at the queue head and retries with the same key. | `src/AcDream.Core.Net/Transport/OutboundFlowQueue.cs` (`SendGameMessage`) | A connectionless-socket `SendTo` failure is effectively unreachable in practice (no route/ICMP errors surface on later receives, not sends, on Windows UDP); recovering it faithfully needs a full outbound packet queue. The N1 review accepted the exposure explicitly. | One `SocketException` on send would desync the outbound cipher permanently (session death; observable as `[net-out-EX]` followed by silence). | `FlowQueue::TransmitNewPackets @ 0x00547C2C` (retry-from-head) |
| TS-56 | Chase-camera mouse input retains acdream's invented post-filter yaw/pitch scalars (`0.004`/`0.003` radians per count), and held-key pitch/zoom retain their non-retail integration shapes. Retail mouse look passes `FilterMouseInput(delta) × configured sensitivity × 1/15` as the replacement scale to `CameraSet::Rotate`, which then applies the shared 8° angle; retail held pitch uses the same angle and zoom scales the viewer offset multiplicatively. | `src/AcDream.App/Input/CameraPointerInputController.cs`; `src/AcDream.App/Input/MouseLookController.cs`; `src/AcDream.App/Rendering/CameraFrameController.cs` | Slice 8 is behavior-preserving ownership work. The named-retail audit proves the mismatch but has not yet extracted the configured mouse-sensitivity default or the exact caller flags needed for a complete feel port; changing only one scalar here would create a mixed input model. | RMB/MMB orbit, held pitch, and zoom can feel slower, faster, or differently accelerated than retail even though callback ordering and filtering are correct. | `CameraSet::Rotate @ 0x00458310`; `CameraSet::MouseLookHandler` call at `0x00458EF9`; `CameraSet::Raise @ 0x00457B00`; `CameraSet::Closer @ 0x004586D0`; `docs/research/2026-06-11-holistic-map/wf2-camera-viewer.md` |
| TS-62 | **Filed 2026-08-02 (physics campaign, continuation-executor slice).** NO Position route in the dormant executor runs a live `ConstrainTo` binding - including the `SetPosition`/`SetPositionSimple` routes. `RuntimeAuthoritativePositionRoute.ConstrainPhase` (None/Before/After) is classified for EVERY accepted route and recorded into the execution trace, but the constrain-before-vs-after distinction exists purely as classified metadata pending a live binding at the production cutover. | `src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs` (`ApplyPositionAction`/`BuildPositionTrace`); `RuntimeAuthoritativePositionRouteClassifier.cs` (`ConstrainPhase`) | Host-cutover work with no Runtime-side owner to bind to yet; the canonical snapshot's Position IS refreshed on every accepted route, so the fact is retained - only the live constrain/smoothing behavior is deferred. The trace carries the exact phase a host must bind. | Until a host wires it, ANY Position continuation applies its raw pose with no constrain-distance clamp or smoothing - a visible pop instead of retail's constrained correction, on exactly the entities created while an authored placement was in flight. | `SmartBox::HandleReceivedPosition` 0x00453FD0, the three `ConstrainTo` sites (~93007 remote-after, ~93024 teleport-after, ~93041 local-ordinary-before) |
| TS-63 | **Filed 2026-08-02 (physics campaign, continuation-executor slice).** `ApplyResidentCellCleanup`'s three branches: (1) claimed-cell + celless + NOT under lost-cell/deferred ownership - retail's genuine `AddObjectToBeDestroyed` case - has no safe Runtime destruction owner yet, so the executor performs a typed ABANDONMENT (`RejectedAuthority`) instead of destroying; (2) claimed + celless + deferred returns `DeferredUnderLostCellOwnership` - retail's destruction bookkeeping for this exact entity is already owned by the lost-cell/deferred `SetPosition` lifetime (a statement, not a parallel mechanism); (3) claimedCell==0 returns `CelllessNoWeenieMarkUnreachable` and is NOT a divergence - every admitted envelope structurally carries a WeenieDescription (`HasValidShape`), so retail's no-weenie destruction alternative is unreachable through this construction. | `src/AcDream.Runtime/Entities/RuntimeInitialCreateContinuationExecutor.cs` (`ApplyResidentCellCleanup`; the Abandon conversion in `ApplyEnvelope`) | No production caller yet; every branch is typed and test-observable; building a parallel destruction mechanism ahead of the object-table/lost-cell cutover wiring would be the exact workaround class CLAUDE.md forbids - failing closed is the honest interim. | Branch (1): a genuinely claimed-but-celless-undeferred entity aborts the drain and SURVIVES where retail destroys it, until the cutover wiring lands. Branch (3): a future envelope construction without a WeenieDescription would break the premise and needs re-examination. | `SmartBox::HandleCreateObject` 0x00454C80 tail (~93933 destruction mark; ~93942-93943 un-mark/no-weenie) |
| TS-64 | **Retail's sound-preference surface is only partly present.** Retail registers eight `[Sound]` keys in `SoundManager::InitPrefs` @ `0x005503F0`; two are unimplemented in acdream. (a) `s_bPlaySoundOnlyWhenActive` (default **1**) is checked against `Device::m_bIsActiveApp` in every entry point and in both `PlaySoundInternal` overloads, so an unfocused retail client is SILENT; acdream keeps playing when the window loses focus. (b) `s_SoundFeatures == 1` forces pan to dead centre; acdream's `RetailSoundMixer.Mix`/`GetPan` take a `panningEnabled` flag with conformance coverage, but no preference is wired behind it, so panning can never be turned off. The three enable bools (`Sound Disabled`, `Ambient Sound Disabled`, `Interface Sound Disabled`) also have no acdream counterpart — note retail's on-disk polarity is inverted relative to its backing variables, so a future reader must not assume the sense. | `src/AcDream.App/Audio/OpenAlAudioEngine.cs` (no focus gate); `src/AcDream.Core/Audio/RetailSoundMixer.cs` (`panningEnabled`, unwired) | Slice A2 kept its blast radius on the mixing model: window-focus state and a preference surface are host plumbing rather than mixing math, and the mixer parameter exists so wiring them later needs no math change. | Alt-tabbed acdream keeps making noise where retail goes quiet; users cannot disable panning or the individual sound classes. | `SoundManager::InitPrefs @ 0x005503F0`; `SoundManager::PlaySoundInternal @ 0x0054FEC0` and `@ 0x00550170`; `docs/research/2026-08-08-audio-retail-soundmanager-core.md` §1 |

View file

@ -271,9 +271,10 @@ the transformed DAT `SortCenter`; only adjacent compatible entries may batch.
Billboard particle textures are resident bindless `sampler2DArray` handles in
the per-instance vertex ABI, so different textures preserve that sorted order
inside one instanced draw; only a DAT blend-mode boundary splits the run. This
keeps dense particle fields from becoming one GL draw per alternating texture.
`RetailPViewRenderer` drains the landscape scope before the optional depth
clear, and `GameWindow` drains the final scope before private viewports/UI.
keeps dense particle fields from becoming one Vulkan draw per alternating
texture. `WalkFrameDriver` drains the landscape and world scopes at the exact
alpha barriers emitted by `RetailFrameWalk`; the frame orchestrator reaches
private viewports/UI only after the world stream completes.
Sky and sealed off-screen render targets remain independent. No DAT reader,
mesh decoder, or second scene graph was introduced. Retail anchors:
`CPhysicsPart::UpdateViewerDistance` `0x0050E030`,
@ -281,7 +282,10 @@ mesh decoder, or second scene graph was introduced. Retail anchors:
`CShadowPart::insertion_sort` `0x006B5130`,
`D3DPolyRender::AddMeshToAlphaList` `0x0059C230`, and
`D3DPolyRender::FlushAlphaList` `0x0059D2E0`. The modern per-cell-order and
EnvCell-shell residual is tracked explicitly as AP-34.
EnvCell-shell residual is tracked explicitly as AP-34. WorldBuilder does not
choose visible cells, portal slices, or cross-cell order: it receives the
walk's ordered records and GPU clip slots and performs asset preparation plus
order-preserving Vulkan batching only.
**Retail portal-space viewport adapter (2026-07-15).**
`src/AcDream.App/Rendering/PortalTunnelPresentation.cs` uses the extracted