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>
This commit is contained in:
Erik 2026-09-04 07:30:25 +02:00
parent 89f1e2676f
commit 9ccb61a8ec
26 changed files with 1180 additions and 326 deletions

View file

@ -0,0 +1,92 @@
using AcDream.App.Rendering;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// S4-c2 fix round 1 (M6, contract C4): <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>
/// is the queue-facing half of an EnvCell transparent shell's detail-off
/// submission — one token per CELL, replayed through a caller-supplied
/// delegate (production wiring:
/// <see cref="AcDream.App.Rendering.Wb.EnvCellRenderer.RenderTransparentOrdered"/>)
/// at whatever flush site the shared <see cref="RetailAlphaQueue"/> drains
/// at. These tests exercise the REAL production class end-to-end against a
/// real queue — only the render destination is faked (a delegate recording
/// what it was asked to draw), so the GPU-backed <c>EnvCellRenderer</c>
/// never needs to be constructed to pin the queue mechanics.
/// </summary>
public sealed class EnvCellAlphaDrawSourceTests
{
/// <summary>Mutation check: if <c>SubmitOrDrawTransparentCellShell</c>
/// drew the cell immediately instead of appending it when detail is off
/// (the M1/M3 bug class this chunk fixes elsewhere), the drawn list
/// would already contain an entry BEFORE <c>Flush</c> runs — the
/// <c>Assert.Empty(drawn)</c> line fails against that mutation.</summary>
[Fact]
public void PendingCellToken_IsDrainedAtTheFlushAndNeverBeforeIt()
{
var drawn = new List<IReadOnlyList<uint>>();
var source = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
cells => drawn.Add(cells.ToArray()));
var queue = new RetailAlphaQueue();
queue.BeginFrame();
int token = source.AddPendingCellId(0x1234u);
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, source, token, false));
Assert.Empty(drawn);
queue.Flush(RetailAlphaFlushSite.DrawBuilding, 0f);
IReadOnlyList<uint> singleDraw = Assert.Single(drawn);
Assert.Equal(new uint[] { 0x1234u }, singleDraw);
queue.EndFrame();
}
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
/// with an unrelated source's entry appended between them, must still
/// produce TWO separate single-cell draw calls around the interposed
/// entry — <see cref="IRetailAlphaDrawSource"/>'s "only adjacent
/// same-source entries batch" invariant (the queue never groups across
/// another entry, which is what keeps compositing order exact). Mutation
/// check: an implementation that draws every prepared cell id in one
/// call regardless of the requested <c>(first, count)</c> range would
/// print both cell ids together on EACH of the two
/// <c>DrawPreparedAlphaBatch</c> invocations instead of once each — the
/// exact sequence assertion below fails against that mutation.</summary>
[Fact]
public void ParticleAppendedBetweenTwoCellTokens_KeepsItsPositionInTheCombinedDrain()
{
var log = new List<string>();
var cellSource = new RetailPViewPassExecutor.EnvCellAlphaDrawSource(
cells => log.Add($"cell:{string.Join(',', cells)}"));
var particleSource = new RecordingSource("particle", log);
var queue = new RetailAlphaQueue();
queue.BeginFrame();
int cell1Token = cellSource.AddPendingCellId(0x100u);
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, cellSource, cell1Token, false));
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, particleSource, 7, false));
int cell2Token = cellSource.AddPendingCellId(0x200u);
Assert.True(queue.TryAppend(RetailAlphaList.Alpha, cellSource, cell2Token, false));
queue.EndFrame();
Assert.Equal(new[] { "cell:256", "particle:7", "cell:512" }, log);
}
private sealed class RecordingSource(string name, List<string> log) : IRetailAlphaDrawSource
{
private int[] _prepared = [];
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens) => _prepared = tokens.ToArray();
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
{
for (int i = 0; i < drawCount; i++)
log.Add($"{name}:{_prepared[firstPreparedDraw + i]}");
}
public void ResetAlphaSubmissions()
{
}
}
}

View file

