acdream/tests/AcDream.App.Tests/Rendering/WalkOutsideViewReassemblyTests.cs
Erik bf53e2ad6e 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.
2026-09-05 05:18:17 +02:00

403 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Linq;
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Walk;
using AcDream.App.Tests.Rendering.Gpu;
using Xunit;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// Campaign FW4 slice 1 — <see cref="ClipFrameAssembler.ReassembleOutsideViewFromWalk"/>:
/// an interior root's outside-view slices come from the walk's OWN
/// <c>outside_view</c> (pixel screen points, origin top-left, +Y down),
/// converted to the assembler's standard-NDC <see cref="ViewPolygon"/>s.
/// </summary>
public class WalkOutsideViewReassemblyTests
{
private const float W = 1024f, H = 720f;
private sealed class StubRays : IWalkRayCaster
{
public Vector3 RayThrough(float screenX, float screenY) =>
Vector3.Normalize(new Vector3(screenX - W / 2f, screenY - H / 2f, 1000f));
}
private static WalkPortalView WalkViewOfPixelQuads(params Vector2[][] pixelQuads)
{
var view = new WalkPortalView();
var rays = new StubRays();
foreach (Vector2[] quad in pixelQuads)
{
var pts = new WalkScreenPoint[quad.Length];
for (int i = 0; i < quad.Length; i++)
pts[i] = new WalkScreenPoint(quad[i].X, quad[i].Y, 0f, 1f);
Assert.True(WalkCopyView.Append(view, pts, rays, Vector3.Zero));
}
return view;
}
private static ClipFrameAssembly BeginAssembly()
=> ClipFrameAssembler.BeginWalkFrame(ClipFrame.NoClip(), outdoorRoot: true);
[Fact]
public void PixelDoorway_MapsToExpectedNdcAabbAndPlanes()
{
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[]
{
new Vector2(256f, 180f), new Vector2(512f, 180f),
new Vector2(512f, 360f), new Vector2(256f, 360f),
});
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
ClipViewSlice slice = Assert.Single(asm.OutsideViewSlices);
Assert.True(asm.OutdoorVisible);
Assert.Equal(4, asm.OutsidePlaneCount);
Assert.Equal(slice.Slot, asm.OutdoorSlot);
Assert.NotEqual(0, slice.Slot);
Assert.Equal(-0.5f, slice.NdcAabb.X, 3);
Assert.Equal(0f, slice.NdcAabb.Y, 3);
Assert.Equal(0f, slice.NdcAabb.Z, 3);
Assert.Equal(0.5f, slice.NdcAabb.W, 3);
Assert.Equal(slice.NdcAabb, asm.OutsideViewNdcAabb);
// Every corner of the doorway satisfies every inward plane
// (n·p + d >= 0), and a point far outside fails at least one.
Vector2[] ndcCorners =
{
new(-0.5f, 0f), new(0f, 0f), new(0f, 0.5f), new(-0.5f, 0.5f),
};
foreach (Vector2 corner in ndcCorners)
{
foreach (Vector4 plane in slice.Planes)
Assert.True(plane.X * corner.X + plane.Y * corner.Y + plane.W >= -1e-4f);
}
bool outsideFails = false;
foreach (Vector4 plane in slice.Planes)
outsideFails |= plane.X * 0.9f + plane.Y * -0.9f + plane.W < 0f;
Assert.True(outsideFails);
}
[Fact]
public void FullViewportWalkQuad_CoversFullNdc()
{
ClipFrameAssembly asm = BeginAssembly();
var walkView = new WalkPortalView();
Assert.True(WalkCopyView.AppendFullViewportQuad(
walkView, new StubRays(), Vector3.Zero, W, H));
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
ClipViewSlice slice = Assert.Single(asm.OutsideViewSlices);
Assert.Equal(new Vector4(-1f, -1f, 1f, 1f), slice.NdcAabb);
}
[Fact]
public void EmptyWalkView_YieldsSkipMode()
{
ClipFrameAssembly asm = BeginAssembly();
Assert.True(asm.OutdoorVisible);
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, new WalkPortalView(), W, H);
Assert.Empty(asm.OutsideViewSlices);
Assert.False(asm.OutdoorVisible);
Assert.False(asm.HasOutsideView);
Assert.Equal(0, asm.OutsidePlaneCount);
Assert.Equal(0, asm.OutdoorSlot);
}
/// <summary>
/// S3 review fix round 1 (F1, BLOCKING): a two-view walk whose FIRST
/// polygon collapses (all three points exactly collinear — zero area,
/// well under <c>ClipPlaneSet</c>'s <c>MinPolygonArea</c>) must still
/// produce a length-2 slice array, index-aligned with the walk's own
/// view count: slice[0] is the collapsed view (flagged
/// <see cref="ClipViewSlice.NothingVisible"/>), slice[1] is the SECOND
/// polygon's real edge planes — not the (nonexistent) first view's, and
/// not shifted down by one. Constructs the <see cref="WalkPortalView"/>
/// directly (bypassing <see cref="WalkCopyView.Append"/>'s own pixel-
/// space collinearity filter, which is not the same test as
/// <c>ClipPlaneSet</c>'s NDC-area gate) so the collapse is exact and
/// deterministic.
/// </summary>
[Fact]
public void FirstViewCollapses_SecondSurvives_SlicesStayIndexAligned()
{
ClipFrameAssembly asm = BeginAssembly();
var walkView = new WalkPortalView();
// View 0: three EXACTLY collinear pixel points -> zero-area triangle,
// collinear before and after the affine pixel->NDC transform.
walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(100f, 100f) });
walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(200f, 100f) });
walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(300f, 100f) });
walkView.View.Polys.Add(new WalkViewPoly(3, 0, 100f, 300f, 100f, 100f));
// View 1: a normal quad.
int secondBase = walkView.View.Vertices.Count;
Vector2[] secondQuad =
[
new(600f, 400f), new(900f, 400f), new(900f, 650f), new(600f, 650f),
];
foreach (Vector2 pixel in secondQuad)
walkView.View.Vertices.Add(new WalkViewVertex { Point = pixel });
walkView.View.Polys.Add(new WalkViewPoly(4, secondBase, 600f, 900f, 400f, 650f));
walkView.ViewCount = 2;
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
Assert.Equal(2, asm.OutsideViewSlices.Length);
ClipViewSlice collapsed = asm.OutsideViewSlices[0];
Assert.True(collapsed.NothingVisible);
Assert.Equal(0, collapsed.Slot);
Assert.Empty(collapsed.Planes);
Assert.Equal(default, collapsed.NdcAabb);
ClipViewSlice survivor = asm.OutsideViewSlices[1];
Assert.False(survivor.NothingVisible);
// The second polygon's own edge planes, converted from pixel space
// (px=600..900, py=400..650) into the assembler's standard NDC.
var expectedNdcVerts = new Vector2[secondQuad.Length];
for (int i = 0; i < secondQuad.Length; i++)
{
expectedNdcVerts[i] = new Vector2(
secondQuad[i].X / W * 2f - 1f,
1f - secondQuad[i].Y / H * 2f);
}
AssertEveryEdgeMidpointLiesOnSomeGpuPlane(expectedNdcVerts, survivor.Planes);
}
/// <summary>
/// F1's punch-leaf half: <c>RetailPViewPassExecutor.DrawWalkPunchFan</c>
/// reading the SAME index-aligned array produced above. Uses a recording
/// <see cref="RecordingGpuDevice"/> surface so the assertion inspects the
/// actual GPU submission (or its absence), not just the CPU slice.
/// activeViewIndex=1 (the survivor) submits a fan with the survivor's
/// planes; activeViewIndex=0 (the collapsed view) submits NOTHING;
/// activeViewIndex=2 (out of range) throws (fail-loud rule).
/// </summary>
[Fact]
public void PunchLeaf_UsesIndexAlignedSlice_DrawsNothingForCollapsed_ThrowsOutOfRange()
{
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) });
walkView.View.Vertices.Add(new WalkViewVertex { Point = new Vector2(300f, 100f) });
walkView.View.Polys.Add(new WalkViewPoly(3, 0, 100f, 300f, 100f, 100f));
int secondBase = walkView.View.Vertices.Count;
Vector2[] secondQuad =
[
new(600f, 400f), new(900f, 400f), new(900f, 650f), new(600f, 650f),
];
foreach (Vector2 pixel in secondQuad)
walkView.View.Vertices.Add(new WalkViewVertex { Point = pixel });
walkView.View.Polys.Add(new WalkViewPoly(4, secondBase, 600f, 900f, 400f, 650f));
walkView.ViewCount = 2;
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
Assert.Equal(2, asm.OutsideViewSlices.Length);
using var device = new RecordingGpuDevice();
var frames = new GpuDeviceFrameLifetime(device);
var scope = new VulkanWorldPassScope(sampleCount: 1);
using var portalDepthMask = new PortalDepthMaskRenderer(device, frames, scope);
// DrawWalkPunchFan only reads _portalDepthMask, worldPolygon.Vertices,
// clipAssembly.OutsideViewSlices, and frame.ViewProjection — a bare
// (constructor-bypassed) executor with only that one field set
// exercises the real leaf without needing a full GL/DAT renderer
// graph. Same GetUninitializedObject pattern WorldRenderCompositionTests
// already uses for stubbing App-layer types.
var executor = (RetailPViewPassExecutor)System.Runtime.CompilerServices.RuntimeHelpers
.GetUninitializedObject(typeof(RetailPViewPassExecutor));
typeof(RetailPViewPassExecutor)
.GetField("_portalDepthMask", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!
.SetValue(executor, portalDepthMask);
var worldPolygon = new WalkPolygon
{
Vertices = new[]
{
new Vector3(-1f, -1f, 0f), new Vector3(1f, -1f, 0f), new Vector3(0f, 1f, 0f),
},
};
RetailPViewFrameInput frame = new RetailPViewFrameInput().Reset(
rootCell: null!,
nearbyBuildingCells: null,
viewerEyePos: Vector3.Zero,
viewProjection: Matrix4x4.Identity,
cells: null!,
camera: null!,
cameraWorldPosition: Vector3.Zero,
frustum: null,
playerLandblockId: null,
animatedEntityIds: null,
renderCenterLbX: 0,
renderCenterLbY: 0,
renderRadius: 0,
landblockEntries: Array.Empty<(uint, Vector3, Vector3, IReadOnlyList<AcDream.Core.World.WorldEntity>, IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity>?)>(),
renderSky: false,
renderWeather: false,
dayFraction: 0f,
activeDayGroup: null,
skyKeyframe: default,
environOverrideActive: false,
viewerCellId: 0,
playerCellId: 0,
playerViewPosition: Vector3.Zero,
cameraView: Matrix4x4.Identity,
cameraCellResolution: default);
void RunDraw(int activeViewIndex)
{
frames.BeginFrame();
portalDepthMask.BeginFrame(frameSlot: 0);
using (IGpuPassEncoder pass = frames.CurrentFrame!.BeginPass(
GpuPassDescription.BackbufferClear(
"s3-f1-punch-leaf", Vector4.Zero, sampleCount: 1)))
using (scope.Publish(pass))
{
executor.DrawWalkPunchFan(frame, asm, worldPolygon, activeViewIndex);
}
frames.EndFrame();
}
// activeViewIndex = 0: the collapsed view. Draws nothing.
int drawsBefore = device.Calls.OfType<GpuRecordedDraw>().Count();
RunDraw(0);
int drawsAfterCollapsed = device.Calls.OfType<GpuRecordedDraw>().Count();
Assert.Equal(drawsBefore, drawsAfterCollapsed);
// activeViewIndex = 1: the surviving view. Submits exactly one fan draw.
RunDraw(1);
int drawsAfterSurvivor = device.Calls.OfType<GpuRecordedDraw>().Count();
Assert.Equal(drawsAfterCollapsed + 1, drawsAfterSurvivor);
// activeViewIndex = 2: out of range. Fails loud instead of drawing unclipped.
var ex = Assert.Throws<ArgumentOutOfRangeException>(() =>
{
frames.BeginFrame();
portalDepthMask.BeginFrame(frameSlot: 0);
using (IGpuPassEncoder pass = frames.CurrentFrame!.BeginPass(
GpuPassDescription.BackbufferClear(
"s3-f1-punch-leaf-oob", Vector4.Zero, sampleCount: 1)))
using (scope.Publish(pass))
{
executor.DrawWalkPunchFan(frame, asm, worldPolygon, 2);
}
});
Assert.Contains("activeViewIndex", ex.Message);
}
/// <summary>Same CPU/GPU equivalence check as
/// <c>ClipFrameLayoutTests.AssertEveryEdgeMidpointLiesOnSomeGpuPlane</c>
/// (private there, duplicated here): every edge midpoint of the source
/// NDC polygon must be non-negative under every plane and ~zero under at
/// least one (its own edge's plane).</summary>
private static void AssertEveryEdgeMidpointLiesOnSomeGpuPlane(
Vector2[] verts, System.ReadOnlySpan<Vector4> gpuPlanes)
{
const float eps = 1e-4f;
for (int i = 0; i < verts.Length; i++)
{
Vector2 a = verts[i];
Vector2 b = verts[(i + 1) % verts.Length];
Vector2 mid = (a + b) / 2f;
var clip = new Vector4(mid.X, mid.Y, 0f, 1f);
float minAbsDistance = float.PositiveInfinity;
foreach (Vector4 plane in gpuPlanes)
{
float distance = Vector4.Dot(plane, clip);
Assert.True(
distance >= -eps,
$"edge {i} midpoint ({mid.X},{mid.Y}) must be inside-or-on every "
+ $"GPU plane; plane {plane} gave distance {distance}");
minAbsDistance = MathF.Min(minAbsDistance, MathF.Abs(distance));
}
Assert.True(
minAbsDistance < eps,
$"edge {i} midpoint ({mid.X},{mid.Y}) should lie ~on its OWN GPU plane; "
+ $"the closest plane was only {minAbsDistance} away");
}
}
/// <summary>
/// S3 review fix round 1 (F2): the outside-view sibling of
/// <c>WalkFrameDriverClipSealTests.ExitSealPath_NineVertexView_...</c> —
/// a 9-vertex outside view is too complex for the &lt;=8-plane budget
/// (<see cref="ClipPlaneSet.IsPlaneOverflow"/>), but unlike the exit-seal
/// path (which falls back to a conservative 4-plane AABB) the outside
/// view/punch-fan path has NO scissor consumer at all (S3 chunk 4 fix
/// round 2, L2) — it draws fully UNCLIPPED: <see cref="ClipViewSlice.Slot"/>
/// stays 0 with an EMPTY <see cref="ClipViewSlice.Planes"/> array, and
/// critically <see cref="ClipViewSlice.NothingVisible"/> is FALSE — this
/// is the "draw unclipped" state F1 distinguishes from the "draw
/// nothing" collapsed-view state.
/// </summary>
[Fact]
public void NineVertexOutsideView_PunchSliceHasZeroPlanes_DrawsUnclipped_NotNothingVisible()
{
ClipFrameAssembly asm = BeginAssembly();
const int n = 9;
var verts = new Vector2[n];
for (int i = 0; i < n; i++)
{
float angle = i * MathF.Tau / n;
verts[i] = new Vector2(0.6f * MathF.Cos(angle), 0.6f * MathF.Sin(angle));
}
var pixelPoints = new WalkScreenPoint[n];
for (int i = 0; i < n; i++)
{
pixelPoints[i] = new WalkScreenPoint(
(verts[i].X + 1f) * W / 2f, (1f - verts[i].Y) * H / 2f, 0f, 1f);
}
var walkView = new WalkPortalView();
Assert.True(WalkCopyView.Append(walkView, pixelPoints, new StubRays(), Vector3.Zero));
// No collinear-merge stole a vertex.
Assert.Equal(n, walkView.View.Polys[0].VertexCount);
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
ClipViewSlice slice = Assert.Single(asm.OutsideViewSlices);
Assert.False(slice.NothingVisible);
Assert.Equal(0, slice.Slot);
Assert.Empty(slice.Planes);
}
[Fact]
public void TwoWalkViews_ProduceTwoSlicesWithDistinctSlots()
{
ClipFrameAssembly asm = BeginAssembly();
WalkPortalView walkView = WalkViewOfPixelQuads(
new[]
{
new Vector2(100f, 100f), new Vector2(300f, 100f),
new Vector2(300f, 300f), new Vector2(100f, 300f),
},
new[]
{
new Vector2(600f, 400f), new Vector2(900f, 400f),
new Vector2(900f, 650f), new Vector2(600f, 650f),
});
ClipFrameAssembler.ReassembleOutsideViewFromWalk(asm, walkView, W, H);
Assert.Equal(2, asm.OutsideViewSlices.Length);
Assert.NotEqual(asm.OutsideViewSlices[0].Slot, asm.OutsideViewSlices[1].Slot);
// The union AABB spans both doorways.
Assert.True(asm.OutsideViewNdcAabb.X < asm.OutsideViewSlices[0].NdcAabb.Z);
Assert.True(asm.OutsideViewNdcAabb.Z >= asm.OutsideViewSlices[1].NdcAabb.X);
}
}