sharpemu/tests/SharpEmu.Libs.Tests/Gpu/Pipelines/ShaderPipelineCacheTests.cs

402 lines
21 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Tests.Gpu.Rendering;
using SharpEmu.Libs.Tests.Gpu.Scheduling;
using Silk.NET.Vulkan;
using Xunit;
using static SharpEmu.Libs.Tests.Gpu.Rendering.RenderExecutorFixtures;
namespace SharpEmu.Libs.Tests.Gpu.Pipelines;
// The graphics pipeline description folds the draw's static state byte for byte; the keys fold all of it.
[Collection(SchedulingStateCollection.Name)]
public sealed class ShaderPipelineCacheTests : IDisposable
{
private const uint Format32x4Float = 77;
private readonly FatalScope _fatal = new();
public void Dispose() => _fatal.Dispose();
// Forwards each pipeline request to the static builder and keeps the description.
private sealed class DescribingProvider : IShaderPipelineProvider
{
public GraphicsPrograms Graphics { get; set; } = Programs();
public List<GraphicsPipelineDescription> Descriptions { get; } = new();
public GraphicsPrograms GetGraphicsPrograms(VertexStageRegisters vertex, PixelStageRegisters pixel, ShaderInterfaceRegisters shaderInterface, ContextRegisters context, ReadOnlySpan<ColorComponentMap> targetExportMapping, bool pixelActive, bool depthBound) => Graphics;
public PipelineHandle CreateGraphicsPipeline(ReadOnlySpan<ColorTargetState> colors, in DepthAttachmentState depth, VertexInputInfo vertexInput, PixelInputInfo? pixelInput, ContextRegisters context, in RenderingState rendering, PrimitiveTopology topology, bool primitiveRestartEnabled, bool disableBlending, ShaderProgram vertexProgram, ShaderProgram pixelProgram)
{
Descriptions.Add(ShaderPipelineCache.BuildGraphicsDescription(
colors, in depth, vertexInput, pixelInput, context, in rendering, topology, primitiveRestartEnabled, disableBlending,
vertexProgram, pixelProgram, SampleCountFlags.Count1Bit | SampleCountFlags.Count4Bit));
return new PipelineHandle(0xA1, 0xB1, false);
}
public ComputeProgram GetComputeProgram(ComputeStageRegisters compute, ShaderInterfaceRegisters shaderInterface, uint dispatchInitiator, uint dimensionX, uint dimensionY, uint dimensionZ) =>
throw new InvalidOperationException("The describing provider has no compute program.");
public PipelineHandle CreateComputePipeline(ComputeInputInfo input, ShaderProgram program) =>
throw new InvalidOperationException("The describing provider has no compute pipeline.");
}
private static GraphicsPipelineDescription Describe(Action<RegisterBanks>? configure = null, GraphicsPrograms? programs = null, uint primitiveType = PrimitiveTriangleList, bool withDepth = false)
{
var host = new RecordingRenderHost();
var provider = new DescribingProvider();
if (programs is not null)
{
provider.Graphics = programs;
}
var banks = Banks(primitiveType, withDepth);
configure?.Invoke(banks);
new RenderExecutor(host, provider).DrawAuto(1, banks, Auto(3));
return Assert.Single(provider.Descriptions);
}
private static GraphicsPipelineDescription With(GraphicsPipelineDescription description, PipelineStaticParameters parameters) => new()
{
Rendering = description.Rendering,
VertexInput = description.VertexInput,
VertexInfo = description.VertexInfo,
VertexProgram = description.VertexProgram,
VertexStage = description.VertexStage,
PixelInfo = description.PixelInfo,
PixelProgram = description.PixelProgram,
PixelStage = description.PixelStage,
StaticParameters = parameters,
};
[Fact]
public void StaticParameters_FoldTheBlendCullAndTopologyRegisters()
{
var description = Describe(banks =>
{
banks.Context.BlendControls[0] = new BlendRegisters
{
Enable = true, ColorSourceFactor = 4, ColorDestinationFactor = 5, ColorFunction = 1,
AlphaSourceFactor = 2, AlphaDestinationFactor = 3, AlphaFunction = 2, SeparateAlpha = true,
};
banks.Context.RasterMode.CullBack = true;
banks.Context.RasterMode.FrontFaceClockwise = true;
});
var parameters = description.StaticParameters;
Assert.Equal(PrimitiveTopology.TriangleList, parameters.Topology);
Assert.False(parameters.PrimitiveRestartEnable);
Assert.Equal(1u, parameters.Samples);
Assert.False(parameters.SampleShadingEnable);
Assert.Equal(1u, parameters.ColorCount);
Assert.NotEqual(0u, parameters.GetColorMask(0));
Assert.True(parameters.GetBlendEnable(0));
Assert.False(parameters.GetBlendBypass(0));
Assert.Equal((4, 5, 1), ((int)parameters.GetColorSourceBlend(0), (int)parameters.GetColorDestinationBlend(0), (int)parameters.GetColorBlendFunction(0)));
Assert.Equal((2, 3, 2), ((int)parameters.GetAlphaSourceBlend(0), (int)parameters.GetAlphaDestinationBlend(0), (int)parameters.GetAlphaBlendFunction(0)));
Assert.True(parameters.GetSeparateAlphaBlend(0));
Assert.True(parameters.CullBack);
Assert.False(parameters.CullFront);
Assert.True(parameters.FrontFaceClockwise);
Assert.False(parameters.WithDepth);
Assert.False(parameters.StencilTestEnable);
Assert.Equal(Format.Undefined, description.Rendering.DepthFormat);
Assert.NotEqual(Format.Undefined, description.Rendering.ColorFormats[0]);
Assert.Equal(1u, description.Rendering.ColorCount);
}
[Fact]
public void DisableBlending_ClearsTheBlendEnableButKeepsTheFactors()
{
var programs = Programs();
var description = Describe(
banks => banks.Context.BlendControls[0] = new BlendRegisters { Enable = true, ColorSourceFactor = 4, ColorDestinationFactor = 5 },
new GraphicsPrograms { Vertex = programs.Vertex, Pixel = programs.Pixel, VertexInput = programs.VertexInput, PixelInput = programs.PixelInput, DisableBlending = true });
Assert.False(description.StaticParameters.GetBlendEnable(0));
Assert.Equal(4, description.StaticParameters.GetColorSourceBlend(0));
}
[Fact]
public void RectangleList_DisablesCullingAndKeepsThePatchTopology()
{
var description = Describe(banks => banks.Context.RasterMode.CullBack = true, primitiveType: 7);
Assert.Equal(PrimitiveTopology.PatchList, description.StaticParameters.Topology);
Assert.False(description.StaticParameters.CullBack);
}
[Fact]
public void DepthTarget_FoldsTheDepthAndStencilState()
{
var description = Describe(withDepth: true);
Assert.True(description.StaticParameters.WithDepth);
Assert.NotEqual(Format.Undefined, description.Rendering.DepthFormat);
Assert.Equal(StencilOperations.Default, description.StaticParameters.StencilFront);
}
[Fact]
public void VertexInputState_FoldsEveryBindingAndAttribute()
{
var defaults = Programs();
var vertexInput = new VertexInputInfo
{
Buffers = [new VertexInputBuffer(VertexBase, 32, 3), new VertexInputBuffer(VertexBase + 0x1000, 16, 3, PerInstance: true)],
Attributes =
[
new VertexAttributeResource(new BufferDescriptorWords(unchecked((uint)VertexBase), (uint)(VertexBase >> 32) | (32u << 16), 3, Format32x4Float << 12), 0, 4, 0, 0, 0, 0),
new VertexAttributeResource(new BufferDescriptorWords(unchecked((uint)VertexBase), (uint)(VertexBase >> 32) | (32u << 16), 3, Format32x4Float << 12), 4, 2, 1, 0, 0, 16),
new VertexAttributeResource(new BufferDescriptorWords(unchecked((uint)VertexBase), (uint)(VertexBase >> 32) | (16u << 16), 3, Format32x4Float << 12), 6, 4, 2, 1, 1, 0),
],
Stage = defaults.VertexInput.Stage,
};
var programs = new GraphicsPrograms { Vertex = defaults.Vertex, Pixel = defaults.Pixel, VertexInput = vertexInput, PixelInput = defaults.PixelInput };
var state = Describe(programs: programs).VertexInput;
Assert.Equal(2, state.BindingCount);
Assert.Equal(3, state.AttributeCount);
Assert.Equal(new PipelineVertexBinding(32, false), state.Bindings[0]);
Assert.Equal(new PipelineVertexBinding(16, true), state.Bindings[1]);
Assert.Equal(new PipelineVertexAttribute(16, 0), state.Attributes[1]);
Assert.Equal(new PipelineVertexAttribute(0, 1), state.Attributes[2]);
}
[Fact]
public void GraphicsPipelineKey_FoldsAll166StaticBytes()
{
var description = Describe();
var key = ShaderPipelineCache.KeyOf(description);
Assert.Equal(key, ShaderPipelineCache.KeyOf(With(description, PipelineStaticParameters.FromBytes(description.StaticParameters.Bytes))));
Assert.Equal(166, description.StaticParameters.Bytes.Length);
for (var index = 0; index < description.StaticParameters.Bytes.Length; index++)
{
var bytes = description.StaticParameters.Bytes.ToArray();
bytes[index] ^= 0x01;
var flipped = ShaderPipelineCache.KeyOf(With(description, PipelineStaticParameters.FromBytes(bytes)));
Assert.False(key.Equals(flipped), $"byte {index} did not change the key");
}
}
[Fact]
public void GraphicsPipelineKey_FoldsTheProgramsTheRenderingAndTheVertexInput()
{
var description = Describe();
var key = ShaderPipelineCache.KeyOf(description);
var otherVertex = new GraphicsPipelineDescription
{
Rendering = description.Rendering, VertexInput = description.VertexInput, VertexInfo = description.VertexInfo,
VertexProgram = new ShaderProgram(0x99), VertexStage = description.VertexStage, PixelInfo = description.PixelInfo,
PixelProgram = description.PixelProgram, PixelStage = description.PixelStage, StaticParameters = description.StaticParameters,
};
Assert.NotEqual(key, ShaderPipelineCache.KeyOf(otherVertex));
var otherRendering = new PipelineRenderingState { ColorCount = 1 };
otherRendering.ColorFormats[0] = Format.R16G16B16A16Sfloat;
var renderingChanged = new GraphicsPipelineDescription
{
Rendering = otherRendering, VertexInput = description.VertexInput, VertexInfo = description.VertexInfo,
VertexProgram = description.VertexProgram, VertexStage = description.VertexStage, PixelInfo = description.PixelInfo,
PixelProgram = description.PixelProgram, PixelStage = description.PixelStage, StaticParameters = description.StaticParameters,
};
Assert.NotEqual(key, ShaderPipelineCache.KeyOf(renderingChanged));
Assert.Equal(key, ShaderPipelineCache.KeyOf(With(description, description.StaticParameters)));
}
[Fact]
public void ComputePipelineKey_IsTheProgramId()
{
Assert.Equal(new ComputePipelineKey(5), new ComputePipelineKey(5));
Assert.NotEqual(new ComputePipelineKey(5), new ComputePipelineKey(6));
var guest = new PipelineTestGuest();
var cache = new ShaderPipelineCache(guest.Context, guest.Host, guest.Compiler, guest.Registry);
var input = ComputeProgram().Input;
var first = cache.CreateComputePipeline(input, new ShaderProgram(7, 1));
var again = cache.CreateComputePipeline(input, new ShaderProgram(7, 1));
var other = cache.CreateComputePipeline(input, new ShaderProgram(8, 2));
Assert.Equal(first, again);
Assert.NotEqual(first, other);
Assert.Equal(2, guest.Host.ComputePipelines.Count);
Assert.Equal(2, cache.ComputePipelineCount);
}
[Fact]
public void TryCreateComputePipeline_ReportsTheHostCompileAndCachesTheResult()
{
var guest = new PipelineTestGuest();
var cache = new ShaderPipelineCache(guest.Context, guest.Host, guest.Compiler, guest.Registry);
var input = ComputeProgram().Input;
var program = new ShaderProgram(7, 1);
guest.Host.PendingComputeCompiles[program.Id] = 2;
Assert.False(cache.TryCreateComputePipeline(input, program, out _));
Assert.False(cache.TryCreateComputePipeline(input, program, out _));
Assert.Empty(guest.Host.ComputePipelines);
Assert.Equal(0, cache.ComputePipelineCount);
Assert.True(cache.TryCreateComputePipeline(input, program, out var ready));
Assert.Single(guest.Host.ComputePipelines);
// The cached pipeline is served without asking the host again.
Assert.True(cache.TryCreateComputePipeline(input, program, out var cached));
Assert.Equal(ready, cached);
Assert.Single(guest.Host.ComputePipelines);
Assert.Equal(1, cache.ComputePipelineCount);
}
[Fact]
public void GraphicsPipelines_AreCachedByTheirKey()
{
var guest = new PipelineTestGuest();
var cache = new ShaderPipelineCache(guest.Context, guest.Host, guest.Compiler, guest.Registry);
var host = new RecordingRenderHost();
var executor = new RenderExecutor(host, new CachingProvider(cache));
executor.DrawAuto(1, Banks(), Auto(3));
executor.DrawAuto(2, Banks(), Auto(3));
executor.DrawAuto(3, Banks(PrimitiveTriangleStrip), Auto(4));
Assert.Equal(2, guest.Host.GraphicsPipelines.Count);
Assert.Equal(2, cache.GraphicsPipelineCount);
}
// Programs from the fixtures, pipelines from the real cache over the fake host.
private sealed class CachingProvider(ShaderPipelineCache cache) : IShaderPipelineProvider
{
private readonly GraphicsPrograms _programs = Programs();
public GraphicsPrograms GetGraphicsPrograms(VertexStageRegisters vertex, PixelStageRegisters pixel, ShaderInterfaceRegisters shaderInterface, ContextRegisters context, ReadOnlySpan<ColorComponentMap> targetExportMapping, bool pixelActive, bool depthBound) => _programs;
public PipelineHandle CreateGraphicsPipeline(ReadOnlySpan<ColorTargetState> colors, in DepthAttachmentState depth, VertexInputInfo vertexInput, PixelInputInfo? pixelInput, ContextRegisters context, in RenderingState rendering, PrimitiveTopology topology, bool primitiveRestartEnabled, bool disableBlending, ShaderProgram vertexProgram, ShaderProgram pixelProgram) =>
cache.CreateGraphicsPipeline(colors, in depth, vertexInput, pixelInput, context, in rendering, topology, primitiveRestartEnabled, disableBlending, vertexProgram, pixelProgram);
public ComputeProgram GetComputeProgram(ComputeStageRegisters compute, ShaderInterfaceRegisters shaderInterface, uint dispatchInitiator, uint dimensionX, uint dimensionY, uint dimensionZ) =>
throw new InvalidOperationException("The caching provider has no compute program.");
public PipelineHandle CreateComputePipeline(ComputeInputInfo input, ShaderProgram program) => cache.CreateComputePipeline(input, program);
}
[Fact]
public void NoAttachments_TakeTheSampleCountFromTheAntialiasingConfig()
{
var banks = Banks();
banks.Context.AntialiasingConfig.SampleCountLog2 = 2;
var programs = Programs();
var rendering = new RenderingState { Samples = 1 };
var description = ShaderPipelineCache.BuildGraphicsDescription(
[], default, programs.VertexInput, programs.PixelInput, banks.Context, in rendering, PrimitiveTopology.TriangleList, false, false,
programs.Vertex, programs.Pixel, SampleCountFlags.Count1Bit | SampleCountFlags.Count4Bit);
Assert.Equal(4u, description.StaticParameters.Samples);
Assert.Equal(0u, description.StaticParameters.ColorCount);
}
[Theory]
[InlineData(0x0u, 0x0u)]
[InlineData(0xFu, 0xFu)]
public void ColorTarget_TheProgramNeverExports_IsNotWritten(uint exportMasks, uint expectedMask)
{
// Astro Bot's depth-only pattern pass keeps a color target bound but exports only to
// the null target; writing the undefined host output drew a rectangle over the movie.
var banks = Banks();
banks.Context.RenderTargetMask = 0xF;
var programs = Programs(pixelStage: Stage(new ShaderProgramInfo { Stage = ShaderStageKind.Pixel, PixelColorExportMasks = exportMasks }));
var resolution = new ColorTargetResolution(
default, 0x1000, 0x10000, new Extent2D(64, 64), 0, 0, 1, ColorComponentMap.Identity, false, false, default);
ColorTargetState[] colors = [new(in resolution, 0, new SharpEmu.Libs.Gpu.Buffers.ResourceSlotIdentifier(1, 1))];
var rendering = new RenderingState { Samples = 1, ColorAttachmentCount = 1 };
rendering.ColorAttachments[0] = new RenderingAttachment(
default, ImageLayout.ColorAttachmentOptimal, Format.R8G8B8A8Unorm, 0, 0, 0, 0, false, false, false, false, false);
var description = ShaderPipelineCache.BuildGraphicsDescription(
colors, default, programs.VertexInput, programs.PixelInput, banks.Context, in rendering, PrimitiveTopology.TriangleList, false, false,
programs.Vertex, programs.Pixel, SampleCountFlags.Count1Bit);
Assert.Equal(expectedMask, description.StaticParameters.GetColorMask(0));
}
[Fact]
public void ColorTarget_WithoutAPixelStage_IsNotWritten()
{
var banks = Banks();
banks.Context.RenderTargetMask = 0xF;
var programs = Programs();
var resolution = new ColorTargetResolution(
default, 0x1000, 0x10000, new Extent2D(64, 64), 0, 0, 1, ColorComponentMap.Identity, false, false, default);
ColorTargetState[] colors = [new(in resolution, 0, new SharpEmu.Libs.Gpu.Buffers.ResourceSlotIdentifier(1, 1))];
var rendering = new RenderingState { Samples = 1, ColorAttachmentCount = 1 };
rendering.ColorAttachments[0] = new RenderingAttachment(
default, ImageLayout.ColorAttachmentOptimal, Format.R8G8B8A8Unorm, 0, 0, 0, 0, false, false, false, false, false);
var description = ShaderPipelineCache.BuildGraphicsDescription(
colors, default, programs.VertexInput, null, banks.Context, in rendering, PrimitiveTopology.TriangleList, false, false,
programs.Vertex, programs.Pixel, SampleCountFlags.Count1Bit);
Assert.Equal(0u, description.StaticParameters.GetColorMask(0));
}
[Fact]
public void NoAttachments_AtAnUnsupportedSampleCount_AreFatal()
{
var banks = Banks();
banks.Context.AntialiasingConfig.SampleCountLog2 = 3;
var programs = Programs();
var rendering = new RenderingState { Samples = 1 };
var fatal = Assert.Throws<SchedulerFatalException>(() => ShaderPipelineCache.BuildGraphicsDescription(
[], default, programs.VertexInput, programs.PixelInput, banks.Context, in rendering, PrimitiveTopology.TriangleList, false, false,
programs.Vertex, programs.Pixel, SampleCountFlags.Count1Bit | SampleCountFlags.Count4Bit));
Assert.Contains("samples=8", fatal.Message);
}
[Theory]
[InlineData(true, 2, true)]
[InlineData(false, 2, false)]
[InlineData(true, 0, false)]
public void SampleShading_IsDerivedFromThePixelInputsAndTheSampleCount(bool pixelSampleShading, byte sampleCountLog2, bool expected)
{
var banks = Banks();
banks.Context.AntialiasingConfig.SampleCountLog2 = sampleCountLog2;
var programs = Programs();
var pixelInput = new PixelInputInfo { InputCount = 1, SampleShading = pixelSampleShading, Stage = programs.PixelInput.Stage };
var rendering = new RenderingState { Samples = 1 };
var description = ShaderPipelineCache.BuildGraphicsDescription(
[], default, programs.VertexInput, pixelInput, banks.Context, in rendering, PrimitiveTopology.TriangleList, false, false,
programs.Vertex, programs.Pixel, SampleCountFlags.Count1Bit | SampleCountFlags.Count4Bit);
Assert.Equal(expected, description.StaticParameters.SampleShadingEnable);
}
[Fact]
public void MissingPrograms_AreFatal()
{
var banks = Banks();
var programs = Programs();
var rendering = new RenderingState { Samples = 1 };
var noVertex = Assert.Throws<SchedulerFatalException>(() => ShaderPipelineCache.BuildGraphicsDescription(
[], default, programs.VertexInput, programs.PixelInput, banks.Context, in rendering, PrimitiveTopology.TriangleList, false, false,
default, programs.Pixel, SampleCountFlags.Count1Bit));
Assert.Contains("no vertex program", noVertex.Message);
var noPixel = Assert.Throws<SchedulerFatalException>(() => ShaderPipelineCache.BuildGraphicsDescription(
[], default, programs.VertexInput, programs.PixelInput, banks.Context, in rendering, PrimitiveTopology.TriangleList, false, false,
programs.Vertex, default, SampleCountFlags.Count1Bit));
Assert.Contains("without a pixel program", noPixel.Message);
}
}