Merge #940 (pr/shader-isa-fixes)
This commit is contained in:
commit
f254d5370a
|
|
@ -1074,14 +1074,21 @@ public static partial class Gen5MslTranslator
|
|||
var immediate = unchecked((uint)(short)(instruction.Words[0] & 0xFFFF));
|
||||
if (instruction.Opcode.StartsWith("SCmpk", StringComparison.Ordinal))
|
||||
{
|
||||
// The unsigned compares take SIMM16 zero-extended; only the signed ones sign-extend it.
|
||||
if (instruction.Opcode.EndsWith("U32", StringComparison.Ordinal))
|
||||
{
|
||||
immediate = instruction.Words[0] & 0xFFFF;
|
||||
}
|
||||
|
||||
return TryEmitScalarCompareK(instruction, destination, immediate, out error);
|
||||
}
|
||||
|
||||
var current = ScalarExpression(destination);
|
||||
var value = instruction.Opcode switch
|
||||
{
|
||||
"SMovkI32" => FormatUInt(immediate),
|
||||
"SAddkI32" => $"({ScalarExpression(destination)} + {FormatUInt(immediate)})",
|
||||
"SMulkI32" => $"({ScalarExpression(destination)} * {FormatUInt(immediate)})",
|
||||
"SAddkI32" => $"({current} + {FormatUInt(immediate)})",
|
||||
"SMulkI32" => $"({current} * {FormatUInt(immediate)})",
|
||||
_ => string.Empty,
|
||||
};
|
||||
if (value.Length == 0)
|
||||
|
|
@ -1090,7 +1097,17 @@ public static partial class Gen5MslTranslator
|
|||
return false;
|
||||
}
|
||||
|
||||
StoreScalar(destination, Temp("uint", value));
|
||||
var stored = Temp("uint", value);
|
||||
// RDNA2 ISA: S_ADDK_I32 writes SCC = signed overflow, exactly like
|
||||
// S_ADD_I32. S_MOVK_I32 and S_MULK_I32 leave SCC alone.
|
||||
if (instruction.Opcode == "SAddkI32")
|
||||
{
|
||||
var addend = FormatUInt(immediate);
|
||||
Line(
|
||||
$"scc = ((~({current} ^ {addend}) & ({current} ^ {stored})) >> 31) != 0u;");
|
||||
}
|
||||
|
||||
StoreScalar(destination, stored);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,10 +147,7 @@ public static partial class Gen5SpirvTranslator
|
|||
break;
|
||||
}
|
||||
case "VCvtU32F32":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.ConvertFToU,
|
||||
_uintType,
|
||||
GetFloatSource(instruction, 0));
|
||||
result = ConvertFloatToUnsignedSaturated(GetFloatSource(instruction, 0));
|
||||
break;
|
||||
case "VCvtU16F16":
|
||||
{
|
||||
|
|
@ -185,9 +182,7 @@ public static partial class Gen5SpirvTranslator
|
|||
source = Ext(8, _floatType, source);
|
||||
}
|
||||
|
||||
result = Bitcast(
|
||||
_uintType,
|
||||
_module.AddInstruction(SpirvOp.ConvertFToS, _intType, source));
|
||||
result = ConvertFloatToSignedSaturated(source);
|
||||
break;
|
||||
}
|
||||
case "VCvtF32I32":
|
||||
|
|
@ -2520,6 +2515,12 @@ public static partial class Gen5SpirvTranslator
|
|||
var immediate = unchecked((uint)(short)(instruction.Words[0] & 0xFFFF));
|
||||
if (instruction.Opcode.StartsWith("SCmpk", StringComparison.Ordinal))
|
||||
{
|
||||
// The unsigned compares take SIMM16 zero-extended; only the signed ones sign-extend it.
|
||||
if (instruction.Opcode.EndsWith("U32", StringComparison.Ordinal))
|
||||
{
|
||||
immediate = instruction.Words[0] & 0xFFFF;
|
||||
}
|
||||
|
||||
return TryEmitScalarCompareK(instruction, destination, immediate, out error);
|
||||
}
|
||||
|
||||
|
|
@ -2541,6 +2542,13 @@ public static partial class Gen5SpirvTranslator
|
|||
return false;
|
||||
}
|
||||
|
||||
// RDNA2 ISA: S_ADDK_I32 writes SCC = signed overflow, exactly like
|
||||
// S_ADD_I32. S_MOVK_I32 and S_MULK_I32 leave SCC alone.
|
||||
if (instruction.Opcode == "SAddkI32")
|
||||
{
|
||||
Store(_scc, SignedAddOverflow(current, UInt(immediate), value));
|
||||
}
|
||||
|
||||
StoreS(destination, value);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -4440,14 +4448,15 @@ public static partial class Gen5SpirvTranslator
|
|||
_module.AddInstruction(operation, _floatType, left, right));
|
||||
}
|
||||
|
||||
// v_min_f32 and v_max_f32 return the other operand when one is NaN. GLSL FMin and FMax
|
||||
// leave that undefined, and Metal's fast math returns the NaN.
|
||||
private uint EmitFloatExtBinary(
|
||||
Gen5ShaderInstruction instruction,
|
||||
uint operation) =>
|
||||
EmitFloatResult(
|
||||
instruction,
|
||||
Ext(
|
||||
NanIgnoringMinMax(
|
||||
operation,
|
||||
_floatType,
|
||||
GetFloatSource(instruction, 0),
|
||||
GetFloatSource(instruction, 1)));
|
||||
|
||||
|
|
@ -4455,14 +4464,62 @@ public static partial class Gen5SpirvTranslator
|
|||
Gen5ShaderInstruction instruction,
|
||||
uint operation)
|
||||
{
|
||||
var first = Ext(
|
||||
var first = NanIgnoringMinMax(
|
||||
operation,
|
||||
_floatType,
|
||||
GetFloatSource(instruction, 0),
|
||||
GetFloatSource(instruction, 1));
|
||||
return EmitFloatResult(
|
||||
instruction,
|
||||
Ext(operation, _floatType, first, GetFloatSource(instruction, 2)));
|
||||
NanIgnoringMinMax(operation, first, GetFloatSource(instruction, 2)));
|
||||
}
|
||||
|
||||
private uint NanIgnoringMinMax(uint operation, uint left, uint right)
|
||||
{
|
||||
var result = Ext(operation, _floatType, left, right);
|
||||
result = _module.AddInstruction(SpirvOp.Select, _floatType, IsNanBits(right), left, result);
|
||||
return _module.AddInstruction(SpirvOp.Select, _floatType, IsNanBits(left), right, result);
|
||||
}
|
||||
|
||||
// NaN tested on the bits, which fast math cannot assume away.
|
||||
private uint IsNanBits(uint value) =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.UGreaterThan,
|
||||
_boolType,
|
||||
BitwiseAnd(Bitcast(_uintType, value), UInt(0x7FFF_FFFF)),
|
||||
UInt(0x7F80_0000));
|
||||
|
||||
// v_cvt_u32_f32 and v_cvt_i32_f32 saturate and convert NaN to zero; SPIR-V leaves
|
||||
// out-of-range conversions undefined.
|
||||
private uint ConvertFloatToUnsignedSaturated(uint value)
|
||||
{
|
||||
var bits = Bitcast(_uintType, value);
|
||||
var converted = _module.AddInstruction(SpirvOp.ConvertFToU, _uintType, value);
|
||||
converted = _module.AddInstruction(
|
||||
SpirvOp.Select, _uintType,
|
||||
_module.AddInstruction(SpirvOp.UGreaterThanEqual, _boolType, bits, UInt(0x4F80_0000)),
|
||||
UInt(uint.MaxValue),
|
||||
converted);
|
||||
// Negative values, including negative NaN, have the sign bit set.
|
||||
converted = _module.AddInstruction(
|
||||
SpirvOp.Select, _uintType,
|
||||
_module.AddInstruction(SpirvOp.UGreaterThanEqual, _boolType, bits, UInt(0x8000_0000)),
|
||||
UInt(0),
|
||||
converted);
|
||||
return _module.AddInstruction(SpirvOp.Select, _uintType, IsNanBits(value), UInt(0), converted);
|
||||
}
|
||||
|
||||
private uint ConvertFloatToSignedSaturated(uint value)
|
||||
{
|
||||
var bits = Bitcast(_uintType, value);
|
||||
var magnitude = BitwiseAnd(bits, UInt(0x7FFF_FFFF));
|
||||
var converted = Bitcast(_uintType, _module.AddInstruction(SpirvOp.ConvertFToS, _intType, value));
|
||||
var negative = _module.AddInstruction(SpirvOp.UGreaterThanEqual, _boolType, bits, UInt(0x8000_0000));
|
||||
converted = _module.AddInstruction(
|
||||
SpirvOp.Select, _uintType,
|
||||
_module.AddInstruction(SpirvOp.UGreaterThanEqual, _boolType, magnitude, UInt(0x4F00_0000)),
|
||||
_module.AddInstruction(SpirvOp.Select, _uintType, negative, UInt(0x8000_0000), UInt(0x7FFF_FFFF)),
|
||||
converted);
|
||||
return _module.AddInstruction(SpirvOp.Select, _uintType, IsNanBits(value), UInt(0), converted);
|
||||
}
|
||||
|
||||
private uint EmitIntegerBinary(
|
||||
|
|
|
|||
|
|
@ -310,6 +310,21 @@ public static partial class Gen5ShaderTranslator
|
|||
return false;
|
||||
}
|
||||
|
||||
// S_CODE_END pads the space after the last instruction. Code placed after the final
|
||||
// S_ENDPGM that ends in a backward branch runs straight into it.
|
||||
if (string.Equals(name, "SCodeEnd", StringComparison.Ordinal))
|
||||
{
|
||||
if (pc < furthestForwardBranchTarget || instructions.Count == 0)
|
||||
{
|
||||
error = $"code-end-inside-program pc=0x{pc:X} branchTarget=0x{furthestForwardBranchTarget:X}";
|
||||
return false;
|
||||
}
|
||||
|
||||
program = new Gen5ShaderProgram(address, instructions);
|
||||
termination = ProgramTermination.EndProgram;
|
||||
return true;
|
||||
}
|
||||
|
||||
var instructionBytes = checked(sizeDwords * sizeof(uint));
|
||||
if (maximumBytes.HasValue && instructionBytes > maximumBytes.Value - pc)
|
||||
{
|
||||
|
|
@ -780,6 +795,7 @@ public static partial class Gen5ShaderTranslator
|
|||
0x18 => "SCbranchCdbguser",
|
||||
0x19 => "SCbranchCdbgsysOrUser",
|
||||
0x1A => "SCbranchCdbgsysAndUser",
|
||||
0x1F => "SCodeEnd",
|
||||
0x20 => "SInstPrefetch",
|
||||
0x21 => "SClause",
|
||||
0x23 => "SWaitcntDepctr",
|
||||
|
|
|
|||
|
|
@ -450,7 +450,13 @@ public sealed class BindingLayout : IEquatable<BindingLayout>
|
|||
}
|
||||
}
|
||||
|
||||
if (instruction.Opcode is "SBitset0B32" or "SBitset1B32")
|
||||
// These read the register their destination field names: the bit sets and the SOPK
|
||||
// accumulations modify it, and the SOPK compares only read it.
|
||||
var comparesDestination = instruction.Encoding == Gen5ShaderEncoding.Sopk &&
|
||||
instruction.Opcode.StartsWith("SCmpk", StringComparison.Ordinal);
|
||||
if (comparesDestination ||
|
||||
instruction.Opcode is "SBitset0B32" or "SBitset1B32" ||
|
||||
instruction.Encoding == Gen5ShaderEncoding.Sopk && instruction.Opcode is "SAddkI32" or "SMulkI32" or "SCmovkI32")
|
||||
{
|
||||
foreach (var destination in instruction.Destinations)
|
||||
{
|
||||
|
|
@ -461,6 +467,11 @@ public sealed class BindingLayout : IEquatable<BindingLayout>
|
|||
}
|
||||
}
|
||||
|
||||
if (comparesDestination)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (instruction.Control)
|
||||
{
|
||||
case Gen5ImageControl image:
|
||||
|
|
|
|||
|
|
@ -49,6 +49,15 @@ public sealed class RuntimeValueValidator
|
|||
ScalarOperation.LogicalOr or ScalarOperation.LogicalAnd or ScalarOperation.LogicalXor or ScalarOperation.LogicalNot or
|
||||
ScalarOperation.FLessThanEqual or ScalarOperation.FGreaterThanEqual or ScalarOperation.FIsNan or
|
||||
ScalarOperation.FMul or ScalarOperation.FTrunc => true,
|
||||
// Every other pure scalar function ScalarOperationSemantics evaluates is as uniform as the
|
||||
// ones above; a descriptor built through them was rejected although the host can compute it.
|
||||
ScalarOperation.UMax32 or ScalarOperation.IAbs32 or ScalarOperation.SMulHi32 or ScalarOperation.UMulHi32 or
|
||||
ScalarOperation.SLessThan32 or ScalarOperation.SLessThanEqual32 or ScalarOperation.SGreaterThan32 or ScalarOperation.SGreaterThanEqual32 or
|
||||
ScalarOperation.ULessThanEqual32 or ScalarOperation.UGreaterThanEqual32 or ScalarOperation.IEqual64 or ScalarOperation.INotEqual64 or
|
||||
ScalarOperation.BitCount32 or ScalarOperation.BitReverse32 or ScalarOperation.FindLowestBit32 or ScalarOperation.FindHighestBit32 or
|
||||
ScalarOperation.QuadMask32 or ScalarOperation.ConvertF32S32 or ScalarOperation.ConvertS32F32 or
|
||||
ScalarOperation.FAdd or ScalarOperation.FSub or ScalarOperation.FMin or ScalarOperation.FMax or
|
||||
ScalarOperation.FEqual or ScalarOperation.FNotEqual or ScalarOperation.FLessThan or ScalarOperation.FGreaterThan => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
// 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;
|
||||
|
||||
// v_cvt_u32_f32 and v_cvt_i32_f32 saturate and turn NaN into zero, and v_min_f32 and
|
||||
// v_max_f32 return the other operand when one is NaN. UE's motion blur turns a NaN velocity
|
||||
// into a sample loop bound this way; with host-defined results the loop never ends.
|
||||
public sealed class FloatNanSemanticsDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture<HeadlessVulkanFixture>
|
||||
{
|
||||
private const uint QuietNan = 0x7FC0_0000;
|
||||
private const uint NegativeNan = 0xFFC0_0000;
|
||||
private const uint PositiveInfinity = 0x7F80_0000;
|
||||
|
||||
[Theory]
|
||||
[InlineData(QuietNan, 0x4000_0000u, 0u, 0u, 0x4000_0000u, 0x4000_0000u)]
|
||||
[InlineData(NegativeNan, 0x4080_0000u, 0u, 0u, 0x4080_0000u, 0x4080_0000u)]
|
||||
[InlineData(0x4000_0000u, QuietNan, 2u, 2u, 0x4000_0000u, 0x4000_0000u)]
|
||||
[InlineData(PositiveInfinity, 0x3F80_0000u, uint.MaxValue, 0x7FFF_FFFFu, 0x3F80_0000u, PositiveInfinity)]
|
||||
[InlineData(0xBF80_0000u, 0x3F80_0000u, 0u, 0xFFFF_FFFFu, 0xBF80_0000u, 0x3F80_0000u)]
|
||||
[InlineData(0x4F95_02F9u, 0x3F80_0000u, uint.MaxValue, 0x7FFF_FFFFu, 0x3F80_0000u, 0x4F95_02F9u)]
|
||||
[InlineData(0xCF32_D05Eu, 0x3F80_0000u, 0u, 0x8000_0000u, 0xCF32_D05Eu, 0x3F80_0000u)]
|
||||
[InlineData(0x406C_CCCDu, 0x3F80_0000u, 3u, 3u, 0x3F80_0000u, 0x406C_CCCDu)]
|
||||
public void ConversionsAndMinMaxFollowTheGuestNanRules(
|
||||
uint value, uint other, uint unsignedResult, uint signedResult, uint minimum, uint maximum)
|
||||
{
|
||||
var vulkan = fixture.Vulkan;
|
||||
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
|
||||
var program = Program(
|
||||
MoveVectorFromScalar(0, 1, 9),
|
||||
Vop1(4, "VCvtU32F32", 2, Gen5Operand.Scalar(8)),
|
||||
Vop1(8, "VCvtI32F32", 3, Gen5Operand.Scalar(8)),
|
||||
Vop2(12, "VMinF32", 5, Gen5Operand.Scalar(8), Gen5Operand.Vector(1)),
|
||||
Vop2(16, "VMaxF32", 6, Gen5Operand.Scalar(8), Gen5Operand.Vector(1)),
|
||||
BufferAccess(20, "BufferStoreDword", 4, offset: 0, vectorData: 2),
|
||||
BufferAccess(28, "BufferStoreDword", 4, offset: 4, vectorData: 3),
|
||||
BufferAccess(36, "BufferStoreDword", 4, offset: 8, vectorData: 5),
|
||||
BufferAccess(44, "BufferStoreDword", 4, offset: 12, vectorData: 6),
|
||||
EndProgram(52));
|
||||
var (plan, resources, layout) = Prepare(program);
|
||||
var request = new ShaderCompileRequest(plan, resources, layout) { LocalSizeX = 1, ThreadCountX = 1 };
|
||||
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(64);
|
||||
var registers = new uint[256];
|
||||
registers[6] = 64;
|
||||
registers[8] = value;
|
||||
registers[9] = other;
|
||||
harness.Run(() => runner.Dispatch(registers,
|
||||
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [result] }, 1));
|
||||
var bytes = runner.ReadBack(result, 0, 4 * sizeof(uint));
|
||||
Assert.Equal(
|
||||
[unsignedResult, signedResult, minimum, maximum],
|
||||
Enumerable.Range(0, 4).Select(index => BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)))).ToArray());
|
||||
harness.AssertNoValidationMessages();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
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;
|
||||
|
||||
// S_CMPK compares an SGPR with SIMM16: zero-extended for the unsigned compares and
|
||||
// sign-extended for the signed ones. NGG geometry programs test "no primitives" with
|
||||
// s_cmpk_le_u32 s4, 0xffff on a packed vertex and primitive count.
|
||||
public sealed class ScalarCompareImmediateDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture<HeadlessVulkanFixture>
|
||||
{
|
||||
private const uint LessEqualSigned = 0x08;
|
||||
private const uint LessEqualUnsigned = 0x0E;
|
||||
|
||||
[Theory]
|
||||
[InlineData(LessEqualUnsigned, 0x0002_0006u, 0xFFFFu, 0u)]
|
||||
[InlineData(LessEqualUnsigned, 0x0000_0010u, 0xFFFFu, 1u)]
|
||||
[InlineData(LessEqualUnsigned, 0x0000_FFFFu, 0xFFFFu, 1u)]
|
||||
[InlineData(LessEqualSigned, 0x0002_0006u, 0xFFFFu, 0u)]
|
||||
[InlineData(LessEqualSigned, 0xFFFF_FFFEu, 0xFFFFu, 1u)]
|
||||
public void CompareUsesTheImmediateExtensionOfItsSignedness(uint opcode, uint value, uint immediate, uint expected)
|
||||
{
|
||||
var vulkan = fixture.Vulkan;
|
||||
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
|
||||
var program = Program(
|
||||
DecodeWords(0x8000_0000u | ((0x60u + opcode) << 23) | (8u << 16) | immediate) with { Pc = 0 },
|
||||
DecodeWords(0x8509_8081u) with { Pc = 4 },
|
||||
MoveVectorFromScalar(8, 2, 9),
|
||||
BufferAccess(12, "BufferStoreDword", 4, vectorData: 2),
|
||||
EndProgram(20));
|
||||
var (plan, resources, layout) = Prepare(program);
|
||||
var request = new ShaderCompileRequest(plan, resources, layout) { LocalSizeX = 1, ThreadCountX = 1 };
|
||||
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(64);
|
||||
var registers = new uint[256];
|
||||
registers[6] = 64;
|
||||
registers[8] = value;
|
||||
harness.Run(() => runner.Dispatch(registers,
|
||||
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [result] }, 1));
|
||||
Assert.Equal(expected, BinaryPrimitives.ReadUInt32LittleEndian(runner.ReadBack(result, 0, sizeof(uint))));
|
||||
harness.AssertNoValidationMessages();
|
||||
}
|
||||
|
||||
private static Gen5ShaderInstruction DecodeWords(uint word)
|
||||
{
|
||||
var bytes = new byte[2 * sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, word);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(sizeof(uint)), 0xBF810000);
|
||||
var context = new CpuContext(new InstructionMemory(bytes), Generation.Gen5);
|
||||
Assert.True(Gen5ShaderTranslator.TryDecodeProgram(context, 0x1000, out var program, out var decodeError), decodeError);
|
||||
return program.Instructions[0];
|
||||
}
|
||||
|
||||
private sealed class InstructionMemory(byte[] bytes) : ICpuMemory
|
||||
{
|
||||
public bool TryRead(ulong address, Span<byte> destination)
|
||||
{
|
||||
if (address < 0x1000 || address - 0x1000 + (ulong)destination.Length > (ulong)bytes.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes.AsSpan((int)(address - 0x1000), destination.Length).CopyTo(destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong address, ReadOnlySpan<byte> source) => false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
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;
|
||||
|
||||
// The wave-wide maximum UE's motion blur takes of its per-lane sample counts: row_shr 1, 2,
|
||||
// 4 and 8 through DPP, v_permlanex16_b32 across the row pairs, then v_readlane_b32 of lanes
|
||||
// 31 and 63. The result bounds a sample loop, so a wrong maximum can hang the GPU.
|
||||
public sealed class WaveMaxReductionDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture<HeadlessVulkanFixture>
|
||||
{
|
||||
private static readonly uint[] Reduction =
|
||||
[
|
||||
0x280404FA, 0xFF011102, // v_max_u32_dpp v2, v2, v2 row_shr:1
|
||||
0x280404FA, 0xFF011202, // row_shr:2
|
||||
0x280404FA, 0xFF011402, // row_shr:4
|
||||
0x280404FA, 0xFF011802, // row_shr:8
|
||||
0xD7781004, 0x03058302, // v_permlanex16_b32 v4, v2, -1, -1
|
||||
0x28040902, // v_max_u32 v2, v2, v4
|
||||
0xD760000A, 0x00013F02, // v_readlane_b32 s10, v2, 31
|
||||
0xD760000B, 0x00017F02, // v_readlane_b32 s11, v2, 63
|
||||
0x84940B0A, // s_max_u32 s20, s10, s11
|
||||
];
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x7E040300u, 63u)] // v_mov_b32 v2, v0: the maximum is in the last lane
|
||||
[InlineData(0x4C0400BFu, 63u)] // v_sub_nc_u32 v2, 63, v0: the maximum is in lane 0
|
||||
public void ReductionYieldsTheWaveMaximum(uint prepareValue, uint expected)
|
||||
{
|
||||
var vulkan = fixture.Vulkan;
|
||||
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
|
||||
uint[] words =
|
||||
[
|
||||
prepareValue,
|
||||
.. Reduction,
|
||||
0x7E060214, // v_mov_b32 v3, s20
|
||||
0x340A0082, // v_lshlrev_b32 v5, 2, v0
|
||||
0xE0701000, 0x80010305, // buffer_store_dword v3, v5, s[4:7], 0 offen
|
||||
0xBF810000, // s_endpgm
|
||||
];
|
||||
var bytes = new byte[words.Length * sizeof(uint)];
|
||||
for (var index = 0; index < words.Length; index++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)), words[index]);
|
||||
}
|
||||
|
||||
var context = new CpuContext(new InstructionMemory(bytes), Generation.Gen5);
|
||||
Assert.True(Gen5ShaderTranslator.TryDecodeProgram(context, 0x1000, out var program, out var decodeError), decodeError);
|
||||
var (plan, resources, layout) = Prepare(program);
|
||||
var request = new ShaderCompileRequest(plan, resources, layout)
|
||||
{
|
||||
LocalSizeX = 64, ThreadCountX = 64, WaveSize = 64,
|
||||
};
|
||||
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 output = runner.CreateBuffer(256);
|
||||
var registers = new uint[256];
|
||||
registers[6] = 256;
|
||||
harness.Run(() => runner.Dispatch(registers,
|
||||
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [output] }, 1));
|
||||
var result = runner.ReadBack(output, 0, 256);
|
||||
for (var lane = 0; lane < 64; lane++)
|
||||
{
|
||||
Assert.Equal(expected, BinaryPrimitives.ReadUInt32LittleEndian(result.AsSpan(lane * sizeof(uint))));
|
||||
}
|
||||
|
||||
harness.AssertNoValidationMessages();
|
||||
}
|
||||
|
||||
private sealed class InstructionMemory(byte[] bytes) : ICpuMemory
|
||||
{
|
||||
public bool TryRead(ulong address, Span<byte> destination)
|
||||
{
|
||||
if (address < 0x1000 || address - 0x1000 + (ulong)destination.Length > (ulong)bytes.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes.AsSpan((int)(address - 0x1000), destination.Length).CopyTo(destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong address, ReadOnlySpan<byte> source) => false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Resources;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Metal.Tests;
|
||||
|
||||
// RDNA2 ISA: S_ADDK_I32 writes SCC = signed overflow, the same rule as
|
||||
// S_ADD_I32. S_MOVK_I32 and S_MULK_I32 leave SCC untouched.
|
||||
public sealed class Gen5ScalarAddImmediateMslTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x2000;
|
||||
private const uint SEndpgm = 0xBF810000;
|
||||
|
||||
// SOPK: bits 31..30 = 0b10, opcode field (bits 29..23) 0x60 + op,
|
||||
// sdst at bits 22..16, SIMM16 in the low half.
|
||||
private const uint SAddkI32S4Max = 0xB7847FFF; // s_addk_i32 s4, 0x7FFF
|
||||
private const uint SMovkI32S4Max = 0xB0047FFF; // s_movk_i32 s4, 0x7FFF
|
||||
private const uint SMulkI32S4Max = 0xB8047FFF; // s_mulk_i32 s4, 0x7FFF
|
||||
|
||||
private static string Compile(uint word)
|
||||
{
|
||||
var request = Gen5ComputeFixtures.RequestOrThrow(
|
||||
DecodeProgram(word, SEndpgm),
|
||||
ShaderStage.Compute,
|
||||
localSizeX: 1);
|
||||
Assert.True(Gen5MslTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
return shader.Source;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddkEmitsTheSignedOverflowSccUpdate()
|
||||
{
|
||||
var source = Compile(SAddkI32S4Max);
|
||||
Assert.Contains("scc = ((~(", source, StringComparison.Ordinal);
|
||||
Assert.Contains(">> 31) != 0u;", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(SMovkI32S4Max)]
|
||||
[InlineData(SMulkI32S4Max)]
|
||||
public void MovkAndMulkLeaveSccAlone(uint word)
|
||||
{
|
||||
Assert.DoesNotContain("scc = ((~(", Compile(word), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static Gen5ShaderProgram DecodeProgram(params uint[] words)
|
||||
{
|
||||
var memory = new AddImmediateTestMemory(ShaderAddress, 0x1000);
|
||||
var shader = new byte[words.Length * sizeof(uint)];
|
||||
for (var index = 0; index < words.Length; index++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(shader.AsSpan(index * sizeof(uint)), words[index]);
|
||||
}
|
||||
|
||||
Assert.True(memory.TryWrite(ShaderAddress, shader));
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
Assert.True(
|
||||
Gen5ShaderTranslator.TryDecodeProgram(ctx, ShaderAddress, out var program, out var error),
|
||||
error);
|
||||
return program;
|
||||
}
|
||||
|
||||
private sealed class AddImmediateTestMemory(ulong baseAddress, int size) : ICpuMemory
|
||||
{
|
||||
private readonly byte[] _storage = new byte[size];
|
||||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, destination.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, source.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source.CopyTo(_storage.AsSpan(offset, source.Length));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryResolve(ulong virtualAddress, int length, out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
if (virtualAddress < baseAddress)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var relative = virtualAddress - baseAddress;
|
||||
if (relative > (ulong)(size - length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = (int)relative;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Tests;
|
||||
|
||||
// S_CODE_END pads the end of a shader. A block placed after the final S_ENDPGM that loops back
|
||||
// with S_BRANCH leads the linear decoder into that padding.
|
||||
public sealed class Gen5CodeEndTests
|
||||
{
|
||||
private const uint BranchScc0Over = 0xBF840002; // s_cbranch_scc0 to 0x0C
|
||||
private const uint MoveScalar = 0xBE800381; // s_mov_b32 s0, 1
|
||||
private const uint EndProgram = 0xBF810000; // s_endpgm
|
||||
private const uint BranchBack = 0xBF82FFFC; // s_branch to 0x04
|
||||
private const uint CodeEnd = 0xBF9F0000; // s_code_end
|
||||
|
||||
[Fact]
|
||||
public void DecodeEndsAtCodeEndAfterTheLastReachableBlock()
|
||||
{
|
||||
var context = Context(BranchScc0Over, MoveScalar, EndProgram, MoveScalar, BranchBack, CodeEnd, CodeEnd);
|
||||
|
||||
Assert.True(Gen5ShaderTranslator.TryDecodeProgram(context, 0x1000, out var program, out var error), error);
|
||||
|
||||
Assert.Equal(
|
||||
["SCbranchScc0", "SMovB32", "SEndpgm", "SMovB32", "SBranch"],
|
||||
program.Instructions.Select(static instruction => instruction.Opcode));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CodeEndBeforeAForwardBranchTargetFails()
|
||||
{
|
||||
var context = Context(BranchScc0Over, CodeEnd, EndProgram, MoveScalar, EndProgram);
|
||||
|
||||
Assert.False(Gen5ShaderTranslator.TryDecodeProgram(context, 0x1000, out _, out var error));
|
||||
Assert.Contains("code-end-inside-program", error);
|
||||
}
|
||||
|
||||
private static CpuContext Context(params uint[] words) => new(new InstructionMemory(words), Generation.Gen5);
|
||||
|
||||
private sealed class InstructionMemory(uint[] words) : ICpuMemory
|
||||
{
|
||||
public bool TryRead(ulong address, Span<byte> destination)
|
||||
{
|
||||
if (address < 0x1000 || (address - 0x1000) % sizeof(uint) != 0 || destination.Length != sizeof(uint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var index = (address - 0x1000) / sizeof(uint);
|
||||
if (index >= (ulong)words.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(destination, words[(int)index]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong address, ReadOnlySpan<byte> source) => false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Tests;
|
||||
|
||||
// RDNA2 ISA: S_ADDK_I32 is "D.i = D.i + signext(SIMM16); SCC = signed overflow",
|
||||
// the same SCC rule as S_ADD_I32. S_MOVK_I32 and S_MULK_I32 leave SCC untouched.
|
||||
public sealed class Gen5ScalarAddImmediateTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x2000;
|
||||
private const uint SEndpgm = 0xBF810000;
|
||||
|
||||
// SOPK: bits 31..30 = 0b10, opcode field (bits 29..23) 0x60 + op,
|
||||
// sdst at bits 22..16, SIMM16 in the low half.
|
||||
private const uint SAddkI32S4Max = 0xB7847FFF; // s_addk_i32 s4, 0x7FFF
|
||||
private const uint SMovkI32S4Max = 0xB0047FFF; // s_movk_i32 s4, 0x7FFF
|
||||
private const uint SMulkI32S4Max = 0xB8047FFF; // s_mulk_i32 s4, 0x7FFF
|
||||
|
||||
[Fact]
|
||||
public void DecodesTheScalarImmediateOpcodes()
|
||||
{
|
||||
var program = DecodeProgram(SAddkI32S4Max, SMovkI32S4Max, SMulkI32S4Max, SEndpgm);
|
||||
Assert.Equal("SAddkI32", program.Instructions[0].Opcode);
|
||||
Assert.Equal("SMovkI32", program.Instructions[1].Opcode);
|
||||
Assert.Equal("SMulkI32", program.Instructions[2].Opcode);
|
||||
Assert.Equal(4u, program.Instructions[0].Destinations[0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddkWritesSccWhileMovkAndMulkDoNot()
|
||||
{
|
||||
// The SCC write is SignedAddOverflow: one extra IEqual/INotEqual/LogicalAnd
|
||||
// sign-comparison triple over the same program without it. The shader
|
||||
// prologue already uses these opcodes, so compare counts rather than
|
||||
// presence.
|
||||
var withAddk = CompiledOpcodes(SAddkI32S4Max);
|
||||
var withMovk = CompiledOpcodes(SMovkI32S4Max);
|
||||
var withMulk = CompiledOpcodes(SMulkI32S4Max);
|
||||
foreach (var opcode in new[] { SpirvOp.IEqual, SpirvOp.INotEqual, SpirvOp.LogicalAnd })
|
||||
{
|
||||
Assert.Equal(Count(withMovk, opcode) + 1, Count(withAddk, opcode));
|
||||
// S_MOVK_I32 and S_MULK_I32 leave SCC alone.
|
||||
Assert.Equal(Count(withMovk, opcode), Count(withMulk, opcode));
|
||||
}
|
||||
}
|
||||
|
||||
private static int Count(IReadOnlyList<ushort> opcodes, SpirvOp opcode)
|
||||
{
|
||||
var total = 0;
|
||||
foreach (var value in opcodes)
|
||||
{
|
||||
if (value == (ushort)opcode)
|
||||
{
|
||||
total++;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ushort> CompiledOpcodes(uint word)
|
||||
{
|
||||
var request = Request(DecodeProgram(word, SEndpgm), userDataCount: 0);
|
||||
Assert.True(
|
||||
Gen5SpirvTranslator.TryCompileProgram(request, out var compiled, out var error),
|
||||
error);
|
||||
return ReadSpirvOpcodes(compiled.Spirv);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ushort> ReadSpirvOpcodes(byte[] spirv)
|
||||
{
|
||||
Assert.Equal(0, spirv.Length % sizeof(uint));
|
||||
Assert.True(spirv.Length >= 5 * sizeof(uint));
|
||||
Assert.Equal(0x07230203u, BinaryPrimitives.ReadUInt32LittleEndian(spirv));
|
||||
|
||||
var opcodes = new List<ushort>();
|
||||
for (var offset = 5 * sizeof(uint); offset < spirv.Length;)
|
||||
{
|
||||
var instruction = BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset));
|
||||
var wordCount = checked((int)(instruction >> 16));
|
||||
Assert.InRange(wordCount, 1, (spirv.Length - offset) / sizeof(uint));
|
||||
opcodes.Add((ushort)instruction);
|
||||
offset += wordCount * sizeof(uint);
|
||||
}
|
||||
|
||||
return opcodes;
|
||||
}
|
||||
|
||||
private static Gen5ShaderProgram DecodeProgram(params uint[] words)
|
||||
{
|
||||
var memory = new AddImmediateTestMemory(ShaderAddress, 0x1000);
|
||||
var shader = new byte[words.Length * sizeof(uint)];
|
||||
for (var index = 0; index < words.Length; index++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(shader.AsSpan(index * sizeof(uint)), words[index]);
|
||||
}
|
||||
|
||||
Assert.True(memory.TryWrite(ShaderAddress, shader));
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
Assert.True(
|
||||
Gen5ShaderTranslator.TryDecodeProgram(ctx, ShaderAddress, out var program, out var error),
|
||||
error);
|
||||
return program;
|
||||
}
|
||||
|
||||
private sealed class AddImmediateTestMemory(ulong baseAddress, int size) : ICpuMemory
|
||||
{
|
||||
private readonly byte[] _storage = new byte[size];
|
||||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, destination.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, source.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source.CopyTo(_storage.AsSpan(offset, source.Length));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryResolve(ulong virtualAddress, int length, out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
if (virtualAddress < baseAddress)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var relative = virtualAddress - baseAddress;
|
||||
if (relative > (ulong)(size - length))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = (int)relative;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler.Resources;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Tests;
|
||||
|
||||
public sealed class RuntimeValueValidatorTests
|
||||
{
|
||||
// The host evaluates runtime descriptor values with ScalarOperationSemantics, so the validator must
|
||||
// not reject an operation the evaluator can compute (Silent Hill's compute plans failed that way).
|
||||
[Fact]
|
||||
public void EveryEvaluableOperationIsAcceptedAsUniform()
|
||||
{
|
||||
ulong[] operands = [1, 2, 3, 4];
|
||||
foreach (var operation in Enum.GetValues<ScalarOperation>())
|
||||
{
|
||||
bool evaluable;
|
||||
try
|
||||
{
|
||||
evaluable = ScalarOperationSemantics.TryEvaluate(operation, operands, out _);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
evaluable = false;
|
||||
}
|
||||
|
||||
if (evaluable)
|
||||
{
|
||||
Assert.True(RuntimeValueValidator.IsUniformOperation(operation), $"{operation} is evaluable but not uniform.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue