Round 1 (2bd353234) was three-lens reviewed: retail-faithful PASS,
production PASS, gate-honesty FAIL. Every finding landed on the
terrace-edge KnownFailure row plus one pin gap. This is the last round.
R2-1 - gate honesty. WalkTraceConformanceTests.AlphaDepthTranscript.cs's
own KnownFailure doc comment claimed "the PC sequence matches exactly";
it never did - Assert.Equal(expected.PmEvents, actualPm) threw first, so
the PC Assert.Equal line never ran. RunAlphaDepthTranscriptGate now
computes BOTH diffs (SequenceEqual, not Assert.Equal) before asserting
either, and Assert.Fail's message prints both sequences together
whenever either diverges - a written claim about one sequence is now
only ever one this method actually evaluated. Re-running the OLD
(unseeded, cold PortalsDrawnCount=0) code path through this new combined
assertion shows terrace-edge's real divergence was WORSE than round 1
believed: both PM and PC diverged (PC expected counter=2, actual
counter=0) - round 1's "PC matches" claim was not just untested, it was
false.
R2-2 - the harness fix. Retail's portalsDrawnCount (wo(008719b4)) is a
PERSISTENT session global; each capture's own FIRST PM or PC line
(cathedral-arrival 0, cathedral-leak 0, foundry-deep 1,
holtburg-doorway-still 2, terrace-edge 2, cathedral-stair-arch 8) already
carries the pre-capture value - terrace-edge's own sample sits in the
"F 1" preamble text a cdb session prints before the first parsed frame
marker (line 1209, ahead of "F 1" at line 1298), so no amount of
replaying the CAPTURED frames from a cold start can ever reach it (this
fixture's own turns never touch the counter - every PC line reads
ov=0). New WalkAlphaDepthTrace.LoadInitialCounter(root, fixtureName)
scans a fixture's raw lines, ignoring frame boundaries, for the first
PM/PC match and returns its counter. RunAlphaDepthTranscriptGate seeds
WalkFrameDriver.PortalsDrawnCount (already an internal, test-assembly-
settable field) from this value, runs ONE Collect+Replay pass, and
deletes the two-pass "priming" replay outright. Result: terrace-edge
turns GREEN with no KnownFailure tag; every other pose stays exact
(a fixture with real steady-state seal activity computes the identical
value a priming pass used to converge to, since a steady state is by
definition unchanged by one more pass). No pose diverges after seeding,
so the packet's "write the divergence into section 9" fallback never
triggers - nothing needed there this round.
R2-3 - the SOME-vs-SAME-plane discriminator. T2's existing five cases
never separated retail's real quantifier ("every vertex lies on the SAME
one of the four +/-12 planes") from a weaker "every vertex lies on SOME
plane" (per-vertex OR across the four planes, ANDed across vertices) -
the existing split-plane case's third vertex sits on NO plane at all, so
both readings admit it for the same reason. New case in
WalkVisibilityMathTests.cs
(Boundary_guard_admits_a_polygon_whose_every_vertex_is_on_SOME_plane_but_not_the_SAME_one)
and a fourth sink.OnPunchGeometry call in WalkFrameDriverTests.cs's
OnPunchGeometry_RejectsOnlyWhenEveryVertexSharesOnePlane_ButPunchesAnyOtherShape
pin close the gap: [(12,0,3),(0,12,3),(12,5,3)] - every vertex touches
SOME plane (vertex1/3 on x=+12, vertex2 on y=+12) but no SINGLE plane
holds all three (x=+12 cleared by vertex2's x=0, y=+12 cleared by
vertex1's y=0) - retail's real four-accumulator guard admits it; the
weaker per-vertex-OR form rejects it.
R2-4 - the sixth pose. Cherry-picked 4f12cd06b (walk+alphadepth capture)
and 3a6aa7eef (pmverts capture) from claude/campaign-w-retail-frame-walk
onto 2bd353234 - both applied clean, no conflicts (README.md merged
without a manual row). New
AlphaDepthTranscript_CathedralStairArch_MatchesRetailFrame2 Fact: root
0xF4180114, seven outside views, sixteen mode=1 building punches (all
counterBefore=0008 - far-Z punches never touch the counter) of which
twelve are guard-rejected on local y=12.000, then eight mode=0 exit
seals with counterBefore running 0..7 against the root's own ov=7
read-then-zero, settling this fixture's own steady-state counter at 8 -
exactly the value LoadInitialCounter reads back out of its own preamble.
PASS.
R2-5 - comment truth. (a) RetailPViewPassExecutor.cs:
DrawPortalDepthWrite's and DrawExitPortalMask's summaries corrected -
neither the retired ">=3 vertices" pre-filter nor "always matches what
reached the GPU" describe F2's actual count-before-clip behavior any
more. (b) The F2 pin's doc comment
(RetailPViewPassExecutorTests.DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing)
named TWO alternative mutations as if both failed; verified only one
does (see mutations below) - the doc comment now says so explicitly and
keeps only the pre-filter mutation. (c) WalkVisibilityMath.cs's doc
comment stated DAT-scan figures as bare facts; independently re-verified
this round with a freshly-written scan (own code, walking the same
production EnvCell.CellPortals/CellStruct.Polygons shape
WalkCellFactory itself uses, over the full 0x0000-0xFFFF landblock-prefix
range) - every cited figure reproduced bit-exact (3,405 landblocks with
cells; 1,854,237 portal polygons; 2,889 any-vertex; 2,163 all-on-plane,
all exit, 0 interior; 16,939 exit portals total, 1,837,298 interior) -
so the doc comment now states the counting rule (id range, what's
counted, how unresolvable polygons/cells are skipped) alongside every
figure instead of leaving them unqualified; landblocksWithCells=3,405
also independently matches CellStructSurfaceConstructionInstalledDatTests'
own OH2 walk of the identical range (that test's own golden-count
comment). The scratch scan itself is not part of this diff (deleted
after use, same as F1's own datScan). (d) This commit body does not
cite "register tests" as evidence - no test reads the register.
Register truth is the human read: no row changes, AD-119 (the C2
blend-vs-write-mask row) unchanged.
Mutations run this round (all verified against production code, then
reverted):
- R2-1/R2-2 (disable seeding, driver.PortalsDrawnCount = 0 instead of
initialCounter): AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2
fails - "terrace-edge.alphadepth: PM DIVERGES - expected
[(mode=1,counterBefore=2), (mode=1,counterBefore=2)], actual
[(mode=1,counterBefore=0), (mode=1,counterBefore=0)]; PC DIVERGES -
expected [(ov=0,counter=2,fc=0), (ov=0,counter=2,fc=0)], actual
[(ov=0,counter=0,fc=0), (ov=0,counter=0,fc=0)]" - proving both the
seeding fix (R2-2) and the honest dual-sequence message (R2-1) at
once.
- R2-3 (replace the four per-plane accumulators with a single
per-vertex "on ANY plane" test ANDed across vertices):
Boundary_guard_admits_a_polygon_whose_every_vertex_is_on_SOME_plane_but_not_the_SAME_one
fails, Assert.False() Expected:False Actual:True;
OnPunchGeometry_RejectsOnlyWhenEveryVertexSharesOnePlane_ButPunchesAnyOtherShape
fails, Assert.Equal() Expected:3 Actual:2.
- R2-5b, mutation ONE (restore the old `localVertices.Length < 3`
pre-filter ahead of the guard):
DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing
fails, Assert.Equal() Expected:1 Actual:0 - kept as the pin's cited
mutation.
- R2-5b, mutation TWO (move `submitted++` to AFTER
`_portalDepthMask.DrawDepthFan(...)` instead of before): same test
STILL PASSES - DrawDepthFan has no effect on the local `submitted`
counter, so the final returned value is identical regardless of
which side of that call the increment sits on. Confirmed unobservable
exactly as R2-5b required; the doc comment no longer names this as a
failing mutation.
Gates: dotnet build tests/AcDream.App.Tests -c Release -> 0 warnings/0
errors. dotnet build src/AcDream.App -c Release -> 0 warnings/0 errors.
Hermetic lane (Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&
Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&Lane!=SystemFont&
Purpose!=Diagnostic&Status!=KnownFailure): 6830/6830 passed (+1 over
round 1's 6829, the new T2 case). InstalledDat lane
(ACDREAM_RUN_INSTALLED_DAT_TESTS=1, --filter Lane=InstalledDat): 254
total (+1 over round 1's 253, the new sixth Fact), 249 passed, 4 failed -
exactly the pre-existing four (LayoutImporterMediaBearingChildSweepTests
and LayoutImporterInvisibleSweepTests, both #383;
TowerAscentReplayTests' KnownFailure; Oh_doorway_still_first_frame_diff,
#458) plus the pre-existing WalkLookInGateSweepTests skip - terrace-edge
no longer fails, no new divergence anywhere else. All six
AlphaDepthTranscript_*_MatchesRetailFrame2 Facts: 6/6 passed standalone.
Shader classes (VulkanShaderDescriptorContractTests +
VulkanShaderManifestTests + RenderPackSpirvValidatorTests +
SkyVertexLayoutTests): 35/35. RetailPViewPassExecutorTests +
WalkFrameDriverTests (full classes, regression check on the R2-3/R2-5b
edits): 57/57.
Commits: two cherry-picks (4f12cd06b, 3a6aa7eef) from
claude/campaign-w-retail-frame-walk, both clean, plus this one fix
commit on top of 2bd353234.
Register: no new AD row; AD-119 unchanged (human read, not a test -
see R2-5d above).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
379 lines
20 KiB
C#
379 lines
20 KiB
C#
using System.Linq;
|
|
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 the counter is SEEDED, not primed by a throwaway pass
|
|
/// (S4-c1 fix round 2, R2-2).</b> Retail's <c>portalsDrawnCount</c> is a
|
|
/// PERSISTENT session global (wo(008719b4)), unaffected by any reset
|
|
/// between captured frames — a running client carries whatever value the
|
|
/// counter last settled at into every subsequent capture. Each fixture's
|
|
/// own FIRST PM or PC line (wherever it falls — including the "F 1"
|
|
/// preamble text a cdb session prints before the first parsed frame
|
|
/// marker, which is where terrace-edge's and holtburg-doorway-still's own
|
|
/// first samples live) already carries that pre-capture value:
|
|
/// cathedral-arrival 0, cathedral-leak 0, foundry-deep 1,
|
|
/// holtburg-doorway-still 2, terrace-edge 2, cathedral-stair-arch 8 (see
|
|
/// <see cref="WalkAlphaDepthTrace.LoadInitialCounter"/>'s own doc comment).
|
|
/// Fix round 1 tried to reach a matching steady state by running a
|
|
/// throwaway "priming" Collect+Replay pass first and discarding its
|
|
/// output — this works only for a fixture whose captured frames themselves
|
|
/// contain enough seal activity to climb from a cold
|
|
/// <c>PortalsDrawnCount=0</c> start up to the needed value (holtburg-
|
|
/// doorway-still, foundry-deep); terrace-edge's own capture never runs a
|
|
/// qualifying interior turn at all (every PC line reads <c>ov=0</c>), so no
|
|
/// number of priming passes from a cold start can ever reach its
|
|
/// <c>counterBefore=2</c> — the value has to come from BEFORE the capture,
|
|
/// not from replaying the capture against itself. Seeding
|
|
/// <see cref="WalkFrameDriver.PortalsDrawnCount"/> directly from the
|
|
/// fixture's own first sample before a SINGLE Collect+Replay pass fixes
|
|
/// this for every pose, terrace-edge included, without changing what a
|
|
/// pose with real seal activity computes (a fixture's own first sample
|
|
/// IS the steady-state value fix round 1's priming pass converged to,
|
|
/// since a steady state is by definition unchanged by one more pass).</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>Seeds <see cref="WalkFrameDriver.PortalsDrawnCount"/> from
|
|
/// <paramref name="fixtureName"/>'s own first observed sample (S4-c1 fix
|
|
/// round 2, R2-2 — see class doc comment), runs ONE Collect+Replay pass,
|
|
/// and asserts BOTH the PM and 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).
|
|
/// <para>R2-1 (gate honesty): both diffs are computed BEFORE either is
|
|
/// asserted, so a PM mismatch can never prevent the PC comparison from
|
|
/// running (or vice versa) — a written claim that one sequence
|
|
/// "matches" is only ever one this method actually evaluated.</para>
|
|
/// </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];
|
|
int initialCounter = WalkAlphaDepthTrace.LoadInitialCounter(OhCaptureRoot, fixtureName);
|
|
|
|
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();
|
|
|
|
// R2-2: seed the persistent counter from the fixture's own
|
|
// pre-capture value instead of priming a throwaway first pass.
|
|
driver.PortalsDrawnCount = initialCounter;
|
|
|
|
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();
|
|
|
|
var actualPm = new List<(int Mode, int CounterBefore)>(sink.Punches);
|
|
actualPm.AddRange(leaf.Seals);
|
|
IReadOnlyList<(int Ov, int Counter, int ForceClear)> actualPc = sink.PcEvents;
|
|
|
|
bool pmMatches = expected.PmEvents.SequenceEqual(actualPm);
|
|
bool pcMatches = expected.PcEvents.SequenceEqual(actualPc);
|
|
if (!pmMatches || !pcMatches)
|
|
{
|
|
Assert.Fail(
|
|
$"{fixtureName}: PM {(pmMatches ? "matches" : "DIVERGES")} " +
|
|
$"— expected {FormatPm(expected.PmEvents)}, actual {FormatPm(actualPm)}; " +
|
|
$"PC {(pcMatches ? "matches" : "DIVERGES")} " +
|
|
$"— expected {FormatPc(expected.PcEvents)}, actual {FormatPc(actualPc)}");
|
|
}
|
|
}
|
|
|
|
private static string FormatPm(IReadOnlyList<(int Mode, int CounterBefore)> events) =>
|
|
"[" + string.Join(", ", events.Select(e => $"(mode={e.Mode},counterBefore={e.CounterBefore})")) + "]";
|
|
|
|
private static string FormatPc(IReadOnlyList<(int Ov, int Counter, int ForceClear)> events) =>
|
|
"[" + string.Join(", ", events.Select(e => $"(ov={e.Ov},counter={e.Counter},fc={e.ForceClear})")) + "]";
|
|
|
|
[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>
|
|
/// S4-c1 fix round 2 (R2-2): seeding <see cref="WalkFrameDriver.PortalsDrawnCount"/>
|
|
/// from terrace-edge's own first observed sample (<c>counterBefore=2</c>,
|
|
/// carried in the file's pre-"F 1" preamble text — see
|
|
/// <see cref="WalkAlphaDepthTrace.LoadInitialCounter"/>'s own doc
|
|
/// comment) closes fix round 1's KnownFailure: both of this fixture's
|
|
/// PM events now read the correct pre-capture value instead of a cold
|
|
/// <c>0</c>, and its PC sequence (<c>ov=0</c> throughout — this pose
|
|
/// never runs a qualifying interior turn at all) already matched.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2()
|
|
=> RunAlphaDepthTranscriptGate("terrace-edge.alphadepth");
|
|
|
|
/// <summary>
|
|
/// S4-c1 fix round 2, R2-4: the sixth pose (#464 artifact pose, root
|
|
/// <c>0xF4180114</c>, seven outside views). Sixteen building punches —
|
|
/// all <c>mode=1</c>, all <c>counterBefore=0008</c> (far-Z punches never
|
|
/// touch the counter) — of which twelve are guard-rejected on local
|
|
/// <c>y=12.000</c> (WalkVisibilityMath's ±12 boundary guard, S4-c1 F1),
|
|
/// leaving four admitted; then eight true-depth exit seals with
|
|
/// <c>counterBefore</c> running <c>0..7</c> against the root's own
|
|
/// <c>ov=7</c> read-then-zero, settling this fixture's own steady-state
|
|
/// counter at <c>8</c> — the same value <see cref="WalkAlphaDepthTrace.LoadInitialCounter"/>
|
|
/// reads back out of this fixture's own preamble, confirming the seed
|
|
/// and the replay agree on the same persistent value.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AlphaDepthTranscript_CathedralStairArch_MatchesRetailFrame2()
|
|
=> RunAlphaDepthTranscriptGate("cathedral-stair-arch.alphadepth");
|
|
|
|
/// <summary>Cathedral-leak carries the exact same steady state (counter
|
|
/// pinned at 0, every exit portal degenerate-onto-plane rejected) as
|
|
/// cathedral-arrival; listed here (S4-c1 fix round 1, F3) because the
|
|
/// original spec's own fixture count (four) undercounts the five
|
|
/// <c>*.alphadepth.log</c> files that were already present in
|
|
/// <c>oh-capture/</c> at that round — see that round's commit body.</summary>
|
|
[Fact]
|
|
public void AlphaDepthTranscript_CathedralLeak_MatchesRetailFrame2()
|
|
=> RunAlphaDepthTranscriptGate("cathedral-leak.alphadepth");
|
|
}
|