fix(G.3): register portals-only connector cells for visibility (#133 ramp grey)
The grey "barrier" at a dungeon ramp was a one-cell registration gap. The ramp's connector cell (0x0007014D) is a portals-only pass-through — CellMesh.Build yields 0 drawable sub-meshes for it (you walk through it on adjacent floors). But the whole registration block — including the portal-VISIBILITY registration (BuildLoadedCell -> _cellVisibility) — was gated behind `if (cellSubMeshes.Count > 0)`. So that cell was never added to the visibility graph; the flood lookup-missed it (PortalVisibilityBuilder :369), couldn't traverse it to the room below, and the grey clear color showed through. Confirmed live via two added probes: [cellreg] registered=204/205 (only 0x014D missing) + [pv-trace] p4->0x0007014D skip=lookup-miss. After the fix: registered=205, hasRamp=True, skip=lookup-miss gone, the room below renders. Fix: compute the cell transforms and call BuildLoadedCell (visibility) for EVERY cell with a valid cellStruct, regardless of drawable sub-meshes — matching retail, which keeps the whole landblock cell array resident before the flood runs. Drawing (RegisterCell, _pendingCellMeshes) and the physics BSP (CacheCellStruct) stay gated on drawable geometry (a portals-only connector has nothing to draw and no collision surface). Not a regression from the FPS-collapse work — a pre-existing gate the now-navigable dungeon exposed (every ramp/stair/cellar mouth would show it). TEMP diagnostics retained for the residual angle-grey investigation (strip after): [cellreg] (GameWindow), the 0x0007 [pv-trace] gate widen + raw-NDC bbox (PortalVisibility- Builder). Three earlier render-math theories (portal_side, on-screen clip, near-eye projection) were each refuted by apparatus/probe before shipping — this is the verified one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7d8da99f79
commit
d90c5385d2
2 changed files with 77 additions and 33 deletions
|
|
@ -61,6 +61,7 @@ public sealed class GameWindow : IDisposable
|
||||||
// though the title-bar FPS is only updated every 0.5s.
|
// though the title-bar FPS is only updated every 0.5s.
|
||||||
private double _lastFps = 60.0;
|
private double _lastFps = 60.0;
|
||||||
private double _lastFrameMs = 16.7;
|
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
|
// Phase I.2: per-frame counters surfaced through the ImGui DebugPanel
|
||||||
// VM closures. Computed once per render pass alongside the frustum
|
// VM closures. Computed once per render pass alongside the frustum
|
||||||
|
|
@ -5664,16 +5665,12 @@ public sealed class GameWindow : IDisposable
|
||||||
// Static objects inside the cell continue to flow through the dispatcher
|
// Static objects inside the cell continue to flow through the dispatcher
|
||||||
// as WorldEntity records below — they have real GfxObj MeshRefs that work
|
// as WorldEntity records below — they have real GfxObj MeshRefs that work
|
||||||
// fine; EnvCellRenderer.RegisterCell receives an empty staticObjects list.
|
// fine; EnvCellRenderer.RegisterCell receives an empty staticObjects list.
|
||||||
var cellSubMeshes = AcDream.Core.Meshing.CellMesh.Build(envCell, cellStruct, _dats);
|
// Transforms — needed by the portal-visibility cell (unlifted) AND the
|
||||||
if (cellSubMeshes.Count > 0)
|
// render/physics path. Computed for EVERY cell with a valid cellStruct,
|
||||||
{
|
// not just drawable ones. Keep the small render lift out of physics; retail
|
||||||
_pendingCellMeshes[envCellId] = cellSubMeshes;
|
// BSP contact planes use the EnvCell origin verbatim. The lift constant is
|
||||||
|
// shared with every draw-space consumer of portal polygons (OutsideView
|
||||||
// Keep the small render lift out of physics; retail BSP
|
// gate, seal/punch fans) — PortalVisibilityBuilder.ShellDrawLiftZ (#130).
|
||||||
// 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 physicsCellOrigin = envCell.Position.Origin + lbOffset;
|
||||||
var cellOrigin = physicsCellOrigin + new System.Numerics.Vector3(
|
var cellOrigin = physicsCellOrigin + new System.Numerics.Vector3(
|
||||||
0f, 0f, AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ);
|
0f, 0f, AcDream.App.Rendering.PortalVisibilityBuilder.ShellDrawLiftZ);
|
||||||
|
|
@ -5684,6 +5681,26 @@ public sealed class GameWindow : IDisposable
|
||||||
System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) *
|
System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) *
|
||||||
System.Numerics.Matrix4x4.CreateTranslation(physicsCellOrigin);
|
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;
|
||||||
|
|
||||||
// Phase A8: register the cell with EnvCellRenderer for rendering.
|
// Phase A8: register the cell with EnvCellRenderer for rendering.
|
||||||
// staticObjects is empty — cell stabs continue as separate WorldEntity
|
// staticObjects is empty — cell stabs continue as separate WorldEntity
|
||||||
// records via the dispatcher (see lines below for the unchanged stab path).
|
// 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,
|
cellRotation: envCell.Position.Orientation,
|
||||||
staticObjects: System.Array.Empty<(uint, System.Numerics.Vector3, System.Numerics.Quaternion, bool, System.Numerics.Matrix4x4)>());
|
staticObjects: System.Array.Empty<(uint, System.Numerics.Vector3, System.Numerics.Quaternion, bool, System.Numerics.Matrix4x4)>());
|
||||||
|
|
||||||
// Step 4: build LoadedCell for portal visibility — with the
|
// Cache CellStruct physics BSP for indoor collision (UNCHANGED — gated
|
||||||
// PHYSICS (unlifted) transform. The +0.02 m render lift above
|
// on drawable cells; a portals-only connector has no collision surface).
|
||||||
// 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).
|
|
||||||
_physicsDataCache.CacheCellStruct(envCellId, envCell, cellStruct, physicsCellTransform);
|
_physicsDataCache.CacheCellStruct(envCellId, envCell, cellStruct, physicsCellTransform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7689,6 +7691,25 @@ public sealed class GameWindow : IDisposable
|
||||||
playerCellId: playerRoot?.CellId ?? 0u,
|
playerCellId: playerRoot?.CellId ?? 0u,
|
||||||
lights: Lighting);
|
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.
|
// Never cull the landblock the player is currently on.
|
||||||
uint? playerLb = null;
|
uint? playerLb = null;
|
||||||
if (_playerMode && _playerController is not null)
|
if (_playerMode && _playerController is not null)
|
||||||
|
|
|
||||||
|
|
@ -759,7 +759,13 @@ public static class PortalVisibilityBuilder
|
||||||
|
|
||||||
private static bool IsHoltburgIndoorProbeCell(uint cellId)
|
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;
|
return false;
|
||||||
uint low = cellId & 0xFFFFu;
|
uint low = cellId & 0xFFFFu;
|
||||||
return low >= 0x016F && low <= 0x0175;
|
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.
|
// genuinely off-screen; the ndc coords (post-clip, bounded) show where on screen it lands.
|
||||||
int projN = -1, clipN = -1;
|
int projN = -1, clipN = -1;
|
||||||
string ndcText = "";
|
string ndcText = "";
|
||||||
|
string rawText = "";
|
||||||
if (i < cameraCell.PortalPolygons.Count)
|
if (i < cameraCell.PortalPolygons.Count)
|
||||||
{
|
{
|
||||||
var poly = cameraCell.PortalPolygons[i];
|
var poly = cameraCell.PortalPolygons[i];
|
||||||
|
|
@ -830,6 +837,21 @@ public static class PortalVisibilityBuilder
|
||||||
projN = clip.Length;
|
projN = clip.Length;
|
||||||
if (clip.Length >= 3)
|
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);
|
var ndc = PortalProjection.ClipToRegion(clip, FullScreenQuad);
|
||||||
clipN = ndc.Length;
|
clipN = ndc.Length;
|
||||||
var ns = new System.Text.StringBuilder(48);
|
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(" D=").Append(float.IsNaN(d) ? "na" : d.ToString("F2"));
|
||||||
sb.Append(side ? " TRV" : " CULL");
|
sb.Append(side ? " TRV" : " CULL");
|
||||||
sb.Append(" proj=").Append(projN).Append(" clip=").Append(clipN);
|
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);
|
if (ndcText.Length > 0) sb.Append(" ndc=").Append(ndcText);
|
||||||
}
|
}
|
||||||
sb.Append(" || outPolys=").Append(frame.OutsideView.Polygons.Count);
|
sb.Append(" || outPolys=").Append(frame.OutsideView.Polygons.Count);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue