acdream/tests/AcDream.App.Tests/Rendering/Packs/AtmosphericShaderAbiTests.cs
Erik 75664805f8 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.
2026-09-05 02:03:13 +02:00

216 lines
9.6 KiB
C#

using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Packs;
using AcDream.Plugin.Abstractions.Rendering;
namespace AcDream.App.Tests.Rendering.Packs;
public sealed class AtmosphericShaderAbiTests
{
[Fact]
public void HostStructsMatchTheCheckedInStd140AtmosphericAbi()
{
Assert.Equal(192, Marshal.SizeOf<AtmosphericFrameUniforms>());
Assert.Equal(0, Offset<AtmosphericFrameUniforms>(nameof(AtmosphericFrameUniforms.SunScreen)));
Assert.Equal(16, Offset<AtmosphericFrameUniforms>(nameof(AtmosphericFrameUniforms.SunColor)));
Assert.Equal(32, Offset<AtmosphericFrameUniforms>(nameof(AtmosphericFrameUniforms.Viewport)));
Assert.Equal(48, Offset<AtmosphericFrameUniforms>(nameof(AtmosphericFrameUniforms.Weather)));
Assert.Equal(64, Offset<AtmosphericFrameUniforms>(nameof(AtmosphericFrameUniforms.SunDirection)));
Assert.Equal(80, Offset<AtmosphericFrameUniforms>(nameof(AtmosphericFrameUniforms.Policy)));
Assert.Equal(96, Offset<AtmosphericFrameUniforms>(nameof(AtmosphericFrameUniforms.InverseViewProjection)));
// Campaign VM VM6: ABI v2 appends the foliage-wind clock/weather and
// amplitude members at 160/176, growing the block from v1's 160
// bytes to 192.
Assert.Equal(160, Offset<AtmosphericFrameUniforms>(nameof(AtmosphericFrameUniforms.ClockWind)));
Assert.Equal(176, Offset<AtmosphericFrameUniforms>(nameof(AtmosphericFrameUniforms.WindAmplitude)));
Assert.Equal(160, RenderPackShaderAbi.AtmosphericFrameSizeBytesV1);
Assert.Equal(192, RenderPackShaderAbi.AtmosphericFrameSizeBytesV2);
Assert.Equal(2, RenderPackShaderAbi.ShaderAbiVersion);
Assert.Equal(64, Marshal.SizeOf<AtmosphericPackPassUniforms>());
Assert.Equal(0, Offset<AtmosphericPackPassUniforms>(nameof(AtmosphericPackPassUniforms.Params0)));
Assert.Equal(16, Offset<AtmosphericPackPassUniforms>(nameof(AtmosphericPackPassUniforms.Params1)));
Assert.Equal(32, Offset<AtmosphericPackPassUniforms>(nameof(AtmosphericPackPassUniforms.Params2)));
Assert.Equal(48, Offset<AtmosphericPackPassUniforms>(nameof(AtmosphericPackPassUniforms.Params3)));
Assert.Equal(256, Marshal.SizeOf<PackSettingsUniforms>());
Assert.Equal(5u, GpuBindingModel.UniformAtmosphericFrame);
Assert.Equal(6u, GpuBindingModel.UniformDirectionalShadow);
Assert.Equal(7u, GpuBindingModel.UniformPackPass);
Assert.Equal(8u, GpuBindingModel.UniformPackSettings);
Assert.Equal(5, VulkanFrameBindings.UniformBindingCount);
Assert.Equal(
[1u, 2u, 3u, 4u],
VulkanPipelineLayouts.DeclaredUniformBindings);
Assert.Equal(3, RenderPackShaderAbi.UniformDescriptorSet);
Assert.Equal(3u, GpuBindingModel.RenderPackUniformSet);
Assert.False(VulkanPipelineLayouts.IsDeclaredUniformBinding(5));
Assert.True(VulkanPipelineLayouts.IsDeclaredPackUniformBinding(5));
Assert.True(VulkanPipelineLayouts.IsDeclaredPackUniformBinding(8));
Assert.Equal(RenderPackShaderAbi.AtmosphericFrameBinding, (int)GpuBindingModel.UniformAtmosphericFrame);
Assert.Equal(RenderPackShaderAbi.AtmosphericFrameSizeBytes, AtmosphericFrameUniforms.SizeInBytes);
Assert.Equal(RenderPackShaderAbi.DirectionalShadowBinding, (int)GpuBindingModel.UniformDirectionalShadow);
Assert.Equal(RenderPackShaderAbi.PackPassBinding, (int)GpuBindingModel.UniformPackPass);
Assert.Equal(RenderPackShaderAbi.PackPassSizeBytes, AtmosphericPackPassUniforms.SizeInBytes);
Assert.Equal(RenderPackShaderAbi.PackSettingsBinding, (int)GpuBindingModel.UniformPackSettings);
Assert.Equal(RenderPackShaderAbi.PackSettingsSizeBytes, PackSettingsUniforms.SizeInBytes);
Assert.Equal(RenderPackShaderAbi.PushConstantSizeBytes, GpuBindingModel.PushConstantBytes);
}
[Fact]
public void CheckedInCommonIncludeNamesTheSameBindingsAndMemberOrder()
{
string text = File.ReadAllText(Path.Combine(
RepositoryRoot(),
"src",
"AcDream.App",
"Rendering",
"Shaders",
"atmospheric_common.glsl"));
AssertOrdered(text,
"ACDREAM_PACK_UBO_SET binding = 5",
"uAtmosphereSunScreen",
"uAtmosphereSunColor",
"uAtmosphereViewport",
"uAtmosphereWeather",
"uAtmosphereSunDirection",
"uAtmospherePolicy",
"uAtmosphereInverseViewProjection",
"uAtmosphereClockWind",
"uAtmosphereWindAmplitude",
"binding = 7",
"uPackParams0",
"uPackParams1",
"uPackParams2",
"uPackParams3",
"FusedAtmosphericPostProcess PackPass ABI",
"binding = 8",
"uPackSettings[16]");
}
[Theory]
[InlineData("mesh_atmospheric.vert")]
[InlineData("directional_shadow_world_opaque.vert")]
[InlineData("directional_shadow_world_opaque_multiview.vert")]
[InlineData("directional_shadow_world_cutout.vert")]
[InlineData("directional_shadow_world_cutout_multiview.vert")]
public void FoliageWindShadersEachCallAcdreamFoliageDisplaceExactlyOnce(string fileName)
{
string text = File.ReadAllText(Path.Combine(
RepositoryRoot(), "src", "AcDream.App", "Rendering", "Shaders", fileName));
Assert.Contains("#include \"foliage_wind.glsl\"", text, StringComparison.Ordinal);
int calls = CountOccurrences(text, "acdreamFoliageDisplace(");
Assert.Equal(1, calls);
}
[Theory]
[InlineData("mesh_modern.vert")]
[InlineData("terrain_modern.vert")]
[InlineData("terrain_atmospheric.vert")]
[InlineData("directional_shadow_terrain.vert")]
[InlineData("directional_shadow_terrain_multiview.vert")]
public void RetailAndTerrainShadersNeverCallAcdreamFoliageDisplace(string fileName)
{
string text = File.ReadAllText(Path.Combine(
RepositoryRoot(), "src", "AcDream.App", "Rendering", "Shaders", fileName));
Assert.DoesNotContain("foliage_wind.glsl", text, StringComparison.Ordinal);
Assert.DoesNotContain("acdreamFoliageDisplace(", text, StringComparison.Ordinal);
}
private static int CountOccurrences(string text, string token)
{
int count = 0;
int index = 0;
while ((index = text.IndexOf(token, index, StringComparison.Ordinal)) >= 0)
{
count++;
index += token.Length;
}
return count;
}
[Fact]
public void FusedLowShadersRetainTheDeclaredOcclusionAndBloomPixelKernels()
{
string shaderRoot = Path.Combine(
RepositoryRoot(),
"src",
"AcDream.App",
"Rendering",
"Shaders");
string occlusion = File.ReadAllText(Path.Combine(
shaderRoot,
"atmospheric_sun_occlusion.frag"));
string rays = File.ReadAllText(Path.Combine(
shaderRoot,
"atmospheric_sun_rays.frag"));
string blur = File.ReadAllText(Path.Combine(
shaderRoot,
"atmospheric_bloom_blur.frag"));
string downsample = File.ReadAllText(Path.Combine(
shaderRoot,
"atmospheric_bloom_downsample.frag"));
string filmic = File.ReadAllText(Path.Combine(
shaderRoot,
"atmospheric_filmic.frag"));
foreach (string threshold in (string[])["0.9975", "0.99995"])
{
Assert.Contains(threshold, occlusion, StringComparison.Ordinal);
Assert.Contains(threshold, rays, StringComparison.Ordinal);
}
foreach (string kernel in (string[])
["0.227027", "0.316216", "1.384615", "0.070270", "3.230769"])
{
Assert.Contains(kernel, blur, StringComparison.Ordinal);
Assert.Contains(kernel, filmic, StringComparison.Ordinal);
}
Assert.Contains("round(clamp(unobstructedSky * enabled", rays,
StringComparison.Ordinal);
Assert.Contains("uPackParams1.z > 0.5", filmic,
StringComparison.Ordinal);
Assert.Contains("brightness - threshold + knee", filmic,
StringComparison.Ordinal);
foreach (string extraction in (string[])
["0.2126", "0.7152", "0.0722", "brightness - threshold + knee"])
{
Assert.Contains(extraction, downsample, StringComparison.Ordinal);
Assert.Contains(extraction, filmic, StringComparison.Ordinal);
}
const double oneDimensionalWeight =
0.227027 + (2 * 0.316216) + (2 * 0.070270);
Assert.InRange(
oneDimensionalWeight * oneDimensionalWeight,
0.99999,
1.00001);
}
private static int Offset<T>(string field) where T : struct =>
Marshal.OffsetOf<T>(field).ToInt32();
private static void AssertOrdered(string text, params string[] tokens)
{
int prior = -1;
foreach (string token in tokens)
{
int next = text.IndexOf(token, prior + 1, StringComparison.Ordinal);
Assert.True(next > prior, $"'{token}' is missing or out of ABI order.");
prior = next;
}
}
private static string RepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null
&& !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
directory = directory.Parent;
return directory?.FullName
?? throw new InvalidOperationException("Could not locate repository root.");
}
}