acdream/tests/AcDream.App.Tests/Rendering/RetailAlphaMeshRouterTests.cs
Erik 252886e84f fix(rendering): repair EnvCell retail CLIP state
Exclude deferred EnvCell subsets from the opaque turn, preserve exact CLIP/ALPHA fixed-state groups through leaf replay, and use retail's row-3 override, blend, depth, and texture-class alpha references. Keep the existing building-detail sentinel distinct from the two CLIP references.

Bound rejected source payload, replace no-op allocation proofs with actual EnvCell and particle RHI paths, rebuild checked-in SPIR-V, and correct AP-238/AP-240 plus the S4-c2 evidence record.

Gates: Release 0W/0E; hermetic 16735/0/0; InstalledDat 255 pass/10 known fail/1 skip; shaders 32/32; focused 239/239; allocation 2/2 at 0 B. The evidence/comment repair re-ran Release 0W/0E, shaders 32/32, affected 37/37, and allocation 2/2; mesh_detail.vert.spv remained byte-identical at SHA-256 5346247ab7d606046943e19b28888c814e08dc6cb27cd9750096ac055457eb57.

Mutation proof, with each mutation restored after its named first failure:

1. Restoring the opaque predicate to !IsAdditive fails WholeLeaf_MixedCellDrawsOpaqueAtTurnThenClipAndAlphaAtDrain first at draw count: expected 1, actual 3.
2. Selecting _alphaPipeline for CLIP fails WholeLeaf_ClipDrainBindsExactStateAndTextureClassReference first at the bind sequence: expected [envcell-clip], actual [envcell-alpha].
3. Disabling CLIP depth write fails that production Theory first at Assert.True(clipPipeline.Depth.Write): expected true, actual false.
4. Swapping palette/DDS references fails the DDS row first: expected 0.784313738, actual 0.392156869; the palette row reports the inverse.
5. Mutating mesh_modern.frag from < to <= fails ClipShaders_UseGreaterEqualForThePerRangeReference first at Assert.Contains("if (color.a < alphaCutoff) discard;"): the required source spelling is absent.
6. Restoring row-3 OverrideClipmap=true fails the real-interface clip Theory first at Assert.False: expected false, actual true.
7. Deleting failed-append rollback fails the flush/end/abort rejection-storm rows first at the bounded pending-count assertion: expected 0, actual 9000.
8. Selecting _transparentDetailPipeline for a ClipMap detail contribution fails the leaf detail pin first at the bind sequence: expected second bind envcell-retail-detail-clip, actual envcell-retail-detail-alpha.
9. Resetting detail ParamB to zero fails the same detail pin first at the second pushed reference: expected 0.784313738, actual 0.
10. Classifying CLIP with exact mask equality excludes legal 0x09 and fails WholeLeaf_PositiveStippleClipMaskUsesClipPipelineAndDdsReference first at pipeline: expected envcell-clip, actual envcell-alpha.
11. Mapping the new blend to SRC_ALPHA/INVSRCALPHA fails AllRetailBlendModesAreRepresentable first at the tuple: expected (One, OneMinusSrcAlpha), actual (SrcAlpha, OneMinusSrcAlpha).
12. Restoring mesh_detail.vert's uParamB > 0.5 category predicate fails ClipShaders_UseGreaterEqualForThePerRangeReference first because vDetailCategory = uParamB == 1.0 is absent.
13. Treating every positive detail uParamB as a cutoff fails that source pin first because isRetailClipReference(uParamB) ? uParamB : 0.05 is absent.
14. Adding arbitrary 0.5 as an accepted reference to either mesh_modern.frag or mesh_detail.frag fails that source pin first at Assert.DoesNotContain("value - 0.5"); both mutations were run and reversed independently.

Retail: D3DPolyRender::SetSurface @ 0x0059c4d0; paired binary @ 0x0059c72a, 0x0059c747/0x0059c74f, 0x0059c821, 0x0059c838, 0x0059c866.
2026-09-04 11:53:45 +02:00

293 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
/// 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_MatchesRestatedBranchTableAcrossEveryCell()
{
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 = RestatedBranchTableRoute(
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>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;
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
/// complete mapping. Ordinary Wb callers pre-filter opaque/ClipMap and
/// retain AP-239's earlier subset-mask reconstruction distinction. Mesh
/// particles, however, genuinely call this method without that ordinary-
/// Wb pre-filter, including for opaque-classified batches.
/// </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);
}
}