acdream/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherClipSlotTests.cs
Erik 354ca746ad test(render): Phase U.4 — cover ResolveEntitySlot clip-slot resolution
Code review flagged the gate-critical per-instance slot resolution as untested.
Add RED→GREEN cases (live=unclipped slot 0, cell-static→cell slot, non-visible→cull,
outdoor-stab→OutsideView/cull, routing-inactive→all slot 0). Note the full-cell-id-space
invariant at ResolveEntitySlot; fix a stale RenderInsideOut comment in EnvCellRenderer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:16:21 +02:00

191 lines
7.6 KiB
C#

// Tests for WbDrawDispatcher's Phase U.4 per-instance clip-slot resolution
// (ResolveEntitySlot / ResolveSlotForFrame). Code review of the U.4 commit
// (7993e06) flagged this gate-critical routing as untested: if it breaks,
// every indoor instance is sent to the wrong clip slot (or wrongly culled),
// producing total visual garbage at the portal-visibility gate. The logic is
// a pure function of (ServerGuid, ParentCellId, the clip-routing state), so we
// extract it to internal static helpers and test the branches directly — no GL
// context required.
//
// Branch map (ResolveSlotForFrame, the call-site policy):
// routing inactive (outdoor root) → slot 0, NOT culled (≡ U.3)
// ServerGuid != 0 (live dynamic) → slot 0, NOT culled (unclipped)
// ParentCellId in cellIdToSlot → that cell's slot
// ParentCellId NOT in cellIdToSlot → CULL
// ParentCellId == null, outdoorVisible → outdoorSlot
// ParentCellId == null, !outdoorVisible → CULL
using System.Collections.Generic;
using AcDream.App.Rendering.Wb;
using Xunit;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class WbDrawDispatcherClipSlotTests
{
// Full cell-id space keys (lbMask | OtherCellId). 0xA9B4 is the Holtburg
// landblock prefix used throughout the indoor-walking work; the low word is
// the EnvCell index. ParentCellId on a cell static is the SAME full id — see
// the L.2e bare-low-byte finding (a 0x29 low-byte key would cull everything).
private const uint VisibleCellA = 0xA9B4_0164u;
private const uint VisibleCellB = 0xA9B4_0165u;
private const uint NotVisibleCell = 0xA9B4_0999u;
private const int SlotA = 3;
private const int SlotB = 7;
private const int OutsideViewSlot = 11;
private static IReadOnlyDictionary<uint, int> Routing() => new Dictionary<uint, int>
{
[VisibleCellA] = SlotA,
[VisibleCellB] = SlotB,
};
// ── Raw resolver (ResolveEntitySlot): only reached when routing is active ──
[Fact]
public void RawResolve_LiveEntity_IsUnclippedSlot0_WhenParentCellNull()
{
// ServerGuid != 0 ⇒ unclipped (slot 0) regardless of cell state.
int slot = WbDrawDispatcher.ResolveEntitySlot(
serverGuid: 0x5000_000Au, parentCellId: null,
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
Assert.Equal(0, slot);
}
[Fact]
public void RawResolve_LiveEntity_IsUnclippedSlot0_EvenWhenParentCellVisible()
{
// A live entity whose ParentCellId IS a visible cell still goes to slot 0,
// NOT SlotA — the live-dynamic check must precede the cell lookup.
int slot = WbDrawDispatcher.ResolveEntitySlot(
serverGuid: 0x5000_000Au, parentCellId: VisibleCellA,
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
Assert.Equal(0, slot);
Assert.NotEqual(SlotA, slot); // guards against ordering regression
}
[Fact]
public void RawResolve_CellStatic_InVisibleSet_GetsThatCellSlot()
{
int slot = WbDrawDispatcher.ResolveEntitySlot(
serverGuid: 0u, parentCellId: VisibleCellB,
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
Assert.Equal(SlotB, slot);
}
[Fact]
public void RawResolve_CellStatic_NotInVisibleSet_IsCulled()
{
int slot = WbDrawDispatcher.ResolveEntitySlot(
serverGuid: 0u, parentCellId: NotVisibleCell,
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
Assert.Equal(WbDrawDispatcher.ClipSlotCull, slot);
}
[Fact]
public void RawResolve_OutdoorStab_OutdoorsVisible_GetsOutsideViewSlot()
{
int slot = WbDrawDispatcher.ResolveEntitySlot(
serverGuid: 0u, parentCellId: null,
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
Assert.Equal(OutsideViewSlot, slot);
}
[Fact]
public void RawResolve_OutdoorStab_OutdoorsNotVisible_IsCulled()
{
int slot = WbDrawDispatcher.ResolveEntitySlot(
serverGuid: 0u, parentCellId: null,
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: false);
Assert.Equal(WbDrawDispatcher.ClipSlotCull, slot);
}
// ── Call-site policy (ResolveSlotForFrame): adds the clipRoutingActive gate ──
// Cases mirror the raw resolver but return the (slot, culled) pair the loop
// body consumes, and add the routing-inactive (outdoor-root) branch.
[Fact]
public void ForFrame_RoutingInactive_EveryEntityIsSlot0AndNotCulled()
{
// The bit-identical-to-U.3 property: when the camera is at an outdoor root
// (ClearClipRouting), ResolveEntitySlot is never consulted — every entity
// maps to slot 0 and nothing is clip-culled. Exercised here for BOTH a
// live entity and a cell static that would otherwise cull, with a null
// routing map to prove the resolver is bypassed entirely.
var live = WbDrawDispatcher.ResolveSlotForFrame(
clipRoutingActive: false, serverGuid: 0x5000_000Au, parentCellId: null,
cellIdToSlot: null, outdoorSlot: OutsideViewSlot, outdoorVisible: true);
Assert.Equal(0u, live.Slot);
Assert.False(live.Culled);
var wouldCull = WbDrawDispatcher.ResolveSlotForFrame(
clipRoutingActive: false, serverGuid: 0u, parentCellId: NotVisibleCell,
cellIdToSlot: null, outdoorSlot: OutsideViewSlot, outdoorVisible: false);
Assert.Equal(0u, wouldCull.Slot);
Assert.False(wouldCull.Culled);
}
[Fact]
public void ForFrame_RoutingActive_LiveEntity_Slot0NotCulled()
{
var r = WbDrawDispatcher.ResolveSlotForFrame(
clipRoutingActive: true, serverGuid: 0x5000_000Au, parentCellId: VisibleCellA,
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
Assert.Equal(0u, r.Slot);
Assert.False(r.Culled);
}
[Fact]
public void ForFrame_RoutingActive_CellStaticVisible_GetsCellSlotNotCulled()
{
var r = WbDrawDispatcher.ResolveSlotForFrame(
clipRoutingActive: true, serverGuid: 0u, parentCellId: VisibleCellA,
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
Assert.Equal((uint)SlotA, r.Slot);
Assert.False(r.Culled);
}
[Fact]
public void ForFrame_RoutingActive_CellStaticNotVisible_Culled()
{
var r = WbDrawDispatcher.ResolveSlotForFrame(
clipRoutingActive: true, serverGuid: 0u, parentCellId: NotVisibleCell,
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
Assert.True(r.Culled);
// When culled the loop body forces slot 0 (the value is never emitted).
Assert.Equal(0u, r.Slot);
}
[Fact]
public void ForFrame_RoutingActive_OutdoorStabVisible_GetsOutsideViewSlot()
{
var r = WbDrawDispatcher.ResolveSlotForFrame(
clipRoutingActive: true, serverGuid: 0u, parentCellId: null,
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
Assert.Equal((uint)OutsideViewSlot, r.Slot);
Assert.False(r.Culled);
}
[Fact]
public void ForFrame_RoutingActive_OutdoorStabNotVisible_Culled()
{
var r = WbDrawDispatcher.ResolveSlotForFrame(
clipRoutingActive: true, serverGuid: 0u, parentCellId: null,
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: false);
Assert.True(r.Culled);
Assert.Equal(0u, r.Slot);
}
}