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:
parent
864fc5f94e
commit
7993e064a0
6 changed files with 748 additions and 67 deletions
|
|
@ -140,6 +140,29 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
private uint _sharedClipRegionSsbo;
|
||||
private uint _fallbackClipRegionSsbo;
|
||||
|
||||
// Phase U.4: per-frame clip-slot routing handed in via SetClipRouting before
|
||||
// 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).
|
||||
private bool _clipRoutingActive;
|
||||
private IReadOnlyDictionary<uint, int>? _cellIdToSlot;
|
||||
private int _outdoorSlot;
|
||||
private bool _outdoorVisible;
|
||||
|
||||
// Phase U.4: the clip slot of the entity currently being classified in Draw's
|
||||
// per-entity loop. Set once per entity (before ClassifyBatches / ApplyCacheHit),
|
||||
// read by the two matrix-append sites (AppendInstanceToGroup + ClassifyBatches)
|
||||
// so every group's Slots[] stays in lockstep with its Matrices[]. Defaults to 0
|
||||
// (no-clip) on the U.3 / outdoor path.
|
||||
private uint _currentEntitySlot;
|
||||
|
||||
// Phase U.4: true when the current entity resolved to the CULL sentinel
|
||||
// (cell not visible, or outdoor stab while no outdoors is visible). Persisted
|
||||
// across the entity's tuples; the per-tuple body skips all instance emission.
|
||||
private bool _currentEntityCulled;
|
||||
|
||||
// Per-frame scratch arrays — Tasks 9-10 fully wire these.
|
||||
private float[] _instanceData = new float[256 * 16]; // mat4 floats per instance
|
||||
private BatchData[] _batchData = new BatchData[256];
|
||||
|
|
@ -283,6 +306,78 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
public void SetClipRegionSsbo(uint sharedClipRegionSsbo)
|
||||
=> _sharedClipRegionSsbo = sharedClipRegionSsbo;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// Pair with <see cref="ClearClipRouting"/> on outdoor-root frames so the
|
||||
/// dispatcher reverts to the U.3 no-clip-everything behavior.
|
||||
/// </summary>
|
||||
/// <param name="cellIdToSlot">cellId → CellClip slot. A cell absent from the map
|
||||
/// is NOT visible → its cell-static instances are culled.</param>
|
||||
/// <param name="outdoorSlot">Slot for outdoor scenery / building shells while
|
||||
/// indoors (the OutsideView slot, or 0 for no-clip over-include).</param>
|
||||
/// <param name="outdoorVisible">False ⇒ cull outdoor scenery / shells this frame
|
||||
/// (the OutsideView is empty).</param>
|
||||
public void SetClipRouting(IReadOnlyDictionary<uint, int> cellIdToSlot, int outdoorSlot, bool outdoorVisible)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(cellIdToSlot);
|
||||
_clipRoutingActive = true;
|
||||
_cellIdToSlot = cellIdToSlot;
|
||||
_outdoorSlot = outdoorSlot;
|
||||
_outdoorVisible = outdoorVisible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.4: revert to U.3 behavior — every instance maps to slot 0 (no-clip),
|
||||
/// nothing is culled by clip routing. Call on outdoor-root frames (camera
|
||||
/// outdoors) and any frame without a portal-visibility result.
|
||||
/// </summary>
|
||||
public void ClearClipRouting()
|
||||
{
|
||||
_clipRoutingActive = false;
|
||||
_cellIdToSlot = null;
|
||||
_outdoorSlot = 0;
|
||||
_outdoorVisible = false;
|
||||
}
|
||||
|
||||
// Phase U.4 CULL sentinel returned by ResolveEntitySlot: the entity's instances
|
||||
// are dropped entirely (not emitted into the binding=0 instance buffer NOR the
|
||||
// binding=3 slot buffer), matching the existing frustum / visible-cell cull.
|
||||
private const int ClipSlotCull = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>cellIdToSlot</c> (not visible / nothing-visible).</item>
|
||||
/// <item>ParentCellId == null (outdoor scenery / building shell) ⇒ the OutsideView
|
||||
/// slot when <c>outdoorVisible</c>, else CULL.</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.
|
||||
/// </summary>
|
||||
private int ResolveEntitySlot(WorldEntity entity)
|
||||
{
|
||||
// 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 (entity.ServerGuid != 0)
|
||||
return 0;
|
||||
|
||||
if (entity.ParentCellId is uint parentCell)
|
||||
return _cellIdToSlot!.TryGetValue(parentCell, out int slot) ? slot : 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;
|
||||
}
|
||||
|
||||
public static Matrix4x4 ComposePartWorldMatrix(
|
||||
Matrix4x4 entityWorld,
|
||||
Matrix4x4 animOverride,
|
||||
|
|
@ -533,7 +628,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
camPos = invView.Translation;
|
||||
|
||||
// ── Phase 1: clear groups, walk entities, build groups ──────────────
|
||||
foreach (var grp in _groups.Values) grp.Matrices.Clear();
|
||||
foreach (var grp in _groups.Values) { grp.Matrices.Clear(); grp.Slots.Clear(); }
|
||||
|
||||
var metaTable = _meshAdapter.MetadataTable;
|
||||
uint anyVao = 0;
|
||||
|
|
@ -676,15 +771,41 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
populateEntityId = null;
|
||||
}
|
||||
currentEntityIncomplete = false;
|
||||
|
||||
// Phase U.4: resolve this entity's clip slot ONCE per entity
|
||||
// (constant across its tuples). On the U.3 / outdoor path
|
||||
// (_clipRoutingActive false) every entity is slot 0, never culled.
|
||||
if (_clipRoutingActive)
|
||||
{
|
||||
int resolved = ResolveEntitySlot(entity);
|
||||
_currentEntityCulled = resolved == ClipSlotCull;
|
||||
_currentEntitySlot = _currentEntityCulled ? 0u : (uint)resolved;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentEntityCulled = false;
|
||||
_currentEntitySlot = 0u;
|
||||
}
|
||||
}
|
||||
prevTupleEntityId = entity.Id;
|
||||
|
||||
// Flush-on-entity-change: if the previous entity accumulated any
|
||||
// batches AND this iteration is for a different entity, populate
|
||||
// its cache entry now and reset the scratch buffer.
|
||||
// its cache entry now and reset the scratch buffer. Runs for ALL
|
||||
// entities (including this-entity-culled) so the PREVIOUS entity's
|
||||
// cache always flushes at the boundary.
|
||||
(populateEntityId, populateLandblockId) = MaybeFlushOnEntityChange(
|
||||
populateEntityId, populateLandblockId, entity.Id, _cache, _populateScratch);
|
||||
|
||||
// Phase U.4: a culled entity (cell not visible, or no outdoors visible
|
||||
// for an outdoor stab) contributes NO instances. Skip after the
|
||||
// boundary flush above so the previous entity still committed; the
|
||||
// next entity's isNewEntity logic is unaffected (prevTupleEntityId is
|
||||
// already updated). Matches the existing visible-cell / frustum cull:
|
||||
// nothing enters _groups, so neither binding=0 nor binding=3 sees it.
|
||||
if (_currentEntityCulled)
|
||||
continue;
|
||||
|
||||
var entityWorld =
|
||||
Matrix4x4.CreateFromQuaternion(entity.Rotation) *
|
||||
Matrix4x4.CreateTranslation(entity.Position);
|
||||
|
|
@ -912,6 +1033,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (_instanceData.Length < needed)
|
||||
_instanceData = new float[needed + 256 * 16];
|
||||
|
||||
// Phase U.4: size the per-instance clip-slot buffer to match the instance
|
||||
// count and lay it out in the SAME group order / cursor as _instanceData,
|
||||
// so instanceClipSlot[i] (binding=3) tracks Instances[i] (binding=0). On
|
||||
// the U.3 / outdoor path every Slots entry is 0 ⇒ identical to U.3.
|
||||
if (_clipSlotData.Length < totalInstances)
|
||||
_clipSlotData = new uint[totalInstances + 256];
|
||||
|
||||
_opaqueDraws.Clear();
|
||||
_translucentDraws.Clear();
|
||||
|
||||
|
|
@ -934,6 +1062,9 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
for (int i = 0; i < grp.Matrices.Count; i++)
|
||||
{
|
||||
WriteMatrix(_instanceData, cursor * 16, grp.Matrices[i]);
|
||||
// Slots[] is parallel to Matrices[] within the group; write the
|
||||
// slot at the same cursor so binding=3 stays aligned with binding=0.
|
||||
_clipSlotData[cursor] = grp.Slots[i];
|
||||
cursor++;
|
||||
}
|
||||
|
||||
|
|
@ -1008,15 +1139,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
fixed (BatchData* bp = _batchData)
|
||||
UploadSsbo(_batchSsbo, 1, bp, totalDraws * sizeof(BatchData));
|
||||
|
||||
// Phase U.3: per-instance clip-slot buffer (binding=3), one uint per
|
||||
// instance, laid out parallel to _instanceData so the shader's
|
||||
// instanceClipSlot[instanceIndex] tracks the same instance as
|
||||
// Instances[instanceIndex]. ALL ZEROS in U.3 ⇒ slot 0 ⇒ no-clip. Grow +
|
||||
// zero the scratch as needed (Array.Resize zero-fills the new tail; the
|
||||
// reused head is re-zeroed below so stale U.4 slot indices can't leak).
|
||||
if (_clipSlotData.Length < totalInstances)
|
||||
_clipSlotData = new uint[totalInstances + 256];
|
||||
Array.Clear(_clipSlotData, 0, totalInstances);
|
||||
// Phase U.4: per-instance clip-slot buffer (binding=3), one uint per
|
||||
// instance, laid out parallel to _instanceData in Phase 3's group loop so
|
||||
// instanceClipSlot[instanceIndex] tracks Instances[instanceIndex]. On the
|
||||
// U.3 / outdoor path every entry is 0 ⇒ slot 0 ⇒ no-clip (identical to
|
||||
// U.3); under indoor routing it holds the per-instance slot from
|
||||
// ResolveEntitySlot. No clear here — Phase 3 wrote exactly totalInstances
|
||||
// entries; only [0..totalInstances) is uploaded, so any stale tail is
|
||||
// never read by the shader (BaseInstance + gl_InstanceID < totalInstances).
|
||||
fixed (uint* sp = _clipSlotData)
|
||||
UploadSsbo(_clipSlotSsbo, 3, sp, totalInstances * sizeof(uint));
|
||||
|
||||
|
|
@ -1460,6 +1590,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_groups[key] = grp;
|
||||
}
|
||||
grp.Matrices.Add(model);
|
||||
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
|
||||
}
|
||||
|
||||
private void ClassifyBatches(
|
||||
|
|
@ -1516,6 +1647,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_groups[key] = grp;
|
||||
}
|
||||
grp.Matrices.Add(model);
|
||||
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
|
||||
collector?.Add(new CachedBatch(key, texHandle, restPose));
|
||||
}
|
||||
}
|
||||
|
|
@ -1772,5 +1904,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
public int InstanceCount;
|
||||
public float SortDistance; // squared distance from camera to first instance, for opaque sort
|
||||
public readonly List<Matrix4x4> Matrices = new();
|
||||
|
||||
// Phase U.4: per-instance clip-slot index, parallel to Matrices (Slots[i]
|
||||
// is the binding=2 CellClip slot for the instance whose matrix is
|
||||
// Matrices[i]). At layout time the dispatcher writes Slots[i] into
|
||||
// _clipSlotData at the same cursor it writes Matrices[i] into _instanceData,
|
||||
// so the binding=3 instanceClipSlot[] tracks the binding=0 instance.
|
||||
public readonly List<uint> Slots = new();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue