Replaces the single scope-global distance-sorted RetailAlphaQueue with
retail's own two independent FIFO lists (CLIP/ALPHA, capacity 3000 each,
D3DPolyRender::AddMeshToAlphaList's exact append-only/capacity-drop
behavior — Ghidra-verified 2026-09-04), routed by a new
RetailAlphaMeshRouter porting DrawMesh's five-row immediate/delayed
branch table and ConstructMesh's subset-mask formula as pure functions,
and drained at retail's four normal-world FlushAlphaList sites
(DrawBuilding/DrawBlock/PView::DrawCells/RenderNormalMode) under the
exact Ghidra-verified no-op predicate (both counts strictly below
threshold*3000). A new WalkFrameEventKind.SortCellExit /
IWalkEventSink.OnSortCellExit / IWalkFrameLeafRenderer.FlushSortCellExit
fires once per admitted land-block cell for DrawBlock's 0.75f valve,
pinned by a dedicated far/near ordering test in RetailFrameWalkTests.cs.
WbDrawDispatcher's two submit sites and ParticleRenderer's one route
through the router; since none of the three ever draws during the Sky
leaf, installs a detail surface, or sets MultiPassAlpha, rows 1/2/4/5
are provably unreachable there and the call sites assert loudly rather
than building unexercisable immediate-draw plumbing. FlushFartherThan,
RetailAlphaOrdering.ComputeViewerDistance, and every viewerDistance
argument on the submit path are deleted.
Scope note (packet s4-depth-alpha-packet.md §10): C4 (routing EnvCell's
transparent shell batches through the shared queue) was not attempted —
EnvCell draws one per-cell MultiDrawIndexedIndirect call with no
per-subset deferred-replay abstraction, and building one without visual
verification (no graphical client in this worktree) was judged out of
this bounded chunk's scope. AP-34 is therefore retired and replaced by
two narrower rows rather than deleted outright: AP-236 (the carried-
forward EnvCell-immediate residual) and AP-237 (a newly identified gap:
TranslucencyKind.AlphaBlend can arise from either retail's Alpha/
Translucent bits, mask 0x02/ALPHA, or the Translucent+ClipMap "cloud"
override, mask 0x08/CLIP — GroupKey doesn't retain the raw bit to tell
them apart, so the router always picks ALPHA; only known example is
cloud GfxObj 0x01004C35). Both are compositing-order-only divergences,
never blend/visual ones.
Mutation checks (each applied, confirmed failing, then reverted):
- FIFO drain order reversed -> 5 RetailAlphaQueueTests fail (order).
- FlushAlphaList `<` -> `<=` -> boundary/scratch tests fail (2250 case
reads drained=0 instead of 2250).
- Capacity check loosened (3000 -> 6000) -> overflow-drop test fails
(TryAppend returns true, PendingCount reads 3001).
- IsFirstForList forced true -> flag test fails once inspected on the
pre-flush two-entry snapshot (the post-flush single-survivor version
of this test was vacuous and rewritten).
- Router row 3 condition inverted -> both the hand-traced Theory (6
cases) and the 160-cell independent-truth-table brute force fail (20
mismatches).
- SortCellExit emitted before OnLandscapeCellTurn instead of after ->
RetailFrameWalkTests ordering pin fails ("SCX must immediately follow
its own cell's SC").
- Prepare-per-list instead of prepare-once-combined -> the CLIP/ALPHA
boundary batching test throws (index out of range).
Gates: Release build 0 warnings/0 errors. Hermetic lane (Lane!=Installed
Dat&...&Status!=KnownFailure) 6855/6855 passed. InstalledDat lane 249
passed / 10 failed — exactly the 4 pre-existing failures (#383 x2
LayoutImporter, TowerAscent KnownFailure, #458 Oh_doorway_still
KnownFailure) plus 6 NEW KnownFailure Facts
(AlphaFlushTranscript_*_MatchesRetailFrame2, one per capture) extending
this gate from PM/PC to AM/FL: the flush SITE+THRESHOLD sequence matches
the capture exactly for all six poses (including zero SortCellExit
drains in every capture, confirming the 0.75 valve is inert at these
scene complexities in both retail and this replay); the drained-COUNT-
per-list dimension diverges because this hermetic harness (matching the
existing PM/PC gate's own EmptyAlphaDepthWorldData design) carries no
live GfxObj/particle content, so every observed count reads (0,0)
against retail's real per-frame volume — both sequences quoted in full
per pose in the packet's new §10. Shader classes (VulkanShaderDescriptor
ContractTests/VulkanShaderManifestTests/RenderPackSpirvValidatorTests)
32/32 passed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
593 lines
32 KiB
C#
593 lines
32 KiB
C#
using System.Linq;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
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;
|
|
|
|
/// <summary>S4-c2 gate G-c2: the real production queue — this gate's
|
|
/// walk carries no live GfxObj/particle mesh content (same design as
|
|
/// this file's own PM/PC harness: <see cref="EmptyAlphaDepthWorldData"/>
|
|
/// returns empty everywhere), so <see cref="_queue"/> stays at
|
|
/// ClipCount/AlphaCount == 0 throughout — but the SEQUENCE of (site,
|
|
/// threshold) calls the walk drives it through, and each call's
|
|
/// real no-op-vs-drain decision under those (always-empty) counts,
|
|
/// are both genuine production behavior, not stubbed.</summary>
|
|
private readonly RetailAlphaQueue _queue;
|
|
|
|
public AlphaDepthLeaf(Dictionary<uint, WalkCell> cells, RetailAlphaQueue queue)
|
|
{
|
|
_cells = cells;
|
|
_queue = queue;
|
|
}
|
|
|
|
/// <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();
|
|
|
|
/// <summary>Every ACTUAL flush this replay drove the queue through,
|
|
/// in order — (site, threshold, clip count observed immediately
|
|
/// BEFORE the drain, alpha count observed immediately BEFORE the
|
|
/// drain). A no-op call (both counts stay strictly under
|
|
/// <c>threshold*3000</c>) is not recorded, matching
|
|
/// <see cref="WalkAlphaDepthTrace.BuildExpectedFlushTranscript"/>'s
|
|
/// own definition of "a flush" on the capture side.</summary>
|
|
public readonly List<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)>
|
|
ActualFlushes = new();
|
|
|
|
private void RecordFlush(RetailAlphaFlushSite site, float threshold)
|
|
{
|
|
int clipBefore = _queue.ClipCount;
|
|
int alphaBefore = _queue.AlphaCount;
|
|
// Same no-op predicate as D3DPolyRender::FlushAlphaList itself
|
|
// (RetailAlphaQueue.Flush) — computed here, before the real
|
|
// call, purely so this recorder can decide whether THIS call
|
|
// counts as "a flush" for the transcript.
|
|
bool noOp = clipBefore < threshold * RetailAlphaQueue.ListCapacity
|
|
&& alphaBefore < threshold * RetailAlphaQueue.ListCapacity;
|
|
_queue.Flush(site, threshold);
|
|
if (!noOp)
|
|
ActualFlushes.Add((site, threshold, clipBefore, alphaBefore));
|
|
}
|
|
|
|
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() => RecordFlush(RetailAlphaFlushSite.LandscapeFlush, 0f);
|
|
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) { }
|
|
public void AlphaBarrier() => RecordFlush(RetailAlphaFlushSite.DrawBuilding, 0f);
|
|
public void FlushSortCellExit() => RecordFlush(RetailAlphaFlushSite.SortCellExit, 0.75f);
|
|
|
|
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();
|
|
// S4-c2: AlphaDepthLeaf now records every flush through a real
|
|
// RetailAlphaQueue (used by the AM/FL sibling gate below); this PM/PC
|
|
// gate doesn't care about that queue's transcript, but it still must
|
|
// be a valid ACTIVE frame or AlphaBarrier/FlushLandscape/
|
|
// FlushSortCellExit's real Flush() calls throw.
|
|
var alphaQueue = new RetailAlphaQueue();
|
|
var leaf = new AlphaDepthLeaf(world.Cells, alphaQueue);
|
|
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;
|
|
|
|
alphaQueue.BeginFrame();
|
|
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();
|
|
alphaQueue.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)}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// S4-c2 gate G-c2: extends this file's own PM/PC transcript gate from
|
|
/// depth events to the AM/FL alpha-queue stream. Reuses the EXACT SAME
|
|
/// replay harness (<see cref="EmptyAlphaDepthWorldData"/>: no live
|
|
/// GfxObj/particle mesh content, matching this file's own established
|
|
/// PM/PC pattern) with a real <see cref="RetailAlphaQueue"/> wired
|
|
/// through <see cref="AlphaDepthLeaf"/>'s flush hooks.
|
|
///
|
|
/// <para><b>What this DOES prove, unconditionally (no KnownFailure
|
|
/// needed):</b> the SEQUENCE of (site, threshold) the walk drives the
|
|
/// queue through — which of the four normal-world sites fires, how many
|
|
/// times, in what order — is purely structural (a function of cell/
|
|
/// building/land-cell traversal, never of mesh content), so it is
|
|
/// checked here as an unconditional pass/fail.</para>
|
|
///
|
|
/// <para><b>What this CANNOT prove with this harness:</b> the drained
|
|
/// entry count PER LIST. This gate's queue carries zero live content
|
|
/// (same design choice as the PM/PC gate it extends), so every 0f-
|
|
/// threshold flush observes count (0, 0) — always a real drain (0 is
|
|
/// never <c>< 0</c>), matching retail's own always-drains behavior at
|
|
/// threshold 0f, but with a DIFFERENT count than the capture's real
|
|
/// content volume. A capture pose whose expected transcript has any
|
|
/// nonzero drained count therefore diverges on the count dimension by
|
|
/// harness design, not by a routing bug — see this Fact's own
|
|
/// <c>[Trait("Status","KnownFailure")]</c> and the S4-c2 packet write-up
|
|
/// (S4 packet §9) for the full per-pose sequences.</para>
|
|
/// </summary>
|
|
private void RunAlphaFlushTranscriptGate(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 AM/FL content.");
|
|
WalkAlphaDepthFrame expectedFrame = depthFrames[1];
|
|
int initialCounter = WalkAlphaDepthTrace.LoadInitialCounter(OhCaptureRoot, fixtureName);
|
|
var expected = WalkAlphaDepthTrace.BuildExpectedFlushTranscript(expectedFrame);
|
|
|
|
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 queue = new RetailAlphaQueue();
|
|
var leaf = new AlphaDepthLeaf(world.Cells, queue);
|
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
|
|
leaf.Driver = driver;
|
|
var sink = new AlphaDepthCollectSink(driver);
|
|
var walk = new RetailFrameWalk();
|
|
driver.PortalsDrawnCount = initialCounter;
|
|
|
|
queue.BeginFrame();
|
|
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);
|
|
}
|
|
fx.FrameLifetime.EndFrame();
|
|
|
|
// SmartBox::RenderNormalMode's own top-level FlushAlphaList(0f) —
|
|
// RetailAlphaQueue.EndFrame() calls this internally, so it is
|
|
// recorded here rather than through a leaf hook (see
|
|
// RetailAlphaQueue.EndFrame's own doc comment).
|
|
int clipBeforeFinal = queue.ClipCount;
|
|
int alphaBeforeFinal = queue.AlphaCount;
|
|
queue.EndFrame();
|
|
leaf.ActualFlushes.Add(
|
|
(RetailAlphaFlushSite.RenderNormalMode, 0f, clipBeforeFinal, alphaBeforeFinal));
|
|
|
|
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> actual =
|
|
leaf.ActualFlushes;
|
|
|
|
bool siteSequenceMatches = expected
|
|
.Select(e => (e.Site, e.Threshold))
|
|
.SequenceEqual(actual.Select(a => (a.Site, a.Threshold)));
|
|
|
|
// The site+threshold sequence is a REAL, unconditional pass/fail —
|
|
// purely structural, never content-dependent.
|
|
Assert.True(
|
|
siteSequenceMatches,
|
|
$"{fixtureName}: flush SITE sequence diverges (content-independent — this must "
|
|
+ $"match unconditionally) — expected {FormatFlushSites(expected)}, "
|
|
+ $"actual {FormatFlushSites(actual)}");
|
|
|
|
// The full tuple (including per-list drained counts) is the part
|
|
// this harness's zero-content design cannot prove — recorded here
|
|
// as a real, evaluated assertion (never weakened), expected to
|
|
// diverge on the count dimension for any pose with real content;
|
|
// callers gate on Status=KnownFailure, not on this passing.
|
|
bool fullMatches = expected.SequenceEqual(actual);
|
|
Assert.True(
|
|
fullMatches,
|
|
$"{fixtureName}: flush transcript diverges on drained COUNT only (site+threshold "
|
|
+ "sequence above already verified exact) — this hermetic replay carries no live "
|
|
+ "GfxObj/particle content (EmptyAlphaDepthWorldData, matching this file's own "
|
|
+ $"PM/PC design), so every count reads (0,0) — expected {FormatFlushCounts(expected)}, "
|
|
+ $"actual {FormatFlushCounts(actual)}");
|
|
}
|
|
|
|
private static string FormatFlushSites(
|
|
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> events) =>
|
|
"[" + string.Join(", ", events.Select(e => $"({e.Site},thresh={e.Threshold})")) + "]";
|
|
|
|
private static string FormatFlushCounts(
|
|
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> events) =>
|
|
"[" + string.Join(
|
|
", ",
|
|
events.Select(e => $"({e.Site},thresh={e.Threshold},clip={e.DrainedClip},alpha={e.DrainedAlpha})")) + "]";
|
|
|
|
/// <summary>S4-c2 gate G-c2. See <see cref="RunAlphaFlushTranscriptGate"/>'s
|
|
/// own doc comment for what this Fact does and does not prove; tagged
|
|
/// KnownFailure because this hermetic harness carries no live mesh
|
|
/// content, so the drained-count dimension diverges from the capture's
|
|
/// real content volume by design — the S4 packet's §9 subsection quotes
|
|
/// both sequences per pose.</summary>
|
|
[Fact]
|
|
[Trait("Status", "KnownFailure")]
|
|
public void AlphaFlushTranscript_CathedralArrival_MatchesRetailFrame2()
|
|
=> RunAlphaFlushTranscriptGate("cathedral-arrival.alphadepth");
|
|
|
|
[Fact]
|
|
[Trait("Status", "KnownFailure")]
|
|
public void AlphaFlushTranscript_CathedralLeak_MatchesRetailFrame2()
|
|
=> RunAlphaFlushTranscriptGate("cathedral-leak.alphadepth");
|
|
|
|
[Fact]
|
|
[Trait("Status", "KnownFailure")]
|
|
public void AlphaFlushTranscript_CathedralStairArch_MatchesRetailFrame2()
|
|
=> RunAlphaFlushTranscriptGate("cathedral-stair-arch.alphadepth");
|
|
|
|
[Fact]
|
|
[Trait("Status", "KnownFailure")]
|
|
public void AlphaFlushTranscript_FoundryDeep_MatchesRetailFrame2()
|
|
=> RunAlphaFlushTranscriptGate("foundry-deep.alphadepth");
|
|
|
|
[Fact]
|
|
[Trait("Status", "KnownFailure")]
|
|
public void AlphaFlushTranscript_HoltburgDoorwayStill_MatchesRetailFrame2()
|
|
=> RunAlphaFlushTranscriptGate("holtburg-doorway-still.alphadepth");
|
|
|
|
[Fact]
|
|
[Trait("Status", "KnownFailure")]
|
|
public void AlphaFlushTranscript_TerraceEdge_MatchesRetailFrame2()
|
|
=> RunAlphaFlushTranscriptGate("terrace-edge.alphadepth");
|
|
|
|
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. Under a cold <c>0</c>
|
|
/// BOTH sequences diverged in the counter field — PM expected
|
|
/// <c>(1,2),(1,2)</c> and PC expected <c>(0,2,0),(0,2,0)</c> against a
|
|
/// cold <c>(1,0)</c>/<c>(0,0,0)</c> — round 1's write-up wrongly called
|
|
/// the PC side a match, and round 2's gate-honesty lens caught the same
|
|
/// sentence re-worded here (lead correction at landing). Seeded, both
|
|
/// sequences match exactly; this pose never runs a qualifying interior
|
|
/// turn, so PC stays <c>ov=0</c> with the seeded counter.
|
|
/// </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) — 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. This gate compares only the
|
|
/// <c>(mode, counterBefore)</c> and <c>(ov, counter, forceClear)</c>
|
|
/// tuples: it does NOT observe which of the sixteen punches the ±12
|
|
/// boundary guard rejects (the sink records every attempt before the
|
|
/// guard runs; the <c>pmverts</c> capture and the DAT, not this Fact,
|
|
/// show twelve of them lying on local <c>y=12.000</c> — S4 packet §9).
|
|
/// The seed <c>8</c> is the first PM/PC sample
|
|
/// <see cref="WalkAlphaDepthTrace.LoadInitialCounter"/> finds in the
|
|
/// file, which for this fixture lies inside frame 1 (this capture has no
|
|
/// pre-"F 1" preamble sample); with the priming pass gone, frame 2's PC
|
|
/// <c>counter</c> equals that seed by construction in steady state.
|
|
/// </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");
|
|
}
|