sharpemu/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/FloatSemanticsDeviceTests.cs

167 lines
9.8 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
namespace SharpEmu.Libs.Tests.Gpu.Vulkan;
public sealed class FloatSemanticsDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture<HeadlessVulkanFixture>
{
private const uint PackBase = 0xAABB_CCDD;
private const uint ThreadCount = 32;
private const uint RowBytes = 32;
[Fact]
public void ConversionsAndNanOperandsMatchTheHardwareRules()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var program = Program(
Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(0)),
MoveScalar(4, 8, Bits(0.25f)),
MoveScalar(12, 9, Bits(-2f)),
MoveScalar(20, 10, Bits(20f)),
MoveScalar(28, 11, Bits(-60f)),
MoveScalar(36, 12, Bits(float.NaN)),
MoveScalar(44, 13, PackBase),
MoveScalar(52, 14, Bits(5f)),
MoveScalar(60, 15, Bits(10f)),
Vop3(68, "VMadF32", 2, Gen5Operand.Vector(1), Gen5Operand.Scalar(8), Gen5Operand.Scalar(9)),
Vop3(76, "VMadF32", 3, Gen5Operand.Vector(1), Gen5Operand.Scalar(10), Gen5Operand.Scalar(11)),
Vop1(84, "VCvtRpiI32F32", 12, Gen5Operand.Vector(2)),
Vop3(88, "VCvtPkU8F32", 13, Gen5Operand.Vector(3), Operand(1), Gen5Operand.Scalar(13)),
Vop2(96, "VMaxF32", 14, Gen5Operand.Scalar(12), Gen5Operand.Vector(1)),
Vop2(100, "VMinF32", 15, Gen5Operand.Vector(1), Gen5Operand.Scalar(12)),
Vop3(104, "VMed3F32", 16, Gen5Operand.Scalar(12), Gen5Operand.Vector(1), Gen5Operand.Scalar(14)),
Vop3(112, "VMed3F32", 17, Gen5Operand.Vector(1), Gen5Operand.Scalar(14), Gen5Operand.Scalar(15)),
Vop2(120, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)),
BufferAccess(124, "BufferStoreDwordx4", 4, vectorData: 12, dwords: 4, offsetEnabled: true, vectorAddress: 7),
BufferAccess(132, "BufferStoreDword", 4, offset: 16, vectorData: 16, offsetEnabled: true, vectorAddress: 7),
BufferAccess(140, "BufferStoreDword", 4, offset: 20, vectorData: 17, offsetEnabled: true, vectorAddress: 7),
EndProgram(148));
var (plan, resources, layout) = Prepare(program);
var request = new ShaderCompileRequest(plan, resources, layout)
{
LocalSizeX = ThreadCount, ThreadCountX = ThreadCount, WaveSize = 32,
};
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
using var harness = new ImageTestHarness(vulkan);
using var runner = new LayoutComputeRunner(harness, request, shader.Spirv);
var result = runner.CreateBuffer(ThreadCount * RowBytes);
var registers = new uint[256];
registers[6] = ThreadCount * RowBytes;
harness.Run(() => runner.Dispatch(registers,
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [result] }, 1));
var actual = runner.ReadBack(result, 0, ThreadCount * RowBytes);
for (var lane = 0u; lane < ThreadCount; lane++)
{
var row = actual.AsSpan((int)(lane * RowBytes));
var rounded = (int)MathF.Floor(lane * 0.25f - 2f + 0.5f);
var packed = (uint)Math.Clamp(lane * 20f - 60f, 0f, 255f);
Assert.Equal(rounded, BinaryPrimitives.ReadInt32LittleEndian(row));
Assert.Equal((PackBase & ~0xFF00u) | (packed << 8), BinaryPrimitives.ReadUInt32LittleEndian(row[4..]));
Assert.Equal(Bits(lane), BinaryPrimitives.ReadUInt32LittleEndian(row[8..]));
Assert.Equal(Bits(lane), BinaryPrimitives.ReadUInt32LittleEndian(row[12..]));
Assert.Equal(Bits(MathF.Min(lane, 5f)), BinaryPrimitives.ReadUInt32LittleEndian(row[16..]));
Assert.Equal(Bits(Math.Clamp(lane, 5f, 10f)), BinaryPrimitives.ReadUInt32LittleEndian(row[20..]));
}
}
[Fact]
public void ConditionalMaskAppliesSourceSignModifiers()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var negateFirst = new Gen5Vop3Control(0, 1, 0, false, 0, null);
var absoluteBothNegateSecond = new Gen5Vop3Control(3, 2, 0, false, 0, null);
var program = Program(
Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(0)),
MoveScalar(4, 8, Bits(1f)),
MoveScalar(12, 9, Bits(-8f)),
MoveScalar(20, 20, uint.MaxValue),
MoveScalar(28, 21, uint.MaxValue),
MoveScalar(36, 22, 0),
MoveScalar(44, 23, 0),
Vop3(52, "VMadF32", 2, Gen5Operand.Vector(1), Gen5Operand.Scalar(8), Gen5Operand.Scalar(9)),
Vop3(60, "VCndmaskB32", 12, Gen5Operand.Vector(2), Gen5Operand.Vector(2), Gen5Operand.Scalar(22)) with { Control = negateFirst },
Vop3(68, "VCndmaskB32", 13, Gen5Operand.Vector(2), Gen5Operand.Vector(2), Gen5Operand.Scalar(20)) with { Control = negateFirst },
Vop3(76, "VCndmaskB32", 14, Gen5Operand.Vector(2), Gen5Operand.Vector(2), Gen5Operand.Scalar(20)) with { Control = absoluteBothNegateSecond },
Vop3(84, "VCndmaskB32", 15, Gen5Operand.Vector(2), Gen5Operand.Vector(2), Gen5Operand.Scalar(22)) with { Control = absoluteBothNegateSecond },
Vop2(92, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)),
BufferAccess(96, "BufferStoreDwordx4", 4, vectorData: 12, dwords: 4, offsetEnabled: true, vectorAddress: 7),
EndProgram(104));
var (plan, resources, layout) = Prepare(program);
var request = new ShaderCompileRequest(plan, resources, layout)
{
LocalSizeX = ThreadCount, ThreadCountX = ThreadCount, WaveSize = 32,
};
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
using var harness = new ImageTestHarness(vulkan);
using var runner = new LayoutComputeRunner(harness, request, shader.Spirv);
var result = runner.CreateBuffer(ThreadCount * RowBytes);
var registers = new uint[256];
registers[6] = ThreadCount * RowBytes;
harness.Run(() => runner.Dispatch(registers,
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [result] }, 1));
var actual = runner.ReadBack(result, 0, ThreadCount * RowBytes);
for (var lane = 0u; lane < ThreadCount; lane++)
{
var row = actual.AsSpan((int)(lane * RowBytes));
var value = Bits(lane - 8f);
Assert.Equal(value ^ 0x8000_0000, BinaryPrimitives.ReadUInt32LittleEndian(row));
Assert.Equal(value, BinaryPrimitives.ReadUInt32LittleEndian(row[4..]));
Assert.Equal(value | 0x8000_0000, BinaryPrimitives.ReadUInt32LittleEndian(row[8..]));
Assert.Equal(value & 0x7FFF_FFFF, BinaryPrimitives.ReadUInt32LittleEndian(row[12..]));
}
}
// op_sel_hi marks a V_FMA_MIX operand as f16, literals included: 0x34CD is 0.3, not an
// f32 denormal. Silent Hill weights a luminance with such literals and divides by it.
[Fact]
public void FusedMixReadsHalfSelectedLiteralsAsHalfFloats()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var halfLiteral = new Gen5Vop3pControl(0, 0b001, 0, 0, false);
var floatLiteral = new Gen5Vop3pControl(0, 0b000, 0, 0, false);
var program = Program(
Vop1(0, "VCvtF32U32", 1, Gen5Operand.Vector(0)),
Vop3(4, "VFmaMixF32", 12, new Gen5Operand(Gen5OperandKind.LiteralConstant, 0x34CD), Gen5Operand.Vector(1), Operand(0)) with { Control = halfLiteral },
Vop3(16, "VFmaMixF32", 13, new Gen5Operand(Gen5OperandKind.LiteralConstant, Bits(0.25f)), Gen5Operand.Vector(1), Operand(0)) with { Control = floatLiteral },
Vop2(28, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)),
BufferAccess(32, "BufferStoreDword", 4, vectorData: 12, offsetEnabled: true, vectorAddress: 7),
BufferAccess(40, "BufferStoreDword", 4, offset: 4, vectorData: 13, offsetEnabled: true, vectorAddress: 7),
EndProgram(48));
var (plan, resources, layout) = Prepare(program);
var request = new ShaderCompileRequest(plan, resources, layout)
{
LocalSizeX = ThreadCount, ThreadCountX = ThreadCount, WaveSize = 32,
};
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
using var harness = new ImageTestHarness(vulkan);
using var runner = new LayoutComputeRunner(harness, request, shader.Spirv);
var result = runner.CreateBuffer(ThreadCount * RowBytes);
var registers = new uint[256];
registers[6] = ThreadCount * RowBytes;
harness.Run(() => runner.Dispatch(registers,
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [result] }, 1));
var actual = runner.ReadBack(result, 0, ThreadCount * RowBytes);
var weight = (float)BitConverter.UInt16BitsToHalf(0x34CD);
for (var lane = 0u; lane < ThreadCount; lane++)
{
var row = actual.AsSpan((int)(lane * RowBytes));
Assert.Equal(Bits(MathF.FusedMultiplyAdd(weight, lane, 0f)), BinaryPrimitives.ReadUInt32LittleEndian(row));
Assert.Equal(Bits(lane * 0.25f), BinaryPrimitives.ReadUInt32LittleEndian(row[4..]));
}
}
private static uint Bits(float value) => BitConverter.SingleToUInt32Bits(value);
}