diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index a42ba321..5a6e2868 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -61,6 +61,7 @@ public sealed class GameWindow : IDisposable // though the title-bar FPS is only updated every 0.5s. private double _lastFps = 60.0; private double _lastFrameMs = 16.7; + private string _lastCellRegSig = ""; // TEMP #133 ramp-flood-collapse [cellreg] dedup // Phase I.2: per-frame counters surfaced through the ImGui DebugPanel // VM closures. Computed once per render pass alongside the frustum @@ -5664,26 +5665,42 @@ public sealed class GameWindow : IDisposable // Static objects inside the cell continue to flow through the dispatcher // as WorldEntity records below — they have real GfxObj MeshRefs that work // fine; EnvCellRenderer.RegisterCell receives an empty staticObjects list. + // Transforms — needed by the portal-visibility cell (unlifted) AND the + // render/physics path. Computed for EVERY cell with a valid cellStruct, + // not just drawable ones. Keep the small render lift out of physics; retail + // BSP contact planes use the EnvCell origin verbatim. The lift constant is + // shared with every draw-space consumer of portal polygons (OutsideView + // gate, seal/punch fans) — PortalVisibilityBuilder.ShellDrawLiftZ (#130). + var physicsCellOrigin = envCell.Position.Origin + lbOffset; + var cellOrigin = physicsCellOrigin + new System.Numerics.Vector3( + 0f, 0f, AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ); + var cellTransform = + System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * + System.Numerics.Matrix4x4.CreateTranslation(cellOrigin); + var physicsCellTransform = + System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * + System.Numerics.Matrix4x4.CreateTranslation(physicsCellOrigin); + + // PORTAL VISIBILITY: register EVERY cell with a valid cellStruct, regardless + // of whether CellMesh.Build produced drawable sub-meshes. A portals-only + // pass-through connector (a ramp / stair / cellar mouth) yields 0 render + // sub-meshes but MUST be in the visibility graph so the flood can traverse it + // to the cells beyond — otherwise the flood lookup-misses the unregistered + // neighbour and the grey clear shows through the opening (#133: ramp + // neighbour 0x0007014D had 0 sub-meshes → unregistered → vis=1 grey barrier + // at the ramp; confirmed via [cellreg] registered=204/205 + [pv-trace] + // skip=lookup-miss). Retail keeps the whole landblock cell array resident + // before the flood runs; BuildLoadedCell reads the cellStruct portals, NOT + // the render sub-meshes. The +0.02 m render lift is a DRAW concern only and + // is intentionally NOT fed into the visibility transform (#119-residual: the + // lift shifted horizontal portal planes 2 cm, side-culling deck/stair cells). + BuildLoadedCell(envCellId, envCell, cellStruct, physicsCellOrigin, physicsCellTransform); + var cellSubMeshes = AcDream.Core.Meshing.CellMesh.Build(envCell, cellStruct, _dats); if (cellSubMeshes.Count > 0) { _pendingCellMeshes[envCellId] = cellSubMeshes; - // Keep the small render lift out of physics; retail BSP - // contact planes use the EnvCell origin verbatim. The lift - // constant is shared with every draw-space consumer of - // portal polygons (OutsideView gate, seal/punch fans) — - // see PortalVisibilityBuilder.ShellDrawLiftZ (#130). - var physicsCellOrigin = envCell.Position.Origin + lbOffset; - var cellOrigin = physicsCellOrigin + new System.Numerics.Vector3( - 0f, 0f, AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ); - var cellTransform = - System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * - System.Numerics.Matrix4x4.CreateTranslation(cellOrigin); - var physicsCellTransform = - System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * - System.Numerics.Matrix4x4.CreateTranslation(physicsCellOrigin); - // Phase A8: register the cell with EnvCellRenderer for rendering. // staticObjects is empty — cell stabs continue as separate WorldEntity // records via the dispatcher (see lines below for the unchanged stab path). @@ -5697,23 +5714,8 @@ public sealed class GameWindow : IDisposable cellRotation: envCell.Position.Orientation, staticObjects: System.Array.Empty<(uint, System.Numerics.Vector3, System.Numerics.Quaternion, bool, System.Numerics.Matrix4x4)>()); - // Step 4: build LoadedCell for portal visibility — with the - // PHYSICS (unlifted) transform. The +0.02 m render lift above - // is a DRAW concern (shell z-fighting vs terrain); feeding it - // into the visibility graph shifted every HORIZONTAL portal - // plane 2 cm up, putting an eye standing on a deck/landing - // 10–20 mm BELOW the lifted plane — outside the side test's - // ±10 mm in-plane window — so the cell behind the portal was - // side-culled: the tower-top staircase vanish + roof flap - // (#119-residual; captured live at eye z=126.803 vs the - // 010A→0107 plane at 126.80, reproduced ONLY with the lift in - // TowerAscentReplayTests.CapturedTopOfStairs_*). Vertical - // doorways were immune (the lift slides their planes along - // themselves), which is why this hit exactly stairs, decks, - // and cellar mouths. - BuildLoadedCell(envCellId, envCell, cellStruct, physicsCellOrigin, physicsCellTransform); - - // Cache CellStruct physics BSP for indoor collision (UNCHANGED). + // Cache CellStruct physics BSP for indoor collision (UNCHANGED — gated + // on drawable cells; a portals-only connector has no collision surface). _physicsDataCache.CacheCellStruct(envCellId, envCell, cellStruct, physicsCellTransform); } } @@ -7689,6 +7691,25 @@ public sealed class GameWindow : IDisposable playerCellId: playerRoot?.CellId ?? 0u, lights: Lighting); + // TEMP (#133 ramp-flood-collapse): cell-registration completeness for the + // player's dungeon landblock. If the ramp neighbour (0x....014D in 0x0007) + // is absent from _cellVisibility, the portal flood can't admit it (lookup-miss + // at PortalVisibilityBuilder.cs:369) and the grey clear shows through. Logs only + // when the count or ramp-presence changes (dedup) — pairs with [pv-trace] skip=. + if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled && playerRoot is not null) + { + uint plb = playerRoot.CellId >> 16; + int reg = _cellVisibility.GetCellsForLandblock(plb).Count; + uint rampId = (plb << 16) | 0x014Du; + bool hasRamp = _cellVisibility.TryGetCell(rampId, out _); + string sig = plb.ToString("X4") + ":" + reg + ":" + hasRamp; + if (sig != _lastCellRegSig) + { + _lastCellRegSig = sig; + Console.WriteLine($"[cellreg] lb=0x{plb:X4} registered={reg} hasRamp0x{rampId:X8}={hasRamp} playerCell=0x{playerRoot.CellId:X8}"); + } + } + // Never cull the landblock the player is currently on. uint? playerLb = null; if (_playerMode && _playerController is not null) diff --git a/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs b/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs index 38f263b8..d31ea93d 100644 --- a/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs +++ b/src/AcDream.App/Rendering/PortalVisibilityBuilder.cs @@ -759,7 +759,13 @@ public static class PortalVisibilityBuilder private static bool IsHoltburgIndoorProbeCell(uint cellId) { - if ((cellId & 0xFFFF0000u) != 0xA9B40000u) + uint lb = cellId & 0xFFFF0000u; + // TEMP (#133 ramp-flood-collapse diagnosis): widen the [pv-trace] gate to the + // 0x0007 Town Network dungeon so the per-portal skip= reason (lookup-miss / + // clip-empty / reciprocal-empty / side) is emitted for the ramp neighbour. + if (lb == 0x00070000u) + return true; + if (lb != 0xA9B40000u) return false; uint low = cellId & 0xFFFFu; return low >= 0x016F && low <= 0x0175; @@ -821,6 +827,7 @@ public static class PortalVisibilityBuilder // genuinely off-screen; the ndc coords (post-clip, bounded) show where on screen it lands. int projN = -1, clipN = -1; string ndcText = ""; + string rawText = ""; if (i < cameraCell.PortalPolygons.Count) { var poly = cameraCell.PortalPolygons[i]; @@ -830,6 +837,21 @@ public static class PortalVisibilityBuilder projN = clip.Length; if (clip.Length >= 3) { + // Raw projected-NDC bbox (pre-screen-clip): WHERE the portal lands on screen, + // even when ClipToRegion drops it to empty. A clip=0 portal whose raw bbox is + // inside [-1,1] is on-screen-but-wrongly-dropped (the bug); a bbox outside + // [-1,1] is genuinely off-screen (correct). Distinguishes the two. + float rminX = float.MaxValue, rminY = float.MaxValue, rmaxX = -float.MaxValue, rmaxY = -float.MaxValue; + foreach (var cv in clip) + { + if (cv.W <= 1e-6f) continue; + float nx = cv.X / cv.W, ny = cv.Y / cv.W; + rminX = MathF.Min(rminX, nx); rmaxX = MathF.Max(rmaxX, nx); + rminY = MathF.Min(rminY, ny); rmaxY = MathF.Max(rmaxY, ny); + } + if (rminX <= rmaxX) + rawText = FormattableString.Invariant($" raw=[{rminX:F1},{rminY:F1}..{rmaxX:F1},{rmaxY:F1}]"); + var ndc = PortalProjection.ClipToRegion(clip, FullScreenQuad); clipN = ndc.Length; var ns = new System.Text.StringBuilder(48); @@ -842,6 +864,7 @@ public static class PortalVisibilityBuilder sb.Append(" D=").Append(float.IsNaN(d) ? "na" : d.ToString("F2")); sb.Append(side ? " TRV" : " CULL"); sb.Append(" proj=").Append(projN).Append(" clip=").Append(clipN); + if (rawText.Length > 0) sb.Append(rawText); if (ndcText.Length > 0) sb.Append(" ndc=").Append(ndcText); } sb.Append(" || outPolys=").Append(frame.OutsideView.Polygons.Count);