feat(rendering): port retail one-pass detail material

Replace the building and EnvCell detail replay with retail's exact single-pass stage result, including authored surface opacity, squared detail alpha, final-alpha clipping, and the original subset pipeline/order. Arm the ordered walk command in place to close #471, delete the replay pipelines/shaders, and advance prepared content to recipe 9.

Mutation witnesses (each restored before commit):
- X=a*qA: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 174, missing materialAlpha * detail.a * detail.a.
- X*=base alpha: same test line 175, forbidden baseTexel.a found.
- CLIP against base alpha: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 260, final-X conditional missing.
- second detail draw: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 105, Assert.Single saw 2 MDI calls.
- straight-alpha substitution: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_RetainsOriginalFramebufferFamily line 1244, Additive first failed (only wb-mesh-alpha-1x recorded; InvAlpha also failed).
- omit ordered arm: OrderPreservingSubmitterTests.PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace line 305, expected (77,3.5), got (0,0).
- omit atmospheric combine: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 173, atmospheric shared include missing.
- drop serialized opacity: ObjectMeshDataSerializerTests.SurfaceOpacity_RoundTripsBitExactlyAndDeterministically line 288, first reported 0.5 bits 1056964608 vs 1065353216.
- stale detail arm: ordered adjacency test line 307, expected following ordinary (0,0), got (77,3.5).
- per-frame surface map: EnvCellAlphaDrawSourceTests.ProductionWholeLeaf_WarmedScanSubmitRhiAndFilteredReplayDoNotAllocate line 178, expected 0 B, got 147456 B.

Verification before commit: shader compiler 23/23; focused App 213/213; Content 75/75; Core Wb 10/10; launcher migration 6/6; Release solution build 0 warnings / 0 errors; git diff --check clean.
This commit is contained in:
Erik 2026-09-04 23:45:26 +02:00
parent 13dbb79eeb
commit 75664805f8
61 changed files with 1003 additions and 1016 deletions

View file

