diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 4de6e2ca..86c51813 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -4304,7 +4304,7 @@ of which draw list the building's shell left. ## #124 — Looking out through an opening: far buildings with openings show missing/transparent back walls -**Status:** FIX SHIPPED — awaiting user visual gate +**Status:** CLOSED (user-gated 2026-06-12 evening: "124, that one is solved") **Severity:** MEDIUM **Filed:** 2026-06-11 (re-gate; pre-existing — "still have that issue"; user 2026-06-12: "especially visible when I look out through a door @@ -4637,22 +4637,27 @@ terrain, outdoor static meshes (the look-in punches need their depth, the dynamics' meshes + ALL attached scene particles + weather + the unattached pass. (This FIXED #132 indoors but not the portal.) -**ROOT CAUSE (fix 3 — the real one, pinned by the teleport capture):** -walking into the portal flipped `pCell` to **0xA9B4017A — the hall's -porch EnvCell**. The portal's swirl emitter is owned by a STATIC inside -ANOTHER BUILDING'S CELL, not an outdoor entity at all. Outdoors the -hall's cells merge into the main frame and the per-cell object pass -runs `DrawCellParticles` → swirl visible. Under an interior root the -#124 look-in sub-pass drew the far cells' shells + statics but had NO -cell-particles call — retail's nested DrawCells draws objects WITH -their emitters (`DrawObjCellForDummies`). Every earlier suspect -(unattached pass, owner-cone verdicts, alpha ordering) was real-but- -adjacent; the cone math was correct all along (the 0xC0A9B462 flips -were a porch torch, geometrically defensible). +**ROOT CAUSE (fix 4 — structurally forced; fixes 1–3 were +real-but-adjacent):** the teleport capture flipped `pCell` to +**0xA9B4017A — the hall's porch EnvCell** (the portal is a SERVER +object standing inside a look-in cell), and the headless replay of the +captured indoor frame proved the look-in flood ADMITS 0x017A (14 cells +incl. the porch — `Issue131SetupProbeTests.Diagnostic_LookInFlood_*`). +The partition routes server objects to the dynamics-last pass, where +(a) the viewcone has NO entries for look-in cells → culled, and (b) +even un-culled they would z-fail post-seal beyond the root's door plane +(the #118 lesson). This is exactly AP-33's recorded "look-in DYNAMICS +are not drawn (deferred)" — the deferred case was the town portal. +Outdoors the merge path puts the porch in the main cone → drawn → +"appears when I walk out." -**Fix 3:** `DrawBuildingLookIns` pass 2 invokes `DrawCellParticles` per -look-in cell with its static bucket (same callback as the main per-cell -pass; no-clip slice when the cell has no slot). +**Fix 4:** look-in-cell DYNAMICS draw inside `DrawBuildingLookIns` +pass 2 (with the statics, whole — AP-33's over-include), and their +emitters ride the same `DrawCellParticles` call (fix 3). Retail +equivalent: the nested DrawCells draws the cell's objects +(`DrawObjCellForDummies` pc:432878+). No double-draw: dynamics-last +keeps culling them (cell absent from the main cone); +DrawDynamicsParticles only sees dynamics-last cone survivors. **Gate:** stand inside, look out the doorway at the town portal — the swirl renders through the door. diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 5c26f137..b7a710c0 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -128,7 +128,7 @@ accepted-divergence entries (#96, #49, #50). | AP-30 | AutonomousPosition diff cadence compares with epsilons (1 mm pos, 1e-4 normal, 1 mm dist); retail's `Frame::is_equal` is an exact float compare | `src/AcDream.App/Input/PlayerMovementController.cs:1541` | Sub-millimeter epsilon is well below any movement worth suppressing; comparisons are against last-SENT state so drift accumulates past the epsilon | Sub-epsilon drift suppresses an AP send retail would have made — negligible today; a consumer expecting retail's exact send-on-any-change cadence sees fewer packets | `Frame::is_equal` pc:700263 | | 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-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (`ShellDrawLiftZ`, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, **#119**-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via `Build(drawLiftZ)`, seal/punch fans — **#130**) | `src/AcDream.App/Rendering/GameWindow.cs:5604` (const at `PortalVisibilityBuilder.ShellDrawLiftZ`) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) | -| AP-33 | Interior-root look-in statics (**#124** sub-pass) draw WHOLE — no per-part viewcone check; retail viewconeCheck's each part vs the installed view. Look-in DYNAMICS are not drawn at all (deferred; retail draws objects per overlapped cell in the landscape stage) | `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) | Statics: a few wasted draws only. Dynamics: an NPC inside a far building seen through two openings is invisible where retail shows it | `viewconeCheck` 0x0054c250; nested `DrawCells` objects pc:432878 | +| 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 | Landscape-stage alpha deferral is a TWO-PHASE slice split (statics-early / dynamics+particles+weather-late around the **#124** look-ins) + outdoor-root attached scene emitters moved to the post-frame pass, not retail's single deferred alpha flush. Residual: building exteriors' / outside-stage dynamics' own translucent MESH batches still draw within their stage draw call (before later stage content) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs` (`DrawLandscapeThroughOutsideView` late loop) + `GameWindow` post-frame Scene pass | The MDI dispatcher draws translucency inside each Draw call; a faithful FlushAlphaList port needs a global deferred alpha list across all landscape draws — the split covers the user-visible cases (#131 portal swirl, #132 candle flame indoors + outdoors) | Translucent landscape content drawn early and screen-overlapped by content drawn later in the stage gets overpainted (no depth self-protection) — the portal-swirl/candle-flame class re-appears in the residual configurations | `D3DPolyRender::FlushAlphaList` (DrawCells pc:432722) | --- diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 5a305809..4993f5c1 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -353,19 +353,33 @@ public sealed class RetailPViewRenderer _envCells.Render(WbRenderPass.Opaque, _oneCell); _envCells.Render(WbRenderPass.Transparent, _oneCell); - if (partition.ByCell.TryGetValue(cellId, out var bucket) && bucket.Count > 0) - { - _cellStaticScratch.Clear(); + _cellStaticScratch.Clear(); + if (partition.ByCell.TryGetValue(cellId, out var bucket)) _cellStaticScratch.AddRange(bucket); + + // #131 ROOT CAUSE: DYNAMICS living in a look-in cell (the + // Holtburg hall-porch PORTAL, pCell 0xA9B4017A) draw NOWHERE + // under an interior root — DrawDynamicsLast viewcone-culls + // them (the main cone has no entries for look-in cells), and + // post-clear they would z-fail against the root's seal anyway + // (the #118 lesson). Retail draws a look-in cell's objects + // inside the NESTED DrawCells (DrawObjCellForDummies, + // pc:432878+), i.e. right here in the landscape stage. Drawn + // WHOLE like the statics (AP-33's documented over-include). + // No double-draw: dynamics-last keeps culling them (their + // cell is absent from the main cone), and their emitters ride + // the DrawCellParticles call below, not DrawDynamicsParticles + // (which only sees dynamics-last cone survivors). + foreach (var e in partition.Dynamics) + if (e.ParentCellId == cellId) + _cellStaticScratch.Add(e); + + if (_cellStaticScratch.Count > 0) + { DrawEntityBucket(ctx, _cellStaticScratch, _oneCell); - // #131: the cell-particles pass for look-in cells — retail's - // nested DrawCells draws objects WITH their emitters - // (DrawObjCellForDummies, pc:432878+). Without this, an - // emitter owned by a far building's cell static (the - // Holtburg hall-porch portal swirl, cell 0x017A) drew ONLY - // when the viewer was outdoors (the merge path runs the - // main per-cell pass) — invisible from inside any cottage. + // The cell-particles pass for look-in cells — retail's + // nested DrawCells draws objects WITH their emitters. foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId)) ctx.DrawCellParticles?.Invoke(new RetailPViewCellSliceContext( cellId, slice, _cellStaticScratch)); diff --git a/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs b/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs index 49cca50f..0c60c71a 100644 --- a/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Issue131SetupProbeTests.cs @@ -19,6 +19,49 @@ public class Issue131SetupProbeTests private readonly ITestOutputHelper _out; public Issue131SetupProbeTests(ITestOutputHelper output) => _out = output; + /// #131: from the captured cottage-interior frame (the user's + /// portal-missing viewpoint), does the look-in flood admit the hall's + /// PORCH cell 0xA9B4017A (the portal's owner cell, pinned by the teleport + /// pCell flip)? If not admitted, no pass can draw the swirl regardless of + /// the emitter plumbing. + [Fact] + public void Diagnostic_LookInFlood_AdmitsHallPorchFromCottage() + { + var datDir = CornerFloodReplayTests.ResolveDatDir(); + if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return; } + using var dats = new DatCollection(datDir, DatAccessType.Read); + + var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, 0xA9B40000u); + _out.WriteLine(FormattableString.Invariant($"loaded {cells.Count} A9B4 interior cells; hasPorch017A={cells.ContainsKey(0xA9B4017Au)}")); + AcDream.App.Rendering.LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out var c) ? c : null; + + // The captured frame: [viewer] root=0xA9B40171 eye=(155.255,14.533,96.074) + // fwd=(0.0702,0.9554,-0.2869) (portal-owner-verdicts.log:135118). + var eye = new System.Numerics.Vector3(155.255f, 14.533f, 96.074f); + var fwd = new System.Numerics.Vector3(0.0702f, 0.9554f, -0.2869f); + var view = System.Numerics.Matrix4x4.CreateLookAt(eye, eye + fwd, System.Numerics.Vector3.UnitZ); + var proj = System.Numerics.Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 1f, 5000f); + var viewProj = view * proj; + + var root = cells[0xA9B40171u]; + var pv = AcDream.App.Rendering.PortalVisibilityBuilder.Build( + root, eye, Lookup, viewProj, + buildingMembership: null, + drawLiftZ: AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ); + _out.WriteLine(FormattableString.Invariant( + $"main flood={pv.OrderedVisibleCells.Count} outPolys={pv.OutsideView.Polygons.Count}")); + + var lookIn = AcDream.App.Rendering.PortalVisibilityBuilder.BuildFromExterior( + cells.Values, eye, Lookup, viewProj, + float.PositiveInfinity, pv.OutsideView.Polygons); + var sb = new System.Text.StringBuilder("look-in admitted:"); + foreach (uint id in lookIn.OrderedVisibleCells) + sb.Append(FormattableString.Invariant($" 0x{id & 0xFFFFu:X4}")); + _out.WriteLine(sb.ToString()); + _out.WriteLine(FormattableString.Invariant( + $"porch 0x017A admitted: {lookIn.OrderedVisibleCells.Contains(0xA9B4017Au)}")); + } + [Fact] public void Diagnostic_DumpOutstageCandidateSetups() {