fix(rendering): carry retail SetSurface state to detail draws

Resolve exact SetSurface blend, alpha-test, and fog state once during extraction and preserve it through recipe-10 prepared payloads, Wb/EnvCell command data, Vulkan pipelines, push constants, and both ordinary/atmospheric one-pass shaders. Preserve AP-240 Wb pure-Clip immediate opaque/A2C while EnvCell uses retail premultiplied Clip; detail-off routing remains unchanged. Correct AP-232 and register the remaining detail-off state divergence.

Mutation first failures (all restored):
- raw Add->SRCALPHA/ONE: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState line 1278, expected wb-mesh-raw-additive-1x.
- inverse-add->alpha-add: same test line 1278, expected wb-mesh-inverse-additive-1x.
- remove Env inverse: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 132, expected envcell-inverse.
- raw IsAdditive precedence: same test line 132, Translucent|Clip|Additive expected envcell-alpha.
- Wb paletted ParamB=0: OrderPreservingSubmitterTests line 333, expected 0.392156869.
- Wb DDS ParamB=0.05: same test line 333, expected 0.784313738.
- disable Alpha+Clip test: WalkStaticStreamPopulatorTests line 1286, expected 0.784313738.
- always fog raw Add: same test line 1287, expected no-fog true.
- disable fog non-Add: same test line 1287, expected no-fog false.
- X=a*qA: RetailDetailTextureContractTests line 261, shared squared-alpha substring absent.
- X includes base alpha: same test line 262, forbidden baseTexel.a present.
- CLIP uses 0.05: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 367.
- second detail draw: EnvCell detail-on line 131, collection contained 2 draws.
- straight-alpha substitute: Wb immediate line 1278, expected wb-mesh-additive-1x.
- omit ordered detail arm: OrderPreservingSubmitterTests line 318, expected (77,3.5), got (0,0).
- omit atmospheric combine: RetailDetailTextureContractTests line 260, shared include absent.
- drop serialized opacity: ObjectMeshDataSerializerTests line 292, expected opacity bits, got 1.0.
- stale detail arm: atmospheric adjacency line 402, expected slot 0, got 77.
- per-frame surface map: EnvCell warmed allocation line 285, expected 0 B, got 204800 B.
This commit is contained in:
Erik 2026-09-05 00:56:52 +02:00
parent 75664805f8
commit 15ed57a1e7
44 changed files with 1154 additions and 187 deletions

View file

@ -8,7 +8,9 @@ using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Wb;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.Content;
using AcDream.Core.Meshing;
using Chorizite.Core.Render.Enums;
using DatReaderWriter.Enums;
using Microsoft.Extensions.Logging.Abstractions;
using CullMode = DatReaderWriter.Enums.CullMode;
@ -21,6 +23,11 @@ namespace AcDream.App.Tests.Rendering;
/// </summary>
public sealed class EnvCellAlphaDrawSourceTests
{
private static readonly Vector4 MaterialBase = new(0.31f, 0.57f, 0.83f, 0.19f);
private static readonly Vector3 MaterialDiffuse = new(0.73f, 0.41f, 0.67f);
private static readonly Vector4 MaterialDetail = new(0.91f, 0.23f, 0.49f, 0.62f);
private static readonly Vector4 MaterialDestination = new(0.17f, 0.37f, 0.71f, 0.29f);
/// <summary>
/// Canonical F4180104 surface 08000BFF's pure Base1ClipMap mask 0x08
/// reaches CLIP while alpha-family 0x02 reaches ALPHA. Mutation check:
@ -75,26 +82,45 @@ public sealed class EnvCellAlphaDrawSourceTests
DrawPipelineNames(fixture.Device));
}
[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)
public static TheoryData<SurfaceType, bool, string, object, float, bool> ExactMaterialRows() => new()
{
BatchSpec spec = specIndex switch
{
0 => BatchSpec.Opaque,
1 => BatchSpec.Alpha,
2 => BatchSpec.Additive,
3 => BatchSpec.ClipDds,
4 => BatchSpec.ClipPaletted,
_ => throw new ArgumentOutOfRangeException(nameof(specIndex)),
};
{ SurfaceType.Base1Image, false, "envcell-opaque", GpuBlendMode.None, 0f, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive, false, "envcell-additive", GpuBlendMode.Additive, 0f, false },
{ SurfaceType.Base1Image | SurfaceType.Additive, false, "envcell-raw-additive", GpuBlendMode.RawAdditive, 0f, false },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha, false, "envcell-inverse", GpuBlendMode.InverseAlpha, 0f, true },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive, false, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, 0f, false },
{ SurfaceType.Translucent, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, true },
{ SurfaceType.Translucent | SurfaceType.Additive, false, "envcell-raw-additive", GpuBlendMode.RawAdditive, 0f, false },
{ SurfaceType.Translucent | SurfaceType.InvAlpha, false, "envcell-inverse", GpuBlendMode.InverseAlpha, 0f, true },
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, false, "envcell-clip", GpuBlendMode.PremultipliedAlpha, 200f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, true, "envcell-clip", GpuBlendMode.PremultipliedAlpha, 100f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Base1ClipMap, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 200f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, "envcell-alpha", GpuBlendMode.StraightAlpha, 100f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "envcell-additive", GpuBlendMode.Additive, 200f / 255f, false },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "envcell-additive", GpuBlendMode.Additive, 100f / 255f, false },
{ SurfaceType.Base1Image | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "envcell-raw-additive", GpuBlendMode.RawAdditive, 200f / 255f, false },
{ SurfaceType.Base1Image | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "envcell-raw-additive", GpuBlendMode.RawAdditive, 100f / 255f, false },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, false, "envcell-inverse", GpuBlendMode.InverseAlpha, 200f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, true, "envcell-inverse", GpuBlendMode.InverseAlpha, 100f / 255f, true },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, 200f / 255f, false },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "envcell-inverse-additive", GpuBlendMode.InverseAdditive, 100f / 255f, false },
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, false, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, false },
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, "envcell-alpha", GpuBlendMode.StraightAlpha, 0f, false },
};
[Theory]
[MemberData(nameof(ExactMaterialRows))]
public void DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity(
SurfaceType surfaceType,
bool paletted,
string expectedPipeline,
object expectedBlendValue,
float expectedReference,
bool expectedFog)
{
var expectedBlend = (GpuBlendMode)expectedBlendValue;
BatchSpec spec = new(surfaceType, paletted, PositiveStippling: false);
using var fixture = new ProductionEnvCellFixture(
detailSurfaceActive: true,
spec);
@ -113,6 +139,20 @@ public sealed class EnvCellAlphaDrawSourceTests
DrawPushConstants(fixture.Device),
constants => constants.ParamA != 0f);
Assert.NotEqual(0u, armed.TextureIndexA);
Assert.Equal(
!expectedFog,
(armed.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag) != 0);
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
surfaceType, texturePresent: true, textureHasPalette: paletted);
GpuPipelineDescription selected = fixture.Device.CreatedPipelines
.Single(pipeline => pipeline.Description.Name == expectedPipeline)
.Description;
Assert.Equal(expectedBlend, selected.Blend);
Assert.True(selected.Depth.Test);
Assert.Equal(
resolved.Blend is RetailSetSurfaceBlend.Opaque or RetailSetSurfaceBlend.Clip,
selected.Depth.Write);
Assert.Equal(WorldDepthContract.WorldCompare, selected.Depth.Compare);
Assert.Equal(0, fixture.Queue.PendingCount);
GpuRecordedStorageBind batchBind = fixture.Device.Calls
.OfType<GpuRecordedStorageBind>()
@ -120,10 +160,77 @@ public sealed class EnvCellAlphaDrawSourceTests
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);
Vector4 source = RetailDetailTextureContract.Combine(
MaterialBase, MaterialDiffuse, MaterialDetail, 0.75f, 0.4f);
AssertVector(new Vector4(0.3534682f, 0.2330118f, 0.5438054f, 0.11532f), source);
RetailDetailTextureContract.FramebufferFamily family = expectedBlend switch
{
GpuBlendMode.None => RetailDetailTextureContract.FramebufferFamily.Opaque,
GpuBlendMode.StraightAlpha => RetailDetailTextureContract.FramebufferFamily.Alpha,
GpuBlendMode.Additive => RetailDetailTextureContract.FramebufferFamily.AlphaAdditive,
GpuBlendMode.RawAdditive => RetailDetailTextureContract.FramebufferFamily.Additive,
GpuBlendMode.InverseAlpha => RetailDetailTextureContract.FramebufferFamily.InverseAlpha,
GpuBlendMode.InverseAdditive => RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive,
GpuBlendMode.PremultipliedAlpha => RetailDetailTextureContract.FramebufferFamily.Clip,
_ => throw new ArgumentOutOfRangeException(nameof(expectedBlend)),
};
AssertVector(
IndependentComposite(source, MaterialDestination, family),
RetailDetailTextureContract.Composite(source, MaterialDestination, family));
if (resolved.AlphaTestEnabled)
{
Assert.False(RetailDetailTextureContract.SurvivesClip(
MathF.BitDecrement(expectedReference), expectedReference));
Assert.True(RetailDetailTextureContract.SurvivesClip(expectedReference, expectedReference));
Assert.True(RetailDetailTextureContract.SurvivesClip(
MathF.BitIncrement(expectedReference), expectedReference));
}
fixture.Queue.EndFrame();
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
}
public static TheoryData<SurfaceType, bool, string, float> DetailOffRows() => new()
{
{ SurfaceType.Base1Image, false, "envcell-opaque", 0f },
{ SurfaceType.Alpha, false, "envcell-alpha", 0f },
{ SurfaceType.Alpha | SurfaceType.Additive, false, "envcell-additive", 0f },
{ SurfaceType.Additive, false, "envcell-additive", 0f },
{ SurfaceType.InvAlpha, false, "envcell-alpha", 0f },
{ SurfaceType.InvAlpha | SurfaceType.Additive, false, "envcell-additive", 0f },
{ SurfaceType.Base1ClipMap, false, "envcell-clip", 200f / 255f },
{ SurfaceType.Base1ClipMap, true, "envcell-clip", 100f / 255f },
{ SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, "envcell-alpha", 0f },
{ SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, false, "envcell-alpha", 0f },
{ SurfaceType.Translucent, false, "envcell-alpha", 0f },
{ SurfaceType.Translucent | SurfaceType.Additive, false, "envcell-additive", 0f },
{ SurfaceType.Translucent | SurfaceType.InvAlpha, false, "envcell-alpha", 0f },
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, "envcell-additive", 0f },
};
[Theory]
[MemberData(nameof(DetailOffRows))]
public void DetailOff_EveryRawStateRetainsThePreFixLogicalPath(
SurfaceType surfaceType,
bool paletted,
string expectedPipeline,
float expectedReference)
{
using var fixture = new ProductionEnvCellFixture(
detailSurfaceActive: false,
new BatchSpec(surfaceType, paletted, PositiveStippling: false));
fixture.Queue.BeginFrame();
fixture.Leaf.DrawCellShell(ProductionEnvCellFixture.CellId);
fixture.Queue.EndFrame();
Assert.Single(fixture.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.Equal([expectedPipeline], DrawPipelineNames(fixture.Device));
GpuPushConstants constants = Assert.Single(DrawPushConstants(fixture.Device));
Assert.Equal(0u, constants.TextureIndexA);
Assert.Equal(0f, constants.ParamA);
Assert.Equal(expectedReference, constants.ParamB);
Assert.Equal(0, constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
}
/// <summary>Two cell tokens from the SAME <see cref="RetailPViewPassExecutor.EnvCellAlphaDrawSource"/>,
/// with an unrelated source's entry appended between them, must still
/// produce TWO separate single-cell draw calls around the interposed
@ -258,9 +365,10 @@ public sealed class EnvCellAlphaDrawSourceTests
foreach (string shader in new[] { modern, atmospheric })
{
Assert.Contains(
"if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff)",
"? isRetailClipReference(uParamB) && alpha < uParamB",
shader,
StringComparison.Ordinal);
Assert.Contains("(uRenderPass & 0x200) == 0", 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);
@ -409,6 +517,38 @@ public sealed class EnvCellAlphaDrawSourceTests
return [.. constants];
}
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 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)),
};
}
private static string RepositoryRoot()
{
DirectoryInfo? cursor = new(AppContext.BaseDirectory);
@ -419,46 +559,32 @@ public sealed class EnvCellAlphaDrawSourceTests
}
private readonly record struct BatchSpec(
byte Mask,
bool IsTransparent,
AcDream.Core.Meshing.TranslucencyKind Translucency,
uint PaletteId)
SurfaceType Type,
bool Paletted,
bool PositiveStippling)
{
internal static BatchSpec Opaque { get; } = new(
0x00,
IsTransparent: false,
AcDream.Core.Meshing.TranslucencyKind.Opaque,
PaletteId: 0);
SurfaceType.Base1Image, Paletted: false, PositiveStippling: false);
internal static BatchSpec ClipDds { get; } = new(
RetailAlphaMeshRouter.MaskClipMap,
IsTransparent: true,
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
PaletteId: 0);
SurfaceType.Base1Image | SurfaceType.Base1ClipMap,
Paletted: false, PositiveStippling: false);
internal static BatchSpec ClipPaletted { get; } = new(
RetailAlphaMeshRouter.MaskClipMap,
IsTransparent: true,
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
PaletteId: 0x04000001u);
SurfaceType.Base1Image | SurfaceType.Base1ClipMap,
Paletted: true, PositiveStippling: false);
internal static BatchSpec ClipPositiveStippleDds { get; } = new(
RetailAlphaMeshRouter.MaskClipMap | RetailAlphaMeshRouter.MaskPositiveStipple,
IsTransparent: true,
AcDream.Core.Meshing.TranslucencyKind.ClipMap,
PaletteId: 0);
SurfaceType.Base1Image | SurfaceType.Base1ClipMap,
Paletted: false, PositiveStippling: true);
internal static BatchSpec Alpha { get; } = new(
RetailAlphaMeshRouter.MaskAlphaFamily,
IsTransparent: true,
AcDream.Core.Meshing.TranslucencyKind.AlphaBlend,
PaletteId: 0);
SurfaceType.Base1Image | SurfaceType.Alpha,
Paletted: false, PositiveStippling: false);
internal static BatchSpec Additive { get; } = new(
RetailAlphaMeshRouter.MaskAlphaFamily,
IsTransparent: true,
AcDream.Core.Meshing.TranslucencyKind.Additive,
PaletteId: 0);
SurfaceType.Base1Image | SurfaceType.Additive,
Paletted: false, PositiveStippling: false);
}
/// <summary>
@ -512,27 +638,45 @@ public sealed class EnvCellAlphaDrawSourceTests
for (int i = 0; i < specs.Length; i++)
{
BatchSpec spec = specs[i];
TranslucencyKind translucency =
TranslucencyKindExtensions.FromSurfaceType(spec.Type);
bool alphaFamily = (spec.Type
& (SurfaceType.Alpha | SurfaceType.InvAlpha | SurfaceType.Additive)) != 0;
byte mask = RetailAlphaMeshRouter.ConstructSubsetMask(
alphaFamily,
(spec.Type & SurfaceType.Base1ClipMap) != 0,
(spec.Type & SurfaceType.Translucent) != 0,
spec.PositiveStippling);
uint paletteId = spec.Paletted ? 0x04000001u : 0u;
batches.Add(new TextureBatchData
{
Key = new TextureKey
{
SurfaceId = 0x08000BFFu + (uint)i,
PaletteId = spec.PaletteId,
PaletteId = paletteId,
},
TextureData = new byte[8 * 8 * 4],
Indices = [0, 1, 2],
IsTransparent = spec.IsTransparent,
IsAdditive = spec.Translucency == AcDream.Core.Meshing.TranslucencyKind.Additive,
Translucency = spec.Translucency,
IsTransparent = translucency != TranslucencyKind.Opaque,
IsAdditive = (spec.Type & SurfaceType.Additive) != 0,
Translucency = translucency,
MaterialState = RetailSetSurfaceMaterialState.Resolve(
spec.Type,
texturePresent: true,
textureHasPalette: spec.Paletted),
SurfaceOpacity = 0.25f,
RetailSurfaceMask = spec.Mask,
RetailSurfaceMask = mask,
CullMode = CullMode.Clockwise,
IsCellShell = true,
SourceSurfaceIndex = i,
});
}
mesh.TextureBatches[(8, 8, TextureFormat.RGBA8)] = batches;
Assert.NotNull(_meshManager.UploadMeshData(mesh));
ObjectRenderData uploaded = Assert.IsType<ObjectRenderData>(
_meshManager.UploadMeshData(mesh));
Assert.Equal(
batches.Select(static batch => batch.MaterialState),
uploaded.Batches.Select(static batch => batch.MaterialState));
Renderer = new EnvCellRenderer(
Device,

View file

@ -80,12 +80,14 @@ public sealed class GpuContractTests
// pure Base1ClipMap with override=false (SetSurface @0x0059c72a), while
// detail is now computed inside those same pipelines. Collapsing any of
// these modes changes the corresponding subset's composition.
Assert.Equal(5, Enum.GetValues<GpuBlendMode>().Length);
Assert.Equal(7, 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.RawAdditive, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
Assert.Contains(GpuBlendMode.InverseAdditive, Enum.GetValues<GpuBlendMode>());
}
[Fact]

View file

@ -45,7 +45,9 @@ public sealed class VulkanShaderManifestTests
// 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",
// Fix round 1 additionally carries the exact SetSurface alpha-test
// reference and raw-additive fog suppression into this same pass.
["mesh_modern.frag.spv"] = "bd47fe8a33e0f1d025fe48fd95b034ba6fe591a6d20e88e636c86238d8773db1",
// 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.

View file

@ -143,6 +143,12 @@ public sealed class VulkanViewportMappingTests
Assert.Equal(
(BlendFactor.SrcAlpha, BlendFactor.One),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.Additive));
Assert.Equal(
(BlendFactor.One, BlendFactor.One),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.RawAdditive));
Assert.Equal(
(BlendFactor.OneMinusSrcAlpha, BlendFactor.One),
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.InverseAdditive));
// Retail's third mode, found at slice V4c. Mapping it onto straight
// alpha would have silently changed how every inverse-alpha surface
// composites.

View file

@ -14,6 +14,45 @@ public sealed class RetailDetailTextureContractTests
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);
public static TheoryData<SurfaceType, bool, RetailSetSurfaceBlend,
RetailSetSurfaceAlphaTest, bool> ResolvedRows() => new()
{
{ SurfaceType.Base1Image, false, RetailSetSurfaceBlend.Opaque, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Alpha, false, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Alpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.AlphaAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Additive, false, RetailSetSurfaceBlend.Additive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.InvAlpha, false, RetailSetSurfaceBlend.InverseAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.InvAlpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.InverseAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Base1ClipMap, false, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Dds, true },
{ SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Paletted, true },
{ SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Paletted, true },
{ SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, RetailSetSurfaceBlend.InverseAdditive, RetailSetSurfaceAlphaTest.Dds, false },
{ SurfaceType.Translucent, false, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Translucent | SurfaceType.Additive, false, RetailSetSurfaceBlend.Additive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Translucent | SurfaceType.InvAlpha, false, RetailSetSurfaceBlend.InverseAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, false },
};
[Theory]
[MemberData(nameof(ResolvedRows))]
public void SetSurfaceResolver_PreservesEveryRawFamilyClipAndLateOverride(
SurfaceType type,
bool paletted,
RetailSetSurfaceBlend expectedBlend,
RetailSetSurfaceAlphaTest expectedAlphaTest,
bool expectedFog)
{
RetailSetSurfaceMaterialState state = RetailSetSurfaceMaterialState.Resolve(
type,
texturePresent: true,
textureHasPalette: paletted);
Assert.Equal(expectedBlend, state.Blend);
Assert.Equal(expectedAlphaTest, state.AlphaTest);
Assert.Equal(expectedFog, state.FogEnabled);
Assert.Equal(state, RetailSetSurfaceMaterialState.FromPackedByte(state.ToPackedByte()));
}
public static TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float> PolicyRows()
{
var rows = new TheoryData<string, SurfaceType, bool, bool, TranslucencyKind, int, float>();
@ -65,27 +104,22 @@ public sealed class RetailDetailTextureContractTests
Assert.Equal(["source-subset"], PhysicalDrawSequence(detailEnabled));
PipelineState state = ExpectedPipelineState(
consumer, expectedKind, paletted, expectedReference);
Assert.Equal(expectedReference, state.AlphaReference);
Assert.Equal(
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
consumer, surfaceType, expectedKind, paletted, detailEnabled);
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
surfaceType,
texturePresent: true,
textureHasPalette: paletted);
if (detailEnabled)
{
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(resolved.AlphaTestReference, state.AlphaReference);
Assert.Equal(resolved.AlphaTestEnabled, state.AlphaTest);
Assert.Equal(resolved.FogEnabled, state.FogEnabled);
}
else
{
Assert.Equal(expectedReference, state.AlphaReference);
Assert.True(state.AlphaTest);
}
Assert.Equal(detailEnabled, DetailIsArmed(detailEnabled));
if (consumer == "Building" && expectedKind == TranslucencyKind.ClipMap)
Assert.Equal("AP-240 immediate A2C", state.Placement);
@ -263,11 +297,38 @@ public sealed class RetailDetailTextureContractTests
private static PipelineState ExpectedPipelineState(
string consumer,
SurfaceType surfaceType,
TranslucencyKind kind,
bool paletted,
float reference)
bool detailEnabled)
{
_ = paletted;
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
surfaceType,
texturePresent: true,
textureHasPalette: paletted);
if (detailEnabled)
{
GpuBlendMode exactBlend = resolved.Blend switch
{
RetailSetSurfaceBlend.Opaque => GpuBlendMode.None,
RetailSetSurfaceBlend.StraightAlpha => GpuBlendMode.StraightAlpha,
RetailSetSurfaceBlend.AlphaAdditive => GpuBlendMode.Additive,
RetailSetSurfaceBlend.Additive => GpuBlendMode.RawAdditive,
RetailSetSurfaceBlend.InverseAlpha => GpuBlendMode.InverseAlpha,
RetailSetSurfaceBlend.InverseAdditive => GpuBlendMode.InverseAdditive,
RetailSetSurfaceBlend.Clip => GpuBlendMode.PremultipliedAlpha,
_ => throw new ArgumentOutOfRangeException(),
};
return new PipelineState(
exactBlend,
resolved.Blend is RetailSetSurfaceBlend.Opaque or RetailSetSurfaceBlend.Clip,
resolved.AlphaTestEnabled,
resolved.AlphaTestReference,
resolved.FogEnabled,
consumer == "Building" && kind == TranslucencyKind.ClipMap
? "AP-240 immediate A2C"
: "source FIFO position");
}
GpuBlendMode blend = kind switch
{
TranslucencyKind.Opaque => GpuBlendMode.None,
@ -283,7 +344,10 @@ public sealed class RetailDetailTextureContractTests
blend,
kind is TranslucencyKind.Opaque or TranslucencyKind.ClipMap,
AlphaTest: true,
reference,
kind == TranslucencyKind.ClipMap
? paletted ? 100f / 255f : 200f / 255f
: 0.05f,
FogEnabled: true,
consumer == "Building" && kind == TranslucencyKind.ClipMap
? "AP-240 immediate A2C"
: "source FIFO position");
@ -310,6 +374,7 @@ public sealed class RetailDetailTextureContractTests
bool DepthWrite,
bool AlphaTest,
float AlphaReference,
bool FogEnabled,
string Placement);
private static Vector4 IndependentComposite(

View file

@ -32,7 +32,8 @@ public sealed class OrderPreservingSubmitterTests
WalkDrawStage stage = WalkDrawStage.Terrain,
TranslucencyKind translucency = TranslucencyKind.Opaque,
CullMode cullMode = CullMode.CounterClockwise,
uint detailCategory = 0) =>
uint detailCategory = 0,
RetailSetSurfaceMaterialState? materialState = null) =>
new(
Key: new GroupKey(
FirstIndex: (uint)index * 3,
@ -41,6 +42,7 @@ public sealed class OrderPreservingSubmitterTests
TextureSlot: new GpuTextureSlot((uint)index),
TextureLayer: 0,
Translucency: translucency,
MaterialState: materialState ?? RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0,
CullMode: cullMode),
Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3),
@ -284,8 +286,12 @@ public sealed class OrderPreservingSubmitterTests
Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device));
}
[Fact]
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace()
[Theory]
[InlineData(false, 200f / 255f)]
[InlineData(true, 100f / 255f)]
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace(
bool paletted,
float expectedReference)
{
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true);
using DrawScope draw = fx.BeginDraw();
@ -293,7 +299,14 @@ public sealed class OrderPreservingSubmitterTests
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(1, detailCategory: 1),
MakeCommand(2, translucency: TranslucencyKind.ClipMap, detailCategory: 1),
MakeCommand(
2,
translucency: TranslucencyKind.ClipMap,
detailCategory: 1,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Base1ClipMap,
texturePresent: true,
textureHasPalette: paletted)),
MakeCommand(3));
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
@ -317,8 +330,90 @@ public sealed class OrderPreservingSubmitterTests
.Select(call => call.PipelineName)
.ToArray());
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
Assert.Equal(expectedReference, runs[2].Constants.ParamB);
GpuPipelineDescription a2c = fx.Device.CreatedPipelines
.Single(pipeline => pipeline.Description.Name == "wb-mesh-opaque-a2c-1x")
.Description;
Assert.Equal(GpuBlendMode.None, a2c.Blend);
Assert.True(a2c.AlphaToCoverage);
Assert.True(a2c.Depth.Test);
Assert.True(a2c.Depth.Write);
Assert.Equal(WorldDepthContract.WorldCompare, a2c.Depth.Compare);
}
[Fact]
public void PrepareThenDraw_PureClipBuildingDetailOffRetainsAp240A2cAndNeutralDetailState()
{
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: false);
using DrawScope draw = fx.BeginDraw();
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(
1,
translucency: TranslucencyKind.ClipMap,
detailCategory: 1,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Base1ClipMap,
texturePresent: true,
textureHasPalette: true)),
MakeCommand(2));
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
Assert.Equal([(0, 1), (1, 1), (2, 1)],
runs.Select(run => (run.Start, run.Count)).ToList());
Assert.Equal(0u, runs[1].Constants.TextureIndexA);
Assert.Equal(0f, runs[1].Constants.ParamA);
Assert.Equal(0f, runs[1].Constants.ParamB);
Assert.Equal(0, runs[1].Constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName == "wb-mesh-opaque-a2c-1x");
}
[Fact]
public void AtmosphericReceiver_AdjacentOrdinaryDetailOrdinaryCommandsDoNotLeakExactState()
{
using var fx = new DispatcherFixture(
detailAvailable: true,
detailEnabled: true,
atmospheric: true);
using DrawScope draw = fx.BeginDraw();
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(
1,
translucency: TranslucencyKind.Additive,
detailCategory: 1,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Additive,
texturePresent: true,
textureHasPalette: false)),
MakeCommand(2));
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
Assert.Equal([(0, 1), (1, 1), (2, 1)],
runs.Select(run => (run.Start, run.Count)).ToList());
Assert.Equal((0u, 0f, 0f, 0), DetailState(runs[0].Constants));
Assert.Equal((77u, 3.5f, 0f, RetailDetailTextureContract.NoFogRenderPassFlag),
DetailState(runs[1].Constants));
Assert.Equal((0u, 0f, 0f, 0), DetailState(runs[2].Constants));
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName == "wb-mesh-atmospheric-raw-additive-1x");
GpuPipelineDescription selected = fx.Device.CreatedPipelines
.Single(pipeline => pipeline.Description.Name == "wb-mesh-atmospheric-raw-additive-1x")
.Description;
Assert.Equal("mesh_atmospheric", selected.Shaders.Name);
Assert.Equal(GpuBlendMode.RawAdditive, selected.Blend);
}
private static (uint Slot, float Tiling, float Reference, int NoFog) DetailState(
GpuPushConstants constants) =>
(constants.TextureIndexA, constants.ParamA, constants.ParamB,
constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
[Fact]
public void PrepareThenDraw_OpaqueRunUsesRenderPassZeroAndAlphaBlendRunUsesRenderPassOne()
{
@ -609,7 +704,10 @@ public sealed class OrderPreservingSubmitterTests
private readonly WbMeshAdapter _meshAdapter;
private readonly TextureCache _textures;
public DispatcherFixture(bool detailAvailable = false, bool detailEnabled = false)
public DispatcherFixture(
bool detailAvailable = false,
bool detailEnabled = false,
bool atmospheric = false)
{
Device = new RecordingGpuDevice();
FrameLifetime = new GpuDeviceFrameLifetime(Device);
@ -640,6 +738,15 @@ public sealed class OrderPreservingSubmitterTests
new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16)
: default,
buildingDetailEnabled: () => detailEnabled);
if (atmospheric)
{
var source = new BindableAtmosphericSource();
WbDrawDispatcher.DirectionalShadowReceiverPipelineState candidate =
Assert.IsType<WbDrawDispatcher.DirectionalShadowReceiverPipelineState>(
Dispatcher.PrepareDirectionalShadowReceiver(source, sampleCount: 1));
Assert.Null(Dispatcher.SwapDirectionalShadowReceiver(candidate));
}
Atmospheric = atmospheric;
}
public RecordingGpuDevice Device { get; }
@ -650,6 +757,8 @@ public sealed class OrderPreservingSubmitterTests
public WbDrawDispatcher Dispatcher { get; }
public bool Atmospheric { get; }
/// <summary>Opens a frame and a backbuffer pass, publishes it on
/// <see cref="Scope"/>, then clears the recorded calls so a test only
/// sees what its own <c>SubmitOrderedStream</c> call produced.</summary>
@ -659,7 +768,11 @@ public sealed class OrderPreservingSubmitterTests
IGpuFrame frame = FrameLifetime.CurrentFrame!;
IGpuPassEncoder pass = frame.BeginPass(
GpuPassDescription.BackbufferClear(
"fw2-ordered-stream-test", Vector4.Zero, sampleCount: 1));
Atmospheric
? DirectionalShadowReceiverPolicy.AtmosphericWorldPassName
: "fw2-ordered-stream-test",
Vector4.Zero,
sampleCount: 1));
IDisposable publication = Scope.Publish(pass);
Device.Clear();
return new DrawScope(frame, pass, publication);
@ -674,6 +787,30 @@ public sealed class OrderPreservingSubmitterTests
}
}
private sealed class BindableAtmosphericSource : IDirectionalShadowReceiverSource
{
public DirectionalShadowPipelineShaders PipelineShaders =>
DirectionalShadowPipelineShaders.Local;
public bool TryGetCurrentFrameBinding(
IGpuFrame frame,
out DirectionalShadowFrameBinding binding)
{
GpuRingAllocation allocation = frame.AllocateRing(
checked((int)DirectionalShadowUniforms.SizeInBytes),
GpuRingUsage.Uniform);
binding = new DirectionalShadowFrameBinding(
frame.Serial,
Enabled: false,
allocation.Buffer,
allocation.OffsetBytes,
DirectionalShadowUniforms.SizeInBytes,
GpuTextureSlot.Unassigned,
CascadeCount: 0);
return true;
}
}
private sealed class NullPreparedAssetSource : IPreparedAssetSource
{
public PreparedAssetSourceStats Stats => default;

View file

@ -28,6 +28,7 @@ public sealed class OrderedDrawStreamTests
TextureSlot: new GpuTextureSlot((uint)index),
TextureLayer: 0,
Translucency: translucency,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0,
CullMode: cullMode),
Transform: Matrix4x4.CreateTranslation(index, index * 2, index * 3),

View file

@ -105,11 +105,13 @@ public sealed class WalkStaticStreamPopulatorTests
int indexCount,
uint textureSlotIndex,
uint textureLayer = 0,
CullMode cullMode = CullMode.CounterClockwise) =>
CullMode cullMode = CullMode.CounterClockwise,
RetailSetSurfaceMaterialState? materialState = null) =>
new()
{
Key = new TextureKey { SurfaceId = surfaceId, IsSolid = false },
Translucency = translucency,
MaterialState = materialState ?? RetailSetSurfaceMaterialState.Opaque,
FirstIndex = firstIndex,
BaseVertex = (uint)baseVertex,
IndexCount = indexCount,
@ -316,14 +318,14 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.DoesNotContain(
"one stable far-to-near stream keyed", architecture, StringComparison.OrdinalIgnoreCase);
Assert.Contains("160 active rows", register, StringComparison.Ordinal);
Assert.Contains("active total to **161**", register, StringComparison.Ordinal);
int apSectionStart = register.IndexOf(
"## 3. Documented approximation (AP)", StringComparison.Ordinal);
int apSectionEnd = register.IndexOf(
"## 4. Temporary stopgap (TS)", StringComparison.Ordinal);
Assert.True(apSectionStart >= 0, "AP section heading must exist.");
Assert.True(apSectionEnd > apSectionStart, "TS heading must follow the AP section.");
foreach (string id in new[] { "AP-241", "AP-242", "AP-243" })
foreach (string id in new[] { "AP-241", "AP-242", "AP-243", "AP-244" })
{
int rowIndex = register.IndexOf($"| {id} |", StringComparison.Ordinal);
Assert.True(
@ -337,8 +339,10 @@ public sealed class WalkStaticStreamPopulatorTests
line.StartsWith("| AP-242 |", StringComparison.Ordinal));
Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-243 |", StringComparison.Ordinal));
Assert.Single(File.ReadLines(registerPath), static line =>
line.StartsWith("| AP-244 |", StringComparison.Ordinal));
Assert.Equal(
160,
161,
File.ReadLines(registerPath).Count(static line =>
line.StartsWith("| AP-", StringComparison.Ordinal)));
Assert.Contains("greater than 50 m", register, StringComparison.Ordinal);
@ -417,7 +421,12 @@ public sealed class WalkStaticStreamPopulatorTests
InjectRenderData(fx.Manager, opaqueGfxObj, MakeFlatMesh(
MakeBatch(0x08000001u, TranslucencyKind.Opaque, firstIndex: 0, baseVertex: 0, indexCount: 3, textureSlotIndex: 1)));
InjectRenderData(fx.Manager, alphaGfxObj, MakeFlatMesh(
MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4, indexCount: 6, textureSlotIndex: 2)));
MakeBatch(0x08000002u, TranslucencyKind.AlphaBlend, firstIndex: 3, baseVertex: 4,
indexCount: 6, textureSlotIndex: 2,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Alpha | SurfaceType.Additive,
texturePresent: true,
textureHasPalette: false))));
var meshRefs = new[]
{
@ -448,6 +457,7 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Equal(3u, translucent.Key.FirstIndex);
Assert.Equal(6, translucent.Key.IndexCount);
Assert.Equal(2u, translucent.Key.TextureSlot.Index);
Assert.Equal(RetailSetSurfaceBlend.AlphaAdditive, translucent.Key.MaterialState.Blend);
Assert.Equal(meshRefs[1].PartTransform * record.Transform.LocalToWorld, translucent.Transform);
Assert.Equal(2, selectionParts.Count);
@ -1095,7 +1105,8 @@ public sealed class WalkStaticStreamPopulatorTests
using var fx = new DispatcherFixture(withAlphaQueue: true);
fx.AlphaQueue!.BeginFrame();
var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
var key = new GroupKey(10, 2, 6, new GpuTextureSlot(3), 1, TranslucencyKind.AlphaBlend,
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0);
Vector3 localSortCenter = new(1, 2, 3);
Matrix4x4 model = Matrix4x4.CreateTranslation(4, 5, 6);
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
@ -1221,13 +1232,35 @@ public sealed class WalkStaticStreamPopulatorTests
}
[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)
[InlineData(SurfaceType.Alpha, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, true)]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive, false, "wb-mesh-additive-1x", GpuBlendMode.Additive, 0f, false)]
[InlineData(SurfaceType.Additive, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 0f, false)]
[InlineData(SurfaceType.InvAlpha, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 0f, true)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive, false, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 0f, false)]
[InlineData(SurfaceType.Translucent, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, true)]
[InlineData(SurfaceType.Translucent | SurfaceType.Additive, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 0f, false)]
[InlineData(SurfaceType.Translucent | SurfaceType.InvAlpha, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 0f, true)]
[InlineData(SurfaceType.Alpha | SurfaceType.Base1ClipMap, false, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 200f / 255f, true)]
[InlineData(SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 100f / 255f, true)]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-additive-1x", GpuBlendMode.Additive, 200f / 255f, false)]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-additive-1x", GpuBlendMode.Additive, 100f / 255f, false)]
[InlineData(SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 200f / 255f, false)]
[InlineData(SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-raw-additive-1x", GpuBlendMode.RawAdditive, 100f / 255f, false)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, false, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 200f / 255f, true)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, true, "wb-mesh-inverse-1x", GpuBlendMode.InverseAlpha, 100f / 255f, true)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, false, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 200f / 255f, false)]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, true, "wb-mesh-inverse-additive-1x", GpuBlendMode.InverseAdditive, 100f / 255f, false)]
[InlineData(SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive,
true, "wb-mesh-alpha-1x", GpuBlendMode.StraightAlpha, 0f, false)]
public void ImmediateBuildingDetail_UsesExactResolvedSetSurfaceState(
SurfaceType type,
bool paletted,
string expectedPipeline,
object expectedBlendValue,
float expectedReference,
bool expectedFog)
{
var expectedBlend = (GpuBlendMode)expectedBlendValue;
using var fx = new DispatcherFixture(
withAlphaQueue: true,
detailAvailable: true,
@ -1235,7 +1268,8 @@ public sealed class WalkStaticStreamPopulatorTests
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(
detailCategory: 1u,
translucency: kind);
surfaceType: type,
paletted: paletted);
fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity);
@ -1243,11 +1277,121 @@ public sealed class WalkStaticStreamPopulatorTests
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName == expectedPipeline);
GpuPushConstants armed = fx.Device.Calls
.OfType<GpuRecordedPushConstants>()
.Last(call => call.Constants.ParamA != 0f)
.Constants;
Assert.Equal(99u, armed.TextureIndexA);
Assert.Equal(2f, armed.ParamA);
Assert.Equal(expectedReference, armed.ParamB);
Assert.Equal(!expectedFog,
(armed.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag) != 0);
RetailSetSurfaceMaterialState resolved = RetailSetSurfaceMaterialState.Resolve(
type, texturePresent: true, textureHasPalette: paletted);
GpuPipelineDescription selected = fx.Device.CreatedPipelines
.Single(pipeline => pipeline.Description.Name == expectedPipeline)
.Description;
Assert.Equal(expectedBlend, selected.Blend);
Assert.True(selected.Depth.Test);
Assert.Equal(
resolved.Blend is RetailSetSurfaceBlend.Opaque or RetailSetSurfaceBlend.Clip,
selected.Depth.Write);
Assert.Equal(WorldDepthContract.WorldCompare, selected.Depth.Compare);
Vector4 source = RetailDetailTextureContract.Combine(
new Vector4(0.31f, 0.57f, 0.83f, 0.19f),
new Vector3(0.73f, 0.41f, 0.67f),
new Vector4(0.91f, 0.23f, 0.49f, 0.62f),
authoredOpacity: 0.75f,
liveOpacity: 0.4f);
Assert.Equal(0.3534682f, source.X, 6);
Assert.Equal(0.2330118f, source.Y, 6);
Assert.Equal(0.5438054f, source.Z, 6);
Assert.Equal(0.11532f, source.W, 6);
Vector4 destination = new(0.17f, 0.37f, 0.71f, 0.29f);
float x = source.W;
RetailDetailTextureContract.FramebufferFamily family = expectedBlend switch
{
GpuBlendMode.StraightAlpha => RetailDetailTextureContract.FramebufferFamily.Alpha,
GpuBlendMode.Additive => RetailDetailTextureContract.FramebufferFamily.AlphaAdditive,
GpuBlendMode.RawAdditive => RetailDetailTextureContract.FramebufferFamily.Additive,
GpuBlendMode.InverseAlpha => RetailDetailTextureContract.FramebufferFamily.InverseAlpha,
GpuBlendMode.InverseAdditive => RetailDetailTextureContract.FramebufferFamily.InverseAlphaAdditive,
_ => throw new ArgumentOutOfRangeException(nameof(expectedBlend)),
};
Vector4 expectedFramebuffer = family switch
{
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,
_ => throw new ArgumentOutOfRangeException(nameof(family)),
};
Vector4 framebuffer = RetailDetailTextureContract.Composite(source, destination, family);
Assert.Equal(expectedFramebuffer.X, framebuffer.X, 6);
Assert.Equal(expectedFramebuffer.Y, framebuffer.Y, 6);
Assert.Equal(expectedFramebuffer.Z, framebuffer.Z, 6);
Assert.Equal(expectedFramebuffer.W, framebuffer.W, 6);
if (resolved.AlphaTestEnabled)
{
Assert.False(RetailDetailTextureContract.SurvivesClip(
MathF.BitDecrement(expectedReference), expectedReference));
Assert.True(RetailDetailTextureContract.SurvivesClip(expectedReference, expectedReference));
Assert.True(RetailDetailTextureContract.SurvivesClip(
MathF.BitIncrement(expectedReference), expectedReference));
}
}
[Theory]
[InlineData(SurfaceType.Alpha, "wb-mesh-alpha-1x")]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.Additive, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.InvAlpha, "wb-mesh-inverse-1x")]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.Alpha | SurfaceType.Base1ClipMap, "wb-mesh-alpha-1x")]
[InlineData(SurfaceType.Alpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.Additive | SurfaceType.Base1ClipMap, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Base1ClipMap, "wb-mesh-inverse-1x")]
[InlineData(SurfaceType.InvAlpha | SurfaceType.Additive | SurfaceType.Base1ClipMap, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.Translucent, "wb-mesh-alpha-1x")]
[InlineData(SurfaceType.Translucent | SurfaceType.Additive, "wb-mesh-additive-1x")]
[InlineData(SurfaceType.Translucent | SurfaceType.InvAlpha, "wb-mesh-inverse-1x")]
[InlineData(SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, "wb-mesh-alpha-1x")]
public void BuildingDetailOff_EveryRawStateRetainsThePreFixLogicalPath(
SurfaceType type,
string expectedPipeline)
{
using var fx = new DispatcherFixture(
withAlphaQueue: true,
detailAvailable: true,
detailEnabled: false);
using DrawScope draw = fx.BeginDraw(beginAlpha: true);
WbDrawDispatcher.WalkClassifiedBatch building = AlphaWalkBatch(
detailCategory: 1u,
surfaceType: type);
fx.Dispatcher.SubmitWalkAlphaInstance(in building, Matrix4x4.Identity);
Assert.Equal(1, fx.AlphaQueue!.PendingCount);
Assert.Empty(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
fx.AlphaQueue.EndFrame();
Assert.Single(fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>());
Assert.Contains(fx.Device.Calls.OfType<GpuRecordedPipelineBind>(),
call => call.PipelineName == expectedPipeline);
GpuPushConstants constants = fx.Device.Calls
.OfType<GpuRecordedPushConstants>()
.Last()
.Constants;
Assert.Equal(0u, constants.TextureIndexA);
Assert.Equal(0f, constants.ParamA);
Assert.Equal(0f, constants.ParamB);
Assert.Equal(0, constants.RenderPass & RetailDetailTextureContract.NoFogRenderPassFlag);
}
private static WbDrawDispatcher.WalkClassifiedBatch AlphaWalkBatch(
uint detailCategory,
TranslucencyKind translucency = TranslucencyKind.AlphaBlend) =>
SurfaceType surfaceType = SurfaceType.Alpha,
bool paletted = false) =>
new(
new GroupKey(
0,
@ -1255,7 +1399,11 @@ public sealed class WalkStaticStreamPopulatorTests
3,
new GpuTextureSlot(1),
0,
translucency,
TranslucencyKindExtensions.FromSurfaceType(surfaceType),
RetailSetSurfaceMaterialState.Resolve(
surfaceType,
texturePresent: true,
textureHasPalette: paletted),
FoliageFlags: 0),
Matrix4x4.Identity,
ClipSlot: 0,
@ -1277,6 +1425,10 @@ public sealed class WalkStaticStreamPopulatorTests
TextureSlot = new GpuTextureSlot(1),
TextureLayer = 0,
Translucency = TranslucencyKind.AlphaBlend,
MaterialState = RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Alpha,
texturePresent: true,
textureHasPalette: false),
};
group.Matrices.Add(Matrix4x4.Identity);
group.SubmissionOrders.Add(0);
@ -1320,7 +1472,8 @@ public sealed class WalkStaticStreamPopulatorTests
using var fx = new DispatcherFixture(withAlphaQueue: true);
fx.AlphaQueue!.BeginFrame();
var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend, FoliageFlags: 0);
var key = new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.AlphaBlend,
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0);
var batch = new WbDrawDispatcher.WalkClassifiedBatch(
key, Matrix4x4.Identity, 0, WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f,
Vector2.Zero, 0, IsOpaque: false, LocalSortCenter: new Vector3(0, 0, 10));
@ -1354,7 +1507,8 @@ public sealed class WalkStaticStreamPopulatorTests
var stream = new OrderedDrawStream();
stream.Append(new OrderedDrawCommand(
new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise),
new GroupKey(0, 0, 3, new GpuTextureSlot(1), 0, TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque, FoliageFlags: 0, CullMode: CullMode.Clockwise),
Matrix4x4.Identity, WalkDrawStage.Terrain, 0, 0,
WbDrawDispatcher.InstanceLightSet.Disabled, 0, 1f, Vector2.Zero, 0));

