#119 decisive probe: ACDREAM_DUMP_ENTITY one-shot entity dump (H-A/H-B/H-C discriminator)
The broken-state log (user-session-capture2.log) shows meshMissing=0 / entSeen==entDrawn WHILE broken stairs are on screen - the staircase is DRAWN WRONG, not missing. This probe discriminates the three live hypotheses in ONE launch (handoff 2026-06-11 s4): - HYDRATE dump (GameWindow.BuildInteriorEntitiesForStreaming): per-part placement-frame translations + dropped-part accounting at the MOMENT MeshRefs are constructed. H-A (SetupMesh.Flatten identity fallback / silent gfx-null part drops under degraded dat reads) shows here as zero translations or built<43. - DRAW dump (WbDrawDispatcher, first tuple per entity): live MeshRefs translation summary + per-part loaded flags + Tier-1 classification cache state (batch count + RestPose translation summary), re-emitted compactly on signature change. H-B (partial/stale cached batch set) shows as correct translations + odd batch count. - WALK-REJECT lines (rate-limited): attributes 'entity never reaches the draw loop' to the specific gate (visibleCellIds/frustum). - Correct everything -> H-C (draw-side compose), instrument next. Targets: ACDREAM_DUMP_ENTITY=0x020003F2,0x020005D8 (the 43-part spiral staircase Setup + the wall barrels; H-A predicts the user's 'barrel' IS the collapsed staircase). Probe is inert when the env var is unset. Parser in RenderingDiagnostics (diagnostic-owner pattern) + 5 unit tests. Suites: App 242+1skip / Core 1427+2skip / UI 420 / Net 294. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
d82f070b88
commit
3cf6bcc219
4 changed files with 246 additions and 2 deletions
|
|
@ -5610,6 +5610,15 @@ public sealed class GameWindow : IDisposable
|
|||
// Phase 2d: static objects inside the EnvCell.
|
||||
foreach (var stab in envCell.StaticObjects)
|
||||
{
|
||||
// #119 decisive probe: HYDRATE-side dump for ACDREAM_DUMP_ENTITY-
|
||||
// targeted stabs. This is the MOMENT MeshRefs are constructed —
|
||||
// a degraded dat read here (setup null / placement frames short /
|
||||
// part GfxObj null) permanently corrupts the entity (H-A), and
|
||||
// nothing downstream ever rebuilds it. Inert when the set is empty.
|
||||
bool dumpStab = AcDream.Core.Rendering.RenderingDiagnostics
|
||||
.DumpEntitySourceIds.Contains(stab.Id);
|
||||
int dumpSetupParts = -1, dumpPlacementFrames = -1, dumpFlattened = -1, dumpDropped = 0;
|
||||
|
||||
var meshRefs = new List<AcDream.Core.World.MeshRef>();
|
||||
if ((stab.Id & 0xFF000000u) == 0x01000000u)
|
||||
{
|
||||
|
|
@ -5620,6 +5629,10 @@ public sealed class GameWindow : IDisposable
|
|||
_ = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(stab.Id, System.Numerics.Matrix4x4.Identity));
|
||||
}
|
||||
else if (dumpStab)
|
||||
{
|
||||
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} GFXOBJ-NULL -> entity dropped");
|
||||
}
|
||||
}
|
||||
else if ((stab.Id & 0xFF000000u) == 0x02000000u)
|
||||
{
|
||||
|
|
@ -5628,18 +5641,39 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
_physicsDataCache.CacheSetup(stab.Id, setup);
|
||||
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
|
||||
if (dumpStab)
|
||||
{
|
||||
dumpSetupParts = setup.Parts.Count;
|
||||
dumpPlacementFrames = setup.PlacementFrames.Count;
|
||||
dumpFlattened = flat.Count;
|
||||
}
|
||||
foreach (var mr in flat)
|
||||
{
|
||||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
if (gfx is null)
|
||||
{
|
||||
dumpDropped++;
|
||||
if (dumpStab)
|
||||
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} part gfx=0x{mr.GfxObjId:X8} GFXOBJ-NULL -> part dropped");
|
||||
continue;
|
||||
}
|
||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
||||
_ = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
meshRefs.Add(mr);
|
||||
}
|
||||
}
|
||||
else if (dumpStab)
|
||||
{
|
||||
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} SETUP-NULL -> entity dropped");
|
||||
}
|
||||
}
|
||||
|
||||
if (meshRefs.Count == 0) continue;
|
||||
if (meshRefs.Count == 0)
|
||||
{
|
||||
if (dumpStab)
|
||||
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} meshRefs=0 -> entity dropped");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stabs inside EnvCells are already in landblock-local coordinates
|
||||
// (same space as LandBlockInfo.Objects stabs). Adding cellOrigin would
|
||||
|
|
@ -5656,6 +5690,21 @@ public sealed class GameWindow : IDisposable
|
|||
MeshRefs = meshRefs,
|
||||
ParentCellId = envCellId,
|
||||
};
|
||||
|
||||
if (dumpStab)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} entId=0x{hydrated.Id:X8} " +
|
||||
$"setupParts={dumpSetupParts} placementFrames={dumpPlacementFrames} flattened={dumpFlattened} " +
|
||||
$"built={meshRefs.Count} dropped={dumpDropped} " +
|
||||
$"pos=({worldPos.X:F2},{worldPos.Y:F2},{worldPos.Z:F2})");
|
||||
for (int i = 0; i < meshRefs.Count; i++)
|
||||
{
|
||||
var t = meshRefs[i].PartTransform.Translation;
|
||||
Console.WriteLine($"[dump-entity] hyd-part[{i:D2}] gfx=0x{meshRefs[i].GfxObjId:X8} t=({t.X:F3},{t.Y:F3},{t.Z:F3})");
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(hydrated);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,6 +251,21 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
private readonly HashSet<ulong> _missRequested = new();
|
||||
private readonly HashSet<ulong> _missLogged = new();
|
||||
|
||||
// #119 decisive probe (2026-06-11): ACDREAM_DUMP_ENTITY one-shot entity
|
||||
// dump. Keyed by entity Id; the stored signature re-emits the header line
|
||||
// whenever (MeshRefs count, cache batch count, zero-translation count,
|
||||
// culled) changes — e.g. the Tier-1 populate landing one frame after the
|
||||
// first slow-path draw. The full per-part listing prints only on first
|
||||
// sight. Inert (one Count==0 check per new entity) when the env var is
|
||||
// unset. Render-thread only.
|
||||
private readonly Dictionary<uint, (int MeshRefCount, int CacheBatches, int ZeroT, bool Culled)> _entityDumpSig = new();
|
||||
|
||||
// Rate limiter for [dump-entity] WALK-REJECT lines: a rejected entity
|
||||
// re-tests every frame; emit the first rejection per entity then every
|
||||
// 300th (~5 s at 60 fps). Static because WalkEntitiesInto is static;
|
||||
// render-thread only like the walk itself.
|
||||
private static readonly Dictionary<uint, int> _walkRejectCounts = new();
|
||||
|
||||
// CPU + GPU timing for [WB-DIAG] under ACDREAM_WB_DIAG=1.
|
||||
private readonly System.Diagnostics.Stopwatch _cpuStopwatch = new();
|
||||
private readonly long[] _cpuSamples = new long[256]; // microseconds
|
||||
|
|
@ -662,6 +677,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (!cellInVis)
|
||||
{
|
||||
if (shellScoped) result.BuildingShellAnchorReject++;
|
||||
MaybeEmitWalkRejectDump(entity, "visibleCellIds-miss");
|
||||
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
|
||||
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||
{
|
||||
|
|
@ -687,6 +703,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
if (!aabbVisible)
|
||||
{
|
||||
MaybeEmitWalkRejectDump(entity, "frustum");
|
||||
if (isCellEntity && RenderingDiagnostics.ProbeIndoorCullEnabled
|
||||
&& indoorProbeState!.ShouldEmit(cellProbeId))
|
||||
{
|
||||
|
|
@ -719,6 +736,94 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #119 decisive probe: rate-limited <c>[dump-entity] WALK-REJECT</c> line
|
||||
/// for an <c>ACDREAM_DUMP_ENTITY</c>-targeted entity that the walk filtered
|
||||
/// out (visibleCellIds gate / per-entity frustum). Absence of any DRAW dump
|
||||
/// plus presence of these lines attributes "entity exists but never reaches
|
||||
/// the draw loop" to the specific gate. Inert when the target set is empty.
|
||||
/// </summary>
|
||||
private static void MaybeEmitWalkRejectDump(WorldEntity entity, string reason)
|
||||
{
|
||||
var targets = RenderingDiagnostics.DumpEntitySourceIds;
|
||||
if (targets.Count == 0 || !targets.Contains(entity.SourceGfxObjOrSetupId)) return;
|
||||
_walkRejectCounts.TryGetValue(entity.Id, out int n);
|
||||
_walkRejectCounts[entity.Id] = n + 1;
|
||||
if (n % 300 != 0) return;
|
||||
Console.WriteLine(
|
||||
$"[dump-entity] WALK-REJECT id=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} " +
|
||||
$"reason={reason} parentCell=0x{(entity.ParentCellId ?? 0u):X8} " +
|
||||
$"pos=({entity.Position.X:F2},{entity.Position.Y:F2},{entity.Position.Z:F2}) n={n + 1}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #119 decisive probe: per-entity state dump at draw time for
|
||||
/// <c>ACDREAM_DUMP_ENTITY</c>-targeted entities. First sight prints a
|
||||
/// header + every MeshRef's GfxObj id, part-transform translation, and
|
||||
/// loaded flag; afterwards a compact header re-emits only when the
|
||||
/// (meshRefs, cacheBatches, zeroTranslations, culled) signature changes.
|
||||
/// Discriminates H-A (hydration-time MeshRef corruption: translations
|
||||
/// collapsed to ~zero / missing parts) from H-B (Tier-1 cache holding a
|
||||
/// partial or stale batch set) from H-C (both healthy ⇒ draw-side compose).
|
||||
/// </summary>
|
||||
private void MaybeEmitEntityDump(WorldEntity entity, uint landblockId, bool culled)
|
||||
{
|
||||
var targets = RenderingDiagnostics.DumpEntitySourceIds;
|
||||
if (targets.Count == 0 || !targets.Contains(entity.SourceGfxObjOrSetupId)) return;
|
||||
|
||||
var refs = entity.MeshRefs;
|
||||
int zeroT = 0;
|
||||
float tzMin = float.MaxValue, tzMax = float.MinValue;
|
||||
for (int i = 0; i < refs.Count; i++)
|
||||
{
|
||||
var t = refs[i].PartTransform.Translation;
|
||||
if (t.LengthSquared() < 1e-9f) zeroT++;
|
||||
if (t.Z < tzMin) tzMin = t.Z;
|
||||
if (t.Z > tzMax) tzMax = t.Z;
|
||||
}
|
||||
|
||||
int cacheBatches = -1;
|
||||
int restZero = 0;
|
||||
float rzMin = float.MaxValue, rzMax = float.MinValue;
|
||||
if (_cache.TryGet(entity.Id, landblockId, out var cacheEntry))
|
||||
{
|
||||
cacheBatches = cacheEntry!.Batches.Length;
|
||||
foreach (var b in cacheEntry.Batches)
|
||||
{
|
||||
var t = b.RestPose.Translation;
|
||||
if (t.LengthSquared() < 1e-9f) restZero++;
|
||||
if (t.Z < rzMin) rzMin = t.Z;
|
||||
if (t.Z > rzMax) rzMax = t.Z;
|
||||
}
|
||||
}
|
||||
|
||||
var sig = (refs.Count, cacheBatches, zeroT, culled);
|
||||
bool first = !_entityDumpSig.TryGetValue(entity.Id, out var prev);
|
||||
if (!first && prev == sig) return;
|
||||
_entityDumpSig[entity.Id] = sig;
|
||||
|
||||
string cacheStr = cacheBatches < 0
|
||||
? (_tier1CacheDisabled ? "disabled" : "miss")
|
||||
: $"hit:{cacheBatches} restZero={restZero} restZ=[{rzMin:F2}..{rzMax:F2}]";
|
||||
Console.WriteLine(
|
||||
$"[dump-entity] DRAW{(first ? "" : "-CHANGED")} id=0x{entity.Id:X8} src=0x{entity.SourceGfxObjOrSetupId:X8} " +
|
||||
$"lb=0x{landblockId:X8} cell=0x{(entity.ParentCellId ?? 0u):X8} " +
|
||||
$"pos=({entity.Position.X:F2},{entity.Position.Y:F2},{entity.Position.Z:F2}) scale={entity.Scale:F2} " +
|
||||
$"meshRefs={refs.Count} tZero={zeroT} tZ=[{tzMin:F2}..{tzMax:F2}] cache={cacheStr} culled={culled}");
|
||||
|
||||
if (first)
|
||||
{
|
||||
for (int i = 0; i < refs.Count; i++)
|
||||
{
|
||||
var mr = refs[i];
|
||||
var t = mr.PartTransform.Translation;
|
||||
bool loaded = _meshAdapter.TryGetRenderData(mr.GfxObjId) is not null;
|
||||
Console.WriteLine(
|
||||
$"[dump-entity] part[{i:D2}] gfx=0x{mr.GfxObjId:X8} t=({t.X:F3},{t.Y:F3},{t.Z:F3}) loaded={loaded}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Draw(
|
||||
ICamera camera,
|
||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||
|
|
@ -920,6 +1025,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_cellIdToSlot, _outdoorSlot, _outdoorVisible);
|
||||
if (_currentEntityCulled)
|
||||
probeCulledEntities++;
|
||||
|
||||
// #119 decisive probe: one-shot dump (+ change re-emission) for
|
||||
// ACDREAM_DUMP_ENTITY-targeted entities. Before the culled-continue
|
||||
// so a routed-out entity still reports its state.
|
||||
MaybeEmitEntityDump(entity, landblockId, _currentEntityCulled);
|
||||
}
|
||||
prevTupleEntityId = entity.Id;
|
||||
|
||||
|
|
|
|||
|
|
@ -373,6 +373,47 @@ public static class RenderingDiagnostics
|
|||
/// </summary>
|
||||
public static bool IsEnvCellId(ulong id) => (id & 0xFFFFu) >= 0x0100u;
|
||||
|
||||
/// <summary>
|
||||
/// #119 tower-staircase decisive probe (2026-06-11). Comma-separated
|
||||
/// Setup / GfxObj source ids (hex, optional 0x prefix) from
|
||||
/// <c>ACDREAM_DUMP_ENTITY</c>. Any <c>WorldEntity</c> whose
|
||||
/// <c>SourceGfxObjOrSetupId</c> is in this set emits:
|
||||
/// (a) a <c>[dump-entity] HYDRATE</c> dump at MeshRef construction time
|
||||
/// (<c>GameWindow.BuildInteriorEntitiesForStreaming</c>) — per-part
|
||||
/// placement-frame translations + dropped-part accounting — discriminating
|
||||
/// hydration-time corruption (H-A: SetupMesh.Flatten identity fallback /
|
||||
/// silent gfx-null part drops under degraded dat reads);
|
||||
/// (b) a <c>[dump-entity] DRAW</c> dump in <c>WbDrawDispatcher</c> at first
|
||||
/// draw — live MeshRefs translations + Tier-1 classification cache state —
|
||||
/// re-emitted compactly whenever that state changes (H-B: stale/partial
|
||||
/// cached batch set); and
|
||||
/// (c) rate-limited <c>[dump-entity] WALK-REJECT</c> lines when the
|
||||
/// dispatcher's walk filters the entity out (absence-of-draw attribution).
|
||||
/// Empty set = probe off; every call site early-outs on <c>Count == 0</c>.
|
||||
/// </summary>
|
||||
public static IReadOnlySet<uint> DumpEntitySourceIds { get; } =
|
||||
ParseDumpEntityIds(Environment.GetEnvironmentVariable("ACDREAM_DUMP_ENTITY"));
|
||||
|
||||
/// <summary>
|
||||
/// Parse the <c>ACDREAM_DUMP_ENTITY</c> value: comma-separated hex ids,
|
||||
/// optional 0x prefix, whitespace tolerated, malformed segments ignored
|
||||
/// (probes are forgiving — a typo'd segment must not take the launch down).
|
||||
/// Internal for unit tests.
|
||||
/// </summary>
|
||||
internal static IReadOnlySet<uint> ParseDumpEntityIds(string? raw)
|
||||
{
|
||||
var set = new HashSet<uint>();
|
||||
if (string.IsNullOrWhiteSpace(raw)) return set;
|
||||
foreach (var seg in raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var s = seg.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? seg[2..] : seg;
|
||||
if (uint.TryParse(s, System.Globalization.NumberStyles.HexNumber,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var id))
|
||||
set.Add(id);
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The top-level render branch: should this frame run the indoor (DrawInside) path?
|
||||
///
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue