feat(render): indoor render WORKS — terminating portal flood + every-cell seal + look-in FPS
Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the interior seals (live-verified by the user). Commits the session render-rewrite foundation together with the fixes that made it functional. - HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept (load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit); only its count is capped. CellViewDedupTests added. - Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey). - Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81 loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled). Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway, confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight): docs/research/2026-06-07-indoor-render-session-handoff.md. Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bff1955066
commit
1405dd8e90
27 changed files with 3635 additions and 814 deletions
|
|
@ -1,20 +1,3 @@
|
|||
// 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;
|
||||
|
|
@ -23,13 +6,10 @@ 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 uint OutdoorCell = 0xA9B4_0020u;
|
||||
|
||||
private const int SlotA = 3;
|
||||
private const int SlotB = 7;
|
||||
|
|
@ -41,30 +21,44 @@ public sealed class WbDrawDispatcherClipSlotTests
|
|||
[VisibleCellB] = SlotB,
|
||||
};
|
||||
|
||||
// ── Raw resolver (ResolveEntitySlot): only reached when routing is active ──
|
||||
|
||||
[Fact]
|
||||
public void RawResolve_LiveEntity_IsUnclippedSlot0_WhenParentCellNull()
|
||||
public void RawResolve_LiveEntity_WithVisibleIndoorParent_GetsThatCellSlot()
|
||||
{
|
||||
// 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
|
||||
Assert.Equal(SlotA, slot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RawResolve_LiveEntity_WithHiddenIndoorParent_IsCulled()
|
||||
{
|
||||
int slot = WbDrawDispatcher.ResolveEntitySlot(
|
||||
serverGuid: 0x5000_000Au, parentCellId: NotVisibleCell,
|
||||
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
|
||||
|
||||
Assert.Equal(WbDrawDispatcher.ClipSlotCull, slot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RawResolve_LiveEntity_WithOutdoorParent_UsesOutsideViewWhenVisible()
|
||||
{
|
||||
int slot = WbDrawDispatcher.ResolveEntitySlot(
|
||||
serverGuid: 0x5000_000Au, parentCellId: OutdoorCell,
|
||||
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
|
||||
|
||||
Assert.Equal(OutsideViewSlot, slot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RawResolve_LiveEntity_WithParentNull_IsCulledWhenRoutingActive()
|
||||
{
|
||||
int slot = WbDrawDispatcher.ResolveEntitySlot(
|
||||
serverGuid: 0x5000_000Au, parentCellId: null,
|
||||
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
|
||||
|
||||
Assert.Equal(WbDrawDispatcher.ClipSlotCull, slot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -107,19 +101,9 @@ public sealed class WbDrawDispatcherClipSlotTests
|
|||
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);
|
||||
|
|
@ -134,16 +118,27 @@ public sealed class WbDrawDispatcherClipSlotTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ForFrame_RoutingActive_LiveEntity_Slot0NotCulled()
|
||||
public void ForFrame_RoutingActive_LiveEntityVisible_GetsCellSlotNotCulled()
|
||||
{
|
||||
var r = WbDrawDispatcher.ResolveSlotForFrame(
|
||||
clipRoutingActive: true, serverGuid: 0x5000_000Au, parentCellId: VisibleCellA,
|
||||
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
|
||||
|
||||
Assert.Equal(0u, r.Slot);
|
||||
Assert.Equal((uint)SlotA, r.Slot);
|
||||
Assert.False(r.Culled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForFrame_RoutingActive_LiveEntityParentNull_Culled()
|
||||
{
|
||||
var r = WbDrawDispatcher.ResolveSlotForFrame(
|
||||
clipRoutingActive: true, serverGuid: 0x5000_000Au, parentCellId: null,
|
||||
cellIdToSlot: Routing(), outdoorSlot: OutsideViewSlot, outdoorVisible: true);
|
||||
|
||||
Assert.True(r.Culled);
|
||||
Assert.Equal(0u, r.Slot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForFrame_RoutingActive_CellStaticVisible_GetsCellSlotNotCulled()
|
||||
{
|
||||
|
|
@ -163,7 +158,6 @@ public sealed class WbDrawDispatcherClipSlotTests
|
|||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue