diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImplicitVccTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImplicitVccTests.cs new file mode 100644 index 00000000..1d2114d1 --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ImplicitVccTests.cs @@ -0,0 +1,90 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections.Generic; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Ir; +using Xunit; + +namespace SharpEmu.ShaderCompiler.Tests; + +public sealed class Gen5ImplicitVccTests +{ + private static Gen5ShaderInstruction Vopc(uint pc, string opcode = "VCmpEqU32") => + new(pc, Gen5ShaderEncoding.Vopc, opcode, [0u], [], [], null); + + private static Gen5ShaderInstruction Vop2(uint pc, string opcode) => + new(pc, Gen5ShaderEncoding.Vop2, opcode, [0u], [], [Gen5Operand.Vector(0)], null); + + private static Gen5ShaderInstruction Nop(uint pc) => + new(pc, Gen5ShaderEncoding.Sop1, "SNop", [0u], [], [], null); + + [Fact] + public void VopcIsRecognisedAsAnImplicitVccWriter() + { + Assert.True(Gen5ScalarSsa.WritesVccImplicitly(Vopc(0))); + Assert.True(Gen5ScalarSsa.WritesVccImplicitly(Vopc(0, "VCmpLtF32"))); + } + + [Fact] + public void Vop2CarryFormsAreRecognised() + { + Assert.True(Gen5ScalarSsa.WritesVccImplicitly(Vop2(0, "VAddCoCiU32"))); + Assert.True(Gen5ScalarSsa.WritesVccImplicitly(Vop2(0, "VSubCoCiU32"))); + Assert.True(Gen5ScalarSsa.WritesVccImplicitly(Vop2(0, "VSubrevCoCiU32"))); + } + + [Fact] + public void PlainVop2DoesNotWriteVcc() + { + Assert.False(Gen5ScalarSsa.WritesVccImplicitly(Vop2(0, "VAddF32"))); + Assert.False(Gen5ScalarSsa.WritesVccImplicitly(Nop(0))); + } + + [Fact] + public void VopcDefinesBothVccHalves() + { + List program = [Vopc(0), Nop(4)]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + + var low = ssa.GetReachingDefinitionAt(4, Gen5ScalarSsa.VccLo); + var high = ssa.GetReachingDefinitionAt(4, Gen5ScalarSsa.VccHi); + + Assert.Equal(IrReachingState.Single, low.State); + Assert.Equal(0u, low.DefinitionPc); + Assert.Equal(IrReachingState.Single, high.State); + Assert.Equal(0u, high.DefinitionPc); + } + + [Fact] + public void WithoutTheImplicitWriteVccWouldLookUndefined() + { + // The regression this models: before implicit VCC was tracked the offset + // register reported "none" and its stale value was used as a byte offset. + List program = [Nop(0), Nop(4)]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + + Assert.Equal(IrReachingState.None, ssa.GetReachingDefinitionAt(4, Gen5ScalarSsa.VccLo).State); + } + + [Fact] + public void VccWrittenOnBothBranchesBecomesMultiple() + { + List program = + [ + new(0, Gen5ShaderEncoding.Sopp, "SCbranchScc0", [2u], [], [], null), + Vopc(4), + new(8, Gen5ShaderEncoding.Sopp, "SBranch", [1u], [], [], null), + Vopc(12), + Nop(16), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + + Assert.Equal( + IrReachingState.Multiple, + ssa.GetReachingDefinitionAt(16, Gen5ScalarSsa.VccLo).State); + } +} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ReachingDefinitionTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ReachingDefinitionTests.cs new file mode 100644 index 00000000..fb42b797 --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ReachingDefinitionTests.cs @@ -0,0 +1,142 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections.Generic; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Ir; +using Xunit; + +namespace SharpEmu.ShaderCompiler.Tests; + +public sealed class Gen5ReachingDefinitionTests +{ + private static Gen5ShaderInstruction Load(uint pc, uint destination) => + new( + pc, + Gen5ShaderEncoding.Smem, + "SLoadDwordx4", + [0u, 0u], + [Gen5Operand.Scalar(0)], + [Gen5Operand.Scalar(destination)], + null); + + private static Gen5ShaderInstruction Nop(uint pc) => + new(pc, Gen5ShaderEncoding.Sop1, "SNop", [0u], [], [], null); + + private static Gen5ShaderInstruction Branch(uint pc, string opcode, short wordOffset) => + new( + pc, + Gen5ShaderEncoding.Sopp, + opcode, + [unchecked((uint)(ushort)wordOffset)], + [], + [], + null); + + [Fact] + public void SingleDefinitionIsTracked() + { + List program = [Load(0, 16), Nop(4)]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + var reaching = ssa.GetReachingDefinitionAt(4, 16); + + Assert.Equal(IrReachingState.Single, reaching.State); + Assert.Equal(0u, reaching.DefinitionPc); + } + + [Fact] + public void TwoDefinitionsOnDifferentPathsBecomeMultiple() + { + // The case that produced garbage descriptors: the same register is + // written on both sides of a branch and read after the join. + List program = + [ + Branch(0, "SCbranchScc0", 2), + Load(4, 16), + Branch(8, "SBranch", 1), + Load(12, 16), + Nop(16), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + + Assert.Equal(IrReachingState.Multiple, ssa.GetReachingDefinitionAt(16, 16).State); + } + + [Fact] + public void DefinitionOnOnlyOnePathIsAlsoMultipleAtTheJoin() + { + List program = + [ + Load(0, 16), + Branch(4, "SCbranchScc0", 1), + Load(8, 16), + Nop(12), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + + Assert.Equal(IrReachingState.Multiple, ssa.GetReachingDefinitionAt(12, 16).State); + } + + [Fact] + public void DefinitionBeforeTheBranchStaysSingle() + { + List program = + [ + Load(0, 16), + Branch(4, "SCbranchScc0", 1), + Load(8, 16), + Nop(12), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + var reaching = ssa.GetReachingDefinitionAt(4, 16); + + Assert.Equal(IrReachingState.Single, reaching.State); + Assert.Equal(0u, reaching.DefinitionPc); + } + + [Fact] + public void UserDataCountsAsADefinition() + { + List program = [Nop(0)]; + + var ssa = Gen5ScalarSsa.Build(program, userData: [0x1111, 0x2222]); + + Assert.Equal(IrReachingState.Single, ssa.GetReachingDefinitionAt(0, 0).State); + Assert.Equal(IrReachingState.None, ssa.GetReachingDefinitionAt(0, 64).State); + } + + [Fact] + public void UnwrittenRegisterHasNoDefinition() + { + List program = [Load(0, 16), Nop(4)]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + + Assert.Equal(IrReachingState.None, ssa.GetReachingDefinitionAt(4, 32).State); + } + + [Fact] + public void ReachingDefinitionSeesLoadsThatConstantPropagationCannot() + { + // The whole point of the second analysis: SLoadDwordx4 has no compile-time + // value, so constant propagation reports Unknown and never Merged. Reaching + // definitions still proves the register has two possible writers. + List program = + [ + Branch(0, "SCbranchScc0", 2), + Load(4, 16), + Branch(8, "SBranch", 1), + Load(12, 16), + Nop(16), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + + Assert.Equal(IrScalarState.Unknown, ssa.GetScalarAt(16, 16).State); + Assert.Equal(IrReachingState.Multiple, ssa.GetReachingDefinitionAt(16, 16).State); + } +} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarSsaTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarSsaTests.cs new file mode 100644 index 00000000..563171fc --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/Gen5ScalarSsaTests.cs @@ -0,0 +1,168 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections.Generic; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Ir; +using Xunit; + +namespace SharpEmu.ShaderCompiler.Tests; + +public sealed class Gen5ScalarSsaTests +{ + private static Gen5ShaderInstruction Mov(uint pc, uint destination, uint literal) => + new( + pc, + Gen5ShaderEncoding.Sop1, + "SMov", + [0u], + [new Gen5Operand(Gen5OperandKind.LiteralConstant, literal)], + [Gen5Operand.Scalar(destination)], + null); + + private static Gen5ShaderInstruction Nop(uint pc) => + new(pc, Gen5ShaderEncoding.Sop1, "SNop", [0u], [], [], null); + + private static Gen5ShaderInstruction Branch(uint pc, string opcode, short wordOffset) => + new( + pc, + Gen5ShaderEncoding.Sopp, + opcode, + [unchecked((uint)(ushort)wordOffset)], + [], + [], + null); + + [Fact] + public void StraightLineMovResolvesToAConstant() + { + List program = + [ + Mov(0, destination: 8, literal: 0x1234), + Nop(4), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + var value = ssa.GetScalarAt(4, 8); + + Assert.Equal(IrScalarState.Constant, value.State); + Assert.Equal(0x1234u, value.Constant); + } + + [Fact] + public void UserDataSeedsTheEntryState() + { + List program = [Nop(0)]; + + var ssa = Gen5ScalarSsa.Build(program, userData: [0x40F55240, 0x00000004]); + + Assert.Equal(IrScalarState.Constant, ssa.GetScalarAt(0, 0).State); + Assert.Equal(0x40F55240u, ssa.GetScalarAt(0, 0).Constant); + Assert.Equal(0x00000004u, ssa.GetScalarAt(0, 1).Constant); + } + + [Fact] + public void ConflictingValuesFromTwoPathsBecomeMerged() + { + // if (cc) s8 = 0xAAAA else s8 = 0xBBBB; use s8 + List program = + [ + Branch(0, "SCbranchScc0", 2), + Mov(4, destination: 8, literal: 0xAAAA), + Branch(8, "SBranch", 1), + Mov(12, destination: 8, literal: 0xBBBB), + Nop(16), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + + Assert.True(ssa.Graph.HasControlFlow); + Assert.Equal(IrScalarState.Merged, ssa.GetScalarAt(16, 8).State); + } + + [Fact] + public void SameValueOnBothPathsStaysResolved() + { + List program = + [ + Branch(0, "SCbranchScc0", 2), + Mov(4, destination: 8, literal: 0xCAFE), + Branch(8, "SBranch", 1), + Mov(12, destination: 8, literal: 0xCAFE), + Nop(16), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + var value = ssa.GetScalarAt(16, 8); + + Assert.Equal(IrScalarState.Constant, value.State); + Assert.Equal(0xCAFEu, value.Constant); + } + + [Fact] + public void RegisterWrittenOnOnlyOnePathIsMergedAtTheJoin() + { + List program = + [ + Mov(0, destination: 8, literal: 0x1111), + Branch(4, "SCbranchScc0", 1), + Mov(8, destination: 8, literal: 0x2222), + Nop(12), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + + Assert.Equal(IrScalarState.Merged, ssa.GetScalarAt(12, 8).State); + } + + [Fact] + public void ValueBeforeTheBranchIsStillResolved() + { + List program = + [ + Mov(0, destination: 8, literal: 0x1111), + Branch(4, "SCbranchScc0", 1), + Mov(8, destination: 8, literal: 0x2222), + Nop(12), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + var value = ssa.GetScalarAt(4, 8); + + Assert.Equal(IrScalarState.Constant, value.State); + Assert.Equal(0x1111u, value.Constant); + } + + [Fact] + public void MergeJoinIsReportedForDivergentBlocks() + { + List program = + [ + Branch(0, "SCbranchScc0", 1), + Mov(4, destination: 8, literal: 1), + Nop(8), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + + Assert.True(ssa.IsInsideDivergentMerge(8)); + Assert.False(ssa.IsInsideDivergentMerge(0)); + } + + [Fact] + public void LoopDoesNotHangTheFixpoint() + { + List program = + [ + Mov(0, destination: 8, literal: 1), + Nop(4), + Branch(8, "SCbranchScc1", -3), + Nop(12), + ]; + + var ssa = Gen5ScalarSsa.Build(program, userData: []); + + Assert.NotEmpty(ssa.Graph.LoopHeaders); + Assert.Equal(IrScalarState.Constant, ssa.GetScalarAt(12, 8).State); + } +} diff --git a/tests/SharpEmu.ShaderCompiler.Tests/IrControlFlowTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/IrControlFlowTests.cs new file mode 100644 index 00000000..8727a538 --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/IrControlFlowTests.cs @@ -0,0 +1,127 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections.Generic; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Ir; +using Xunit; + +namespace SharpEmu.ShaderCompiler.Tests; + +public sealed class IrControlFlowTests +{ + private sealed class Resolver : IIrBranchResolver + { + public Dictionary Branches { get; } = []; + + public bool IsBranch(Gen5ShaderInstruction instruction) => + Branches.ContainsKey(instruction.Pc); + + public bool IsConditional(Gen5ShaderInstruction instruction) => + Branches.TryGetValue(instruction.Pc, out var branch) && branch.Conditional; + + public bool TryGetBranchTarget(Gen5ShaderInstruction instruction, out uint targetPc) + { + if (Branches.TryGetValue(instruction.Pc, out var branch)) + { + targetPc = branch.Target; + return true; + } + + targetPc = 0; + return false; + } + } + + private static Gen5ShaderInstruction Instruction(uint pc) => + new(pc, Gen5ShaderEncoding.Sop1, "SMov", [], [], [], null); + + private static List Straight(params uint[] pcs) + { + var list = new List(); + foreach (var pc in pcs) + { + list.Add(Instruction(pc)); + } + + return list; + } + + [Fact] + public void StraightLineCodeIsASingleBlock() + { + var instructions = Straight(0, 4, 8, 12); + + var cfg = IrControlFlowGraph.Build(instructions, new Resolver()); + + Assert.Single(cfg.Blocks); + Assert.False(cfg.HasControlFlow); + Assert.Empty(cfg.LoopHeaders); + } + + [Fact] + public void ConditionalBranchSplitsIntoThreeBlocks() + { + var instructions = Straight(0, 4, 8, 12, 16); + var resolver = new Resolver(); + resolver.Branches[4] = (Conditional: true, Target: 12); + + var cfg = IrControlFlowGraph.Build(instructions, resolver); + + Assert.Equal(3, cfg.Blocks.Count); + Assert.True(cfg.HasControlFlow); + + var entry = cfg.BlockByStartPc[0]; + var fallthrough = cfg.BlockByStartPc[8]; + var target = cfg.BlockByStartPc[12]; + + Assert.Contains(target, cfg.Successors[entry]); + Assert.Contains(fallthrough, cfg.Successors[entry]); + Assert.Contains(entry, cfg.Predecessors[target]); + } + + [Fact] + public void UnconditionalBranchDoesNotFallThrough() + { + var instructions = Straight(0, 4, 8, 12); + var resolver = new Resolver(); + resolver.Branches[4] = (Conditional: false, Target: 12); + + var cfg = IrControlFlowGraph.Build(instructions, resolver); + + var entry = cfg.BlockByStartPc[0]; + var skipped = cfg.BlockByStartPc[8]; + var target = cfg.BlockByStartPc[12]; + + Assert.Equal([target], cfg.Successors[entry]); + Assert.DoesNotContain(skipped, cfg.Successors[entry]); + } + + [Fact] + public void BackwardBranchMarksALoopHeader() + { + var instructions = Straight(0, 4, 8, 12); + var resolver = new Resolver(); + resolver.Branches[8] = (Conditional: true, Target: 4); + + var cfg = IrControlFlowGraph.Build(instructions, resolver); + + var header = cfg.BlockByStartPc[4]; + Assert.Contains(header, cfg.LoopHeaders); + Assert.Contains(header, cfg.Successors[cfg.BlockByStartPc[4]]); + } + + [Fact] + public void MergeBlockRecordsBothPredecessors() + { + var instructions = Straight(0, 4, 8, 12, 16, 20); + var resolver = new Resolver(); + resolver.Branches[0] = (Conditional: true, Target: 12); + resolver.Branches[8] = (Conditional: false, Target: 16); + + var cfg = IrControlFlowGraph.Build(instructions, resolver); + + var merge = cfg.BlockByStartPc[16]; + Assert.Equal(2, cfg.Predecessors[merge].Count); + } +}