refactor(rendering): delete superseded visibility probes

Delete the callerless portal-BFS research graph and spent renderer probe families while retaining the production RetailFrameWalk path, terrain diagnostics, membership invariant, and walk transcript.

Mutation evidence (all restored):

1. Restored PortalVisibilityBuilder type -> AppAssembly_ContainsNoSupersededPortalGraphTypes first failed Assert.Empty with AcDream.App.Rendering.PortalVisibilityBuilder.

2. Restored ACDREAM_PROBE_FACILITY_STAIRS -> ProductionSource_ContainsNoDeletedRendererProbe_AndRetainsWalkTranscriptProof first failed Assert.Empty on RenderingDiagnostics.cs.

3. Added a second RetailFrameWalk field -> WalkFrameOwners_AreUnique first failed Assert.Single with _frameWalk and _mutatedSecondFrameWalk.

4. Added OrderBy to OrderedStream -> OrderedWalkStream_HasNoCrossStreamReorder first failed Assert.DoesNotContain on OrderBy(.

5. Added IDatReaderWriter parameter -> FrameTimeWalkOwners_HaveNoRawDatDependency first failed Assert.Empty on RetailFrameWalk.MutatedRawDatParameter.
This commit is contained in:
Erik 2026-09-05 04:46:31 +02:00
parent b77989c323
commit bf53e2ad6e
82 changed files with 451 additions and 10108 deletions

View file

@ -325,11 +325,6 @@ public sealed class HostInputCameraCompositionTests
GpuFrameFlightController? frameFlights,
IGpuDevice device) => frameFlights!;
public WorldRenderDiagnostics? CreateWorldRenderDiagnostics(
GameWindowGraphics graphics,
IRenderFrameDiagnosticLog log) =>
new(new GlStateReader(), log);
public SilkKeyboardSource CreateKeyboardSource(
IKeyboard keyboard,
HostQuiescenceGate quiescence) =>
@ -498,12 +493,6 @@ public sealed class HostInputCameraCompositionTests
public void Delete(nint fence) { }
}
private sealed class GlStateReader : IRenderGlStateReader
{
public RenderGlStateSnapshot CaptureState() => default;
public RenderGlScissorSnapshot CaptureScissor() => default;
}
private sealed class DiagnosticLog : IRenderFrameDiagnosticLog
{
public void WriteLine(string message) { }

View file

@ -19,7 +19,7 @@ namespace AcDream.App.Tests.Rendering;
/// each class's <c>finally</c> sufficient.
/// </para>
///
/// <para>Three distinct globals are covered, and every one of them is a shared
/// <para>The shared globals below are covered, and every one of them is an
/// edge between at least two classes in this collection:</para>
/// <list type="bullet">
/// <item><description><c>AcDream.Core.Rendering.CameraDiagnostics</c> —
@ -28,15 +28,12 @@ namespace AcDream.App.Tests.Rendering;
/// <c>RetailChaseCamera.Update</c>, <c>CameraController.Active</c>,
/// <c>CameraFrameController</c>, <c>WorldRenderFrameBuilder</c> and
/// <c>MouseLookController</c>.</description></item>
/// <item><description><c>AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled</c>
/// — written by <c>CornerFloodReplayTests</c> and
/// <c>Issue181WallPressEquilibriumTests</c>.</description></item>
/// <item><description><c>AcDream.Core.Rendering.RenderingDiagnostics.DumpWalkTranscriptEnabled</c>
/// (Campaign OVERHAUL S3 chunk 1) — written by
/// <c>WalkFrameDriverTests</c>' transcript-emitter tests
/// (<c>WalkFrameDriverTranscriptTests.cs</c>).</description></item>
/// <item><description><c>System.Console.Out</c> — redirected via
/// <c>Console.SetOut</c> by those same classes to capture probe output.
/// <c>Console.SetOut</c> by the transcript tests to capture output.
/// Interleaved redirection can restore a DISPOSED <c>StringWriter</c> as the
/// process-wide <c>Console.Out</c>, which then throws in unrelated
/// tests.</description></item>

View file

@ -416,7 +416,7 @@ public sealed class ChargenPreviewControllerTests
/// </summary>
private bool TryOpen(out DatCollection? dats, out DatCollectionAdapter? adapter)
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
_out.WriteLine("SKIP: dats unavailable");

View file

@ -29,7 +29,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuild_AluvianMaleDefaultSelection_ProducesANonEmptyStaticPoseEntity()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -87,7 +87,7 @@ public sealed class ChargenPreviewEntityBuilderTests
ChargenPreviewEntityBuilder.PreviewBackdropRenderId,
ChargenPreviewEntityBuilder.SummaryPreviewBackdropRenderId);
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -121,7 +121,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuild_UnknownSetupId_ReturnsNull()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -145,7 +145,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuild_OlthoiHeritage_ResolvesADifferentRestPoseDidThanStandardHeritages()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -186,7 +186,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildAnimated_AluvianMaleDefaultSelection_ResolvesARealIdleCycle()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -232,7 +232,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildAnimated_UnknownSetupId_ReturnsNull()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -274,7 +274,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[InlineData(0x10000013u)] // OlthoiAcid's shared idle/rest enum key.
public void OlthoiFamily_SharedIdleRestEnumKey_ResolvesToARealInstalledDid(uint sharedEnumKey)
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -297,7 +297,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildAnimated_OlthoiHeritage_ResolvesARealIdleAnimationToo()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -337,7 +337,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildBackdrop_AluvianHeritage_ResolvesANonEmptyMesh()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -376,7 +376,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildBackdrop_ExplicitRenderId_StampsThatIdOnTheEntity()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -405,7 +405,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildBackdrop_UnsetEnvironmentSetupId_ReturnsNull()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -419,7 +419,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildBackdrop_UnknownSetupId_ReturnsNull()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
@ -440,7 +440,7 @@ public sealed class ChargenPreviewEntityBuilderTests
[Fact]
public void TryBuildBackdrop_AllThirteenHeritages_ResolveOrAreReportedByName()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);

View file

@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
@ -7,30 +6,12 @@ namespace AcDream.App.Tests.Rendering;
public class ClipFrameAssemblerTests
{
private static ViewPolygon Square(float cx, float cy, float half) => new(new[]
{
new Vector2(cx - half, cy - half),
new Vector2(cx + half, cy - half),
new Vector2(cx + half, cy + half),
new Vector2(cx - half, cy + half),
});
private static CellView ViewOf(params ViewPolygon[] polys)
{
var view = new CellView();
foreach (var p in polys)
view.Add(p);
return view;
}
[Fact]
public void BeginWalkFrame_OutdoorRoot_SeedsOneFullScreenDefaultView()
{
using ClipFrame frame = ClipFrame.NoClip();
ClipFrameAssembly assembly = ClipFrameAssembler.BeginWalkFrame(
frame,
outdoorRoot: true);
ClipFrameAssembly assembly = ClipFrameAssembler.BeginWalkFrame(frame, outdoorRoot: true);
ClipViewSlice slice = Assert.Single(assembly.OutsideViewSlices);
Assert.Equal(0, slice.Slot);
@ -45,9 +26,7 @@ public class ClipFrameAssemblerTests
public void BeginWalkFrame_InteriorRoot_ResetsReusedOutdoorAssemblyToEmpty()
{
using ClipFrame frame = ClipFrame.NoClip();
ClipFrameAssembly reuse = ClipFrameAssembler.BeginWalkFrame(
frame,
outdoorRoot: true);
ClipFrameAssembly reuse = ClipFrameAssembler.BeginWalkFrame(frame, outdoorRoot: true);
ClipFrameAssembly assembly = ClipFrameAssembler.BeginWalkFrame(
frame,
@ -62,270 +41,4 @@ public class ClipFrameAssemblerTests
Assert.Equal(0, assembly.ScissorFallbacks);
Assert.Equal(1, frame.SlotCount);
}
[Fact]
public void TwoVisibleCells_PlusOutsideView_ProducesCorrectSlotMapAndCounts()
{
const uint cellA = 0xA9B40100;
const uint cellB = 0xA9B40101;
var pv = new PortalVisibilityFrame();
pv.CellViews[cellA] = ViewOf(Square(-0.3f, 0f, 0.3f));
pv.CellViews[cellB] = ViewOf(Square(0.3f, 0f, 0.2f));
pv.OrderedVisibleCells.Add(cellA);
pv.OrderedVisibleCells.Add(cellB);
pv.OutsideView.Add(Square(0f, 0.5f, 0.25f));
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.Equal(4, asm.Frame.SlotCount); // slot 0 + two cells + one outside slice
Assert.Contains(cellA, asm.CellIdToSlot.Keys);
Assert.Contains(cellB, asm.CellIdToSlot.Keys);
Assert.NotEqual(0, asm.CellIdToSlot[cellA]);
Assert.NotEqual(0, asm.CellIdToSlot[cellB]);
Assert.NotEqual(asm.CellIdToSlot[cellA], asm.CellIdToSlot[cellB]);
Assert.Equal(4, asm.PerCellPlaneCounts[cellA]);
Assert.Equal(4, asm.PerCellPlaneCounts[cellB]);
Assert.True(asm.OutdoorVisible);
Assert.NotEqual(0, asm.OutdoorSlot);
Assert.Single(asm.OutsideViewSlices);
Assert.Equal(asm.OutdoorSlot, asm.OutsideViewSlices[0].Slot);
Assert.Equal(4, asm.OutsidePlaneCount);
Assert.Equal(0, asm.ScissorFallbacks);
}
[Fact]
public void NothingVisibleCell_IsExcludedFromSlotMap_AndNotAppended()
{
const uint cellA = 0xA9B40100;
const uint cellB = 0xA9B40101;
var pv = new PortalVisibilityFrame();
pv.CellViews[cellA] = ViewOf(Square(0f, 0f, 0.3f));
pv.CellViews[cellB] = new CellView();
pv.OrderedVisibleCells.Add(cellA);
pv.OrderedVisibleCells.Add(cellB);
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.Equal(2, asm.Frame.SlotCount);
Assert.Contains(cellA, asm.CellIdToSlot.Keys);
Assert.DoesNotContain(cellB, asm.CellIdToSlot.Keys);
Assert.False(asm.OutdoorVisible);
Assert.Empty(asm.OutsideViewSlices);
Assert.Equal(0, asm.OutsidePlaneCount);
}
[Fact]
public void OutsideViewMultiPolygon_PreservesRetailSlices()
{
const uint cellA = 0xA9B40100;
var pv = new PortalVisibilityFrame();
pv.CellViews[cellA] = ViewOf(Square(0f, 0f, 0.3f));
pv.OrderedVisibleCells.Add(cellA);
pv.OutsideView.Add(Square(-0.5f, 0f, 0.1f));
pv.OutsideView.Add(Square(0.5f, 0f, 0.1f));
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.True(asm.OutdoorVisible);
Assert.NotEqual(0, asm.OutdoorSlot);
Assert.Equal(2, asm.OutsideViewSlices.Length);
Assert.NotEqual(asm.OutsideViewSlices[0].Slot, asm.OutsideViewSlices[1].Slot);
Assert.Equal(4, asm.OutsidePlaneCount);
Assert.Equal(0, asm.ScissorFallbacks);
Assert.Equal(4, asm.Frame.SlotCount); // slot 0 + cell + two outside slices
}
[Fact]
public void CellMultiPolygonView_PreservesRetailViewSlices()
{
const uint cellA = 0xA9B40100;
var pv = new PortalVisibilityFrame();
pv.CellViews[cellA] = ViewOf(
Square(-0.4f, 0f, 0.1f),
Square(0.4f, 0f, 0.1f));
pv.OrderedVisibleCells.Add(cellA);
pv.OutsideView.Add(Square(0f, 0f, 0.3f));
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.True(asm.CellIdToSlot[cellA] > 0);
Assert.Equal(2, asm.CellIdToViewSlots[cellA].Length);
Assert.Equal(2, asm.CellIdToViewSlices[cellA].Length);
Assert.NotEqual(asm.CellIdToViewSlots[cellA][0], asm.CellIdToViewSlots[cellA][1]);
Assert.Equal(4, asm.PerCellPlaneCounts[cellA]);
Assert.Single(asm.OutsideViewSlices);
Assert.Equal(4, asm.Frame.SlotCount); // slot 0 + two cell slices + outside slice
Assert.Equal(0, asm.ScissorFallbacks);
}
[Fact]
public void Assemble_OutsideViewWithExitPortal_HasOutsideViewTrue_AabbMatchesBounds()
{
var pv = new PortalVisibilityFrame();
pv.OutsideView.Add(Square(-0.3f, 0.2f, 0.25f));
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.True(asm.HasOutsideView);
Assert.Single(asm.OutsideViewSlices);
var expected = new Vector4(
pv.OutsideView.MinX, pv.OutsideView.MinY,
pv.OutsideView.MaxX, pv.OutsideView.MaxY);
Assert.Equal(expected, asm.OutsideViewNdcAabb);
}
[Fact]
public void Assemble_OutsideViewMultiPolygon_PreservesSlicesAndUnionAabb()
{
var pv = new PortalVisibilityFrame();
pv.OutsideView.Add(Square(-0.6f, 0f, 0.15f));
pv.OutsideView.Add(Square(0.6f, 0f, 0.15f));
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.True(asm.HasOutsideView);
Assert.Equal(2, asm.OutsideViewSlices.Length);
var expected = new Vector4(
pv.OutsideView.MinX, pv.OutsideView.MinY,
pv.OutsideView.MaxX, pv.OutsideView.MaxY);
Assert.Equal(expected, asm.OutsideViewNdcAabb);
}
[Fact]
public void Assemble_EmptyOutsideView_HasOutsideViewFalse_AabbZero()
{
const uint cellA = 0xA9B40100;
var pv = new PortalVisibilityFrame();
pv.CellViews[cellA] = ViewOf(Square(0f, 0f, 0.3f));
pv.OrderedVisibleCells.Add(cellA);
var asm = ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
Assert.False(asm.HasOutsideView);
Assert.Equal(Vector4.Zero, asm.OutsideViewNdcAabb);
}
[Fact]
public void Reset_ReusesFrame_NoSlotLeakAcrossAssemblies()
{
var frame = ClipFrame.NoClip();
var pv1 = new PortalVisibilityFrame();
pv1.CellViews[0xA9B40100] = ViewOf(Square(-0.3f, 0f, 0.2f));
pv1.CellViews[0xA9B40101] = ViewOf(Square(0.3f, 0f, 0.2f));
pv1.OrderedVisibleCells.Add(0xA9B40100);
pv1.OrderedVisibleCells.Add(0xA9B40101);
pv1.OutsideView.Add(Square(0f, 0.4f, 0.2f));
var asm1 = ClipFrameAssembler.Assemble(frame, pv1);
Assert.Equal(4, asm1.Frame.SlotCount);
var pv2 = new PortalVisibilityFrame();
pv2.CellViews[0xA9B40200] = ViewOf(Square(0f, 0f, 0.25f));
pv2.OrderedVisibleCells.Add(0xA9B40200);
var asm2 = ClipFrameAssembler.Assemble(frame, pv2);
Assert.Equal(2, asm2.Frame.SlotCount);
Assert.Contains(0xA9B40200, asm2.CellIdToSlot.Keys);
Assert.DoesNotContain(0xA9B40100, asm2.CellIdToSlot.Keys);
Assert.False(asm2.OutdoorVisible);
}
[Fact]
public void Assemble_ReusedAssembly_ClearsStateAndReusesExactLengthArrays()
{
const uint firstCell = 0xA9B40100;
const uint secondCell = 0xA9B40200;
var frame = ClipFrame.NoClip();
var reuse = new ClipFrameAssembly();
var first = new PortalVisibilityFrame();
first.CellViews[firstCell] = ViewOf(
Square(-0.4f, 0f, 0.15f),
Square(0.4f, 0f, 0.15f));
first.OrderedVisibleCells.Add(firstCell);
first.OutsideView.Add(Square(0f, 0.5f, 0.2f));
ClipFrameAssembly firstResult = ClipFrameAssembler.Assemble(frame, first, reuse);
Assert.Same(reuse, firstResult);
int sliceAllocations = reuse.SliceArrayAllocationCount;
int slotAllocations = reuse.SlotArrayAllocationCount;
var second = new PortalVisibilityFrame();
second.CellViews[secondCell] = ViewOf(
Square(-0.4f, 0f, 0.15f),
Square(0.4f, 0f, 0.15f));
second.OrderedVisibleCells.Add(secondCell);
second.OutsideView.Add(Square(0f, 0.5f, 0.2f));
for (int i = 0; i < 12; i++)
{
ClipFrameAssembly actual = ClipFrameAssembler.Assemble(frame, second, reuse);
Assert.Same(reuse, actual);
Assert.DoesNotContain(firstCell, actual.CellIdToViewSlices.Keys);
Assert.Contains(secondCell, actual.CellIdToViewSlices.Keys);
Assert.Equal(2, actual.CellIdToViewSlices[secondCell].Length);
Assert.Equal(2, actual.CellIdToViewSlots[secondCell].Length);
Assert.Single(actual.OutsideViewSlices);
Assert.Equal(sliceAllocations, reuse.SliceArrayAllocationCount);
Assert.Equal(slotAllocations, reuse.SlotArrayAllocationCount);
}
var empty = new PortalVisibilityFrame();
ClipFrameAssembly cleared = ClipFrameAssembler.Assemble(frame, empty, reuse);
Assert.Empty(cleared.CellIdToSlot);
Assert.Empty(cleared.CellIdToViewSlots);
Assert.Empty(cleared.CellIdToViewSlices);
Assert.Empty(cleared.PerCellPlaneCounts);
Assert.Empty(cleared.OutsideViewSlices);
}
[Fact]
public void Assemble_ReusedAssembly_ChangingCardinalityStaysBoundedThenReachesSteadyState()
{
const uint cellId = 0xA9B40100;
var frame = ClipFrame.NoClip();
var reuse = new ClipFrameAssembly();
static PortalVisibilityFrame FrameWithSlices(uint id, int count)
{
var portalFrame = new PortalVisibilityFrame();
var view = new CellView();
for (int i = 0; i < count; i++)
{
// The assembler consumes the retail view_poly list directly. Distinct placement is
// irrelevant to this cache-cardinality stress; avoid CellView dedup collapsing it.
view.Polygons.Add(Square(i * 0.001f, 0f, 0.0004f));
}
portalFrame.CellViews[id] = view;
portalFrame.OrderedVisibleCells.Add(id);
return portalFrame;
}
for (int count = 1; count <= 180; count++)
ClipFrameAssembler.Assemble(frame, FrameWithSlices(cellId, count), reuse);
// Flush the final live arrays into the cache too.
ClipFrameAssembler.Assemble(frame, new PortalVisibilityFrame(), reuse);
Assert.InRange(reuse.RetainedSliceItems, 0, ClipFrameAssembly.MaxRetainedSliceItems);
Assert.InRange(reuse.RetainedSlotItems, 0, ClipFrameAssembly.MaxRetainedSlotItems);
Assert.InRange(reuse.RetainedSliceArrays, 0, ClipFrameAssembly.MaxRetainedArraysPerPool);
Assert.InRange(reuse.RetainedSlotArrays, 0, ClipFrameAssembly.MaxRetainedArraysPerPool);
PortalVisibilityFrame steady = FrameWithSlices(cellId, 73);
ClipFrameAssembler.Assemble(frame, steady, reuse);
int sliceAllocations = reuse.SliceArrayAllocationCount;
int slotAllocations = reuse.SlotArrayAllocationCount;
for (int i = 0; i < 16; i++)
{
ClipFrameAssembler.Assemble(frame, steady, reuse);
Assert.Equal(sliceAllocations, reuse.SliceArrayAllocationCount);
Assert.Equal(slotAllocations, reuse.SlotArrayAllocationCount);
}
}
}

View file

@ -1,685 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// §4 corner-press flood replay (2026-06-10). The §2b camera-collision hypothesis is
/// REFUTED (CameraCornerSealReplayTests, commit b21bb28): the eye legitimately enters
/// the neighbour room through the 0171↔0173↔0172 doorway chain. The user still sees
/// BACKGROUND at certain angles — so the defect is in the FLOOD/CLIP output for those
/// eye positions. This harness drives the REAL <see cref="PortalVisibilityBuilder.Build"/>
/// over the REAL Holtburg building cells (dat-loaded, mirroring EnvCellLandblockBuildBuilder)
/// along the captured corner eye path, and prints which cells the flood keeps/drops per
/// step. The player's room (0172) dropping while the player is visible on screen is the
/// background defect; the step where it happens pins the mechanism.
///
/// Geometry (corner-seal-capture.log + the b21bb28 room map): all cells share origin
/// (161.93, 7.50, 94.00), local = R180z·(worldorigin). The 0171-side doorway plane is
/// local x=4.10 (world x≈157.83), the 0172-side plane local x=3.90 (world x≈158.03);
/// the opening is 1.9 m wide, full height (z 94..96.5). Player dwell position during the
/// corner press: (159.94, 7.70, 94.00) in 0172; captured eyes hover near (157.4..157.5,
/// 7.91, 96.25) — inside 0171, 0.35 m past the doorway plane, near the ceiling.
/// </summary>
[Collection(CameraDiagnosticsCollection.Name)]
public class CornerFloodReplayTests
{
private readonly ITestOutputHelper _out;
public CornerFloodReplayTests(ITestOutputHelper output) => _out = output;
internal const uint Landblock = 0xA9B40000u;
private const uint EnvironmentFilePrefix = 0x0D000000u;
internal static string? ResolveDatDir()
{
var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv))
return fromEnv;
var def = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(def) ? def : null;
}
/// <summary>
/// Dat → LoadedCell, mirroring EnvCellLandblockBuildBuilder.
/// field-for-field: portals (with OtherPortalId back-link), clip planes from the
/// portal polygon's first 3 verts + centroid InsideSide, full portal polygons in
/// cell-local space, local AABB, world transform from EnvCell.Position.
/// </summary>
internal static LoadedCell LoadCell(DatCollection dats, uint cellId)
=> LoadCell(dats, cellId, Vector3.Zero);
/// <summary>worldOffset: block offset for multi-landblock fixtures (dat cell
/// positions are landblock-local; a neighbour block needs ±192 per axis).</summary>
internal static LoadedCell LoadCell(DatCollection dats, uint cellId, Vector3 worldOffset)
{
var envCell = dats.Get<DatEnvCell>(cellId)
?? throw new InvalidOperationException($"EnvCell 0x{cellId:X8} not found");
var environment = dats.Get<DatEnvironment>(EnvironmentFilePrefix | envCell.EnvironmentId)
?? throw new InvalidOperationException($"Environment 0x{envCell.EnvironmentId:X8} not found");
if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct) || cellStruct is null)
throw new InvalidOperationException($"CellStruct {envCell.CellStructure} missing");
var cellTransform =
Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) *
Matrix4x4.CreateTranslation(envCell.Position.Origin + worldOffset);
Matrix4x4.Invert(cellTransform, out var inverse);
var boundsMin = new Vector3(float.MaxValue);
var boundsMax = new Vector3(float.MinValue);
foreach (var kvp in cellStruct.VertexArray.Vertices)
{
var v = kvp.Value;
var pos = new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z);
boundsMin = Vector3.Min(boundsMin, pos);
boundsMax = Vector3.Max(boundsMax, pos);
}
if (boundsMin.X == float.MaxValue) { boundsMin = Vector3.Zero; boundsMax = Vector3.Zero; }
var portals = new List<CellPortalInfo>();
var clipPlanes = new List<PortalClipPlane>();
var portalPolygons = new List<Vector3[]>();
foreach (var portal in envCell.CellPortals)
{
portals.Add(new CellPortalInfo(
portal.OtherCellId, portal.PolygonId, (ushort)portal.Flags, portal.OtherPortalId));
if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var poly)
&& poly.VertexIds.Count >= 3
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[0], out var v0)
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[1], out var v1)
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[2], out var v2))
{
var p0 = new Vector3(v0.Origin.X, v0.Origin.Y, v0.Origin.Z);
var p1 = new Vector3(v1.Origin.X, v1.Origin.Y, v1.Origin.Z);
var p2 = new Vector3(v2.Origin.X, v2.Origin.Y, v2.Origin.Z);
var normal = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0));
float d = -Vector3.Dot(normal, p0);
// InsideSide from the dat PortalSide bit — mirrors the production fix
// (EnvCellLandblockBuildBuilder): retail InitCell (0x005a4b70) + physics
// CellTransit use the dat bit; the old AABB-centroid guess mis-sided thin
// connectors (#186). Kept identical here so this replay stays a faithful mirror.
clipPlanes.Add(new PortalClipPlane
{
Normal = normal, D = d, InsideSide = ((ushort)portal.Flags & 0x2) == 0 ? 1 : 0,
});
}
else
{
clipPlanes.Add(default);
}
Vector3[] polyVerts = Array.Empty<Vector3>();
if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var portalPoly)
&& portalPoly.VertexIds.Count >= 3)
{
polyVerts = new Vector3[portalPoly.VertexIds.Count];
bool allResolved = true;
for (int vi = 0; vi < portalPoly.VertexIds.Count; vi++)
{
if (cellStruct.VertexArray.Vertices.TryGetValue(
(ushort)portalPoly.VertexIds[vi], out var pv))
polyVerts[vi] = new Vector3(pv.Origin.X, pv.Origin.Y, pv.Origin.Z);
else { allResolved = false; break; }
}
if (!allResolved) polyVerts = Array.Empty<Vector3>();
}
portalPolygons.Add(polyVerts);
}
uint lbPrefix = cellId & 0xFFFF0000u;
var visibleCells = new List<uint>();
if (envCell.VisibleCells is not null)
foreach (var lowId in envCell.VisibleCells)
visibleCells.Add(lbPrefix | lowId);
return new LoadedCell
{
CellId = cellId,
WorldPosition = envCell.Position.Origin + worldOffset,
WorldTransform = cellTransform,
InverseWorldTransform = inverse,
LocalBoundsMin = boundsMin,
LocalBoundsMax = boundsMax,
Portals = portals,
ClipPlanes = clipPlanes,
PortalPolygons = portalPolygons,
VisibleCells = visibleCells,
SeenOutside = envCell.Flags.HasFlag(DatReaderWriter.Enums.EnvCellFlags.SeenOutside),
};
}
internal static Dictionary<uint, LoadedCell> LoadBuilding(DatCollection dats)
{
var cells = new Dictionary<uint, LoadedCell>();
for (uint low = 0x016Fu; low <= 0x0175u; low++)
{
uint id = Landblock | low;
cells[id] = LoadCell(dats, id);
}
return cells;
}
// Production projection: ChaseCamera/FlyCamera use FovY ~1.2, near 1, far 5000,
// 1280x720 (the capture's viewport). The flood's clip is near-independent
// (PortalProjection header), so near/far exactness is not load-bearing.
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 1f, 5000f);
return view * proj;
}
// World-x of the doorway planes (local x=4.10 / 3.90 under the R180z transform
// about origin x=161.93).
private const float Plane0171X = 161.93f - 4.10f; // ≈157.83
private const float Plane0172X = 161.93f - 3.90f; // ≈158.03
private static uint RootCellFor(float eyeX) =>
eyeX > Plane0172X ? (Landblock | 0x0172u)
: eyeX > Plane0171X ? (Landblock | 0x0173u)
: (Landblock | 0x0171u);
/// <summary>
/// Diagnostic: full [pv-trace] decision log for one GOOD step (47) vs one GLITCH
/// step (48) of the sweep below — 2 cm apart, root 0171 both, yet 0172 (and its
/// downstream 016F + outside) vanish from the flood at 48. The trace diff pins the
/// exact gate that flips.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_TraceGoodVsGlitchStep()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var player = new Vector3(159.936676f, 7.701012f, 94.000000f);
var pivot = player + new Vector3(0f, 0f, 1.5f);
foreach (int i in new[] { 47, 48 })
{
float ex = 158.43f - i * 0.02f;
var eye = new Vector3(ex, 7.912722f, 96.248833f);
uint root = RootCellFor(ex);
var sw = new StringWriter();
var prev = Console.Out;
PortalVisibilityFrame frame;
try
{
Console.SetOut(sw);
AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled = true;
frame = PortalVisibilityBuilder.Build(cells[root], eye, lookup, ViewProjFor(eye, pivot));
}
finally
{
AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled = false;
Console.SetOut(prev);
}
_out.WriteLine($"########## step {i} (eyeX={ex:F2}) ##########");
foreach (var line in sw.ToString().Split('\n', StringSplitOptions.RemoveEmptyEntries))
_out.WriteLine(line.TrimEnd());
// Per-hop region vertices: where does the sliver come from?
foreach (uint low in new uint[] { 0x0171u, 0x0173u, 0x0172u })
{
if (!frame.CellViews.TryGetValue(Landblock | low, out var view))
{
_out.WriteLine($" view 0x{low:X4}: (absent)");
continue;
}
foreach (var p in view.Polygons)
{
string verts = "";
foreach (var v in p.Vertices)
verts += System.FormattableString.Invariant($" ({v.X:F5},{v.Y:F5})");
_out.WriteLine($" view 0x{low:X4} [{p.Vertices.Length}]:{verts}");
}
if (view.Polygons.Count == 0)
_out.WriteLine($" view 0x{low:X4}: 0 polygons (rejected by Add)");
}
}
}
/// <summary>
/// §4 conformance gate (2026-06-08 handoff §7's pre-gate, realized 2026-06-10):
/// a smooth monotonic eye moving through/near a doorway must produce a STABLE,
/// monotone flood — the full cell chain present on every step, the outside view
/// always reached, and the player-room region never collapsing nor oscillating.
/// Before the homogeneous-reciprocal fix this failed on ~10 of 61 steps (room
/// 0172 vanished entirely or collapsed to a sub-pixel sliver — the corner /
/// transition background strobe). Guards the ApplyReciprocalClip pipeline.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
public void CornerSweep_FloodIsCompleteAndMonotone()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var player = new Vector3(159.936676f, 7.701012f, 94.000000f);
var pivot = player + new Vector3(0f, 0f, 1.5f);
// W=0 port (2026-06-11) — the criterion is retail's, not the rescue's:
// - Cells BEHIND the camera are NOT required (retail polyClipFinish clips an
// all-behind portal to empty; the rescue used to flood them anyway). The eye
// looks AT the player throughout, so the doorway chain is behind the camera
// until the eye recedes through it: require 0173 from root=0173 on, 0171
// from root=0171 on. 0172 (the looked-at room with the player) is required
// at EVERY step — that is THE user-visible §4 invariant.
// - The two KNIFE-EDGE steps (eye exactly ON a doorway plane, the sweep grid
// lands on the plane constants) propagate retail's zero-area degenerate view:
// the chain stays in the draw list (cells draw whole) but the 0172 region is
// legitimately zero-area and the onward flood (016F/outside) legitimately
// dies for that frame — exempt those assertions there.
// - Monotone shrink holds WITHIN a root regime; the root flip is a legitimate
// discontinuity (FullScreen root view -> portal-clipped view).
var failures = new List<string>();
float prevArea = float.MaxValue;
uint prevRoot = 0;
for (int i = 0; i <= 60; i++)
{
float ex = 158.43f - i * 0.02f;
var eye = new Vector3(ex, 7.912722f, 96.248833f);
uint root = RootCellFor(ex);
bool knifeEdge = MathF.Abs(ex - Plane0172X) < 0.005f || MathF.Abs(ex - Plane0171X) < 0.005f;
var frame = PortalVisibilityBuilder.Build(
cells[root], eye, lookup, ViewProjFor(eye, pivot));
// Knife-edge steps: the eye sits EXACTLY on a cell-boundary plane, so the
// root pick itself is ambiguous (production BSP picks either side; a damped
// float eye never lands exactly on the plane). The portal whose plane
// contains the eye is ~perpendicular to the gaze here — genuinely
// off-screen, retail's screen-bounded clip floods nothing through it from
// the far-side root. Require only the root; the neighbouring steps (±2 cm,
// where the real strobe class lived) carry the full criterion.
var required = new List<uint>();
if (knifeEdge)
{
required.Add(root & 0xFFFFu);
}
else
{
required.Add(0x0172u);
if (root != (Landblock | 0x0172u)) required.Add(0x0173u);
if (root == (Landblock | 0x0171u)) required.Add(0x0171u);
required.Add(0x016Fu);
}
foreach (uint low in required)
if (!frame.OrderedVisibleCells.Contains(Landblock | low))
failures.Add($"step {i}: 0x{low:X4} missing from flood (root=0x{root & 0xFFFFu:X4}{(knifeEdge ? ", knife-edge" : "")})");
if (!knifeEdge && frame.OutsideView.Polygons.Count == 0)
failures.Add($"step {i}: outside view empty");
float area = 0f;
if (frame.CellViews.TryGetValue(Landblock | 0x0172u, out var view))
foreach (var p in view.Polygons)
area += MathF.Max(0f, p.MaxX - p.MinX) * MathF.Max(0f, p.MaxY - p.MinY);
if (!knifeEdge && area < 0.5f)
failures.Add(System.FormattableString.Invariant(
$"step {i}: 0172 region collapsed (area={area:F3})"));
// Monotone shrink as the eye recedes — allow float-noise upticks only.
// Reset at root flips + knife-edge frames (legitimate discontinuities).
if (root == prevRoot && !knifeEdge && prevArea != float.MaxValue && area > prevArea + 0.01f)
failures.Add(System.FormattableString.Invariant(
$"step {i}: 0172 region grew {prevArea:F3}->{area:F3} (oscillation)"));
prevArea = knifeEdge ? float.MaxValue : area;
prevRoot = root;
}
Assert.True(failures.Count == 0,
"Flood instability under a monotonic eye sweep (the §4 strobe):\n "
+ string.Join("\n ", failures));
}
/// <summary>
/// #120 repro (T5 gate, 2026-06-11): the T5 launch log fired
/// `[pv-ERROR] in-place propagation tripwire at depth 128` on cottage
/// interior cells 0xA9B40175/0174 (+0162, a different building) while
/// the user walked the doorways — i.e. the eye-on-portal-plane regime.
/// Sweep the eye ACROSS every portal plane of every cell in this
/// building (±6 cm in 5 mm steps, looking through the opening), seeding
/// <see cref="PortalVisibilityBuilder.Build"/> from BOTH sides' cells.
/// The in-place growth's fixpoint invariant must hold at every step —
/// the tripwire count stays 0.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
public void PortalPlaneCrossings_InPlacePropagationConverges()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
PortalVisibilityBuilder.ConvergenceTripwireCount = 0;
var firings = new List<string>();
foreach (var cell in cells.Values)
{
for (int i = 0; i < cell.Portals.Count && i < cell.PortalPolygons.Count; i++)
{
var poly = cell.PortalPolygons[i];
if (poly == null || poly.Length < 3) continue;
if (i >= cell.ClipPlanes.Count) continue;
var plane = cell.ClipPlanes[i];
if (plane.Normal.LengthSquared() < 1e-6f) continue;
// Portal centroid + plane normal in WORLD space.
var centroidLocal = Vector3.Zero;
foreach (var v in poly) centroidLocal += v;
centroidLocal /= poly.Length;
var centroidWorld = Vector3.Transform(centroidLocal, cell.WorldTransform);
var normalWorld = Vector3.Normalize(
Vector3.TransformNormal(plane.Normal, cell.WorldTransform));
uint neighbourId = cell.Portals[i].OtherCellId == 0xFFFF
? 0u
: (Landblock | cell.Portals[i].OtherCellId);
var roots = new List<LoadedCell> { cell };
if (neighbourId != 0u && cells.TryGetValue(neighbourId, out var nb))
roots.Add(nb);
for (int step = -12; step <= 12; step++)
{
var eye = centroidWorld + normalWorld * (step * 0.005f);
// Look through the opening (along -normal), slightly down
// — the portal fills the view, maximizing flood activity.
var lookAt = centroidWorld - normalWorld * 2f + new Vector3(0f, 0f, -0.2f);
foreach (var root in roots)
{
int before = PortalVisibilityBuilder.ConvergenceTripwireCount;
PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, lookAt));
int after = PortalVisibilityBuilder.ConvergenceTripwireCount;
if (after != before)
firings.Add(System.FormattableString.Invariant(
$"cell=0x{cell.CellId:X8} portal#{i}->0x{cell.Portals[i].OtherCellId:X4} step={step} root=0x{root.CellId:X8} eye=({eye.X:F4},{eye.Y:F4},{eye.Z:F4})"));
}
}
}
}
Assert.True(firings.Count == 0,
"#120: in-place propagation convergence tripwire fired during the " +
"portal-plane sweep:\n " + string.Join("\n ", firings));
}
/// <summary>
/// #120 repro attempt 2: eyes INSIDE each cell's volume (3×3 XY grid ×
/// 2 Z levels) with full yaw (8) × pitch (3) direction sweeps — the
/// walking-and-turning regime of the T5 session, including the steep
/// pitches of the cellar stairs. Same invariant: the tripwire count
/// stays 0 across every Build.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
public void InCellDirectionSweep_InPlacePropagationConverges()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
PortalVisibilityBuilder.ConvergenceTripwireCount = 0;
var firings = new List<string>();
int builds = 0;
foreach (var cell in cells.Values)
{
var lo = cell.LocalBoundsMin;
var hi = cell.LocalBoundsMax;
if (hi.X - lo.X < 0.05f || hi.Y - lo.Y < 0.05f) continue;
for (int gx = 0; gx < 3; gx++)
for (int gy = 0; gy < 3; gy++)
for (int gz = 0; gz < 2; gz++)
{
var local = new Vector3(
lo.X + (hi.X - lo.X) * (0.2f + 0.3f * gx),
lo.Y + (hi.Y - lo.Y) * (0.2f + 0.3f * gy),
lo.Z + (hi.Z - lo.Z) * (gz == 0 ? 0.3f : 0.8f));
var eye = Vector3.Transform(local, cell.WorldTransform);
for (int yaw = 0; yaw < 8; yaw++)
for (int pitch = -1; pitch <= 1; pitch++)
{
float a = yaw * MathF.PI / 4f;
var dir = new Vector3(
MathF.Cos(a), MathF.Sin(a), pitch * 0.9f);
var lookAt = eye + Vector3.Normalize(dir) * 3f;
int before = PortalVisibilityBuilder.ConvergenceTripwireCount;
PortalVisibilityBuilder.Build(cell, eye, lookup, ViewProjFor(eye, lookAt));
builds++;
int after = PortalVisibilityBuilder.ConvergenceTripwireCount;
if (after != before)
firings.Add(System.FormattableString.Invariant(
$"cell=0x{cell.CellId:X8} eye=({eye.X:F3},{eye.Y:F3},{eye.Z:F3}) yaw={yaw} pitch={pitch}"));
}
}
}
_out.WriteLine($"builds={builds} firings={firings.Count}");
Assert.True(firings.Count == 0,
"#120: convergence tripwire fired during the in-cell direction sweep:\n "
+ string.Join("\n ", firings));
}
/// <summary>
/// Diagnostic: microscope on the failing hop. Replays the two portal hops
/// (0171→0173, then 0173→0172) through the PUBLIC PortalProjection APIs for the
/// good step (47) and the glitch step (48), printing the homogeneous subject
/// vertices and full-precision outputs at every stage — the concrete numbers for
/// the collapsing intersection.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_Hop2Microscope()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
var c171 = cells[Landblock | 0x0171u];
var c173 = cells[Landblock | 0x0173u];
var player = new Vector3(159.936676f, 7.701012f, 94.000000f);
var pivot = player + new Vector3(0f, 0f, 1.5f);
// 0171's portal to 0173 is index 1; 0173's portal to 0172 is index 1 (room map).
var poly171to173 = c171.PortalPolygons[1];
var poly173to172 = c173.PortalPolygons[1];
foreach (int i in new[] { 47, 48 })
{
float ex = 158.43f - i * 0.02f;
var eye = new Vector3(ex, 7.912722f, 96.248833f);
var vp = ViewProjFor(eye, pivot);
_out.WriteLine($"########## step {i} (eyeX={ex:F2}) ##########");
// Hop 1: 0171 -> 0173 against the full screen.
var subj1 = PortalProjection.ProjectToClip(poly171to173, c171.WorldTransform, vp);
DumpClip("hop1 subject (0171->0173 portal, clip space)", subj1);
var region173 = PortalProjection.ClipToRegion(subj1, new[]
{
new Vector2(-1f, -1f), new Vector2(1f, -1f), new Vector2(1f, 1f), new Vector2(-1f, 1f),
});
DumpNdc("hop1 out = 0173 region", region173);
if (region173.Length < 3) continue;
// Hop 2: 0173 -> 0172 against the 0173 region.
var subj2 = PortalProjection.ProjectToClip(poly173to172, c173.WorldTransform, vp);
DumpClip("hop2 subject (0173->0172 portal, clip space)", subj2);
var region172 = PortalProjection.ClipToRegion(subj2, region173);
DumpNdc("hop2 out = 0172 region", region172);
}
}
/// <summary>Scratch: the homogeneous reciprocal primitive in isolation, on the
/// synthetic fixture geometry that Build_AppliesReciprocalOtherPortalClip uses.</summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Scratch_ReciprocalPrimitive_SyntheticPair()
{
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f);
var vp = view * proj;
Vector3[] Quad(float cx, float cy, float hw, float hh, float z) => new[]
{
new Vector3(cx - hw, cy - hh, z), new Vector3(cx + hw, cy - hh, z),
new Vector3(cx + hw, cy + hh, z), new Vector3(cx - hw, cy + hh, z),
};
var wide = Quad(0f, 0f, 0.9f, 0.9f, -3f);
var narrow = Quad(0f, 0f, 0.3f, 0.9f, -3f);
// Near-side region: wide portal clipped against the full screen.
var wideClip = PortalProjection.ProjectToClip(wide, Matrix4x4.Identity, vp);
DumpClip("wide subject", wideClip);
var wideRegion = PortalProjection.ClipToRegion(wideClip, new[]
{
new Vector2(-1f, -1f), new Vector2(1f, -1f), new Vector2(1f, 1f), new Vector2(-1f, 1f),
});
DumpNdc("wide region (near-side)", wideRegion);
// Reciprocal: narrow portal, homogeneous, clipped against the wide region.
var narrowClip = PortalProjection.ProjectToClip(narrow, Matrix4x4.Identity, vp);
DumpClip("narrow subject (reciprocal)", narrowClip);
var tightened = PortalProjection.ClipToRegion(narrowClip, wideRegion);
DumpNdc("tightened = narrow ∩ wide", tightened);
// Now the FULL Build on the same synthetic pair (mirrors
// Build_AppliesReciprocalOtherPortalClip) with output dumps.
var a = new LoadedCell
{
CellId = 0x0001, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity,
Portals = new List<CellPortalInfo> { new CellPortalInfo(0x0002, 0, 0, 0) },
};
a.PortalPolygons.Add(wide);
var b = new LoadedCell
{
CellId = 0x0002, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity,
Portals = new List<CellPortalInfo> { new CellPortalInfo(0x0001, 0, 0, 0) },
};
b.PortalPolygons.Add(narrow);
var all = new Dictionary<uint, LoadedCell> { [0x0001u] = a, [0x0002u] = b };
var f = PortalVisibilityBuilder.Build(
a, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, vp);
foreach (var kv in f.CellViews)
foreach (var p in kv.Value.Polygons)
{
string s = "";
foreach (var v in p.Vertices)
s += System.FormattableString.Invariant($" ({v.X:F4},{v.Y:F4})");
_out.WriteLine($" Build view 0x{kv.Key:X4} [{p.Vertices.Length}]:{s}");
}
}
private void DumpClip(string label, Vector4[] verts)
{
string s = "";
foreach (var v in verts)
s += System.FormattableString.Invariant($" ({v.X:F4},{v.Y:F4},{v.Z:F4},w={v.W:F4})");
_out.WriteLine($" {label} [{verts.Length}]:{s}");
}
private void DumpNdc(string label, Vector2[] verts)
{
string s = "";
foreach (var v in verts)
s += System.FormattableString.Invariant($" ({v.X:F7},{v.Y:F7})");
_out.WriteLine($" {label} [{verts.Length}]:{s}");
}
/// <summary>
/// Diagnostic (no assertions yet): sweep the eye along the corner-press path —
/// from inside the player's room (0172), across the doorway threshold (0173),
/// into the neighbour room (0171) — looking back at the player throughout, and
/// print the flood result per step. The defect signature: the PLAYER's room
/// (0172) absent from the flood (or its view region empty) while the root is
/// 0171/0173 — the screen area where the player stands then shows background.
/// </summary>
[Fact]
[Trait("Lane", "InstalledDat")]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_CornerPress_FloodAcrossDoorwayPlane()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadBuilding(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
// The captured corner scenario: player parked in 0172, eye orbiting near the
// doorway plane at head height (z=96.25), looking at the pivot.
var player = new Vector3(159.936676f, 7.701012f, 94.000000f);
var pivot = player + new Vector3(0f, 0f, 1.5f);
_out.WriteLine("step | eyeX (worldX | dPlane0171) | root | flood | 0171/0173/0172 polys | outside");
for (int i = 0; i <= 60; i++)
{
// Sweep world-x 158.43 (inside 0172) -> 157.23 (inside 0171), 2 cm steps,
// crossing the 0172 plane (~158.03) and the 0171 plane (~157.83).
float ex = 158.43f - i * 0.02f;
var eye = new Vector3(ex, 7.912722f, 96.248833f);
uint root = RootCellFor(ex);
var frame = PortalVisibilityBuilder.Build(
cells[root], eye, lookup, ViewProjFor(eye, pivot));
string flood = "";
foreach (uint id in frame.OrderedVisibleCells)
flood += $" {id & 0xFFFFu:X4}";
string PolyInfo(uint low)
{
uint id = Landblock | low;
if (!frame.CellViews.TryGetValue(id, out var view)) return "-";
int n = view.Polygons.Count;
float area = 0f;
foreach (var p in view.Polygons)
area += MathF.Max(0f, p.MaxX - p.MinX) * MathF.Max(0f, p.MaxY - p.MinY);
return System.FormattableString.Invariant($"{n}:{area:F3}");
}
string p71 = PolyInfo(0x0171u);
string p73 = PolyInfo(0x0173u);
string p72 = PolyInfo(0x0172u);
_out.WriteLine(System.FormattableString.Invariant(
$"{i,3} | {ex:F2} ({ex - Plane0171X,6:F3}) | {root & 0xFFFFu:X4} |{flood} | {p71} / {p73} / {p72} | out={frame.OutsideView.Polygons.Count}"));
}
}
}

