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
|
|
@ -144,8 +144,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// each Draw. When _clipRoutingActive is false (the U.3 path / outdoor root /
|
||||
// no portal frame), every instance maps to slot 0 (no-clip) and no instance is
|
||||
// culled — identical to U.3. When active, each instance's slot is resolved by
|
||||
// ResolveEntitySlot per the U.4 policy (live-dynamic unclipped; cell statics to
|
||||
// their cell slot; outdoor scenery to the OutsideView slot; non-visible culled).
|
||||
// ResolveEntitySlot per the U.4 policy (cell-owned entities to their cell slot;
|
||||
// outdoor-owned entities to OutsideView; non-visible/unresolved indoors culled).
|
||||
private bool _clipRoutingActive;
|
||||
private IReadOnlyDictionary<uint, int>? _cellIdToSlot;
|
||||
private int _outdoorSlot;
|
||||
|
|
@ -310,8 +310,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
/// Phase U.4: install the per-frame clip-slot routing for an INDOOR root.
|
||||
/// Call once per frame BEFORE <see cref="Draw"/> when the camera's root cell is
|
||||
/// non-null; the next <see cref="Draw"/> resolves each instance's binding=3
|
||||
/// clip slot via the U.4 policy (live-dynamic unclipped, cell statics to their
|
||||
/// cell slot, outdoor scenery to the OutsideView slot, non-visible culled).
|
||||
/// clip slot via the U.4 policy (cell-owned entities to their cell slot,
|
||||
/// outdoor-owned entities to OutsideView, non-visible/unresolved indoors culled).
|
||||
/// Pair with <see cref="ClearClipRouting"/> on outdoor-root frames so the
|
||||
/// dispatcher reverts to the U.3 no-clip-everything behavior.
|
||||
/// </summary>
|
||||
|
|
@ -354,12 +354,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
/// Phase U.4: resolve the clip slot for one entity per the slot/gate policy.
|
||||
/// Returns <see cref="ClipSlotCull"/> to drop the entity's instances entirely.
|
||||
/// <list type="bullet">
|
||||
/// <item>ServerGuid != 0 (live dynamic: player / NPC / items / doors) ⇒ slot 0
|
||||
/// (UNCLIPPED — retail draws live-dynamic unclipped; depth only).</item>
|
||||
/// <item>ParentCellId != null (cell static) ⇒ the cell's slot, or CULL when the
|
||||
/// cell isn't in <paramref name="cellIdToSlot"/> (not visible / nothing-visible).</item>
|
||||
/// <item>ParentCellId == null (outdoor scenery / building shell) ⇒ the OutsideView
|
||||
/// slot when <paramref name="outdoorVisible"/>, else CULL.</item>
|
||||
/// <item>Indoor ParentCellId: the cell's slot, or CULL when hidden.</item>
|
||||
/// <item>Outdoor ParentCellId or ParentCellId == null static scenery: the OutsideView slot
|
||||
/// when <paramref name="outdoorVisible"/>, else CULL.</item>
|
||||
/// <item>ServerGuid != 0 with ParentCellId == null: CULL while routing is active.</item>
|
||||
/// </list>
|
||||
/// Only called when <c>_clipRoutingActive</c> (indoor root). On the U.3 / outdoor
|
||||
/// path every instance is slot 0 and nothing is culled — see
|
||||
|
|
@ -385,20 +383,37 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
int outdoorSlot,
|
||||
bool outdoorVisible)
|
||||
{
|
||||
// Live-dynamic entities render unclipped regardless of cell — retail draws
|
||||
// the player / NPCs / dropped items through the depth buffer without portal
|
||||
// clipping. ServerGuid is the live-dynamic marker (0 for dat-hydrated).
|
||||
if (serverGuid != 0)
|
||||
return 0;
|
||||
|
||||
// Live-dynamic entities are not a global indoor overlay. When they
|
||||
// have current cell ownership, route them through the same visible
|
||||
// cell/OutsideView graph as every other object. Parentless live objects
|
||||
// are unresolved indoors, so cull them while clip routing is active.
|
||||
if (parentCellId is uint parentCell)
|
||||
return cellIdToSlot.TryGetValue(parentCell, out int slot) ? slot : ClipSlotCull;
|
||||
{
|
||||
if (IsIndoorCellId(parentCell))
|
||||
{
|
||||
if (!cellIdToSlot.ContainsKey(parentCell))
|
||||
return ClipSlotCull;
|
||||
|
||||
return cellIdToSlot[parentCell];
|
||||
}
|
||||
|
||||
return outdoorVisible ? outdoorSlot : ClipSlotCull;
|
||||
}
|
||||
|
||||
if (serverGuid != 0)
|
||||
return ClipSlotCull;
|
||||
|
||||
// Outdoor scenery / building shell (no ParentCellId). Indoor root: gate to
|
||||
// the OutsideView slot, or cull when nothing outdoors is visible.
|
||||
return outdoorVisible ? outdoorSlot : ClipSlotCull;
|
||||
}
|
||||
|
||||
private static bool IsIndoorCellId(uint cellId)
|
||||
{
|
||||
uint low = cellId & 0xFFFFu;
|
||||
return low >= 0x0100u && low != 0xFFFFu;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.4: the call-site clip-slot decision for one entity, returning the
|
||||
/// <c>(Slot, Culled)</c> pair the per-entity loop body consumes. Wraps
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue