acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkTraceConformanceTests.AlphaDepthTranscript.cs
Erik 9ccb61a8ec fix(render): S4 chunk 2 round 1 - particle throw, detail-off immediate draws, EnvCell FIFO, gate honesty
Fix round 1 of the two the plan allows for S4-c2 (048d5b12f), addressing
every item M1-M8/A1-A8 from the campaign branch's packet section 11.

M1 (BLOCKING): ParticleRenderer.DeferToRetailAlphaQueue threw whenever a
mesh-particle batch's TranslucencyKind.Opaque classification (mask 0x00)
reached a non-Append DrawMesh row - a real, reachable outcome since
TryAppendMeshDraws applies no IsOpaque-style pre-filter unlike
WbDrawDispatcher's path. Deleted both throws (ParticleRenderer.cs,
WbDrawDispatcher.cs SubmitToAlphaQueue), extracted the router-input
derivation into internal static ParticleRenderer.RouteParticleSubmission
(directly testable), and implemented rows 1/5's immediate draw by reusing
the existing per-instance billboard/mesh draw code (new
DrawImmediateParticleSubmissionRhi, no new renderer, no sort per A8).
materialHasAlpha (row 4) is derived from CMaterial::CheckAlphaValues
@0x005396a0 (Ghidra-verified 2026-09-04): has_alpha=0 only when
Ambient.a==Diffuse.a==Specular.a==Emissive.a==1.0f, else 1 - and
CMaterial::SetTranslucencySimple @0x005396f0 (reached from
CPhysicsPart::SetTranslucency <- Particle::Update @0x0051c290's per-frame
alpha-fade) sets all four channels to (1f - translucency) uniformly, so
has_alpha == "this particle's current alpha isn't 1.0" - already carried in
ColorArgb's top byte (Lerp(StartAlpha,EndAlpha,tLife), ParticleSystem.cs
1039-1041/1453).

M3: SubmitToAlphaQueue now takes isBuildingShell + viewProjection and feeds
Row 1's detailSurfaceActive gate for translucent building-shell instances
(retail DrawBuilding @0x0059f2a0 installs building detail before its own
shell subsets reach DrawMesh). Row 1 (detail on) draws the instance
immediately at its own walk-stream AlphaSubmitMark via new
DrawImmediateAlphaInstance/DrawImmediateAlphaInstanceRhi (reusing
WriteDeferredAlphaEntrySlot + the drain's pipeline/detail code for slot 0);
never touches _deferredAlpha/the queue for pure-Immediate submissions (fixes
a latent leak: previously reserving a _deferredAlpha slot for every
candidate meant a frame with ALL-immediate content never registered a
source, so ResetAlphaSubmissions never ran). M3c: DrawPreparedAlphaBatchRhi's
own per-command hasDetail/DrawBuildingDetailRangeRhi branch is deleted -
detail-eligible content no longer reaches the drain (spec: delayed replay
is always detailEnabled=0). New AD-120 register row for the "in place -> at
the entity's own stream mark" granularity adaptation this introduces.

M6: RetailPViewPassExecutor.SubmitOrDrawTransparentCellShell routes an
EnvCell's transparent shell through the SAME RetailAlphaMeshRouter table
(mask 0x02, detailSurfaceActive = EnvCellRenderer.TransparentDetailEnabled)
instead of a hand-coded branch. Detail on: unchanged immediate draw. Detail
off: appends ONE token per CELL via new EnvCellAlphaDrawSource (internal,
delegate-based so it's unit-testable without a GPU harness), replayed
through EnvCellRenderer.RenderTransparentOrdered at the next flush -
RenderTransparentOrdered already replayed a cell's batch on demand, so no
new deferred-replay abstraction was needed (the original landing's claim
that one was required is corrected in the packet).

M4: RetailAlphaEntry.IsFirstForList deleted. Retail's per-DrawMesh-
invocation "first for this list" flag is trivially true for every subset
retail ever appends (DrawMesh @0x0059d4a0 sets both flags true at entry,
0059d4cc/0059d4d0, clearing each independently after ITS OWN first append
within that call, 0059d5ef) - the captures confirm it (new=1 on 9,685/10,556
AM lines, impossible under "first since last drain"). acdream's deleted
isFirstForList = target.Count == 0 computed the wrong quantifier. One
acdream append already IS one DrawMesh call, so the field carried no
information and its own pin was vacuous (feedback_every_new_pin_must_be_
shown_to_fail) - deleted rather than kept as an always-true no-op.

M5: split all six AlphaFlushTranscript_* KnownFailure Facts into live
AlphaFlushSites_* (site+threshold sequence only, content-independent,
unconditional pass) and AlphaFlushCounts_* (KnownFailure, full tuple incl.
drained counts) sharing one RunAlphaFlushTranscriptReplay helper. Rewrote
the KnownFailure reason honestly: THREE independent causes, not "harness
content only" - (1) zero live mesh content, (2) acdream appends one entry
per INSTANCE vs retail's one per SUBSET per DrawMesh call, (3) acdream's
CLIP list is structurally empty for ordinary content (M2). Added ONE live
valve pin (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAtThe
ExactBoundary, a Theory over 2250/2249) that goes through WalkFrameDriver.
Replay's own SortCellExit arm, not RetailAlphaQueue.Flush called directly.

M2/A2/M8c (register): ~~AP-34~~ restored as struck-through RETIRED (it had
been deleted outright, not struck, breaking the register's own convention)
citing residuals AP-238/AP-239/AP-240. The chunk-2 commit's "AP-236"/
"AP-237" collided with an id ALREADY filed-and-retired on main by an
unrelated #132 fix (docs/ISSUES.md:19976) - ids are never reused, so both
are renumbered: AP-236->AP-238 (EnvCell; rewritten per M6 - granularity-only
residual now, "never through the shared queue" is retired), AP-237->AP-239
(mask-reconstruction gap; the dangling "AP-236b" citation in
RetailAlphaMeshRouter.cs never matched either filed id and is corrected to
AP-239). New AP-240 filed for M2: WbDrawDispatcher.IsOpaque filters
ClipMap out upstream, so ordinary clip-mapped content never reaches
retail's CLIP FIFO (draws immediately on the pre-existing
OpaqueAlphaToCoverage pipeline instead) - no pipeline change this round,
per the contract. AP section header recounted honestly: 159 active (was
stale at 161); AD section 92 (was 91, +AD-120).

M7: AP-239's only cited example (cloud GfxObj 0x01004C35, surface
0x08000023) is DISPROVEN - decoded Type=0x10114 has alpha-family bits
that win in BOTH retail's real priority and acdream's reconstruction, so it
never actually diverges. A 2026-09-04 scan of all 6,152 Portal-dat Surfaces
found 27 real Translucent+Base1ClipMap-no-alpha-family instances
(Type=0x14); cross-referencing GfxObjs found surface 0x08000015 on GfxObj
0x010001EC - cited in place of the disproven cloud example.

M8: (a) RenderDeviceD3D::DrawBlock is @0x005a17c0 per symbols.json, not
@0x005a18d0 (that is its per-land-cell loop head) - fixed across
RetailAlphaQueue.cs, RetailPViewPassExecutor.cs, WalkEvents.cs,
WalkFrameDriver.cs (x3), RetailFrameWalk.cs. (b) RetailAlphaQueue.cs's four
FlushAlphaList addresses were labelled "return site" but are the CALL
instruction's own address (x86 CALL rel32 is 5 bytes) - relabelled "call at
X (returns to X+5)". (d) worldbuilder-inventory.md:420's AP-34 citation
updated.

A1: RetailPViewRenderer.cs's outdoor-root passes.FlushLandscapeAlpha() call
(labelled LandscapeFlush) is deleted - retail's real LandscapeFlush
(PView::DrawCells @0x005a4840's own FlushAlphaList(0f) @0x005a4872) sits
strictly inside if (outside_view.view_count > 0) on the INTERIOR PView; an
outdoor root never reaches it (terrace-edge capture: zero 005a4877 FL
lines). Verified no opaque draw intervenes between this deletion point and
RetailAlphaQueue.EndFrame()'s own RenderNormalMode pass-end flush
(WorldSceneRenderer.cs: DrawPostWorldParticles's PView branch is an
explicit no-op; only diagnostic wireframes follow) - #132's "drain after
the finished opaque world" invariant is kept by that flush alone.

A4: RetailAlphaQueue's ApplyScratchRetention call sites passed the ENTRY
count as the SOURCE-count argument too (observedClip + observedAlpha
twice), so _sources/_sourceDrawOffsets never shrank toward the real source
count. Fixed to capture _sources.Count before Clear().

A5/A6/A7: WalkStaticStreamPopulatorTests.cs's stale "same viewer distance"
banner corrected; two off-by-one entry counts in the packet's section 10
sequences corrected (foundry-deep x10->x11, holtburg-doorway-still x5->x6);
WalkAlphaDepthTrace.cs's "new=" comment explains why IsNew is retained but
never compared (M4's finding makes a per-subset comparison meaningless at
acdream's per-instance granularity); RetailAlphaMeshRouterTests.cs's
IndependentReferenceRoute (never actually independently-shaped) renamed to
RestatedBranchTableRoute with an honest doc comment, and the unverified
"flipped in turn and failed every time" claim deleted.

A3 was resolved as a side effect of M1's SubmitToAlphaQueue doc-comment
rewrite (the wrong "0x02/0x03, 0x04/0x05, 0x08/0x09" enumeration no longer
exists); RetailAlphaMeshRouter.cs's own MaskFromTranslucencyKind comment
updated in the same spirit (M1's particle-site Opaque-reachability finding).

Mutation checks (each applied to a scratch-restored copy, confirmed
failing, then byte-exact reverted - diffed against a /tmp backup after
restore):

- A1 (RetailPViewRendererTests.DrawInside_NeverCallsFlushLandscapeAlphaDirectly):
  restoring the deleted "if (ctx.RootCell.IsOutdoorNode)
  passes.FlushLandscapeAlpha();" line reintroduces a direct
  RetailPViewPassExecutor.FlushLandscapeAlpha call in DrawInside's compiled
  body. Actual failure: "Assert.DoesNotContain() Failure: Filter matched in
  collection ... CompiledCall { ... Target = Void FlushLandscapeAlpha() }".

- A4 (RetailAlphaQueueTests.RetainedSourceCapacity_ConvergesToTheReal
  SourceCountNotTheEntryCount): reverting both ApplyScratchRetention calls
  to (observedClip + observedAlpha, observedClip + observedAlpha) - with
  100 distinct sources in the spike frame (so _sources.Capacity grows to
  128, past its initial 4) followed by three low-demand frames of 10
  entries via ONE repeated source - makes sourceTarget compute from 10
  (Math.Max(4,10*2)=20) instead of 1 (Math.Max(4,1*2)=4). Actual failure:
  "Expected the retained source-array capacity to converge toward the real
  source count (1), but it stayed at 20". (A single-source-only spike does
  NOT discriminate this bug - Math.Min(sourceTarget, _sources.Capacity)
  clamps both formulas to the same unchanged initial 4 - hence the
  100-source setup.)

- M5 valve (WalkFrameDriverTests.SortCellExit_ValveDrainsThroughReplayAt
  TheExactBoundary, 2250 case): flipping RetailAlphaQueue.Flush's no-op
  comparison from "<" to "<=" (S4-c1/c2's own boundary mutation) makes the
  2250-entries case a no-op instead of draining. Actual failure:
  "Assert.Equal() Failure: Values differ Expected: 0 Actual: 2250".

- M6 (EnvCellAlphaDrawSourceTests, both Facts): adding an immediate
  renderTransparentOrdered(new[]{cellId}) call inside
  EnvCellAlphaDrawSource.AddPendingCellId (simulating the M1/M3 "drew
  immediately instead of deferring" bug class) makes
  PendingCellToken_IsDrainedAtTheFlushAndNeverBeforeIt fail with
  "Assert.Empty() Failure: Collection was not empty Collection: [[4660]]"
  and ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombined
  Drain fail with 'Expected: string[] ["cell:256", "particle:7",
  "cell:512"] Actual: List<string> ["cell:256", "cell:512", "cell:256",
  "particle:7", "cell:512"]'.

- M1 (ParticleRendererRouteTests.OpaqueClassifiedMeshBatch_WithMaterialAlpha
  _RoutesToAlphaAppend): hardcoding materialHasAlpha to false inside
  RouteParticleSubmission makes the Opaque-mask+has-alpha case route
  Immediate instead of Append. Actual failure: "Assert.Equal() Failure:
  Values differ Expected: Append Actual: Immediate".

Gates (real output lines):

- dotnet build AcDream.slnx -c Release -> "Build succeeded. 0 Warning(s)
  0 Error(s)".
- Hermetic lane (--filter "Lane!=InstalledDat&Lane!=PreparedPackage&
  Lane!=Live&Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&
  Lane!=SystemFont&Purpose!=Diagnostic&Status!=KnownFailure") ->
  "Passed! - Failed: 0, Passed: 6864, Skipped: 0, Total: 6864".
- InstalledDat lane (ACDREAM_RUN_INSTALLED_DAT_TESTS=1,
  ACDREAM_DAT_DIR="C:/Users/erikn/Documents/Asheron's Call",
  --filter "Lane=InstalledDat") -> "Failed: 10, Passed: 255, Skipped: 1,
  Total: 266" - the exact 4 pre-existing failures
  (TowerAscent_StaircaseStaysConeVisible_EveryStep,
  LayoutImporterMediaBearingChildSweepTests, LayoutImporterInvisibleSweep
  Tests, Oh_doorway_still_first_frame_diff) plus the six
  AlphaFlushCounts_* KnownFailure Facts, nothing else; all six
  AlphaFlushSites_* Facts pass inside this same lane (the whole
  WalkTraceConformanceTests partial class carries Lane=InstalledDat, so
  they cannot run hermetically).
- Shader classes (VulkanShaderDescriptorContractTests,
  VulkanShaderManifestTests, RenderPackSpirvValidatorTests) -> "Passed! -
  Failed: 0, Passed: 32, Skipped: 0, Total: 32".
- RetailAlphaQueueTests, RetailAlphaMeshRouterTests, RetailFrameWalkTests,
  WalkFrameDriverTests, ParticleRenderer*Tests, EnvCellRenderer*Tests
  (combined filter) -> "Passed! - Failed: 0, Passed: 120, Skipped: 0,
  Total: 120".
- Register collision grep: AP-238/AP-239/AP-240/AD-120 each count exactly
  1; ~~AP-34~~ count exactly 1; live totals AP=159, AD=92.

Not done / deferred: none - all M1-M8/A1-A8 items landed; A8 was a
no-action note.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 11:53:03 +02:00

647 lines
34 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, split by S4-c2 fix round 1 (M5): 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. Returns BOTH the expected
/// and actual transcripts so <see cref="RunAlphaFlushSitesGate"/> and
/// <see cref="RunAlphaFlushCountsGate"/> can assert their own dimension
/// over the SAME replay logic without duplicating the harness setup.
/// </summary>
private static (
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> Expected,
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> Actual)
RunAlphaFlushTranscriptReplay(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));
return (expected, leaf.ActualFlushes);
}
/// <summary>
/// S4-c2 fix round 1 (M5): the LIVE half of gate G-c2 — 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 a real, unconditional pass/fail with
/// no KnownFailure tag — a future site-sequence regression is now
/// visible in the hermetic/InstalledDat lanes instead of being buried
/// under the (expected-red) count dimension.
/// </summary>
private void RunAlphaFlushSitesGate(string fixtureName)
{
(
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> expected,
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> actual
) = RunAlphaFlushTranscriptReplay(fixtureName);
bool siteSequenceMatches = expected
.Select(e => (e.Site, e.Threshold))
.SequenceEqual(actual.Select(a => (a.Site, a.Threshold)));
Assert.True(
siteSequenceMatches,
$"{fixtureName}: flush SITE sequence diverges (content-independent — this must "
+ $"match unconditionally) — expected {FormatFlushSites(expected)}, "
+ $"actual {FormatFlushSites(actual)}");
}
/// <summary>
/// S4-c2 fix round 1 (M5): the KnownFailure half of gate G-c2 — the
/// drained entry count PER LIST can never match by construction, for
/// THREE independent reasons (not just "harness content only", the
/// prior write-up's incomplete claim): (1) this harness carries no live
/// GfxObj/particle mesh content at all (<see cref="EmptyAlphaDepthWorldData"/>),
/// so every 0f-threshold flush observes count (0, 0) — always a real
/// drain (0 is never <c>&lt; 0</c>), matching retail's own always-drains
/// behavior at threshold 0f, but with a different count than the
/// capture's real content volume; (2) even WITH content, acdream appends
/// one queue entry per INSTANCE, where retail's <c>AddMeshToAlphaList</c>
/// appends one entry per SUBSET per <c>DrawMesh</c> call — a single
/// multi-subset instance inflates retail's count relative to acdream's;
/// (3) acdream's CLIP list is structurally empty for ordinary content
/// (M2's new register row) — <c>WbDrawDispatcher.IsOpaque</c> filters
/// clip-mapped subsets out before they ever reach a submit call, so
/// acdream's CLIP count reads 0 wherever retail's capture shows nonzero
/// CLIP entries. See the S4 packet's §9/§10/§11 subsections for the
/// full per-pose sequences.
/// </summary>
private void RunAlphaFlushCountsGate(string fixtureName)
{
(
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> expected,
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> actual
) = RunAlphaFlushTranscriptReplay(fixtureName);
// Recorded 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 (the site+threshold "
+ "sequence is separately verified exact by AlphaFlushSites_*) — see this method's "
+ $"own doc comment for the three reasons this can never match — 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 fix round 1 (M5): the live half — see
/// <see cref="RunAlphaFlushSitesGate"/>'s own doc comment. No
/// KnownFailure tag: this dimension is content-independent and must
/// match unconditionally.</summary>
[Fact]
public void AlphaFlushSites_CathedralArrival_MatchesRetailFrame2()
=> RunAlphaFlushSitesGate("cathedral-arrival.alphadepth");
/// <summary>S4-c2 fix round 1 (M5): the KnownFailure half — see
/// <see cref="RunAlphaFlushCountsGate"/>'s own doc comment for the three
/// independent reasons the drained COUNT can never match by
/// construction.</summary>
[Fact]
[Trait("Status", "KnownFailure")]
public void AlphaFlushCounts_CathedralArrival_MatchesRetailFrame2()
=> RunAlphaFlushCountsGate("cathedral-arrival.alphadepth");
[Fact]
public void AlphaFlushSites_CathedralLeak_MatchesRetailFrame2()
=> RunAlphaFlushSitesGate("cathedral-leak.alphadepth");
[Fact]
[Trait("Status", "KnownFailure")]
public void AlphaFlushCounts_CathedralLeak_MatchesRetailFrame2()
=> RunAlphaFlushCountsGate("cathedral-leak.alphadepth");
[Fact]
public void AlphaFlushSites_CathedralStairArch_MatchesRetailFrame2()
=> RunAlphaFlushSitesGate("cathedral-stair-arch.alphadepth");
[Fact]
[Trait("Status", "KnownFailure")]
public void AlphaFlushCounts_CathedralStairArch_MatchesRetailFrame2()
=> RunAlphaFlushCountsGate("cathedral-stair-arch.alphadepth");
[Fact]
public void AlphaFlushSites_FoundryDeep_MatchesRetailFrame2()
=> RunAlphaFlushSitesGate("foundry-deep.alphadepth");
[Fact]
[Trait("Status", "KnownFailure")]
public void AlphaFlushCounts_FoundryDeep_MatchesRetailFrame2()
=> RunAlphaFlushCountsGate("foundry-deep.alphadepth");
[Fact]
public void AlphaFlushSites_HoltburgDoorwayStill_MatchesRetailFrame2()
=> RunAlphaFlushSitesGate("holtburg-doorway-still.alphadepth");
[Fact]
[Trait("Status", "KnownFailure")]
public void AlphaFlushCounts_HoltburgDoorwayStill_MatchesRetailFrame2()
=> RunAlphaFlushCountsGate("holtburg-doorway-still.alphadepth");
[Fact]
public void AlphaFlushSites_TerraceEdge_MatchesRetailFrame2()
=> RunAlphaFlushSitesGate("terrace-edge.alphadepth");
[Fact]
[Trait("Status", "KnownFailure")]
public void AlphaFlushCounts_TerraceEdge_MatchesRetailFrame2()
=> RunAlphaFlushCountsGate("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");
}