View file

@ -738,7 +738,6 @@ public sealed class EnvCellAlphaDrawSourceTests
particleRenderer: null,
portalDepthMask: null,
Queue,
(WorldRenderDiagnostics)RuntimeHelpers.GetUninitializedObject(typeof(WorldRenderDiagnostics)),
(TerrainDrawDiagnosticsController)RuntimeHelpers.GetUninitializedObject(
typeof(TerrainDrawDiagnosticsController)));
Leaf = new WalkProductionLeafRenderer(

View file

@ -1,46 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public class IndoorDrawPlanTests
{
private static ViewPolygon Quad() => new(new[]
{ new Vector2(-1, -1), new Vector2(1, -1), new Vector2(1, 1), new Vector2(-1, 1) });
private static PortalVisibilityFrame FrameWith(params uint[] orderedCells)
{
var f = new PortalVisibilityFrame();
foreach (var id in orderedCells)
{
f.OrderedVisibleCells.Add(id);
var v = new CellView(); v.Add(Quad());
f.CellViews[id] = v;
}
return f;
}
[Fact]
public void ShellPass_IncludesEveryVisibleCell_NoFilter()
{
// The grey bug: a cell in OrderedVisibleCells must NEVER be dropped from the
// shell pass. (Old code dropped cells lacking a ClipFrameAssembler slot.)
var f = FrameWith(0x01, 0x02, 0x03);
var plan = IndoorDrawPlan.ShellPass(f);
Assert.Equal(new uint[] { 0x03, 0x02, 0x01 }, plan.Select(e => e.CellId).ToArray()); // reverse = far→near
Assert.All(plan, e => Assert.NotEmpty(e.Slices));
}
[Fact]
public void ShellPass_ExcludesEmptyViewCells()
{
var f = FrameWith(0x01);
f.OrderedVisibleCells.Add(0x02); // present in the list…
f.CellViews[0x02] = new CellView(); // …but empty view → not drawable
var plan = IndoorDrawPlan.ShellPass(f);
Assert.Equal(new uint[] { 0x01 }, plan.Select(e => e.CellId).ToArray());
}
}

View file

@ -0,0 +1,17 @@
namespace AcDream.App.Tests.Rendering;
internal static class InstalledDatTestPath
{
internal static string? Resolve()
{
string? configured = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(configured) && Directory.Exists(configured))
return configured;
string defaultPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents",
"Asheron's Call");
return Directory.Exists(defaultPath) ? defaultPath : null;
}
}

View file

@ -1,243 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #113 phantom exterior staircase (2026-06-10): the Holtburg meeting hall
/// (AAB3 building[0], model 0x010014C3 at AAB3-local (36,84,116)) shows its
/// INTERIOR stair cells painted across the west exterior wall when viewed from
/// outside. Attribution: retail clips drawn cell geometry to the accumulated
/// portal aperture (Render::set_view portal scissor + polyClipFinish with
/// planeMask=0xffffffff, decomp :343750/:427922); our DrawEnvCellShells routes
/// per-cell clip planes via ClipFrameAssembler — but a slice whose aperture
/// polygon exceeds the 8-plane budget falls back to slot 0 ("the renderer uses
/// scissor for passes that need that fallback", ClipFrameAssembler.cs:13-15)
/// and the shell pass never implemented that scissor → the cell draws fully
/// unclipped. This harness replays the production outdoor per-building flood
/// (PortalVisibilityBuilder.ConstructViewBuilding, the R-A2 path) + the
/// production assembler from the user's click-time viewpoint and pins which
/// hall cells draw and with what clip.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue113MeetingHallFloodTests
{
private readonly ITestOutputHelper _out;
public Issue113MeetingHallFloodTests(ITestOutputHelper output) => _out = output;
private const uint Landblock = 0xAAB30000u;
private const uint EnvironmentFilePrefix = 0x0D000000u;
private static string? ResolveDatDir()
{
var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv))
return fromEnv;
var def = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(def) ? def : null;
}
// Mirrors CornerFloodReplayTests.LoadCell (EnvCellLandblockBuildBuilder shape).
private static LoadedCell LoadCell(DatCollection dats, uint cellId)
{
var envCell = dats.Get<DatEnvCell>(cellId)
?? throw new InvalidOperationException($"EnvCell 0x{cellId:X8} not found");
var environment = dats.Get<DatEnvironment>(EnvironmentFilePrefix | envCell.EnvironmentId)
?? throw new InvalidOperationException($"Environment 0x{envCell.EnvironmentId:X8} not found");
if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct) || cellStruct is null)
throw new InvalidOperationException($"CellStruct {envCell.CellStructure} missing");
var cellTransform =
Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) *
Matrix4x4.CreateTranslation(envCell.Position.Origin);
Matrix4x4.Invert(cellTransform, out var inverse);
var boundsMin = new Vector3(float.MaxValue);
var boundsMax = new Vector3(float.MinValue);
foreach (var kvp in cellStruct.VertexArray.Vertices)
{
var v = kvp.Value;
var pos = new Vector3(v.Origin.X, v.Origin.Y, v.Origin.Z);
boundsMin = Vector3.Min(boundsMin, pos);
boundsMax = Vector3.Max(boundsMax, pos);
}
if (boundsMin.X == float.MaxValue) { boundsMin = Vector3.Zero; boundsMax = Vector3.Zero; }
var portals = new List<CellPortalInfo>();
var clipPlanes = new List<PortalClipPlane>();
var portalPolygons = new List<Vector3[]>();
foreach (var portal in envCell.CellPortals)
{
portals.Add(new CellPortalInfo(
portal.OtherCellId, portal.PolygonId, (ushort)portal.Flags, portal.OtherPortalId));
if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var poly)
&& poly.VertexIds.Count >= 3
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[0], out var v0)
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[1], out var v1)
&& cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[2], out var v2))
{
var p0 = new Vector3(v0.Origin.X, v0.Origin.Y, v0.Origin.Z);
var p1 = new Vector3(v1.Origin.X, v1.Origin.Y, v1.Origin.Z);
var p2 = new Vector3(v2.Origin.X, v2.Origin.Y, v2.Origin.Z);
var normal = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0));
float d = -Vector3.Dot(normal, p0);
// InsideSide from the dat PortalSide bit — mirrors the production fix
// (EnvCellLandblockBuildBuilder): retail InitCell (0x005a4b70) + physics
// CellTransit use the dat bit; the old AABB-centroid guess mis-sided thin
// connectors (#186). Kept identical here so this replay stays a faithful mirror.
clipPlanes.Add(new PortalClipPlane
{
Normal = normal, D = d, InsideSide = ((ushort)portal.Flags & 0x2) == 0 ? 1 : 0,
});
}
else
{
clipPlanes.Add(default);
}
Vector3[] polyVerts = Array.Empty<Vector3>();
if (cellStruct.Polygons.TryGetValue(portal.PolygonId, out var portalPoly)
&& portalPoly.VertexIds.Count >= 3)
{
polyVerts = new Vector3[portalPoly.VertexIds.Count];
bool allResolved = true;
for (int vi = 0; vi < portalPoly.VertexIds.Count; vi++)
{
if (cellStruct.VertexArray.Vertices.TryGetValue(
(ushort)portalPoly.VertexIds[vi], out var pv))
polyVerts[vi] = new Vector3(pv.Origin.X, pv.Origin.Y, pv.Origin.Z);
else { allResolved = false; break; }
}
if (!allResolved) polyVerts = Array.Empty<Vector3>();
}
portalPolygons.Add(polyVerts);
}
uint lbPrefix = cellId & 0xFFFF0000u;
var visibleCells = new List<uint>();
if (envCell.VisibleCells is not null)
foreach (var lowId in envCell.VisibleCells)
visibleCells.Add(lbPrefix | lowId);
return new LoadedCell
{
CellId = cellId,
WorldPosition = envCell.Position.Origin,
WorldTransform = cellTransform,
InverseWorldTransform = inverse,
LocalBoundsMin = boundsMin,
LocalBoundsMax = boundsMax,
Portals = portals,
ClipPlanes = clipPlanes,
PortalPolygons = portalPolygons,
VisibleCells = visibleCells,
SeenOutside = envCell.Flags.HasFlag(DatReaderWriter.Enums.EnvCellFlags.SeenOutside),
};
}
private static Dictionary<uint, LoadedCell> LoadHall(DatCollection dats)
{
var cells = new Dictionary<uint, LoadedCell>();
for (uint low = 0x0100u; low <= 0x0106u; low++)
{
uint id = Landblock | low;
cells[id] = LoadCell(dats, id);
}
return cells;
}
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
/// <summary>
/// Replay the production outdoor flood + assembler from the user's
/// click-time viewpoint (player AAB3-local (24.4, 83.4, 116), chase eye
/// behind at eye height, looking at the hall's west door). Prints every
/// hall cell that enters the drawn set with its slice slot / plane count /
/// NDC AABB. The #113 defect signature: a hall cell drawn with a slice
/// that has Slot==0 AND zero planes (the assembler's scissor-fallback
/// contract) — DrawEnvCellShells draws that slice fully unclipped, so the
/// interior staircase paints across the exterior wall.
/// </summary>
[Fact]
public void WestApproach_HallCellSlices_HaveUsableClip()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadHall(dats);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
// Chase-camera sweep along the west approach: player walked from the
// cottage (west) toward the hall; eye behind the player at z≈117.6,
// looking at the hall's west door (local (29.4, 84.0, 117.3)).
var lookAt = new Vector3(29.4f, 84.0f, 117.3f);
int unclippedDrawn = 0;
var report = new List<string>();
for (int i = 0; i <= 20; i++)
{
float ex = 14f + i * 0.5f; // eye local x 14 → 24
var eye = new Vector3(ex, 83.4f, 117.6f);
var vp = ViewProjFor(eye, lookAt);
var frame = PortalVisibilityBuilder.ConstructViewBuilding(
cells.Values.ToList(), eye, lookup, vp, maxSeedDistance: 48f);
var clipFrame = ClipFrame.NoClip();
var assembly = ClipFrameAssembler.Assemble(clipFrame, frame);
string flood = string.Join(" ", frame.OrderedVisibleCells.Select(c => $"{c & 0xFFFFu:X3}"));
var lineParts = new List<string>();
foreach (uint cellId in frame.OrderedVisibleCells)
{
if (!assembly.CellIdToViewSlices.TryGetValue(cellId, out var slices))
{
// No slices at all → DrawEnvCellShells falls to NoClipSlice → UNCLIPPED.
lineParts.Add($"0x{cellId & 0xFFFFu:X3}:NO-SLICES(unclipped)");
unclippedDrawn++;
continue;
}
foreach (var s in slices)
{
bool unclipped = s.Slot == 0 && s.Planes.Length == 0;
if (unclipped) unclippedDrawn++;
string tag = unclipped ? "(UNCLIPPED)" : "";
lineParts.Add(FormattableString.Invariant(
$"0x{cellId & 0xFFFFu:X3}:slot{s.Slot}/p{s.Planes.Length}[{s.NdcAabb.X:F2},{s.NdcAabb.Y:F2}..{s.NdcAabb.Z:F2},{s.NdcAabb.W:F2}]{tag}"));
}
}
report.Add(FormattableString.Invariant(
$"eyeX={ex,5:F1} flood=[{flood}] {string.Join(" ", lineParts)}"));
}
foreach (var line in report) _out.WriteLine(line);
_out.WriteLine($"unclipped-drawn slices across sweep: {unclippedDrawn}");
// Documents the defect when it fires; flips to 0 once the shell pass
// implements the assembler's scissor-fallback contract (or exact clip).
Assert.True(unclippedDrawn == 0,
$"#113: {unclippedDrawn} hall-cell slices would draw fully unclipped " +
"(slot 0, no planes, no scissor) — interior stair geometry paints " +
"across the exterior wall (the phantom staircase).");
}
}

View file

@ -1,196 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #120 reproduction (2026-06-11): the armed tripwire self-attributed on the
/// re-gate launch (regate-118-119-launch.log) — a pure TWO-CELL reciprocal
/// ping-pong, 64 laps each: `chain root=0xA9B4015C eye=(109.995,37.158,96.249)
/// cells: 0xA9B40162x64 0xA9B4015Cx64` and `root=0xA9B3010F
/// eye=(175.771,-107.310,118.814) cells: 0xA9B30103x64 0xA9B3010Fx64`.
///
/// Mechanism: with the eye within PortalSideEpsilon (±1 cm — the T2-refuted-
/// to-tighten root-lag tolerance; retail's is 0.0002) of the portal plane,
/// the in-plane case counts as interior for BOTH cells, so views flow
/// A→B→A…; each lap re-clips through two near-edge-on apertures whose
/// intersection numerics wobble by more than CellView's 1e-3 dedup grid →
/// every lap keys as a NEW polygon → 128-deep in-place recursion. The prior
/// convergence sweeps (CornerFloodReplayTests) could not reproduce because
/// they only load the corner building 0x016F-0x0175 — both firing pairs are
/// OUTSIDE that set.
///
/// The fix class is the handoff's own prediction ("dedup admitting
/// near-duplicates per lap"): a round-trip re-emission is, in exact math, a
/// SUBSET of the region that originated it — CellView.Add must reject
/// contained polygons, which makes union growth strictly area-increasing and
/// the flood convergent regardless of clip-numerics drift.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue120ReciprocalPingPongTests
{
private readonly ITestOutputHelper _out;
public Issue120ReciprocalPingPongTests(ITestOutputHelper output) => _out = output;
internal static Dictionary<uint, LoadedCell> LoadAllInteriorCells(DatCollection dats, uint landblock)
{
var lbi = dats.Get<DatLandBlockInfo>(landblock | 0xFFFEu);
Assert.NotNull(lbi);
var cells = new Dictionary<uint, LoadedCell>();
for (uint low = 0x0100u; low < 0x0100u + lbi!.NumCells; low++)
{
uint id = landblock | low;
try { cells[id] = CornerFloodReplayTests.LoadCell(dats, id); }
catch (InvalidOperationException) { /* sparse cell ids — skip */ }
}
return cells;
}
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
public static readonly TheoryData<uint, uint, uint, float, float, float> CapturedSites = new()
{
// landblock, cellA (root at firing), cellB, eye x, y, z — straight from the chain dumps
{ 0xA9B40000u, 0x015Cu, 0x0162u, 109.995f, 37.158f, 96.249f },
{ 0xA9B30000u, 0x010Fu, 0x0103u, 175.771f, -107.310f, 118.814f },
};
/// <summary>
/// The captured firing sites, swept over orientations (the dump has no view
/// matrix) and over both cells as root. Invariants: the tripwire stays 0
/// AND no CellView accumulates a pathological polygon pile (the duplicate
/// build-up is the defect even below the depth-128 failsafe).
/// </summary>
[Theory]
[MemberData(nameof(CapturedSites))]
public void CapturedPingPongSites_Converge(uint landblock, uint lowA, uint lowB, float ex, float ey, float ez)
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadAllInteriorCells(dats, landblock);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
Assert.True(cells.ContainsKey(landblock | lowA), $"cell 0x{landblock | lowA:X8} not loaded");
Assert.True(cells.ContainsKey(landblock | lowB), $"cell 0x{landblock | lowB:X8} not loaded");
var eye = new Vector3(ex, ey, ez);
var roots = new[] { cells[landblock | lowA], cells[landblock | lowB] };
PortalVisibilityBuilder.ConvergenceTripwireCount = 0;
var failures = new List<string>();
int builds = 0, maxPolys = 0;
foreach (var root in roots)
{
for (int yaw = 0; yaw < 8; yaw++)
{
float a = yaw * MathF.PI / 4f;
foreach (float pitch in new[] { -0.4f, 0f, 0.4f })
{
var dir = Vector3.Normalize(new Vector3(MathF.Cos(a), MathF.Sin(a), pitch));
int before = PortalVisibilityBuilder.ConvergenceTripwireCount;
var frame = PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, eye + dir * 3f));
builds++;
int after = PortalVisibilityBuilder.ConvergenceTripwireCount;
int polys = frame.CellViews.Count == 0 ? 0 : frame.CellViews.Values.Max(v => v.Polygons.Count);
if (polys > maxPolys) maxPolys = polys;
if (after != before || polys > 32)
failures.Add(FormattableString.Invariant(
$"root=0x{root.CellId:X8} yaw={yaw} pitch={pitch} tripwire={after - before} maxCellPolys={polys}"));
}
}
}
_out.WriteLine($"builds={builds} maxCellPolys={maxPolys} failures={failures.Count}");
foreach (var f in failures) _out.WriteLine($" {f}");
Assert.True(failures.Count == 0,
$"{failures.Count}/{builds} builds broke convergence at the captured #120 site (see output)");
}
/// <summary>
/// The geometric trigger, driven directly: eye swept through the
/// both-sides-pass window (±PortalSideEpsilon = ±1 cm) of the reciprocal
/// portal plane between the two captured cells, looking across and along
/// the aperture. Same convergence invariants.
/// </summary>
[Theory]
[MemberData(nameof(CapturedSites))]
public void PortalPlaneWindow_BothSidesPass_Converges(uint landblock, uint lowA, uint lowB, float ex, float ey, float ez)
{
_ = ex; _ = ey; _ = ez; // geometric variant derives its own eyes
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = LoadAllInteriorCells(dats, landblock);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var cellA = cells[landblock | lowA];
uint cellBId = landblock | lowB;
// the portal from A to B (the ping-pong pair)
int portalIdx = -1;
for (int i = 0; i < cellA.Portals.Count; i++)
if (cellA.Portals[i].OtherCellId == (ushort)lowB) { portalIdx = i; break; }
Assert.True(portalIdx >= 0, $"no portal 0x{lowA:X4}->0x{lowB:X4}");
var poly = cellA.PortalPolygons[portalIdx];
Assert.True(poly is { Length: >= 3 }, "portal polygon degenerate");
var centroidLocal = Vector3.Zero;
foreach (var v in poly!) centroidLocal += v;
centroidLocal /= poly.Length;
var centroidWorld = Vector3.Transform(centroidLocal, cellA.WorldTransform);
var plane = cellA.ClipPlanes[portalIdx];
var normalWorld = Vector3.Normalize(Vector3.TransformNormal(plane.Normal, cellA.WorldTransform));
PortalVisibilityBuilder.ConvergenceTripwireCount = 0;
var failures = new List<string>();
int builds = 0, maxPolys = 0;
foreach (float off in new[] { -0.009f, -0.004f, 0f, 0.004f, 0.009f })
{
var eye = centroidWorld + normalWorld * off;
foreach (var root in new[] { cellA, cells[cellBId] })
{
for (int yaw = 0; yaw < 8; yaw++)
{
float a = yaw * MathF.PI / 4f;
var dir = Vector3.Normalize(new Vector3(MathF.Cos(a), MathF.Sin(a), 0.1f));
int before = PortalVisibilityBuilder.ConvergenceTripwireCount;
var frame = PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, eye + dir * 3f));
builds++;
int after = PortalVisibilityBuilder.ConvergenceTripwireCount;
int polys = frame.CellViews.Count == 0 ? 0 : frame.CellViews.Values.Max(v => v.Polygons.Count);
if (polys > maxPolys) maxPolys = polys;
if (after != before || polys > 32)
failures.Add(FormattableString.Invariant(
$"off={off:F3} root=0x{root.CellId:X8} yaw={yaw} tripwire={after - before} maxCellPolys={polys}"));
}
}
}
_out.WriteLine($"builds={builds} maxCellPolys={maxPolys} failures={failures.Count}");
foreach (var f in failures) _out.WriteLine($" {f}");
Assert.True(failures.Count == 0,
$"{failures.Count}/{builds} builds broke convergence in the ±ε portal-plane window (see output)");
}
}

View file

@ -1,154 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #124 — far-building interiors under an INTERIOR root. Retail seeds the
/// look-in flood by clipping a building's aperture against the CURRENTLY
/// INSTALLED view (PView::GetClip 0x005a4320 inside ConstructView(CBldPortal)
/// 0x005a59a0): full screen outdoors, the accumulated doorway (outside) view
/// when looked into from inside. These tests pin BuildFromExterior's
/// seedRegion parameter — the port of that installed-view clip — against the
/// real Holtburg corner-building door.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue124LookInSeedRegionTests
{
private readonly ITestOutputHelper _out;
public Issue124LookInSeedRegionTests(ITestOutputHelper output) => _out = output;
private const uint ExitCellId = CornerFloodReplayTests.Landblock | 0x0170u;
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 1f, 5000f);
return view * proj;
}
private static (Dictionary<uint, LoadedCell> cells, LoadedCell exitCell, int exitIdx,
Vector3 centroid, Vector3 outward) LoadFixture(DatCollection dats)
{
var cells = CornerFloodReplayTests.LoadBuilding(dats);
var exitCell = cells[ExitCellId];
int exitIdx = -1;
for (int i = 0; i < exitCell.Portals.Count; i++)
{
if (exitCell.Portals[i].OtherCellId == 0xFFFF && i < exitCell.PortalPolygons.Count
&& exitCell.PortalPolygons[i].Length >= 3)
{ exitIdx = i; break; }
}
Assert.True(exitIdx >= 0);
var localPoly = exitCell.PortalPolygons[exitIdx];
Vector3 centroid = Vector3.Zero;
foreach (var lp in localPoly)
centroid += Vector3.Transform(lp, exitCell.WorldTransform);
centroid /= localPoly.Length;
var plane = exitCell.ClipPlanes[exitIdx];
var normal = Vector3.TransformNormal(plane.Normal, exitCell.WorldTransform);
var cellCenter = Vector3.Transform(
(exitCell.LocalBoundsMin + exitCell.LocalBoundsMax) * 0.5f, exitCell.WorldTransform);
// outward = away from the cell interior.
if (Vector3.Dot(normal, cellCenter - centroid) > 0)
normal = -normal;
return (cells, exitCell, exitIdx, centroid, Vector3.Normalize(normal));
}
private static Vector4 ApertureNdcAabb(LoadedCell cell, int idx, Matrix4x4 viewProj)
{
float minX = float.MaxValue, minY = float.MaxValue, maxX = float.MinValue, maxY = float.MinValue;
foreach (var lp in cell.PortalPolygons[idx])
{
var w = Vector3.Transform(lp, cell.WorldTransform);
var c = Vector4.Transform(new Vector4(w, 1f), viewProj);
Assert.True(c.W > 0.05f, "fixture eye must keep the aperture fully in front");
minX = MathF.Min(minX, c.X / c.W); maxX = MathF.Max(maxX, c.X / c.W);
minY = MathF.Min(minY, c.Y / c.W); maxY = MathF.Max(maxY, c.Y / c.W);
}
return new Vector4(minX, minY, maxX, maxY);
}
private static ViewPolygon Quad(float minX, float minY, float maxX, float maxY) =>
new(new[]
{
new Vector2(minX, minY), new Vector2(maxX, minY),
new Vector2(maxX, maxY), new Vector2(minX, maxY),
});
[Fact]
public void SeedRegion_ContainingAperture_Floods_DisjointRegion_DoesNot()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var (cells, exitCell, exitIdx, centroid, outward) = LoadFixture(dats);
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out var c) ? c : null;
// Eye OUTSIDE the building, 3 m in front of the exit door, gaze at it
// — the look-in geometry of a viewer peering at this building through
// some other opening.
var eye = centroid + outward * 3f;
var viewProj = ViewProjFor(eye, centroid);
var ap = ApertureNdcAabb(exitCell, exitIdx, viewProj);
_out.WriteLine(FormattableString.Invariant(
$"aperture ndc=({ap.X:F3},{ap.Y:F3},{ap.Z:F3},{ap.W:F3})"));
// Sanity: the full-screen (outdoor-root) seed floods.
var full = PortalVisibilityBuilder.BuildFromExterior(
cells.Values, eye, Lookup, viewProj);
Assert.True(full.OrderedVisibleCells.Count > 0, "full-screen seed must flood");
// A region containing the aperture floods — and never MORE than the
// full-screen seed (region-restricting can only shrink the flood).
var containing = new[] { Quad(ap.X - 0.05f, ap.Y - 0.05f, ap.Z + 0.05f, ap.W + 0.05f) };
var seeded = PortalVisibilityBuilder.BuildFromExterior(
cells.Values, eye, Lookup, viewProj, float.PositiveInfinity, containing);
Assert.True(seeded.OrderedVisibleCells.Count > 0, "containing region must flood");
Assert.True(seeded.OrderedVisibleCells.Count <= full.OrderedVisibleCells.Count);
// A region strictly disjoint from the aperture must not flood — the
// doorway doesn't show this building, so its interior never builds
// (retail: GetClip vs the installed view returns empty → no look-in).
Assert.True(ap.Z < 0.70f || ap.X > -0.70f, "fixture aperture unexpectedly fills the screen");
var disjoint = ap.Z < 0.70f
? new[] { Quad(0.75f, 0.75f, 0.99f, 0.99f) }
: new[] { Quad(-0.99f, -0.99f, -0.75f, -0.75f) };
var none = PortalVisibilityBuilder.BuildFromExterior(
cells.Values, eye, Lookup, viewProj, float.PositiveInfinity, disjoint);
Assert.True(none.OrderedVisibleCells.Count == 0,
FormattableString.Invariant($"disjoint region flooded {none.OrderedVisibleCells.Count} cells"));
}
[Fact]
public void EyeOnInteriorSide_ExitDoorNeverSeeds()
{
// The root's own doorway must not look-in on itself: the seed eye-side
// test (retail ConstructView's sidedness vs portal_side) excludes any
// aperture the eye is on the interior side of — this is what lets the
// interior-root gather pass ALL nearby buildings including the
// viewer's own without special-casing.
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var (cells, exitCell, _, centroid, outward) = LoadFixture(dats);
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out var c) ? c : null;
var eye = centroid - outward * 2f; // 2 m INSIDE the doorway
var viewProj = ViewProjFor(eye, centroid);
var frame = PortalVisibilityBuilder.BuildFromExterior(
new[] { exitCell }, eye, Lookup, viewProj);
Assert.True(frame.OrderedVisibleCells.Count == 0,
"an interior-side eye must not seed its own cell's exit portal");
}
}

View file

@ -1,351 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #127 — per-building flood admissions are BISTABLE per frame under the
/// outdoor root (the building-flap mechanism: tower roof/edges flap, #123
/// buildings vanishing when running past). Captured live with the [viewer]
/// fwd= probe (flap-fwd-capture2.log): a ~4 cm eye wobble flips the merged
/// flood 24↔25 with a byte-identical gaze:
/// flood=25 eye=(129.051,-15.636,99.512) fwd=(-0.7565,0.5877,-0.2869)
/// flood=24 eye=(129.009,-15.610,99.503) fwd=(same)
/// flood=25 eye=(129.051,-15.642,99.514) fwd=(same)
/// (player parked at pCell=0xA9B40019, Holtburg town, A9B4-anchored world
/// frame — A9B4-local == world; A9B3 cells get a (0,-192,0) block offset.)
///
/// This harness replays the captured (eye, fwd) pairs through the
/// production outdoor pipeline — Build(outdoorNode) + per-building
/// ConstructViewBuilding merged (RetailPViewRenderer.MergeNearbyBuildingFloods
/// shape) — and DIFFS the admitted cell sets: the flipping cell names the
/// bistable admission, and the bisect along the eye segment names the knife
/// edge.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue127FloodFlipReplayTests
{
private readonly ITestOutputHelper _out;
public Issue127FloodFlipReplayTests(ITestOutputHelper output) => _out = output;
// The captured flip pair (A9B4-anchored world frame).
private static readonly Vector3 EyeA = new(129.051f, -15.636f, 99.512f);
private static readonly Vector3 EyeB = new(129.009f, -15.610f, 99.503f);
private static readonly Vector3 Fwd = Vector3.Normalize(new Vector3(-0.7565f, 0.5877f, -0.2869f));
private sealed record World(
Dictionary<uint, LoadedCell> Cells,
List<List<LoadedCell>> BuildingGroups,
Func<uint, LoadedCell?> Lookup);
private static void LoadLandblockBuildings(
DatCollection dats, uint landblock, Vector3 offset,
Dictionary<uint, LoadedCell> cells, List<List<LoadedCell>> groups)
{
var lbi = dats.Get<DatLandBlockInfo>(landblock | 0xFFFEu);
if (lbi is null) return;
// all interior cells (the lookup needs every portal-reachable cell)
for (uint low = 0x0100u; low < 0x0100u + lbi.NumCells; low++)
{
uint id = landblock | low;
try { cells[id] = CornerFloodReplayTests.LoadCell(dats, id, offset); }
catch (InvalidOperationException) { }
}
// per-building groups from the bldPortal stab lists (the dat's own
// building→cells mapping; mirrors BuildingLoader's seed+walk result)
if (lbi.Buildings is null) return;
foreach (var bld in lbi.Buildings)
{
var set = new HashSet<uint>();
if (bld.Portals is not null)
foreach (var bp in bld.Portals)
if (bp.StabList is not null)
foreach (ushort low in bp.StabList)
set.Add(landblock | low);
var group = new List<LoadedCell>();
foreach (uint id in set)
if (cells.TryGetValue(id, out var c))
group.Add(c);
if (group.Count > 0)
groups.Add(group);
}
}
private World? LoadWorld()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); return null; }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = new Dictionary<uint, LoadedCell>();
var groups = new List<List<LoadedCell>>();
// A9B4 = the anchor (world == local); the live gather spans the
// streaming near window, so load a 5x5 block neighbourhood with
// block offsets relative to the anchor.
for (int dx = -2; dx <= 2; dx++)
for (int dy = -2; dy <= 2; dy++)
{
uint lbX = (uint)(0xA9 + dx);
uint lbY = (uint)(0xB4 + dy);
uint lb = (lbX << 24) | (lbY << 16);
LoadLandblockBuildings(dats, lb, new Vector3(dx * 192f, dy * 192f, 0f), cells, groups);
}
_out.WriteLine($"loaded {cells.Count} cells in {groups.Count} building groups");
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
return new World(cells, groups, lookup);
}
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 fwd, float fovY)
{
var view = Matrix4x4.CreateLookAt(eye, eye + fwd * 3f, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(fovY, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
// PortalBounds per group: the union AABB of the group's exit-portal
// polygons in world space — exactly BuildingLoader's construction
// (BuildingLoader.cs:114-144).
private static (bool Has, Vector3 Min, Vector3 Max) PortalBoundsFor(List<LoadedCell> group)
{
bool has = false;
var min = new Vector3(float.MaxValue);
var max = new Vector3(float.MinValue);
foreach (var cell in group)
{
int n = Math.Min(cell.Portals.Count, cell.PortalPolygons.Count);
for (int i = 0; i < n; i++)
{
if (cell.Portals[i].OtherCellId != 0xFFFF) continue;
var poly = cell.PortalPolygons[i];
if (poly is null) continue;
foreach (var v in poly)
{
var world = Vector3.Transform(v, cell.WorldTransform);
has = true;
min = Vector3.Min(min, world);
max = Vector3.Max(max, world);
}
}
}
return (has, min, max);
}
// The production outdoor pipeline: full-screen outdoor root + per-building
// exterior floods merged as a union (RetailPViewRenderer.DrawInside +
// MergeNearbyBuildingFloods + MergeBuildingFrame), INCLUDING the gather's
// per-building frustum pre-gate on PortalBounds (GameWindow:7542-7545) —
// controlled by <paramref name="withPreGate"/> so the gate itself can be
// implicated or exonerated.
private static HashSet<uint> AdmittedCells(World w, Vector3 eye, Vector3 fwd, float fovY, bool withPreGate)
{
var viewProj = ViewProjFor(eye, fwd, fovY);
var frustum = FrustumPlanes.FromViewProjection(viewProj);
var outdoor = OutdoorCellNode.Build(0xA9B40019u);
var pv = PortalVisibilityBuilder.Build(outdoor, eye, w.Lookup, viewProj);
foreach (var group in w.BuildingGroups)
{
if (withPreGate)
{
var (has, min, max) = PortalBoundsFor(group);
if (has && !FrustumCuller.IsAabbVisible(frustum, min, max))
continue;
}
var bf = PortalVisibilityBuilder.ConstructViewBuilding(group, eye, w.Lookup, viewProj);
foreach (uint cellId in bf.OrderedVisibleCells)
{
if (!bf.CellViews.TryGetValue(cellId, out var srcView)) continue;
if (pv.CellViews.TryGetValue(cellId, out var existing))
{
foreach (var p in srcView.Polygons) existing.Add(p);
continue;
}
pv.CellViews[cellId] = srcView;
pv.OrderedVisibleCells.Add(cellId);
}
}
return new HashSet<uint>(pv.OrderedVisibleCells);
}
/// <summary>
/// Reproduce + name: the captured 4 cm pair must admit the SAME cell set
/// (a stable world does not flap a building interior for a 4 cm eye
/// wobble at identical gaze). The diff names the bistable cell(s); the
/// bisect prints the knife-edge location and transition count.
/// </summary>
[Fact]
public void CapturedFlipPair_AdmissionIsStable()
{
var w = LoadWorld();
if (w is null) return;
foreach (bool preGate in new[] { false, true })
foreach (float fov in new[] { MathF.PI / 3f, 1.2f, MathF.PI / 2f, 0.9f })
{
var a = AdmittedCells(w, EyeA, Fwd, fov, preGate);
var b = AdmittedCells(w, EyeB, Fwd, fov, preGate);
var onlyA = a.Except(b).OrderBy(x => x).ToList();
var onlyB = b.Except(a).OrderBy(x => x).ToList();
_out.WriteLine(FormattableString.Invariant(
$"preGate={preGate} fov={fov:F2}: |A|={a.Count} |B|={b.Count} onlyA=[{string.Join(",", onlyA.Select(x => $"0x{x:X8}"))}] onlyB=[{string.Join(",", onlyB.Select(x => $"0x{x:X8}"))}]"));
if (onlyA.Count == 0 && onlyB.Count == 0) continue;
// bisect the segment: count admission transitions per flipping cell
foreach (uint cell in onlyA.Concat(onlyB))
{
int transitions = 0;
bool? prev = null;
for (int i = 0; i <= 40; i++)
{
var eye = Vector3.Lerp(EyeA, EyeB, i / 40f);
bool admitted = AdmittedCells(w, eye, Fwd, fov, preGate).Contains(cell);
if (prev.HasValue && admitted != prev.Value) transitions++;
prev = admitted;
}
_out.WriteLine(FormattableString.Invariant(
$" cell 0x{cell:X8}: {transitions} admission transition(s) along the 4 cm segment"));
}
Assert.Fail($"flood admission differs across the captured 4 cm pair (preGate={preGate}, fov={fov:F2}) — see output for the flipping cells");
}
}
// Centre of a building group's exit-portal AABB (world space).
private static (bool Has, Vector3 Center) PortalCenterFor(List<LoadedCell> group)
{
var (has, min, max) = PortalBoundsFor(group);
return (has, (min + max) * 0.5f);
}
// Per-building admitted cells (this group only) at one (eye, gaze) — the
// production per-building flood + optional PortalBounds frustum pre-gate.
private static HashSet<uint> BuildingAdmits(
World w, List<LoadedCell> group, Vector3 eye, Matrix4x4 viewProj,
FrustumPlanes frustum, bool withPreGate)
{
var result = new HashSet<uint>();
if (withPreGate)
{
var (has, min, max) = PortalBoundsFor(group);
if (has && !FrustumCuller.IsAabbVisible(frustum, min, max))
return result;
}
var bf = PortalVisibilityBuilder.ConstructViewBuilding(group, eye, w.Lookup, viewProj);
foreach (uint id in bf.OrderedVisibleCells)
result.Add(id);
return result;
}
/// <summary>
/// #127 distant-building churn detector. The captured 4 cm pair is now
/// stable (the near-eye W=0 clip port), but the user symptom is buildings
/// flickering when RUNNING PAST at a distance. This strafes the eye past
/// each loaded building at several distances in 1 mm steps with the gaze
/// fixed forward (the run-past geometry) and counts, per building cell, how
/// many times its admission toggles over the monotone strafe. A stable
/// flood toggles a cell AT MOST ONCE along a monotone eye path (it enters
/// or leaves the view a single time); >=2 toggles is churn — the building
/// flickers. preGate off vs on separates flood-math churn from the
/// PortalBounds frustum pre-gate.
///
/// RESULT (2026-06-12, HEAD post-W=0-clip-port + #120 containment): ZERO
/// churning cases across all 21 building groups x {10,30,60,120,190} m x
/// 100 mm-steps, both preGate states. The near-eye knife-edge class the
/// W=0 polyClipFinish port (987313a) killed was the distant-building
/// flicker too; the user re-gate ("Seems to have been fixed") agrees.
/// Now the REGRESSION PIN — it asserts zero churn.
/// </summary>
[Fact]
public void DistantBuildingStrafe_NoAdmissionChurn()
{
var w = LoadWorld();
if (w is null) return;
const float fovY = MathF.PI / 3f;
const float eyeHeight = 1.8f;
const float strafeSpanM = 0.10f; // 10 cm strafe
const int strafeSteps = 100; // 1 mm/step
var distances = new[] { 10f, 30f, 60f, 120f, 190f };
int totalChurn = 0;
foreach (bool preGate in new[] { false, true })
{
int worstToggles = 0;
string worstDesc = "(none)";
int churningCases = 0;
for (int gi = 0; gi < w.BuildingGroups.Count; gi++)
{
var group = w.BuildingGroups[gi];
var (has, center) = PortalCenterFor(group);
if (!has) continue;
foreach (float dist in distances)
{
// Eye south of the building at eye height, gaze NORTH toward
// the building centre; strafe along world +X (run-past).
var gaze = Vector3.Normalize(new Vector3(0f, 1f, -0.05f));
var strafeDir = Vector3.Normalize(Vector3.Cross(Vector3.UnitZ, gaze)); // ~world +X
var eyeBase = new Vector3(center.X, center.Y - dist, center.Z + eyeHeight)
- strafeDir * (strafeSpanM * 0.5f);
var toggleCount = new Dictionary<uint, int>();
var prevIn = new Dictionary<uint, bool>();
for (int s = 0; s <= strafeSteps; s++)
{
var eye = eyeBase + strafeDir * (strafeSpanM * s / strafeSteps);
var vp = ViewProjFor(eye, gaze, fovY);
var frustum = FrustumPlanes.FromViewProjection(vp);
var admits = BuildingAdmits(w, group, eye, vp, frustum, preGate);
var seen = new HashSet<uint>(admits);
foreach (uint id in seen)
{
bool wasIn = prevIn.TryGetValue(id, out var p) && p;
if (!wasIn && prevIn.ContainsKey(id))
toggleCount[id] = toggleCount.GetValueOrDefault(id) + 1;
prevIn[id] = true;
}
foreach (var id in new List<uint>(prevIn.Keys))
if (!seen.Contains(id))
{
if (prevIn[id])
toggleCount[id] = toggleCount.GetValueOrDefault(id) + 1;
prevIn[id] = false;
}
}
foreach (var (id, toggles) in toggleCount)
{
if (toggles < 2) continue; // <=1 = clean enter/leave
churningCases++;
if (toggles > worstToggles)
{
worstToggles = toggles;
worstDesc = FormattableString.Invariant(
$"group#{gi} dist={dist:F0}m cell=0x{id:X8} toggles={toggles}");
}
}
}
}
_out.WriteLine(FormattableString.Invariant(
$"preGate={preGate}: churningCases={churningCases} worst={worstDesc} (worstToggles={worstToggles})"));
totalChurn += churningCases;
}
Assert.True(totalChurn == 0,
$"{totalChurn} distant-building admission churn case(s) — a building's cells toggle >=2x " +
"over a monotone run-past strafe (the #127 flicker); see output for the worst building/distance/cell");
}
}

View file

@ -1,113 +0,0 @@
using System;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatSetup = DatReaderWriter.DBObjs.Setup;
using DatGfxObj = DatReaderWriter.DBObjs.GfxObj;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #131 diagnostic (throwaway): identify the Holtburg portal among the
/// outside-stage setup ids captured by the [outstage] probe, by dumping each
/// candidate setup's parts + bounds from the dat. The portal's setup is the
/// translucent swirl; lamp posts / creatures / signs identify by part shape.
/// </summary>
[Trait("Purpose", "Diagnostic")]
[Trait("Lane", "InstalledDat")]
public class Issue131SetupProbeTests
{
private readonly ITestOutputHelper _out;
public Issue131SetupProbeTests(ITestOutputHelper output) => _out = output;
/// <summary>#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.</summary>
[Fact]
public void Diagnostic_LookInFlood_AdmitsHallPorchFromCottage()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
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);
_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()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: dats unavailable"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
uint[] candidates =
{
0x020010AC, // 0x7A9B4050 PASS r=11.9 — portal candidate A
0x02000B8E, // 0x7A9B403B PASS r=11.6 — portal candidate B
0x020019FF, // many instances (lamp posts?)
0x02000290,
0x02000001, // baseline (human?)
0x02000E08,
};
foreach (uint setupId in candidates)
{
var setup = dats.Get<DatSetup>(setupId);
if (setup is null)
{
_out.WriteLine(FormattableString.Invariant($"setup 0x{setupId:X8}: NOT FOUND"));
continue;
}
_out.WriteLine(FormattableString.Invariant(
$"setup 0x{setupId:X8}: parts={setup.Parts.Count}"));
int shown = 0;
foreach (uint partId in setup.Parts)
{
if (shown++ >= 4) { _out.WriteLine(" ..."); break; }
var gfx = dats.Get<DatGfxObj>(partId);
if (gfx is null) { _out.WriteLine(FormattableString.Invariant($" part 0x{partId:X8}: not found")); continue; }
var sb = new System.Text.StringBuilder();
sb.Append(FormattableString.Invariant(
$" part 0x{partId:X8}: polys={gfx.Polygons.Count} verts={gfx.VertexArray.Vertices.Count} surfaces=["));
int sShown = 0;
foreach (uint surfId in gfx.Surfaces)
{
if (sShown++ >= 6) { sb.Append(" ..."); break; }
sb.Append(FormattableString.Invariant($" 0x{surfId:X8}"));
}
sb.Append(" ]");
_out.WriteLine(sb.ToString());
}
}
}
}

View file

@ -1,284 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #176 (purple flashing at corridor seams, camera-angle dependent) + #177
/// (stairs pop in/out) — headless portal-flood replay in the Facility Hub
/// landblock 0x8A02. The unified hypothesis after the dat + draw-path reads:
/// both artifacts are FLOOD ADMISSION instability (a cell dropping out of
/// PortalVisibilityBuilder's admitted set paints the fog-purple clear color
/// where its geometry was; stair cells failing admission = the pop).
///
/// Production-matched inputs: Build(root, eye, lookup, viewProj,
/// buildingMembership: null) — the production call shape (FW3.3 retired drawLiftZ)
/// mirrors RetailPViewRenderer.DrawInside.
///
/// Scenarios:
/// A. #177 approach — stand in corridor 0x0178, look +X at the stair ramp
/// (0x0182) and the lower cell (0x0183): are they admitted?
/// B. #177 descent — eye path down the ramp crossing into 0x0183: does
/// 0x0182 (the ramp geometry's owner) drop near the transit?
/// C. #176 gaze sweep — eye parked in 0x016E near the 0x017A seam, yaw
/// sweep at several pitches: any cell admitted at angle k, gone at k+1,
/// back at k+2 (the bistability signature)?
/// D. #176 walk — eye tracks down the corridor across two seams, gaze
/// locked +X: per-step admitted-set diffs (drop-for-one-step churn).
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue176177FacilityHubFloodReplayTests
{
private const uint FacilityHub = 0x8A020000u;
private readonly ITestOutputHelper _out;
public Issue176177FacilityHubFloodReplayTests(ITestOutputHelper output) => _out = output;
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 gazeDir)
{
var view = Matrix4x4.CreateLookAt(eye, eye + gazeDir, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
private static List<uint> Flood(
Dictionary<uint, LoadedCell> cells, uint rootId, Vector3 eye, Vector3 gazeDir)
{
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var frame = PortalVisibilityBuilder.Build(
cells[rootId], eye, lookup, ViewProjFor(eye, gazeDir),
buildingMembership: null);
var result = new List<uint>(frame.OrderedVisibleCells);
result.Sort();
return result;
}
private static string CellSetString(IEnumerable<uint> ids)
=> string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}"));
[Fact]
public void ScenarioA_StairApproach_AdmissionsFromCorridor()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
Assert.True(cells.ContainsKey(FacilityHub | 0x0178u), "0x0178 not loaded");
// 0x0178 spans x<=95 at z 6..3 (floor 6); the ramp cell 0x0182 runs
// x 95→98.33 descending; 0x0183 continues at z 9 beyond x=98.33.
// Eye at standing height (~1.7 m above the 6 floor), approaching the
// stair portal at x=95, gazing +X with a slight downward pitch (the
// natural look at a descending stair).
foreach (float eyeX in new[] { 88f, 90f, 92f, 94f, 94.9f })
{
var eye = new Vector3(eyeX, -40f, -4.3f);
foreach (var (gaze, label) in new (Vector3, string)[]
{
(new Vector3(1f, 0f, 0f), "level"),
(Vector3.Normalize(new Vector3(1f, 0f, -0.35f)), "pitch-19"),
(Vector3.Normalize(new Vector3(1f, 0f, -0.7f)), "pitch-35"),
})
{
var visible = Flood(cells, FacilityHub | 0x0178u, eye, gaze);
bool ramp = visible.Contains(FacilityHub | 0x0182u);
bool lower = visible.Contains(FacilityHub | 0x0183u);
_out.WriteLine($"eyeX={eyeX,5:F1} gaze={label,-8} flood={visible.Count,2} " +
$"ramp0182={(ramp ? "Y" : "MISSING")} lower0183={(lower ? "Y" : "MISSING")} " +
$"[{CellSetString(visible)}]");
}
}
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_ScenarioB_StairDescent_RampCellRetention()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
// Descend the ramp: floor z goes 6 (x=95) → 9 (x=98.33), then flat.
// Eye rides ~1.7 m above the local floor. Root = the cell containing
// the eye by x-range (0x0178 x<95, ramp 0x0182 95..98.33, 0x0183 after).
// Gaze: forward and slightly down (running down stairs).
var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.4f));
List<uint>? prev = null;
for (float x = 94.0f; x <= 100.5f; x += 0.1f)
{
float floorZ = x < 95f ? -6f
: x < 98.333f ? -6f - 3f * (x - 95f) / 3.333f
: -9f;
var eye = new Vector3(x, -40f, floorZ + 1.7f);
uint rootId = x < 95f ? FacilityHub | 0x0178u
: x < 98.333f ? FacilityHub | 0x0182u
: FacilityHub | 0x0183u;
var visible = Flood(cells, rootId, eye, gazeDir);
bool ramp = visible.Contains(FacilityHub | 0x0182u);
bool upper = visible.Contains(FacilityHub | 0x0178u);
string diff = "";
if (prev is not null)
{
var removed = prev.Except(visible).ToList();
var added = visible.Except(prev).ToList();
if (removed.Count > 0) diff += $" REMOVED=[{CellSetString(removed)}]";
if (added.Count > 0) diff += $" added=[{CellSetString(added)}]";
}
_out.WriteLine($"x={x,6:F1} root={rootId & 0xFFFFu:X4} eyeZ={eye.Z,6:F2} flood={visible.Count,2} " +
$"ramp0182={(ramp ? "Y" : "MISSING")} up0178={(upper ? "Y" : "-")}{diff}");
prev = visible;
}
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_ScenarioC_CorridorSeamGazeSweep_Bistability()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
// Parked eye in 0x016E near the 0x017A seam (x=85), standing height.
var eye = new Vector3(83.5f, -40f, -4.3f);
uint rootId = FacilityHub | 0x016Eu;
int churnEvents = 0;
foreach (float pitchZ in new[] { 0f, -0.35f, -0.75f, -1.2f })
{
List<uint>? prevSet = null;
float prevYaw = 0f;
var perYawSets = new List<(float yaw, List<uint> set)>();
for (float yawDeg = -180f; yawDeg <= 180f; yawDeg += 2f)
{
float rad = yawDeg * MathF.PI / 180f;
var gaze = Vector3.Normalize(new Vector3(MathF.Cos(rad), MathF.Sin(rad), pitchZ));
var visible = Flood(cells, rootId, eye, gaze);
perYawSets.Add((yawDeg, visible));
if (prevSet is not null)
{
var removed = prevSet.Except(visible).ToList();
var added = visible.Except(prevSet).ToList();
if (removed.Count > 0 || added.Count > 0)
{
_out.WriteLine($"pitch={pitchZ,5:F2} yaw {prevYaw,6:F0}->{yawDeg,6:F0}: " +
$"flood {prevSet.Count}->{visible.Count}" +
(removed.Count > 0 ? $" REMOVED=[{CellSetString(removed)}]" : "") +
(added.Count > 0 ? $" added=[{CellSetString(added)}]" : ""));
}
}
prevSet = visible;
prevYaw = yawDeg;
}
// Bistability: a cell present at yaw k, absent at k+1, present at k+2.
for (int i = 2; i < perYawSets.Count; i++)
{
var flicker = perYawSets[i - 2].set
.Intersect(perYawSets[i].set)
.Except(perYawSets[i - 1].set)
.ToList();
if (flicker.Count > 0)
{
churnEvents++;
_out.WriteLine($">>> BISTABLE pitch={pitchZ:F2} yaw={perYawSets[i - 1].yaw:F0}: " +
$"cells [{CellSetString(flicker)}] dropped for ONE 2-degree step");
}
}
}
_out.WriteLine($"bistable one-step drop events: {churnEvents}");
}
/// <summary>
/// THE production lag-window scenario (from launch-137-gate2.log
/// [cell-transit] lines): membership transits fire 0.10.6 m PAST the
/// portal plane in the travel direction (016E→017A at x=85.3385.47 vs
/// the plane at x=85.00; 0182→0183 at 98.5698.64 vs 98.33). The render
/// root (viewer cell, same membership machinery) therefore holds the OLD
/// cell while the camera eye is already beyond the boundary portal's
/// plane. This scenario reproduces exactly that window: root=old cell,
/// eye stepped across and past the plane, gaze forward. If the forward
/// chain (the next corridor cells) drops inside the window, that is #176
/// (purple = fog clear color where the forward cells' geometry was) and
/// #177(a)/(c) at the stair transit.
/// </summary>
[Theory]
[Trait("Purpose", "Diagnostic")]
[InlineData(0x016Eu, 0x017Au, 85.00f, -4.3f)] // corridor seam, plane x=85
[InlineData(0x0182u, 0x0183u, 98.333f, -7.3f)] // stair-bottom transit, plane x=98.33
public void Diagnostic_ScenarioE_RootLagWindow_ForwardChainRetention(
uint rootLow, uint forwardLow, float planeX, float eyeZ)
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
uint rootId = FacilityHub | rootLow;
uint forwardId = FacilityHub | forwardLow;
var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.3f));
_out.WriteLine($"root=0x{rootLow:X4} forward=0x{forwardLow:X4} plane x={planeX:F2} " +
"(eye sweeps across; root HELD at the old cell = the production lag window)");
foreach (float dx in new[] { -0.30f, -0.10f, -0.02f, 0.00f, 0.02f, 0.05f, 0.10f, 0.20f, 0.30f, 0.45f, 0.60f })
{
var eye = new Vector3(planeX + dx, -40f, eyeZ);
var visible = Flood(cells, rootId, eye, gazeDir);
bool fwd = visible.Contains(forwardId);
_out.WriteLine($" eyeX=plane{(dx >= 0 ? "+" : "")}{dx:F2} flood={visible.Count,2} " +
$"forward={(fwd ? "Y" : ">>> DROPPED <<<")} [{CellSetString(visible)}]");
}
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_ScenarioD_CorridorWalk_PerStepChurn()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
// Walk 0x0165 → 0x016E → 0x017A along y=40 (seams at x=75 and x=85),
// gaze locked +X, slight downward pitch (the running view). Root flips
// by x-range at the seams (the camera transits the same portals).
var gazeDir = Vector3.Normalize(new Vector3(1f, 0f, -0.3f));
List<uint>? prev = null;
int churn = 0;
for (float x = 71f; x <= 89f; x += 0.05f)
{
uint rootId = x < 75f ? FacilityHub | 0x0165u
: x < 85f ? FacilityHub | 0x016Eu
: FacilityHub | 0x017Au;
if (!cells.ContainsKey(rootId)) continue;
var eye = new Vector3(x, -40f, -4.3f);
var visible = Flood(cells, rootId, eye, gazeDir);
if (prev is not null)
{
var removed = prev.Except(visible).ToList();
var added = visible.Except(prev).ToList();
if (removed.Count > 0 || added.Count > 0)
{
churn++;
_out.WriteLine($"x={x,6:F2} root={rootId & 0xFFFFu:X4} flood={visible.Count,2}" +
(removed.Count > 0 ? $" REMOVED=[{CellSetString(removed)}]" : "") +
(added.Count > 0 ? $" added=[{CellSetString(added)}]" : ""));
}
}
prev = visible;
}
_out.WriteLine($"admitted-set change events over the walk: {churn}");
}
}

View file

@ -1,727 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Scene.Arch;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Physics;
using AcDream.Core.Rendering;
using AcDream.Core.World;
using AcDream.Core.Meshing;
using DatReaderWriter;
using DatReaderWriter.Options;
using DatEnvCell = DatReaderWriter.DBObjs.EnvCell;
using DatEnvironment = DatReaderWriter.DBObjs.Environment;
using DatGfxObj = DatReaderWriter.DBObjs.GfxObj;
using DatSetup = DatReaderWriter.DBObjs.Setup;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #177 decisive full-chain replay: drive the REAL <see cref="RetailChaseCamera"/>
/// + REAL <see cref="PhysicsCameraCollisionProbe"/> down the Facility Hub
/// staircase against the REAL BSP, then feed the swept (ViewerCellId, eye,
/// View*Projection) into <see cref="PortalVisibilityBuilder.Build"/> — exactly
/// the production coupling (GameWindow ~9055-9065 + RetailPViewRenderer.DrawInside).
///
/// The earlier Issue176177 replays fed SYNTHETIC (root, eye) pairs: Scenario A
/// admitted the stairs fine from a coherent corridor root, Scenario E collapsed
/// the flood 12->1 but only because it HELD the root at the old cell while the
/// eye crossed a portal plane. This test removes the assumption: the root IS the
/// swept camera cell, the eye IS the swept camera eye, both from the same sweep.
/// If the stairs still collapse, #177 is a production root/eye coupling defect
/// and the printed per-portal side-test D names the failing gate. If they stay
/// admitted, the flood is exonerated and the artifact lives downstream.
/// </summary>
[Collection(CameraDiagnosticsCollection.Name)]
[Trait("Lane", "InstalledDat")]
public class Issue177StairDescentCameraFloodTests
{
private const uint FacilityHub = 0x8A020000u;
private readonly ITestOutputHelper _out;
public Issue177StairDescentCameraFloodTests(ITestOutputHelper output) => _out = output;
private static string? ResolveDatDir()
{
var fromEnv = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (!string.IsNullOrWhiteSpace(fromEnv) && Directory.Exists(fromEnv)) return fromEnv;
var def = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"Documents", "Asheron's Call");
return Directory.Exists(def) ? def : null;
}
private static PhysicsEngine BuildHubEngine(DatCollection dats)
{
var cache = new PhysicsDataCache();
var engine = new PhysicsEngine { DataCache = cache };
for (uint low = 0x0100u; low <= 0x01FFu; low++)
{
uint id = FacilityHub | low;
var datCell = dats.Get<DatEnvCell>(id);
if (datCell is null) continue;
var environment = dats.Get<DatEnvironment>(0x0D000000u | datCell.EnvironmentId);
if (environment is null) continue;
if (!environment.Cells.TryGetValue(datCell.CellStructure, out var cellStruct) || cellStruct is null)
continue;
var world = Matrix4x4.CreateFromQuaternion(datCell.Position.Orientation) *
Matrix4x4.CreateTranslation(datCell.Position.Origin);
cache.CacheCellStruct(id, datCell, cellStruct, world);
}
var heights = new byte[81];
var heightTable = new float[256];
for (int i = 0; i < 256; i++) heightTable[i] = -1000f;
engine.AddLandblock(FacilityHub, new TerrainSurface(heights, heightTable),
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
return engine;
}
// Analytic ramp floor from Scenario B: flat -6 (x<95), descends to -9 across
// x 95..98.333, then flat -9.
private static float FloorZ(float x)
=> x < 95f ? -6f
: x < 98.333f ? -6f - 3f * (x - 95f) / 3.333f
: -9f;
// Replicated side test (PortalVisibilityBuilder.CameraOnInteriorSide is private).
private const float PortalSideEpsilon = 0.01f;
private static bool EyeInteriorSide(LoadedCell cell, int i, Vector3 eye)
{
if (i >= cell.ClipPlanes.Count) return true;
var pl = cell.ClipPlanes[i];
if (pl.Normal.LengthSquared() < 1e-8f) return true;
var local = Vector3.Transform(eye, cell.InverseWorldTransform);
float dot = Vector3.Dot(pl.Normal, local) + pl.D;
return pl.InsideSide == 0 ? dot >= -PortalSideEpsilon : dot <= PortalSideEpsilon;
}
private static float SideD(LoadedCell cell, int i, Vector3 eye)
{
if (i >= cell.ClipPlanes.Count) return float.NaN;
var pl = cell.ClipPlanes[i];
if (pl.Normal.LengthSquared() < 1e-8f) return float.NaN;
var local = Vector3.Transform(eye, cell.InverseWorldTransform);
return Vector3.Dot(pl.Normal, local) + pl.D;
}
private static string CellSet(IEnumerable<uint> ids)
=> string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}"));
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_StairCellComposition_ShellVsStatics()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
foreach (uint low in new uint[] { 0x015E, 0x015F, 0x01C8, 0x01C4, 0x01C9, 0x0210, 0x020E, 0x01C1, 0x01C0 })
{
uint id = FacilityHub | low;
var envCell = dats.Get<DatEnvCell>(id);
if (envCell is null) { _out.WriteLine($"0x{low:X4}: (no dat cell)"); continue; }
var environment = dats.Get<DatEnvironment>(0x0D000000u | envCell.EnvironmentId);
environment!.Cells.TryGetValue(envCell.CellStructure, out var cs);
int polys = cs?.Polygons.Count ?? -1;
int verts = cs?.VertexArray.Vertices.Count ?? -1;
var mn = new Vector3(float.MaxValue); var mx = new Vector3(float.MinValue);
if (cs is not null)
foreach (var kv in cs.VertexArray.Vertices)
{ var o = kv.Value.Origin; var v = new Vector3(o.X, o.Y, o.Z); mn = Vector3.Min(mn, v); mx = Vector3.Max(mx, v); }
int statics = envCell.StaticObjects?.Count ?? 0;
bool seenOut = (envCell.Flags & DatReaderWriter.Enums.EnvCellFlags.SeenOutside) != 0;
_out.WriteLine(FormattableString.Invariant(
$"0x{low:X4} env=0x{envCell.EnvironmentId:X4} struct={envCell.CellStructure} shellPolys={polys} shellVerts={verts} bounds=[{mn.X:F1},{mn.Y:F1},{mn.Z:F1}]..[{mx.X:F1},{mx.Y:F1},{mx.Z:F1}] portals={envCell.CellPortals.Count} statics={statics} seenOut={seenOut}"));
if (envCell.StaticObjects is not null)
foreach (var stab in envCell.StaticObjects)
{
var o = stab.Frame.Origin;
_out.WriteLine(FormattableString.Invariant(
$" static Id=0x{stab.Id:X8} kind={(stab.Id >> 24 == 0x02 ? "Setup" : stab.Id >> 24 == 0x01 ? "GfxObj" : "?")} pos=({o.X:F2},{o.Y:F2},{o.Z:F2})"));
}
// Portal normals (local space) — horizontal (|Nz|~1) = FLOOR/CEILING portal
// (the edge-on-clip suspect); vertical (|Nz|~0) = wall/doorway portal.
var lc = renderLookup(id);
if (lc is not null)
for (int i = 0; i < lc.Portals.Count; i++)
{
var n = i < lc.ClipPlanes.Count ? lc.ClipPlanes[i].Normal : Vector3.Zero;
string kind = MathF.Abs(n.Z) > 0.7f ? "FLOOR/CEIL" : MathF.Abs(n.Z) < 0.3f ? "wall" : "slope";
_out.WriteLine(FormattableString.Invariant(
$" portal[{i}]->0x{lc.Portals[i].OtherCellId:X4} N=({n.X:F2},{n.Y:F2},{n.Z:F2}) {kind} inside={lc.ClipPlanes[i].InsideSide}"));
}
}
LoadedCell? renderLookup(uint id)
{
try { return CornerFloodReplayTests.LoadCell(dats, id); }
catch (InvalidOperationException) { return null; }
}
}
/// <summary>
/// Campaign OVERHAUL S2 chunk 2: retargeted from the deleted
/// <c>ShadowObjectRegistry.ComputeStaticRenderCells</c> pure-function
/// side channel to the production
/// <see cref="ShadowObjectRegistry.RegisterMultiPart"/> transaction
/// (the SAME BSP-collision + <c>partArray:</c> recipe
/// <c>LandblockPhysicsPublisher.PublishStaticEntity</c> uses, and the
/// same one <c>RetailCellArrayComparatorInstalledDatTests</c>' fixture 1
/// exercises for this exact Setup) plus
/// <see cref="ShadowObjectRegistry.TryGetRetailCellArray"/>. The entity
/// id now MUST match the projection's
/// <see cref="RenderSourceMetadata.LocalEntityId"/> — chunk 2's
/// <see cref="WalkProductionWorldData"/> indoor sweep looks the retail
/// CELLARRAY up BY that id, unlike the deleted method's seed+position
/// pure function, which never carried entity identity. The former
/// "WithoutCollisionRows" name no longer describes production: a part
/// array is always supplied at the SAME registration as the collision
/// shapes now (chunk 1b), so <c>GetOwnerCells</c> for this BSP-bearing
/// Setup reflects that registration — and, per the comparator's fixture-1
/// evidence, equals the retail CELLARRAY exactly.
/// </summary>
[Fact]
public void FacilityStairAssembly_RegisterAcross015FTo015E()
{
string? datDir = ResolveDatDir();
if (datDir is null)
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
using var dats = new DatCollection(datDir, DatAccessType.Read);
const uint setupId = 0x02000623u;
DatSetup setup = Assert.IsType<DatSetup>(dats.Get<DatSetup>(setupId));
PhysicsEngine engine = BuildHubEngine(dats);
PhysicsDataCache cache = Assert.IsType<PhysicsDataCache>(engine.DataCache);
IReadOnlyList<MeshRef> meshRefs = SetupMesh.Flatten(setup);
foreach (MeshRef meshRef in meshRefs)
{
DatGfxObj gfxObj = Assert.IsType<DatGfxObj>(
dats.Get<DatGfxObj>(meshRef.GfxObjId));
cache.CacheGfxObj(meshRef.GfxObjId, gfxObj);
}
DatEnvCell parent = Assert.IsType<DatEnvCell>(
dats.Get<DatEnvCell>(FacilityHub | 0x015Fu));
var stair = Assert.Single(
parent.StaticObjects,
staticObject =>
staticObject.Id == setupId);
var rootPosition = new Vector3(
stair.Frame.Origin.X,
stair.Frame.Origin.Y,
stair.Frame.Origin.Z);
Quaternion rootRotation = stair.Frame.Orientation;
List<ShadowShape> bspShapes = ShadowShapeBuilder.FromLandblockBspParts(
meshRefs, isBuildingShell: false, cache.GetGfxObj);
List<ShadowShape> parts = ShadowShapeBuilder.FromStaticRenderParts(
meshRefs,
cache.GetGfxObj,
cache.GetVisualBounds,
out bool hasPhysicsBsp);
Assert.True(hasPhysicsBsp);
Assert.NotEmpty(parts);
Assert.NotEmpty(bspShapes);
const uint entityId = 0x48A02000u;
engine.ShadowObjects.RegisterMultiPart(
entityId,
rootPosition,
rootRotation,
bspShapes,
state: 0u,
flags: EntityCollisionFlags.None,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: FacilityHub,
seedCellId: FacilityHub | 0x015Fu,
isStatic: true,
partArray: parts);
Assert.True(engine.ShadowObjects.TryGetRetailCellArray(
entityId, out IReadOnlyList<uint> cells));
ShadowShape part = parts[0];
ShadowPartBox box = ShadowPartBox.FromShape(
part,
rootPosition,
rootRotation);
LoadedCell rootCell = CornerFloodReplayTests.LoadCell(
dats,
FacilityHub | 0x015Fu);
box.RefitToLocal(rootCell.InverseWorldTransform, out Vector3 boxMin, out Vector3 boxMax);
_out.WriteLine(FormattableString.Invariant(
$"stair root=({rootPosition.X:F3},{rootPosition.Y:F3},{rootPosition.Z:F3}) localBox=({boxMin.X:F3},{boxMin.Y:F3},{boxMin.Z:F3})..({boxMax.X:F3},{boxMax.Y:F3},{boxMax.Z:F3}) cells=[{string.Join(',', cells.Select(static id => $"0x{id:X8}"))}]"));
Assert.Contains(FacilityHub | 0x015Fu, cells);
Assert.Contains(FacilityHub | 0x015Eu, cells);
// The registration carries collision shapes at the SAME transaction
// now (production always does — chunk 1b threaded the part array
// through the SAME RegisterMultiPart call the collision dispatch
// uses), so GetOwnerCells reflects that registration instead of
// staying empty; retail == collision for this BSP-bearing Setup
// (RetailCellArrayComparatorInstalledDatTests fixture 1), so the two
// sets agree exactly.
Assert.Equal(
cells.OrderBy(id => id),
engine.ShadowObjects.GetOwnerCells(entityId).OrderBy(id => id));
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionId projectionId = RenderProjectionId.FromRaw(entityId);
RenderTransform transform = RenderTransform.FromRoot(
rootPosition,
rootRotation,
1f);
RenderProjectionRecord projection = new RenderProjectionRecord() with
{
Id = projectionId,
ProjectionClass = RenderProjectionClass.IndoorCellStatic,
OwnerIncarnation = RenderOwnerIncarnation.FromRaw(1),
Transform = transform,
PreviousTransform = new PreviousRenderTransform(transform.LocalToWorld),
Residency = new RenderSpatialResidency(
RenderSpatialBucket.FromRaw(FacilityHub | 0x015Fu),
FacilityHub,
FacilityHub | 0x015Fu),
Flags = RenderProjectionFlags.Draw,
Source = new RenderSourceMetadata() with
{
LocalEntityId = entityId,
SourceId = setupId,
ParentCellId = FacilityHub | 0x015Fu,
},
EntityPayload = new RenderEntityPayload(meshRefs, null, false),
};
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(
new WalkBuildingRegistry(),
engine.ShadowObjects);
worldData.BeginFrame(
scene.OpenQuery(),
FacilityHub,
renderCenterLbX: 0x8A,
renderCenterLbY: 0x02);
Assert.Contains(
worldData.GetCellStatics(FacilityHub | 0x015Eu).Records,
record => record.Id == projectionId);
Assert.Equal(0, worldData.UnregisteredRenderMembershipCount);
}
/// <summary>
/// #177 re-diagnosis (fix#1 failed the visual gate): the vanish flips on a SLIGHT turn +
/// depends on zoom, so it is a gaze/eye knife-edge, not the eye-squarely-in-opening case
/// fix#1 handled. Drive the REAL camera at a fixed player spot on the 015F stairs, sweep
/// yaw FINELY at two zooms, and print the viewer cell + stair-cell admission each step to
/// locate the knife-edge and which cell flips.
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_RealStaircase_FineYawZoomSweep_FindKnifeEdge()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
var engine = BuildHubEngine(dats);
Func<uint, LoadedCell?> lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
try
{
CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
uint[] stair = { 0x015Eu, 0x015Fu, 0x01C1u, 0x01C0u, 0x020Fu };
// Player standing mid-015F stairs, eye near the ceiling boundary (per the sweep char).
foreach (var playerPos in new[] { new Vector3(60.5f, -46f, -2.5f), new Vector3(60.5f, -47f, -3.0f) })
foreach (float dist in new[] { 2.61f, 12.0f })
{
_out.WriteLine(FormattableString.Invariant(
$"=== player=({playerPos.X:F1},{playerPos.Y:F1},{playerPos.Z:F1}) zoom={dist} ==="));
var cam = new RetailChaseCamera
{
CollisionProbe = new PhysicsCameraCollisionProbe(engine),
FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist,
};
uint prevViewer = 0; string prevAdmit = "";
for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 6f)
{
float yaw = yawDeg * MathF.PI / 180f;
var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Fu, playerPos + new Vector3(0, 0, 0.1f));
for (int i = 0; i < 160; i++)
cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A);
var eye = cam.Position; uint viewer = cam.ViewerCellId;
string admit = "?";
if (viewer != 0u && renderCells.TryGetValue(viewer, out var rc))
{
var f = PortalVisibilityBuilder.Build(rc, eye, lookup, cam.View * cam.Projection, null);
var s = f.OrderedVisibleCells;
admit = string.Concat(stair.Select(low => (s.Contains(FacilityHub | low) ? "1" : "0")));
}
// Print only when viewer or the admit pattern CHANGES (knife-edges).
if (viewer != prevViewer || admit != prevAdmit)
{
_out.WriteLine(FormattableString.Invariant(
$" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} stair[5E,5F,C1,C0,0F]={admit}"));
prevViewer = viewer; prevAdmit = admit;
}
}
}
}
finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; }
}
/// <summary>
/// #177 retail-vs-us flood DEPTH: retail's cdb capture shows PView cell_draw_num reaching 26
/// cells from camera cell 015E (avg 14.3) looking down the spiral shaft. Sweep OUR real camera
/// from 015E over many gaze/zoom poses and report the max/avg OrderedVisibleCells — if ours
/// caps well below retail's 26, our flood truncates the spiral (the vanish).
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_FloodDepthFrom015E_VsRetail26()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
var engine = BuildHubEngine(dats);
Func<uint, LoadedCell?> lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
try
{
CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
int maxCells = 0; uint bestViewer = 0; double sum = 0; int n = 0; string bestSet = "";
foreach (var playerPos in new[] { new Vector3(61f, -42f, -4f), new Vector3(60f, -41f, -5f), new Vector3(62f, -43f, -4f) })
foreach (float dist in new[] { 2.61f, 4f, 8f })
foreach (float pitch in new[] { 0.291f, 0.7f, -0.2f })
{
var cam = new RetailChaseCamera { CollisionProbe = new PhysicsCameraCollisionProbe(engine), FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist, Pitch = pitch };
for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 10f)
{
float yaw = yawDeg * MathF.PI / 180f;
var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Eu, playerPos + new Vector3(0, 0, 0.1f));
for (int i = 0; i < 140; i++) cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A);
uint viewer = cam.ViewerCellId;
if (viewer == 0u || !renderCells.TryGetValue(viewer, out var rc)) continue;
var f = PortalVisibilityBuilder.Build(rc, cam.Position, lookup, cam.View * cam.Projection, null);
int c = f.OrderedVisibleCells.Count; sum += c; n++;
if (c > maxCells) { maxCells = c; bestViewer = viewer; bestSet = CellSet(f.OrderedVisibleCells.OrderBy(v => v)); }
}
}
_out.WriteLine(FormattableString.Invariant(
$"OUR flood from 015E-area: maxCells={maxCells} (retail max=26) avg={sum / Math.Max(1, n):F1} (retail avg=14.3) bestViewer=0x{bestViewer & 0xFFFF:X4}"));
_out.WriteLine($" best set: [{bestSet}]");
}
finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; }
}
private static Matrix4x4 ViewProj(Vector3 eye, Vector3 dir)
=> Matrix4x4.CreateLookAt(eye, eye + Vector3.Normalize(dir), Vector3.UnitZ)
* Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 0.1f, 5000f);
// Signed distance of a WORLD point to a render cell's portal[i] plane (local-space plane).
private static float PortalPlaneDistance(LoadedCell cell, int i, Vector3 worldPt)
{
var pl = cell.ClipPlanes[i];
var local = Vector3.Transform(worldPt, cell.InverseWorldTransform);
return Vector3.Dot(pl.Normal, local) + pl.D;
}
/// <summary>
/// #177 THE decisive experiment. Reproduce the captured failing frame — camera in
/// cell 0x015F with the eye sitting IN the ceiling-portal plane (production [flap]:
/// root=015F eye=(60.47,-46.57,-0.00), p->01C1 clip=0) — and confirm the upper
/// stair cell 01C1 drops from the flood. Then move the SAME eye 0.4 m off that
/// plane (into the cell interior) and confirm 01C1 is re-admitted. If the contrast
/// holds, #177's fix is a viewpoint-off-the-portal-plane rule, and the load-bearing
/// clip (#119/#181) is correct as-is.
/// </summary>
[Fact]
public void EdgeOnCeilingPortal_DropsUpperCell_OffPlaneReadmits()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var root = cells[FacilityHub | 0x015Fu];
uint upperId = FacilityHub | 0x01C1u;
// Find 015F's portal to 01C1 (the ceiling/up portal) + report its normal.
int upIdx = -1;
for (int i = 0; i < root.Portals.Count; i++)
if (root.Portals[i].OtherCellId == 0x01C1u) { upIdx = i; break; }
Assert.True(upIdx >= 0, "015F has no portal to 01C1");
var upN = root.ClipPlanes[upIdx].Normal;
_out.WriteLine(FormattableString.Invariant(
$"015F portal[{upIdx}]->01C1 localN=({upN.X:F2},{upN.Y:F2},{upN.Z:F2})"));
// Look UP the stairs so the ceiling portal + 01C1 land on screen.
var gaze = new Vector3(0f, -0.3f, 1f);
// Captured FAILING eye — in the ceiling-portal plane (world z ~0).
var eyeEdgeOn = new Vector3(60.47f, -46.57f, -0.00f);
// Same eye, pulled 0.4 m DOWN (off the ceiling portal, into 015F's interior).
var eyeOffPlane = eyeEdgeOn + new Vector3(0f, 0f, -0.40f);
foreach (var (label, eye) in new[] { ("edge-on(captured)", eyeEdgeOn), ("off-plane(-0.4z)", eyeOffPlane) })
{
float d = PortalPlaneDistance(root, upIdx, eye);
var frame = PortalVisibilityBuilder.Build(
root, eye, lookup, ViewProj(eye, gaze),
buildingMembership: null);
var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList();
bool up = vis.Contains(upperId);
string admit = up ? "ADMITTED" : "DROPPED";
_out.WriteLine(FormattableString.Invariant(
$"{label,-18} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) D->01C1={d,6:F3} 01C1={admit} flood={vis.Count} [{CellSet(vis)}]"));
}
// #177 CHARACTERIZATION (fix#1 reverted — this edge-on-in-plane case was NOT the
// production mechanism; the real vanish is a grazing/sliver flood collapse in the
// spiral, root 0x01C8, portals off-screen/sliver, camera NOT collision-jammed).
// Kept as a mechanism pin: an eye exactly in the ceiling-portal plane drops the upper
// cell (edge-on → <3 clip), an eye 0.4 m off admits it via the normal clip.
var fEdge = PortalVisibilityBuilder.Build(root, eyeEdgeOn, lookup, ViewProj(eyeEdgeOn, gaze), null);
var fOff = PortalVisibilityBuilder.Build(root, eyeOffPlane, lookup, ViewProj(eyeOffPlane, gaze), null);
Assert.DoesNotContain(upperId, fEdge.OrderedVisibleCells); // edge-on drops (mechanism)
Assert.Contains(upperId, fOff.OrderedVisibleCells); // off-plane admits (control)
}
/// <summary>
/// #177 sweep characterization: place the player on the 015F staircase and drive the
/// REAL chase camera + collision sweep, reporting where the eye lands relative to the
/// 015F ceiling-portal plane (world z=0). If the eye rests AT the plane (|D|~0) the
/// flood collapses (see EdgeOnCeilingPortal test); this shows whether that is a swept
/// contact leaving the eye on the surface vs free-space boom geometry.
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_StaircaseSweep_EyeClearanceFromCeilingPortal()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
var engine = BuildHubEngine(dats);
var root015F = renderCells[FacilityHub | 0x015Fu];
int upIdx = 0; // portal[0]->01C1 (ceiling)
bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
try
{
CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
// Player on the 015F stairs (world y~-46, ceiling portal at world z~0), swept
// at a range of heights + facings (up-stairs = -Y, down-stairs = +Y).
foreach (float pz in new[] { -1.0f, -1.5f, -2.0f, -2.5f, -3.0f, -3.5f })
foreach (var (fLabel, yaw) in new (string, float)[] { ("up(-Y)", -MathF.PI / 2f), ("down(+Y)", MathF.PI / 2f) })
{
var playerPos = new Vector3(60.5f, -46f, pz);
var (pcell, _) = engine.AdjustPosition(FacilityHub | 0x015Fu, playerPos + new Vector3(0, 0, 0.1f));
var cam = new RetailChaseCamera
{
CollisionProbe = new PhysicsCameraCollisionProbe(engine),
FovY = MathF.PI / 3f, Aspect = 16f / 9f,
};
for (int i = 0; i < 300; i++)
cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, pcell, 0x5000000A);
var eye = cam.Position;
uint viewer = cam.ViewerCellId;
// D from the eye to 015F's ceiling-portal plane (only meaningful if viewer==015F).
float dCeil = PortalPlaneDistance(root015F, upIdx, eye);
_out.WriteLine(FormattableString.Invariant(
$"playerZ={pz,5:F1} face={fLabel,-8} pcell=0x{pcell & 0xFFFF:X4} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} D->ceilPortal={dCeil,6:F3}"));
}
}
finally { CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA; }
}
[Theory]
[Trait("Purpose", "Diagnostic")]
// spot label, player x, player cell, description
[InlineData(90.0f, 0x0178u, "TOP corridor 0178 (approach view)")]
[InlineData(100.0f, 0x0183u, "BOTTOM room 0183 (look-back view)")]
public void Diagnostic_ParkedYawZoomSweep_StairAdmission(float px, uint pcell, string label)
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
var engine = BuildHubEngine(dats);
Func<uint, LoadedCell?> lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
bool sC = CameraDiagnostics.CollideCamera; bool sA = CameraDiagnostics.AlignToSlope;
float sT = CameraDiagnostics.TranslationStiffness, sR = CameraDiagnostics.RotationStiffness;
try
{
CameraDiagnostics.CollideCamera = true; CameraDiagnostics.AlignToSlope = true;
CameraDiagnostics.TranslationStiffness = 0.45f; CameraDiagnostics.RotationStiffness = 0.45f;
var playerPos = new Vector3(px, -40f, FloorZ(px));
uint playerCell = FacilityHub | pcell;
_out.WriteLine($"=== {label} @ ({px:F1},-40,{FloorZ(px):F2}) ===");
foreach (float dist in new[] { 2.61f, 8.0f })
{
_out.WriteLine($" -- zoom Distance={dist} --");
var cam = new RetailChaseCamera
{
CollisionProbe = new PhysicsCameraCollisionProbe(engine),
FovY = MathF.PI / 3f, Aspect = 16f / 9f, Distance = dist,
};
for (float yawDeg = 0f; yawDeg < 360f; yawDeg += 30f)
{
float yaw = yawDeg * MathF.PI / 180f;
// Settle at this yaw (static player).
for (int i = 0; i < 200; i++)
cam.Update(playerPos, yaw, Vector3.Zero, true, Vector3.UnitZ, 1f / 60f, playerCell, 0x5000000A);
Vector3 eye = cam.Position; uint viewer = cam.ViewerCellId;
var vp = cam.View * cam.Projection;
if (viewer != 0u && renderCells.TryGetValue(viewer, out var rootCell))
{
var f = PortalVisibilityBuilder.Build(rootCell, eye, lookup, vp, null);
var vis = f.OrderedVisibleCells.OrderBy(v => v).ToList();
string flags = $"0178={(vis.Contains(FacilityHub | 0x0178u) ? "Y" : "-")} 0182={(vis.Contains(FacilityHub | 0x0182u) ? "Y" : "-")} 0183={(vis.Contains(FacilityHub | 0x0183u) ? "Y" : "-")} 0181={(vis.Contains(FacilityHub | 0x0181u) ? "Y" : "-")}";
_out.WriteLine(FormattableString.Invariant(
$" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer & 0xFFFF:X4} {flags} flood={vis.Count} [{CellSet(vis)}]"));
}
else
_out.WriteLine(FormattableString.Invariant($" yaw={yawDeg,3:F0} eye=({eye.X,6:F2},{eye.Y,6:F2},{eye.Z,6:F2}) viewer=0x{viewer:X8} ROOT-MISSING"));
}
}
}
finally
{
CameraDiagnostics.CollideCamera = sC; CameraDiagnostics.AlignToSlope = sA;
CameraDiagnostics.TranslationStiffness = sT; CameraDiagnostics.RotationStiffness = sR;
}
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_Descent_RealCameraSweep_StairCellRetention()
{
var datDir = ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var renderCells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
var engine = BuildHubEngine(dats);
Func<uint, LoadedCell?> lookup = id => renderCells.TryGetValue(id, out var c) ? c : null;
bool savedColl = CameraDiagnostics.CollideCamera;
bool savedAlign = CameraDiagnostics.AlignToSlope;
float savedT = CameraDiagnostics.TranslationStiffness;
float savedR = CameraDiagnostics.RotationStiffness;
try
{
CameraDiagnostics.CollideCamera = true;
CameraDiagnostics.AlignToSlope = true;
CameraDiagnostics.TranslationStiffness = 0.45f;
CameraDiagnostics.RotationStiffness = 0.45f;
var cam = new RetailChaseCamera
{
CollisionProbe = new PhysicsCameraCollisionProbe(engine),
FovY = MathF.PI / 3f,
Aspect = 16f / 9f,
};
const float yaw = 0f; // facing +X, descending
const float speed = 3.0f; // m/s run
const float dt = 1f / 60f;
float startX = 88f, endX = 101.5f;
// Seat the player + settle the camera at the start pose.
uint playerCell = FacilityHub | 0x0178u;
Vector3 P(float x) => new Vector3(x, -40f, FloorZ(x));
for (int i = 0; i < 400; i++)
cam.Update(P(startX), yaw, Vector3.Zero, true, Vector3.UnitZ, dt, playerCell, 0x5000000A);
_out.WriteLine("x playerCell eye=(x,y,z) viewerCell coh flood stairs rootPortalsPastPlane");
float x = startX;
uint prevViewer = cam.ViewerCellId;
int collapses = 0, incoherentFrames = 0;
while (x <= endX)
{
var playerPos = P(x);
// Track player membership like production: AdjustPosition carry-forward.
var (pc, found) = engine.AdjustPosition(playerCell, playerPos + new Vector3(0, 0, 0.1f));
if (found) playerCell = pc;
cam.Update(playerPos, yaw, new Vector3(speed, 0, 0), true, Vector3.UnitZ, dt, playerCell, 0x5000000A);
Vector3 eye = cam.Position;
uint viewer = cam.ViewerCellId;
var viewProj = cam.View * cam.Projection;
string floodStr, stairs, coh, past;
if (viewer != 0u && renderCells.TryGetValue(viewer, out var rootCell))
{
var frame = PortalVisibilityBuilder.Build(
rootCell, eye, lookup, viewProj,
buildingMembership: null);
var vis = frame.OrderedVisibleCells.OrderBy(v => v).ToList();
floodStr = $"{vis.Count,2} [{CellSet(vis)}]";
bool ramp = vis.Contains(FacilityHub | 0x0182u);
bool lower = vis.Contains(FacilityHub | 0x0183u);
stairs = $"r{(ramp ? "Y" : "-")}l{(lower ? "Y" : "-")}";
// Coherence: is the eye on the interior side of ALL of root's portals?
// If not, the eye is outside root -> the crossed portal's subtree drops.
var pastList = new List<string>();
bool coherent = true;
for (int i = 0; i < rootCell.Portals.Count && i < rootCell.ClipPlanes.Count; i++)
{
if (!EyeInteriorSide(rootCell, i, eye))
{
coherent = false;
pastList.Add($"p{i}->0x{rootCell.Portals[i].OtherCellId:X4}(D={SideD(rootCell, i, eye):F3})");
}
}
coh = coherent ? "IN " : "OUT";
past = pastList.Count == 0 ? "-" : string.Join(",", pastList);
if (!coherent) incoherentFrames++;
if (vis.Count <= 1) collapses++;
}
else
{
floodStr = "root-missing/0"; stairs = "--"; coh = "?"; past = $"viewer=0x{viewer:X8}";
}
bool viewerFlip = viewer != prevViewer;
prevViewer = viewer;
_out.WriteLine(FormattableString.Invariant(
$"{x,6:F2} 0x{playerCell:X8} ({eye.X,7:F3},{eye.Y,7:F3},{eye.Z,7:F3}) 0x{viewer:X8} {coh} {floodStr} {stairs} {(viewerFlip ? "FLIP " : "")}{past}"));
x += speed * dt;
}
_out.WriteLine($"SUMMARY: collapses(flood<=1)={collapses} incoherentFrames(eye-outside-root)={incoherentFrames}");
}
finally
{
CameraDiagnostics.CollideCamera = savedColl;
CameraDiagnostics.AlignToSlope = savedAlign;
CameraDiagnostics.TranslationStiffness = savedT;
CameraDiagnostics.RotationStiffness = savedR;
}
}
}

View file

