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

228 lines
13 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..]));
}
}
// V_CVT_PKRTZ_F16_F32 rounds toward zero: finite values past the f16 range saturate at
// +-65504 instead of becoming infinity, and subnormal results keep whole 2^-24 steps.
[Fact]
public void PackToHalfRoundsTowardZeroWithoutOverflowingToInfinity()
{
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(5003.7f)),
MoveScalar(12, 9, Bits(-80000.3f)),
MoveScalar(20, 10, Bits(3.3e-6f)),
MoveScalar(28, 11, Bits(-5e-5f)),
MoveScalar(36, 12, Bits(float.NaN)),
MoveScalar(44, 13, Bits(float.PositiveInfinity)),
Vop3(52, "VMadF32", 2, Gen5Operand.Vector(1), Gen5Operand.Scalar(8), Gen5Operand.Scalar(9)),
Vop3(60, "VMadF32", 3, Gen5Operand.Vector(1), Gen5Operand.Scalar(10), Gen5Operand.Scalar(11)),
Vop2(68, "VCvtPkrtzF16F32", 12, Gen5Operand.Vector(2), Gen5Operand.Vector(3)),
Vop3(72, "VCvtPkrtzF16F32", 13, Gen5Operand.Scalar(12), Gen5Operand.Scalar(13)),
Vop2(80, "VLshlrevB32", 7, Operand(5), Gen5Operand.Vector(0)),
BufferAccess(84, "BufferStoreDword", 4, vectorData: 12, offsetEnabled: true, vectorAddress: 7),
BufferAccess(92, "BufferStoreDword", 4, offset: 4, vectorData: 13, offsetEnabled: true, vectorAddress: 7),
EndProgram(100));
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 large = MathF.FusedMultiplyAdd(lane, 5003.7f, -80000.3f);
var small = MathF.FusedMultiplyAdd(lane, 3.3e-6f, -5e-5f);
var packed = BinaryPrimitives.ReadUInt32LittleEndian(row);
Assert.Equal(HalfTowardZero(large), packed & 0xFFFF);
Assert.Equal(HalfTowardZero(small), packed >> 16);
var special = BinaryPrimitives.ReadUInt32LittleEndian(row[4..]);
Assert.True((special & 0x7C00) == 0x7C00 && (special & 0x3FF) != 0, $"lane {lane}: 0x{special:X8}");
Assert.Equal(0x7C00u, special >> 16);
}
}
private static uint HalfTowardZero(float value)
{
var sign = value < 0 || (value == 0 && float.IsNegative(value)) ? 0x8000u : 0u;
var magnitude = MathF.Abs(value);
if (magnitude >= 65504f) return sign | 0x7BFF;
if (magnitude < 6.10351562e-05f) return sign | (uint)Math.Floor(magnitude * 16777216.0);
var bits = BitConverter.SingleToUInt32Bits(magnitude);
return sign | ((((bits >> 23) - 127 + 15) << 10) | ((bits >> 13) & 0x3FF));
}
private static uint Bits(float value) => BitConverter.SingleToUInt32Bits(value);
}