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

@ -102,7 +102,7 @@ public sealed class DirectionalShadowReceiverTests
Assert.Contains("instanceIndoor[instanceIndex]", vertex, StringComparison.Ordinal);
Assert.Contains("vSelectionLighting", fragment, StringComparison.Ordinal);
Assert.Contains("vOpacityMultiplier", fragment, StringComparison.Ordinal);
Assert.Contains("if (color.a < 0.05) discard", fragment, StringComparison.Ordinal);
Assert.Contains("color.a < alphaCutoff", fragment, StringComparison.Ordinal);
Assert.Contains("ACDREAM_SAMPLE_ARRAY", fragment, StringComparison.Ordinal);
Assert.Contains("combineOverlays", terrain, StringComparison.Ordinal);
Assert.Contains("combineRoad", terrain, StringComparison.Ordinal);
@ -115,12 +115,13 @@ public sealed class DirectionalShadowReceiverTests
Assert.Contains("radius = clamp(radius, 0, 2)", receiver, StringComparison.Ordinal);
Assert.Contains("float softness = max(uShadowControl.y, 1.0)", receiver, StringComparison.Ordinal);
string detailVertex = File.ReadAllText(Path.Combine(
shaderRoot,
"mesh_detail.vert"));
Assert.Contains("int instanceIndex = transformIndex - int(uTextureIndexB)", detailVertex, StringComparison.Ordinal);
Assert.Contains("Instances[transformIndex].transform", detailVertex, StringComparison.Ordinal);
Assert.Contains("instanceDetailCategory[instanceIndex]", detailVertex, StringComparison.Ordinal);
foreach (string worldVertexName in new[] { "mesh_modern.vert", "mesh_atmospheric.vert" })
{
string worldVertex = File.ReadAllText(Path.Combine(shaderRoot, worldVertexName));
Assert.Contains("Instances[", worldVertex, StringComparison.Ordinal);
Assert.Contains("].transform", worldVertex, StringComparison.Ordinal);
Assert.Contains("instanceDetailCategory[instanceIndex]", worldVertex, StringComparison.Ordinal);
}
}
private static string RepositoryRoot()

View file