@ -1,165 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #181 — the portal-flood admitted set flaps 31↔32 on MICRON eye noise at a
/// parked camera (live: launch-176-leakfix.log, root 0x8A020142, vis flips
/// every ~100200 frames for 517k frames; the swept eye carries ~7 µm
/// float-roundtrip noise). The flapping cell's visibility-scoped lights + the
/// union-AABB scissor rect strobe = the #176 washed-region flicker.
///
/// This replay hunts the knife edge headlessly: at the live parked eye
/// (49.15, 38.62, 3.98), sweep gazes and perturb the eye by ±0.5 mm per
/// axis; ANY admitted-set difference at sub-mm perturbation is the #181
/// instability, and the symmetric difference names the flapping cell.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue181VisFlapReplayTests
{
private const uint FacilityHub = 0x8A020000u;
private const uint LiveRoot = FacilityHub | 0x0142u;
private static readonly Vector3 LiveEye = new(49.15f, -38.62f, -3.98f);
private readonly ITestOutputHelper _out;
public Issue181VisFlapReplayTests(ITestOutputHelper output) => _out = output;
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 gazeDir)
{
var view = Matrix4x4.CreateLookAt(eye, eye + gazeDir, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 0.1f, 5000f);
return view * proj;
}
private static List<uint> Flood(
Dictionary<uint, LoadedCell> cells, uint rootId, Vector3 eye, Vector3 gazeDir)
{
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
var frame = PortalVisibilityBuilder.Build(
cells[rootId], eye, lookup, ViewProjFor(eye, gazeDir),
buildingMembership: null);
var result = new List<uint>(frame.OrderedVisibleCells);
result.Sort();
return result;
}
private static string CellSetString(IEnumerable<uint> ids)
=> string.Join(" ", ids.Select(id => $"{id & 0xFFFFu:X4}"));
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_FlappingCellViewRegion_SliverOrLarge()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
uint flapper = FacilityHub | 0x0181u;
// The most unstable pose from the perturbation sweep.
float yaw = 15f * MathF.PI / 180f, pitch = -20f * MathF.PI / 180f;
var gaze = new Vector3(
MathF.Cos(pitch) * MathF.Cos(yaw),
MathF.Cos(pitch) * MathF.Sin(yaw),
MathF.Sin(pitch));
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
foreach (var (label, eye) in new (string, Vector3)[]
{
("base ", LiveEye),
("+0.5mm x ", LiveEye + new Vector3(0.0005f, 0, 0)),
("-0.5mm x ", LiveEye - new Vector3(0.0005f, 0, 0)),
("+5mm x ", LiveEye + new Vector3(0.005f, 0, 0)),
("-5mm x ", LiveEye - new Vector3(0.005f, 0, 0)),
("+2cm z ", LiveEye + new Vector3(0, 0, 0.02f)),
("-2cm z ", LiveEye - new Vector3(0, 0, 0.02f)),
})
{
var frame = PortalVisibilityBuilder.Build(
cells[LiveRoot], eye, lookup, ViewProjFor(eye, gaze),
buildingMembership: null);
if (!frame.CellViews.TryGetValue(flapper, out var view) || view.IsEmpty)
{
_out.WriteLine($"{label}: 0181 NOT ADMITTED");
continue;
}
// NDC shoelace area of the admitted region (screen is 4.0 NDC-units total).
float area = 0f; int polys = 0, verts = 0;
foreach (var vp in view.Polygons)
{
var v = vp.Vertices;
polys++; verts += v.Length;
float a = 0f;
for (int i = 0; i < v.Length; i++)
{
var p = v[i]; var q = v[(i + 1) % v.Length];
a += p.X * q.Y - q.X * p.Y;
}
area += MathF.Abs(a) * 0.5f;
}
_out.WriteLine(FormattableString.Invariant(
$"{label}: 0181 admitted polys={polys} verts={verts} ndcArea={area:F4} ({area / 4f * 100f:F1}% of screen)"));
}
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_SubMillimeterEyePerturbation_MustNotChangeAdmission()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null) { _out.WriteLine("SKIP: no dat dir"); Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md."); }
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, FacilityHub);
Assert.True(cells.ContainsKey(LiveRoot), "live root 0x0142 not loaded");
// ±0.5 mm per axis — an order of magnitude above the live µm noise, well
// below anything visually meaningful.
var perturbations = new[]
{
new Vector3( 0.0005f, 0, 0), new Vector3(-0.0005f, 0, 0),
new Vector3(0, 0.0005f, 0), new Vector3(0, -0.0005f, 0),
new Vector3(0, 0, 0.0005f), new Vector3(0, 0, -0.0005f),
};
int unstableGazes = 0;
for (int yawDeg = 0; yawDeg < 360; yawDeg += 15)
{
foreach (float pitchDeg in new[] { 0f, -20f, -35f })
{
float yaw = yawDeg * MathF.PI / 180f, pitch = pitchDeg * MathF.PI / 180f;
var gaze = new Vector3(
MathF.Cos(pitch) * MathF.Cos(yaw),
MathF.Cos(pitch) * MathF.Sin(yaw),
MathF.Sin(pitch));
var baseline = Flood(cells, LiveRoot, LiveEye, gaze);
foreach (var d in perturbations)
{
var perturbed = Flood(cells, LiveRoot, LiveEye + d, gaze);
if (perturbed.SequenceEqual(baseline)) continue;
unstableGazes++;
var removed = baseline.Except(perturbed).ToList();
var added = perturbed.Except(baseline).ToList();
_out.WriteLine(FormattableString.Invariant(
$"UNSTABLE yaw={yawDeg} pitch={pitchDeg} d=({d.X * 1000:F1},{d.Y * 1000:F1},{d.Z * 1000:F1})mm base={baseline.Count} pert={perturbed.Count} removed=[{CellSetString(removed)}] added=[{CellSetString(added)}]"));
}
}
}
_out.WriteLine($"unstable (gaze, perturbation) pairs: {unstableGazes}");
// Diagnostic first: report, don't assert — the fix turns this into a hard pin.
}
}

View file

@ -155,29 +155,12 @@ public class Issue181WallPressEquilibriumTests
_out.WriteLine(FormattableString.Invariant(
$"viewer cells seen: {cells.Count} transitions={cellTransitions} eye=({cam.Position.X:F6},{cam.Position.Y:F6},{cam.Position.Z:F6}) cell=0x{cam.ViewerCellId:X8}"));
// Orbit structure: 16 consecutive frames at 6dp, with the sweep's
// own [flap-sweep] lines captured for the same frames.
bool savedFlap = RenderingDiagnostics.ProbeFlapEnabled;
var savedOut = Console.Out;
try
// Orbit structure: 16 consecutive frames at 6dp.
for (int i = 0; i < 16; i++)
{
RenderingDiagnostics.ProbeFlapEnabled = true;
using var writer = new StringWriter();
Console.SetOut(writer);
for (int i = 0; i < 16; i++)
{
Step();
writer.WriteLine(FormattableString.Invariant(
$"orbit[{i:D2}] eye=({cam.Position.X:F6},{cam.Position.Y:F6},{cam.Position.Z:F6})"));
}
Console.SetOut(savedOut);
foreach (var line in writer.ToString().Split('\n'))
if (line.Length > 1) _out.WriteLine(line.TrimEnd());
}
finally
{
Console.SetOut(savedOut);
RenderingDiagnostics.ProbeFlapEnabled = savedFlap;
Step();
_out.WriteLine(FormattableString.Invariant(
$"orbit[{i:D2}] eye=({cam.Position.X:F6},{cam.Position.Y:F6},{cam.Position.Z:F6})"));
}
}
finally

View file

@ -1,200 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
using DatLandBlockInfo = DatReaderWriter.DBObjs.LandBlockInfo;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #95 MEASUREMENT (2026-06-13): entering the 0x0007 dungeon (Town Network) explodes
/// WB-DIAG to ~9.1M instances/frame. Suspected cause: <see cref="PortalVisibilityBuilder.Build"/>
/// floods the dungeon's portal graph WITHOUT the retail grab_visible_cells stab_list bounding
/// (decomp:311878). A dungeon cell has <c>seen_outside==0</c>; retail's PVS for it is just the
/// cell's <c>stab_list</c> (<see cref="LoadedCell.VisibleCells"/>) — typically a small bounded
/// set. If our flood instead visits ~all cells of the landblock, that is the blowup.
///
/// This is a DIAGNOSTIC, not a fix: it loads the real 0x0007 interior cells, runs the real
/// production flood from representative dungeon-cell roots, and PRINTS the ground-truth numbers —
/// flood visited-cell-set size (<see cref="PortalVisibilityFrame.OrderedVisibleCells"/>) vs the
/// root's stab_list size (<see cref="LoadedCell.VisibleCells"/>), plus how many visited cells
/// cross landblocks. The single assertion just guarantees the test ran; the VALUE is the output.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class Issue95DungeonFloodDiagnosticTests
{
private const uint TownNetwork = 0x00070000u;
private readonly ITestOutputHelper _out;
public Issue95DungeonFloodDiagnosticTests(ITestOutputHelper output) => _out = output;
// Production-ish projection (mirrors the sibling harnesses): FovY ~1.2, 1280x720,
// near 0.1, far 5000. The flood's clip is near-independent, so exactness is not
// load-bearing for cell-count measurement.
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Measure_DungeonFlood_VisibleCellCount()
{
var datDir = CornerFloodReplayTests.ResolveDatDir();
if (datDir is null)
{
_out.WriteLine("SKIP: dat dir did not resolve (ACDREAM_DAT_DIR unset and "
+ "%USERPROFILE%\\Documents\\Asheron's Call absent). No numbers measured.");
// Diagnostic test: do not hard-fail when dats are absent (matches sibling harnesses).
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");
}
_out.WriteLine($"dat dir resolved: {datDir}");
using var dats = new DatCollection(datDir, DatAccessType.Read);
// 1) LandBlockInfo header — NumCells for 0x0007.
var lbi = dats.Get<DatLandBlockInfo>(TownNetwork | 0xFFFEu);
if (lbi is null)
{
_out.WriteLine($"SKIP: LandBlockInfo 0x{TownNetwork | 0xFFFEu:X8} not found in the dat "
+ "(0x0007 may not exist in this client_cell_1.dat).");
return;
}
_out.WriteLine($"=== 0x0007 (Town Network) LandBlockInfo ===");
_out.WriteLine($"NumCells (DatLandBlockInfo.NumCells) = {lbi.NumCells}");
// 2) Load ALL interior cells (sparse ids tolerated — see LoadAllInteriorCells).
var loaded = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, TownNetwork);
_out.WriteLine($"cells actually loaded = {loaded.Count}");
Assert.True(loaded.Count > 0, "no interior cells loaded for 0x0007 — cannot measure");
Func<uint, LoadedCell?> lookup = id => loaded.TryGetValue(id, out var c) ? c : null;
// 3) Per-cell stab_list (VisibleCells) distribution across ALL loaded cells.
// This is the bounded retail PVS size we expect the flood to roughly match.
var stabSizes = loaded.Values.Select(c => c.VisibleCells.Count).ToList();
int seenOutsideCount = loaded.Values.Count(c => c.SeenOutside);
int interiorCount = loaded.Count - seenOutsideCount;
_out.WriteLine("");
_out.WriteLine("=== stab_list (LoadedCell.VisibleCells) distribution over ALL loaded cells ===");
_out.WriteLine($"cells with SeenOutside==true (entrance/exterior-facing) = {seenOutsideCount}");
_out.WriteLine($"cells with SeenOutside==false (interior dungeon) = {interiorCount}");
if (stabSizes.Count > 0)
_out.WriteLine(FormattableString.Invariant(
$"VisibleCells.Count min={stabSizes.Min()} max={stabSizes.Max()} avg={stabSizes.Average():F1} sum={stabSizes.Sum()}"));
int emptyStab = stabSizes.Count(s => s == 0);
_out.WriteLine($"cells with EMPTY stab_list (no dat PVS) = {emptyStab}");
// 4) Pick representative DUNGEON roots: the first interior (SeenOutside==false) cells in
// ascending id order. If none exist, fall back to 0x00070100 and report that.
var interiorRoots = loaded
.Where(kv => !kv.Value.SeenOutside)
.OrderBy(kv => kv.Key)
.Select(kv => kv.Value)
.Take(5)
.ToList();
if (interiorRoots.Count == 0)
{
_out.WriteLine("");
_out.WriteLine("NOTE: NO cell has SeenOutside==false (all cells see the exterior). "
+ "Falling back to root 0x00070100 for the flood measurement.");
if (loaded.TryGetValue(TownNetwork | 0x0100u, out var fallback))
interiorRoots.Add(fallback);
else
{
_out.WriteLine("WARN: 0x00070100 not loaded either; using the lowest-id loaded cell.");
interiorRoots.Add(loaded.OrderBy(kv => kv.Key).First().Value);
}
}
_out.WriteLine("");
_out.WriteLine("=== PER-ROOT FLOOD MEASUREMENT (PortalVisibilityBuilder.Build) ===");
_out.WriteLine("property read for the visited-cell set: PortalVisibilityFrame.OrderedVisibleCells");
_out.WriteLine("root | seenOut | stab(VisibleCells) | flood(OrderedVisibleCells) | crossLB | dir");
var floodSizes = new List<int>();
foreach (var root in interiorRoots)
{
// Eye at the root cell's world origin, looking toward its first portal (or +X if none),
// so the flood actually fires through an opening. Sweep all 6 axis directions and KEEP
// the maximum visited-set — the blowup is a worst-case-over-orientation quantity.
var eye = root.WorldPosition;
int bestFlood = -1;
string bestDir = "?";
int bestCrossLb = -1;
List<uint>? bestVisited = null;
// Direction candidates: toward each portal's polygon centroid (the natural look-through),
// plus the 6 cardinal axes as a fallback sweep.
var lookTargets = new List<(Vector3 target, string label)>();
for (int pi = 0; pi < root.Portals.Count && pi < root.PortalPolygons.Count; pi++)
{
var poly = root.PortalPolygons[pi];
if (poly is { Length: >= 1 })
{
var cl = Vector3.Zero;
foreach (var v in poly) cl += v;
cl /= poly.Length;
lookTargets.Add((Vector3.Transform(cl, root.WorldTransform),
$"portal{pi}->0x{root.Portals[pi].OtherCellId:X4}"));
}
}
foreach (var (d, lbl) in new (Vector3, string)[]
{
(Vector3.UnitX, "+X"), (-Vector3.UnitX, "-X"),
(Vector3.UnitY, "+Y"), (-Vector3.UnitY, "-Y"),
(Vector3.UnitZ, "+Z"), (-Vector3.UnitZ, "-Z"),
})
lookTargets.Add((eye + d * 5f, lbl));
foreach (var (target, label) in lookTargets)
{
if (Vector3.DistanceSquared(target, eye) < 1e-6f) continue;
var frame = PortalVisibilityBuilder.Build(root, eye, lookup, ViewProjFor(eye, target));
int floodN = frame.OrderedVisibleCells.Count;
if (floodN > bestFlood)
{
bestFlood = floodN;
bestDir = label;
bestVisited = frame.OrderedVisibleCells;
bestCrossLb = frame.OrderedVisibleCells.Count(id => (id & 0xFFFF0000u) != TownNetwork);
}
}
floodSizes.Add(bestFlood);
_out.WriteLine(FormattableString.Invariant(
$"0x{root.CellId:X8} | {(root.SeenOutside ? "Y" : "N"),5} | {root.VisibleCells.Count,18} | {bestFlood,26} | {bestCrossLb,7} | {bestDir}"));
// For the FIRST root, also print the actual visited set + stab set for eyeballing.
if (ReferenceEquals(root, interiorRoots[0]) && bestVisited is not null)
{
_out.WriteLine(" first-root visited (OrderedVisibleCells, low ids): "
+ string.Join(" ", bestVisited.Select(id => $"{id & 0xFFFFu:X4}")));
_out.WriteLine(" first-root stab_list (VisibleCells, low ids): "
+ string.Join(" ", root.VisibleCells.Select(id => $"{id & 0xFFFFu:X4}")));
}
}
// 5) Aggregate flood-size stats across the sampled roots — the headline numbers.
_out.WriteLine("");
_out.WriteLine("=== AGGREGATE over sampled roots ===");
if (floodSizes.Count > 0)
_out.WriteLine(FormattableString.Invariant(
$"flood visited-set size (OrderedVisibleCells): min={floodSizes.Min()} max={floodSizes.Max()} avg={floodSizes.Average():F1} (NumCells={lbi.NumCells}, loaded={loaded.Count})"));
var sampledStab = interiorRoots.Select(r => r.VisibleCells.Count).ToList();
if (sampledStab.Count > 0)
_out.WriteLine(FormattableString.Invariant(
$"sampled roots' stab_list size (VisibleCells): min={sampledStab.Min()} max={sampledStab.Max()} avg={sampledStab.Average():F1}"));
_out.WriteLine("");
_out.WriteLine("INTERPRETATION: if flood max ~= loaded.Count (visits ~all cells) while stab "
+ "is small, that is the #95 blowup — the flood is unbounded by the retail stab_list PVS.");
}
}

View file

@ -1,52 +1,21 @@
using System;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// The OUTDOOR render root (R-A2, 2026-06-08): a portal-less <see cref="LoadedCell"/> whose only job is
/// to root the render outdoors so the flood seeds OutsideView FULL-SCREEN (terrain draws). Buildings are
/// flooded SEPARATELY per-building (<see cref="PortalVisibilityBuilder.ConstructViewBuilding"/>), so the
/// node carries NO reverse portals — the pre-R-A2 reverse-portal unified flood (which oscillated at the
/// doorway) is gone. Its tests moved here; the old <c>UnifiedFloodTests</c> was retired.
/// </summary>
public class OutdoorCellNodeTests
{
[Fact]
public void Build_ReturnsPortallessOutdoorRoot()
{
uint outdoorId = 0xA9B40031;
var node = OutdoorCellNode.Build(outdoorId);
LoadedCell node = OutdoorCellNode.Build(outdoorId);
Assert.Equal(outdoorId, node.CellId);
Assert.True(node.IsOutdoorNode); // the flag PortalVisibilityBuilder keys the full-screen OutsideView on
Assert.True(node.IsOutdoorNode);
Assert.True(node.SeenOutside);
Assert.Equal(Matrix4x4.Identity, node.WorldTransform);
Assert.Empty(node.Portals); // R-A2: no reverse portals into buildings (buildings flood per-building)
}
[Fact]
public void Build_OutdoorRoot_SeedsFullScreenOutsideView_OnlyNodeVisible()
{
// The load-bearing outdoor-terrain behavior: rooting at the IsOutdoorNode node seeds OutsideView
// with the FULL-SCREEN NDC quad, so ClipFrameAssembler yields TerrainMode != Skip and
// DrawLandscapeThroughOutsideView draws terrain/sky/scenery everywhere. The portal-less node
// resolves to exactly {node} — no interior cells flooded from the root (those come per-building).
var node = OutdoorCellNode.Build(0xA9B40031);
var eye = new Vector3(0, -3, 1);
var view = Matrix4x4.CreateLookAt(eye, new Vector3(0, 5, 1), Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 16f / 9f, 1f, 5000f);
var frame = PortalVisibilityBuilder.Build(node, eye, _ => null, view * proj);
Assert.Equal(new[] { 0xA9B40031u }, frame.OrderedVisibleCells); // only the node
Assert.False(frame.OutsideView.IsEmpty);
Assert.Equal(-1f, frame.OutsideView.MinX, 3);
Assert.Equal(-1f, frame.OutsideView.MinY, 3);
Assert.Equal(1f, frame.OutsideView.MaxX, 3);
Assert.Equal(1f, frame.OutsideView.MaxY, 3);
Assert.Empty(node.Portals);
}
}

View file

