feat(dispatcher): [indoor-lookup] + [indoor-xform] probes

Instruments the per-MeshRef draw loop in WbDrawDispatcher:

- [indoor-lookup]: per cell entity, dumps render-data hit/miss,
  IsSetup, parts count, and a partsHit/partsMiss tally over the
  SetupParts. Disambiguates hypothesis H2 (WB produces empty
  ObjectRenderData with zero parts) and H6 (dispatcher fails to
  traverse Setup).

- [indoor-xform]: only fires for the cell's synthetic geometry part
  (the SetupPart whose GfxObjId has bit 32 set, per WB's
  PrepareEnvCellMeshData cellGeomId convention). Logs the three
  composed transform translations: entityWorld, meshRef.PartTransform,
  partTransform, and the final composed matrix translation. Disambiguates
  hypothesis H5 (transform double-apply — composedT lands at 2 ×
  cellOrigin).

Rate-limited via the ShouldEmitIndoorProbe instance helper added in
Task 6 (now consumed — no longer dead code).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-19 11:50:50 +02:00
parent 36a29ceff5
commit 9b948b6ad5

View file

@ -685,6 +685,42 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
ulong gfxObjId = meshRef.GfxObjId;
var renderData = _meshAdapter.TryGetRenderData(gfxObjId);
// [indoor-lookup] probe — emit once per cell entity per sec.
// Fires BEFORE the null-renderData early-continue so a miss still
// emits hit=false, distinguishing H2 (empty batches) from H6
// (dispatcher fails to traverse Setup).
ulong lookupCellId = (ulong)gfxObjId;
if (RenderingDiagnostics.IsEnvCellId(lookupCellId)
&& RenderingDiagnostics.ProbeIndoorLookupEnabled
// Rate-limit in a separate namespace from [indoor-walk]/[indoor-cull]
// (which key on the same gfxObjId). Without this, IndoorAll=1 would
// silence the lookup probe whenever the walk probe fired first.
&& ShouldEmitIndoorProbe(lookupCellId | 0x8000_0000_0000_0000UL))
{
bool hit = renderData is not null;
bool isSetup = hit && renderData!.IsSetup;
int partCount = isSetup ? renderData!.SetupParts.Count : 0;
int partsHit = 0, partsMiss = 0;
if (isSetup)
{
foreach (var (partId, _) in renderData!.SetupParts)
{
if (_meshAdapter.TryGetRenderData(partId) is not null) partsHit++;
else partsMiss++;
}
}
bool hasEnvCellGeom = isSetup
&& renderData!.SetupParts.Exists(t => (t.GfxObjId & 0x1_0000_0000UL) != 0);
Console.WriteLine(
$"[indoor-lookup] cellId=0x{lookupCellId:X8} " +
$"hit={hit} isSetup={isSetup} partCount={partCount} " +
$"hasEnvCellGeom={hasEnvCellGeom} partsHit={partsHit} partsMiss={partsMiss}");
}
if (renderData is null)
{
// Tier 1 cache (#53): mesh data is still async-decoding via
@ -717,6 +753,23 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
var model = ComposePartWorldMatrix(
entityWorld, meshRef.PartTransform, partTransform);
// [indoor-xform] probe — only for the cell's synthetic
// geometry part (bit 32 set, per WB's PrepareEnvCellMeshData
// cellGeomId convention). One line per part per sec.
// Disambiguates hypothesis H5 (transform double-apply —
// composedT lands at 2 × cellOrigin).
if ((partGfxObjId & 0x1_0000_0000UL) != 0
&& RenderingDiagnostics.ProbeIndoorXformEnabled
&& ShouldEmitIndoorProbe(partGfxObjId))
{
Console.WriteLine(
$"[indoor-xform] cellGeomId=0x{partGfxObjId:X16} " +
$"entityWorldT=({entityWorld.Translation.X:F2},{entityWorld.Translation.Y:F2},{entityWorld.Translation.Z:F2}) " +
$"meshRefT=({meshRef.PartTransform.Translation.X:F2},{meshRef.PartTransform.Translation.Y:F2},{meshRef.PartTransform.Translation.Z:F2}) " +
$"partT=({partTransform.Translation.X:F2},{partTransform.Translation.Y:F2},{partTransform.Translation.Z:F2}) " +
$"composedT=({model.Translation.X:F2},{model.Translation.Y:F2},{model.Translation.Z:F2})");
}
var restPose = partTransform * meshRef.PartTransform;
ClassifyBatches(partData, partGfxObjId, model, entity, meshRef, palHash, metaTable, restPose, collector);
drewAny = true;