diff --git a/src/AcDream.App/Rendering/IndoorDrawPlan.cs b/src/AcDream.App/Rendering/IndoorDrawPlan.cs new file mode 100644 index 00000000..a3aab2a2 --- /dev/null +++ b/src/AcDream.App/Rendering/IndoorDrawPlan.cs @@ -0,0 +1,30 @@ +// IndoorDrawPlan.cs +// +// Pure (GL-free) port of the membership half of retail PView::DrawCells (0x5a4840): +// the reverse cell_draw_list iterated per portal_view slice. EVERY visible cell with a +// non-empty view is included — there is NO "drawable" filter. Dropping cells without a +// clip-slot was the grey-walls bug (the cell's sealed shell never drew → clear color showed). +using System.Collections.Generic; + +namespace AcDream.App.Rendering; + +public readonly record struct CellDrawEntry(uint CellId, IReadOnlyList Slices); + +public static class IndoorDrawPlan +{ + /// Reverse OrderedVisibleCells (far→near), each visible cell with its view + /// slices. Mirrors DrawCells' shell/object loops. Cells whose view is empty are skipped + /// (they are not actually visible); no other cell is ever dropped. + public static List ShellPass(PortalVisibilityFrame frame) + { + var result = new List(frame.OrderedVisibleCells.Count); + for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--) + { + uint cellId = frame.OrderedVisibleCells[i]; + if (!frame.CellViews.TryGetValue(cellId, out var view) || view.IsEmpty) + continue; + result.Add(new CellDrawEntry(cellId, view.Polygons)); + } + return result; + } +} diff --git a/tests/AcDream.App.Tests/Rendering/IndoorDrawPlanTests.cs b/tests/AcDream.App.Tests/Rendering/IndoorDrawPlanTests.cs new file mode 100644 index 00000000..6b0de649 --- /dev/null +++ b/tests/AcDream.App.Tests/Rendering/IndoorDrawPlanTests.cs @@ -0,0 +1,46 @@ +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()); + } +}