using System.Numerics;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Walk;
using DatReaderWriter;
namespace AcDream.App.Tests.Rendering.Walk;
///
/// S4-c1 fix round 1, F3: the automated depth-event transcript gate.
/// Replays each OH alpha/depth capture's frame 2 through the SAME ported
/// walk ( + ,
/// +
/// — the identical replay harness
/// already uses) with a RECORDING sink/leaf pair that reproduces retail's
/// own PM (mode, counterBefore) and PC (ov, counter, forceClear) sequences,
/// then compares them against 's parse of
/// the real capture, exactly (pointers ignored).
///
/// Why two observation points, not one. Retail's PM line fires
/// at DrawPortalPolyInternal's function ENTRY — before the four-plane
/// boundary guard (S4-c1 F1) has run — so a PM line is emitted for every
/// ATTEMPT, guard-rejected or not. A building punch's guard decision runs
/// INSIDE (a Collect-time
/// hook): a rejected punch never reaches
/// at all, so intercepts
/// OnPunchGeometry ITSELF, at Collect time, before the guard runs —
/// reading there gives the
/// PRE-reset value retail's own punches observe (Collect completes entirely
/// before Replay starts, so reading the counter at Replay time would already
/// see the root's own post-reset value — the wrong number, confirmed against
/// holtburg-doorway-still's own counterBefore=0002 punches immediately
/// followed by counterBefore=0000/0001 seals in the SAME turn). Exit seals
/// have no such Collect-time per-portal hook at all ( takes no cell/portal
/// argument — RetailPViewPassExecutor.DrawPortalDepthWrite owns the
/// real per-portal loop entirely at Replay time), so
/// reproduces that SAME loop (S4-c1 F1's guard, F2's count-before-clip order)
/// directly over the walk's own
/// / / —
/// the identical LOCAL portal-polygon data the production seal path reads,
/// at Replay time, which is also where retail's own exit-seal loop runs
/// (established: punches always precede the single per-root ExitSeals
/// Collect-time event, so simple list concatenation reproduces the exact
/// interleaved PM order without needing a shared Collect/Replay timeline).
///
///
/// Why two Collect+Replay passes per pose, not one. Retail's
/// portalsDrawnCount is a PERSISTENT global, unaffected by any reset
/// between captured frames whose interior turn never fires (see
/// terrace-edge below). holtburg-doorway-still and foundry-deep both show a
/// STEADY-STATE cycle (the same N seals accepted every captured frame,
/// carrying the SAME counter value into the next frame's own PC read) —
/// reachable from a cold PortalsDrawnCount=0 start after exactly one
/// full "priming" pass, so this gate discards a first Collect+Replay pass's
/// own output and compares only the second.
///
public sealed partial class WalkTraceConformanceTests
{
/// Collect-time decorator: forwards
/// every hook to the real unchanged (so its
/// own event collection stays complete and correct), while separately
/// recording the two families this gate needs — see this file's own
/// class doc comment for why punches must be captured HERE rather than
/// at Replay.
private sealed class AlphaDepthCollectSink : IWalkEventSink
{
private readonly WalkFrameDriver _driver;
private readonly IWalkEventSink _inner;
public AlphaDepthCollectSink(WalkFrameDriver driver)
{
_driver = driver;
_inner = driver;
}
public readonly List<(int Mode, int CounterBefore)> Punches = new();
public readonly List<(int Ov, int Counter, int ForceClear)> PcEvents = new();
public void Emit(in WalkEvent walkEvent)
{
// PView::DrawCells @0x005a4840's own PC print fires at function
// ENTRY for every call — the root's own turn AND every building
// look-in's re-entrant call alike (oh1-depth-lifecycle.md: the
// breakpoint doesn't distinguish). WalkEventKind.DrawCells's
// Emit call is the walk's own "this DrawCells call happened"
// signal at that SAME entry-order point (its own doc comment:
// "sits at breakpoint-ENTRY order"); forceClear (oh1: no write
// site found anywhere in the binary) is always 0.
if (walkEvent.Kind == WalkEventKind.DrawCells)
PcEvents.Add((walkEvent.OutsideViewCount, _driver.PortalsDrawnCount, 0));
_inner.Emit(walkEvent);
}
public void OnLandscapeViews(WalkPortalView activeViews) =>
_inner.OnLandscapeViews(activeViews);
public void OnLandCellTurn(uint landblockId, int sideCellCount, int cellIndex) =>
_inner.OnLandCellTurn(landblockId, sideCellCount, cellIndex);
public void OnSortCellTurn(uint landblockId, int sideCellCount, int cellIndex) =>
_inner.OnSortCellTurn(landblockId, sideCellCount, cellIndex);
public void OnLandscapeCellTurn(uint cellId) =>
_inner.OnLandscapeCellTurn(cellId);
public void OnLandscapeCellTurn(uint landblockId, int sideCellCount, int cellIndex) =>
_inner.OnLandscapeCellTurn(landblockId, sideCellCount, cellIndex);
public void OnBuildingTurn(WalkBuilding building) =>
_inner.OnBuildingTurn(building);
public void OnBuildingShellTurn(WalkBuilding building) =>
_inner.OnBuildingShellTurn(building);
public void OnPunchGeometry(
WalkBuilding building, WalkPolygon polygon, int activeViewIndex)
{
// Far-Z punches never touch portalsDrawnCount (R2); this reads
// it purely as a snapshot for the transcript, BEFORE the SAME
// interior root's own OnInteriorFloodDrawTurn reset — see class
// doc comment.
Punches.Add((1, _driver.PortalsDrawnCount));
_inner.OnPunchGeometry(building, polygon, activeViewIndex);
}
public void OnInteriorFloodDrawTurn(IReadOnlyList cells, int outsideViewCount) =>
_inner.OnInteriorFloodDrawTurn(cells, outsideViewCount);
public void OnWeatherTurn(uint viewerCellId) =>
_inner.OnWeatherTurn(viewerCellId);
}
/// Replay-time : every
/// member besides is a plain no-op (this
/// gate cares only about the depth/count mechanism, not mesh content).
/// reproduces
/// RetailPViewPassExecutor.DrawPortalDepthWrite's real per-portal
/// loop — S4-c1 F1's guard, F2's count-before-clip order — directly over
/// the walk's own flood/cell data (no fake GPU clip needed: F2
/// established the count happens unconditionally once the guard
/// passes).
private sealed class AlphaDepthLeaf : IWalkFrameLeafRenderer
{
private readonly Dictionary _cells;
public AlphaDepthLeaf(Dictionary cells) => _cells = cells;
/// Set once, immediately after construction — the leaf and
/// its owning are mutually referential
/// (the driver's constructor requires the leaf), so this cannot be a
/// constructor parameter.
public WalkFrameDriver Driver { get; set; } = null!;
public readonly List<(int Mode, int CounterBefore)> Seals = new();
public void DrawSky() { }
public void DrawLandCellBatch(
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells) { }
public bool HasRenderableEmittersInCell(uint cellId) => false;
public void DrawCellShell(uint cellId) { }
public void DrawStaticParticles(uint cellId) { }
public void DrawCellParticles(uint cellId) { }
public void ClearInteriorDepth() { }
public void FlushLandscape() { }
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) { }
public void AlphaBarrier() { }
public int DrawExitSeals()
{
int accepted = 0;
List flood = Driver.InteriorFloodCells;
// Reverse order — the SAME direction DrawWalkExitPortalMasks
// walks driver.InteriorFloodCells in production
// (RetailPViewRenderer.cs).
for (int i = flood.Count - 1; i >= 0; i--)
{
if (!_cells.TryGetValue(flood[i], out WalkCell? cell))
continue;
int sliceCount = Driver.InteriorFloodViewSliceCountAt(i);
for (int slice = 0; slice < sliceCount; slice++)
{
for (int p = 0; p < cell.Portals.Length; p++)
{
WalkCellPortal portal = cell.Portals[p];
// Walk-side exit sentinel is 32-bit 0xFFFFFFFF
// (WalkCellPortal's own doc comment) — the render-
// side CellPortalInfo's 16-bit 0xFFFF is a DIFFERENT
// representation of the same fact.
if (portal.OtherCellId != 0xFFFFFFFFu)
continue;
if ((uint)portal.PolygonIndex >= (uint)cell.PortalPolygons.Length)
continue;
WalkPolygon poly = cell.PortalPolygons[portal.PolygonIndex];
// F2: count happens BEFORE clip (unconditionally,
// once the guard passes) — counterBefore is the
// running total BEFORE this specific portal's own
// decision.
Seals.Add((0, Driver.PortalsDrawnCount + accepted));
if (!WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(
poly.Vertices))
{
accepted++;
}
}
}
}
return accepted;
}
}
/// Minimal : this gate never
/// inspects mesh content, only the depth/count event sequence, so every
/// query returns the shared empty record.
private sealed class EmptyAlphaDepthWorldData : IWalkFrameWorldData
{
public WalkFrameStaticRecords GetCellStatics(uint cellId) => WalkFrameStaticRecords.Empty;
public WalkFrameStaticRecords GetCellDynamics(uint cellId) => WalkFrameStaticRecords.Empty;
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) => WalkFrameStaticRecords.Empty;
public WalkFrameStaticRecords GetOutdoorDynamics(uint cellId) => WalkFrameStaticRecords.Empty;
public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building) =>
WalkFrameStaticRecords.Empty;
public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building) => Matrix4x4.Identity;
}
/// Runs the two-pass replay (see class doc comment for why two)
/// and asserts the SECOND pass's PM/PC sequences against
/// 's captured frame 2
/// ('s frames[1], matching every
/// other OH-capture row's own frame-2 convention in this file).
private void RunAlphaDepthTranscriptGate(string fixtureName)
{
IReadOnlyList poseFrames = WalkOracleTrace.Load(OhCaptureRoot, fixtureName);
Assert.NotEmpty(poseFrames);
Assert.True(poseFrames.Count > 1, $"{fixtureName}: need a captured frame 2.");
WalkOracleFrame poseFrame = poseFrames[1];
Assert.NotNull(poseFrame.Pose);
WalkOraclePose pose = poseFrame.Pose!;
IReadOnlyList depthFrames = WalkAlphaDepthTrace.Load(OhCaptureRoot, fixtureName);
Assert.True(depthFrames.Count > 1, $"{fixtureName}: need a captured frame 2's PM/PC content.");
WalkAlphaDepthFrame expected = depthFrames[1];
using DatCollection dats = OpenDats();
WalkLandscapeDatBuilder.BuiltWorld world =
WalkLandscapeDatBuilder.Build(dats, pose.CellId, pose.Origin);
var ctx = new WalkTraceReplayContext(pose, world.Cells) { Buildings = world.Buildings };
WalkCell? camera = (pose.CellId & 0xFFFFu) >= 0x100
? Assert.Contains(pose.CellId, world.Cells)
: null;
using var fx = new WalkFrameDriverTests.DispatcherFixture();
var worldData = new EmptyAlphaDepthWorldData();
var leaf = new AlphaDepthLeaf(world.Cells);
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
leaf.Driver = driver;
var sink = new AlphaDepthCollectSink(driver);
var walk = new RetailFrameWalk();
void RunOnePass()
{
using (WalkFrameDriverTests.DrawScope draw = fx.BeginDraw())
{
driver.BeginFrame(ctx, Matrix4x4.Identity, pose.Origin);
walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, sink);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
}
// DrawScope.Dispose ends only the pass/publication — the frame
// itself (RecordingGpuDevice's "one open frame" invariant) is
// ended separately, same order WalkOutsideViewReassemblyTests'
// own multi-draw loop uses.
fx.FrameLifetime.EndFrame();
}
// Priming pass: reaches the steady state a real running session
// would already be in by the time retail's capture began (see class
// doc comment) — its own recorded output is discarded.
RunOnePass();
sink.Punches.Clear();
sink.PcEvents.Clear();
leaf.Seals.Clear();
// Comparison pass.
RunOnePass();
var actualPm = new List<(int Mode, int CounterBefore)>(sink.Punches);
actualPm.AddRange(leaf.Seals);
Assert.Equal(expected.PmEvents, actualPm);
Assert.Equal(expected.PcEvents, sink.PcEvents);
}
[Fact]
public void AlphaDepthTranscript_CathedralArrival_MatchesRetailFrame2()
=> RunAlphaDepthTranscriptGate("cathedral-arrival.alphadepth");
[Fact]
public void AlphaDepthTranscript_FoundryDeep_MatchesRetailFrame2()
=> RunAlphaDepthTranscriptGate("foundry-deep.alphadepth");
[Fact]
public void AlphaDepthTranscript_HoltburgDoorwayStill_MatchesRetailFrame2()
=> RunAlphaDepthTranscriptGate("holtburg-doorway-still.alphadepth");
///
/// PINNED KnownFailure (S4-c1 fix round 1, F3): terrace-edge never runs
/// a qualifying interior turn anywhere in its own capture (every PC line
/// reads ov=0) — its two building punches' counterBefore=2
/// is a PERSISTENT session value carried over from BEFORE the capture
/// even started (the file's own pre-"F 1" content already reads 2; no
/// mechanism inside the captured frames ever changes it). A fresh two-
/// pass replay from PortalsDrawnCount=0 has no way to derive that
/// leftover value — there is no seal activity anywhere in this fixture
/// to prime it — so this pose's own comparison pass reads
/// counterBefore=0 where retail shows 2. This is an initial-
/// condition gap in the fixture itself, not a guard/count defect: the
/// SAME replay harness reproduces holtburg-doorway-still's and foundry-
/// deep's own steady-state counter values exactly from a cold start.
/// Observed divergence (both PM events, the fixture's only two):
/// expected [(mode=1, counterBefore=2), (mode=1, counterBefore=2)],
/// actual [(mode=1, counterBefore=0), (mode=1, counterBefore=0)];
/// the PC sequence (ov=0 both times) matches exactly.
///
[Fact]
[Trait("Status", "KnownFailure")]
public void AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2()
=> RunAlphaDepthTranscriptGate("terrace-edge.alphadepth");
/// See 's
/// own doc comment — cathedral-leak carries the exact same steady state
/// (counter pinned at 0, every exit portal degenerate-onto-plane
/// rejected) as cathedral-arrival, so it is expected to pass; listed
/// here because the spec's own fixture count (four) undercounts the
/// five *.alphadepth.log files actually present in
/// oh-capture/ — see this round's commit body.
[Fact]
public void AlphaDepthTranscript_CathedralLeak_MatchesRetailFrame2()
=> RunAlphaDepthTranscriptGate("cathedral-leak.alphadepth");
}