@ -1,999 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public class PortalVisibilityBuilderTests
{
private static Matrix4x4 ViewProj()
{
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f);
return view * proj;
}
private static Vector3[] Quad(float cx, float cy, float halfW, float halfH, float z) => new[]
{
new Vector3(cx - halfW, cy - halfH, z), new Vector3(cx + halfW, cy - halfH, z),
new Vector3(cx + halfW, cy + halfH, z), new Vector3(cx - halfW, cy + halfH, z),
};
// Full-height (y in [-0.9, 0.9]) quad spanning [minX, maxX] in X at plane z.
// Used by the multi-back-portal fixture where the X span is the only thing
// distinguishing the LEFT and RIGHT apertures.
private static Vector3[] QuadX(float minX, float maxX, float z) => new[]
{
new Vector3(minX, -0.9f, z), new Vector3(maxX, -0.9f, z),
new Vector3(maxX, 0.9f, z), new Vector3(minX, 0.9f, z),
};
private static LoadedCell Cell(uint id, params CellPortalInfo[] portals) => new LoadedCell
{
CellId = id, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity,
Portals = new List<CellPortalInfo>(portals),
};
private static PortalVisibilityFrame Build(LoadedCell cam, Dictionary<uint, LoadedCell> all)
=> PortalVisibilityBuilder.Build(cam, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, ViewProj());
[Fact]
public void Build_ReusedFrame_NestedGraph_MatchesFreshAndStabilizesScratch()
{
var root = Cell(0x0001,
new CellPortalInfo(0x0002, 0, 0, 0),
new CellPortalInfo(0x0003, 1, 0, 0));
root.PortalPolygons.Add(QuadX(-0.8f, -0.05f, -2f));
root.PortalPolygons.Add(QuadX(0.05f, 0.8f, -3f));
var left = Cell(0x0002, new CellPortalInfo(0x0004, 0, 0, 0));
left.PortalPolygons.Add(QuadX(-0.8f, -0.05f, -5f));
var right = Cell(0x0003, new CellPortalInfo(0x0004, 0, 0, 0));
right.PortalPolygons.Add(QuadX(0.05f, 0.8f, -5f));
var exit = Cell(0x0004, new CellPortalInfo(0xFFFF, 0, 0, 0));
exit.PortalPolygons.Add(QuadX(-0.8f, 0.8f, -8f));
var cells = new Dictionary<uint, LoadedCell>
{
[root.CellId] = root,
[left.CellId] = left,
[right.CellId] = right,
[exit.CellId] = exit,
};
LoadedCell? Lookup(uint id) => cells.TryGetValue(id, out LoadedCell? cell) ? cell : null;
PortalVisibilityFrame fresh = PortalVisibilityBuilder.Build(
root, Vector3.Zero, Lookup, ViewProj());
uint[] expectedOrder = fresh.OrderedVisibleCells.ToArray();
uint[] expectedCells = fresh.CellViews.Keys.OrderBy(id => id).ToArray();
int expectedOutsidePolygons = fresh.OutsideView.Polygons.Count;
var reuse = new PortalVisibilityFrame();
PortalVisibilityBuilder.Build(root, Vector3.Zero, Lookup, ViewProj(), reuseFrame: reuse);
int scratchHighWater = reuse.ClipRegionScratchCount;
int vertexArrayHighWater = reuse.PolygonVertexAllocationCount;
Assert.True(scratchHighWater > 0);
Assert.True(vertexArrayHighWater > 0);
for (int frame = 0; frame < 16; frame++)
{
PortalVisibilityFrame actual = PortalVisibilityBuilder.Build(
root, Vector3.Zero, Lookup, ViewProj(), reuseFrame: reuse);
Assert.Same(reuse, actual);
Assert.Equal(expectedOrder, actual.OrderedVisibleCells);
Assert.Equal(expectedCells, actual.CellViews.Keys.OrderBy(id => id).ToArray());
Assert.Equal(expectedOutsidePolygons, actual.OutsideView.Polygons.Count);
Assert.Equal(scratchHighWater, reuse.ClipRegionScratchCount);
Assert.Equal(vertexArrayHighWater, reuse.PolygonVertexAllocationCount);
Assert.Empty(reuse.TodoScratch);
}
}
[Fact]
public void Build_ReusedFrame_AfterLookupException_HasNoStaleWorkOrViews()
{
var root = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
root.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -2f));
var reuse = new PortalVisibilityFrame();
Assert.Throws<InvalidOperationException>(() => PortalVisibilityBuilder.Build(
root,
Vector3.Zero,
_ => throw new InvalidOperationException("fixture lookup failure"),
ViewProj(),
reuseFrame: reuse));
var isolatedRoot = Cell(0x0010);
PortalVisibilityFrame recovered = PortalVisibilityBuilder.Build(
isolatedRoot, Vector3.Zero, _ => null, ViewProj(), reuseFrame: reuse);
Assert.Same(reuse, recovered);
Assert.Equal(new[] { isolatedRoot.CellId }, recovered.OrderedVisibleCells);
Assert.Equal(new[] { isolatedRoot.CellId }, recovered.CellViews.Keys);
Assert.Empty(recovered.CrossBuildingViews);
Assert.True(recovered.OutsideView.IsEmpty);
Assert.Empty(reuse.TodoScratch);
}
[Fact]
public void ResetForBuild_DropsPathologicalContainerHighWater_AfterIdleHysteresis()
{
const int pathologicalCount =
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity * 4;
var frame = new PortalVisibilityFrame();
var placeholder = new LoadedCell();
for (int i = 0; i < pathologicalCount; i++)
{
uint id = (uint)i + 1u;
frame.CellViews.Add(id, new CellView());
frame.CrossBuildingViews.Add(id, new CellView());
frame.OrderedVisibleCells.Add(id);
frame.QueuedScratch.Add(id);
frame.DrawListedScratch.Add(id);
frame.ProcessedViewCountsScratch.Add(id, i);
frame.TodoScratch.Add((placeholder, i));
}
Assert.True(
frame.CellViewMapCapacity
> PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.True(
frame.QueuedCapacity
> PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
frame.ResetForBuild();
Assert.Empty(frame.CellViews);
Assert.Empty(frame.CrossBuildingViews);
Assert.Empty(frame.OrderedVisibleCells);
Assert.Empty(frame.QueuedScratch);
Assert.Empty(frame.DrawListedScratch);
Assert.Empty(frame.ProcessedViewCountsScratch);
Assert.Empty(frame.TodoScratch);
Assert.InRange(
frame.RetainedCellViewCount,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.True(
frame.CellViewMapCapacity
> PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
for (int i = 0; i < PortalVisibilityFrame.CapacityTrimIdleFrames; i++)
frame.ResetForBuild();
Assert.InRange(
frame.CellViewMapCapacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.CrossBuildingViewMapCapacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.QueuedCapacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.DrawListedCapacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.ProcessedViewCountCapacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.OrderedVisibleCells.Capacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
Assert.InRange(
frame.TodoScratch.Capacity,
0,
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity);
LoadedCell root = Cell(0x1000);
PortalVisibilityFrame rebuilt = PortalVisibilityBuilder.Build(
root,
Vector3.Zero,
_ => null,
ViewProj(),
reuseFrame: frame);
Assert.Same(frame, rebuilt);
Assert.Equal(new[] { root.CellId }, rebuilt.OrderedVisibleCells);
Assert.True(rebuilt.CellViews.ContainsKey(root.CellId));
}
[Fact]
public void ResetForBuild_RecurringLargeFlood_PreservesWarmCapacity()
{
const int recurringCount =
PortalVisibilityFrame.MaxRetainedBuildCollectionCapacity + 163;
var frame = new PortalVisibilityFrame();
for (int iteration = 0;
iteration < PortalVisibilityFrame.CapacityTrimIdleFrames + 5;
iteration++)
{
for (int i = 0; i < recurringCount; i++)
{
uint id = (uint)i + 1u;
frame.QueuedScratch.Add(id);
frame.OrderedVisibleCells.Add(id);
}
int queuedCapacity = frame.QueuedCapacity;
int orderedCapacity = frame.OrderedVisibleCells.Capacity;
frame.ResetForBuild();
Assert.Equal(queuedCapacity, frame.QueuedCapacity);
Assert.Equal(orderedCapacity, frame.OrderedVisibleCells.Capacity);
}
}
[Fact]
public void Builder_Cellar_WindowClippedToStairwell_NotFullWindow()
{
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.1f, 1.0f, -3f)); // narrow stairwell
var ground = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
ground.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -6f)); // wide window
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = ground };
var frame = Build(cam, all);
Assert.False(frame.OutsideView.IsEmpty);
float outsideWidth = frame.OutsideView.MaxX - frame.OutsideView.MinX;
float windowOnlyWidth = PortalFrameTestHelper.ProjectedWidth(
new[] { new Vector3(-1, 0, -6), new Vector3(1, 0, -6) }, ViewProj());
Assert.True(outsideWidth < windowOnlyWidth * 0.5f,
$"OutsideView width {outsideWidth} should be a sliver, far less than full window {windowOnlyWidth}");
}
[Fact]
public void Build_IsDeterministic_IdenticalInputsGiveIdenticalVisibleSet()
{
// Flap root-cause apparatus (2026-06-07): the live threshold flap shows OrderedVisibleCells
// flipping 2<->6 at an eye+player identical to cm. Build is a pure static function with
// all-fresh per-call state, so identical inputs MUST yield an identical visible set. If this
// FAILS, the flap is a determinism bug INSIDE Build; if it PASSES (expected), the live flip is
// sub-cm INPUT jitter and the fix must make membership robust, not Build deterministic.
// Exercises the re-enqueue fixpoint via a diamond: 0x0004 is reached from BOTH 0x0002 and 0x0003.
var cam = Cell(0x0001,
new CellPortalInfo(0x0002, 0, 0, 0),
new CellPortalInfo(0x0003, 1, 0, 0));
cam.PortalPolygons.Add(QuadX(-0.6f, -0.05f, -3f)); // left -> 0002
cam.PortalPolygons.Add(QuadX(0.05f, 0.6f, -3f)); // right -> 0003
var left = Cell(0x0002, new CellPortalInfo(0x0004, 0, 0, 0));
left.PortalPolygons.Add(QuadX(-0.6f, -0.05f, -6f));
var right = Cell(0x0003, new CellPortalInfo(0x0004, 0, 0, 0));
right.PortalPolygons.Add(QuadX(0.05f, 0.6f, -6f));
var back = Cell(0x0004, new CellPortalInfo(0xFFFF, 0, 0, 0));
back.PortalPolygons.Add(QuadX(-0.6f, 0.6f, -9f));
var all = new Dictionary<uint, LoadedCell>
{ [0x0001] = cam, [0x0002] = left, [0x0003] = right, [0x0004] = back };
var a = Build(cam, all);
var b = Build(cam, all);
Assert.Equal(a.OrderedVisibleCells, b.OrderedVisibleCells);
Assert.Equal(
a.CellViews.Keys.OrderBy(k => k).ToArray(),
b.CellViews.Keys.OrderBy(k => k).ToArray());
}
[Fact]
public void Build_EyeStandingInInteriorPortal_FloodsNeighbour()
{
// R1 void fix (2026-06-05): when the chase camera roots in a thin doorway cell and the eye is
// STANDING IN an interior portal opening, the live capture showed the vestibule->room portal at
// D=0.16 m projecting to 0 verts (proj=0), so the neighbour was wrongly culled (cells=1) and
// only the thin cell drew -> bluish void. Retail's 3D portal clip imposes no constraint for a
// portal the eye is inside, so the neighbour is fully visible. The flood MUST reach the neighbour.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.3f, 0.3f, -0.03f)); // opening 3 cm in front — eye standing in it
var room = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
room.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = room };
var frame = Build(cam, all);
Assert.True(frame.CellViews.ContainsKey(0x0002),
"eye standing in the doorway must flood the neighbour (degenerate projection was culling it -> void)");
Assert.Contains(0x0002u, frame.OrderedVisibleCells);
}
[Fact]
public void Build_DegeneratePortalToTheSide_NotFlooded_NoOverInclusion()
{
// Guard against the fix over-flooding: a portal whose opening the eye is NOT standing in (3 cm
// in front but 2 m to the SIDE) also projects degenerate, but the eye is OUTSIDE the opening, so
// it must stay culled — otherwise the eye-in-doorway fix would blow up the visible set (#95) by
// flooding every degenerate-projecting portal regardless of where the eye actually is.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(2.0f, 0f, 0.3f, 0.3f, -0.03f)); // 2 m to the side — eye NOT in it
var room = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
room.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = room };
var frame = Build(cam, all);
Assert.False(frame.CellViews.ContainsKey(0x0002),
"a degenerate portal the eye is NOT standing in must stay culled (no over-inclusion / #95 blowup)");
}
[Fact]
public void Build_PortalFullyBehindEye_NotFlooded_RetailEmptyClipRule()
{
// W=0 port (2026-06-11): a portal ENTIRELY behind the eye clips to empty in retail
// polyClipFinish (every vertex w < 0 -> <3 survivors -> reject), so the flood does not
// reach its neighbour — the cell is off-screen and drawing nothing through it is correct.
//
// HISTORY: this test used to assert the OPPOSITE (rescue-era pin from a 2026-06-06 cellar
// capture, "0174->0175 must flood at 1.4 m behind the camera"). That pinned the
// EyeInsidePortalOpening rescue — the documented compensation for ProjectToClip's old
// EyePlaneW=1e-4 divergence — not retail. The rescue is deleted with the polyClipFinish
// W=0 port (docs/research/2026-06-11-polyclipfinish-w0-clip-pseudocode.md); the live
// cellar behaviors are re-verified by the dat-backed replay harnesses + the visual gate.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.35f, 0.35f, 1.4f)); // entirely behind the eye
var stairs = Cell(0x0002);
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = stairs };
var vp = ViewProj();
Assert.True(PortalProjection.ProjectToClip(cam.PortalPolygons[0], Matrix4x4.Identity, vp).Length < 3,
"a fully-behind portal must clip to empty (polyClipFinish part 1)");
var frame = PortalVisibilityBuilder.Build(
cam, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, vp);
Assert.DoesNotContain(0x0002u, frame.OrderedVisibleCells);
}
[Fact]
public void Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit()
{
// Retail PView tracks update_count vs view_count. If B is processed through a LEFT slice, then
// a later path reaches B through a RIGHT slice, B must propagate that new RIGHT slice to its
// exit portal. Enqueue-once builders flap here: OutsideView stays empty until the camera moves
// enough to discover B in the other order.
const uint A = 0x0001, B = 0x0002, D = 0x0003;
var a = Cell(A,
new CellPortalInfo((ushort)B, PolygonId: 0, Flags: 0, OtherPortalId: 0),
new CellPortalInfo((ushort)D, PolygonId: 1, Flags: 0, OtherPortalId: 0));
a.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f)); // nearer LEFT path to B
a.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f)); // farther RIGHT path to D
var b = Cell(B,
new CellPortalInfo((ushort)A, PolygonId: 0, Flags: 0, OtherPortalId: 0),
new CellPortalInfo(0xFFFF, PolygonId: 1, Flags: 0, OtherPortalId: 0),
new CellPortalInfo((ushort)D, PolygonId: 2, Flags: 0, OtherPortalId: 0));
b.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f)); // reciprocal LEFT back to A
b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f)); // RIGHT exit, invisible from LEFT slice
b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f)); // reciprocal RIGHT back to D
var d = Cell(D, new CellPortalInfo((ushort)B, PolygonId: 0, Flags: 0, OtherPortalId: 2));
d.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f)); // later RIGHT path into B
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b, [D] = d };
var frame = Build(a, all);
Assert.Contains(B, frame.OrderedVisibleCells);
Assert.Contains(D, frame.OrderedVisibleCells);
Assert.False(frame.OutsideView.IsEmpty);
}
[Fact]
public void Build_ViewGrowthAfterDoneCell_RePopsGrownCell()
{
// Same A->B(near LEFT) + A->D(far RIGHT) + D->B(later) topology as
// Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit: B is popped via LEFT, then D grows B
// through RIGHT after B is done -> B re-pops. This is retail-faithful late growth (kept by the fix).
const uint A = 0x0001, B = 0x0002, D = 0x0003;
var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0), new CellPortalInfo((ushort)D, 1, 0, 0));
a.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f));
a.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
var b = Cell(B, new CellPortalInfo((ushort)A, 0, 0, 0),
new CellPortalInfo(0xFFFF, 1, 0, 0), new CellPortalInfo((ushort)D, 2, 0, 0));
b.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -2f));
b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f));
b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
var d = Cell(D, new CellPortalInfo((ushort)B, 0, 0, 2));
d.PortalPolygons.Add(QuadX(0.3f, 0.9f, -5f));
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b, [D] = d };
var frame = Build(a, all);
// Membership (the flap-relevant output) is each cell once, regardless of re-pops.
Assert.Equal(new[] { B }, frame.OrderedVisibleCells.Where(c => c == B).ToArray());
Assert.Contains(D, frame.OrderedVisibleCells);
}
[Fact]
public void Builder_SealedCellar_NoExitPortal_OutsideViewEmpty()
{
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0, 0, 0.1f, 1f, -3f));
var inner = Cell(0x0002); // no portals at all
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = inner };
Assert.True(Build(cam, all).OutsideView.IsEmpty);
}
[Fact]
public void Builder_CameraCellWithDirectExit_OutsideViewIsFullWindow()
{
var cam = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0, 0, 1f, 1f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam };
var frame = Build(cam, all);
Assert.False(frame.OutsideView.IsEmpty);
Assert.True(frame.OutsideView.MaxX - frame.OutsideView.MinX > 0.3f);
}
[Fact]
public void Builder_BackFacingPortal_NotTraversed()
{
// Portal to 0x0002, but its clip plane puts the camera (origin) on the OUTSIDE.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0, 0, 0.5f, 0.5f, -3f));
cam.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, 0, 1), D = -1f, InsideSide = 0 });
// dot = (0,0,1)·origin + (-1) = -1 < 0; InsideSide==0 requires dot >= -eps → camera OUTSIDE → skip.
var ground = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
ground.PortalPolygons.Add(Quad(0, 0, 1f, 1f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = ground };
var frame = Build(cam, all);
Assert.False(frame.CellViews.ContainsKey(0x0002)); // neighbour never reached
Assert.True(frame.OutsideView.IsEmpty); // its window never marked
}
[Fact]
public void Build_BackFacingPortal_EyeStandingInOpening_StillCulled()
{
// R-A2b (Option B1, the flap fix): retail PView::InitCell (:432962) culls a back-facing portal by
// the side test REGARDLESS of eye proximity — there is NO eye-in-opening bypass. The live pin
// (flap-sidechk.log, 2026-06-09) showed the back portal 0173->0171 with camInterior=False but
// eyeIn=True (eye within 1.75 m of the shared doorway); the OLD `&& !eyeInsideOpening` side-cull
// bypass let it through -> the 0171<->0173 flood cycle -> re-enqueue churn -> the doorway flap.
// A back-facing portal the eye is STANDING IN must stay culled (the forward-portal clip-empty void
// rescue, tested by Build_EyeStandingInInteriorPortal_FloodsNeighbour, is a separate path and stays).
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -1f)); // 1 m in front -> eyeInsideOpening = True
// ClipPlane puts the eye on the EXIT side (camInterior = False), like the back portal of a doorway
// just crossed: Normal·origin + D = 1 > 0, InsideSide==1 wants dot<=eps -> not interior.
cam.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, 0, 1), D = 1f, InsideSide = 1 });
var ground = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
ground.PortalPolygons.Add(Quad(0f, 0f, 1f, 1f, -6f));
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = ground };
var frame = Build(cam, all);
Assert.False(frame.CellViews.ContainsKey(0x0002),
"a back-facing portal (camInterior=False) must stay culled even when the eye is standing in its " +
"opening (eyeInsideOpening=True) — retail's side test has no bypass; the bypass WAS the flap cycle");
Assert.DoesNotContain(0x0002u, frame.OrderedVisibleCells);
}
[Fact]
public void Builder_CwWoundExitPortal_OutsideRegionIsCcw()
{
// Exit portal authored CLOCKWISE — the builder must normalize to CCW so downstream stays valid.
var cwQuad = new[]
{
new Vector3(-1, -1, -6), new Vector3(-1, 1, -6), new Vector3(1, 1, -6), new Vector3(1, -1, -6),
};
var cam = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
cam.PortalPolygons.Add(cwQuad);
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam };
var frame = Build(cam, all);
Assert.False(frame.OutsideView.IsEmpty);
var p = frame.OutsideView.Polygons[0].Vertices;
float area2 = 0f;
for (int i = 0; i < p.Length; i++) { var a = p[i]; var b = p[(i + 1) % p.Length]; area2 += a.X * b.Y - b.X * a.Y; }
Assert.True(area2 > 0f, "clipped OutsideView region should be CCW after winding normalization");
}
[Fact]
public void Builder_CyclicGraph_TerminatesWithBoundedPolys()
{
// A <-> B cycle; B also has an exit window. Must terminate and not blow up.
var a = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -3f));
var b = Cell(0x0002, new CellPortalInfo(0x0001, 0, 0, 0), new CellPortalInfo(0xFFFF, 1, 0, 0));
b.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -2f)); // back to A
b.PortalPolygons.Add(Quad(0f, 0f, 1.0f, 1.0f, -6f)); // exit window
var all = new Dictionary<uint, LoadedCell> { [0x0001] = a, [0x0002] = b };
var frame = Build(a, all); // must return (no infinite loop)
Assert.False(frame.OutsideView.IsEmpty);
Assert.True(frame.OutsideView.Polygons.Count < 256,
$"OutsideView poly count {frame.OutsideView.Polygons.Count} — termination/dedup regression guard");
}
// -----------------------------------------------------------------------
// Phase U.2a: ordered visible-cell list (closest-first) + grow-watermark
// fixpoint termination (replaces MaxReprocessPerCell hard cap).
// -----------------------------------------------------------------------
// Straight chain A -> B -> C, camera in A looking down -Z. Each onward portal
// is progressively farther in -Z so the camera-to-portal distance is monotonic,
// forcing the priority queue to dequeue A, then B, then C in that order.
private static (LoadedCell[] cells, Dictionary<uint, LoadedCell> lookup) SyntheticChain()
{
const uint A = 0x0001, B = 0x0002, C = 0x0003;
var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -2f)); // portal A->B at z=-2 (nearer)
var b = Cell(B, new CellPortalInfo((ushort)C, 0, 0, 0));
b.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -5f)); // portal B->C at z=-5 (farther)
var c = Cell(C, new CellPortalInfo(0xFFFF, 0, 0, 0));
c.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -8f)); // exit window
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b, [C] = c };
return (new[] { a, b, c }, all);
}
[Fact] // closest-first ordering
public void Build_OrdersVisibleCells_ClosestFirst()
{
var (cells, lookup) = SyntheticChain();
var f = PortalVisibilityBuilder.Build(
cells[0], Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.Equal(new uint[] { 0x0001, 0x0002, 0x0003 }, f.OrderedVisibleCells.ToArray());
}
// Hub cell with 4 rooms, each room portal-linked BACK to the hub (a cycle on
// every spoke). A naive FIFO with no real fixpoint re-enqueues the hub once per
// returning spoke and the rooms once per hub re-process — the watermark must
// converge instead, bounding the visible set to {hub + 4 rooms} with no dupes.
private static (LoadedCell hub, Dictionary<uint, LoadedCell> lookup) SyntheticCyclicHub()
{
const uint HUB = 0x0010;
uint[] rooms = { 0x0011, 0x0012, 0x0013, 0x0014 };
// Hub has one portal to each room; rooms sit at distinct depths so ordering is deterministic.
var hub = Cell(HUB,
new CellPortalInfo((ushort)rooms[0], 0, 0, 0), new CellPortalInfo((ushort)rooms[1], 1, 0, 0),
new CellPortalInfo((ushort)rooms[2], 2, 0, 0), new CellPortalInfo((ushort)rooms[3], 3, 0, 0));
for (int i = 0; i < 4; i++)
hub.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -2f - i)); // -2,-3,-4,-5
var all = new Dictionary<uint, LoadedCell> { [HUB] = hub };
for (int i = 0; i < 4; i++)
{
var room = Cell(rooms[i], new CellPortalInfo((ushort)HUB, 0, 0, 0)); // links back to hub → cycle
room.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -2f - i));
all[rooms[i]] = room;
}
return (hub, all);
}
[Fact] // cyclic graph terminates and bounds the visible set
public void Build_CyclicHub_TerminatesAndBounds()
{
var (hub, lookup) = SyntheticCyclicHub();
var f = PortalVisibilityBuilder.Build(
hub, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.True(f.OrderedVisibleCells.Count <= 5,
$"hub + 4 rooms expected, got {f.OrderedVisibleCells.Count} — fixpoint failed to converge");
Assert.Equal(f.OrderedVisibleCells.Count, f.OrderedVisibleCells.Distinct().Count()); // no dup cells
}
// -----------------------------------------------------------------------
// Phase U.2b: reciprocal OtherPortalClip (retail PView::OtherPortalClip
// decomp:433524). When a portal leads to a loaded neighbour, the
// propagated region must ALSO be clipped against the neighbour's matching
// (reciprocal) back-portal polygon — the result is the intersection of the
// opening seen from BOTH sides. This can only tighten, never widen.
// -----------------------------------------------------------------------
// Camera in A looking down -Z through a WIDE near-side portal (A->B). B's
// matching back-portal (B->A) is a NARROW opening at the same plane, so the
// reciprocal opening projects to a strictly smaller NDC region. Without the
// reciprocal clip, B's CellView equals the wide near-side projection; with
// it, B's CellView is bounded by the narrow reciprocal opening.
private static (LoadedCell camCell, LoadedCell neighbour, Dictionary<uint, LoadedCell> lookup)
SyntheticReciprocalPair()
{
const uint A = 0x0001, B = 0x0002;
// A's portal into B: wide opening (half-width 0.9) at z = -3. Its
// reciprocal back-portal lives at index 0 in B (OtherPortalId = 0).
var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.9f, 0.9f, -3f));
// B's reciprocal portal back to A: NARROW opening (half-width 0.3),
// same height and plane, so it projects fully inside the near-side rect
// but covers only ~1/3 of its width.
var b = Cell(B, new CellPortalInfo((ushort)A, 0, 0, 0));
b.PortalPolygons.Add(Quad(0f, 0f, 0.3f, 0.9f, -3f));
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b };
return (a, b, all);
}
[Fact]
public void Build_AppliesReciprocalOtherPortalClip()
{
var (camCell, neighbour, lookup) = SyntheticReciprocalPair();
var vp = ViewProj();
var f = PortalVisibilityBuilder.Build(
camCell, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, vp);
Assert.True(f.CellViews.ContainsKey(0x0002), "neighbour cell view should be populated");
// The reciprocal opening's area in NDC: project B's narrow back-portal
// polygon and take its (CCW-magnitude) shoelace area. This is the
// tightest the neighbour region can be — the propagated region must not
// exceed it once both-sides clipping is applied.
float reciprocalArea = ProjectedPolygonArea(neighbour.PortalPolygons[0], vp);
float area = CellViewArea(f.CellViews[0x0002]);
const float eps = 1e-4f;
Assert.True(area <= reciprocalArea + eps,
$"neighbour CellView area {area} must be clipped to the narrower reciprocal opening " +
$"{reciprocalArea} (without OtherPortalClip it equals the WIDE near-side projection)");
// Falsifiability guard: the near-side projection is genuinely WIDER than
// the reciprocal, so a no-op clip would have failed the assertion above.
float nearSideArea = ProjectedPolygonArea(camCell.PortalPolygons[0], vp);
Assert.True(nearSideArea > reciprocalArea * 1.5f,
$"fixture sanity: near-side area {nearSideArea} must dominate reciprocal {reciprocalArea}");
}
[Fact]
public void Build_ReciprocalClip_DegradesGracefully_WhenNoBackPortal()
{
// Same wide A->B opening, but B has NO portal pointing back to A (data gap).
// The reciprocal clip must no-op, so B's CellView equals the WIDE near-side
// projection — proving degradation never under-includes (and never throws).
const uint A = 0x0001, B = 0x0002;
var a = Cell(A, new CellPortalInfo((ushort)B, 0, 0, 0));
a.PortalPolygons.Add(Quad(0f, 0f, 0.9f, 0.9f, -3f));
var b = Cell(B); // no portals at all → no back-portal to match
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b };
var vp = ViewProj();
var f = PortalVisibilityBuilder.Build(
a, Vector3.Zero, id => all.TryGetValue(id, out var c) ? c : null, vp);
Assert.True(f.CellViews.ContainsKey(B), "neighbour must still be reached when no back-portal exists");
float nearSideArea = ProjectedPolygonArea(a.PortalPolygons[0], vp);
float area = CellViewArea(f.CellViews[B]);
Assert.True(System.MathF.Abs(area - nearSideArea) < 1e-3f,
$"with no reciprocal portal the region must equal the near-side projection " +
$"(got {area}, near-side {nearSideArea}) — degrade must not tighten or expand");
}
// -----------------------------------------------------------------------
// Phase U.2b CRITICAL — multiple portals to the SAME neighbour must each
// resolve their OWN reciprocal back-portal (retail arg2->other_portal_id,
// decomp:433557), NOT the first OtherCellId match. This is the real
// Holtburg-cellar shape: cell 0x148 has two portals to 0x149 (poly 40, 41)
// and 0x149 has two reciprocals back to 0x148. A scan-by-first-match clips
// BOTH near-side openings against the FIRST reciprocal — if the apertures
// are disjoint, the second opening's intersection underflows to empty and
// its geometry is HIDDEN (under-inclusion). Direct-index resolution clips
// each opening against the matching reciprocal, so both survive.
// -----------------------------------------------------------------------
// Camera in A looking down -Z. A has TWO portals to the SAME neighbour B,
// with DISJOINT openings: a LEFT aperture (x in [-0.9,-0.3]) and a RIGHT
// aperture (x in [0.3,0.9]). B has two reciprocals back to A, one matching
// each side. A.Portals[0].OtherPortalId = 0 (B's LEFT reciprocal),
// A.Portals[1].OtherPortalId = 1 (B's RIGHT reciprocal).
//
// Scan-by-first-match resolves BOTH A-portals to B.Portals[0] (LEFT),
// so the RIGHT near-side region (x>0) intersects an x<0 reciprocal → empty
// → B's CellView never reaches the RIGHT aperture. Direct-index keeps it.
private static (LoadedCell camCell, LoadedCell neighbour, Dictionary<uint, LoadedCell> lookup)
SyntheticMultiBackPortalPair()
{
const uint A = 0x0001, B = 0x0002;
// A: portal[0] → B through the LEFT opening, portal[1] → B through the RIGHT opening.
// The OtherPortalId back-links pick the matching reciprocal in B (0 = LEFT, 1 = RIGHT).
var a = Cell(A,
new CellPortalInfo((ushort)B, PolygonId: 0, Flags: 0, OtherPortalId: 0), // LEFT → B recip[0]
new CellPortalInfo((ushort)B, PolygonId: 1, Flags: 0, OtherPortalId: 1)); // RIGHT → B recip[1]
a.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -3f)); // LEFT near-side aperture
a.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f)); // RIGHT near-side aperture
// B: two reciprocals back to A. Index 0 covers the LEFT opening, index 1
// the RIGHT opening — disjoint, same plane (z=-3) as the near side so each
// reciprocal exactly overlaps its matching A aperture.
var b = Cell(B,
new CellPortalInfo((ushort)A, PolygonId: 0, Flags: 0, OtherPortalId: 0),
new CellPortalInfo((ushort)A, PolygonId: 1, Flags: 0, OtherPortalId: 1));
b.PortalPolygons.Add(QuadX(-0.9f, -0.3f, -3f)); // reciprocal[0] = LEFT
b.PortalPolygons.Add(QuadX(0.3f, 0.9f, -3f)); // reciprocal[1] = RIGHT
var all = new Dictionary<uint, LoadedCell> { [A] = a, [B] = b };
return (a, b, all);
}
[Fact]
public void Build_MultiplePortalsToSameNeighbour_EachResolvesOwnReciprocal()
{
var (camCell, neighbour, lookup) = SyntheticMultiBackPortalPair();
var vp = ViewProj();
var f = PortalVisibilityBuilder.Build(
camCell, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, vp);
Assert.True(f.CellViews.ContainsKey(0x0002), "neighbour cell view should be populated");
// The RIGHT reciprocal opening (B.PortalPolygons[1]) projects to a region
// with strictly positive NDC X. Geometry visible through the SECOND opening
// survives ONLY if A.Portals[1] was clipped against B's RIGHT reciprocal
// (index 1) rather than the LEFT one (index 0).
var rightNdc = PortalProjection.ProjectToNdc(neighbour.PortalPolygons[1], Matrix4x4.Identity, vp);
float rightMinX = float.MaxValue, rightMaxX = float.MinValue;
foreach (var v in rightNdc) { if (v.X < rightMinX) rightMinX = v.X; if (v.X > rightMaxX) rightMaxX = v.X; }
Assert.True(rightMinX > 0f, $"fixture sanity: RIGHT reciprocal must project to positive NDC X (got minX {rightMinX})");
// The neighbour CellView must extend into the RIGHT aperture. With the
// scan-by-first-match bug both near-side openings clip against the LEFT
// reciprocal (x<0), so the CellView's MaxX stays well left of rightMinX
// and this assertion FAILS (the RIGHT opening's geometry is hidden).
var bView = f.CellViews[0x0002];
Assert.True(bView.MaxX >= rightMinX - 1e-4f,
$"neighbour CellView MaxX {bView.MaxX} must reach the RIGHT reciprocal opening " +
$"[{rightMinX}, {rightMaxX}] — under scan-by-first-match the second opening is clipped " +
$"against the LEFT reciprocal and HIDDEN (under-inclusion bug #102 M-4).");
// And the LEFT aperture must still be present (the first opening was never
// in question) — guards against a fix that accidentally drops the LEFT side.
var leftNdc = PortalProjection.ProjectToNdc(neighbour.PortalPolygons[0], Matrix4x4.Identity, vp);
float leftMinX = float.MaxValue;
foreach (var v in leftNdc) if (v.X < leftMinX) leftMinX = v.X;
Assert.True(bView.MinX <= leftMinX + 1e-4f,
$"neighbour CellView MinX {bView.MinX} must still cover the LEFT reciprocal opening (minX {leftMinX})");
}
// -----------------------------------------------------------------------
// Phase U.4c: an earlier synthetic flap test (Build_NearBoundaryIntermediatePortal)
// lived here. It modeled the doorway flap as a BUILDER side-test cull dropping the
// exit cell. The live ACDREAM_PROBE_FLAP capture (2026-05-31) DISPROVED that model:
// the real cause is the visibility ROOT being driven by the 3rd-person camera EYE —
// the eye drifts out of the player's cell, FindCameraCell returns a STALE cell for
// its grace frames, and the doorway is then culled as "behind" the eye. The fix is
// at the GameWindow integration level (root visibility at the PLAYER's cell:
// visRootPos), NOT in the builder — the builder's side test is correct and unchanged,
// so the old test asserted a non-bug and was removed rather than left red. The fix is
// validated by the visual gate + the [flap] probe (RenderingDiagnostics.ProbeFlapEnabled);
// see docs/research/2026-05-31-u4c-flap-characterization.md.
// -----------------------------------------------------------------------
// -----------------------------------------------------------------------
// Phase W Stage 4/5: BFS exit-portal + root-cell ordering guarantees
// -----------------------------------------------------------------------
[Fact]
public void Build_ExitPortalVisible_OutsideViewNonEmpty()
{
// A cell with a single exit portal (OtherCellId == 0xFFFF) whose polygon
// projects in front of the camera. The BFS must union the clipped region
// into OutsideView, making it non-empty.
var cam = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -4f)); // in front, in frustum
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam };
var frame = Build(cam, all);
Assert.True(frame.OutsideView.Polygons.Count > 0,
"An exit portal (OtherCellId=0xFFFF) in front of the camera must populate OutsideView");
}
[Fact]
public void Build_NoExitPortal_OutsideViewEmpty()
{
// A cell whose only portal leads to another interior cell (OtherCellId != 0xFFFF).
// With no exit portal in any reachable cell the OutsideView must remain empty.
var cam = Cell(0x0001, new CellPortalInfo(0x0002, 0, 0, 0));
cam.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -3f));
var inner = Cell(0x0002); // no portals — no exit
var all = new Dictionary<uint, LoadedCell> { [0x0001] = cam, [0x0002] = inner };
var frame = Build(cam, all);
Assert.True(frame.OutsideView.Polygons.Count == 0,
"No exit portal in any reachable cell must leave OutsideView empty");
}
[Fact]
public void BuildFromExterior_SeedsInteriorCellThroughOutsidePortal()
{
var room = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
room.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -4f));
room.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = 3f,
InsideSide = 1,
});
var frame = PortalVisibilityBuilder.BuildFromExterior(
new[] { room },
Vector3.Zero,
id => id == room.CellId ? room : null,
ViewProj());
Assert.Contains(room.CellId, frame.OrderedVisibleCells);
Assert.True(frame.CellViews.TryGetValue(room.CellId, out var view));
Assert.False(view!.IsEmpty);
Assert.True(view.MaxX - view.MinX < 1.0f,
"exterior seed should be clipped to the door opening, not full-screen");
}
[Fact]
public void BuildFromExterior_ReusedFrame_ClearsPriorOutputAndProducesSameResult()
{
var room = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
room.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -4f));
room.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = 3f,
InsideSide = 1,
});
var reuse = new PortalVisibilityFrame();
LoadedCell? Lookup(uint id) => id == room.CellId ? room : null;
var first = PortalVisibilityBuilder.BuildFromExterior(
new[] { room }, Vector3.Zero, Lookup, ViewProj(), reuseFrame: reuse);
Assert.Same(reuse, first);
Assert.Equal(new[] { room.CellId }, first.OrderedVisibleCells);
var empty = PortalVisibilityBuilder.BuildFromExterior(
Array.Empty<LoadedCell>(), Vector3.Zero, Lookup, ViewProj(), reuseFrame: reuse);
Assert.Same(reuse, empty);
Assert.Empty(empty.OrderedVisibleCells);
Assert.Empty(empty.CellViews);
Assert.True(empty.OutsideView.IsEmpty);
var second = PortalVisibilityBuilder.BuildFromExterior(
new[] { room }, Vector3.Zero, Lookup, ViewProj(), reuseFrame: reuse);
Assert.Same(reuse, second);
Assert.Equal(new[] { room.CellId }, second.OrderedVisibleCells);
Assert.True(second.CellViews.TryGetValue(room.CellId, out CellView? view));
Assert.False(view!.IsEmpty);
}
[Fact]
public void BuildFromExterior_DoesNotSeedWhenCameraIsOnInteriorSide()
{
var room = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
room.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -4f));
room.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = -1f,
InsideSide = 1,
});
var frame = PortalVisibilityBuilder.BuildFromExterior(
new[] { room },
Vector3.Zero,
id => id == room.CellId ? room : null,
ViewProj());
Assert.Empty(frame.OrderedVisibleCells);
Assert.Empty(frame.CellViews);
}
[Fact]
public void BuildFromExterior_TraversesDeeperInteriorPortals()
{
var entry = Cell(0x0001,
new CellPortalInfo(0xFFFF, 0, 0, 0),
new CellPortalInfo(0x0002, 1, 0, 0));
entry.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -3f));
entry.PortalPolygons.Add(Quad(0f, 0f, 0.35f, 0.35f, -5f));
entry.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = 2f,
InsideSide = 1,
});
var backRoom = Cell(0x0002);
var all = new Dictionary<uint, LoadedCell>
{
[entry.CellId] = entry,
[backRoom.CellId] = backRoom,
};
var frame = PortalVisibilityBuilder.BuildFromExterior(
new[] { entry },
Vector3.Zero,
id => all.TryGetValue(id, out var c) ? c : null,
ViewProj());
Assert.Equal(new uint[] { 0x0001, 0x0002 }, frame.OrderedVisibleCells.ToArray());
Assert.True(frame.CellViews.ContainsKey(0x0002));
}
[Fact]
public void BuildFromExterior_MaxSeedDistanceSkipsDistantExitPortal()
{
var nearby = Cell(0x0001, new CellPortalInfo(0xFFFF, 0, 0, 0));
nearby.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -4f));
nearby.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = 3f,
InsideSide = 1,
});
var distant = Cell(0x0002, new CellPortalInfo(0xFFFF, 0, 0, 0));
distant.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -200f));
distant.ClipPlanes.Add(new PortalClipPlane
{
Normal = new Vector3(0f, 0f, 1f),
D = 199f,
InsideSide = 1,
});
var frame = PortalVisibilityBuilder.BuildFromExterior(
new[] { nearby, distant },
Vector3.Zero,
id => id == nearby.CellId ? nearby : id == distant.CellId ? distant : null,
ViewProj(),
maxSeedDistance: 48f);
Assert.Contains(nearby.CellId, frame.OrderedVisibleCells);
Assert.DoesNotContain(distant.CellId, frame.OrderedVisibleCells);
}
[Fact]
public void Build_RootCellAlwaysFirstInOrderedVisibleCells()
{
// The camera cell seeds the BFS at distance 0 so it must always pop
// first → OrderedVisibleCells[0] == cameraCell.CellId.
// Use the three-cell chain: A→B→C with exit window on C.
var (cells, lookup) = SyntheticChain();
var f = PortalVisibilityBuilder.Build(
cells[0], Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.NotEmpty(f.OrderedVisibleCells);
Assert.Equal(cells[0].CellId, f.OrderedVisibleCells[0]);
}
// Note: Build_CyclicPortalGraph_Terminates is already covered by
// Builder_CyclicGraph_TerminatesWithBoundedPolys and Build_CyclicHub_TerminatesAndBounds.
// The dedup invariant (each cell appears at most once) is validated by
// Build_CyclicHub_TerminatesAndBounds via OrderedVisibleCells.Distinct().Count().
// Signed-area-magnitude (shoelace) sum over a CellView's polygons in NDC.
private static float CellViewArea(CellView view)
{
float total = 0f;
foreach (var poly in view.Polygons) total += ShoelaceAbs(poly.Vertices);
return total;
}
// Project a cell-local polygon (identity world transform in these fixtures)
// to NDC via the same path the builder uses, then take its area magnitude.
private static float ProjectedPolygonArea(Vector3[] localPoly, Matrix4x4 vp)
{
var ndc = PortalProjection.ProjectToNdc(localPoly, Matrix4x4.Identity, vp);
return ShoelaceAbs(ndc);
}
private static float ShoelaceAbs(Vector2[] poly)
{
if (poly == null || poly.Length < 3) return 0f;
float area2 = 0f;
for (int i = 0; i < poly.Length; i++)
{
var p = poly[i];
var q = poly[(i + 1) % poly.Length];
area2 += p.X * q.Y - q.X * p.Y;
}
return System.MathF.Abs(area2) * 0.5f;
}
}
internal static class PortalFrameTestHelper
{
public static float ProjectedWidth(Vector3[] worldSeg, Matrix4x4 vp)
{
var a = Vector4.Transform(new Vector4(worldSeg[0], 1f), vp);
var b = Vector4.Transform(new Vector4(worldSeg[1], 1f), vp);
return System.MathF.Abs(a.X / a.W - b.X / b.W);
}
}

View file

@ -1,96 +0,0 @@
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// R-A2 conformance: the per-building flood (<see cref="PortalVisibilityBuilder.ConstructViewBuilding"/>)
/// is the robustness mechanism for the doorway flap. These are SOUNDNESS gates against the measured
/// retail values (handoff 2026-06-08 OPTION-A §3.4): a per-building flood touches ≈2 cells and its
/// membership is stable under the eye's ~36 µm rest jitter. They prove the per-building path is built
/// correctly and does not regress; the live doorway is the visual acceptance test for "the flap is
/// gone" (a synthetic fixture cannot reproduce the cottage's exact knife-edge geometry).
/// </summary>
public class PortalVisibilityRobustnessTests
{
private static Matrix4x4 ViewProj()
{
var view = Matrix4x4.CreateLookAt(Vector3.Zero, new Vector3(0, 0, -1), Vector3.UnitY);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1.0f, 0.1f, 1000f);
return view * proj;
}
private static Vector3[] Quad(float cx, float cy, float halfW, float halfH, float z) => new[]
{
new Vector3(cx - halfW, cy - halfH, z), new Vector3(cx + halfW, cy - halfH, z),
new Vector3(cx + halfW, cy + halfH, z), new Vector3(cx - halfW, cy + halfH, z),
};
private static LoadedCell Cell(uint id, params CellPortalInfo[] portals) => new LoadedCell
{
CellId = id, WorldTransform = Matrix4x4.Identity, InverseWorldTransform = Matrix4x4.Identity,
Portals = new List<CellPortalInfo>(portals),
};
// A realistic 2-cell building viewed from OUTSIDE: a vestibule (0x0170) with an entrance portal to
// the outdoors plus an interior portal to a back room (0x0171). The entrance clip plane puts the
// origin eye on the EXTERIOR side (D=3, InsideSide=1 → dot=3 > ε), so the exterior seed fires —
// exactly the BuildFromExterior seeding contract (mirrors the existing BuildFromExterior_* fixtures).
private static (LoadedCell[] cells, Dictionary<uint, LoadedCell> lookup) TwoCellBuilding()
{
const uint VEST = 0x0170, ROOM = 0x0171;
var vest = Cell(VEST,
new CellPortalInfo(0xFFFF, PolygonId: 0, Flags: 0, OtherPortalId: 0), // entrance to outdoors
new CellPortalInfo((ushort)ROOM, PolygonId: 1, Flags: 0, OtherPortalId: 0)); // interior to room
vest.PortalPolygons.Add(Quad(0f, 0f, 0.5f, 0.5f, -2f)); // entrance opening, in front of eye
vest.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -4f)); // vestibule -> room, deeper
vest.ClipPlanes.Add(new PortalClipPlane { Normal = new Vector3(0, 0, 1), D = 3f, InsideSide = 1 });
var room = Cell(ROOM, new CellPortalInfo((ushort)VEST, PolygonId: 0, Flags: 0, OtherPortalId: 1));
room.PortalPolygons.Add(Quad(0f, 0f, 0.6f, 0.6f, -4f)); // reciprocal back to vestibule
var all = new Dictionary<uint, LoadedCell> { [VEST] = vest, [ROOM] = room };
return (new[] { vest, room }, all);
}
[Fact]
public void ConstructViewBuilding_FloodsTheBuildingFromItsEntrance()
{
var (cells, lookup) = TwoCellBuilding();
var frame = PortalVisibilityBuilder.ConstructViewBuilding(
cells, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.Contains(0x0170u, frame.OrderedVisibleCells); // entrance cell seeded
Assert.Contains(0x0171u, frame.OrderedVisibleCells); // back room reached through the interior portal
}
[Fact]
public void ConstructViewBuilding_TouchesAboutTwoCells()
{
// Conformance to §3.4: each retail per-building flood has cell_draw_num ≈ 2.
var (cells, lookup) = TwoCellBuilding();
var frame = PortalVisibilityBuilder.ConstructViewBuilding(
cells, Vector3.Zero, id => lookup.TryGetValue(id, out var c) ? c : null, ViewProj());
Assert.InRange(frame.OrderedVisibleCells.Count, 1, 3);
}
[Fact]
public void ConstructViewBuilding_MembershipStableUnderMicrometreEyeJitter()
{
// Conformance to §3.4: retail's per-building membership is stable while the eye jitters ~36 µm
// at rest (measured X≈15 µm, Y≈36 µm, Z≈8 µm). The entrance-bounded seed must return the SAME
// OrderedVisibleCells for the eye and the eye + that per-axis jitter — no flap.
var (cells, lookup) = TwoCellBuilding();
var vp = ViewProj();
System.Func<uint, LoadedCell?> lk = id => lookup.TryGetValue(id, out var c) ? c : null;
var a = PortalVisibilityBuilder.ConstructViewBuilding(cells, Vector3.Zero, lk, vp);
var b = PortalVisibilityBuilder.ConstructViewBuilding(
cells, new Vector3(15e-6f, 36e-6f, 8e-6f), lk, vp);
Assert.Equal(a.OrderedVisibleCells, b.OrderedVisibleCells);
}
}

View file

