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>
186 lines
7.4 KiB
C#
186 lines
7.4 KiB
C#
using AcDream.Core.Rendering;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Rendering;
|
|
|
|
public sealed class RenderingDiagnosticsTests
|
|
{
|
|
// Each flag-mutating test snapshots the IndoorAll state on entry and
|
|
// restores it via try/finally. RenderingDiagnostics is a process-wide
|
|
// static (env-var-initialized); without restoration a mutated state
|
|
// leaks into other tests + into parallel test runs. Mirrors the
|
|
// PhysicsDiagnosticsTests pattern at line 30-49.
|
|
|
|
[Fact]
|
|
public void IndoorAll_True_TurnsAllFlagsOn()
|
|
{
|
|
bool initial = RenderingDiagnostics.IndoorAll;
|
|
try
|
|
{
|
|
// Reset all flags off first to make the test deterministic
|
|
// regardless of env-var state on the test runner.
|
|
RenderingDiagnostics.ProbeIndoorWalkEnabled = false;
|
|
RenderingDiagnostics.ProbeIndoorLookupEnabled = false;
|
|
RenderingDiagnostics.ProbeIndoorUploadEnabled = false;
|
|
RenderingDiagnostics.ProbeIndoorXformEnabled = false;
|
|
RenderingDiagnostics.ProbeIndoorCullEnabled = false;
|
|
|
|
RenderingDiagnostics.IndoorAll = true;
|
|
|
|
Assert.True(RenderingDiagnostics.ProbeIndoorWalkEnabled);
|
|
Assert.True(RenderingDiagnostics.ProbeIndoorLookupEnabled);
|
|
Assert.True(RenderingDiagnostics.ProbeIndoorUploadEnabled);
|
|
Assert.True(RenderingDiagnostics.ProbeIndoorXformEnabled);
|
|
Assert.True(RenderingDiagnostics.ProbeIndoorCullEnabled);
|
|
Assert.True(RenderingDiagnostics.IndoorAll);
|
|
}
|
|
finally
|
|
{
|
|
RenderingDiagnostics.IndoorAll = initial;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void IndoorAll_False_TurnsAllFlagsOff()
|
|
{
|
|
bool initial = RenderingDiagnostics.IndoorAll;
|
|
try
|
|
{
|
|
RenderingDiagnostics.IndoorAll = true; // start from all-on
|
|
RenderingDiagnostics.IndoorAll = false;
|
|
|
|
Assert.False(RenderingDiagnostics.ProbeIndoorWalkEnabled);
|
|
Assert.False(RenderingDiagnostics.ProbeIndoorLookupEnabled);
|
|
Assert.False(RenderingDiagnostics.ProbeIndoorUploadEnabled);
|
|
Assert.False(RenderingDiagnostics.ProbeIndoorXformEnabled);
|
|
Assert.False(RenderingDiagnostics.ProbeIndoorCullEnabled);
|
|
Assert.False(RenderingDiagnostics.IndoorAll);
|
|
}
|
|
finally
|
|
{
|
|
RenderingDiagnostics.IndoorAll = initial;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void IndoorAll_OneOff_ReadsAsFalse()
|
|
{
|
|
bool initial = RenderingDiagnostics.IndoorAll;
|
|
try
|
|
{
|
|
RenderingDiagnostics.IndoorAll = true;
|
|
RenderingDiagnostics.ProbeIndoorCullEnabled = false; // flip one off
|
|
Assert.False(RenderingDiagnostics.IndoorAll);
|
|
}
|
|
finally
|
|
{
|
|
RenderingDiagnostics.IndoorAll = initial;
|
|
}
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x00000029ul, false)] // outdoor cell 0x29 in 8x8 grid
|
|
[InlineData(0xA9B40029ul, false)] // outdoor cell with landblock prefix
|
|
[InlineData(0x00000100ul, true)] // indoor cell minimum
|
|
[InlineData(0x00000105ul, true)] // typical Holtburg Inn interior
|
|
[InlineData(0xA9B40105ul, true)] // indoor with landblock prefix
|
|
[InlineData(0xA9B401FFul, true)] // indoor near top of range
|
|
public void IsEnvCellId_DistinguishesOutdoorVsIndoorByLow16Bits(ulong id, bool expected)
|
|
{
|
|
Assert.Equal(expected, RenderingDiagnostics.IsEnvCellId(id));
|
|
}
|
|
|
|
// ── Render inside/outside branch (retail RenderNormalMode is_player_outside) ──
|
|
// The top-level render branch decides DrawInside vs DrawOutside. Retail
|
|
// (SmartBox::RenderNormalMode 0x453aa0:92665) keys it on is_player_outside (the
|
|
// PLAYER's cell, 0x451e80), NOT the camera cell. acdream previously branched on the
|
|
// camera cell, so a chase camera lagging in a doorway while the player was already
|
|
// outside took the DrawInside path and degenerated to a grey world + entities showing
|
|
// through walls. These pin the player-keyed branch and loaded player-root requirement.
|
|
|
|
[Fact]
|
|
public void ShouldRenderIndoor_PlayerOutside_CameraInside_ReturnsFalse()
|
|
{
|
|
// THE doorway-grey regression: the player stepped onto a landcell (0x...0031) but the
|
|
// chase camera still resolves an interior EnvCell. Branch on the PLAYER → outdoor.
|
|
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40031u, renderRootResolved: true));
|
|
}
|
|
|
|
[Fact]
|
|
public void ShouldRenderIndoor_PlayerInside_CameraInside_ReturnsTrue()
|
|
{
|
|
Assert.True(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40171u, renderRootResolved: true));
|
|
}
|
|
|
|
[Fact]
|
|
public void ShouldRenderIndoor_PlayerInside_RootNotLoaded_ReturnsFalse()
|
|
{
|
|
// Opposite lag (camera pulled outside while the player is inside): no viewer cell to
|
|
// root DrawInside at → outdoor. Defensive; matches prior null-CameraCell behavior.
|
|
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40171u, renderRootResolved: false));
|
|
}
|
|
|
|
[Fact]
|
|
public void ShouldRenderIndoor_PlayerOutside_CameraOutside_ReturnsFalse()
|
|
{
|
|
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0xA9B40031u, renderRootResolved: false));
|
|
}
|
|
|
|
[Fact]
|
|
public void ShouldRenderIndoor_UnknownPlayerCell_TreatedAsOutside_ReturnsFalse()
|
|
{
|
|
// playerCellId == 0 (unresolved) → treat as outside (safe default: outdoor render).
|
|
Assert.False(RenderingDiagnostics.ShouldRenderIndoor(playerCellId: 0u, renderRootResolved: true));
|
|
}
|
|
|
|
[Fact]
|
|
public void ProbePortalChurn_DefaultsFalse_WhenEnvUnset()
|
|
{
|
|
// Env var is absent in the test host, so the flag must default false (inert probe).
|
|
Assert.False(AcDream.Core.Rendering.RenderingDiagnostics.ProbePortalChurnEnabled);
|
|
}
|
|
|
|
// ── ACDREAM_DUMP_ENTITY parser (#119 decisive probe) ──────────────────
|
|
|
|
[Fact]
|
|
public void ParseDumpEntityIds_NullOrEmpty_ReturnsEmptySet()
|
|
{
|
|
Assert.Empty(RenderingDiagnostics.ParseDumpEntityIds(null));
|
|
Assert.Empty(RenderingDiagnostics.ParseDumpEntityIds(""));
|
|
Assert.Empty(RenderingDiagnostics.ParseDumpEntityIds(" "));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseDumpEntityIds_CommaSeparatedWithPrefixes_ParsesAll()
|
|
{
|
|
var set = RenderingDiagnostics.ParseDumpEntityIds("0x020003F2,0x020005D8");
|
|
Assert.Equal(2, set.Count);
|
|
Assert.Contains(0x020003F2u, set);
|
|
Assert.Contains(0x020005D8u, set);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseDumpEntityIds_NoPrefixMixedCaseAndWhitespace_Parses()
|
|
{
|
|
var set = RenderingDiagnostics.ParseDumpEntityIds(" 020003f2 , 0X020005D8 ");
|
|
Assert.Equal(2, set.Count);
|
|
Assert.Contains(0x020003F2u, set);
|
|
Assert.Contains(0x020005D8u, set);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseDumpEntityIds_MalformedSegment_IgnoredNotFatal()
|
|
{
|
|
// A typo'd segment must not take the launch down — parse what's valid.
|
|
var set = RenderingDiagnostics.ParseDumpEntityIds("0x020003F2,zzz,,0x");
|
|
Assert.Single(set);
|
|
Assert.Contains(0x020003F2u, set);
|
|
}
|
|
|
|
[Fact]
|
|
public void DumpEntitySourceIds_DefaultsEmpty_WhenEnvUnset()
|
|
{
|
|
// Env var is absent in the test host → probe inert.
|
|
Assert.Empty(RenderingDiagnostics.DumpEntitySourceIds);
|
|
}
|
|
}
|