View file

@ -28,6 +28,7 @@ using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Wb;
using AcDream.App.Tests.Rendering.Gpu;
using AcDream.Content;
using AcDream.Core.Meshing;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
using CullMode = DatReaderWriter.Enums.CullMode;
@ -186,7 +187,8 @@ public class EnvCellRendererTests
"_mdiDrawRanges", BindingFlags.NonPublic | BindingFlags.Instance)!;
var ranges = (List<EnvCellRenderer.MdiDrawRange>)rangesField.GetValue(renderer)!;
ranges.Clear();
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 0, firstCommand: 0, commandCount: 1);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 0, firstCommand: 0, commandCount: 1,
RetailSetSurfaceMaterialState.Opaque);
var allInstances = new List<InstanceData>();
for (int i = 0; i < instanceCount; i++)
@ -239,7 +241,7 @@ public class EnvCellRendererTests
}
[Fact]
public void DetailUsesOnlyTheOriginalFourShellPipelines()
public void DetailUsesOnlySetSurfaceShellPipelinesAndNoReplayPipeline()
{
using var device = new RecordingGpuDevice();
using var meshManager = CreateMeshManager(device);
@ -250,7 +252,7 @@ public class EnvCellRendererTests
meshManager,
new WbFrustum());
Assert.Equal(4, device.CreatedPipelines.Count);
Assert.Equal(7, device.CreatedPipelines.Count);
Assert.DoesNotContain(device.CreatedPipelines,
pipeline => pipeline.Description.Name.Contains("detail", StringComparison.Ordinal));
}
@ -260,11 +262,14 @@ public class EnvCellRendererTests
{
var ranges = new List<EnvCellRenderer.MdiDrawRange>();
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 3, commandCount: 4);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3,
RetailSetSurfaceMaterialState.Opaque);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 3, commandCount: 4,
RetailSetSurfaceMaterialState.Opaque);
Assert.Equal(
[new EnvCellRenderer.MdiDrawRange(GroupIndex: 2, FirstCommand: 0, CommandCount: 7)],
[new EnvCellRenderer.MdiDrawRange(GroupIndex: 2, FirstCommand: 0, CommandCount: 7,
RetailSetSurfaceMaterialState.Opaque)],
ranges);
}
@ -273,17 +278,21 @@ public class EnvCellRendererTests
{
var ranges = new List<EnvCellRenderer.MdiDrawRange>();
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 6, firstCommand: 3, commandCount: 2);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 5, commandCount: 1);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 9, commandCount: 2);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 3,
RetailSetSurfaceMaterialState.Opaque);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 6, firstCommand: 3, commandCount: 2,
RetailSetSurfaceMaterialState.Opaque);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 5, commandCount: 1,
RetailSetSurfaceMaterialState.Opaque);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 9, commandCount: 2,
RetailSetSurfaceMaterialState.Opaque);
Assert.Equal(
[
new EnvCellRenderer.MdiDrawRange(2, 0, 3),
new EnvCellRenderer.MdiDrawRange(6, 3, 2),
new EnvCellRenderer.MdiDrawRange(2, 5, 1),
new EnvCellRenderer.MdiDrawRange(2, 9, 2),
new EnvCellRenderer.MdiDrawRange(2, 0, 3, RetailSetSurfaceMaterialState.Opaque),
new EnvCellRenderer.MdiDrawRange(6, 3, 2, RetailSetSurfaceMaterialState.Opaque),
new EnvCellRenderer.MdiDrawRange(2, 5, 1, RetailSetSurfaceMaterialState.Opaque),
new EnvCellRenderer.MdiDrawRange(2, 9, 2, RetailSetSurfaceMaterialState.Opaque),
],
ranges);
}
@ -293,7 +302,8 @@ public class EnvCellRendererTests
{
var ranges = new List<EnvCellRenderer.MdiDrawRange>();
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 0);
EnvCellRenderer.AppendMdiDrawRange(ranges, groupIndex: 2, firstCommand: 0, commandCount: 0,
RetailSetSurfaceMaterialState.Opaque);
Assert.Empty(ranges);
}