@ -0,0 +1,76 @@
using AcDream.App.Rendering;
using AcDream.Core.Meshing;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// S4-c2 fix round 1 (M1, blocking): <see cref="ParticleRenderer.RouteParticleSubmission"/>
/// is the pure router-input derivation the blocking fix extracted from
/// <c>DeferToRetailAlphaQueue</c> so it is directly testable without a GPU/
/// mesh-manager harness. Before this round, a mesh-particle batch classified
/// <see cref="TranslucencyKind.Opaque"/> (mask 0x00) reaching <c>DrawMesh</c>
/// row 5 (Immediate) hit an unconditional throw in the render loop — these
/// tests pin the two outcomes the fix made reachable.
/// </summary>
public sealed class ParticleRendererRouteTests
{
/// <summary>M1(d) pin 1: an Opaque-classified mesh-particle batch (mask
/// 0x00) with a non-fully-opaque current alpha (top ColorArgb byte !=
/// 0xFF, i.e. materialHasAlpha true) routes to ALPHA (Append) — retail's
/// row 4 material-alpha fallback — not Immediate. Mutation check:
/// reverting to the pre-fix code (which threw whenever the decision
/// wasn't Append) would not distinguish this from pin 2 below at all
/// (both would just throw); reverting ONLY the materialHasAlpha
/// derivation to a hardcoded `false` makes this assertion fail because
/// the decision becomes Immediate instead of Append.</summary>
[Fact]
public void OpaqueClassifiedMeshBatch_WithMaterialAlpha_RoutesToAlphaAppend()
{
// Top byte (alpha) = 0x80 -> not 0xFF -> materialHasAlpha = true.
const uint colorArgbWithAlpha = 0x80FFFFFFu;
RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission(
ParticleSubmissionKind.Mesh, TranslucencyKind.Opaque, colorArgbWithAlpha);
Assert.Equal(RetailAlphaMeshAction.Append, decision.Action);
Assert.Equal(RetailAlphaList.Alpha, decision.List);
}
/// <summary>M1(d) pin 2: the SAME Opaque-classified mesh-particle batch
/// with a fully-opaque current alpha (top ColorArgb byte == 0xFF, i.e.
/// materialHasAlpha false) draws immediately and never enters a list —
/// retail's row 5 fallthrough. Mutation check: this is the EXACT case
/// that threw before the fix (mask 0x00, materialHasAlpha false ->
/// row 5 Immediate -> the old "rows 1/2/4/5 unreachable" guard fired);
/// reverting the throw's removal reproduces
/// <c>InvalidOperationException</c> here, which this test would report
/// as a failure (an unhandled exception) rather than an assertion
/// mismatch.</summary>
[Fact]
public void OpaqueClassifiedMeshBatch_WithNoMaterialAlpha_RoutesImmediate()
{
const uint fullyOpaqueColorArgb = 0xFFFFFFFFu;
RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission(
ParticleSubmissionKind.Mesh, TranslucencyKind.Opaque, fullyOpaqueColorArgb);
Assert.Equal(RetailAlphaMeshAction.Immediate, decision.Action);
}
/// <summary>A billboard submission always carries the alpha-family mask
/// (0x02) regardless of the (unused) mesh parameters, and therefore
/// always satisfies row 3 (Append, ALPHA) under the fixed default delay
/// mask — never Immediate, never CLIP. Mutation check: swapping
/// <see cref="RetailAlphaMeshRouter.MaskAlphaFamily"/> for
/// <see cref="RetailAlphaMeshRouter.MaskClipMap"/> in the billboard
/// branch would route this to the CLIP list instead of ALPHA.</summary>
[Fact]
public void Billboard_AlwaysRoutesToAlphaAppend()
{
RetailAlphaMeshDecision decision = ParticleRenderer.RouteParticleSubmission(
ParticleSubmissionKind.Billboard, default, default);
Assert.Equal(RetailAlphaMeshAction.Append, decision.Action);
Assert.Equal(RetailAlphaList.Alpha, decision.List);
}
}

View file

