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); } }