acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs
Erik 5de42a12f3 fix(render): S4 chunk 1 round 1 — the ±12 guard with retail's quantifier, seal count order, the PM/PC depth-event transcript gate
Three-lens review of c7ab5b6d8 found the round-0 port of
DrawPortalPolyInternal's degenerate-input guard inverted retail's
quantifier: it rejected a polygon when ANY vertex sat on ANY of the
four +/-12 planes. The PDB-paired bytes (VA 0x59BCD6-0x59BD66: four
per-plane fld/fcomp 12.0/fnstsw/test ah,0x44/jnp sequences, each
accumulating a "every vertex on this plane" boolean, cleared by the
first vertex NOT on it) and the named decomp's own nested-if reading
of the post-loop branch (0x59bd42-0x59bd6c) agree: retail rejects only
when EVERY local vertex lies on the SAME one of the four planes — a
polygon degenerate onto a plane, not one that merely touches a plane
at one vertex.

F1 — the quantifier. WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard
now tracks four booleans (everyVertexOnPlusX/MinusX/PlusY/MinusY), each
starting true and cleared by the first vertex that fails its own plane
test; the guard rejects iff any of the four survives the whole vertex
loop. T2 (WalkVisibilityMathTests.cs) is rewritten to the five §7
cases: every vertex on x=+12 -> rejected; every vertex on y=-12 ->
rejected; one vertex on x=+12, rest inside -> KEPT; every vertex at
11.999 -> kept; vertices split across x=+12 and y=+12 (no common
plane) -> kept. WalkFrameDriverTests' punch-side pin
(OnPunchGeometry_RejectsOnlyWhenEveryVertexSharesOnePlane_ButPunchesAnyOtherShape)
carries the same correction — its old "one vertex on the plane"
polygon was wrongly rejected under round 0; it punches now, and a new
all-on-plane polygon is added as the actual rejected case. The doc
comment's "essentially never" sentence is deleted and replaced with a
fresh DAT-scan citation (see datScan below).
MUTATION (verified): restore the any-vertex form -> the one-vertex and
split-plane admit cases fail (Assert.False Expected:False Actual:True,
x2; the WalkFrameDriverTests punch pin's Assert.Equal Expected:2
Actual:1). Delete the guard (always return false) -> the all-on-plane
reject cases fail (Assert.True Expected:True Actual:False, x3 —
plusX-plane, minusY-plane, and the vacuous-empty-polygon pin; the
punch pin's Assert.Equal Expected:2 Actual:3).

datScan (DAT counts backing F1's doc comment): a throwaway xunit test
(deleted after this commit — not part of the diff) walked the full
0x0000-0xFFFF landblock-prefix space (LandBlockInfo/EnvCell carry no
usable DatCollection.GetAllIdsOfType range — both are landblock-
relative — matching the method CellStructSurfaceConstructionInstalledDatTests
already established), resolved each EnvCell's Environment/CellStruct,
and for every CellPortal's polygon tested each vertex's local x/y
against +/-12. Against the installed client_cell_1.dat: 3,405
landblocks with cells, 1,854,237 total portal polygons. 2,889 polygons
have at least one vertex on a +/-12 plane; 2,163 have EVERY vertex on
the SAME plane — all 2,163 are EXIT portals (OtherCellId==0xFFFF, out
of 16,939 exit portals total); 0 interior (non-exit) portals ever
qualify (interior: 1,837,298 total, 340 any-vertex, 0 all-on-plane).
Duration ~15s. This exactly reproduces the review's cited numbers.

F2 — seal-path count order. RetailPViewPassExecutor's exit-seal loop
dropped the `localVertices.Length < 3` pre-filter that ran BEFORE both
the boundary guard and the `submitted` increment (round 0's order:
length check -> guard -> transform -> draw -> count). Retail's real
order is guard -> transform -> clip -> count, with the counter
incremented BEFORE polyClipFinish runs (0x59BD70-0x59BD74 precedes
0x59BDB0) — the counter records accepted ATTEMPTS, not successful GPU
fans. `submitted++` now runs immediately after the guard-gated
transform+EmitSeamMask, before DrawDepthFan (whose own `< 3` guard is
this port's stand-in for retail's post-clip `var_4 >= 3` check). A new
pin (RetailPViewPassExecutorTests.DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing)
constructs a real PortalDepthMaskRenderer (RecordingGpuDevice-backed)
and a constructor-bypassed executor (RuntimeHelpers.GetUninitializedObject,
the same pattern WalkOutsideViewReassemblyTests already uses) with a
2-vertex ordinary-coordinate portal polygon: DrawExitPortalMask returns
submitted=1 while zero draw calls reach the device.
MUTATION (verified): restore the `< 3` pre-filter ahead of the guard
-> the pin's Assert.Equal fails, Expected:1 Actual:0.

F3 — the depth-event transcript gate, automated. New
WalkAlphaDepthTrace.cs (beside WalkOracleTrace.cs) parses the four
`PM poly=<ptr> mode=<0|1> counterBefore=<hex>` / `PC ov=<n>
counter=<hex> fc=<0|1>` lines per oh-capture/*.alphadepth.log frame,
using the same F-marker frame delimiting as WalkOracleTrace. New
WalkTraceConformanceTests.AlphaDepthTranscript.cs (beside
WalkTraceConformanceTests.cs, which is now `partial`) replays each
pose's frame 2 through the real WalkFrameDriver/RetailFrameWalk over
WalkLandscapeDatBuilder-built installed-DAT world data, with:
  - AlphaDepthCollectSink: an IWalkEventSink decorator forwarding every
    hook to the real driver unchanged, while recording mode=1 punch
    events (OnPunchGeometry, counterBefore = driver.PortalsDrawnCount
    at COLLECT time — Collect finishes entirely before Replay starts,
    so reading the counter at Replay time would already see the SAME
    turn's post-reset value; holtburg-doorway-still's own capture
    proves this: its punches show counterBefore=2 immediately followed
    by its seals' counterBefore=0/1 in the identical turn) and every
    PC tuple (Emit, WalkEventKind.DrawCells — fires for the root's own
    turn AND every building look-in's re-entrant call, matching
    retail's breakpoint not distinguishing them).
  - AlphaDepthLeaf: an IWalkFrameLeafRenderer whose DrawExitSeals
    reproduces DrawPortalDepthWrite's real per-portal loop (F1's guard,
    F2's count-before-clip order) directly over
    driver.InteriorFloodCells / WalkCell.Portals / .PortalPolygons —
    the identical data the render-side production path reads — since
    the interface gives DrawExitSeals no cell/portal parameter at all.
  - Two Collect+Replay passes per pose (a fresh driver reaches the SAME
    steady-state a running session already carries by the time
    retail's capture began — holtburg-doorway-still and foundry-deep
    both show a stable per-frame accepted-seal count that regenerates
    identically every captured frame): the first pass's own output is
    discarded, only the second is compared.
Five Facts (one per pose — the spec's "four" undercounts the five
*.alphadepth.log files actually present in oh-capture/):
  AlphaDepthTranscript_CathedralArrival_MatchesRetailFrame2 — PASS
  AlphaDepthTranscript_CathedralLeak_MatchesRetailFrame2 — PASS
  AlphaDepthTranscript_FoundryDeep_MatchesRetailFrame2 — PASS
  AlphaDepthTranscript_HoltburgDoorwayStill_MatchesRetailFrame2 — PASS
    (proves the incrementing 0/1/2 accepted-seal sequence AND the
    pre-reset counterBefore=2 punches, end to end, against real DAT
    portal data)
  AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2 — KnownFailure
    (tagged [Trait("Status","KnownFailure")]): terrace-edge never runs
    a qualifying interior turn in its own capture (every PC line reads
    ov=0) — its punches' counterBefore=2 is a session value carried
    over from BEFORE the capture even started (the file's own pre-"F 1"
    content already reads 2; nothing inside the captured frames ever
    touches the counter). A fresh two-pass replay from
    PortalsDrawnCount=0 has no mechanism to derive that leftover value.
    Printed divergence: expected [(mode=1,counterBefore=2) x2], actual
    [(mode=1,counterBefore=0) x2]; the PC sequence (ov=0 both times)
    matches exactly. An initial-condition gap in the fixture, not a
    guard/count defect — the identical harness reproduces holtburg-
    doorway-still's and foundry-deep's own steady states exactly from
    cold starts.
MUTATION (verified): swap AlphaDepthLeaf's recorded seal mode from 0
to 1 -> AlphaDepthTranscript_HoltburgDoorwayStill_MatchesRetailFrame2
fails (Assert.Equal Collections differ at pos 5: expected
(0,0),(0,1)... actual (1,0),(1,1)...).
DispatcherFixture and DrawScope (WalkFrameDriverTests.cs) are widened
from private to internal so the new gate can drive a real
WbDrawDispatcher without duplicating that fixture.

F4 — manifest comment truth. VulkanShaderManifestTests.cs's
portal_depth.vert.spv re-pin comment had the ULP direction backward
(claimed the OLD 0x3F7FFFFE literal was NEARER the camera than
retail's real 0x3F7FFFEF constant; portal_depth.vert's own shader-
source comment already had this correct — FARTHER, larger z/w) and
cited T1 as living in WalkVisibilityMathTests when
PortalDepthVert_FarPunchConstant_MatchesRetailExactBits is in this
same file. Both corrected.

Gates: dotnet build tests/AcDream.App.Tests -c Release -> 0 warnings/0
errors. Hermetic lane (Lane!=InstalledDat&...&Status!=KnownFailure):
6829/6829 passed. InstalledDat lane (real client_cell_1.dat): 253
total, 247 passed, 5 failed — exactly the two #383 layout tests
(LayoutImporterMediaBearingChildSweepTests/LayoutImporterInvisibleSweepTests),
TowerAscentReplayTests' KnownFailure, Oh_doorway_still_first_frame_diff
(#458), and this round's own AlphaDepthTranscript_TerraceEdge
KnownFailure — no other divergence. Shader tests (VulkanShaderDescriptorContractTests
+ VulkanShaderManifestTests + RenderPackSpirvValidatorTests +
SkyVertexLayoutTests): 35/35. Register/divergence tests: 52/52.
dotnet build src/AcDream.App -c Release: 0 warnings/0 errors.

Register: no new AD row. F1/F2 are straight bug fixes toward retail
fidelity (not deviations); AD-119 (the C2 blend-vs-write-mask row)
does not describe the count order, so its neighbour needed no
correction per F2's own instruction.

Deviation from the fix-round spec's literal text: §7 F1 said "swap the
any-vertex hits instead of the four per-plane accumulators" as the
split-plane case's mutation — the code actually ported uses per-vertex
`if (x != 12f) flag = false;` clears, not a per-vertex OR; the mutation
verified above (restore the any-vertex FORM entirely) exercises the
same defect class and is the mutation actually run. §7 also names
"four" alphadepth.log fixtures where five exist on disk
(cathedral-arrival, cathedral-leak, foundry-deep,
holtburg-doorway-still, terrace-edge); all five are gated rather than
four, with cathedral-leak an unremarked addition alongside the four
the spec names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 23:38:52 +02:00

340 lines
17 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Walk;
using DatReaderWriter;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>
/// 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 (<see cref="RetailFrameWalk"/> + <see cref="WalkFrameDriver"/>,
/// <see cref="WalkTraceReplayContext"/> + <see cref="WalkLandscapeDatBuilder"/>
/// — the identical replay harness <see cref="WalkTraceConformanceTests"/>
/// 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 <see cref="WalkAlphaDepthTrace"/>'s parse of
/// the real capture, exactly (pointers ignored).
///
/// <para><b>Why two observation points, not one.</b> Retail's PM line fires
/// at <c>DrawPortalPolyInternal</c>'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 <see cref="WalkFrameDriver.OnPunchGeometry"/> (a Collect-time
/// hook): a rejected punch never reaches <see cref="IWalkFrameLeafRenderer.DrawPunchFan"/>
/// at all, so <see cref="AlphaDepthCollectSink"/> intercepts
/// <c>OnPunchGeometry</c> ITSELF, at Collect time, before the guard runs —
/// reading <see cref="WalkFrameDriver.PortalsDrawnCount"/> 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 (<see
/// cref="IWalkFrameLeafRenderer.DrawExitSeals"/> takes no cell/portal
/// argument — <c>RetailPViewPassExecutor.DrawPortalDepthWrite</c> owns the
/// real per-portal loop entirely at Replay time), so <see cref="AlphaDepthLeaf"/>
/// reproduces that SAME loop (S4-c1 F1's guard, F2's count-before-clip order)
/// directly over the walk's own <see cref="WalkFrameDriver.InteriorFloodCells"/>
/// / <see cref="WalkCell.Portals"/> / <see cref="WalkCell.PortalPolygons"/> —
/// 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).
/// </para>
///
/// <para><b>Why two Collect+Replay passes per pose, not one.</b> Retail's
/// <c>portalsDrawnCount</c> 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 <c>PortalsDrawnCount=0</c> start after exactly one
/// full "priming" pass, so this gate discards a first Collect+Replay pass's
/// own output and compares only the second.</para>
/// </summary>
public sealed partial class WalkTraceConformanceTests
{
/// <summary>Collect-time <see cref="IWalkEventSink"/> decorator: forwards
/// every hook to the real <see cref="WalkFrameDriver"/> 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.</summary>
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<uint> cells, int outsideViewCount) =>
_inner.OnInteriorFloodDrawTurn(cells, outsideViewCount);
public void OnWeatherTurn(uint viewerCellId) =>
_inner.OnWeatherTurn(viewerCellId);
}
/// <summary>Replay-time <see cref="IWalkFrameLeafRenderer"/>: every
/// member besides <see cref="DrawExitSeals"/> is a plain no-op (this
/// gate cares only about the depth/count mechanism, not mesh content).
/// <see cref="DrawExitSeals"/> reproduces
/// <c>RetailPViewPassExecutor.DrawPortalDepthWrite</c>'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).</summary>
private sealed class AlphaDepthLeaf : IWalkFrameLeafRenderer
{
private readonly Dictionary<uint, WalkCell> _cells;
public AlphaDepthLeaf(Dictionary<uint, WalkCell> cells) => _cells = cells;
/// <summary>Set once, immediately after construction — the leaf and
/// its owning <see cref="WalkFrameDriver"/> are mutually referential
/// (the driver's constructor requires the leaf), so this cannot be a
/// constructor parameter.</summary>
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<uint> 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;
}
}
/// <summary>Minimal <see cref="IWalkFrameWorldData"/>: this gate never
/// inspects mesh content, only the depth/count event sequence, so every
/// query returns the shared empty record.</summary>
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;
}
/// <summary>Runs the two-pass replay (see class doc comment for why two)
/// and asserts the SECOND pass's PM/PC sequences against
/// <paramref name="fixtureName"/>'s captured frame 2
/// (<see cref="WalkAlphaDepthTrace"/>'s <c>frames[1]</c>, matching every
/// other OH-capture row's own frame-2 convention in this file).</summary>
private void RunAlphaDepthTranscriptGate(string fixtureName)
{
IReadOnlyList<WalkOracleFrame> 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<WalkAlphaDepthFrame> 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");
/// <summary>
/// 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 <c>ov=0</c>) — its two building punches' <c>counterBefore=2</c>
/// 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 <c>PortalsDrawnCount=0</c> 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
/// <c>counterBefore=0</c> 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 <c>[(mode=1, counterBefore=2), (mode=1, counterBefore=2)]</c>,
/// actual <c>[(mode=1, counterBefore=0), (mode=1, counterBefore=0)]</c>;
/// the PC sequence (ov=0 both times) matches exactly.
/// </summary>
[Fact]
[Trait("Status", "KnownFailure")]
public void AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2()
=> RunAlphaDepthTranscriptGate("terrace-edge.alphadepth");
/// <summary>See <see cref="AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2"/>'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 <c>*.alphadepth.log</c> files actually present in
/// <c>oh-capture/</c> — see this round's commit body.</summary>
[Fact]
public void AlphaDepthTranscript_CathedralLeak_MatchesRetailFrame2()
=> RunAlphaDepthTranscriptGate("cathedral-leak.alphadepth");
}