View file

@ -162,6 +162,7 @@ public sealed class FoliageWindClassificationTests
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(42),
TextureLayer: 0,
Translucency: TranslucencyKind.ClipMap,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise,
FoliageFlags: sceneryFlags);
var landblockStaticKey = sceneryKey with { FoliageFlags = landblockStaticFlags };

View file

@ -270,6 +270,7 @@ public class InstanceGroupClearTests
TextureSlot: Slot(0xAA),
TextureLayer: 0,
Translucency: TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0u);
var cached = new CachedBatch(
key,
@ -312,6 +313,7 @@ public class InstanceGroupClearTests
TextureSlot: Slot(0x77),
TextureLayer: 0,
Translucency: TranslucencyKind.ClipMap,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0x2u,
CullMode: DatReaderWriter.Enums.CullMode.CounterClockwise);
@ -416,6 +418,7 @@ public class InstanceGroupClearTests
TextureSlot: Slot(textureSlot),
TextureLayer: 0,
Translucency: TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0u);
private static WbDrawDispatcher.InstanceGroup MakeGroup(

View file

@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using AcDream.Content;
using AcDream.Core.Meshing;
using Chorizite.Core.Render.Enums;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
@ -255,6 +256,60 @@ public sealed class MeshExtractorSolidFaceExtractionTests
BitConverter.SingleToInt32Bits(batch.SurfaceOpacity));
}
public static TheoryData<SurfaceType, bool, RetailSetSurfaceBlend,
RetailSetSurfaceAlphaTest, bool> SetSurfaceExtractionRows() => new()
{
{ SurfaceType.Base1Image, false, RetailSetSurfaceBlend.Opaque, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha, false, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.AlphaAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Base1Image | SurfaceType.Additive, false, RetailSetSurfaceBlend.Additive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha, false, RetailSetSurfaceBlend.InverseAlpha, RetailSetSurfaceAlphaTest.Disabled, true },
{ SurfaceType.Base1Image | SurfaceType.InvAlpha | SurfaceType.Additive, false, RetailSetSurfaceBlend.InverseAdditive, RetailSetSurfaceAlphaTest.Disabled, false },
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, false, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Dds, true },
{ SurfaceType.Base1Image | SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.Clip, RetailSetSurfaceAlphaTest.Paletted, true },
{ SurfaceType.Base1Image | SurfaceType.Alpha | SurfaceType.Base1ClipMap, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Paletted, true },
{ SurfaceType.Base1Image | SurfaceType.Translucent | SurfaceType.Base1ClipMap | SurfaceType.Additive, true, RetailSetSurfaceBlend.StraightAlpha, RetailSetSurfaceAlphaTest.Disabled, false },
};
[Theory]
[MemberData(nameof(SetSurfaceExtractionRows))]
public void OrdinaryAndCellExtraction_CarryIdenticalResolvedSetSurfaceState(
SurfaceType type,
bool paletted,
RetailSetSurfaceBlend expectedBlend,
RetailSetSurfaceAlphaTest expectedAlphaTest,
bool expectedFog)
{
var dats = new FakeMeshExtractorDats();
uint paletteId = paletted ? 0x04000001u : 0u;
RegisterTexturedQuad(
dats,
type,
PixelFormat.PFID_A8R8G8B8,
new byte[4 * 4 * 4],
paletteId: paletteId);
var extractor = new MeshExtractor(dats, NullLogger.Instance, sideStagedSink: null);
ObjectMeshData ordinary = Assert.IsType<ObjectMeshData>(
extractor.PrepareMeshData(GfxObjId, isSetup: false));
ObjectMeshData cell = Assert.IsType<ObjectMeshData>(
extractor.PrepareCellStructMeshData(
id: 1,
BuildQuadCellStruct(RetailCullMode.Landblock),
surfaceOverrides: [2],
Matrix4x4.Identity,
CancellationToken.None));
RetailSetSurfaceMaterialState ordinaryState =
Assert.Single(Assert.Single(ordinary.TextureBatches).Value).MaterialState;
RetailSetSurfaceMaterialState cellState =
Assert.Single(Assert.Single(cell.TextureBatches).Value).MaterialState;
Assert.Equal(ordinaryState, cellState);
Assert.Equal(expectedBlend, ordinaryState.Blend);
Assert.Equal(expectedAlphaTest, ordinaryState.AlphaTest);
Assert.Equal(expectedFog, ordinaryState.FogEnabled);
}
/// <summary>
/// OH2/S1 chunk-2 (2026-09-02): these two winding tests used to register
/// an UNTEXTURED (Base1Solid) surface. Under the exact contract, an
@ -800,7 +855,8 @@ public sealed class MeshExtractorSolidFaceExtractionTests
SurfaceType surfaceType,
PixelFormat pixelFormat,
byte[] sourceData,
float translucency = 0.0f)
float translucency = 0.0f,
uint paletteId = 0)
{
dats.RegisterRootGfxObj(GfxObjId, BuildQuadGfxObj(TexturedSurfaceId, noPos: false));
dats.Register(TexturedSurfaceId, new Surface
@ -818,6 +874,7 @@ public sealed class MeshExtractorSolidFaceExtractionTests
Width = 4,
Height = 4,
Format = pixelFormat,
DefaultPaletteId = paletteId,
SourceData = sourceData,
});
}

View file

@ -97,6 +97,8 @@ public static class ObjectMeshDataEquality {
Assert.True(expected.CullMode == actual.CullMode, $"{path}.CullMode: expected {expected.CullMode}, got {actual.CullMode}");
Assert.True(expected.Translucency == actual.Translucency,
$"{path}.Translucency: expected {expected.Translucency}, got {actual.Translucency}");
Assert.True(expected.MaterialState == actual.MaterialState,
$"{path}.MaterialState: expected {expected.MaterialState}, got {actual.MaterialState}");
Assert.True(expected.IsTransparent == actual.IsTransparent, $"{path}.IsTransparent: expected {expected.IsTransparent}, got {actual.IsTransparent}");
Assert.True(expected.IsAdditive == actual.IsAdditive, $"{path}.IsAdditive: expected {expected.IsAdditive}, got {actual.IsAdditive}");
Assert.True(expected.HasWrappingUVs == actual.HasWrappingUVs, $"{path}.HasWrappingUVs: expected {expected.HasWrappingUVs}, got {actual.HasWrappingUVs}");

View file

@ -192,6 +192,10 @@ public class ObjectMeshDataSerializerTests {
Indices = new List<ushort> { 0, 1, 2 },
CullMode = CullMode.Clockwise,
Translucency = TranslucencyKind.Opaque,
MaterialState = new RetailSetSurfaceMaterialState(
RetailSetSurfaceBlend.InverseAdditive,
RetailSetSurfaceAlphaTest.Paletted,
FogEnabled: false),
IsTransparent = false,
IsAdditive = false,
HasWrappingUVs = false,
@ -290,6 +294,37 @@ public class ObjectMeshDataSerializerTests {
BitConverter.SingleToInt32Bits(read.SurfaceOpacity));
}
[Theory]
[InlineData(0, 0, true)]
[InlineData(1, 0, true)]
[InlineData(2, 1, true)]
[InlineData(3, 2, false)]
[InlineData(4, 0, true)]
[InlineData(5, 1, false)]
[InlineData(6, 2, true)]
public void SetSurfaceState_RoundTripsEveryPackedFieldDeterministically(
int blendValue,
int alphaTestValue,
bool fogEnabled) {
ObjectMeshData data = WithCellShellSubset();
TextureBatchData batch = Assert.Single(Assert.Single(data.TextureBatches).Value);
batch.MaterialState = new RetailSetSurfaceMaterialState(
(RetailSetSurfaceBlend)blendValue,
(RetailSetSurfaceAlphaTest)alphaTestValue,
fogEnabled);
using var first = new MemoryStream();
ObjectMeshDataSerializer.Write(data, first);
using var second = new MemoryStream();
ObjectMeshDataSerializer.Write(data, second);
Assert.Equal(first.ToArray(), second.ToArray());
TextureBatchData read = Assert.Single(Assert.Single(
ObjectMeshDataSerializer.Read(first.ToArray()).TextureBatches).Value);
Assert.Equal(batch.MaterialState, read.MaterialState);
Assert.Equal(batch.MaterialState.ToPackedByte(), read.MaterialState.ToPackedByte());
}
// ---- determinism -----------------------------------------------------
[Fact]

View file

@ -10,13 +10,14 @@ public class PakFormatTests {
}
/// <summary>
/// S5-c4: recipe 9 carries authored surface opacity in every prepared
/// texture batch. It remains a payload recipe change, not pak framing.
/// S5-c4 fix round 1: recipe 10 carries exact SetSurface state after
/// recipe 10's resolved SetSurface state after recipe 9's authored opacity.
/// It remains a payload recipe change.
/// </summary>
[Fact]
public void FormatVersion_StaysAtTwo_AcrossTheS5C4RecipeBump() {
Assert.Equal(2u, PakFormat.CurrentFormatVersion);
Assert.Equal(9u, PakFormat.CurrentBakeToolVersion);
Assert.Equal(10u, PakFormat.CurrentBakeToolVersion);
}
[Fact]

View file

@ -136,7 +136,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
}
/// <summary>
/// S5-c4: an older recipe-8 package must be rejected by a recipe-9
/// S5-c4 fix round 1: an older recipe-9 package must be rejected by a recipe-10
/// consumer through the catalog-identity check
/// alone, BEFORE any TextureBatchData payload field is ever
/// deserialized -- so an older pak can never be silently misread as
@ -160,7 +160,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
fs.Position = 36;
Span<byte> buf = stackalloc byte[4];
System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(
buf, 8u);
buf, 9u);
fs.Write(buf);
}
@ -168,7 +168,7 @@ public sealed class PreparedAssetSourceTests : IDisposable
() => new PakPreparedAssetSource(path, Identity));
Assert.Contains("does not match the installed DAT set", error.Message);
Assert.Contains("Re-bake", error.Message);
Assert.Contains("bake tool 8 != 9", error.Message);
Assert.Contains("bake tool 9 != 10", error.Message);
}
[Fact]

View file

@ -337,6 +337,7 @@ public class EntityClassificationCacheTests
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
TextureLayer: 0,
Translucency: TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0u);
return new CachedBatch(key, new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot), Matrix4x4.Identity);
}

View file

@ -426,6 +426,7 @@ public sealed class WbDrawDispatcherBucketingTests
TextureSlot: new AcDream.App.Rendering.Gpu.GpuTextureSlot(texSlot),
TextureLayer: 0,
Translucency: TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
FoliageFlags: 0u);
return new CachedBatch(
key,

View file

@ -20,9 +20,9 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
// Arrange — three groups: 2 opaque (12+1 instances) + 1 transparent (12 instances)
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
{
new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureIndex: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, SurfaceOpacity: 0.75f),
new(IndexCount: 200, FirstIndex: 100, BaseVertex: 0, InstanceCount: 12, FirstInstance: 12, TextureIndex: 0xBB, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend, SurfaceOpacity: 0.25f),
new(IndexCount: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureIndex: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, SurfaceOpacity: 0.5f),
new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureIndex: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, MaterialState: RetailSetSurfaceMaterialState.Opaque, SurfaceOpacity: 0.75f),
new(IndexCount: 200, FirstIndex: 100, BaseVertex: 0, InstanceCount: 12, FirstInstance: 12, TextureIndex: 0xBB, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend, MaterialState: RetailSetSurfaceMaterialState.Opaque, SurfaceOpacity: 0.25f),
new(IndexCount: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureIndex: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque, MaterialState: RetailSetSurfaceMaterialState.Opaque, SurfaceOpacity: 0.5f),
};
var indirect = new DrawElementsIndirectCommand[16];
@ -75,12 +75,15 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
{
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0,
TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.Opaque,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
CullMode: CullMode.Clockwise),
new(IndexCount: 20, FirstIndex: 10, BaseVertex: 0, InstanceCount: 1, FirstInstance: 1,
TextureIndex: 0x2, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
CullMode: CullMode.None),
new(IndexCount: 30, FirstIndex: 30, BaseVertex: 0, InstanceCount: 1, FirstInstance: 2,
TextureIndex: 0x3, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap,
MaterialState: RetailSetSurfaceMaterialState.Opaque,
CullMode: CullMode.Landblock),
};
var indirect = new DrawElementsIndirectCommand[4];
@ -116,7 +119,7 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
// because the discard handles transparency, not blending.
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
{
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0, TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap),
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0, TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap, MaterialState: RetailSetSurfaceMaterialState.Opaque),
};
var indirect = new DrawElementsIndirectCommand[4];
var batch = new WbDrawDispatcher.BatchDataPublic[4];
@ -146,7 +149,8 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
FirstInstance: index,
TextureIndex: (uint)index,
TextureLayer: 0,
Translucency: kind)).ToList();
Translucency: kind,
MaterialState: RetailSetSurfaceMaterialState.Opaque)).ToList();
var indirect = new DrawElementsIndirectCommand[kinds.Length];
var batches = new WbDrawDispatcher.BatchDataPublic[kinds.Length];

View file

@ -77,4 +77,28 @@ public sealed class ContentMigrationCatalogTests
Assert.Contains("CellStruct", plan.Reason, StringComparison.OrdinalIgnoreCase);
Assert.Contains("surface opacity", plan.Reason, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void RecipeNineToTenRequiresOneExplicitFullRebuild()
{
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(9, 10);
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
Assert.Equal(9u, plan.FromRecipeVersion);
Assert.Equal(10u, plan.TargetRecipeVersion);
Assert.Contains("SetSurface", plan.Reason, StringComparison.OrdinalIgnoreCase);
Assert.Empty(plan.EffectiveDatIds);
Assert.Empty(plan.EffectiveLandblocks);
}
[Fact]
public void AnyOlderRecipeToTenCollapsesToOneFullRebuild()
{
ContentMigrationPlan plan = ContentMigrationCatalog.Resolve(1, 10);
Assert.Equal(ContentWorkKind.FullRebuild, plan.Kind);
Assert.Equal(10u, plan.TargetRecipeVersion);
Assert.Contains("surface opacity", plan.Reason, StringComparison.OrdinalIgnoreCase);
Assert.Contains("SetSurface", plan.Reason, StringComparison.OrdinalIgnoreCase);
}
}