@ -564,12 +564,8 @@ public sealed class RetailPViewPassExecutorTests
var frames = new GpuDeviceFrameLifetime(device);
var scope = new VulkanWorldPassScope(sampleCount: 1);
using var portalDepthMask = new PortalDepthMaskRenderer(device, frames, scope);
var diagnostics = new WorldRenderDiagnostics(new NeverCalledGlStateReader(), new NeverCalledDiagnosticLog());
// DrawPortalDepthWrite only reads _portalDepthMask, frame.Cells,
// frame.RootCell.IsOutdoorNode, frame.ViewProjection, and
// _diagnostics (short-circuited off by RenderingDiagnostics.
// ProbeSeamDrawEnabled's default-off value) — the same
// frame.RootCell.IsOutdoorNode, and frame.ViewProjection — the same
// constructor-bypass pattern WalkOutsideViewReassemblyTests already
// uses for exercising a real leaf without a full GL/DAT renderer
// graph.
@ -578,10 +574,6 @@ public sealed class RetailPViewPassExecutorTests
typeof(RetailPViewPassExecutor)
.GetField("_portalDepthMask", BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(executor, portalDepthMask);
typeof(RetailPViewPassExecutor)
.GetField("_diagnostics", BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(executor, diagnostics);
var root = new LoadedCell { CellId = 0xF4180003u, IsOutdoorNode = false };
RetailPViewFrameInput frame = new RetailPViewFrameInput().Reset(
rootCell: root,
@ -625,21 +617,6 @@ public sealed class RetailPViewPassExecutorTests
public LoadedCell? Find(uint cellId) => cellId == cell.CellId ? cell : null;
}
private sealed class NeverCalledGlStateReader : IRenderGlStateReader
{
public RenderGlStateSnapshot CaptureState() =>
throw new InvalidOperationException("EmitSeamMask must short-circuit before reading GL state.");
public RenderGlScissorSnapshot CaptureScissor() =>
throw new InvalidOperationException("EmitSeamMask must short-circuit before reading GL state.");
}
private sealed class NeverCalledDiagnosticLog : IRenderFrameDiagnosticLog
{
public void WriteLine(string message) =>
throw new InvalidOperationException("EmitSeamMask must short-circuit before logging (ProbeSeamDrawEnabled defaults off).");
}
private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,

View file

@ -9,7 +9,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new RecordingLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(false, world, facts, log);
controller.Begin();
@ -25,7 +25,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new ThrowOnceLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.Begin();
@ -47,7 +47,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new ThrowOnFirstFrameLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.Begin();
@ -70,7 +70,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new RecordingLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.Begin();
@ -99,7 +99,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new RecordingLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.AccumulateWalkBatch(1_000);
@ -121,7 +121,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
// not depend on AccumulateWalkBatch ever having been called.
var log = new RecordingLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.CompleteWalkFrame(10_000);
@ -135,7 +135,7 @@ public sealed class TerrainDrawDiagnosticsControllerTests
{
var log = new RecordingLog();
var facts = new RecordingFacts();
var world = new WorldRenderDiagnostics(new NullGlStateReader(), log);
var world = new WorldRenderDiagnostics(log);
var controller = new TerrainDrawDiagnosticsController(true, world, facts, log);
controller.AccumulateWalkBatch(5_000);
@ -171,12 +171,6 @@ public sealed class TerrainDrawDiagnosticsControllerTests
}
}
private sealed class NullGlStateReader : IRenderGlStateReader
{
public RenderGlStateSnapshot CaptureState() => default;
public RenderGlScissorSnapshot CaptureScissor() => default;
}
private sealed class RecordingLog : IRenderFrameDiagnosticLog
{
public List<string> Messages { get; } = [];

View file

@ -1,429 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using DatReaderWriter;
using DatReaderWriter.Options;
using Xunit;
using Xunit.Abstractions;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// #119 residual — the tower-ascent harness (the #118 exit-walk pattern,
/// vertical). User report (post-#120 build): running UP the AAB3 tower
/// (cell 0xAAB30107, building[1] model 0x01001117) the TOP stairs disappear
/// visually but stay walkable, and at the top the roof/edges FLAP in and
/// out. The tower's upper cells are thin landing/roof slabs with EXIT
/// portals (0x0108 z≈121123, 0x0109 z≈112114.5, 0x010A z≈126.8127.2 —
/// the roof lip), so the ascent walks the eye across exit-portal planes —
/// the same decision-stack territory as #118/#120.
///
/// Per step of a helix matching the spiral staircase, this drives the
/// production stack headlessly: viewer-cell resolution (smallest containing
/// AABB; outdoor when none) → PortalVisibilityBuilder.Build (+ the
/// per-building exterior floods on outdoor steps, mirroring
/// RetailPViewRenderer.MergeNearbyBuildingFloods) → ClipFrameAssembler →
/// ViewconeCuller → the DrawCellObjectLists predicate for the staircase
/// static (Setup 0x020003F2, ParentCellId 0x0107). The failing steps pin
/// the vanish; root/visibility instability across adjacent steps pins the
/// flap.
/// </summary>
[Trait("Lane", "InstalledDat")]
public class TowerAscentReplayTests
{
private readonly ITestOutputHelper _out;
public TowerAscentReplayTests(ITestOutputHelper output) => _out = output;
private const uint Landblock = 0xAAB30000u;
private const uint MainCell = Landblock | 0x0107u;
// Tower geometry (Issue119TowerDumpTests dat facts): origin (108,60,112);
// the staircase Setup spans local x,y ∈ [-4,4], z ∈ [0,15.5]. Production
// EntitySphere = AABB center + half-diagonal.
private static readonly Vector3 TowerCenter = new(108f, 60f, 112f);
private static readonly Vector3 StairSphereCenter = new(108f, 60f, 119.75f);
private const float StairSphereRadius = 9.45f;
private sealed record AscentStep(
int Index,
Vector3 Eye,
uint RootCellId, // 0 = outdoor
bool FloodHasMainCell,
bool StairsConeVisible,
int OutsidePolys,
int FloodCells);
private static Matrix4x4 ViewProjFor(Vector3 eye, Vector3 lookAt)
{
var view = Matrix4x4.CreateLookAt(eye, lookAt, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1280f / 720f, 0.1f, 5000f);
return view * proj;
}
private static uint? ResolveInteriorCellByAabb(
Dictionary<uint, LoadedCell> cells, Vector3 worldPoint, float margin = 0.05f)
{
uint? best = null;
float bestVolume = float.MaxValue;
foreach (var (id, cell) in cells)
{
var local = Vector3.Transform(worldPoint, cell.InverseWorldTransform);
var min = cell.LocalBoundsMin - new Vector3(margin);
var max = cell.LocalBoundsMax + new Vector3(margin);
if (local.X < min.X || local.Y < min.Y || local.Z < min.Z
|| local.X > max.X || local.Y > max.Y || local.Z > max.Z)
continue;
var ext = cell.LocalBoundsMax - cell.LocalBoundsMin;
float volume = MathF.Max(ext.X, 1e-3f) * MathF.Max(ext.Y, 1e-3f) * MathF.Max(ext.Z, 1e-3f);
if (volume < bestVolume) { bestVolume = volume; best = id; }
}
return best;
}
// Mirror of RetailPViewRenderer.MergeBuildingFrame (union semantics).
private static void MergeFrame(PortalVisibilityFrame target, PortalVisibilityFrame src)
{
foreach (uint cellId in src.OrderedVisibleCells)
{
if (!src.CellViews.TryGetValue(cellId, out var srcView)) continue;
if (target.CellViews.TryGetValue(cellId, out var existing))
{
foreach (var p in srcView.Polygons) existing.Add(p);
continue;
}
target.CellViews[cellId] = srcView;
target.OrderedVisibleCells.Add(cellId);
}
}
private List<AscentStep> RunAscent()
{
var datDir = RequireDatDir();
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
Assert.True(cells.ContainsKey(MainCell), "tower main cell not loaded");
// The tower building's cell list (Issue119TowerDumpTests: building[1]
// stabs = 0x0107..0x010A) — the per-building flood candidates on
// outdoor-root steps, mirroring GameWindow's gather.
var towerCells = new List<LoadedCell>();
for (uint low = 0x0107u; low <= 0x010Au; low++)
if (cells.TryGetValue(Landblock | low, out var c))
towerCells.Add(c);
// Eye helix matching the spiral: two turns, radius 2.5 m, z from head
// height at the base to above the roof lip (cell 0x010A tops at 127.2).
const int Steps = 220;
const float ZStart = 113.6f, ZEnd = 128.6f;
var clipFrame = ClipFrame.NoClip();
var steps = new List<AscentStep>(Steps + 1);
for (int i = 0; i <= Steps; i++)
{
float t = i / (float)Steps;
float theta = t * 4f * MathF.PI;
float z = ZStart + t * (ZEnd - ZStart);
var eye = new Vector3(
TowerCenter.X + 2.5f * MathF.Cos(theta),
TowerCenter.Y + 2.5f * MathF.Sin(theta),
z);
// look AT the staircase (the user's framing while climbing: the
// camera tracks the player on the stairs). An unambiguous pin:
// if the staircase cone-culls while looked at directly, the
// visibility stack is wrong — no gaze excuse.
var gaze = StairSphereCenter - eye;
if (gaze.LengthSquared() < 1e-4f) gaze = new Vector3(0f, 0f, -1f);
var viewProj = ViewProjFor(eye, eye + Vector3.Normalize(gaze) * 3f);
uint rootCellId = 0u;
PortalVisibilityFrame pv;
var contained = ResolveInteriorCellByAabb(cells, eye);
if (contained.HasValue)
{
rootCellId = contained.Value;
pv = PortalVisibilityBuilder.Build(cells[rootCellId], eye, lookup, viewProj);
}
else
{
// outdoor root: full-screen outside view + the per-building
// exterior floods (RetailPViewRenderer.MergeNearbyBuildingFloods)
var outdoor = OutdoorCellNode.Build(Landblock | 0x0001u);
pv = PortalVisibilityBuilder.Build(outdoor, eye, lookup, viewProj);
var bf = PortalVisibilityBuilder.ConstructViewBuilding(
towerCells, eye, lookup, viewProj);
MergeFrame(pv, bf);
}
var asm = ClipFrameAssembler.Assemble(clipFrame, pv);
var cone = ViewconeCuller.Build(asm, viewProj);
bool floodHasMain = pv.CellViews.ContainsKey(MainCell);
// DrawCellObjectLists predicate for a 0x0107 static: sphere vs the
// cell's views (RetailPViewRenderer.cs DrawCellObjectLists / T3).
bool stairsVisible = cone.SphereVisibleInCell(MainCell, StairSphereCenter, StairSphereRadius);
steps.Add(new AscentStep(
i, eye, rootCellId, floodHasMain, stairsVisible,
pv.OutsideView.Polygons.Count, pv.OrderedVisibleCells.Count));
}
return steps;
}
private void DumpSteps(IEnumerable<AscentStep> steps, Func<AscentStep, bool>? filter = null)
{
foreach (var st in steps)
{
if (filter is not null && !filter(st)) continue;
string root = st.RootCellId == 0 ? "OUTDOOR " : FormattableString.Invariant($"0x{st.RootCellId:X8}");
string stairs = st.StairsConeVisible ? "VIS " : "CULL";
_out.WriteLine(FormattableString.Invariant(
$"step={st.Index,3} eye=({st.Eye.X:F2},{st.Eye.Y:F2},{st.Eye.Z:F2}) root={root} floodMain={st.FloodHasMainCell} stairs={stairs} outPolys={st.OutsidePolys} flood={st.FloodCells}"));
}
}
/// <summary>
/// The vanish pin: the staircase static must stay cone-visible on every
/// ascent step (the gaze looks STRAIGHT AT it — no gaze excuse).
///
/// PINNED RED 2026-06-11 (skip carries the defect; un-skip with the fix):
/// steps 195201 (eye z 126.9127.3, the roof-lip band between the main
/// cell's ceiling at 126.8 and the roof aperture plane at ~127.2) resolve
/// OUTDOOR and the per-building exterior flood admits NOTHING (flood=1 =
/// the outdoor node alone) — the eye is above every side aperture's
/// useful view and ON/INSIDE the roof aperture's plane, so
/// BuildFromExterior's seed side-test/in-plane reject refuses every exit
/// portal. The tower interior never floods → the staircase (a 0x0107
/// static) has no views → culled while walkable, and the roof-lip cell
/// geometry flaps as the live eye bobs across the band's edges. Fix
/// needs the retail oracle: does retail keep the viewer cell INTERIOR
/// through this band (curr_cell keep-curr above open-top cells), or can
/// ConstructView(CBldPortal) seed with an in-plane eye?
/// </summary>
[Fact]
[Trait("Status", "KnownFailure")]
public void TowerAscent_StaircaseStaysConeVisible_EveryStep()
{
var steps = RunAscent();
var failures = steps.FindAll(s => !s.StairsConeVisible);
if (failures.Count > 0)
{
_out.WriteLine($"--- {failures.Count} stairs-CULLED steps ---");
DumpSteps(failures);
}
Assert.True(failures.Count == 0,
$"{failures.Count}/{steps.Count} ascent steps cone-cull the staircase (first at step {(failures.Count > 0 ? failures[0].Index : -1)}) — see output");
}
/// <summary>
/// The flap pin: along a smooth monotone ascent, the resolved root may
/// transition between cells/outdoor but must not PING-PONG (A→B→A within
/// three consecutive steps = a knife-edge decision the live camera jitter
/// turns into per-frame flapping).
/// </summary>
[Fact]
public void TowerAscent_RootDoesNotPingPong()
{
var steps = RunAscent();
var flips = new List<int>();
for (int i = 2; i < steps.Count; i++)
{
uint a = steps[i - 2].RootCellId, b = steps[i - 1].RootCellId, c = steps[i].RootCellId;
if (a != b && b != c && a == c)
flips.Add(i - 1);
}
if (flips.Count > 0)
{
_out.WriteLine($"--- {flips.Count} root ping-pong steps ---");
DumpSteps(steps, s => flips.Contains(s.Index) || flips.Contains(s.Index - 1) || flips.Contains(s.Index + 1));
}
Assert.True(flips.Count == 0,
$"{flips.Count} root ping-pongs along a monotone ascent — knife-edge root decisions (see output)");
}
/// <summary>
/// THE captured top-of-stairs vanish (flap-diff-capture.log, live user
/// pan): root=0xAAB3010A (the roof-lip cell), eye 3 mm above its floor
/// portal plane, gazing DOWN at the staircase — and the flood admitted
/// ONLY {010A}: 0x0107 (the whole tower interior, staircase included)
/// dropped while being looked straight at. Exact production inputs:
/// eye=(301.448,-128.448,126.803) fwd=(-0.9361,-0.2459,-0.2514)
/// eye=(301.371,-128.401,126.807) fwd=(-0.8938,-0.3596,-0.2678)
/// The 010A→0107 portal is HORIZONTAL at z≈126.8 — the eye rides within
/// millimetres of its plane at the stair top, knife-edge projection
/// territory (the #120 family's geometry, admission-side).
/// </summary>
[Theory]
[InlineData(301.448f, -128.448f, 126.803f, -0.9361f, -0.2459f, -0.2514f)]
[InlineData(301.371f, -128.401f, 126.807f, -0.8938f, -0.3596f, -0.2678f)]
public void CapturedTopOfStairs_MainCellStaysInFlood(
float ex, float ey, float ez, float fx, float fy, float fz)
{
var datDir = RequireDatDir();
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
Func<uint, LoadedCell?> lookup = id => cells.TryGetValue(id, out var c) ? c : null;
// The capture is A9B4-anchored world; the loaded cells are AAB3-local
// (LoadCell without offset). AAB3 = A9B4 + (1,-1) blocks → local =
// world - (192,-192,0).
var eye = new Vector3(ex - 192f, ey + 192f, ez);
var fwd = Vector3.Normalize(new Vector3(fx, fy, fz));
// ROOT CAUSE (found via this capture): production fed BuildLoadedCell
// the +0.02 m RENDER-LIFTED transform ("keep the small render lift out
// of physics", GameWindow:5567) — the visibility graph's HORIZONTAL
// portal planes all sat 2 cm high, so an eye standing on the deck
// (3 mm above the TRUE plane) was 17 mm BELOW the lifted plane —
// outside the side test's ±10 mm in-plane window → 010A→0107
// side-culled → the staircase vanished while walkable. FIXED: the
// visibility LoadedCell now gets the PHYSICS (unlifted) transform.
//
// Arm 1 (unlifted = post-fix production): the main cell must be
// admitted. Arm 2 (lifted = the old bug, kept as the mechanism
// canary): the main cell is dropped — if this arm ever starts
// PASSING, the in-plane window has changed enough to absorb a 2 cm
// lift; re-evaluate both this canary and the no-lift rule.
foreach (bool lift in new[] { false, true })
{
var armCells = lift ? new Dictionary<uint, LoadedCell>() : cells;
if (lift)
{
var datDir2 = CornerFloodReplayTests.ResolveDatDir()!;
using var dats2 = new DatCollection(datDir2, DatAccessType.Read);
var lbi = dats2.Get<DatReaderWriter.DBObjs.LandBlockInfo>(Landblock | 0xFFFEu)!;
for (uint low = 0x0100u; low < 0x0100u + lbi.NumCells; low++)
{
try { armCells[Landblock | low] = CornerFloodReplayTests.LoadCell(dats2, Landblock | low, new Vector3(0f, 0f, 0.02f)); }
catch (InvalidOperationException) { }
}
}
Func<uint, LoadedCell?> armLookup = id => armCells.TryGetValue(id, out var c) ? c : null;
var root = armCells[Landblock | 0x010Au];
foreach (float fov in new[] { MathF.PI / 3f, 1.2f, MathF.PI / 2f })
{
var view = Matrix4x4.CreateLookAt(eye, eye + fwd * 3f, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(fov, 1280f / 720f, 0.1f, 5000f);
var pv = PortalVisibilityBuilder.Build(root, eye, armLookup, view * proj);
bool hasMain = pv.CellViews.ContainsKey(MainCell);
_out.WriteLine(FormattableString.Invariant(
$"lift={lift} fov={fov:F2}: flood={pv.OrderedVisibleCells.Count} hasMain={hasMain}"));
if (!lift)
Assert.True(hasMain,
$"fov={fov:F2}: the tower main cell 0x{MainCell:X8} is NOT in the flood from the roof-lip root (unlifted = post-fix production)");
else
Assert.False(hasMain,
$"fov={fov:F2}: the LIFTED canary unexpectedly admits the main cell — the in-plane window now absorbs a 2 cm lift; re-evaluate the canary + the no-lift rule");
}
}
}
/// <summary>
/// Gate-by-gate diagnosis of the captured drop: for the 010A→0107 portal
/// at the exact captured eye/gaze, print each admission gate's verdict —
/// side test (CameraOnInteriorSide replica), homogeneous clip vs the
/// full-screen view (ProjectToClip + ClipToRegion, the forward-hop
/// pipeline), and the eye-in-opening rescue inputs (perp distance +
/// footprint containment).
/// </summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_TopOfStairs_GateByGate()
{
var datDir = RequireDatDir();
using var dats = new DatCollection(datDir, DatAccessType.Read);
var cells = Issue120ReciprocalPingPongTests.LoadAllInteriorCells(dats, Landblock);
var root = cells[Landblock | 0x010Au];
// capture eye converted to the cells' AAB3-local frame (world - (192,-192,0))
var eye = new Vector3(301.448f - 192f, -128.448f + 192f, 126.803f);
var fwd = Vector3.Normalize(new Vector3(-0.9361f, -0.2459f, -0.2514f));
var view = Matrix4x4.CreateLookAt(eye, eye + fwd * 3f, Vector3.UnitZ);
var proj = Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 1280f / 720f, 0.1f, 5000f);
var viewProj = view * proj;
for (int i = 0; i < root.Portals.Count; i++)
{
var portal = root.Portals[i];
var poly = i < root.PortalPolygons.Count ? root.PortalPolygons[i] : null;
_out.WriteLine($"portal[{i}] -> 0x{portal.OtherCellId:X4} polyLen={poly?.Length ?? -1}");
if (poly is null || poly.Length < 3) continue;
// gate 1: side test (CameraOnInteriorSide replica, ε=0.01 in-plane-allowed)
string side = "no-plane(allow)";
if (i < root.ClipPlanes.Count && root.ClipPlanes[i].Normal.LengthSquared() >= 1e-8f)
{
var plane = root.ClipPlanes[i];
var localCam = Vector3.Transform(eye, root.InverseWorldTransform);
float dot = Vector3.Dot(plane.Normal, localCam) + plane.D;
bool pass = plane.InsideSide == 0 ? dot >= -0.01f : dot <= 0.01f;
side = FormattableString.Invariant($"dot={dot:F4} insideSide={plane.InsideSide} -> {(pass ? "TRAVERSE" : "CULL")}");
}
_out.WriteLine($" side: {side}");
// gate 2: homogeneous clip vs the full-screen view
var clip = PortalProjection.ProjectToClip(poly, root.WorldTransform, viewProj);
var fullScreen = new[] { new Vector2(-1f, -1f), new Vector2(1f, -1f), new Vector2(1f, 1f), new Vector2(-1f, 1f) };
var region = clip.Length >= 3 ? PortalProjection.ClipToRegion(clip, fullScreen) : Array.Empty<Vector2>();
_out.WriteLine($" clip: projVerts={clip.Length} regionVerts={region.Length}");
// gate 3: rescue inputs — perp distance to the portal plane + the
// perpendicular foot inside the opening's footprint
var centroid = Vector3.Zero;
foreach (var v in poly) centroid += Vector3.Transform(v, root.WorldTransform);
centroid /= poly.Length;
var n0 = Vector3.Transform(poly[0], root.WorldTransform);
var n1 = Vector3.Transform(poly[1], root.WorldTransform);
var n2 = Vector3.Transform(poly[2], root.WorldTransform);
var planeN = Vector3.Normalize(Vector3.Cross(n1 - n0, n2 - n0));
float perp = MathF.Abs(Vector3.Dot(planeN, eye - centroid));
_out.WriteLine(FormattableString.Invariant(
$" rescue: perpDist={perp:F4} centroid=({centroid.X:F2},{centroid.Y:F2},{centroid.Z:F2}) planeN=({planeN.X:F2},{planeN.Y:F2},{planeN.Z:F2})"));
}
// knife-edge sweep: the probe rounds the eye to mm — sweep z within the
// rounding envelope and count clip flips for the 010A→0107 portal. A
// chaotic 6↔0 region inside ±2 mm proves the live drop is the same
// frame at sub-mm precision.
{
var poly = root.PortalPolygons[0];
var fullScreen = new[] { new Vector2(-1f, -1f), new Vector2(1f, -1f), new Vector2(1f, 1f), new Vector2(-1f, 1f) };
int flips = 0;
bool? prev = null;
var results = new System.Text.StringBuilder();
for (int i = -20; i <= 20; i++)
{
var e2 = eye with { Z = eye.Z + i * 0.0002f };
var v2 = Matrix4x4.CreateLookAt(e2, e2 + fwd * 3f, Vector3.UnitZ);
var vp2 = v2 * proj;
var clip2 = PortalProjection.ProjectToClip(poly, root.WorldTransform, vp2);
var region2 = clip2.Length >= 3 ? PortalProjection.ClipToRegion(clip2, fullScreen) : Array.Empty<Vector2>();
bool ok = region2.Length >= 3;
if (prev.HasValue && ok != prev.Value) flips++;
prev = ok;
results.Append(ok ? '1' : '0');
}
_out.WriteLine($" knife-edge sweep (z ±4 mm, 0.2 mm steps): {results} flips={flips}");
}
}
/// <summary>Full per-step table for the investigation record.</summary>
[Fact]
[Trait("Purpose", "Diagnostic")]
public void Diagnostic_TowerAscent_PerStepTable()
{
var steps = RunAscent();
DumpSteps(steps);
}
private static string RequireDatDir()
=> CornerFloodReplayTests.ResolveDatDir()
?? throw new InvalidOperationException(
"Installed client_portal.dat is required for the tower-ascent lane.");
}

View file

@ -1,60 +0,0 @@
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public sealed class ViewconeCullerReuseTests
{
[Fact]
public void ReusedCuller_ReplacesPriorCellAndOutsideState()
{
const uint firstCell = 0xA9B40100;
const uint secondCell = 0xA9B40101;
var clipFrame = ClipFrame.NoClip();
var assemblyScratch = new ClipFrameAssembly();
var culler = new ViewconeCuller();
var first = Frame(firstCell, includeOutside: true);
ClipFrameAssembly firstAssembly = ClipFrameAssembler.Assemble(
clipFrame,
first,
assemblyScratch);
Assert.Same(culler, ViewconeCuller.Build(firstAssembly, Matrix4x4.Identity, culler));
Assert.True(culler.SphereVisibleInCell(firstCell, Vector3.Zero, 0f));
Assert.True(culler.SphereVisibleOutside(Vector3.Zero, 0f));
var second = Frame(secondCell, includeOutside: false);
ClipFrameAssembly secondAssembly = ClipFrameAssembler.Assemble(
clipFrame,
second,
assemblyScratch);
Assert.Same(culler, ViewconeCuller.Build(secondAssembly, Matrix4x4.Identity, culler));
Assert.False(culler.SphereVisibleInCell(firstCell, Vector3.Zero, 0f));
Assert.True(culler.SphereVisibleInCell(secondCell, Vector3.Zero, 0f));
Assert.False(culler.SphereVisibleInCell(secondCell, new Vector3(2f, 0f, 0f), 0f));
Assert.False(culler.SphereVisibleOutside(Vector3.Zero, 0f));
}
private static PortalVisibilityFrame Frame(uint cellId, bool includeOutside)
{
var result = new PortalVisibilityFrame();
var cellView = new CellView();
cellView.Add(Square(0.5f));
result.CellViews.Add(cellId, cellView);
result.OrderedVisibleCells.Add(cellId);
if (includeOutside)
result.OutsideView.Add(Square(0.5f));
return result;
}
private static ViewPolygon Square(float half)
=> new(new[]
{
new Vector2(-half, -half),
new Vector2(half, -half),
new Vector2(half, half),
new Vector2(-half, half),
});
}

View file

@ -12,7 +12,7 @@ public sealed class WalkBuildingInstalledDatCensusTests
[Fact]
public void CompleteInstalledBuildingCorpusMatchesBoundedSelectionCensus()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
using var dats = new DatCollection(datDir!, DatAccessType.Read);

View file

@ -23,7 +23,7 @@ public sealed class WalkBuildingJoinDiagnosticTests
public void Traced_lookin_cells_are_reachable_through_the_building_portal_table(
uint buildingCellId, uint[] tracedLookInCells)
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");

View file

@ -446,7 +446,7 @@ public sealed partial class WalkFrameDriverTests
Assert.Equal(2, mdiCalls.Sum(c => (int)c.DrawCount));
// Exit seals replay through the exact portal_view slices captured by
// this walk. The legacy PortalVisibilityFrame is not consulted.
// this walk; there is no independently reconstructed visibility graph.
Assert.Equal(1, driver.InteriorFloodViewSliceCountAt(0));
Assert.Equal(1, driver.InteriorFloodViewSliceCountAt(1));
Assert.Equal(4, driver.InteriorFloodViewClipPlanesAt(0, 0).Length);

View file

@ -25,8 +25,8 @@ namespace AcDream.App.Tests.Rendering.Walk;
/// <see cref="RenderingDiagnostics.DumpWalkTranscriptEnabled"/> and
/// <see cref="Console.Out"/> are both process-wide mutable statics — joins
/// <see cref="CameraDiagnosticsCollection"/> for the SAME reason
/// <c>CornerFloodReplayTests</c>/<c>Issue181WallPressEquilibriumTests</c> do
/// (that collection's own doc comment, issue #251): interleaved
/// <c>Issue181WallPressEquilibriumTests</c> does (that collection's own doc
/// comment, issue #251): interleaved
/// <c>Console.SetOut</c> redirection across parallel test classes can
/// restore a disposed <see cref="StringWriter"/> process-wide.
/// </para>

View file

@ -31,7 +31,7 @@ public sealed class WalkLookInGateSweepTests
+ "(the doorway-still fixture tests it in isolation).")]
public void Sweep_the_lookin_gate_decodes_against_the_street_fixture()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");

View file

@ -20,7 +20,7 @@ public sealed class WalkPortalGateDumpTests
[Fact]
public void Dump_the_gate_inputs_for_a_punching_and_a_silent_building()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
@ -102,7 +102,7 @@ public sealed class WalkPortalGateDumpTests
[Fact]
public void Dump_the_exit_view_look_in_inputs_for_walkout_f2()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");
@ -232,7 +232,7 @@ public sealed class WalkPortalGateDumpTests
[Fact]
public void Dump_the_foundry_f67_bsp_traversal_for_0036()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory.");

View file

@ -59,7 +59,7 @@ public sealed class WalkProductionWorldConformanceTests
private static DatCollection OpenDats()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");

View file

