From 9b948b6ad56d20dcd36816b2aae9853114b733e3 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 19 May 2026 11:50:50 +0200 Subject: [PATCH] feat(dispatcher): [indoor-lookup] + [indoor-xform] probes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Rendering/Wb/WbDrawDispatcher.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 5af05ed..6dbf0c8 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -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;