acdream/tests/AcDream.App.Tests/Rendering/RetailAlphaMeshRouterTests.cs
Erik 89f1e2676f feat(render): S4 chunk 2 — retail's two-list alpha FIFO cutover
Replaces the single scope-global distance-sorted RetailAlphaQueue with
retail's own two independent FIFO lists (CLIP/ALPHA, capacity 3000 each,
D3DPolyRender::AddMeshToAlphaList's exact append-only/capacity-drop
behavior — Ghidra-verified 2026-09-04), routed by a new
RetailAlphaMeshRouter porting DrawMesh's five-row immediate/delayed
branch table and ConstructMesh's subset-mask formula as pure functions,
and drained at retail's four normal-world FlushAlphaList sites
(DrawBuilding/DrawBlock/PView::DrawCells/RenderNormalMode) under the
exact Ghidra-verified no-op predicate (both counts strictly below
threshold*3000). A new WalkFrameEventKind.SortCellExit /
IWalkEventSink.OnSortCellExit / IWalkFrameLeafRenderer.FlushSortCellExit
fires once per admitted land-block cell for DrawBlock's 0.75f valve,
pinned by a dedicated far/near ordering test in RetailFrameWalkTests.cs.

WbDrawDispatcher's two submit sites and ParticleRenderer's one route
through the router; since none of the three ever draws during the Sky
leaf, installs a detail surface, or sets MultiPassAlpha, rows 1/2/4/5
are provably unreachable there and the call sites assert loudly rather
than building unexercisable immediate-draw plumbing. FlushFartherThan,
RetailAlphaOrdering.ComputeViewerDistance, and every viewerDistance
argument on the submit path are deleted.

Scope note (packet s4-depth-alpha-packet.md §10): C4 (routing EnvCell's
transparent shell batches through the shared queue) was not attempted —
EnvCell draws one per-cell MultiDrawIndexedIndirect call with no
per-subset deferred-replay abstraction, and building one without visual
verification (no graphical client in this worktree) was judged out of
this bounded chunk's scope. AP-34 is therefore retired and replaced by
two narrower rows rather than deleted outright: AP-236 (the carried-
forward EnvCell-immediate residual) and AP-237 (a newly identified gap:
TranslucencyKind.AlphaBlend can arise from either retail's Alpha/
Translucent bits, mask 0x02/ALPHA, or the Translucent+ClipMap "cloud"
override, mask 0x08/CLIP — GroupKey doesn't retain the raw bit to tell
them apart, so the router always picks ALPHA; only known example is
cloud GfxObj 0x01004C35). Both are compositing-order-only divergences,
never blend/visual ones.

Mutation checks (each applied, confirmed failing, then reverted):
- FIFO drain order reversed -> 5 RetailAlphaQueueTests fail (order).
- FlushAlphaList `<` -> `<=` -> boundary/scratch tests fail (2250 case
  reads drained=0 instead of 2250).
- Capacity check loosened (3000 -> 6000) -> overflow-drop test fails
  (TryAppend returns true, PendingCount reads 3001).
- IsFirstForList forced true -> flag test fails once inspected on the
  pre-flush two-entry snapshot (the post-flush single-survivor version
  of this test was vacuous and rewritten).
- Router row 3 condition inverted -> both the hand-traced Theory (6
  cases) and the 160-cell independent-truth-table brute force fail (20
  mismatches).