@ -453,31 +453,6 @@ public sealed class WalkProductionWorldDataTests
Assert.Equal(first.Records.Count, second.Records.Count);
}
[Fact]
public void StaticBucketContains_FindsARegisteredSourceIdInItsRetailCell()
{
const uint entityId = 0x48A02040u;
const uint sourceId = 0x020009A2u;
const uint retailCellId = 0xF4180112u;
var shadows = new ShadowObjectRegistry();
Register(shadows, entityId, retailCellId, landblockId: 0xF4180000u);
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord projection =
IndoorStaticRecord(entityId, sourceId, retailCellId);
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0xF4180000u, renderCenterLbX: 0xF4, renderCenterLbY: 0x18);
Assert.True(worldData.StaticBucketContains(retailCellId, sourceId));
Assert.False(worldData.StaticBucketContains(retailCellId, sourceId + 1));
Assert.False(worldData.StaticBucketContains(0xF4180107u, sourceId));
}
private static RenderProjectionRecord Record(uint id, Vector3 position) =>
new RenderProjectionRecord() with
{

View file

@ -0,0 +1,239 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Walk;
using AcDream.App.Rendering.Wb;
using AcDream.Content;
using DatReaderWriter;
namespace AcDream.App.Tests.Rendering.Walk;
public sealed class WalkRendererArchitectureGuardTests
{
private static readonly Assembly AppAssembly = typeof(RetailFrameWalk).Assembly;
private static readonly string[] DeletedGraphTypes =
[
"PortalVisibilityBuilder",
"PortalVisibilityFrame",
"ExteriorPortalSeed",
"IndoorDrawPlan",
"CellDrawEntry",
"ViewconeCuller",
];
private static readonly string[] DeletedProbeSymbols =
[
"ProbeFacilityStairsEnabled",
"ACDREAM_PROBE_FACILITY_STAIRS",
"ProbeIndoorWalkEnabled",
"ProbeIndoorLookupEnabled",
"ProbeIndoorUploadEnabled",
"ProbeIndoorXformEnabled",
"ProbeIndoorCullEnabled",
"ACDREAM_PROBE_INDOOR_WALK",
"ACDREAM_PROBE_INDOOR_LOOKUP",
"ACDREAM_PROBE_INDOOR_UPLOAD",
"ACDREAM_PROBE_INDOOR_XFORM",
"ACDREAM_PROBE_INDOOR_CULL",
"ACDREAM_PROBE_INDOOR_ALL",
"ProbeVisibilityEnabled",
"ACDREAM_PROBE_VIS",
"EmitVis",
"ProbeEnvCellEnabled",
"ACDREAM_PROBE_ENVCELL",
"ProbeFlapEnabled",
"ACDREAM_PROBE_FLAP",
"ProbePvInputEnabled",
"ACDREAM_PROBE_PVINPUT",
"ProbeGlStateEnabled",
"ACDREAM_PROBE_GLSTATE",
"ProbeClipRouteEnabled",
"ACDREAM_PROBE_CLIPROUTE",
"ProbePortalChurnEnabled",
"ACDREAM_PROBE_PORTAL_CHURN",
"ProbeIndoorLightEnabled",
"ACDREAM_PROBE_INDOOR_LIGHT",
"EmitIndoorLight",
"ProbeSeamDrawEnabled",
"ACDREAM_PROBE_SEAMDRAW",
"SeamDrawTargetCells",
];
[Fact]
public void AppAssembly_ContainsNoSupersededPortalGraphTypes()
{
string[] survivors = AppAssembly.GetTypes()
.Where(type => DeletedGraphTypes.Contains(type.Name, StringComparer.Ordinal))
.Select(type => type.FullName ?? type.Name)
.OrderBy(name => name, StringComparer.Ordinal)
.ToArray();
Assert.Empty(survivors);
}
[Fact]
public void WalkFrameOwners_AreUnique()
{
const BindingFlags Fields = BindingFlags.Instance | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
Type[] sourceTypes = AppAssembly.GetTypes()
.Where(type => !type.IsDefined(typeof(CompilerGeneratedAttribute), inherit: false))
.ToArray();
(Type Owner, FieldInfo Field)[] pviewOwners = sourceTypes
.SelectMany(type => type.GetFields(Fields).Select(field => (type, field)))
.Where(pair => pair.field.FieldType == typeof(WalkPView))
.Select(pair => (pair.type, pair.field))
.ToArray();
Assert.All(pviewOwners, pair => Assert.Equal(typeof(RetailFrameWalk), pair.Owner));
Assert.Equal(2, pviewOwners.Length);
(Type Owner, FieldInfo Field)[] frameWalkOwners = sourceTypes
.SelectMany(type => type.GetFields(Fields).Select(field => (type, field)))
.Where(pair => pair.field.FieldType == typeof(RetailFrameWalk))
.Select(pair => (pair.type, pair.field))
.ToArray();
(Type Owner, FieldInfo Field) owner = Assert.Single(frameWalkOwners);
Assert.Equal(typeof(RetailPViewRenderer), owner.Owner);
}
[Fact]
public void FrameTimeWalkOwners_HaveNoRawDatDependency()
{
Type[] owners =
[
typeof(RetailFrameWalk),
typeof(WalkFrameDriver),
typeof(RetailPViewRenderer),
typeof(RetailPViewPassExecutor),
typeof(WalkProductionLeafRenderer),
typeof(WalkStaticStreamPopulator),
typeof(WbDrawDispatcher),
];
var violations = new List<string>();
foreach (Type owner in owners)
{
const BindingFlags Members = BindingFlags.Instance | BindingFlags.Static |
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
foreach (FieldInfo field in owner.GetFields(Members))
AddIfRawDat(violations, owner, $"field {field.Name}", field.FieldType);
foreach (PropertyInfo property in owner.GetProperties(Members))
AddIfRawDat(violations, owner, $"property {property.Name}", property.PropertyType);
IEnumerable<MethodBase> methods = owner.GetMethods(Members).Cast<MethodBase>()
.Concat(owner.GetConstructors(Members));
foreach (MethodBase method in methods)
{
foreach (ParameterInfo parameter in method.GetParameters())
{
AddIfRawDat(
violations,
owner,
$"parameter {method.Name}.{parameter.Name}",
parameter.ParameterType);
}
if (method is MethodInfo methodInfo)
AddIfRawDat(violations, owner, $"return {method.Name}", methodInfo.ReturnType);
}
}
Assert.Empty(violations);
}
[Fact]
public void ProductionSource_ContainsNoSecondPortalGraph()
{
string[] sources = ProductionSources();
string[] banned = [.. DeletedGraphTypes, "ClipFrameAssembler.Assemble"];
AssertNoSourceMatches(sources, banned);
}
[Fact]
public void OrderedWalkStream_HasNoCrossStreamReorder()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src", "AcDream.App", "Rendering", "Wb", "WbDrawDispatcher.OrderedStream.cs"));
Assert.DoesNotContain(".Sort(", source, StringComparison.Ordinal);
Assert.DoesNotContain("OrderBy(", source, StringComparison.Ordinal);
Assert.DoesNotContain("OrderByDescending(", source, StringComparison.Ordinal);
}
[Fact]
public void ProductionSource_ContainsNoDeletedRendererProbe_AndRetainsWalkTranscriptProof()
{
AssertNoSourceMatches(ProductionSources(), DeletedProbeSymbols);
string root = FindRepoRoot();
string diagnostics = File.ReadAllText(Path.Combine(
root, "src", "AcDream.Core", "Rendering", "RenderingDiagnostics.cs"));
string transcriptTests = File.ReadAllText(Path.Combine(
root,
"tests", "AcDream.App.Tests", "Rendering", "Walk",
"WalkFrameDriverTranscriptTests.cs"));
Assert.Contains("DumpWalkTranscriptEnabled", diagnostics, StringComparison.Ordinal);
Assert.Contains(
"TranscriptEmitter_FlagOff_EveryPrintMethodIsAZeroCostNoOp",
transcriptTests,
StringComparison.Ordinal);
Assert.Contains(
"Collect_TranscriptFlagOn_PrintedLinesRoundTripThroughTheParser",
transcriptTests,
StringComparison.Ordinal);
}
private static void AddIfRawDat(
ICollection<string> violations,
Type owner,
string member,
Type memberType)
{
if (ContainsRawDat(memberType))
violations.Add($"{owner.FullName} {member}: {memberType}");
}
private static bool ContainsRawDat(Type type)
{
if (type.IsByRef || type.IsArray || type.IsPointer)
return ContainsRawDat(type.GetElementType()!);
if (type == typeof(DatCollection) || type == typeof(IDatReaderWriter))
return true;
return type.IsGenericType && type.GetGenericArguments().Any(ContainsRawDat);
}
private static void AssertNoSourceMatches(IEnumerable<string> sources, IEnumerable<string> banned)
{
string[] violations = sources
.SelectMany(path => banned
.Where(symbol => File.ReadAllText(path).Contains(symbol, StringComparison.Ordinal))
.Select(symbol => $"{Path.GetRelativePath(FindRepoRoot(), path)}: {symbol}"))
.OrderBy(value => value, StringComparer.Ordinal)
.ToArray();
Assert.Empty(violations);
}
private static string[] ProductionSources()
{
string root = Path.Combine(FindRepoRoot(), "src");
return Directory.GetFiles(root, "*.cs", SearchOption.AllDirectories)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
private static string FindRepoRoot()
{
string? directory = AppContext.BaseDirectory;
while (directory is not null)
{
if (File.Exists(Path.Combine(directory, "AcDream.slnx")))
return directory;
directory = Directory.GetParent(directory)?.FullName;
}
throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
}
}

View file

@ -318,7 +318,7 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.DoesNotContain(
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
Assert.Contains("active total to **161**", register, StringComparison.Ordinal);
Assert.Contains("current active total to **160**", register, StringComparison.Ordinal);
int apSectionStart = register.IndexOf(
"## 3. Documented approximation (AP)", StringComparison.Ordinal);
int apSectionEnd = register.IndexOf(
@ -342,7 +342,7 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-244 |", StringComparison.Ordinal));
Assert.Equal(
161,
160,
File.ReadLines(registerPath).Count(static line =>
line.StartsWith("| AP-", StringComparison.Ordinal)));
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);

View file

@ -114,7 +114,7 @@ public sealed partial class WalkTraceConformanceTests
private static DatCollection OpenDats()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");

View file

@ -19,7 +19,7 @@ public sealed class WalkWorldDatAdapterTests
{
private static DatCollection OpenDats()
{
string? datDir = CornerFloodReplayTests.ResolveDatDir();
string? datDir = InstalledDatTestPath.Resolve();
if (datDir is null)
{
Assert.Fail("Lane=InstalledDat requires an installed retail DAT directory; see docs/release-gate.md.");

View file

@ -39,21 +39,13 @@ public class WalkOutsideViewReassemblyTests
return view;
}
private static ClipFrameAssembly AssembledWithOldOutside()
{
var pv = new PortalVisibilityFrame();
pv.OutsideView.Add(new ViewPolygon(new[]
{
new Vector2(-0.9f, -0.9f), new Vector2(0.9f, -0.9f),
new Vector2(0.9f, 0.9f), new Vector2(-0.9f, 0.9f),
}));
return ClipFrameAssembler.Assemble(ClipFrame.NoClip(), pv);
}
private static ClipFrameAssembly BeginAssembly()
=> ClipFrameAssembler.BeginWalkFrame(ClipFrame.NoClip(), outdoorRoot: true);
[Fact]
public void PixelDoorway_MapsToExpectedNdcAabbAndPlanes()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
// Pixel quad x∈[256,512], y∈[180,360] → NDC x∈[-0.5,0], y∈[0,0.5]
// (yNdc = 1 2·py/H flips the axis: py=180 → +0.5, py=360 → 0).
WalkPortalView walkView = WalkViewOfPixelQuads(new[]
@ -95,7 +87,7 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void FullViewportWalkQuad_CoversFullNdc()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
var walkView = new WalkPortalView();
Assert.True(WalkCopyView.AppendFullViewportQuad(
walkView, new StubRays(), Vector3.Zero, W, H));
@ -109,8 +101,8 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void EmptyWalkView_YieldsSkipMode()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
Assert.True(asm.OutdoorVisible); // the old view was visible pre-cutover
ClipFrameAssembly asm = BeginAssembly();
Assert.True(asm.OutdoorVisible);
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, new WalkPortalView(), W, H);
@ -138,7 +130,7 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void FirstViewCollapses_SecondSurvives_SlicesStayIndexAligned()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
var walkView = new WalkPortalView();
// View 0: three EXACTLY collinear pixel points -> zero-area triangle,
@ -196,7 +188,7 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void PunchLeaf_UsesIndexAlignedSlice_DrawsNothingForCollapsed_ThrowsOutOfRange()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
var walkView = new WalkPortalView();
walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(100f, 100f) });
walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(200f, 100f) });
@ -356,7 +348,7 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void NineVertexOutsideView_PunchSliceHasZeroPlanes_DrawsUnclipped_NotNothingVisible()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
const int n = 9;
var verts = new Vector2[n];
@ -387,7 +379,7 @@ public class WalkOutsideViewReassemblyTests
[Fact]
public void TwoWalkViews_ProduceTwoSlicesWithDistinctSlots()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
ClipFrameAssembly asm = BeginAssembly();
WalkPortalView walkView = WalkViewOfPixelQuads(
new[]
{

View file

@ -1,194 +1,29 @@
using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering;
using AcDream.Core.Vfx;
namespace AcDream.App.Tests.Rendering;
public sealed class WorldRenderDiagnosticsTests
{
[Fact]
public void GlTripwire_DisabledPerformsNoGlRead()
{
var gl = new RecordingGlStateReader();
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(gl, log);
diagnostics.EmitGlStateTripwireIfChanged(enabled: false);
Assert.Equal(0, gl.StateCaptureCount);
Assert.Empty(log.Messages);
}
[Fact]
public void GlTripwire_LogsOnlyChangesAndReportsStableFrameCount()
{
var gl = new RecordingGlStateReader
{
State = State(depthFunction: 0x201),
};
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(gl, log);
diagnostics.EmitGlStateTripwireIfChanged(enabled: true);
diagnostics.EmitGlStateTripwireIfChanged(enabled: true);
gl.State = State(depthFunction: 0x203);
diagnostics.EmitGlStateTripwireIfChanged(enabled: true);
Assert.Equal(3, gl.StateCaptureCount);
Assert.Collection(
log.Messages,
first =>
{
Assert.StartsWith("[gl-state] frame=1 stable=0", first);
Assert.Contains("dfunc=0x201", first);
},
changed =>
{
Assert.StartsWith("[gl-state] frame=3 stable=1", changed);
Assert.Contains("dfunc=0x203", changed);
});
}
[Fact]
public void ScissorProbe_SequenceAdvancesAcrossSuppressedDuplicates()
{
var gl = new RecordingGlStateReader
{
Scissor = new RenderGlScissorSnapshot(
true,
new IntRenderRectangle(1, 2, 3, 4)),
};
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(gl, log);
diagnostics.EmitClipRouteScissorProbe(true, true, new Vector4(-1, -1, 1, 1));
diagnostics.EmitClipRouteScissorProbe(true, true, new Vector4(-1, -1, 1, 1));
gl.Scissor = new RenderGlScissorSnapshot(
true,
new IntRenderRectangle(5, 6, 7, 8));
diagnostics.EmitClipRouteScissorProbe(true, true, new Vector4(-1, -1, 1, 1));
Assert.Equal(3, gl.ScissorCaptureCount);
Assert.Collection(
log.Messages,
first => Assert.StartsWith("[clip-route-scis] n=1", first),
changed => Assert.StartsWith("[clip-route-scis] n=3", changed));
}
[Fact]
public void Owner_RetainsNoBorrowedFrameProductsOrBroadRuntimeOwners()
{
FieldInfo[] fields = typeof(WorldRenderDiagnostics).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Type[] forbidden =
[
typeof(GameWindow),
typeof(PortalVisibilityFrame),
typeof(ClipFrameAssembly),
typeof(InteriorEntityPartition.Result),
typeof(AcDream.Core.Physics.PhysicsEngine),
typeof(CameraController),
typeof(CellVisibility),
];
foreach (Type type in forbidden)
Assert.DoesNotContain(fields, field => field.FieldType == type);
Assert.DoesNotContain(
fields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
}
// Campaign V slice V11 deleted SilkRenderGlStateReader, the raw-GL
// IRenderGlStateReader implementation this test pinned (it was never
// constructed in production — NullRenderGlStateReader.Instance is the
// sole surviving implementer), so the "consume the entering GL error
// before other state reads" source-order tripwire it protected has
// nothing left to pin.
[Fact]
public void TerrainDiagnostics_RetryFailedPublicationWithoutLosingSamples()
{
var log = new ThrowOnceLog();
var diagnostics = new WorldRenderDiagnostics(new RecordingGlStateReader(), log);
var facts = new TerrainRenderDiagnosticFacts(VisibleSlots: 3, Draws: 3, LoadedSlots: 5, CapacitySlots: 8);
var diagnostics = new WorldRenderDiagnostics(log);
var facts = new TerrainRenderDiagnosticFacts(3, 3, 5, 8);
diagnostics.BeginTerrainDraw();
diagnostics.EndTerrainDraw();
Assert.Throws<InvalidOperationException>(() =>
diagnostics.PublishTerrainDiagnostics(facts));
Assert.Throws<InvalidOperationException>(() => diagnostics.PublishTerrainDiagnostics(facts));
diagnostics.BeginTerrainDraw();
diagnostics.EndTerrainDraw();
diagnostics.PublishTerrainDiagnostics(facts);
Assert.Single(log.Messages);
Assert.Contains("draws=3/frame", log.Messages[0]);
Assert.Contains("visible=3", log.Messages[0]);
Assert.Contains("loaded=5", log.Messages[0]);
Assert.Contains("capacity=8", log.Messages[0]);
}
[Fact]
public void RenderSignature_LogsOnlyChangesAndCarriesStableCount()
{
var log = new RecordingLog();
var diagnostics = new WorldRenderDiagnostics(new RecordingGlStateReader(), log);
EmitMinimalSignature(diagnostics, "flat");
EmitMinimalSignature(diagnostics, "flat");
EmitMinimalSignature(diagnostics, "pview");
Assert.Collection(
log.Messages,
first => Assert.StartsWith("[render-sig] frame=1 stable=0 branch=flat", first),
changed => Assert.StartsWith("[render-sig] frame=3 stable=1 branch=pview", changed));
}
private static RenderGlStateSnapshot State(int depthFunction) => new(
DepthTest: true,
DepthWrite: true,
DepthFunction: depthFunction,
Blend: false,
BlendSource: 1,
BlendDestination: 0,
CullFace: true,
CullMode: 0x405,
FrontFace: 0x900,
Scissor: false,
ScissorBox: new IntRenderRectangle(0, 0, 1280, 720),
Viewport: new IntRenderRectangle(0, 0, 1280, 720),
DrawFramebuffer: 0,
AlphaToCoverage: false,
Stencil: false,
ClipBits: 0,
Error: 0);
private sealed class RecordingGlStateReader : IRenderGlStateReader
{
public RenderGlStateSnapshot State { get; set; } = WorldRenderDiagnosticsTests.State(0x201);
public RenderGlScissorSnapshot Scissor { get; set; }
public int StateCaptureCount { get; private set; }
public int ScissorCaptureCount { get; private set; }
public RenderGlStateSnapshot CaptureState()
{
StateCaptureCount++;
return State;
}
public RenderGlScissorSnapshot CaptureScissor()
{
ScissorCaptureCount++;
return Scissor;
}
}
private sealed class RecordingLog : IRenderFrameDiagnosticLog
{
public List<string> Messages { get; } = [];
public void WriteLine(string message) => Messages.Add(message);
string message = Assert.Single(log.Messages);
Assert.Contains("draws=3/frame", message);
Assert.Contains("visible=3", message);
Assert.Contains("loaded=5", message);
Assert.Contains("capacity=8", message);
}
private sealed class ThrowOnceLog : IRenderFrameDiagnosticLog
@ -207,33 +42,4 @@ public sealed class WorldRenderDiagnosticsTests
Messages.Add(message);
}
}
private static void EmitMinimalSignature(
WorldRenderDiagnostics diagnostics,
string branch) =>
diagnostics.EmitRenderSignatureIfChanged(
enabled: true,
branch: branch,
clipRoot: null,
viewerRoot: null,
playerRoot: null,
viewerCellId: 0,
playerCellId: 0,
playerIndoorGate: false,
cameraInsideCell: false,
renderSkyGate: false,
drawSkyThisFrame: false,
terrainDrawn: false,
terrainClipMode: TerrainClipMode.Skip,
skyDrawn: false,
depthClear: false,
outdoorSceneryDrawn: false,
liveDynamicDrawnCount: 0,
sceneParticles: "none",
visibleCells: null,
clipAssembly: null,
drawableCells: null,
partition: null,
cameraPosition: Vector3.Zero,
playerPosition: Vector3.Zero);
}

View file

@ -198,7 +198,6 @@ public sealed class WorldSceneRendererTests
"passes:disable-clip",
"particles:global",
"flat:weather",
"diagnostics:signature:OutdoorRoot",
"diagnostics:draw",
"alpha:end",
"visibility:complete",
@ -238,10 +237,8 @@ public sealed class WorldSceneRendererTests
"passes:begin",
"pview:draw",
"visibility:mark",
"diagnostics:pview",
"passes:disable-clip",
"particles:none",
"diagnostics:signature:RetailPViewInside",
"particles:skipped",
"diagnostics:draw",
"alpha:end",
"visibility:complete",
@ -307,7 +304,7 @@ public sealed class WorldSceneRendererTests
}
[Fact]
public void OutdoorPView_UsesPViewOwnersForPostWorldParticlesWithoutFlatWeather()
public void OutdoorPView_SkipsPostWorldParticleReplayAndFlatWeather()
{
var root = new LoadedCell
{
@ -319,7 +316,7 @@ public sealed class WorldSceneRendererTests
WorldRenderFrameOutcome result = rig.Renderer.Render(default);
Assert.True(result.NormalWorldDrawn);
Assert.Contains("particles:pviewScoped", rig.Calls);
Assert.Contains("particles:skipped", rig.Calls);
Assert.DoesNotContain("flat:weather", rig.Calls);
}
@ -498,7 +495,6 @@ public sealed class WorldSceneRendererTests
Type[] borrowedTypes =
[
typeof(RetailPViewFrameResult),
typeof(PortalVisibilityFrame),
typeof(ClipFrameAssembly),
];
@ -690,9 +686,7 @@ public sealed class WorldSceneRendererTests
new WbFrustum());
_clipFrame = ClipFrame.NoClip();
var glDiagnostics = new WorldRenderDiagnostics(
new DefaultGlStateReader(),
new NullDiagnosticLog());
var glDiagnostics = new WorldRenderDiagnostics(new NullDiagnosticLog());
var terrainDiagnostics = new TerrainDrawDiagnosticsController(
enabled: false,
glDiagnostics,
@ -710,7 +704,6 @@ public sealed class WorldSceneRendererTests
particleRenderer: null,
portalDepthMask: null,
alpha,
glDiagnostics,
terrainDiagnostics);
const uint landblockId = 0xF4180000u;
@ -859,13 +852,6 @@ public sealed class WorldSceneRendererTests
}
}
private sealed class DefaultGlStateReader : IRenderGlStateReader
{
public RenderGlStateSnapshot CaptureState() => default;
public RenderGlScissorSnapshot CaptureScissor() => default;
}
private sealed class NullDiagnosticLog : IRenderFrameDiagnosticLog
{
public void WriteLine(string message) { }
@ -1093,20 +1079,13 @@ public sealed class WorldSceneRendererTests
uint? playerLandblockId,
HashSet<uint> animatedEntityIds) => calls.Add("flat:entities");
public string DrawPostWorldParticles(
public void DrawPostWorldParticles(
LoadedCell? clipRoot,
ClipFrameAssembly? clipAssembly,
in WorldCameraFrame camera,
string currentSignature)
in WorldCameraFrame camera)
{
string kind = clipRoot switch
{
null => "global",
{ IsOutdoorNode: true } => currentSignature,
_ => currentSignature,
};
string kind = clipRoot is null ? "global" : "skipped";
calls.Add($"particles:{kind}");
return kind;
}
public void DrawFlatWeather(
@ -1135,37 +1114,6 @@ public sealed class WorldSceneRendererTests
{
public CameraCellResolution CameraCellResolution => CameraCellResolution.None;
public void EmitPViewInput(
IReadOnlySet<uint> visibleCells,
int outsideViewCount,
Matrix4x4 viewProjection,
LoadedCell clipRoot,
Vector3 cameraPosition,
Vector3 playerPosition) => calls.Add("diagnostics:pview");
public void EmitRenderSignature(
string branch,
LoadedCell? clipRoot,
LoadedCell? viewerRoot,
LoadedCell? playerRoot,
uint viewerCellId,
uint playerCellId,
bool playerIndoorGate,
bool cameraInsideCell,
bool renderSky,
bool drawSkyThisFrame,
bool terrainDrawn,
TerrainClipMode terrainClipMode,
bool skyDrawn,
bool depthClear,
bool outdoorSceneryDrawn,
int liveDynamicDrawnCount,
string sceneParticles,
RetailPViewFrameResult? pviewResult,
Vector3 cameraPosition,
Vector3 playerPosition) =>
calls.Add($"diagnostics:signature:{branch}");
public WorldSceneDiagnosticOutcome DrawAndPublish(
in WorldCameraFrame camera,
IReadOnlyList<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax)> bounds)

View file

@ -5,175 +5,24 @@ namespace AcDream.Core.Tests.Rendering;
public sealed class RenderingDiagnosticsTests
{
// Each flag-mutating test snapshots the IndoorAll state on entry and
// restores it via try/finally. RenderingDiagnostics is a process-wide
// static (env-var-initialized); without restoration a mutated state
// leaks into other tests + into parallel test runs. Mirrors the
// PhysicsDiagnosticsTests pattern at line 30-49.
[Fact]
public void IndoorAll_True_TurnsAllFlagsOn()
{
bool initial = RenderingDiagnostics.IndoorAll;
try
{
// Reset all flags off first to make the test deterministic
// regardless of env-var state on the test runner.
RenderingDiagnostics.ProbeIndoorWalkEnabled = false;
RenderingDiagnostics.ProbeIndoorLookupEnabled = false;
RenderingDiagnostics.ProbeIndoorUploadEnabled = false;
RenderingDiagnostics.ProbeIndoorXformEnabled = false;
RenderingDiagnostics.ProbeIndoorCullEnabled = false;
RenderingDiagnostics.IndoorAll = true;
Assert.True(RenderingDiagnostics.ProbeIndoorWalkEnabled);
Assert.True(RenderingDiagnostics.ProbeIndoorLookupEnabled);
Assert.True(RenderingDiagnostics.ProbeIndoorUploadEnabled);
Assert.True(RenderingDiagnostics.ProbeIndoorXformEnabled);
Assert.True(RenderingDiagnostics.ProbeIndoorCullEnabled);
Assert.True(RenderingDiagnostics.IndoorAll);
}
finally
{
RenderingDiagnostics.IndoorAll = initial;
}
}
[Fact]
public void IndoorAll_False_TurnsAllFlagsOff()
{
bool initial = RenderingDiagnostics.IndoorAll;
try
{
RenderingDiagnostics.IndoorAll = true; // start from all-on
RenderingDiagnostics.IndoorAll = false;
Assert.False(RenderingDiagnostics.ProbeIndoorWalkEnabled);
Assert.False(RenderingDiagnostics.ProbeIndoorLookupEnabled);
Assert.False(RenderingDiagnostics.ProbeIndoorUploadEnabled);
Assert.False(RenderingDiagnostics.ProbeIndoorXformEnabled);
Assert.False(RenderingDiagnostics.ProbeIndoorCullEnabled);
Assert.False(RenderingDiagnostics.IndoorAll);
}
finally
{
RenderingDiagnostics.IndoorAll = initial;
}
}
[Fact]
public void IndoorAll_OneOff_ReadsAsFalse()
{
bool initial = RenderingDiagnostics.IndoorAll;
try
{
RenderingDiagnostics.IndoorAll = true;
RenderingDiagnostics.ProbeIndoorCullEnabled = false; // flip one off
Assert.False(RenderingDiagnostics.IndoorAll);
}
finally
{
RenderingDiagnostics.IndoorAll = initial;
}
}
[Theory]
[InlineData(0x00000029ul, false)]
[InlineData(0xA9B40029ul, false)]
[InlineData(0x00000100ul, true)]
[InlineData(0xA9B40105ul, true)]
public void IsEnvCellId_DistinguishesOutdoorVsIndoorByLow16Bits(ulong id, bool expected)
=> Assert.Equal(expected, RenderingDiagnostics.IsEnvCellId(id));
[Theory]
[InlineData(0x00000029ul, false)] // outdoor cell 0x29 in 8x8 grid
[InlineData(0xA9B40029ul, false)] // outdoor cell with landblock prefix
[InlineData(0x00000100ul, true)] // indoor cell minimum
[InlineData(0x00000105ul, true)] // typical Holtburg Inn interior
[InlineData(0xA9B40105ul, true)] // indoor with landblock prefix
[InlineData(0xA9B401FFul, true)] // indoor near top of range
public void IsEnvCellId_DistinguishesOutdoorVsIndoorByLow16Bits(ulong id, bool expected)
{
Assert.Equal(expected, RenderingDiagnostics.IsEnvCellId(id));
}
// ── Render inside/outside branch (retail RenderNormalMode is_player_outside) ──
// The top-level render branch decides DrawInside vs DrawOutside. Retail
// (SmartBox::RenderNormalMode 0x453aa0:92665) keys it on is_player_outside (the
// PLAYER's cell, 0x451e80), NOT the camera cell. acdream previously branched on the
// camera cell, so a chase camera lagging in a doorway while the player was already
// outside took the DrawInside path and degenerated to a grey world + entities showing
// through walls. These pin the player-keyed branch and loaded player-root requirement.
[Fact]
public void ShouldRenderIndoor_PlayerOutside_CameraInside_ReturnsFalse()
{
// THE doorway-grey regression: the player stepped onto a landcell (0x...0031) but the
// chase camera still resolves an interior EnvCell. Branch on the PLAYER → outdoor.
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40031u, renderRootResolved: true));
}
[Fact]
public void ShouldRenderIndoor_PlayerInside_CameraInside_ReturnsTrue()
{
Assert.True(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40171u, renderRootResolved: true));
}
[Fact]
public void ShouldRenderIndoor_PlayerInside_RootNotLoaded_ReturnsFalse()
{
// Opposite lag (camera pulled outside while the player is inside): no viewer cell to
// root DrawInside at → outdoor. Defensive; matches prior null-CameraCell behavior.
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40171u, renderRootResolved: false));
}
[Fact]
public void ShouldRenderIndoor_PlayerOutside_CameraOutside_ReturnsFalse()
{
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40031u, renderRootResolved: false));
}
[Fact]
public void ShouldRenderIndoor_UnknownPlayerCell_TreatedAsOutside_ReturnsFalse()
{
// playerCellId == 0 (unresolved) → treat as outside (safe default: outdoor render).
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0u, renderRootResolved: true));
}
[Fact]
public void ProbePortalChurn_DefaultsFalse_WhenEnvUnset()
{
// Env var is absent in the test host, so the flag must default false (inert probe).
Assert.False(AcDream.Core.Rendering.RenderingDiagnostics.ProbePortalChurnEnabled);
}
// ── ACDREAM_DUMP_ENTITY parser (#119 decisive probe) ──────────────────
[Fact]
public void ParseDumpEntityIds_NullOrEmpty_ReturnsEmptySet()
{
Assert.Empty(RenderingDiagnostics.ParseDumpEntityIds(null));
Assert.Empty(RenderingDiagnostics.ParseDumpEntityIds(""));
Assert.Empty(RenderingDiagnostics.ParseDumpEntityIds(" "));
}
[Fact]
public void ParseDumpEntityIds_CommaSeparatedWithPrefixes_ParsesAll()
{
var set = RenderingDiagnostics.ParseDumpEntityIds("0x020003F2,0x020005D8");
Assert.Equal(2, set.Count);
Assert.Contains(0x020003F2u, set);
Assert.Contains(0x020005D8u, set);
}
[Fact]
public void ParseDumpEntityIds_NoPrefixMixedCaseAndWhitespace_Parses()
{
var set = RenderingDiagnostics.ParseDumpEntityIds(" 020003f2 , 0X020005D8 ");
Assert.Equal(2, set.Count);
Assert.Contains(0x020003F2u, set);
Assert.Contains(0x020005D8u, set);
}
[Fact]
public void ParseDumpEntityIds_MalformedSegment_IgnoredNotFatal()
{
// A typo'd segment must not take the launch down — parse what's valid.
var set = RenderingDiagnostics.ParseDumpEntityIds("0x020003F2,zzz,,0x");
Assert.Single(set);
Assert.Contains(0x020003F2u, set);
}
[InlineData(0xA9B40031u, true, false)]
[InlineData(0xA9B40171u, true, true)]
[InlineData(0xA9B40171u, false, false)]
[InlineData(0u, true, false)]
public void ShouldRenderIndoor_UsesPlayerCellAndResolvedRoot(
uint playerCellId,
bool renderRootResolved,
bool expected)
=> Assert.Equal(
expected,
RenderingDiagnostics.ShouldRenderIndoor(playerCellId, renderRootResolved));
}

View file

@ -1,145 +0,0 @@
// Phase A8 — visibility probe flag tests.
// Phase U.2d (2026-05-30) — EmitVis formatter + cell-change gating tests.
using System;
using System.Collections.Generic;
using System.IO;
using AcDream.Core.Rendering;
using Xunit;
namespace AcDream.Core.Tests.Rendering;
public class RenderingDiagnosticsVisibilityTests
{
// RenderingDiagnostics is a process-wide static (env-var-initialized) and
// EmitVis keeps a private last-root-cell field. Both leak between tests +
// parallel runs if not restored, and this codebase has documented
// static-leak flakiness — so every test here snapshots ProbeVisibilityEnabled,
// resets the cell tracker, and restores in finally. Mirrors the
// RenderingDiagnosticsTests / PhysicsDiagnosticsTests pattern.
[Fact]
public void ProbeVisibilityEnabled_CanBeToggled()
{
var prev = RenderingDiagnostics.ProbeVisibilityEnabled;
try
{
RenderingDiagnostics.ProbeVisibilityEnabled = true;
Assert.True(RenderingDiagnostics.ProbeVisibilityEnabled);
RenderingDiagnostics.ProbeVisibilityEnabled = false;
Assert.False(RenderingDiagnostics.ProbeVisibilityEnabled);
}
finally
{
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
}
}
[Fact]
public void ProbeVisibilityEnabled_DefaultsToEnvVarPresence()
{
// The static initializer reads ACDREAM_PROBE_VIS == "1". Reconstruct the
// documented default from the current env and assert the property's
// *defined* default matches it. (We can't re-run the static initializer,
// but the initial value is a pure function of the env var, so this pins
// the documented contract without depending on the runner's env.)
bool expected = Environment.GetEnvironmentVariable("ACDREAM_PROBE_VIS") == "1";
var prev = RenderingDiagnostics.ProbeVisibilityEnabled;
try
{
RenderingDiagnostics.ResetVisibilityProbeForTests();
// Drive the property to the env-derived default explicitly, then read
// it back — this asserts the property faithfully stores the env value
// (the same expression the field initializer uses).
RenderingDiagnostics.ProbeVisibilityEnabled = expected;
Assert.Equal(expected, RenderingDiagnostics.ProbeVisibilityEnabled);
}
finally
{
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
}
}
[Fact]
public void EmitVis_NoOp_WhenProbeDisabled()
{
var prev = RenderingDiagnostics.ProbeVisibilityEnabled;
var prevOut = Console.Out;
try
{
RenderingDiagnostics.ResetVisibilityProbeForTests();
RenderingDiagnostics.ProbeVisibilityEnabled = false;
using var sw = new StringWriter();
Console.SetOut(sw);
RenderingDiagnostics.EmitVis(
rootCellId: 0xA9B40105u,
visibleCells: new uint[] { 0xA9B40105u, 0xA9B40164u },
outsidePolyCount: 1,
outsidePlaneCount: 4,
perCellPlaneCounts: new Dictionary<uint, int> { [0xA9B40105u] = 0, [0xA9B40164u] = 4 },
scissorFallbacks: 1);
Console.SetOut(prevOut);
Assert.Equal(string.Empty, sw.ToString());
}
finally
{
Console.SetOut(prevOut);
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
RenderingDiagnostics.ResetVisibilityProbeForTests();
}
}
[Fact]
public void EmitVis_FiresOnceOnNewRoot_SuppressedOnUnchangedRoot()
{
var prev = RenderingDiagnostics.ProbeVisibilityEnabled;
var prevOut = Console.Out;
try
{
RenderingDiagnostics.ResetVisibilityProbeForTests();
RenderingDiagnostics.ProbeVisibilityEnabled = true;
using var sw = new StringWriter();
Console.SetOut(sw);
var cells = new uint[] { 0xA9B40105u, 0xA9B40164u };
var perCell = new Dictionary<uint, int> { [0xA9B40105u] = 0, [0xA9B40164u] = 4 };
// First call on a fresh root → fires.
RenderingDiagnostics.EmitVis(0xA9B40105u, cells, 1, 4, perCell, 0);
// Same root again → suppressed (no-op).
RenderingDiagnostics.EmitVis(0xA9B40105u, cells, 1, 4, perCell, 0);
// New root → fires again.
RenderingDiagnostics.EmitVis(0xA9B40164u, cells, 2, 8, perCell, 1);
Console.SetOut(prevOut);
string output = sw.ToString();
string[] lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
// Exactly two [vis] lines: one per distinct root transition.
Assert.Equal(2, lines.Length);
Assert.All(lines, l => Assert.StartsWith("[vis]", l.TrimStart()));
Assert.Contains("root=0xA9B40105", lines[0]);
Assert.Contains("root=0xA9B40164", lines[1]);
// Information-density spot checks on the first line.
Assert.Contains("cells=2", lines[0]);
Assert.Contains("0xA9B40105", lines[0]);
Assert.Contains("0xA9B40164", lines[0]);
Assert.Contains("polys=1", lines[0]);
Assert.Contains("planes=4", lines[0]);
Assert.Contains("fallbacks=0", lines[0]);
}
finally
{
Console.SetOut(prevOut);
RenderingDiagnostics.ProbeVisibilityEnabled = prev;
RenderingDiagnostics.ResetVisibilityProbeForTests();
}
}
}