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

116 lines
5.7 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
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
{
// This is the wave-minimum idiom at 0x80003C5100/0x1CC, isolated from the
// texture loop. A tiny primitive leaves lane 31 absent from its fragment
// subgroup. Red exports every live lane's input, green exports the reduced
// result, so the CPU can compare it with the true minimum of the live lanes.
[Fact]
public void PixelWaveMinimum_UsesItsOwnValueForAbsentSubgroupLanes()
{
if (!Ready()) 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);
// Four pixels at most: deliberately much smaller than an NVIDIA wave32.
harness.Write(vertices, Triangle(-1f, -1f, -0.875f, -1f, -1f, -0.875f));
var provider = new FixedProgramProvider((IShaderPipelineHost)presenter.Instance, vertices,
interpolationShader: CompilePixelWaveMinimum());
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 covered = Enumerable.Range(0, checked((int)(Size * Size)))
.Select(index => BitConverter.ToUInt32(pixels, index * sizeof(uint)))
.Where(value => value >> 24 == byte.MaxValue)
.ToArray();
Assert.InRange(covered.Length, 1, 31);
var trueMinimum = covered.Min(value => value & byte.MaxValue);
Assert.All(covered, value =>
{
Assert.Equal(trueMinimum, (value >> 8) & byte.MaxValue);
Assert.Equal(byte.MaxValue, value >> 24);
});
harness.Shutdown();
_vulkan.AssertNoValidationMessages();
}
private static byte[] CompilePixelWaveMinimum()
{
const uint One = 0x3F80_0000;
const uint OneOver32 = 0x3D00_0000;
var instructions = new List<Gen5ShaderInstruction>();
void Add(Gen5ShaderInstruction instruction) => instructions.Add(instruction with { Pc = (uint)instructions.Count * 8 });
// v14 = subgroup invocation id + 1. It makes every present lane distinct.
Add(Vop2(0, "VMbcntLoU32B32", 14, Operand(uint.MaxValue), Operand(1)));
Add(Vopc(0, "VCmpEqU32", Gen5Operand.Vector(14), 14));
Add(Sop2(0, "SAndB64", 6, Gen5Operand.Scalar(126), Gen5Operand.Scalar(106)));
// 0xBEEA287E: save EXEC and force it, exactly like the game's reduction.
Add(Sop1(0, "SOrn2SaveexecB64", 106, Gen5Operand.Scalar(126)));
Add(new Gen5ShaderInstruction(0, Gen5ShaderEncoding.Vop3, "VCndmaskB32", [0, 0],
[Operand(uint.MaxValue), Gen5Operand.Vector(14), Gen5Operand.Scalar(6)], [Gen5Operand.Vector(13)],
new Gen5Vop3Control(0, 0, 0, false, 0, null)));
foreach (var shift in new uint[] { 1, 2, 4, 8 })
{
// 0x261A1AFA_FF011?0D: v_min_u32 v13, v13, v13 row_shr:shift.
Add(Vop2(0, "VMinU32", 13, Gen5Operand.Vector(13), Gen5Operand.Vector(13)) with
{
Control = new Gen5DppControl(0x110 + shift, false, false, 0, 0, 15, 15),
});
}
// 0xD778100C_0305830D: exchange the two rows, then reduce them.
Add(Vop3(0, "VPermlanex16B32", 12, Gen5Operand.Vector(13), Operand(uint.MaxValue), Operand(uint.MaxValue)) with
{
Control = new Gen5Vop3Control(0, 0, 0, false, 2, null),
});
Add(Vop2(0, "VMinU32", 13, Gen5Operand.Vector(13), Gen5Operand.Vector(12)));
Add(Sop1(0, "SMovB64", 126, Gen5Operand.Scalar(106)));
Add(ReadLane(0, 6, 13, 31));
Add(ReadLane(0, 7, 13, 63));
Add(Sop2(0, "SMinU32", 8, Gen5Operand.Scalar(6), Gen5Operand.Scalar(7)));
// Red is the input, green is the shared reduction, both normalized by 32.
Add(Vop1(0, "VCvtF32U32", 0, Gen5Operand.Vector(14)));
Add(MoveVectorFromScalar(0, 1, 8));
Add(Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(1)));
Add(Vop2(0, "VMulF32", 0, Gen5Operand.Vector(0), Operand(OneOver32)));
Add(Vop2(0, "VMulF32", 1, Gen5Operand.Vector(1), Operand(OneOver32)));
Add(MoveVector(0, 2, One));
Add(MoveVector(0, 3, One));
Add(new Gen5ShaderInstruction(0, Gen5ShaderEncoding.Exp, "Exp", [],
[Gen5Operand.Vector(0), Gen5Operand.Vector(1), Gen5Operand.Vector(2), Gen5Operand.Vector(3)],
[], new Gen5ExportControl(0, 15, false, true, true)));
Add(EndProgram(0));
var (plan, resources, layout) = Prepare(Program([.. instructions]), ShaderStage.Pixel, userDataCount: 0);
var request = new ShaderCompileRequest(plan, resources, layout)
{
PixelOutputs = [new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float)],
EnableGraphicsSubgroupOperations = true,
};
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
return shader.Spirv;
}
}