- SortCellExit emitted before OnLandscapeCellTurn instead of after ->
  RetailFrameWalkTests ordering pin fails ("SCX must immediately follow
  its own cell's SC").
- Prepare-per-list instead of prepare-once-combined -> the CLIP/ALPHA
  boundary batching test throws (index out of range).

Gates: Release build 0 warnings/0 errors. Hermetic lane (Lane!=Installed
Dat&...&Status!=KnownFailure) 6855/6855 passed. InstalledDat lane 249
passed / 10 failed — exactly the 4 pre-existing failures (#383 x2
LayoutImporter, TowerAscent KnownFailure, #458 Oh_doorway_still
KnownFailure) plus 6 NEW KnownFailure Facts
(AlphaFlushTranscript_*_MatchesRetailFrame2, one per capture) extending
this gate from PM/PC to AM/FL: the flush SITE+THRESHOLD sequence matches
the capture exactly for all six poses (including zero SortCellExit
drains in every capture, confirming the 0.75 valve is inert at these
scene complexities in both retail and this replay); the drained-COUNT-
per-list dimension diverges because this hermetic harness (matching the
existing PM/PC gate's own EmptyAlphaDepthWorldData design) carries no
live GfxObj/particle content, so every observed count reads (0,0)
against retail's real per-frame volume — both sequences quoted in full
per pose in the packet's new §10. Shader classes (VulkanShaderDescriptor
ContractTests/VulkanShaderManifestTests/RenderPackSpirvValidatorTests)
32/32 passed.

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

286 lines
13 KiB
C#

using AcDream.App.Rendering;
using AcDream.Core.Meshing;
namespace AcDream.App.Tests.Rendering;
/// <summary>
/// S4-c2: <see cref="RetailAlphaMeshRouter"/>'s port of retail
/// <c>D3DPolyRender::DrawMesh</c> @0x0059d4a0's branch table (spec §4).
/// </summary>
public sealed class RetailAlphaMeshRouterTests
{
/// <summary>
/// Hand-traced literal cases against the Ghidra-verified decomp — each
/// one differentiates exactly one row boundary, with the mutation each
/// guards against spelled out. delayMask is fixed at the real default
/// (0x0E) throughout, matching the contract's fixed axis.
/// </summary>
public static IEnumerable<object[]> RowBoundaryCases()
{
// Row 1a: sky wins outright even over a mask that would otherwise
// hit row 3. Mutation: dropping the sky check entirely returns
// Append(Alpha) instead.
yield return new object[]
{
true, false, false, (byte)0x02, false,
(int)RetailAlphaMeshAction.Immediate, (int)RetailAlphaList.Alpha, false,
};
// Row 1c: detailSurfaceActive wins outright even with multipass=true
// and a clip-bearing mask that would otherwise hit row 2. Mutation:
// dropping the detail check returns AppendClipAndImmediate instead.
yield return new object[]
{
false, true, true, (byte)0x08, false,
(int)RetailAlphaMeshAction.Immediate, (int)RetailAlphaList.Alpha, false,
};
// Row 2a: multipass + clip bit (exact 0x08) -> CLIP,
// overrideClipmap=true, ALSO immediate. Mutation: an `==` (exact
// mask match) instead of a bit test would still pass THIS case...
yield return new object[]
{
false, false, true, (byte)0x08, false,
(int)RetailAlphaMeshAction.AppendClipAndImmediate, (int)RetailAlphaList.Clip, true,
};
// ...but Row 2b (0x09 = clip bit + stipple bit) exposes an `==`
// mutation: it would fall through to row 3 (Append, no immediate
// pass) instead of also matching row 2's bit test.
yield return new object[]
{
false, false, true, (byte)0x09, false,
(int)RetailAlphaMeshAction.AppendClipAndImmediate, (int)RetailAlphaList.Clip, true,
};
// Row 2c: multipass=true but mask has NO clip bit (0x02) -> must
// fall through to row 3, not row 2. Mutation: a row-2 test that
// ignores the clip-bit requirement (fires on multipass alone) would
// return AppendClipAndImmediate here instead of plain Append(Alpha).
yield return new object[]
{
false, false, true, (byte)0x02, false,
(int)RetailAlphaMeshAction.Append, (int)RetailAlphaList.Alpha, false,
};
// Row 3a: alpha-family mask -> ALPHA, no immediate pass. Mutation:
// swapping the CLIP/ALPHA selection (always ALPHA regardless of bit
// 0x08) would still pass this one — paired with 3b below.
yield return new object[]
{
false, false, false, (byte)0x02, false,
(int)RetailAlphaMeshAction.Append, (int)RetailAlphaList.Alpha, false,
};
// Row 3b: clip-map mask -> CLIP (bit 0x08 selects the list even
// without multipass). Mutation: always routing row 3 to ALPHA
// regardless of the clip bit fails THIS case (expects Clip).
yield return new object[]
{
false, false, false, (byte)0x08, false,
(int)RetailAlphaMeshAction.Append, (int)RetailAlphaList.Clip, false,
};
// Row 3c: translucent-only mask (0x04, no clip bit) -> ALPHA.
yield return new object[]
{
false, false, false, (byte)0x04, false,
(int)RetailAlphaMeshAction.Append, (int)RetailAlphaList.Alpha, false,
};
// Row-3-inert-stipple: mask 0x01 (stipple bit ONLY, no alpha/clip/
// translucent bit) does NOT intersect the fixed default delay mask
// 0x0E (bit 0 is absent from 0x0E) — falls through to row 4.
// Mutation: including bit 0 in the effective delay test (treating
// 0x0E as if it were 0x0F) would make this case ALSO match row 3
// (Append) instead of falling to row 4's material-fallback decision.
yield return new object[]
{
false, false, false, (byte)0x01, true,
(int)RetailAlphaMeshAction.Append, (int)RetailAlphaList.Alpha, false,
};
// Row 4a: plain mask (0x00), material has_alpha true -> ALPHA
// fallback. Mutation: requiring the TRANSLUCENT bit (0x04) to
// ALREADY be set on the mask itself (rather than just checking the
// DELAY mask's own bit 0x04, which is fixed on regardless of the
// subset's mask) would make this case fall to row 5 (Immediate)
// instead.
yield return new object[]
{
false, false, false, (byte)0x00, true,
(int)RetailAlphaMeshAction.Append, (int)RetailAlphaList.Alpha, false,
};
// Row 4 vs 5: plain mask, material has_alpha FALSE -> row 4's own
// condition fails -> row 5 Immediate. Mutation: dropping the
// materialHasAlpha check from row 4 (treating it as "always append
// when mask is plain") would return Append here instead.
yield return new object[]
{
false, false, false, (byte)0x00, false,
(int)RetailAlphaMeshAction.Immediate, (int)RetailAlphaList.Alpha, false,
};
}
/// <summary>
/// <paramref name="expectedActionRaw"/>/<paramref name="expectedListRaw"/>
/// are boxed as <see langword="int"/> (not the internal enum types)
/// because <c>[Theory]</c> methods must be <see langword="public"/> and a
/// public method cannot expose an <see langword="internal"/> parameter
/// type — <see cref="RetailAlphaMeshAction"/>/<see cref="RetailAlphaList"/>
/// stay internal (this assembly's whole alpha-queue surface is internal).
/// </summary>
[Theory]
[MemberData(nameof(RowBoundaryCases))]
public void Route_MatchesTheHandTracedDecompBoundary(
bool sky, bool detail, bool multipass, byte mask, bool hasAlpha,
int expectedActionRaw, int expectedListRaw,
bool expectedOverrideClipmap)
{
var expectedAction = (RetailAlphaMeshAction)expectedActionRaw;
var expectedList = (RetailAlphaList)expectedListRaw;
RetailAlphaMeshDecision decision = RetailAlphaMeshRouter.Route(
sky, RetailAlphaMeshRouter.DefaultDelayMask, detail, multipass, mask, hasAlpha);
Assert.Equal(expectedAction, decision.Action);
if (expectedAction != RetailAlphaMeshAction.Immediate)
{
Assert.Equal(expectedList, decision.List);
Assert.Equal(expectedOverrideClipmap, decision.OverrideClipmap);
}
}
/// <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).
/// </summary>
[Fact]
public void Route_MatchesIndependentTruthTableAcrossEveryCell()
{
byte[] masks = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09];
bool[] bothBools = [false, true];
int mismatches = 0;
var firstMismatch = "";
foreach (byte mask in masks)
foreach (bool sky in bothBools)
foreach (bool detail in bothBools)
foreach (bool multipass in bothBools)
foreach (bool hasAlpha in bothBools)
{
RetailAlphaMeshDecision actual = RetailAlphaMeshRouter.Route(
sky, RetailAlphaMeshRouter.DefaultDelayMask, detail, multipass, mask, hasAlpha);
RetailAlphaMeshDecision expected = IndependentReferenceRoute(
sky, RetailAlphaMeshRouter.DefaultDelayMask, detail, multipass, mask, hasAlpha);
bool matches = actual.Action == expected.Action
&& (actual.Action == RetailAlphaMeshAction.Immediate
|| (actual.List == expected.List
&& actual.OverrideClipmap == expected.OverrideClipmap));
if (!matches && mismatches++ == 0)
{
firstMismatch =
$"sky={sky} detail={detail} multipass={multipass} mask=0x{mask:x2} "
+ $"hasAlpha={hasAlpha}: expected {expected}, got {actual}";
}
}
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(
bool sky, byte delayMask, bool detail, bool multipass, byte mask, bool hasAlpha)
{
bool row1 = sky || delayMask == 0 || detail;
if (row1)
return new RetailAlphaMeshDecision(RetailAlphaMeshAction.Immediate, default, false);
bool clipBit = (mask & 0b1000) == 0b1000;
bool row2 = multipass switch
{
true => clipBit,
false => false,
};
if (row2)
return new RetailAlphaMeshDecision(RetailAlphaMeshAction.AppendClipAndImmediate, RetailAlphaList.Clip, true);
int intersect = delayMask & mask;
bool row3 = intersect != 0;
if (row3)
{
RetailAlphaList list = clipBit ? RetailAlphaList.Clip : RetailAlphaList.Alpha;
return new RetailAlphaMeshDecision(RetailAlphaMeshAction.Append, list, false);
}
bool delayHasTranslucentBit = (delayMask & 0b0100) == 0b0100;
bool row4 = delayHasTranslucentBit && hasAlpha;
if (row4)
return new RetailAlphaMeshDecision(RetailAlphaMeshAction.Append, RetailAlphaList.Alpha, false);
return new RetailAlphaMeshDecision(RetailAlphaMeshAction.Immediate, default, false);
}
/// <summary>
/// <see cref="RetailAlphaMeshRouter.MaskFromTranslucencyKind"/>'s
/// reachable mapping (opaque/ClipMap are pre-filtered upstream and never
/// actually call this from a live site, but the function itself must
/// still map them defensively).
/// </summary>
[Theory]
[InlineData(TranslucencyKind.AlphaBlend, RetailAlphaMeshRouter.MaskAlphaFamily)]
[InlineData(TranslucencyKind.Additive, RetailAlphaMeshRouter.MaskAlphaFamily)]
[InlineData(TranslucencyKind.InvAlpha, RetailAlphaMeshRouter.MaskAlphaFamily)]
[InlineData(TranslucencyKind.ClipMap, RetailAlphaMeshRouter.MaskClipMap)]
[InlineData(TranslucencyKind.Opaque, (byte)0)]
public void MaskFromTranslucencyKind_MapsEveryKind(TranslucencyKind kind, byte expectedMask)
=> Assert.Equal(expectedMask, RetailAlphaMeshRouter.MaskFromTranslucencyKind(kind));
/// <summary>Spec §2's priority chain: alpha-family bits win outright over
/// ClipMap and Translucent even when both are also present. Mutation
/// check: checking ClipMap before the alpha-family union would return
/// 0x08 here instead of 0x02.</summary>
[Fact]
public void ConstructSubsetMask_AlphaFamilyWinsOverClipMapAndTranslucent()
{
byte mask = RetailAlphaMeshRouter.ConstructSubsetMask(
hasAlphaFamilyBit: true, hasClipMapBit: true, hasTranslucentBit: true,
hasPositiveStippling: false);
Assert.Equal(RetailAlphaMeshRouter.MaskAlphaFamily, mask);
}
/// <summary>ClipMap wins over Translucent when no alpha-family bit is
/// present. Mutation check: checking Translucent before ClipMap would
/// return 0x04 here instead of 0x08.</summary>
[Fact]
public void ConstructSubsetMask_ClipMapWinsOverTranslucentWithoutAlphaFamily()
{
byte mask = RetailAlphaMeshRouter.ConstructSubsetMask(
hasAlphaFamilyBit: false, hasClipMapBit: true, hasTranslucentBit: true,
hasPositiveStippling: false);
Assert.Equal(RetailAlphaMeshRouter.MaskClipMap, mask);
}
/// <summary>Positive stippling ORs bit 0x01 onto whichever base mask
/// applies. Mutation check: forgetting the OR (or using an exclusive
/// select) would leave the base mask value (0x04) instead of 0x05.</summary>
[Fact]
public void ConstructSubsetMask_PositiveStipplingOrsIntoTheBaseMask()
{
byte mask = RetailAlphaMeshRouter.ConstructSubsetMask(
hasAlphaFamilyBit: false, hasClipMapBit: false, hasTranslucentBit: true,
hasPositiveStippling: true);
Assert.Equal((byte)(RetailAlphaMeshRouter.MaskTranslucent | RetailAlphaMeshRouter.MaskPositiveStipple), mask);
}
}