sharpemu/tests/SharpEmu.Libs.Tests/VideoOut/RenderHostDeviceTests.Inter...

156 lines
7.1 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE.Host;
using SharpEmu.Libs.Gpu.GpuCommands;
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Gpu.Vulkan;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
using static SharpEmu.Libs.Tests.Gpu.Images.ImageCacheTestSupport;
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
namespace SharpEmu.Libs.Tests.VideoOut;
public sealed unsafe partial class RenderHostDeviceTests
{
[Theory]
[InlineData(0u, true, 191)]
[InlineData(1u, true, 64)]
[InlineData(2u, true, 64)]
[InlineData(0u, false, 128)]
[InlineData(1u, false, 0)]
[InlineData(2u, false, 64)]
public void InterpolationParameter_DrawReadsSelectedVertex(uint selector, bool custom, int expected)
{
VerifyInterpolationDraw(selector, custom, interpolate: false, expected);
}
[Fact]
public void InterpolationPhases_DrawUsesBarycentricRegisters()
{
VerifyInterpolationDraw(2, false, interpolate: true, expected: 96);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void RectangleList_PreservesInterpolationAndProvokingVertexOutsideTheInputTriangle(bool flat)
{
if (!Ready()) return;
if (!_vulkan.SupportsFillRectangle || !_vulkan.SupportsFragmentShaderBarycentric)
{
Assert.False(GatePrerequisites.DeviceRequired, "The required device lacks rectangle fill or fragment barycentrics.");
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var target = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var words = RegisterWords.Color(target, Size, Size);
harness.Write(vertices, Triangle(-1, -1, 1, -1, -1, 1));
var provider = new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices,
interpolationShader: CompileInterpolationFragment(2, flat, !flat));
var banks = Banks(words);
banks.UserConfig.PrimitiveType = 7;
banks.Context.ShaderInterface.GeometryOutputPrimitiveType = 3;
var executor = new RenderExecutor(presenter.RenderHost, provider);
presenter.Run(() => executor.DrawAuto(1, banks, Draw()));
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
harness.Finish();
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
// This corner lies outside the input triangle. The smooth value continues its
// plane across the rectangle; flat input still comes from original vertex zero.
var actual = Pixel(pixels, Size - 2, Size - 2);
var expected = flat ? 64 : 126;
Assert.InRange((int)(actual & 255), expected - 1, expected + 1);
Assert.Equal(255u, actual >> 24);
harness.Shutdown();
_vulkan.AssertNoValidationMessages();
}
[Fact]
public void InterpolationShader_RejectsMissingDeviceFeatureBeforeModuleCreation()
{
if (!Ready()) return;
using var presenter = new PresenterUnderTest(_vulkan);
presenter.SetField("_supportsFragmentShaderBarycentric", false);
var shader = new VulkanCompiledGuestShader(CompileInterpolationFragment(2, true, false));
presenter.Run(() =>
{
var error = Assert.Throws<NotSupportedException>(() =>
((IShaderPipelineHost)presenter.Instance).CreateShaderModule(shader, ShaderStage.Pixel, 1, 1));
Assert.Contains("fragmentShaderBarycentric", error.Message);
});
presenter.Harness.Shutdown();
}
private void VerifyInterpolationDraw(uint selector, bool custom, bool interpolate, int expected)
{
if (!Ready()) return;
if (!_vulkan.SupportsFragmentShaderBarycentric)
{
Assert.False(GatePrerequisites.DeviceRequired, "The required device lacks fragmentShaderBarycentric.");
Console.Error.WriteLine("[TEST][SKIP] The device lacks fragmentShaderBarycentric.");
return;
}
using var presenter = new PresenterUnderTest(_vulkan);
presenter.LoadRenderingCommands();
var harness = presenter.Harness;
var target = harness.MapBacked(0x10000, ReadWrite);
var vertices = harness.MapBacked(0x10000, ReadWrite);
var words = RegisterWords.Color(target, Size, Size);
harness.Write(vertices, Triangle(-1f, -1f, 3f, -1f, -1f, 3f));
var provider = new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices,
interpolationShader: CompileInterpolationFragment(selector, custom, interpolate));
var executor = new RenderExecutor(presenter.RenderHost, provider);
presenter.Run(() => executor.DrawAuto(1, Banks(words), Draw()));
presenter.Run(() => presenter.InvokeMethod("FlushBatchedGuestCommands"));
harness.Finish();
var pixels = harness.ReadImageBytes(TargetImage(presenter, words));
var actual = Pixel(pixels, Size / 2, Size / 2);
Assert.InRange((int)(actual & 255), Math.Max(0, expected - 1), expected + 1);
Assert.Equal(255u, actual >> 24);
harness.Shutdown();
_vulkan.AssertNoValidationMessages();
}
private static byte[] CompileInterpolationFragment(uint selector, bool custom, bool interpolate)
{
var instructions = new List<Gen5ShaderInstruction>();
void Parameter(string opcode, uint source) => instructions.Add(new Gen5ShaderInstruction(
(uint)instructions.Count * 4, Gen5ShaderEncoding.Vintrp, opcode, [source],
[Gen5Operand.Vector(source)], [Gen5Operand.Vector(4)], new Gen5InterpolationControl(0, 0)));
Parameter("VInterpMovF32", selector);
if (interpolate)
{
Parameter("VInterpP1F32", 0);
Parameter("VInterpP2F32", 1);
}
instructions.Add(MoveVector((uint)instructions.Count * 4, 5, 0x3F800000));
instructions.Add(new Gen5ShaderInstruction((uint)instructions.Count * 4, Gen5ShaderEncoding.Exp,
"Exp", [], [Gen5Operand.Vector(4), Gen5Operand.Vector(4), Gen5Operand.Vector(4), Gen5Operand.Vector(5)],
[], new Gen5ExportControl(0, 15, false, true, true)));
instructions.Add(EndProgram((uint)instructions.Count * 4));
var (plan, resources, layout) = Prepare(Program([.. instructions]), ShaderStage.Pixel, userDataCount: 0);
var request = new ShaderCompileRequest(plan, resources, layout)
{
PixelInputAddress = 2,
PixelInputEnable = 2,
PixelInputCntl = [custom ? 0x400u : 0u],
PixelCustomInterpolationMask = custom ? 1u : 0u,
PixelOutputs = [new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float)],
EnableGraphicsSubgroupOperations = false,
};
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
return shader.Spirv;
}
}