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.
403 lines
17 KiB
C#
403 lines
17 KiB
C#
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 <=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);
|
||
}
|
||
}
|