sharpemu/tests/SharpEmu.ShaderCompiler.Tests/Gen5StructuredControlFlowTe...

351 lines
14 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.ShaderCompiler.Vulkan;
using System.Runtime.InteropServices;
using System.Text;
using Xunit;
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
namespace SharpEmu.ShaderCompiler.Tests;
// Programs with scalar control flow, shared by the translation tests here and the device tests that
// run them through both control-flow forms. Inputs are s8 and s9; results are stored from v4/v5
// through the buffer V# in s4..s7. Every instruction takes one 4-byte slot.
public sealed class Gen5StructuredControlFlowTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public void RegisterFiles_BelongToTheFunctionExecutingTheProgram(bool forceDispatcher)
{
var code = Compile(NestedIfSharedMerge(), forceDispatcher);
var words = MemoryMarshal.Cast<byte, uint>(code.AsSpan()).ToArray();
var names = new Dictionary<uint, string>();
var variables = new Dictionary<uint, uint>();
for (var index = 5; index < words.Length; index += (int)(words[index] >> 16))
{
var length = (int)(words[index] >> 16);
var opcode = (SpirvOp)(words[index] & 0xFFFF);
if (opcode == SpirvOp.Name)
names[words[index + 1]] = Encoding.UTF8.GetString(code.AsSpan((index + 2) * 4, (length - 2) * 4)).TrimEnd('\0');
if (opcode == SpirvOp.Variable) variables[words[index + 2]] = words[index + 3];
}
foreach (var prefix in new[] { "s", "v" })
{
// The compiler now keeps a variable per touched register rather than one array.
var registers = names.Where(entry => entry.Value.StartsWith(prefix, StringComparison.Ordinal) &&
uint.TryParse(entry.Value.AsSpan(1), out _)).ToArray();
Assert.NotEmpty(registers);
foreach (var register in registers)
Assert.Equal((uint)SpirvStorageClass.Function, variables[register.Key]);
}
Gen5LargeDispatcherValidationTests.ValidateWithSpirvToolsWhenAvailable(code);
}
private static Gen5Operand S(uint register) => Gen5Operand.Scalar(register);
private static Gen5ShaderInstruction Store(uint pc, uint vector, int offset) =>
BufferAccess(pc, "BufferStoreDword", 4, offset: offset, vectorData: vector);
// for (i = 0; i < s8; i++) acc += (i & 1) != 0 ? i : 100; stores acc, i.
public static Gen5ShaderProgram LoopWithIfElse() => Program(
MoveScalar(0, 10, 0),
MoveScalar(4, 11, 0),
Sopc(8, "SCmpLtU32", S(11), S(8)),
Branch(12, "SCbranchScc0", 7),
Sop2(16, "SAndB32", 12, S(11), Operand(1)),
Branch(20, "SCbranchScc0", 2),
Sop2(24, "SAddU32", 10, S(10), S(11)),
Branch(28, "SBranch", 1),
Sop2(32, "SAddU32", 10, S(10), Operand(100)),
Sop2(36, "SAddU32", 11, S(11), Operand(1)),
Branch(40, "SBranch", -9),
MoveVectorFromScalar(44, 4, 10),
MoveVectorFromScalar(48, 5, 11),
Store(52, 4, 0),
Store(56, 5, 4),
EndProgram(60));
public static (uint Acc, uint Second) LoopWithIfElseExpected(uint n, uint unused)
{
uint acc = 0;
uint i = 0;
for (; i < n; i++)
{
acc += (i & 1) != 0 ? i : 100u;
}
return (acc, i);
}
// Nested loops that leave the program early:
// for (i < s8) for (j < i) { acc += j; if (acc == s9) { store acc, 0xDEAD; end } } stores acc, i.
public static Gen5ShaderProgram NestedLoopsWithEarlyExit() => Program(
MoveScalar(0, 10, 0),
MoveScalar(4, 11, 0),
Sopc(8, "SCmpLtU32", S(11), S(8)),
Branch(12, "SCbranchScc0", 10),
MoveScalar(16, 12, 0),
Sopc(20, "SCmpLtU32", S(12), S(11)),
Branch(24, "SCbranchScc0", 5),
Sop2(28, "SAddU32", 10, S(10), S(12)),
Sopc(32, "SCmpEqU32", S(10), S(9)),
Branch(36, "SCbranchScc1", 9),
Sop2(40, "SAddU32", 12, S(12), Operand(1)),
Branch(44, "SBranch", -7),
Sop2(48, "SAddU32", 11, S(11), Operand(1)),
Branch(52, "SBranch", -12),
MoveVectorFromScalar(56, 4, 10),
MoveVectorFromScalar(60, 5, 11),
Store(64, 4, 0),
Store(68, 5, 4),
EndProgram(72),
MoveVectorFromScalar(76, 4, 10),
MoveVector(80, 5, 0xDEAD),
Store(84, 4, 0),
Store(88, 5, 4),
EndProgram(92));
public static (uint Acc, uint Second) NestedLoopsWithEarlyExitExpected(uint n, uint target)
{
uint acc = 0;
for (uint i = 0; i < n; i++)
{
for (uint j = 0; j < i; j++)
{
acc += j;
if (acc == target)
{
return (acc, 0xDEAD);
}
}
}
return (acc, n);
}
// if (s8 & 1) { acc += 1; if (s8 & 2) acc += 10; } acc += 100; the inner selection merges where
// the outer one does.
public static Gen5ShaderProgram NestedIfSharedMerge() => Program(
MoveScalar(0, 10, 0),
Sop2(4, "SAndB32", 12, S(8), Operand(1)),
Branch(8, "SCbranchScc0", 4),
Sop2(12, "SAddU32", 10, S(10), Operand(1)),
Sop2(16, "SAndB32", 12, S(8), Operand(2)),
Branch(20, "SCbranchScc0", 1),
Sop2(24, "SAddU32", 10, S(10), Operand(10)),
Sop2(28, "SAddU32", 10, S(10), Operand(100)),
MoveVectorFromScalar(32, 4, 10),
MoveVector(36, 5, 0),
Store(40, 4, 0),
Store(44, 5, 4),
EndProgram(48));
public static (uint Acc, uint Second) NestedIfSharedMergeExpected(uint n, uint unused)
{
uint acc = 0;
if ((n & 1) != 0)
{
acc += 1;
if ((n & 2) != 0)
{
acc += 10;
}
}
return (acc + 100, 0);
}
// Two entries into one cycle (A <-> B), so the graph is irreducible and keeps the dispatcher.
public static Gen5ShaderProgram IrreducibleCycle() => Program(
MoveScalar(0, 10, 0),
Sopc(4, "SCmpEqU32", S(8), Operand(0)),
Branch(8, "SCbranchScc1", 3),
Sop2(12, "SAddU32", 10, S(10), Operand(1)),
Sopc(16, "SCmpLtU32", S(10), S(9)),
Branch(20, "SCbranchScc0", 3),
Sop2(24, "SAddU32", 10, S(10), Operand(2)),
Sopc(28, "SCmpLtU32", S(10), S(9)),
Branch(32, "SCbranchScc1", -6),
MoveVectorFromScalar(36, 4, 10),
MoveVector(40, 5, 0),
Store(44, 4, 0),
Store(48, 5, 4),
EndProgram(52));
public static (uint Acc, uint Second) IrreducibleCycleExpected(uint n, uint limit)
{
uint acc = 0;
var inB = n == 0;
while (true)
{
if (!inB)
{
acc += 1;
if (!(acc < limit))
{
break;
}
}
inB = false;
acc += 2;
if (!(acc < limit))
{
break;
}
}
return (acc, 0);
}
public static IEnumerable<object[]> StructuredPrograms()
{
yield return [nameof(LoopWithIfElse)];
yield return [nameof(NestedLoopsWithEarlyExit)];
yield return [nameof(NestedIfSharedMerge)];
}
public static Gen5ShaderProgram Create(string name) => name switch
{
nameof(LoopWithIfElse) => LoopWithIfElse(),
nameof(NestedLoopsWithEarlyExit) => NestedLoopsWithEarlyExit(),
nameof(NestedIfSharedMerge) => NestedIfSharedMerge(),
nameof(IrreducibleCycle) => IrreducibleCycle(),
_ => throw new ArgumentOutOfRangeException(nameof(name)),
};
public static (uint Acc, uint Second) Expected(string name, uint input0, uint input1) => name switch
{
nameof(LoopWithIfElse) => LoopWithIfElseExpected(input0, input1),
nameof(NestedLoopsWithEarlyExit) => NestedLoopsWithEarlyExitExpected(input0, input1),
nameof(NestedIfSharedMerge) => NestedIfSharedMergeExpected(input0, input1),
nameof(IrreducibleCycle) => IrreducibleCycleExpected(input0, input1),
_ => throw new ArgumentOutOfRangeException(nameof(name)),
};
[Theory]
[MemberData(nameof(StructuredPrograms))]
public void StructurableProgramIsEmittedWithoutTheDispatcher(string name)
{
var structured = Compile(Create(name), forceDispatcher: false);
var dispatcher = Compile(Create(name), forceDispatcher: true);
// The structured body is its own function, called once from the entry point.
Assert.Contains(SpirvOp.FunctionCall, Gen5LargeDispatcherValidationTests.ReadSpirvOpcodes(structured));
Assert.DoesNotContain(SpirvOp.FunctionCall, Gen5LargeDispatcherValidationTests.ReadSpirvOpcodes(dispatcher));
foreach (var code in new[] { structured, dispatcher })
{
Gen5LargeDispatcherValidationTests.ValidateStructuredControlFlow(code);
Gen5LargeDispatcherValidationTests.ValidateWithSpirvToolsWhenAvailable(code);
}
}
[Fact]
public void IrreducibleProgramKeepsTheDispatcher()
{
var code = Compile(IrreducibleCycle(), forceDispatcher: false);
Assert.DoesNotContain(SpirvOp.FunctionCall, Gen5LargeDispatcherValidationTests.ReadSpirvOpcodes(code));
Gen5LargeDispatcherValidationTests.ValidateStructuredControlFlow(code);
Gen5LargeDispatcherValidationTests.ValidateWithSpirvToolsWhenAvailable(code);
}
[Fact]
public void LoopsAreDeclaredWithLoopMerges()
{
var opcodes = Gen5LargeDispatcherValidationTests.ReadSpirvOpcodes(Compile(NestedLoopsWithEarlyExit(), forceDispatcher: false));
Assert.Equal(2, opcodes.Count(static opcode => opcode == SpirvOp.LoopMerge));
}
// A loop that can leave through a chain of diamonds, each of whose sides flows into the next.
// Copying that exit tail at the loop exit doubles at every diamond.
public static Gen5ShaderProgram LoopExitThroughDiamondChain(int diamonds)
{
var instructions = new List<Gen5ShaderInstruction>
{
MoveScalar(0, 10, 0),
MoveScalar(4, 11, 0),
Sop2(8, "SAddU32", 11, S(11), Operand(1)),
Sopc(12, "SCmpEqU32", S(11), S(9)),
Branch(16, "SCbranchScc1", 3), // -> 32, the diamond chain
Sopc(20, "SCmpLtU32", S(11), S(8)),
Branch(24, "SCbranchScc1", -5), // -> 8
EndProgram(28),
};
uint pc = 32;
for (var diamond = 0; diamond < diamonds; diamond++)
{
instructions.Add(Sopc(pc, "SCmpEqU32", S(11), Operand((uint)diamond)));
instructions.Add(Branch(pc + 4, "SCbranchScc1", 2));
instructions.Add(Sop2(pc + 8, "SAddU32", 10, S(10), Operand(1)));
instructions.Add(Branch(pc + 12, "SBranch", 1));
instructions.Add(Sop2(pc + 16, "SAddU32", 10, S(10), Operand(2)));
pc += 20;
}
instructions.Add(MoveVectorFromScalar(pc, 4, 10));
instructions.Add(Store(pc + 4, 4, 0));
instructions.Add(EndProgram(pc + 8));
return Program([.. instructions]);
}
[Fact]
public void ExitTailCopiesStayBounded()
{
var structured = Compile(LoopExitThroughDiamondChain(14), forceDispatcher: false);
var dispatcher = Compile(LoopExitThroughDiamondChain(14), forceDispatcher: true);
Assert.True(
structured.Length <= dispatcher.Length * 4,
$"structured={structured.Length} bytes, dispatcher={dispatcher.Length} bytes");
Gen5LargeDispatcherValidationTests.ValidateStructuredControlFlow(structured);
Gen5LargeDispatcherValidationTests.ValidateWithSpirvToolsWhenAvailable(structured);
}
// The control-flow skeleton of a Yotei compute shader: early execz exits to the final
// S_ENDPGM before and after a loop, and vccnz skips over the loop.
public static Gen5ShaderProgram EarlyExitsAroundLoop() => Program(
Nop(0), Branch(4, "SCbranchExecz", 22), Nop(8), Branch(12, "SCbranchExecz", 20), Nop(16),
Branch(20, "SCbranchVccnz", 10), Nop(24), Branch(28, "SCbranchVccnz", 8), Nop(32), Nop(36),
Branch(40, "SCbranchVccz", 3), Nop(44), Branch(48, "SBranch", -4), Nop(52), Nop(56), Nop(60),
Branch(64, "SCbranchExecz", 7), Nop(68), Branch(72, "SCbranchExecz", 3), Nop(76), Nop(80),
Nop(84), Nop(88), Nop(92), EndProgram(96));
[Fact]
public void EarlyExitsAroundLoopAreStructured()
{
var code = Compile(EarlyExitsAroundLoop(), forceDispatcher: false);
Assert.Contains(SpirvOp.FunctionCall, Gen5LargeDispatcherValidationTests.ReadSpirvOpcodes(code));
Gen5LargeDispatcherValidationTests.ValidateStructuredControlFlow(code);
Gen5LargeDispatcherValidationTests.ValidateWithSpirvToolsWhenAvailable(code);
}
// One arm reaches the shared block directly; the other either reaches it or ends the program
// on its own, so the shared block post-dominates nothing.
public static Gen5ShaderProgram EarlyReturnBesideSharedBlock() => Program(
Nop(0), Branch(4, "SCbranchScc1", 5), Nop(8), Branch(12, "SCbranchScc1", 1),
Branch(16, "SBranch", 2), Nop(20), EndProgram(24), Nop(28), EndProgram(32));
[Fact]
public void EarlyReturnBesideSharedBlockIsStructured()
{
var code = Compile(EarlyReturnBesideSharedBlock(), forceDispatcher: false);
Assert.Contains(SpirvOp.FunctionCall, Gen5LargeDispatcherValidationTests.ReadSpirvOpcodes(code));
Gen5LargeDispatcherValidationTests.ValidateStructuredControlFlow(code);
Gen5LargeDispatcherValidationTests.ValidateWithSpirvToolsWhenAvailable(code);
}
internal static byte[] Compile(Gen5ShaderProgram program, bool forceDispatcher)
{
var (plan, resources, layout) = Prepare(program);
var request = new ShaderCompileRequest(plan, resources, layout)
{
LocalSizeX = 1,
ThreadCountX = 1,
ForceDispatcher = forceDispatcher,
};
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
return shader.Spirv;
}
}