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:
parent
13dbb79eeb
commit
75664805f8
61 changed files with 1003 additions and 1016 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue