feat(render): Phase U.4 — unified gated draw pass (indoor root)

Wire the portal-visibility result through the clip pipeline: build a per-frame
ClipFrame (slot 0 no-clip, slot 1 OutsideView, slot 2..N per visible cell) +
cellIdToSlot from PortalVisibilityBuilder; call the (previously dormant)
EnvCellRenderer.Render for cell shells inside the clip bracket; assign per-instance
clip slots in WbDrawDispatcher (live-dynamic unclipped per retail, cell statics to
their cell slot, outdoor scenery to OutsideView, non-visible culled); gate/scissor/
skip terrain per OutsideView (empty ⇒ no terrain — the bleed fix). Emit ACDREAM_PROBE_VIS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-30 17:59:21 +02:00
parent 864fc5f94e
commit 7993e064a0
6 changed files with 748 additions and 67 deletions

View file

@ -237,6 +237,24 @@ public sealed unsafe class EnvCellRenderer : IDisposable
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
// Phase U.4: per-frame cellId→CellClip-slot map for the cell shells. When
// non-null, RenderModernMDIInternal writes instanceClipSlot[i] =
// _cellIdToSlot[allInstances[i].CellId] so each cell's shell instances are
// gated to that cell's portal-clip region. When null (U.3 path), every
// instance maps to slot 0 (no-clip). A cell absent from the map writes slot 0
// (no-clip) — but the caller's Render filter already restricts the draw to the
// map's keys, so that fallback should not fire in practice.
private IReadOnlyDictionary<uint, int>? _cellIdToSlot;
/// <summary>
/// Phase U.4: install the per-frame cellId→slot map used to gate cell shells
/// to their portal-clip regions. Call once per frame BEFORE
/// <see cref="Render(WbRenderPass, HashSet{uint}?)"/>. Pass null to revert to
/// the U.3 no-clip behavior (every shell instance → slot 0).
/// </summary>
public void SetClipRouting(IReadOnlyDictionary<uint, int>? cellIdToSlot)
=> _cellIdToSlot = cellIdToSlot;
// ---------------------------------------------------------------------------
// GetEnvCellGeomId
// Verbatim copy of WB EnvCellRenderManager.cs:94-103.
@ -1047,14 +1065,25 @@ public sealed unsafe class EnvCellRenderer : IDisposable
(nuint)(totalDraws * sizeof(ModernBatchData)), ptr);
}
// Phase U.3: upload the per-instance clip-slot buffer (binding=3), all
// zeros ⇒ every instance maps to slot 0 ⇒ no-clip. Re-zero the reused head
// each frame so stale U.4 slot indices can't leak. Sized to
// uniqueInstanceCount; the buffer was already grown above with the
// instance buffer when capacity increased.
// Phase U.4: upload the per-instance clip-slot buffer (binding=3). When
// _cellIdToSlot is set (indoor routing), each cell shell instance is gated
// to its cell's CellClip slot via allInstances[i].CellId; cells absent from
// the map (shouldn't happen — the Render filter is the map's keys) and the
// U.3 path both map to slot 0 (no-clip). allInstances is laid out in the
// SAME order as the binding=0 transforms (_gpuInstanceTransforms below), so
// instanceClipSlot[i] tracks Instances[i] through the MDI BaseInstance.
if (_clipSlotData.Length < uniqueInstanceCount)
_clipSlotData = new uint[Math.Max(_clipSlotData.Length * 2, uniqueInstanceCount)];
Array.Clear(_clipSlotData, 0, uniqueInstanceCount);
if (_cellIdToSlot is null)
{
Array.Clear(_clipSlotData, 0, uniqueInstanceCount);
}
else
{
for (int i = 0; i < uniqueInstanceCount; i++)
_clipSlotData[i] = _cellIdToSlot.TryGetValue(allInstances[i].CellId, out int slot)
? (uint)slot : 0u;
}
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _clipSlotBuffer);
_gl.BufferData(GLEnum.ShaderStorageBuffer,
(nuint)(uniqueInstanceCount * sizeof(uint)), null, GLEnum.DynamicDraw);