feat(render): Phase A8 R2 — WbDrawDispatcher.EntitySet taxonomy partition
Reshapes the dormant EntitySet enum from binary IndoorOnly/OutdoorOnly to
a three-way taxonomy-aware partition:
IndoorPass — cell mesh + cell statics + building shells
(ParentCellId.HasValue OR IsBuildingShell), live-dynamic
excluded
OutdoorScenery — outdoor scenery only (ParentCellId == null AND
!IsBuildingShell), live-dynamic excluded
LiveDynamic — ServerGuid != 0 (player, NPCs, dropped items)
Centralizes the membership predicate in EntityMatchesSet to keep the three
call sites (two in WalkEntitiesInto, one in WalkEntitiesForTest) DRY.
R1's IsBuildingShell flag is now consumed at render time. Integration into
the render frame ships in R3.
Tests rebuilt from scratch — 7 cases cover the new partition truth table.
Existing dispatcher tests (Tier 1 cache, etc.) continue to pass under the
default EntitySet.All.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ed72704f7b
commit
55f26f2a9c
2 changed files with 205 additions and 70 deletions
|
|
@ -62,22 +62,37 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
/// <summary>
|
||||
/// Phase A8 — which subset of entities to walk in a single Draw call.
|
||||
/// Used to split indoor entities (drawn first, stencil OFF) from outdoor
|
||||
/// entities (drawn after, stencil-gated to portal silhouettes) when the
|
||||
/// camera is inside an EnvCell.
|
||||
/// Used to split the indoor-cell visibility pipeline into three passes
|
||||
/// when the camera is inside an EnvCell.
|
||||
///
|
||||
/// Taxonomy reference: docs/research/2026-05-26-a8-entity-taxonomy.md.
|
||||
/// </summary>
|
||||
public enum EntitySet
|
||||
{
|
||||
/// <summary>Pre-A8 behavior: every entity walked, gated only by
|
||||
/// the existing ParentCellId ∈ visibleCellIds filter.</summary>
|
||||
/// the existing <c>ParentCellId ∈ visibleCellIds</c> filter.
|
||||
/// Used when the camera is OUTSIDE any EnvCell.</summary>
|
||||
All,
|
||||
/// <summary>Only entities with <c>ParentCellId.HasValue</c> (indoor).
|
||||
/// Existing visibleCellIds filter still applied on top.</summary>
|
||||
IndoorOnly,
|
||||
/// <summary>Only entities with <c>ParentCellId == null</c> (outdoor
|
||||
/// stabs, scenery, live-spawned). visibleCellIds is ignored for
|
||||
/// this set since outdoor entities never have a ParentCellId.</summary>
|
||||
OutdoorOnly,
|
||||
|
||||
/// <summary>Cell mesh + cell statics (<see cref="WorldEntity.ParentCellId"/>
|
||||
/// non-null) PLUS building shell stabs (<see cref="WorldEntity.IsBuildingShell"/>
|
||||
/// true, regardless of ParentCellId). These render unconditionally
|
||||
/// when the camera is inside their building — building shells ARE
|
||||
/// the indoor walls. Live-dynamic (<c>ServerGuid != 0</c>) is
|
||||
/// excluded; it flows through <see cref="LiveDynamic"/>.</summary>
|
||||
IndoorPass,
|
||||
|
||||
/// <summary>Outdoor scenery stabs (<c>ParentCellId == null</c>,
|
||||
/// <c>!IsBuildingShell</c>) plus procedurally-generated scenery.
|
||||
/// Drawn stencil-gated to portal silhouettes when the camera is
|
||||
/// inside. Live-dynamic excluded.</summary>
|
||||
OutdoorScenery,
|
||||
|
||||
/// <summary>Server-spawned dynamic entities (<c>ServerGuid != 0</c>):
|
||||
/// player, NPCs, monsters, dropped items, animated and idle doors.
|
||||
/// Drawn last with stencil disabled so they're depth-tested against
|
||||
/// everything else but not stencil-clipped.</summary>
|
||||
LiveDynamic,
|
||||
}
|
||||
|
||||
private readonly GL _gl;
|
||||
|
|
@ -358,8 +373,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
if (!entry.AnimatedById.TryGetValue(animatedId, out var entity)) continue;
|
||||
// Phase A8: EntitySet partition for indoor/outdoor split passes.
|
||||
if (set == EntitySet.IndoorOnly && !entity.ParentCellId.HasValue) continue;
|
||||
if (set == EntitySet.OutdoorOnly && entity.ParentCellId.HasValue) continue;
|
||||
if (!EntityMatchesSet(entity, set)) continue;
|
||||
if (entity.MeshRefs.Count == 0) continue;
|
||||
if (entity.ParentCellId.HasValue && visibleCellIds is not null
|
||||
&& !visibleCellIds.Contains(entity.ParentCellId.Value)) continue;
|
||||
|
|
@ -373,8 +387,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
foreach (var entity in entry.Entities)
|
||||
{
|
||||
// Phase A8: EntitySet partition for indoor/outdoor split passes.
|
||||
if (set == EntitySet.IndoorOnly && !entity.ParentCellId.HasValue) continue;
|
||||
if (set == EntitySet.OutdoorOnly && entity.ParentCellId.HasValue) continue;
|
||||
if (!EntityMatchesSet(entity, set)) continue;
|
||||
if (entity.MeshRefs.Count == 0) continue;
|
||||
|
||||
// Detect cell entity for indoor probes — first MeshRef.GfxObjId
|
||||
|
|
@ -1341,6 +1354,25 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
buf[offset + 12] = m.M41; buf[offset + 13] = m.M42; buf[offset + 14] = m.M43; buf[offset + 15] = m.M44;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 — entity-taxonomy-aware membership test for the three-way
|
||||
/// EntitySet partition. See <see cref="EntitySet"/> for the doctrine.
|
||||
/// </summary>
|
||||
private static bool EntityMatchesSet(WorldEntity entity, EntitySet set)
|
||||
{
|
||||
if (set == EntitySet.All) return true;
|
||||
|
||||
bool isLiveDynamic = entity.ServerGuid != 0;
|
||||
if (set == EntitySet.LiveDynamic) return isLiveDynamic;
|
||||
if (isLiveDynamic) return false; // IndoorPass/OutdoorScenery exclude live-dynamic
|
||||
|
||||
bool isIndoor = entity.ParentCellId.HasValue || entity.IsBuildingShell;
|
||||
if (set == EntitySet.IndoorPass) return isIndoor;
|
||||
if (set == EntitySet.OutdoorScenery) return !isIndoor;
|
||||
|
||||
throw new InvalidOperationException($"Unhandled EntitySet value: {set}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 test helper: runs the EntitySet partition + visibleCellIds
|
||||
/// gate against an in-memory entity list, returning the IDs that
|
||||
|
|
@ -1355,8 +1387,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
var output = new List<uint>();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (set == EntitySet.IndoorOnly && !entity.ParentCellId.HasValue) continue;
|
||||
if (set == EntitySet.OutdoorOnly && entity.ParentCellId.HasValue) continue;
|
||||
if (!EntityMatchesSet(entity, set)) continue;
|
||||
if (entity.MeshRefs.Count == 0) continue;
|
||||
|
||||
bool cellInVis = !(entity.ParentCellId.HasValue
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue