feat(render): IndoorDrawPlan.ShellPass — every visible cell, no drawable filter (R1)

Pure port of retail DrawCells membership: reverse cell_draw_list, per-slice. Pins the
grey regression — a cell in OrderedVisibleCells is never dropped from the shell pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-06 21:59:26 +02:00
parent 2ec8f41200
commit bff1955066
2 changed files with 76 additions and 0 deletions

View file

@ -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<ViewPolygon> Slices);
public static class IndoorDrawPlan
{
/// <summary>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.</summary>
public static List<CellDrawEntry> ShellPass(PortalVisibilityFrame frame)
{
var result = new List<CellDrawEntry>(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;
}
}

View file

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