@ -1,6 +1,7 @@
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
@ -74,34 +75,53 @@ public sealed class EnvCellAlphaDrawSourceTests
DrawPipelineNames(fixture.Device));
}
[Fact]
public void DetailOn_DrawsAtCellTurnWithDetail_AndQueuesNothing()
[Theory]
[InlineData(0, "envcell-opaque", 0f)]
[InlineData(1, "envcell-alpha", 0f)]
[InlineData(2, "envcell-additive", 0f)]
[InlineData(3, "envcell-clip", 200f / 255f)]
[InlineData(4, "envcell-clip", 100f / 255f)]
public void DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity(
int specIndex,
string expectedPipeline,
float expectedReference)
{
BatchSpec spec = specIndex switch
{
0 => BatchSpec.Opaque,
1 => BatchSpec.Alpha,
2 => BatchSpec.Additive,
3 => BatchSpec.ClipDds,
4 => BatchSpec.ClipPaletted,
_ => throw new ArgumentOutOfRangeException(nameof(specIndex)),
};
using var fixture = new ProductionEnvCellFixture(
detailSurfaceActive: true,
BatchSpec.ClipDds);
spec);
fixture.Queue.BeginFrame();
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
Assert.Equal(2, fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.Equal(
["envcell-clip", "envcell-retail-detail-clip"],
[expectedPipeline],
DrawPipelineNames(fixture.Device));
Assert.All(
DrawPushConstants(fixture.Device),
constants => Assert.Equal(200f / 255f, constants.ParamB));
GpuPipelineDescription detailPipeline = fixture.Device.CreatedPipelines
.Single(static pipeline =>
pipeline.Description.Name == "envcell-retail-detail-clip")
.Description;
Assert.Equal(GpuBlendMode.RetailDetail, detailPipeline.Blend);
Assert.True(detailPipeline.Depth.Test);
Assert.True(detailPipeline.Depth.Write);
Assert.Equal(GpuCompareOp.Equal, detailPipeline.Depth.Compare);
constants => Assert.Equal(expectedReference, constants.ParamB));
GpuPushConstants armed = Assert.Single(
DrawPushConstants(fixture.Device),
constants => constants.ParamA != 0f);
Assert.NotEqual(0u, armed.TextureIndexA);
Assert.Equal(0, fixture.Queue.PendingCount);
GpuRecordedStorageBind batchBind = fixture.Device.Calls
.OfType<GpuRecordedStorageBind>()
.Last(call => call.Binding == GpuBindingModel.StorageBatches);
ReadOnlySpan<ModernBatchData> gpuBatches = MemoryMarshal.Cast<byte, ModernBatchData>(
fixture.Device.RingBytes.Slice((int)batchBind.OffsetBytes, (int)batchBind.SizeBytes));
Assert.Equal(0.25f, Assert.Single(gpuBatches.ToArray()).SurfaceOpacity);
fixture.Queue.EndFrame();
Assert.Equal(2, fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
}
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
@ -232,28 +252,23 @@ public sealed class EnvCellAlphaDrawSourceTests
string root = RepositoryRoot();
string modern = File.ReadAllText(Path.Combine(
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_modern.frag"));
string detail = File.ReadAllText(Path.Combine(
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_detail.frag"));
string detailVertex = File.ReadAllText(Path.Combine(
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_detail.vert"));
string atmospheric = File.ReadAllText(Path.Combine(
root, "src", "AcDream.App", "Rendering", "Shaders", "mesh_atmospheric.frag"));
Assert.Contains("if (color.a < alphaCutoff) discard;", modern, StringComparison.Ordinal);
Assert.DoesNotContain("if (color.a <= alphaCutoff) discard;", modern, StringComparison.Ordinal);
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", modern, StringComparison.Ordinal);
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", detail, StringComparison.Ordinal);
foreach (string shader in new[] { modern, detail })
foreach (string shader in new[] { modern, atmospheric })
{
Assert.Contains(
"if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff)",
shader,
StringComparison.Ordinal);
Assert.DoesNotContain("alpha <= alphaCutoff", shader, StringComparison.Ordinal);
Assert.DoesNotContain("color.a <= alphaCutoff", shader, StringComparison.Ordinal);
Assert.Contains("isRetailClipReference(uParamB) ? uParamB : 0.05", shader, StringComparison.Ordinal);
Assert.Contains("abs(value - (100.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
Assert.Contains("abs(value - (200.0 / 255.0)) < 0.000001", shader, StringComparison.Ordinal);
Assert.DoesNotContain("uParamB > 0.0", shader, StringComparison.Ordinal);
Assert.DoesNotContain("value - 0.5", shader, StringComparison.Ordinal);
}
Assert.Contains("if (base.a < alphaCutoff)", detail, StringComparison.Ordinal);
Assert.DoesNotContain("if (base.a <= alphaCutoff)", detail, StringComparison.Ordinal);
Assert.Contains(
"vDetailCategory = uParamB == 1.0",
detailVertex,
StringComparison.Ordinal);
}
[Theory]
@ -438,6 +453,12 @@ public sealed class EnvCellAlphaDrawSourceTests
IsTransparent: true,
AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
PaletteId: 0);
internal static BatchSpec Additive { get; } = new(
RetailAlphaMeshRouter.MaskAlphaFamily,
IsTransparent: true,
AcDream.Core.Meshing.TranslucencyKind.Additive,
PaletteId: 0);
}
/// <summary>
@ -501,7 +522,9 @@ public sealed class EnvCellAlphaDrawSourceTests
TextureData = new byte[8 * 8 * 4],
Indices = [0, 1, 2],
IsTransparent = spec.IsTransparent,
IsAdditive = spec.Translucency == AcDream.Core.Meshing.TranslucencyKind.Additive,
Translucency = spec.Translucency,
SurfaceOpacity = 0.25f,
RetailSurfaceMask = spec.Mask,
CullMode = CullMode.Clockwise,
IsCellShell = true,

View file

@ -37,9 +37,8 @@ public sealed class GpuContractTests
[Fact]
public void StorageBindingsMatchTheShaderSources()
{
// mesh_modern.vert declares std430 bindings 0..8 in exactly this order;
// #226's mesh_detail.vert additionally declares binding 9 for the
// per-instance building category.
// Both world vertex families declare std430 bindings 0..9 in exactly
// this order; binding 9 carries per-instance detail eligibility.
Assert.Equal(0u, GpuBindingModel.StorageInstances);
Assert.Equal(1u, GpuBindingModel.StorageBatches);
Assert.Equal(2u, GpuBindingModel.StorageClipRegions);
@ -79,15 +78,14 @@ public sealed class GpuContractTests
// WbDrawDispatcher.ApplyRetailBlend selects a blend function from each DAT
// surface's TranslucencyKind. Retail also selects ONE/INVSRCALPHA for a
// pure Base1ClipMap with override=false (SetSurface @0x0059c72a), while
// the detail combine is a separate renderer state. Collapsing any of
// detail is now computed inside those same pipelines. Collapsing any of
// these modes changes the corresponding subset's composition.
Assert.Equal(6, Enum.GetValues<GpuBlendMode>().Length);
Assert.Equal(5, Enum.GetValues<GpuBlendMode>().Length);
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.PremultipliedAlpha, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.RetailDetail, Enum.GetValues<GpuBlendMode>());
}
[Fact]

View file

@ -41,23 +41,15 @@ public sealed class VulkanShaderManifestTests
{
["debug_line.frag.spv"] = "02fc04880bc5eb74353566f914675244038125c71443964decdc28e8199264df",
["debug_line.vert.spv"] = "f9c6a9b575bb07a426fb6ade677bca96a7752ca6b120e8f6451363ba73b51140",
// Re-pinned 2026-09-04 (Campaign OVERHAUL S4-c2 owner-authorized
// repair): Vulkan expresses SetSurface's pure-ClipMap alpha test
// through uParamB, recognizing only the paletted 100/255 and DDS
// 200/255 references and using discard-on-less so equality passes.
// The paired binary selects those globals and
// D3DCMP_GREATEREQUAL at 0x0059c73c..0x0059c838.
["mesh_modern.frag.spv"] = "1b3a59d0cc7c5bb6acf793d57d9673d860a7eb50bdd51770781f59feac4604a1",
// mesh_modern.vert re-pinned 2026-09-03 (S3 review fix round 1,
// F5): the binding=2 ClipRegionBuf (CellClip) SSBO, its
// gl_ClipDistance loop, and the gl_PerVertex redeclaration are
// deleted outright — the ONE WbDrawDispatcher API that could
// ever arm per-instance clip routing had ZERO production
// callers, so every instance has always mapped to slot
// 0 (no-clip) in every shipped build; the shader-side clip test
// clipped nothing. binding=3 instanceClipSlot[] stays declared,
// unread (fed 0; the instance-buffer layout is S5's).
["mesh_modern.vert.spv"] = "f9ed4ee7140ccd136130559dbea68545f733f6022f52b085b6df4bcb787223c6",
// Re-pinned 2026-09-04 (Campaign OVERHAUL S5-c4): retail's
// single-pass building detail material now runs inside the base
// fragment. Final alpha is authoredOpacity*liveFade*detail.a^2;
// CLIP tests that value while detail-off retains base-alpha logic.
["mesh_modern.frag.spv"] = "576af229b96a2a5e9b3b1ff16ab686d96578c2d9d1762b0e225df31bfd7aa86f",
// S5-c4 replaces the padding word at batch offset 4 with authored
// surface opacity and forwards it plus exact detail category/flags
// flat to the fragment shader. The 16-byte ABI is unchanged.
["mesh_modern.vert.spv"] = "1ff1b7118153ba0fddfcdfca39bd973a4b8f6b46db6db772e9886061d15561d8",
["particle.frag.spv"] = "680da227704e0b3afa9b5226a7d73dd65aa9d8759d081cf4d5009d30e148726b",
// Re-pinned 2026-08-27: portal-view clip slots now travel with
// deferred billboard particles, matching retail PortalList draws.

View file

@ -149,12 +149,6 @@ public sealed class VulkanViewportMappingTests
Assert.Equal(
(BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.InverseAlpha));
// VM2 (2026-08-22): real hardware runs the single-pass detail combine,
// a lerp expressed as the ordinary straight-alpha-over factor pair —
// not the two-pass DstColor fallback.
Assert.Equal(
(BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.RetailDetail));
}
[Fact]

View file

@ -113,7 +113,6 @@ public sealed class AtmosphericShaderAbiTests
[InlineData("terrain_atmospheric.vert")]
[InlineData("directional_shadow_terrain.vert")]
[InlineData("directional_shadow_terrain_multiview.vert")]
[InlineData("mesh_detail.vert")]
public void RetailAndTerrainShadersNeverCallAcdreamFoliageDisplace(string fileName)
{
string text = File.ReadAllText(Path.Combine(

View file

@ -177,6 +177,23 @@ public sealed class RenderPackSpirvValidatorTests
Assert.True(result.Success, result.Reason);
}
[Fact]
public void WorldDirectionalShadowReceiver_AllowsOnePassDetailCategoryBinding()
{
PipelineVariantDeclaration variant = BuiltInAtmosphericRenderPack.Descriptor
.PipelineVariants.Single(static value =>
value.Semantic
== RenderPipelineVariantSemantic.WorldDirectionalShadowReceiver);
RenderPackSpirvValidationResult result =
RenderPackSpirvValidator.ValidatePipelineVariantShader(
Shader("mesh_atmospheric.vert.spv"),
RenderPackShaderStage.Vertex,
variant);
Assert.True(result.Success, result.Reason);
}
[Fact]
public void DirectionalShadowUniformRequiresSixMembersAndSelectedSourceAtOffset320()
{

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)),
};
}
}

View file

@ -23,7 +23,7 @@ namespace AcDream.App.Tests.Rendering;
/// .TryCreateDetailTexture</c> (~TerrainAtlas.cs:488-556) must upload a FULL
/// mip chain, and it must be sampled with the repeat/linear world sampler,
/// or the "attenuation is the sampler's linear mip chain" claim in
/// mesh_detail.vert's header comment and the VM1 commit is unverified.
/// the one-pass world material contract and the VM1 commit is unverified.
/// Drives the private method directly (reflection, same pattern as
/// EnvCellRendererTests' private-method tests) against a synthetic
/// PFID_A8R8G8B8 RenderSurface, so no installed DAT is required — this lane

View file

@ -285,19 +285,38 @@ public sealed class OrderPreservingSubmitterTests
}
[Fact]
public void PrepareThenDraw_ADetailCategoryCommandRecordsItsOwnSoloDraw()
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace()
{
using var fx = new DispatcherFixture();
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true);
using DrawScope draw = fx.BeginDraw();
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(1, detailCategory: 1),
MakeCommand(2));
MakeCommand(2, translucency: TranslucencyKind.ClipMap, detailCategory: 1),
MakeCommand(3));
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
Assert.Equal([(0, 1), (1, 1), (2, 1)], DecodeDrawRanges(fx.Device));
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
Assert.Equal([(0, 1), (1, 1), (2, 1), (3, 1)],
runs.Select(run => (run.Start, run.Count)).ToList());
Assert.Equal((0u, 0f), (runs[0].Constants.TextureIndexA, runs[0].Constants.ParamA));
Assert.Equal((77u, 3.5f), (runs[1].Constants.TextureIndexA, runs[1].Constants.ParamA));
Assert.Equal((77u, 3.5f), (runs[2].Constants.TextureIndexA, runs[2].Constants.ParamA));
Assert.Equal((0u, 0f), (runs[3].Constants.TextureIndexA, runs[3].Constants.ParamA));
Assert.Equal(
[
"wb-mesh-opaque-1x",
"wb-mesh-opaque-a2c-1x",
"wb-mesh-opaque-a2c-1x",
"wb-mesh-opaque-a2c-1x",
"wb-mesh-opaque-a2c-1x",
],
fx.Device.Calls.OfType<GpuRecordedPipelineBind>()
.Select(call => call.PipelineName)
.ToArray());
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
}
[Fact]
@ -590,7 +609,7 @@ public sealed class OrderPreservingSubmitterTests
private readonly WbMeshAdapter _meshAdapter;
private readonly TextureCache _textures;
public DispatcherFixture()
public DispatcherFixture(bool detailAvailable = false, bool detailEnabled = false)
{
Device = new RecordingGpuDevice();
FrameLifetime = new GpuDeviceFrameLifetime(Device);
@ -615,7 +634,12 @@ public sealed class OrderPreservingSubmitterTests
_meshAdapter,
entitySpawnAdapter,
new EntityClassificationCache(),
new AcDream.Core.Rendering.TranslucencyFadeManager());
new AcDream.Core.Rendering.TranslucencyFadeManager(),
buildingDetail: detailAvailable
? new TerrainAtlas.RetailDetailTextureBinding(
new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16)
: default,
buildingDetailEnabled: () => detailEnabled);
}
public RecordingGpuDevice Device { get; }

