fix(render): Phase A8.F — empty OutsideView draws no outdoor terrain (cellar flood fix)

First-fix from the visual-gate-failure handoff: an empty OutsideView means
"no outdoors visible from here," not "all outdoors." When inside a building
with an empty clipped mask, Step 4 now draws NO terrain/scenery instead of
disabling the stencil and flooding ungated terrain over the cell interior
(the Step-3 walls already occupy the framebuffer). Visual-confirmed: Holtburg
cottage cellar walls are solid now, no terrain bleed-through.

Also adds portal diagnostics that root-caused so-called "Bug B":
- PortalVisibilityBuilder: per-camera-cell CAMPORTAL census (polyLen +
  side-test result) emitted BEFORE the BFS guards, so an empty OUTSIDEVIEW
  can be traced to the exact gate.
- A8CellAudit `portals`: replicate BuildLoadedCell's polygon-vertex
  resolution so PortalPolygons[i] validity is checkable offline.

Finding: the builder is largely CORRECT — it produces narrowed clipped
OutsideView regions for most cells (0172/0173/0162/015E/0165/016F). The
empty cases are mostly legitimate (windowless cellar can't see out; the
3rd-person camera eye on the outdoor side of a front-door plane culls that
exit). The handoff's Finding 2 ("under-produces, never narrows") is
substantially not real. Remaining wall-missing regressions in OTHER
buildings live in the cross-building Step-5 enforcement, escalated separately.

All gated behind ACDREAM_A8_INDOOR_BRANCH=1; default play unaffected.
App tests 108/108.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-29 15:17:21 +02:00
parent cf3d49cbd7
commit 9417d3c4ce
3 changed files with 104 additions and 31 deletions

View file

@ -137,6 +137,14 @@ static void DumpCellPortals(DatCollection dats, uint envCellId)
return;
}
// Resolve cellStruct so we can replicate BuildLoadedCell's portal-polygon
// vertex resolution and report what LoadedCell.PortalPolygons[i] would hold.
var envId = 0x0D000000u | envCell.EnvironmentId;
var environment = dats.Get<DatReaderWriter.DBObjs.Environment>(envId);
DatReaderWriter.Types.CellStruct? cellStruct = null;
if (environment is not null && environment.Cells.TryGetValue(envCell.CellStructure, out var cs))
cellStruct = cs;
uint lbPrefix = envCellId & 0xFFFF0000u;
int exitCount = 0;
int interiorCount = 0;
@ -148,9 +156,42 @@ static void DumpCellPortals(DatCollection dats, uint envCellId)
string dest = isExit
? "EXIT(outdoor)"
: $"0x{(lbPrefix | (uint)portal.OtherCellId):X8}";
// Replicate BuildLoadedCell (GameWindow.cs:5816-5838): look up the portal
// polygon by portal.PolygonId, require >= 3 verts, resolve every VertexId.
// If any vertex is missing the loader stores an EMPTY array, which the
// PortalVisibilityBuilder's guard (poly.Length < 3) silently skips.
string resolveText;
if (cellStruct is null)
{
resolveText = "no-cellStruct";
}
else if (!cellStruct.Polygons.TryGetValue(portal.PolygonId, out var poly))
{
resolveText = $"polygon {portal.PolygonId} NOT in cellStruct.Polygons (keys=[{string.Join(",", cellStruct.Polygons.Keys.OrderBy(k => k))}])";
}
else
{
int vc = poly.VertexIds.Count;
int resolved = 0;
var missing = new List<int>();
for (int vi = 0; vi < vc; vi++)
{
if (cellStruct.VertexArray.Vertices.ContainsKey((ushort)poly.VertexIds[vi]))
resolved++;
else
missing.Add(poly.VertexIds[vi]);
}
bool wouldResolve = vc >= 3 && resolved == vc;
resolveText =
$"polyVerts={vc} resolved={resolved} BUILDER_SEES={(wouldResolve ? "OK" : "EMPTY/SKIPPED")} " +
$"vids=[{string.Join(",", poly.VertexIds)}]" +
(missing.Count > 0 ? $" MISSING=[{string.Join(",", missing)}]" : "");
}
Console.WriteLine(
$" portal[{i}] other=0x{portal.OtherCellId:X4} -> {dest} " +
$"flags={portal.Flags} polyId={portal.PolygonId}");
$"flags={portal.Flags} polyId={portal.PolygonId} | {resolveText}");
}
Console.WriteLine(