@ -152,20 +152,25 @@ public sealed class RetailAlphaMeshRouterTests
}
/// <summary>
/// Full brute-force sweep over every (mask 0x00-0x09) x sky x detail x
/// multipass x hasAlpha cell at the fixed default delay mask (0x0E),
/// checked against an INDEPENDENTLY-shaped reference derivation (a
/// switch-driven truth table, not a copy of <see cref="RetailAlphaMeshRouter.Route"/>'s
/// own if-chain) so a control-flow slip in one is unlikely to survive in
/// the other with the same wrong answer. Mutation check: flipping any
/// single row's condition in <c>Route</c> (e.g. row 3's <c>!= 0</c> to
/// <c>== 0</c>) changes that row's outcome for roughly half the 160
/// cells, which this sweep catches immediately (verified by hand during
/// S4-c2 development: each of the five rows' conditions was flipped in
/// turn and this test failed every time).
/// S4-c2 fix round 1 (A7): this was documented as an "INDEPENDENTLY-
/// shaped reference derivation (a switch-driven truth table, not a copy
/// of <see cref="RetailAlphaMeshRouter.Route"/>'s own if-chain)" — false;
/// <see cref="RestatedBranchTableRoute"/> below is the SAME five-row
/// if-chain shape with different local variable names, not a switch or
/// lookup table. It is renamed and redocumented honestly rather than
/// rewritten into a literal 160-row table (spec §4's five rows over ten
/// masks × sky × detail × multipass × hasAlpha) — the per-row
/// <see cref="RowBoundaryCases"/> Theory above already carries one
/// mutation text per row boundary; this brute-force sweep's real value
/// is catching a copy-paste/off-by-one divergence BETWEEN the two
/// restatements (a typo in one that the other doesn't share), not an
/// independent verification of the spec itself. The claim that "each of
/// the five rows' conditions was flipped in turn and this test failed
/// every time" is deleted — that hand-verification was never re-run and
/// is not reproduced in this round's evidence.
/// </summary>
[Fact]
public void Route_MatchesIndependentTruthTableAcrossEveryCell()
public void Route_MatchesRestatedBranchTableAcrossEveryCell()
{
byte[] masks = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09];
bool[] bothBools = [false, true];
@ -180,7 +185,7 @@ public sealed class RetailAlphaMeshRouterTests
{
RetailAlphaMeshDecision actual = RetailAlphaMeshRouter.Route(
sky, RetailAlphaMeshRouter.DefaultDelayMask, detail, multipass, mask, hasAlpha);
RetailAlphaMeshDecision expected = IndependentReferenceRoute(
RetailAlphaMeshDecision expected = RestatedBranchTableRoute(
sky, RetailAlphaMeshRouter.DefaultDelayMask, detail, multipass, mask, hasAlpha);
bool matches = actual.Action == expected.Action
@ -198,9 +203,10 @@ public sealed class RetailAlphaMeshRouterTests
Assert.True(mismatches == 0, $"{mismatches} mismatches; first: {firstMismatch}");
}
/// <summary>Independent re-derivation of spec §4's table using a
/// switch/lookup shape rather than the production if-chain.</summary>
private static RetailAlphaMeshDecision IndependentReferenceRoute(
/// <summary>Re-derivation of spec §4's table in independently-named
/// local variables (NOT a switch/lookup shape — see this Fact's own doc
/// comment above, A7).</summary>
private static RetailAlphaMeshDecision RestatedBranchTableRoute(
bool sky, byte delayMask, bool detail, bool multipass, byte mask, bool hasAlpha)
{
bool row1 = sky || delayMask == 0 || detail;

View file

@ -311,42 +311,6 @@ public sealed class RetailAlphaQueueTests
queue.AbortFrame();
}
/// <summary>The first entry appended to a list after it was last drained
/// is flagged <c>IsFirstForList</c>; later entries in the same
/// uninterrupted run are not. Mutation check: always setting the flag
/// true (or always false) fails this exact sequence assertion.</summary>
[Fact]
public void TryAppend_FlagsOnlyTheFirstEntrySinceTheLastDrain()
{
var log = new List<string>();
var source = new RecordingSource("alpha", log);
var queue = new RetailAlphaQueue();
FieldInfo alphaField = typeof(RetailAlphaQueue).GetField(
"_alpha", BindingFlags.NonPublic | BindingFlags.Instance)!;
queue.BeginFrame();
queue.TryAppend(RetailAlphaList.Alpha, source, 1, false);
queue.TryAppend(RetailAlphaList.Alpha, source, 2, false);
// Inspect BEFORE flushing: two entries in the SAME list snapshot,
// discriminating true (first) from false (second) — checking only
// the post-flush single-survivor list (as an earlier draft of this
// test did) is vacuous, since a one-element list is trivially
// "first" whether or not the flag logic is correct.
var beforeFlush = (List<RetailAlphaEntry>)alphaField.GetValue(queue)!;
Assert.Equal(2, beforeFlush.Count);
Assert.True(beforeFlush[0].IsFirstForList);
Assert.False(beforeFlush[1].IsFirstForList);
queue.Flush(RetailAlphaFlushSite.RenderNormalMode, 0f);
queue.TryAppend(RetailAlphaList.Alpha, source, 3, false);
var afterDrain = (List<RetailAlphaEntry>)alphaField.GetValue(queue)!;
Assert.Single(afterDrain);
Assert.True(afterDrain[0].IsFirstForList);
queue.AbortFrame();
}
[Fact]
public void RetainedScratchConvergesAfterAOneScopeSpike()
{
@ -371,6 +335,74 @@ public sealed class RetailAlphaQueueTests
Assert.Equal(0, queue.PendingCount);
}
/// <summary>
/// S4-c2 fix round 1 (A4): <c>DrainAndReset</c>/<c>AbortFrame</c> used to
/// pass the ENTRY count as <c>ApplyScratchRetention</c>'s SOURCE-count
/// argument too (<c>ApplyScratchRetention(observedClip + observedAlpha,
/// observedClip + observedAlpha)</c>). A single spike frame first
/// registers 100 DISTINCT sources (one entry each) so <c>_sources</c>'
/// own List-growth capacity climbs well past its initial 4 — otherwise
/// <c>Math.Min(sourceTarget, _sources.Capacity)</c> clamps ANY
/// <c>sourceTarget</c> down to that unchanged initial 4 and the two
/// formulas become indistinguishable, which is why a single-source
/// spike does not discriminate this bug. Three low-demand frames then
/// each resubmit 10 entries through the SAME ONE source (matching the
/// three consecutive observations <see cref="Residency.RetainedScratchCapacityPolicy"/>
/// requires before it recommends shrinking) — at the moment the shrink
/// fires, the ENTRY count (10) and the real SOURCE count (1) genuinely
/// diverge. Mutation check: reverting to
/// <c>ApplyScratchRetention(observedClip + observedAlpha, observedClip +
/// observedAlpha)</c> makes <c>sourceTarget</c> compute from 10
/// (<c>Math.Max(4, 10*2)=20</c>, clamped by the now-≥20 <c>_sources.Capacity</c>
/// to 20) instead of from 1 (<c>Math.Max(4, 1*2)=4</c>) — the actual
/// observed capacity under that mutation is 20, and the upper-bound
/// assertion below fails against it.
/// </summary>
[Fact]
public void RetainedSourceCapacity_ConvergesToTheRealSourceCountNotTheEntryCount()
{
const int budgetBytes = 128 * 1024;
var manySources = new CountingSource[100];
for (int i = 0; i < manySources.Length; i++)
manySources[i] = new CountingSource();
var queue = new RetailAlphaQueue(budgetBytes);
FieldInfo sourcesField = typeof(RetailAlphaQueue).GetField(
"_sources", BindingFlags.NonPublic | BindingFlags.Instance)!;
// 8,192 entries (enough to also push the ENTRY capacity itself past
// its low-demand budget, matching RetainedScratchConvergesAfterAOneScopeSpike's
// own spike size) spread across the 100 distinct sources, so BOTH
// _sources' own capacity AND the entry-side capacity are in their
// post-spike high-water state together.
queue.BeginFrame();
for (int i = 0; i < 8_192; i++)
queue.TryAppend(RetailAlphaList.Alpha, manySources[i % manySources.Length], i, false);
queue.EndFrame();
var sourcesAfterSpike = (List<IRetailAlphaDrawSource>)sourcesField.GetValue(queue)!;
Assert.True(
sourcesAfterSpike.Capacity > 8,
"Test setup check: the 100-distinct-source spike must grow _sources' own capacity "
+ $"past its initial 4 (observed {sourcesAfterSpike.Capacity}) — otherwise the "
+ "Math.Min clamp below hides the bug regardless of which formula runs.");
CountingSource repeatedSource = manySources[0];
for (int i = 0; i < 3; i++)
{
queue.BeginFrame();
for (int j = 0; j < 10; j++)
queue.TryAppend(RetailAlphaList.Alpha, repeatedSource, j, false);
queue.EndFrame();
}
var sources = (List<IRetailAlphaDrawSource>)sourcesField.GetValue(queue)!;
Assert.True(
sources.Capacity <= 8,
"Expected the retained source-array capacity to converge toward the real source "
+ $"count (1), but it stayed at {sources.Capacity} — the entry count (10), not the "
+ "source count (1), must have driven ApplyScratchRetention's second argument.");
}
[Fact]
public void AbortFrame_DiscardsPayloadAndAllowsTheNextFrameToRender()
{

View file

@ -8,6 +8,7 @@ using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Walk;
using AcDream.App.Rendering.Wb;
using AcDream.App.Tests.Architecture;
using AcDream.App.Tests.Rendering.Gpu;
@ -158,6 +159,44 @@ public sealed class RetailPViewPassExecutorTests
call => call.Target.DeclaringType == typeof(WalkTranscriptDump));
}
/// <summary>
/// S4-c2 fix round 1 (M6, contract C4): a cell's transparent shell's
/// immediate-vs-append decision must come from the SAME
/// <see cref="RetailAlphaMeshRouter"/> table every other alpha submitter
/// uses — never a hand-coded <c>if (detailEnabled)</c> branch that
/// bypasses it. Pins the STRUCTURE: <see cref="EnvCellRenderer.TransparentDetailEnabled"/>
/// is read to feed <see cref="RetailAlphaMeshRouter.Route"/>'s own
/// <c>detailSurfaceActive</c> argument BEFORE the route call, and the
/// route call precedes BOTH possible outcomes
/// (<see cref="EnvCellRenderer.RenderTransparentOrdered"/> for
/// Immediate, <see cref="RetailAlphaQueue.TryAppend"/> for Append).
/// Mutation check: deleting the <c>Route</c> call and branching directly
/// on <c>TransparentDetailEnabled</c> instead removes the
/// <see cref="RetailAlphaMeshRouter"/> call entirely — <c>RequiredCallIndex</c>
/// fails its <c>index &gt;= 0</c> assertion for the route call.
/// </summary>
[Fact]
public void SubmitOrDrawTransparentCellShell_RoutesThroughTheSharedTableBeforeEitherOutcome()
{
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
"SubmitOrDrawTransparentCellShell",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
int detailProbe = RequiredCallIndex(
calls, typeof(EnvCellRenderer), "get_TransparentDetailEnabled");
int route = RequiredCallIndex(
calls, typeof(RetailAlphaMeshRouter), nameof(RetailAlphaMeshRouter.Route));
int renderImmediate = RequiredCallIndex(
calls, typeof(EnvCellRenderer), nameof(EnvCellRenderer.RenderTransparentOrdered));
int append = RequiredCallIndex(
calls, typeof(RetailAlphaQueue), nameof(RetailAlphaQueue.TryAppend));
Assert.True(detailProbe < route);
Assert.True(route < renderImmediate);
Assert.True(route < append);
}
/// <summary>
/// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
/// (the walk's own screen-space terrain-clip writer + its per-frame

View file

@ -0,0 +1,56 @@
using System.Reflection;
using AcDream.App.Rendering;
using AcDream.App.Tests.Architecture;
namespace AcDream.App.Tests.Rendering;
public sealed class RetailPViewRendererTests
{
/// <summary>
/// S4-c2 fix round 1 addendum (A1): retail's LandscapeFlush
/// (<c>PView::DrawCells</c> @0x005a4840's own <c>FlushAlphaList(0f)</c>
/// @0x005a4872) only ever fires on an INTERIOR PView, strictly inside
/// <c>if (outside_view.view_count &gt; 0)</c> — an outdoor root never
/// reaches it (terrace-edge's capture: zero <c>005a4877</c> FL lines).
/// In production the interior case reaches
/// <see cref="RetailPViewPassExecutor.FlushLandscapeAlpha"/> through the
/// walk driver's LandscapeFlush leaf (<c>FlushWalkLandscape</c>, a
/// SEPARATE method the driver calls during <c>Replay</c>) — never as a
/// direct call compiled into <see cref="RetailPViewRenderer.DrawInside"/>'s
/// own body. The outdoor-root direct call that used to sit inline here
/// (mislabelled LandscapeFlush) is deleted; retail's real outdoor-root
/// drains are the <c>DrawBlock</c> 0.75 valve (already wired via
/// <c>WalkFrameEventKind.SortCellExit</c>) and
/// <c>SmartBox::RenderNormalMode</c>'s pass-end flush
/// (<see cref="RetailAlphaQueue.EndFrame"/>, called from
/// <c>WorldSceneRenderer.CompleteWorldFrame</c>).
///
/// <para>Mutation check: reverting the deletion (restoring
/// <c>if (ctx.RootCell.IsOutdoorNode) passes.FlushLandscapeAlpha();</c>)
/// reintroduces a direct call to
/// <see cref="RetailPViewPassExecutor.FlushLandscapeAlpha"/> inside
/// <see cref="RetailPViewRenderer.DrawInside"/>'s compiled body — the
/// <c>Assert.DoesNotContain</c> below fails against that mutation.</para>
/// </summary>
[Fact]
public void DrawInside_NeverCallsFlushLandscapeAlphaDirectly()
{
MethodInfo drawInside = typeof(RetailPViewRenderer).GetMethod(
"DrawInside",
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(drawInside);
Assert.DoesNotContain(
calls,
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
&& call.Target.Name == nameof(RetailPViewPassExecutor.FlushLandscapeAlpha));
// The interior-cell unattached-emitter draw beside the deleted call
// must remain — this pin only removes the mislabelled flush, not
// the particle submission next to it.
Assert.Contains(
calls,
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
&& call.Target.Name == nameof(RetailPViewPassExecutor.DrawUnattachedSceneParticles));
}
}

View file

@ -87,9 +87,17 @@ public static class WalkAlphaDepthTrace
// own printf. listSel nonzero selects CLIP
// (alphaedMeshCountClip/List); zero selects ALPHA
// (alphaedMeshCountAlpha/List) — Ghidra-verified 2026-09-04, see
// RetailAlphaList's own doc comment. "new" is the per-entry
// RetailAlphaList's own doc comment. "new" is retail's per-entry
// first-for-list flag (param_4); "clip" (param_5, unused here)
// is overrideClipmap.
// is overrideClipmap. S4-c2 fix round 1 (M4/A6): IsNew is parsed
// and RETAINED purely as a captured/parsed FACT (fidelity to the
// capture's own format) but is never compared against acdream's
// own routing — M4 established that flag is trivially true for
// EVERY subset retail appends (one DrawMesh invocation owns one
// subset per list), so a per-subset comparison would be
// meaningless at acdream's coarser per-INSTANCE granularity (the
// same content-volume mismatch M5's KnownFailure count gate
// documents) — no dead-parse-as-evidence claim is made here.
Match am = AmPattern.Match(line);
if (am.Success)
{

View file

@ -1736,6 +1736,75 @@ public sealed partial class WalkFrameDriverTests
Assert.Single(leaf.LandCellBatches));
}
/// <summary>
/// S4-c2 fix round 1 (M5): the ONE live pin exercising the valve THIS
/// CHUNK added, through <see cref="WalkFrameDriver.Replay"/>'s own
/// <c>WalkFrameEventKind.SortCellExit</c> arm (not
/// <see cref="RetailAlphaQueue.Flush"/> called directly —
/// <c>RetailAlphaQueueTests</c> already has that boundary). The queue is
/// preloaded directly (a dummy source, never real mesh content — this
/// pin proves the VALVE's threshold behavior, not classification, which
/// is covered elsewhere) with entries sitting in ALPHA when the walk's
/// own <c>DrawBlock</c>-equivalent land-cell turn reaches its
/// <c>FlushAlphaList(0.75f)</c> call. Mutation check: forcing
/// <see cref="RetailAlphaQueue.Flush"/>'s no-op comparison to
/// <c>&lt;=</c> instead of <c>&lt;</c> (S4-c1/c2's own boundary mutation)
/// makes the 2250 case read <c>PendingCount == 2250</c> (a no-op)
/// instead of 0 — the first assertion below fails against that
/// mutation.
/// </summary>
[Theory]
[InlineData(2250, 0)]
[InlineData(2249, 2249)]
public void SortCellExit_ValveDrainsThroughReplayAtTheExactBoundary(
int preloadedCount, int expectedPendingAfter)
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log, fx.AlphaQueue);
leaf.CellsWithoutEmitters.UnionWith(CoarseLandscapeBuckets(0xF4180000u));
var ctx = new TestContext();
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
var walk = new RetailFrameWalk();
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
var block = new WalkLandBlock
{
LandblockId = 0xF4180000u, SideCellCount = 1, MaxZ = 10f, MinZ = 0f,
};
block.EnsureCellArrays();
landscape.Blocks[0] = block;
var dummySource = new DummyAlphaSource();
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
for (int i = 0; i < preloadedCount; i++)
{
Assert.True(fx.AlphaQueue.TryAppend(RetailAlphaList.Alpha, dummySource, i, false));
}
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
walk.DrawLandscape(landscape, OneDegenerateView(), ctx, driver);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(new[] { preloadedCount }, leaf.AlphaPendingAtSortCellExit);
Assert.Equal(expectedPendingAfter, fx.AlphaQueue.PendingCount);
}
private sealed class DummyAlphaSource : IRetailAlphaDrawSource
{
public void PrepareAlphaDraws(ReadOnlySpan<int> tokens)
{
}
public void DrawPreparedAlphaBatch(int firstPreparedDraw, int drawCount)
{
}
public void ResetAlphaSubmissions()
{
}
}
// ── F4(b) (S3 chunk 3 fix round 1 §9.6): an interior root with one
// surviving exit view — the SAME fixture as
// RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells

View file

@ -551,8 +551,11 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Equal(record.Transform.LocalToWorld, call.LocalToWorld);
}
// ── SubmitWalkAlphaInstance: same viewer distance + per-instance data as
// DeferTransparentGroups, through the REAL RetailAlphaQueue. ───────────
// ── SubmitWalkAlphaInstance: same per-instance data and router decision
// as DeferTransparentGroups, through the REAL RetailAlphaQueue — S4-c2
// fix round 1 (A5): this banner's stale "same viewer distance" text is
// corrected; the FIFO cutover deleted viewer distance from the alpha
// path entirely. ────────────────────────────────────────────────────
/// <summary>S4-c2: retail's queues are FIFO, not distance-sorted — this
/// pins the routing decision instead (an AlphaBlend batch's constructed

View file

@ -368,33 +368,21 @@ public sealed partial class WalkTraceConformanceTests
}
/// <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>&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. 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>
/// 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 void RunAlphaFlushTranscriptGate(string fixtureName)
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);
@ -447,34 +435,74 @@ public sealed partial class WalkTraceConformanceTests
leaf.ActualFlushes.Add(
(RetailAlphaFlushSite.RenderNormalMode, 0f, clipBeforeFinal, alphaBeforeFinal));
IReadOnlyList<(RetailAlphaFlushSite Site, float Threshold, int DrainedClip, int DrainedAlpha)> actual =
leaf.ActualFlushes;
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)));
// 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;
/// <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 (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)}");
$"{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(
@ -487,41 +515,67 @@ public sealed partial class WalkTraceConformanceTests
", ",
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>
/// <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 AlphaFlushTranscript_CathedralArrival_MatchesRetailFrame2()
=> RunAlphaFlushTranscriptGate("cathedral-arrival.alphadepth");
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 AlphaFlushTranscript_CathedralLeak_MatchesRetailFrame2()
=> RunAlphaFlushTranscriptGate("cathedral-leak.alphadepth");
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 AlphaFlushTranscript_CathedralStairArch_MatchesRetailFrame2()
=> RunAlphaFlushTranscriptGate("cathedral-stair-arch.alphadepth");
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 AlphaFlushTranscript_FoundryDeep_MatchesRetailFrame2()
=> RunAlphaFlushTranscriptGate("foundry-deep.alphadepth");
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 AlphaFlushTranscript_HoltburgDoorwayStill_MatchesRetailFrame2()
=> RunAlphaFlushTranscriptGate("holtburg-doorway-still.alphadepth");
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 AlphaFlushTranscript_TerraceEdge_MatchesRetailFrame2()
=> RunAlphaFlushTranscriptGate("terrace-edge.alphadepth");
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})")) + "]";