sharpemu/tests/SharpEmu.ShaderCompiler.Tests/Resources/ScalarValueGraphTests.cs

480 lines
19 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using Xunit;
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
namespace SharpEmu.ShaderCompiler.Tests.Resources;
public sealed class ScalarValueGraphTests
{
private static Gen5ShaderInstruction[] Descriptor(uint pc, uint register, uint dword1, uint dword2, uint dword3) =>
[
MoveScalar(pc, register + 1, dword1),
MoveScalar(pc + 4, register + 2, dword2),
MoveScalar(pc + 8, register + 3, dword3),
];
[Fact]
public void ImmediateFlatteningAndValueNumbering()
{
var program = Program(
MoveScalar(0, 4, 0x1000),
MoveScalar(4, 5, 0),
ScalarLoad(8, 4, destination: 8, immediateOffset: 0x20),
ScalarLoad(12, 4, destination: 9, immediateOffset: 0x20),
MoveScalar(16, 10, 16),
MoveScalar(20, 11, 0),
BufferLoad(24, 8),
EndProgram(28));
var plan = Extract(program);
Assert.Single(plan.TableReads);
Assert.Empty(plan.DynamicReads);
Assert.True(plan.Memory.Find(8)!.PlanningOnly);
var memory = new TestWordMemory { Words = new uint[16] };
memory.At(0x1020) = 0xFEEDBEEF;
Assert.True(RuntimeValueEvaluator.FlattenResourceTable(plan, Inputs([], memory.Read), out var table));
Assert.Equal([0xFEEDBEEFu], table);
Assert.Equal(1u, memory.Reads);
}
[Fact]
public void RawScalarComponentAlignment()
{
var program = Program(
MoveScalar(0, 4, 0x1003),
MoveScalar(4, 5, 0),
MoveScalar(8, 6, 2),
ScalarLoad(12, 4, destination: 8, immediateOffset: 2, dynamicOffsetRegister: 6),
MoveScalar(16, 9, 0),
MoveScalar(20, 10, 16),
MoveScalar(24, 11, 0),
BufferLoad(28, 8),
EndProgram(32));
var plan = Extract(program);
var memory = new TestWordMemory();
memory.At(0x1000) = 0x12345678;
Assert.True(RuntimeValueEvaluator.FlattenResourceTable(plan, Inputs([], memory.Read), out var table));
Assert.Equal([0x12345678u], table);
Assert.Equal(1u, memory.Reads);
}
[Fact]
public void DynamicReadRemainsExplicit()
{
var program = Program(
MoveScalar(0, 4, 0x1000),
MoveScalar(4, 5, 0),
ScalarLoad(8, 4, destination: 8, dynamicOffsetRegister: 2),
MoveScalar(12, 9, 0),
MoveScalar(16, 10, 16),
MoveScalar(20, 11, 0),
BufferLoad(24, 8),
EndProgram(28));
var plan = Extract(program);
Assert.Empty(plan.TableReads);
var read = Assert.Single(plan.DynamicReads);
Assert.Equal(ScalarValueKind.ScalarAddressWord, read.Kind);
}
[Fact]
public void NestedResourceTableWalk()
{
var program = Program(
MoveScalar(0, 4, 0x1000),
MoveScalar(4, 5, 0),
ScalarLoad(8, 4, destination: 6),
MoveScalar(12, 7, 0),
ScalarLoad(16, 6, destination: 8),
MoveScalar(20, 9, 0),
MoveScalar(24, 10, 16),
MoveScalar(28, 11, 0),
BufferLoad(32, 8),
EndProgram(36));
var plan = Extract(program);
var memory = new TestWordMemory { Base = 0x1000, Words = new uint[0x1100 / 4] };
memory.At(0x1000) = 0x2000;
memory.At(0x2000) = 0xABCDEF01;
Assert.True(RuntimeValueEvaluator.FlattenResourceTable(plan, Inputs([], memory.Read), out var table));
Assert.Equal([0x2000u, 0xABCDEF01u], table);
}
[Fact]
public void ShaderBaseAndUserData()
{
var program = Program(
new Gen5ShaderInstruction(0, Gen5ShaderEncoding.Sop1, "SGetpcB64", [0u], [], [Gen5Operand.Scalar(4)], null),
Sop2(4, "SAddU32", 6, Gen5Operand.Scalar(2), Operand(4)),
MoveScalar(8, 7, 0),
BufferLoad(12, 4),
EndProgram(20));
var plan = Extract(program, userDataCount: 3);
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Buffers[0].Source, Inputs([0, 0, 0x20], shaderBase: 0x12345678ABCDEF00), out var result));
Assert.Equal(0xABCDEF04u, result.Dwords[0]);
Assert.Equal(0x12345678u, result.Dwords[1]);
Assert.Equal(0x24u, result.Dwords[2]);
}
[Fact]
public void CarryAndBitFields()
{
var program = Program(
Sop2(0, "SAddU32", 4, Operand(0xFFFFFFFF), Operand(2)),
Sop2(4, "SCselectB32", 5, Operand(1), Operand(0)),
Sop2(8, "SBfeI32", 6, Operand(0xF0), Operand(4 | (4 << 16))),
Sop2(12, "SBfmB32", 7, Operand(8), Operand(4)),
BufferLoad(16, 4),
EndProgram(24));
var plan = Extract(program);
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Buffers[0].Source, Inputs([]), out var result));
Assert.Equal([1u, 1u, 0xFFFFFFFFu, 0xFF0u], result.Dwords);
}
private static Gen5ShaderProgram PhiProgram(uint first, uint second) => Program(
Branch(0, "SCbranchScc0", 2),
MoveScalar(4, 8, first),
Branch(8, "SBranch", 1),
MoveScalar(12, 8, second),
MoveScalar(16, 9, 0),
MoveScalar(20, 10, 16),
MoveScalar(24, 11, 0),
BufferLoad(28, 8),
EndProgram(32));
[Fact]
public void InvariantAndDivergentPhi()
{
var invariant = Extract(PhiProgram(7, 7));
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(invariant, invariant.Info.Buffers[0].Source, Inputs([]), out var result));
Assert.Equal(7u, result.Dwords[0]);
var error = Assert.Throws<ResourcePlanException>(() => Extract(PhiProgram(7, 9)));
Assert.Contains("not a valid runtime value", error.Message);
}
[Fact]
public void ControlDependentStandaloneLoadStaysExplicit()
{
var program = Program(
Branch(0, "SCbranchScc0", 2),
MoveScalar(4, 4, 0x1000),
Branch(8, "SBranch", 1),
MoveScalar(12, 4, 0x2000),
MoveScalar(16, 5, 0),
ScalarLoad(20, 4, destination: 8),
EndProgram(28));
var plan = Extract(program);
Assert.Empty(plan.TableReads);
Assert.False(plan.Memory.Find(20)!.PlanningOnly);
Assert.True(plan.Info.UsesDeviceAddresses);
}
[Fact]
public void Runtime64BitDescriptorOperations()
{
var program = Program(
Sop2(0, "SLshlB64", 4, Operand(0x1234), Operand(32)),
MoveScalar(4, 6, 0),
MoveScalar(8, 7, 0xFFFF),
Sop2(12, "SAndB64", 4, Gen5Operand.Scalar(4), Gen5Operand.Scalar(6)),
Sop2(16, "SAddU32", 4, Gen5Operand.Scalar(4), Operand(0xABCD)),
Sop2(20, "SAddcU32", 5, Gen5Operand.Scalar(5), Operand(1)),
MoveScalar(24, 6, 16),
MoveScalar(28, 7, 0),
BufferLoad(32, 4),
EndProgram(40));
var plan = Extract(program);
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Buffers[0].Source, Inputs([]), out var result));
Assert.Equal(0xABCDu, result.Dwords[0]);
Assert.Equal(0x1235u, result.Dwords[1]);
}
[Fact]
public void UniformFirstLaneSamplerLod()
{
var program = Program(
Vop1(0, "VCvtF32U32", 1, Gen5Operand.Scalar(8)),
Vop2(4, "VMulF32", 1, Operand(0x43800000), Gen5Operand.Vector(1)),
Vop1(12, "VCvtU32F32", 1, Gen5Operand.Vector(1)),
Vop2(16, "VMinU32", 1, Operand(0xFFF), Gen5Operand.Vector(1)),
Vop2(24, "VAndB32", 1, Operand(0xFFF), Gen5Operand.Vector(1)),
Vop3(32, "VLshlOrU32", 1, Gen5Operand.Vector(1), Operand(12), Gen5Operand.Vector(1)),
ReadFirstLane(40, 12, 1),
MoveScalar(44, 13, 0),
MoveScalar(48, 14, 16),
MoveScalar(52, 15, 0),
BufferLoad(56, 12),
EndProgram(64));
var plan = Extract(program);
var userData = new uint[9];
userData[8] = 3;
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Buffers[0].Source, Inputs(userData), out var result));
Assert.Equal(0x00300300u, result.Dwords[0]);
userData[8] = 20;
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Buffers[0].Source, Inputs(userData), out result));
Assert.Equal(0x00FFFFFFu, result.Dwords[0]);
}
[Fact]
public void DivergentFirstLaneValue_IsRejected()
{
var program = Program(
ReadFirstLane(0, 12, 0),
MoveScalar(4, 13, 0),
MoveScalar(8, 14, 16),
MoveScalar(12, 15, 0),
BufferLoad(16, 12),
EndProgram(24));
var error = Assert.Throws<ResourcePlanException>(() => Extract(program));
Assert.Contains("not a valid runtime value", error.Message);
Assert.Contains("pc=0x00000010", error.Message);
}
[Fact]
public void DifferentGpuDependentFirstLaneReads_KeepSeparateIdentities()
{
var program = Program(
ReadFirstLane(0, 12, 0),
BufferLoad(4, 12),
ReadFirstLane(12, 12, 1),
BufferLoad(16, 12),
EndProgram(24));
var graph = ScalarValueGraph.Build(program, 0, 16);
var firstSelector = graph.Accesses[0]!.Handle!.Operands[0];
var secondSelector = graph.Accesses[1]!.Handle!.Operands[0];
Assert.Equal(ScalarValueKind.FirstLane, firstSelector.Kind);
Assert.Equal(ScalarValueKind.FirstLane, secondSelector.Kind);
Assert.True(firstSelector.Operands[0].IsUndefined);
Assert.True(secondSelector.Operands[0].IsUndefined);
Assert.NotSame(firstSelector, secondSelector);
Assert.False(graph.Equivalent(firstSelector, secondSelector));
}
[Fact]
public void ConstantBufferBounds()
{
static Gen5ShaderProgram Read(int immediate) => Program(
MoveScalar(0, 0, 0x3000),
MoveScalar(4, 1, 0),
MoveScalar(8, 2, 16),
MoveScalar(12, 3, 0),
ScalarBufferLoad(16, 0, destination: 8, immediateOffset: immediate),
MoveScalar(24, 9, 0),
MoveScalar(28, 10, 16),
MoveScalar(32, 11, 0),
BufferLoad(36, 8),
EndProgram(44));
var memory = new TestWordMemory { Base = 0x3000 };
memory.At(0x300C) = 0xA5A5A5A5;
var plan = Extract(Read(12), userDataCount: 0);
Assert.True(RuntimeValueEvaluator.FlattenResourceTable(plan, Inputs([], memory.Read), out var table));
Assert.Equal([0xA5A5A5A5u], table);
var overflow = Extract(Read(16), userDataCount: 0);
Assert.False(RuntimeValueEvaluator.FlattenResourceTable(overflow, Inputs([], memory.Read), out _));
}
// A loop whose body runs under a divergent mask still reaches a fixpoint: the
// undefined mask has one identity, so the values built over it stop changing.
[Fact]
public void MaskedLoop_Converges()
{
var program = Program(
MoveScalar(0, 0, 4),
Vop1(4, "VMovB32", 1, Operand(0)),
Vopc(8, "VCmpGtU32", Gen5Operand.Scalar(0), 2),
Sop1(12, "SAndSaveexecB32", 1, Gen5Operand.Scalar(106)),
Vop2(16, "VAddI32", 1, Gen5Operand.Scalar(0), Gen5Operand.Vector(1)),
MoveScalarRegister(20, 126, 1),
Sop2(24, "SSubI32", 0, Gen5Operand.Scalar(0), Operand(1)),
Sopc(28, "SCmpLgU32", Gen5Operand.Scalar(0), Operand(0)),
Branch(32, "SCbranchScc1", -7),
MoveScalar(36, 9, 0),
MoveScalar(40, 10, 64),
MoveScalar(44, 11, 0),
BufferStore(48, 8),
EndProgram(56));
var plan = Extract(program);
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Buffers[0].Source, Inputs([0, 0, 0, 0, 0, 0, 0, 0, 0x1000]), out var result));
Assert.Equal(0x1000u, result.Dwords[0]);
Assert.Equal(64u, result.Dwords[2]);
}
[Fact]
public void UndefinedRuntimeValueFails()
{
var error = Assert.Throws<ResourcePlanException>(() => Extract(Program(BufferLoad(0, 20), EndProgram(8)), userDataCount: 16));
Assert.Contains("not a valid runtime value", error.Message);
}
// ---- Requirement A: lane provenance and uniform vector values ----
[Fact]
public void FixedLaneSaveRestore_PreservesDescriptorProvenance()
{
var program = Program(
WriteLane(0, vectorRegister: 18, scalarRegister: 84, lane: 2),
WriteLane(8, vectorRegister: 18, scalarRegister: 85, lane: 5),
MoveScalar(16, 84, 0xDEADBEEF),
MoveScalar(24, 85, 0xBAD0CAFE),
ReadLane(32, scalarRegister: 84, vectorRegister: 18, lane: 2),
ReadLane(40, scalarRegister: 85, vectorRegister: 18, lane: 5),
ScalarLoad(48, 84, destination: 4),
EndProgram(56));
var plan = Extract(program, userDataBase: 84, userDataCount: 2);
var read = Assert.Single(plan.TableReads);
var handle = read.Value.Operands[0];
Assert.Equal(ScalarValueKind.UserData, handle.Operands[0].Kind);
Assert.Equal(84u, handle.Operands[0].UserDataRegister);
Assert.Equal(85u, handle.Operands[1].UserDataRegister);
var expectedAddress = 0x0000_0004_413B_A5B0ul;
var reader = (ulong address, out uint word) =>
{
word = 0x12345678;
return address == expectedAddress;
};
Assert.True(RuntimeValueEvaluator.FlattenResourceTable(plan, Inputs([0x413BA5B0, 4], new GuestWordReader(reader)), out var table));
Assert.Equal([0x12345678u], table);
}
[Fact]
public void FullVectorWrite_InvalidatesSavedLanes()
{
var program = Program(
WriteLane(0, vectorRegister: 18, scalarRegister: 84, lane: 2),
Vop1(8, "VMovB32", 18, Gen5Operand.Scalar(0)),
MoveScalar(12, 84, 0xDEADBEEF),
ReadLane(16, scalarRegister: 84, vectorRegister: 18, lane: 2),
ScalarLoad(24, 84, destination: 4),
EndProgram(32));
var plan = Extract(program, userDataBase: 84, userDataCount: 2);
Assert.Empty(plan.TableReads);
var access = plan.Accesses[plan.Memory.Count - 1]!;
Assert.True(access.Handle!.Operands[0].IsUndefined);
Assert.True(plan.Info.UsesDeviceAddresses);
}
// The restored pointer meets the zeroed one at the join, so the read is not a
// host-side table read; it stays an in-shader device-address read.
[Fact]
public void RestoredPointer_MeetsTheOtherPathAtTheJoin()
{
var program = Program(
WriteLane(0, vectorRegister: 18, scalarRegister: 84, lane: 2),
WriteLane(8, vectorRegister: 18, scalarRegister: 85, lane: 5),
MoveScalar(16, 84, 0),
MoveScalar(20, 85, 0),
Branch(24, "SCbranchScc0", 4),
ReadLane(28, scalarRegister: 84, vectorRegister: 18, lane: 2),
ReadLane(36, scalarRegister: 85, vectorRegister: 18, lane: 5),
ScalarLoad(44, 84, destination: 4),
EndProgram(52));
var plan = Extract(program, userDataBase: 84, userDataCount: 2);
Assert.Empty(plan.TableReads);
var access = plan.Accesses[plan.Memory.Count - 1]!;
Assert.Equal(ScalarValueKind.Phi, access.Handle!.Operands[0].Kind);
Assert.Null(plan.Graph.ResolveInvariantPhi(access.Handle.Operands[0]));
Assert.True(plan.Info.UsesDeviceAddresses);
}
[Fact]
public void UniformVectorDerivedValue_IsAccepted()
{
var program = Program(
Vop1(0, "VMovB32", 0, Gen5Operand.Scalar(4)),
ReadFirstLane(4, 8, 0),
MoveScalar(8, 9, 0),
MoveScalar(12, 10, 16),
MoveScalar(16, 11, 0),
BufferLoad(20, 8),
EndProgram(28));
var plan = Extract(program);
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, plan.Info.Buffers[0].Source, Inputs([0, 0, 0, 0, 0x77]), out var result));
Assert.Equal(0x77u, result.Dwords[0]);
}
[Fact]
public void DivergentVectorValue_IsRejected()
{
var perLane = Program(
BufferLoad(0, 0, dwords: 1),
ReadFirstLane(8, 8, 4),
MoveScalar(12, 9, 0),
MoveScalar(16, 10, 16),
MoveScalar(20, 11, 0),
BufferLoad(24, 8),
EndProgram(32));
var error = Assert.Throws<ResourcePlanException>(() => Extract(perLane));
Assert.Contains("not a valid runtime value", error.Message);
Assert.Contains("pc=0x00000018", error.Message);
var laneCount = Program(
Vop2(0, "VMbcntLoU32B32", 1, Operand(0xFFFFFFFF), Gen5Operand.Vector(0)),
ReadFirstLane(8, 8, 1),
MoveScalar(12, 9, 0),
MoveScalar(16, 10, 16),
MoveScalar(20, 11, 0),
BufferLoad(24, 8),
EndProgram(32));
Assert.Throws<ResourcePlanException>(() => Extract(laneCount));
}
[Fact]
public void UserDataChangesBetweenDraws_ChangeTheDescriptorWithoutRebuildingThePlan()
{
var plan = Extract(Program(BufferLoad(0, 4), EndProgram(8)));
var source = plan.Info.Buffers[0].Source;
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, source, Inputs([0, 0, 0, 0, 0x1000, 1, 2, 3]), out var first));
Assert.True(RuntimeValueEvaluator.EvaluateDescriptorSource(plan, source, Inputs([0, 0, 0, 0, 0x2000, 4, 5, 6]), out var second));
Assert.Equal([0x1000u, 1u, 2u, 3u], first.Dwords);
Assert.Equal([0x2000u, 4u, 5u, 6u], second.Dwords);
Assert.All(plan.DescriptorSources[(int)source].Dwords, dword => Assert.Equal(ScalarValueKind.UserData, dword.Kind));
}
[Fact]
public void ShaderBaseChangesBetweenDraws_ChangeAnAddressDerivedDescriptor()
{
var program = Program(
new Gen5ShaderInstruction(0, Gen5ShaderEncoding.Sop1, "SGetpcB64", [0u], [], [Gen5Operand.Scalar(4)], null),
Sop2(4, "SAddU32", 4, Gen5Operand.Scalar(4), Operand(0x100)),
Sop2(8, "SAddcU32", 5, Gen5Operand.Scalar(5), Operand(0)),
ScalarLoad(12, 4, destination: 8, count: 4),
BufferLoad(20, 8),
EndProgram(28));
var plan = Extract(program);
Assert.Equal(4, plan.TableReads.Count);
foreach (var shaderBase in new ulong[] { 0x1_0000_0000, 0x2_0000_0000 })
{
var reader = (ulong address, out uint word) =>
{
word = (uint)(address >> 32);
return address >= shaderBase + 0x104 && address < shaderBase + 0x114;
};
Assert.True(RuntimeValueEvaluator.FlattenResourceTable(plan, Inputs([], new GuestWordReader(reader), shaderBase: shaderBase), out var table));
Assert.Equal([(uint)(shaderBase >> 32), (uint)(shaderBase >> 32), (uint)(shaderBase >> 32), (uint)(shaderBase >> 32)], table);
}
}
}