View file

@ -316,7 +316,7 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.DoesNotContain(
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
Assert.Contains("161 active rows", register, StringComparison.Ordinal);
Assert.Contains("160 active rows", register, StringComparison.Ordinal);
int apSectionStart = register.IndexOf(
"## 3. Documented approximation (AP)", StringComparison.Ordinal);
int apSectionEnd = register.IndexOf(
@ -338,7 +338,7 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-243 |", StringComparison.Ordinal));
Assert.Equal(
161,
160,
File.ReadLines(registerPath).Count(static line =>
line.StartsWith("| AP-", StringComparison.Ordinal)));
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);
@ -1120,9 +1120,9 @@ public sealed class WalkStaticStreamPopulatorTests
/// <summary>
/// S4-c2 final M3 production pin for <c>SubmitWalkAlphaInstance</c>:
/// building shell + detail ON executes the immediate base+detail draws at
/// building shell + detail ON executes one armed original-pipeline draw at
/// the submit mark and queues nothing; detail OFF queues, then drains the
/// base draw without a detail pass; an ordinary batch remains deferred
/// neutral base draw; an ordinary batch remains deferred
/// even while the global detail option is on. Mutation check: hardcoding
/// <c>detailSurfaceActive=false</c> at the production submit makes the
/// first case's pending-count assertion fail with 1.</summary>
@ -1145,9 +1145,11 @@ public sealed class WalkStaticStreamPopulatorTests
on.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName.Contains("alpha", StringComparison.Ordinal)
&& !call.PipelineName.Contains("detail", StringComparison.Ordinal));
Assert.Contains(
on.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName.Contains("retail-detail-alpha", StringComparison.Ordinal));
Assert.Single(on.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.DoesNotContain(on.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName.Contains("detail", StringComparison.Ordinal));
Assert.Contains(on.Device.Calls.OfType<GpuRecordedPushConstants>(),
call => call.Constants.TextureIndexA == 99u && call.Constants.ParamA == 2f);
}
using (var off = new DispatcherFixture(
@ -1164,9 +1166,8 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Equal(1, off.AlphaQueue!.PendingCount);
Assert.Empty(off.Device.Calls.OfType<GpuRecordedPipelineBind>());
off.AlphaQueue.EndFrame();
Assert.DoesNotContain(
off.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName.Contains("retail-detail", StringComparison.Ordinal));
Assert.DoesNotContain(off.Device.Calls.OfType<GpuRecordedPushConstants>(),
call => call.Constants.TextureIndexA == 99u || call.Constants.ParamA != 0f);
}
using (var ordinary = new DispatcherFixture(
@ -1212,12 +1213,41 @@ public sealed class WalkStaticStreamPopulatorTests
defer.Invoke(fx.Dispatcher, [Matrix4x4.Identity]);
Assert.Equal(0, fx.AlphaQueue!.PendingCount);
Assert.Contains(
fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName.Contains("retail-detail-alpha", StringComparison.Ordinal));
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.DoesNotContain(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName.Contains("detail", StringComparison.Ordinal));
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPushConstants>(),
call => call.Constants.TextureIndexA == 99u && call.Constants.ParamA == 2f);
}
private static WbDrawDispatcher.WalkClassifiedBatch AlphaWalkBatch(uint detailCategory) =>
[Theory]
[InlineData(TranslucencyKind.AlphaBlend, "wb-mesh-alpha-1x")]
[InlineData(TranslucencyKind.Additive, "wb-mesh-additive-1x")]
[InlineData(TranslucencyKind.InvAlpha, "wb-mesh-inverse-1x")]
public void ImmediateBuildingDetail_RetainsOriginalFramebufferFamily(
TranslucencyKind kind,
string expectedPipeline)
{
using var fx = new DispatcherFixture(
withAlphaQueue: true,
detailAvailable: true,
detailEnabled: true);
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(
detailCategory: 1u,
translucency: kind);
fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity);
Assert.Equal(0, fx.AlphaQueue!.PendingCount);
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName == expectedPipeline);
}
private static WbDrawDispatcher.WalkClassifiedBatch AlphaWalkBatch(
uint detailCategory,
TranslucencyKind translucency = TranslucencyKind.AlphaBlend) =>
new(
new GroupKey(
0,
@ -1225,7 +1255,7 @@ public sealed class WalkStaticStreamPopulatorTests
3,
new GpuTextureSlot(1),
0,
TranslucencyKind.AlphaBlend,
translucency,
FoliageFlags: 0),
Matrix4x4.Identity,
ClipSlot: 0,

View file

@ -101,16 +101,16 @@ public class EnvCellRendererTests
Vector4.Zero,
sampleCount: 1));
EnvCellRenderer.BindEnvironmentDetailCategory(pass, frame);
using EnvCellRenderer renderer = CreateRenderer();
renderer.BindEnvironmentDetailCategory(pass, frame, instanceCount: 3);
GpuRecordedStorageBind storageBind = Assert.Single(
device.Calls.OfType<GpuRecordedStorageBind>());
Assert.Equal(GpuBindingModel.StorageInstanceDetailCategory, storageBind.Binding);
Assert.Equal((uint)sizeof(uint), storageBind.SizeBytes);
Assert.Equal(
1u,
MemoryMarshal.Read<uint>(
device.RingBytes.Slice((int)storageBind.OffsetBytes, sizeof(uint))));
Assert.Equal((uint)(3 * sizeof(uint)), storageBind.SizeBytes);
ReadOnlySpan<uint> categories = MemoryMarshal.Cast<byte, uint>(
device.RingBytes.Slice((int)storageBind.OffsetBytes, (int)storageBind.SizeBytes));
Assert.Equal([1u, 1u, 1u], categories.ToArray());
}
/// <summary>
@ -118,17 +118,14 @@ public class EnvCellRendererTests
/// StorageInstances/StorageBatches/StorageClipSlots/StorageGlobalLights/
/// StorageInstanceLightSets every frame but never
/// GpuBindingModel.StorageInstanceAlpha (binding 7) — the SSBO both
/// mesh_modern.vert (interior shells) and mesh_detail.vert (interior
/// detail replay) read as instanceAlpha[instanceIndex]. Predates VM1
/// (6c79d35c has the same omission); with mesh_detail.vert now reading
/// that binding too, an unfixed gap here would have made the interior
/// detail overlay read whatever section a DIFFERENT renderer (or a
/// both world vertex families read as instanceAlpha[instanceIndex].
/// Predates VM1 (6c79d35c has the same omission); an unfixed gap here
/// would have made the interior detail material read whatever section a DIFFERENT renderer (or a
/// stale ring slot) last left in binding 7, indexed by these cells'
/// instance ids. This pins that SubmitRhi binds its own constant-1.0f
/// section, sized to the live instance count, before it records any
/// draw in the pass — one bind serves every subsequent draw in that
/// pass, mesh_modern's shell pipeline and (when enabled) mesh_detail's
/// replay alike.
/// pass and every shell pipeline.
/// </summary>
[Theory]
[InlineData(WbRenderPass.Opaque)]
@ -242,7 +239,7 @@ public class EnvCellRendererTests
}
[Fact]
public void RetailDetailPipelinesPreserveOpaqueAndTransparentDepthWriteContracts()
public void DetailUsesOnlyTheOriginalFourShellPipelines()
{
using var device = new RecordingGpuDevice();
using var meshManager = CreateMeshManager(device);
@ -253,23 +250,9 @@ public class EnvCellRendererTests
meshManager,
new WbFrustum());
GpuPipelineDescription opaqueDetail = Assert.Single(
device.CreatedPipelines,
pipeline => pipeline.Description.Name == "envcell-retail-detail")
.Description;
GpuPipelineDescription transparentDetail = Assert.Single(
device.CreatedPipelines,
pipeline => pipeline.Description.Name == "envcell-retail-detail-alpha")
.Description;
Assert.Equal(GpuBlendMode.RetailDetail, opaqueDetail.Blend);
Assert.True(opaqueDetail.Depth.Write);
Assert.Equal(GpuCompareOp.Equal, opaqueDetail.Depth.Compare);
Assert.False(opaqueDetail.AlphaToCoverage);
Assert.Equal(GpuBlendMode.RetailDetail, transparentDetail.Blend);
Assert.False(transparentDetail.Depth.Write);
Assert.Equal(GpuCompareOp.LessOrEqual, transparentDetail.Depth.Compare);
Assert.False(transparentDetail.AlphaToCoverage);
Assert.Equal(4, device.CreatedPipelines.Count);
Assert.DoesNotContain(device.CreatedPipelines,
pipeline => pipeline.Description.Name.Contains("detail", StringComparison.Ordinal));
}
[Fact]

View file

@ -20,7 +20,7 @@ namespace AcDream.App.Tests.Rendering.Wb;
///
/// Layout under test (std430, 16 bytes total):
/// offset 0 : uint TextureTableIndex
/// offset 4 : uint Reserved (pad)
/// offset 4 : float SurfaceOpacity
/// offset 8 : uint TextureIndex (layer within the texture array)
/// offset 12 : uint Flags
/// </summary>
@ -36,7 +36,7 @@ public class ModernBatchDataLayoutTests
public void FieldOffsets_MatchStd430Layout()
{
Assert.Equal(0, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.TextureTableIndex)));
Assert.Equal(4, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.Reserved)));
Assert.Equal(4, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.SurfaceOpacity)));
Assert.Equal(8, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.TextureIndex)));
Assert.Equal(12, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.Flags)));
}
@ -47,11 +47,12 @@ public class ModernBatchDataLayoutTests
var data = new ModernBatchData
{
TextureTableIndex = 7u,
SurfaceOpacity = 0.25f,
TextureIndex = 3u,
};
Assert.Equal(7u, data.TextureTableIndex);
Assert.Equal(0u, data.Reserved);
Assert.Equal(0.25f, data.SurfaceOpacity);
Assert.Equal(3u, data.TextureIndex);
Assert.Equal(0u, data.Flags);
}