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;
///
/// Campaign FW4 slice 1 — :
/// an interior root's outside-view slices come from the walk's OWN
/// outside_view (pixel screen points, origin top-left, +Y down),
/// converted to the assembler's standard-NDC s.
///
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 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);
}
[Fact]
public void PixelDoorway_MapsToExpectedNdcAabbAndPlanes()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
// 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 = AssembledWithOldOutside();
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 = AssembledWithOldOutside();
Assert.True(asm.OutdoorVisible); // the old view was visible pre-cutover
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);
}
///
/// S3 review fix round 1 (F1, BLOCKING): a two-view walk whose FIRST
/// polygon collapses (all three points exactly collinear — zero area,
/// well under ClipPlaneSet's MinPolygonArea) must still
/// produce a length-2 slice array, index-aligned with the walk's own
/// view count: slice[0] is the collapsed view (flagged
/// ), slice[1] is the SECOND
/// polygon's real edge planes — not the (nonexistent) first view's, and
/// not shifted down by one. Constructs the
/// directly (bypassing 's own pixel-
/// space collinearity filter, which is not the same test as
/// ClipPlaneSet's NDC-area gate) so the collapse is exact and
/// deterministic.
///
[Fact]
public void FirstViewCollapses_SecondSurvives_SlicesStayIndexAligned()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
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);
}
///
/// F1's punch-leaf half: RetailPViewPassExecutor.DrawWalkPunchFan
/// reading the SAME index-aligned array produced above. Uses a recording
/// 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).
///
[Fact]
public void PunchLeaf_UsesIndexAlignedSlice_DrawsNothingForCollapsed_ThrowsOutOfRange()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
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, IReadOnlyDictionary?)>(),
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().Count();
RunDraw(0);
int drawsAfterCollapsed = device.Calls.OfType().Count();
Assert.Equal(drawsBefore, drawsAfterCollapsed);
// activeViewIndex = 1: the surviving view. Submits exactly one fan draw.
RunDraw(1);
int drawsAfterSurvivor = device.Calls.OfType().Count();
Assert.Equal(drawsAfterCollapsed + 1, drawsAfterSurvivor);
// activeViewIndex = 2: out of range. Fails loud instead of drawing unclipped.
var ex = Assert.Throws(() =>
{
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);
}
/// Same CPU/GPU equivalence check as
/// ClipFrameLayoutTests.AssertEveryEdgeMidpointLiesOnSomeGpuPlane
/// (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).
private static void AssertEveryEdgeMidpointLiesOnSomeGpuPlane(
Vector2[] verts, System.ReadOnlySpan 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");
}
}
///
/// S3 review fix round 1 (F2): the outside-view sibling of
/// WalkFrameDriverClipSealTests.ExitSealPath_NineVertexView_... —
/// a 9-vertex outside view is too complex for the <=8-plane budget
/// (), 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:
/// stays 0 with an EMPTY array, and
/// critically is FALSE — this
/// is the "draw unclipped" state F1 distinguishes from the "draw
/// nothing" collapsed-view state.
///
[Fact]
public void NineVertexOutsideView_PunchSliceHasZeroPlanes_DrawsUnclipped_NotNothingVisible()
{
ClipFrameAssembly asm = AssembledWithOldOutside();
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 = AssembledWithOldOutside();
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);
}
}