mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 06:59:45 +08:00
[shader] add cfg and dataflow tests
This commit is contained in:
@@ -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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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<Gen5ShaderInstruction> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<uint, (bool Conditional, uint Target)> 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<Gen5ShaderInstruction> Straight(params uint[] pcs)
|
||||
{
|
||||
var list = new List<Gen5ShaderInstruction>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user