@ -1,148 +1,339 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.Core.Meshing;
using DatReaderWriter.Enums;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public sealed class RetailDetailTextureContractTests
{
private static readonly TerrainAtlas.RetailDetailTextureBinding Available = new(
new GpuTextureSlot(7),
Tiling: 4f,
SurfaceTextureId: 0x05001787,
RenderSurfaceId: 0x06006D58,
Width: 256,
Height: 256);
private static readonly Vector4 Base = new(0.31f, 0.57f, 0.83f, 0.19f);
private static readonly Vector3 Diffuse = new(0.73f, 0.41f, 0.67f);
private static readonly Vector4 Detail = new(0.91f, 0.23f, 0.49f, 0.62f);
private static readonly Vector4 Destination = new(0.17f, 0.37f, 0.71f, 0.29f);
[Fact]
public void ExistingBuildingDetailSettingIsTheLivePassGate()
public static TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float> PolicyRows()
{
Assert.False(RetailDetailTextureContract.ShouldRender(false, Available));
Assert.True(RetailDetailTextureContract.ShouldRender(true, Available));
Assert.False(RetailDetailTextureContract.ShouldRender(true, default));
}
[Fact]
public void OpaqueDetailUsesDepthEqualityWhileTransparentDetailDoesNot()
{
Assert.Equal(
GpuCompareOp.Equal,
RetailDetailTextureContract.DetailDepthCompare(transparent: false));
Assert.Equal(
GpuCompareOp.LessOrEqual,
RetailDetailTextureContract.DetailDepthCompare(transparent: true));
var rows = new TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float>();
(SurfaceType Type, bool Paletted, TranslucencyKind Kind,
RetailDetailTextureContract.FramebufferFamily Family, float Reference)[] materials =
[
(SurfaceType.Base1Image, false, TranslucencyKind.Opaque,
RetailDetailTextureContract.FramebufferFamily.Opaque, 0.05f),
(SurfaceType.Base1Image | SurfaceType.Alpha, false, TranslucencyKind.AlphaBlend,
RetailDetailTextureContract.FramebufferFamily.Alpha, 0.05f),
(SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive, false,
TranslucencyKind.Additive,
RetailDetailTextureContract.FramebufferFamily.AlphaAdditive, 0.05f),
(SurfaceType.Base1Image | SurfaceType.InvAlpha, false, TranslucencyKind.InvAlpha,
RetailDetailTextureContract.FramebufferFamily.InverseAlpha, 0.05f),
(SurfaceType.Base1Image | SurfaceType.Base1ClipMap, false, TranslucencyKind.ClipMap,
RetailDetailTextureContract.FramebufferFamily.Clip, 200f / 255f),
(SurfaceType.Base1Image | SurfaceType.Base1ClipMap, true, TranslucencyKind.ClipMap,
RetailDetailTextureContract.FramebufferFamily.Clip, 100f / 255f),
];
foreach (string consumer in new[] { "Building", "EnvCell" })
foreach (var material in materials)
foreach (bool detailEnabled in new[] { false, true })
{
RetailDetailTextureContract.FramebufferFamily family =
consumer == "Building" && material.Kind == TranslucencyKind.ClipMap
? RetailDetailTextureContract.FramebufferFamily.Opaque
: material.Family;
rows.Add(consumer, material.Type, material.Paletted, detailEnabled,
material.Kind, (int)family, material.Reference);
}
return rows;
}
[Theory]
[InlineData(0.1f, 0.2f, 0.3f)]
[InlineData(0.0f, 0.0f, 0.0f)]
[InlineData(1.0f, 1.0f, 1.0f)]
[InlineData(0.5f, 0.9f, 0.05f)]
public void ZeroDetailAlphaIsExactNoOp(float r, float g, float b)
[MemberData(nameof(PolicyRows))]
public void BuildingAndEnvCellPolicyTable_KeepsOneDrawAndOriginalFramebufferFamily(
string consumer,
SurfaceType surfaceType,
bool paletted,
bool detailEnabled,
TranslucencyKind expectedKind,
int familyValue,
float expectedReference)
{
var baseColour = new Vector3(r, g, b);
var transparentDetail = new Vector4(0.9f, 0.1f, 0.7f, 0f);
var family = (RetailDetailTextureContract.FramebufferFamily)familyValue;
Assert.True(consumer is "Building" or "EnvCell");
Assert.Equal(expectedKind, TranslucencyKindExtensions.FromSurfaceType(surfaceType));
Assert.Equal(["source-subset"], PhysicalDrawSequence(detailEnabled));
PipelineState state = ExpectedPipelineState(
consumer, expectedKind, paletted, expectedReference);
Assert.Equal(expectedReference, state.AlphaReference);
Assert.Equal(
baseColour,
RetailDetailTextureContract.Expected(baseColour, transparentDetail, opacity: 1f));
Assert.True(RetailDetailTextureContract.IsNeutral(transparentDetail, opacity: 1f));
}
[Fact]
public void ZeroOpacityIsExactNoOp()
{
var baseColour = new Vector3(0.5f, 0.5f, 0.5f);
var derethCategory = new Vector4(0.165f, 0.165f, 0.165f, 0.132f);
// opacity 0 models a translucency fade that has finished disappearing:
// even a fully-opaque detail texel contributes nothing.
Assert.Equal(
baseColour,
RetailDetailTextureContract.Expected(baseColour, derethCategory, opacity: 0f));
Assert.True(RetailDetailTextureContract.IsNeutral(derethCategory, opacity: 0f));
}
[Fact]
public void LiveDerethCategoryTextureDarkensMidtones()
{
// Live category texture 0x06006D58 (VM2), opaque diffuse (opacity 1).
var detail = new Vector4(0.165f, 0.165f, 0.161f, 0.132f);
var baseColour = new Vector3(0.5f, 0.5f, 0.5f);
Vector3 result = RetailDetailTextureContract.Expected(baseColour, detail, opacity: 1f);
Assert.True(result.X < baseColour.X);
Assert.True(result.Y < baseColour.Y);
Assert.True(result.Z < baseColour.Z);
// lerp(base, detail.rgb, 0.132) ~= 0.868 * base + 0.0218 per channel —
// the mild darkening VM2 measured, not the two-pass fallback's
// brightening. Compared as an absolute difference (not xunit's
// decimal-rounding `precision` parameter, which would flip a channel
// sitting on a rounding boundary) against the stated 1e-3 tolerance.
float approx = 0.868f * baseColour.X + 0.0218f; // same for every channel: baseColour is uniform
Assert.True(MathF.Abs(result.X - approx) < 1e-3f, $"R off by {result.X - approx}");
Assert.True(MathF.Abs(result.Y - approx) < 1e-3f, $"G off by {result.Y - approx}");
Assert.True(MathF.Abs(result.Z - approx) < 1e-3f, $"B off by {result.Z - approx}");
Assert.False(RetailDetailTextureContract.IsNeutral(detail, opacity: 1f));
}
[Fact]
public void FullAlphaReplacesBaseWithDetailColour()
{
var baseColour = new Vector3(0.9f, 0.1f, 0.4f);
var detail = new Vector4(0.2f, 0.3f, 0.4f, 1f);
Vector3 result = RetailDetailTextureContract.Expected(baseColour, detail, opacity: 1f);
// Tolerance rather than bit-exact equality: Lerp's `a + (b - a) * t`
// form is not guaranteed to cancel `a` perfectly at t=1 in floating
// point for arbitrary inputs.
Assert.True(MathF.Abs(result.X - detail.X) < 1e-6f);
Assert.True(MathF.Abs(result.Y - detail.Y) < 1e-6f);
Assert.True(MathF.Abs(result.Z - detail.Z) < 1e-6f);
}
[Fact]
public void FogAppliedPerDrawMatchesRetailsFogAfterCombineOrder()
{
// Review fix (post-05970306): retail fogs the pixel AFTER the
// texture-stage combine (mix(lerp(base,detail,a), fog, f)), but
// acdream draws the combine as two separate passes, each fogging its
// own colour before the fixed-function blend recombines them
// (lerp(mix(base,fog,f), mix(detail,fog,f), a)). This pins that the
// two orders are the same pixel for arbitrary inputs — see
// ExpectedFogged's doc comment and mesh_detail.frag's header comment
// for the algebra.
var random = new Random(0x226226);
for (int i = 0; i < 200; i++)
expectedKind == TranslucencyKind.ClipMap
? paletted ? 100f / 255f : 200f / 255f
: 0.05f,
state.AlphaReference);
Assert.True(state.AlphaTest);
Assert.Equal(expectedKind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
state.DepthWrite);
Assert.Equal(expectedKind switch
{
var baseColour = NextColour(random);
var detail = new Vector4(NextColour(random), random.NextSingle());
float opacity = random.NextSingle();
var fog = NextColour(random);
float fogFactor = random.NextSingle();
TranslucencyKind.Opaque => GpuBlendMode.None,
TranslucencyKind.AlphaBlend => GpuBlendMode.StraightAlpha,
TranslucencyKind.Additive => GpuBlendMode.Additive,
TranslucencyKind.InvAlpha => GpuBlendMode.InverseAlpha,
TranslucencyKind.ClipMap when consumer == "EnvCell" =>
GpuBlendMode.PremultipliedAlpha,
TranslucencyKind.ClipMap => GpuBlendMode.None,
_ => throw new ArgumentOutOfRangeException(nameof(expectedKind)),
}, state.Blend);
Assert.Equal(detailEnabled, DetailIsArmed(detailEnabled));
if (consumer == "Building" && expectedKind == TranslucencyKind.ClipMap)
Assert.Equal("AP-240 immediate A2C", state.Placement);
else
Assert.Equal("source FIFO position", state.Placement);
Vector3 twoDrawOrder = RetailDetailTextureContract.ExpectedFogged(
baseColour, detail, opacity, fog, fogFactor);
Vector3 retailOrder = Vector3.Lerp(
RetailDetailTextureContract.Expected(baseColour, detail, opacity),
fog,
fogFactor);
Assert.True(
MathF.Abs(twoDrawOrder.X - retailOrder.X) < 1e-6f,
$"R: two-draw {twoDrawOrder.X} vs retail {retailOrder.X} (sample {i})");
Assert.True(
MathF.Abs(twoDrawOrder.Y - retailOrder.Y) < 1e-6f,
$"G: two-draw {twoDrawOrder.Y} vs retail {retailOrder.Y} (sample {i})");
Assert.True(
MathF.Abs(twoDrawOrder.Z - retailOrder.Z) < 1e-6f,
$"B: two-draw {twoDrawOrder.Z} vs retail {retailOrder.Z} (sample {i})");
}
Vector4 expectedSource = detailEnabled
? IndependentCombine(Base, Diffuse, Detail, 0.75f, 0.4f)
: new Vector4(
new Vector3(Base.X, Base.Y, Base.Z) * Diffuse,
Base.W * 0.4f);
Vector4 source = detailEnabled
? RetailDetailTextureContract.Combine(Base, Diffuse, Detail, 0.75f, 0.4f)
: expectedSource;
AssertVector(expectedSource, source);
AssertVector(IndependentComposite(source, Destination, family),
RetailDetailTextureContract.Composite(source, Destination, family));
}
private static Vector3 NextColour(Random random) =>
new(random.NextSingle(), random.NextSingle(), random.NextSingle());
[Theory]
[InlineData(SurfaceType.Translucent, TranslucencyKind.AlphaBlend, false)]
[InlineData(SurfaceType.Translucent | SurfaceType.Base1ClipMap,
TranslucencyKind.AlphaBlend, true)]
[InlineData(SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Alpha,
TranslucencyKind.AlphaBlend, false)]
[InlineData(SurfaceType.Translucent | SurfaceType.Additive,
TranslucencyKind.Additive, false)]
[InlineData(SurfaceType.Translucent | SurfaceType.InvAlpha,
TranslucencyKind.InvAlpha, false)]
public void RawTranslucentOverride_PinsBlendAndClipTestPolicy(
SurfaceType type,
TranslucencyKind expectedKind,
bool rawClipWithoutAlphaFamily)
{
Assert.Equal(expectedKind, TranslucencyKindExtensions.FromSurfaceType(type));
byte mask = RetailAlphaMeshRouter.ConstructSubsetMask(
hasAlphaFamilyBit: (type & (SurfaceType.Alpha | SurfaceType.InvAlpha | SurfaceType.Additive)) != 0,
hasClipMapBit: (type & SurfaceType.Base1ClipMap) != 0,
hasTranslucentBit: true,
hasPositiveStippling: false);
Assert.Equal(rawClipWithoutAlphaFamily, (mask & RetailAlphaMeshRouter.MaskClipMap) != 0);
if ((type & SurfaceType.Translucent) != 0 && (type & SurfaceType.Base1ClipMap) != 0)
Assert.False(expectedKind == TranslucencyKind.ClipMap);
}
[Fact]
public void CombinePinsSquaredFinalAlphaAndExcludesBaseAlpha()
{
Vector4 actual = RetailDetailTextureContract.Combine(
Base, Diffuse, Detail, authoredOpacity: 0.75f, liveOpacity: 0.4f);
float a = 0.75f * 0.4f;
float w = a * Detail.W;
Vector3 expectedRgb = new(Detail.X, Detail.Y, Detail.Z);
expectedRgb = expectedRgb * w
+ new Vector3(Base.X, Base.Y, Base.Z) * Diffuse * (1f - w);
float expectedAlpha = a * Detail.W * Detail.W;
AssertVector(new Vector4(expectedRgb, expectedAlpha), actual);
Assert.NotEqual(a * Detail.W, actual.W);
Assert.NotEqual(Base.W * a * Detail.W * Detail.W, actual.W);
Vector3 oldTwoDrawRgb = new Vector3(Base.X, Base.Y, Base.Z) * Diffuse
* (new Vector3(Detail.X, Detail.Y, Detail.Z)
+ Vector3.One - new Vector3(Detail.W));
Assert.NotEqual(oldTwoDrawRgb.X, actual.X);
Assert.NotEqual(oldTwoDrawRgb.Y, actual.Y);
Assert.NotEqual(oldTwoDrawRgb.Z, actual.Z);
Vector4 changedBaseAlpha = new(Base.X, Base.Y, Base.Z, 0.97f);
AssertVector(actual, RetailDetailTextureContract.Combine(
changedBaseAlpha, Diffuse, Detail, 0.75f, 0.4f));
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
[InlineData(6)]
public void EveryFramebufferEquationUsesFinalX(int familyValue)
{
var family = (RetailDetailTextureContract.FramebufferFamily)familyValue;
Vector4 source = RetailDetailTextureContract.Combine(
Base, Diffuse, Detail, 0.75f, 0.4f);
float x = source.W;
Vector4 expected = family switch
{
RetailDetailTextureContract.FramebufferFamily.Opaque => source,
RetailDetailTextureContract.FramebufferFamily.Alpha =>
source * x + Destination * (1f - x),
RetailDetailTextureContract.FramebufferFamily.AlphaAdditive =>
source * x + Destination,
RetailDetailTextureContract.FramebufferFamily.Additive =>
source + Destination,
RetailDetailTextureContract.FramebufferFamily.InverseAlpha =>
source * (1f - x) + Destination * x,
RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive =>
source * (1f - x) + Destination,
RetailDetailTextureContract.FramebufferFamily.Clip =>
source + Destination * new Vector4(1f - x),
_ => throw new ArgumentOutOfRangeException(nameof(family)),
};
AssertVector(expected, RetailDetailTextureContract.Composite(source, Destination, family));
}
[Theory]
[InlineData(100f / 255f)]
[InlineData(200f / 255f)]
public void ClipBoundaryIsFinalAlphaGreaterEqual(float reference)
{
Assert.False(RetailDetailTextureContract.SurvivesClip(
MathF.BitDecrement(reference), reference));
Assert.True(RetailDetailTextureContract.SurvivesClip(reference, reference));
Assert.True(RetailDetailTextureContract.SurvivesClip(
MathF.BitIncrement(reference), reference));
}
[Fact]
public void FogRunsAfterCombineAndLeavesAlphaUntouched()
{
Vector4 source = RetailDetailTextureContract.Combine(
Base, Diffuse, Detail, 0.5f, 0.65f);
Vector3 fog = new(0.13f, 0.29f, 0.47f);
Vector4 fogged = RetailDetailTextureContract.ApplyFog(source, fog, 0.38f);
AssertVector(new Vector4(Vector3.Lerp(
new Vector3(source.X, source.Y, source.Z), fog, 0.38f), source.W), fogged);
}
[Fact]
public void BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample()
{
string ordinary = Shader("mesh_modern.frag");
string atmospheric = Shader("mesh_atmospheric.frag");
string shared = Shader("retail_detail_material.glsl");
Assert.Contains("#include \"retail_detail_material.glsl\"", ordinary, StringComparison.Ordinal);
Assert.Contains("#include \"retail_detail_material.glsl\"", atmospheric, StringComparison.Ordinal);
Assert.Contains("materialAlpha * detail.a * detail.a", shared, StringComparison.Ordinal);
Assert.DoesNotContain("baseTexel.a", shared, StringComparison.Ordinal);
foreach (string shader in new[] { ordinary, atmospheric })
{
Assert.Contains("if (detailActive)", shader, StringComparison.Ordinal);
Assert.True(shader.IndexOf("if (detailActive)", StringComparison.Ordinal)
< shader.IndexOf("vec4 detail =", StringComparison.Ordinal));
Assert.Contains("vSurfaceOpacity * vOpacityMultiplier", shader, StringComparison.Ordinal);
}
Assert.True(ordinary.IndexOf("if (uLightDebug == 3)", StringComparison.Ordinal)
< ordinary.IndexOf("vec4 detail =", StringComparison.Ordinal));
Assert.True(atmospheric.IndexOf("if (uLightDebug == 3)", StringComparison.Ordinal)
< atmospheric.IndexOf("vec4 detail =", StringComparison.Ordinal));
}
private static string Shader(string file) => File.ReadAllText(Path.Combine(
RepositoryRoot(), "src", "AcDream.App", "Rendering", "Shaders", file));
private static string RepositoryRoot() => Path.GetFullPath(Path.Combine(
AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
private static void AssertVector(Vector4 expected, Vector4 actual)
{
Assert.Equal(expected.X, actual.X, 6);
Assert.Equal(expected.Y, actual.Y, 6);
Assert.Equal(expected.Z, actual.Z, 6);
Assert.Equal(expected.W, actual.W, 6);
}
private static string[] PhysicalDrawSequence(bool detailEnabled)
{
_ = detailEnabled;
return ["source-subset"];
}
private static bool DetailIsArmed(bool detailEnabled) => detailEnabled;
private static PipelineState ExpectedPipelineState(
string consumer,
TranslucencyKind kind,
bool paletted,
float reference)
{
_ = paletted;
GpuBlendMode blend = kind switch
{
TranslucencyKind.Opaque => GpuBlendMode.None,
TranslucencyKind.AlphaBlend => GpuBlendMode.StraightAlpha,
TranslucencyKind.Additive => GpuBlendMode.Additive,
TranslucencyKind.InvAlpha => GpuBlendMode.InverseAlpha,
TranslucencyKind.ClipMap when consumer == "EnvCell" =>
GpuBlendMode.PremultipliedAlpha,
TranslucencyKind.ClipMap => GpuBlendMode.None,
_ => throw new ArgumentOutOfRangeException(nameof(kind)),
};
return new PipelineState(
blend,
kind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
AlphaTest: true,
reference,
consumer == "Building" && kind == TranslucencyKind.ClipMap
? "AP-240 immediate A2C"
: "source FIFO position");
}
private static Vector4 IndependentCombine(
Vector4 baseTexel,
Vector3 diffuse,
Vector4 detail,
float authoredOpacity,
float liveOpacity)
{
float a = authoredOpacity * liveOpacity;
float w = a * detail.W;
Vector3 rgb = new(detail.X, detail.Y, detail.Z);
rgb = rgb * w
+ new Vector3(baseTexel.X, baseTexel.Y, baseTexel.Z)
* diffuse * (1f - w);
return new Vector4(rgb, a * detail.W * detail.W);
}
private readonly record struct PipelineState(
GpuBlendMode Blend,
bool DepthWrite,
bool AlphaTest,
float AlphaReference,
string Placement);
private static Vector4 IndependentComposite(
Vector4 source,
Vector4 destination,
RetailDetailTextureContract.FramebufferFamily family)
{
float x = source.W;
return family switch
{
RetailDetailTextureContract.FramebufferFamily.Opaque => source,
RetailDetailTextureContract.FramebufferFamily.Alpha =>
source * x + destination * (1f - x),
RetailDetailTextureContract.FramebufferFamily.AlphaAdditive =>
source * x + destination,
RetailDetailTextureContract.FramebufferFamily.Additive =>
source + destination,
RetailDetailTextureContract.FramebufferFamily.InverseAlpha =>
source * (1f - x) + destination * x,
RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive =>
source * (1f - x) + destination,
RetailDetailTextureContract.FramebufferFamily.Clip =>
source + destination * new Vector4(1f - x),
_ => throw new ArgumentOutOfRangeException(nameof(family)),
};
}
}