feat(dispatcher): [indoor-walk] + [indoor-cull] probes
Instruments WalkVisibleEntities to identify whether cell entities (first MeshRef.GfxObjId low-16-bits >= 0x0100) pass all visibility filters or get culled. Three emission paths: - [indoor-cull] reason=visibleCellIds-miss -- when the ParentCellId filter rejects the entity. - [indoor-cull] reason=frustum -- when AABB frustum cull rejects. - [indoor-walk] -- when the entity passes all filters and reaches the draw list. Rate-limited to once per cellId per ~1 sec (30 frames at 30 Hz) via IndoorProbeState, a nested class wrapping _lastIndoorProbeFrame dictionary and _indoorProbeFrameCounter (bumped at top of Draw()). WalkEntitiesInto accepts a new optional IndoorProbeState? parameter (null = probes off, default) so the test-friendly WalkEntities overload is unaffected. The ShouldEmitIndoorProbe instance helper is also retained for Task 7 use. Disambiguates hypothesis H3 (cull bug -- cell entity dropped before draw). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1dd20ddd40
commit
36a29ceff5
1 changed files with 144 additions and 6 deletions
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Rendering;
|
||||
using AcDream.Core.Terrain;
|
||||
using AcDream.Core.World;
|
||||
using Chorizite.OpenGLSDLBackend.Lib;
|
||||
|
|
@ -140,6 +141,31 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Per-cell-entity last-log frame number for rate-limiting the
|
||||
/// [indoor-walk] / [indoor-lookup] / [indoor-xform] / [indoor-cull]
|
||||
/// probes. Defaults to 30 frames at 30Hz = 1 sec.
|
||||
/// </summary>
|
||||
private readonly Dictionary<ulong, int> _lastIndoorProbeFrame = new();
|
||||
private int _indoorProbeFrameCounter;
|
||||
private const int IndoorProbeRateLimitFrames = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true at most once per <see cref="IndoorProbeRateLimitFrames"/>
|
||||
/// frames per cellId. Caller must already have checked that an indoor
|
||||
/// probe flag is enabled.
|
||||
/// </summary>
|
||||
private bool ShouldEmitIndoorProbe(ulong cellId)
|
||||
{
|
||||
if (!_lastIndoorProbeFrame.TryGetValue(cellId, out int last)
|
||||
|| _indoorProbeFrameCounter - last >= IndoorProbeRateLimitFrames)
|
||||
{
|
||||
_lastIndoorProbeFrame[cellId] = _indoorProbeFrameCounter;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Diagnostic counters logged once per ~5s under ACDREAM_WB_DIAG=1.
|
||||
private int _entitiesSeen;
|
||||
private int _entitiesDrawn;
|
||||
|
|
@ -271,6 +297,16 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
/// list. <see cref="Draw"/> reuses a per-dispatcher scratch field across frames to
|
||||
/// avoid the 480+ KB / frame GC pressure that the test-friendly overload incurs.
|
||||
/// Returns walk count via <paramref name="result"/>'s <c>EntitiesWalked</c> field.
|
||||
///
|
||||
/// <para>
|
||||
/// When <paramref name="indoorProbeState"/> is non-null the method emits
|
||||
/// <c>[indoor-cull]</c> lines for cell entities rejected by the
|
||||
/// visibleCellIds or frustum filters, and <c>[indoor-walk]</c> lines for
|
||||
/// cell entities that pass all filters. Rate-limited by
|
||||
/// <see cref="IndoorProbeState"/>. Pass <see langword="null"/> (the default)
|
||||
/// to disable all probe emission — used by the test-friendly
|
||||
/// <see cref="WalkEntities"/> overload.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static void WalkEntitiesInto(
|
||||
IEnumerable<LandblockEntry> landblockEntries,
|
||||
|
|
@ -279,7 +315,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
HashSet<uint>? visibleCellIds,
|
||||
HashSet<uint>? animatedEntityIds,
|
||||
List<(WorldEntity Entity, int MeshRefIndex, uint LandblockId)> scratch,
|
||||
ref WalkResult result)
|
||||
ref WalkResult result,
|
||||
IndoorProbeState? indoorProbeState = null)
|
||||
{
|
||||
scratch.Clear();
|
||||
result.EntitiesWalked = 0;
|
||||
|
|
@ -314,19 +351,65 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
if (entity.MeshRefs.Count == 0) continue;
|
||||
|
||||
if (entity.ParentCellId.HasValue && visibleCellIds is not null
|
||||
&& !visibleCellIds.Contains(entity.ParentCellId.Value))
|
||||
// Detect cell entity for indoor probes — first MeshRef.GfxObjId
|
||||
// is an EnvCell id (low 16 bits ≥ 0x0100). Cheap to compute;
|
||||
// result reused for all probe checks below.
|
||||
ulong cellProbeId = (ulong)entity.MeshRefs[0].GfxObjId;
|
||||
bool isCellEntity = indoorProbeState is not null
|
||||
&& RenderingDiagnostics.IsEnvCellId(cellProbeId);
|
||||
|
||||
bool cellInVis = !(entity.ParentCellId.HasValue
|
||||
&& visibleCellIds is not null
|
||||
&& !visibleCellIds.Contains(entity.ParentCellId.Value));
|
||||
if (!cellInVis)
|
||||
{
|
||||
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
|
||||
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[indoor-cull] cellEnt=0x{entity.Id:X8} " +
|
||||
$"reason=visibleCellIds-miss " +
|
||||
$"parentCell=0x{entity.ParentCellId!.Value:X8}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Per-entity AABB frustum cull (perf #3). Animated entities bypass —
|
||||
// they're tracked at landblock level + need per-frame work regardless.
|
||||
// A.5 T18 Change #2: read cached AABB, refresh lazily on AabbDirty.
|
||||
bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true;
|
||||
bool aabbVisible = true;
|
||||
if (frustum is not null && !isAnimated && entry.LandblockId != neverCullLandblockId)
|
||||
{
|
||||
if (entity.AabbDirty) entity.RefreshAabb();
|
||||
if (!FrustumCuller.IsAabbVisible(frustum.Value, entity.AabbMin, entity.AabbMax))
|
||||
continue;
|
||||
aabbVisible = FrustumCuller.IsAabbVisible(frustum.Value, entity.AabbMin, entity.AabbMax);
|
||||
}
|
||||
|
||||
if (!aabbVisible)
|
||||
{
|
||||
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
|
||||
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[indoor-cull] cellEnt=0x{entity.Id:X8} " +
|
||||
$"reason=frustum " +
|
||||
$"aabbMin=({entity.AabbMin.X:F1},{entity.AabbMin.Y:F1},{entity.AabbMin.Z:F1}) " +
|
||||
$"aabbMax=({entity.AabbMax.X:F1},{entity.AabbMax.Y:F1},{entity.AabbMax.Z:F1})");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Passed all filters — emit walk probe.
|
||||
if (isCellEntity && RenderingDiagnostics.ProbeIndoorWalkEnabled
|
||||
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[indoor-walk] cellEnt=0x{entity.Id:X8} " +
|
||||
$"pos=({entity.Position.X:F1},{entity.Position.Y:F1},{entity.Position.Z:F1}) " +
|
||||
$"parentCell=0x{(entity.ParentCellId ?? 0u):X8} " +
|
||||
$"meshRef0=0x{cellProbeId:X8} " +
|
||||
$"meshRefCount={entity.MeshRefs.Count} " +
|
||||
$"landblockVisible=true aabbVisible=true cellInVis=true");
|
||||
}
|
||||
|
||||
result.EntitiesWalked++;
|
||||
|
|
@ -347,6 +430,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
HashSet<uint>? animatedEntityIds = null)
|
||||
{
|
||||
_shader.Use();
|
||||
_indoorProbeFrameCounter++;
|
||||
var vp = camera.View * camera.Projection;
|
||||
_shader.SetMatrix4("uViewProjection", vp);
|
||||
|
||||
|
|
@ -391,6 +475,24 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// A.5 T26 follow-up (Bug B): use the no-alloc WalkEntitiesInto overload
|
||||
// that populates _walkScratch (a per-dispatcher field reused across frames)
|
||||
// instead of allocating a fresh List<(WorldEntity, int)> per frame.
|
||||
//
|
||||
// Pass an IndoorProbeState when any indoor probe is active so the static
|
||||
// WalkEntitiesInto can emit rate-limited [indoor-cull] / [indoor-walk]
|
||||
// lines without needing access to instance fields. Null = probes off.
|
||||
IndoorProbeState? probeState = null;
|
||||
if (RenderingDiagnostics.ProbeIndoorCullEnabled || RenderingDiagnostics.ProbeIndoorWalkEnabled)
|
||||
{
|
||||
// _currentFrame is snapped at construction time. Construct
|
||||
// once per Draw() call only — a second construction within
|
||||
// the same frame would stamp the dictionary with the
|
||||
// (already-advanced) counter value, suppressing the second
|
||||
// pass's emissions for IndoorProbeRateLimitFrames frames.
|
||||
// Today Draw() is called exactly once per frame; if a
|
||||
// future refactor adds a shadow / reflection / second pass,
|
||||
// this assumption needs revisiting.
|
||||
probeState = new IndoorProbeState(_lastIndoorProbeFrame, _indoorProbeFrameCounter);
|
||||
}
|
||||
|
||||
var walkResult = default(WalkResult);
|
||||
WalkEntitiesInto(
|
||||
ToEntries(landblockEntries),
|
||||
|
|
@ -399,7 +501,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
visibleCellIds,
|
||||
animatedEntityIds,
|
||||
_walkScratch,
|
||||
ref walkResult);
|
||||
ref walkResult,
|
||||
probeState);
|
||||
|
||||
// Tier 1 cache (#53) flush-tracking locals. _walkScratch holds one tuple
|
||||
// per (entity, MeshRefIndex) and is in entity-order, so all MeshRefs of
|
||||
|
|
@ -1289,6 +1392,41 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Thin wrapper around an instance's rate-limit dictionary + frame
|
||||
/// counter, passed into the static <see cref="WalkEntitiesInto"/>
|
||||
/// overload so it can emit rate-limited probe lines without access
|
||||
/// to instance fields. Null = probes disabled (test-friendly overload).
|
||||
/// </summary>
|
||||
internal sealed class IndoorProbeState
|
||||
{
|
||||
private readonly Dictionary<ulong, int> _lastFrame;
|
||||
private readonly int _currentFrame;
|
||||
private const int RateLimit = IndoorProbeRateLimitFrames;
|
||||
|
||||
internal IndoorProbeState(Dictionary<ulong, int> lastFrame, int currentFrame)
|
||||
{
|
||||
_lastFrame = lastFrame;
|
||||
_currentFrame = currentFrame;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true at most once per <see cref="IndoorProbeRateLimitFrames"/>
|
||||
/// frames per <paramref name="cellId"/>. Side-effect: stamps the frame
|
||||
/// number into the dictionary on success.
|
||||
/// </summary>
|
||||
internal bool ShouldEmit(ulong cellId)
|
||||
{
|
||||
if (!_lastFrame.TryGetValue(cellId, out int last)
|
||||
|| _currentFrame - last >= RateLimit)
|
||||
{
|
||||
_lastFrame[cellId] = _currentFrame;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InstanceGroup
|
||||
{
|
||||
public uint Ibo;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue