fix(rendering): preserve opaque building coverage under detail MSAA (#473)

This commit is contained in:
Erik 2026-09-05 08:07:51 +02:00
parent d35ed1aece
commit bf23673f3d
7 changed files with 387 additions and 46 deletions

View file

@ -1,6 +1,7 @@
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
@ -288,58 +289,201 @@ public sealed class OrderPreservingSubmitterTests
}
[Theory]
[InlineData(false, 200f / 255f)]
[InlineData(true, 100f / 255f)]
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace(
bool paletted,
float expectedReference)
[InlineData(false, 1)]
[InlineData(false, 4)]
[InlineData(true, 1)]
[InlineData(true, 4)]
public void PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassWithoutOpaqueCoverage(
bool atmospheric,
int sampleCount)
{
using var fx = new DispatcherFixture(detailAvailable: true, detailEnabled: true);
foreach (bool paletted in new[] { false, true })
{
using var fx = new DispatcherFixture(
detailAvailable: true,
detailEnabled: true,
atmospheric: atmospheric,
sampleCount: sampleCount);
using DrawScope draw = fx.BeginDraw();
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(1, 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);
string prefix = atmospheric ? "wb-mesh-atmospheric" : "wb-mesh";
string suffix = sampleCount == 1 ? "-1x" : string.Empty;
string opaque = $"{prefix}-opaque{suffix}";
string opaqueA2c = $"{prefix}-opaque-a2c{suffix}";
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, 0f), DetailFields(runs[0].Constants));
Assert.Equal((77u, 3.5f, 0f), DetailFields(runs[1].Constants));
Assert.Equal(
(77u, 3.5f, paletted ? 100f / 255f : 200f / 255f),
DetailFields(runs[2].Constants));
Assert.Equal((0u, 0f, 0f), DetailFields(runs[3].Constants));
Assert.Equal(
[opaque, opaqueA2c, opaque, opaqueA2c, opaqueA2c],
fx.Device.Calls.OfType<GpuRecordedPipelineBind>()
.Select(call => call.PipelineName)
.ToArray());
Assert.Equal(4, fx.Device.Calls.OfType<GpuRecordedMultiDrawIndirect>().Count());
AssertOpaquePipeline(fx.Device, opaque, atmospheric, sampleCount, alphaToCoverage: false);
AssertOpaquePipeline(fx.Device, opaqueA2c, atmospheric, sampleCount, alphaToCoverage: true);
}
}
[Theory]
[InlineData(false, 1)]
[InlineData(false, 4)]
[InlineData(true, 1)]
[InlineData(true, 4)]
public void ClassicGrouped_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassWithoutOpaqueCoverage(
bool atmospheric,
int sampleCount)
{
using var fx = new DispatcherFixture(
detailAvailable: true,
detailEnabled: true,
atmospheric: atmospheric,
sampleCount: sampleCount);
using DrawScope draw = fx.BeginDraw();
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(1, detailCategory: 1),
MakeCommand(
ExecuteClassicGroups(
fx,
MakeClassicGroup(0, detailCategory: 0),
MakeClassicGroup(1, detailCategory: 1),
MakeClassicGroup(
2,
translucency: TranslucencyKind.ClipMap,
detailCategory: 1,
translucency: TranslucencyKind.ClipMap,
materialState: RetailSetSurfaceMaterialState.Resolve(
SurfaceType.Base1ClipMap,
texturePresent: true,
textureHasPalette: paletted)),
MakeCommand(3));
textureHasPalette: false)),
MakeClassicGroup(3, detailCategory: 0));
string prefix = atmospheric ? "wb-mesh-atmospheric" : "wb-mesh";
string suffix = sampleCount == 1 ? "-1x" : string.Empty;
string opaque = $"{prefix}-opaque{suffix}";
string opaqueA2c = $"{prefix}-opaque-a2c{suffix}";
var transcript = DecodePipelineRuns(fx.Device);
Assert.Equal([opaqueA2c, opaque, opaqueA2c, opaqueA2c],
transcript.Select(entry => entry.Pipeline));
Assert.All(transcript, entry => Assert.Equal(1u, entry.Draw.DrawCount));
Assert.Equal((0u, 0f, 0f), DetailFields(transcript[0].Constants));
Assert.Equal((77u, 3.5f, 0f), DetailFields(transcript[1].Constants));
Assert.Equal((77u, 3.5f, 200f / 255f), DetailFields(transcript[2].Constants));
Assert.Equal((0u, 0f, 0f), DetailFields(transcript[3].Constants));
AssertOpaquePipeline(fx.Device, opaque, atmospheric, sampleCount, alphaToCoverage: false);
AssertOpaquePipeline(fx.Device, opaqueA2c, atmospheric, sampleCount, alphaToCoverage: true);
}
[Theory]
[InlineData(false, 1)]
[InlineData(false, 4)]
[InlineData(true, 1)]
[InlineData(true, 4)]
public void ClassicGrouped_MixedOrdinaryAndBuildingInstances_UsesNonCoverageForWholeCommand(
bool atmospheric,
int sampleCount)
{
using var fx = new DispatcherFixture(
detailAvailable: true,
detailEnabled: true,
atmospheric: atmospheric,
sampleCount: sampleCount);
using DrawScope draw = fx.BeginDraw();
WbDrawDispatcher.InstanceGroup mixed = MakeClassicGroup(0, detailCategory: 0);
AppendClassicInstance(mixed, index: 1, detailCategory: 1);
ExecuteClassicGroups(fx, mixed);
string prefix = atmospheric ? "wb-mesh-atmospheric" : "wb-mesh";
string suffix = sampleCount == 1 ? "-1x" : string.Empty;
var entry = Assert.Single(DecodePipelineRuns(fx.Device));
Assert.Equal($"{prefix}-opaque{suffix}", entry.Pipeline);
Assert.Equal(1u, entry.Draw.DrawCount);
Assert.Equal((77u, 3.5f, 0f), DetailFields(entry.Constants));
Assert.Equal([0u, 1u], mixed.DetailCategories);
Assert.Equal(2, mixed.Matrices.Count);
}
[Theory]
[InlineData(false, true, true)]
[InlineData(true, false, true)]
[InlineData(true, true, false)]
public void PrepareThenDraw_DetailUnavailableDisabledOrQualityA2cFalsePreservesSelection(
bool detailAvailable,
bool detailEnabled,
bool alphaToCoverage)
{
using var fx = new DispatcherFixture(
detailAvailable: detailAvailable,
detailEnabled: detailEnabled,
sampleCount: 4,
alphaToCoverage: alphaToCoverage);
using DrawScope draw = fx.BeginDraw();
OrderedDrawStream stream = StreamOf(
MakeCommand(0),
MakeCommand(1, detailCategory: 1));
PrepareAndDrawWhole(fx.Dispatcher, draw, stream);
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));
bool detailActive = detailAvailable && detailEnabled;
string selected = alphaToCoverage ? "wb-mesh-opaque-a2c" : "wb-mesh-opaque";
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",
],
["wb-mesh-opaque", selected, detailActive ? "wb-mesh-opaque" : selected],
fx.Device.Calls.OfType<GpuRecordedPipelineBind>()
.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);
.Select(call => call.PipelineName));
List<(GpuPushConstants Constants, int Start, int Count)> runs = DecodeRuns(fx.Device);
Assert.Equal([(0, 1), (1, 1)], runs.Select(run => (run.Start, run.Count)));
Assert.Equal((0u, 0f, 0f), DetailFields(runs[0].Constants));
Assert.Equal(
detailActive ? (77u, 3.5f, 0f) : (0u, 0f, 0f),
DetailFields(runs[1].Constants));
}
[Theory]
[InlineData(false, true, true)]
[InlineData(true, false, true)]
[InlineData(true, true, false)]
public void ClassicGrouped_DetailUnavailableDisabledOrQualityA2cFalsePreservesSelection(
bool detailAvailable,
bool detailEnabled,
bool alphaToCoverage)
{
using var fx = new DispatcherFixture(
detailAvailable: detailAvailable,
detailEnabled: detailEnabled,
sampleCount: 4,
alphaToCoverage: alphaToCoverage);
using DrawScope draw = fx.BeginDraw();
ExecuteClassicGroups(fx, MakeClassicGroup(0, detailCategory: 1));
bool detailActive = detailAvailable && detailEnabled;
var entry = Assert.Single(DecodePipelineRuns(fx.Device));
Assert.Equal(alphaToCoverage && !detailActive
? "wb-mesh-opaque-a2c"
: "wb-mesh-opaque", entry.Pipeline);
Assert.Equal(1u, entry.Draw.DrawCount);
Assert.Equal(
detailActive ? (77u, 3.5f, 0f) : (0u, 0f, 0f),
DetailFields(entry.Constants));
}
[Fact]
@ -832,6 +976,130 @@ public sealed class OrderPreservingSubmitterTests
private static List<(int Start, int Count)> DecodeDrawRanges(RecordingGpuDevice device) =>
[.. DecodeRuns(device).Select(r => (r.Start, r.Count))];
private static (uint TextureIndexA, float ParamA, float ParamB) DetailFields(
GpuPushConstants constants) =>
(constants.TextureIndexA, constants.ParamA, constants.ParamB);
private static void AssertOpaquePipeline(
RecordingGpuDevice device,
string name,
bool atmospheric,
int sampleCount,
bool alphaToCoverage)
{
GpuPipelineDescription description = device.CreatedPipelines
.Single(pipeline => pipeline.Description.Name == name)
.Description;
Assert.Equal(atmospheric ? "mesh_atmospheric" : "mesh_modern", description.Shaders.Name);
Assert.Equal(GpuBlendMode.None, description.Blend);
Assert.Equal(alphaToCoverage, description.AlphaToCoverage);
Assert.Equal(sampleCount, description.SampleCount);
Assert.True(description.Depth.Test);
Assert.True(description.Depth.Write);
Assert.Equal(WorldDepthContract.WorldCompare, description.Depth.Compare);
}
private static WbDrawDispatcher.InstanceGroup MakeClassicGroup(
int index,
uint detailCategory,
TranslucencyKind translucency = TranslucencyKind.Opaque,
RetailSetSurfaceMaterialState? materialState = null)
{
var group = new WbDrawDispatcher.InstanceGroup
{
FirstIndex = (uint)index * 3,
BaseVertex = index * 4,
IndexCount = 3,
TextureSlot = new GpuTextureSlot((uint)index),
TextureLayer = 0,
Translucency = translucency,
MaterialState = materialState ?? RetailSetSurfaceMaterialState.Opaque,
SurfaceOpacity = 1f,
CullMode = CullMode.CounterClockwise,
FoliageFlags = 0,
};
group.Matrices.Add(Matrix4x4.CreateTranslation(index, index * 2, index * 3));
group.SubmissionOrders.Add(index);
group.Slots.Add(0);
group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
group.IndoorFlags.Add(0);
group.DetailCategories.Add(detailCategory);
group.Opacities.Add(1f);
group.SelectionLighting.Add(Vector2.Zero);
return group;
}
private static void AppendClassicInstance(
WbDrawDispatcher.InstanceGroup group,
int index,
uint detailCategory)
{
group.Matrices.Add(Matrix4x4.CreateTranslation(index, index * 2, index * 3));
group.SubmissionOrders.Add(index);
group.Slots.Add(0);
group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
group.IndoorFlags.Add(0);
group.DetailCategories.Add(detailCategory);
group.Opacities.Add(1f);
group.SelectionLighting.Add(Vector2.Zero);
}
private static void ExecuteClassicGroups(
DispatcherFixture fixture,
params WbDrawDispatcher.InstanceGroup[] groups)
{
fixture.Dispatcher.BeginFrame(frameSlot: 0);
MethodInfo execute = typeof(WbDrawDispatcher).GetMethod(
"ExecuteClassifiedGroups",
BindingFlags.Instance | BindingFlags.NonPublic)!;
execute.Invoke(
fixture.Dispatcher,
[
Matrix4x4.Identity,
Vector3.Zero,
0u,
groups,
WbDrawDispatcher.EntitySet.All,
groups.Length,
groups.Length,
false,
false,
]);
}
private static List<(
string Pipeline,
GpuPushConstants Constants,
GpuRecordedMultiDrawIndirect Draw)> DecodePipelineRuns(RecordingGpuDevice device)
{
var transcript = new List<(
string Pipeline,
GpuPushConstants Constants,
GpuRecordedMultiDrawIndirect Draw)>();
IReadOnlyList<GpuRecordedCall> calls = device.Calls;
for (int i = 0; i < calls.Count; i++)
{
if (calls[i] is not GpuRecordedMultiDrawIndirect draw)
continue;
string? pipeline = null;
GpuPushConstants? constants = null;
for (int prior = i - 1; prior >= 0 && (pipeline is null || constants is null); prior--)
{
if (pipeline is null && calls[prior] is GpuRecordedPipelineBind bind)
pipeline = bind.PipelineName;
if (constants is null && calls[prior] is GpuRecordedPushConstants push)
constants = push.Constants;
}
Assert.NotNull(pipeline);
Assert.NotNull(constants);
transcript.Add((pipeline!, constants!.Value, draw));
}
return transcript;
}
private static List<(GpuPushConstants Constants, int Start, int Count)> DecodeRuns(
RecordingGpuDevice device)
{
@ -892,6 +1160,7 @@ public sealed class OrderPreservingSubmitterTests
bool atmospheric = false,
bool receiverBindingAvailable = true,
int sampleCount = 1,
bool alphaToCoverage = true,
RecordingGpuDevice? device = null)
{
Device = device ?? new RecordingGpuDevice();
@ -923,6 +1192,7 @@ public sealed class OrderPreservingSubmitterTests
new GpuTextureSlot(77), 3.5f, 0x05000001, 0x08000001, 16, 16)
: default,
buildingDetailEnabled: () => detailEnabled);
Dispatcher.AlphaToCoverage = alphaToCoverage;
if (atmospheric)
{
var source = new BindableAtmosphericSource(receiverBindingAvailable);