diff --git a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs index 7c980cb2..c5e413df 100644 --- a/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs +++ b/src/SharpEmu.ShaderCompiler/Gen5ShaderScalarEvaluator.cs @@ -7,6 +7,7 @@ using System.Buffers.Binary; using System.Collections.Concurrent; using System.Diagnostics; using System.Numerics; +using System.Runtime.CompilerServices; namespace SharpEmu.ShaderCompiler; @@ -36,6 +37,113 @@ public static class Gen5ShaderScalarEvaluator StringComparison.Ordinal); private static readonly object _scalarFallbackTraceGate = new(); private static readonly HashSet<(ulong Shader, uint Pc)> _tracedScalarFallbacks = []; + private static readonly HashSet<(ulong Shader, uint Pc)> _tracedDivergentDescriptors = []; + + private static readonly ConditionalWeakTable _scalarSsaCache = []; + + private static readonly bool _divergentDescriptorGuard = !string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_IR_DESCRIPTOR_GUARD"), + "0", + StringComparison.Ordinal); + + private static Ir.Gen5ScalarSsa GetScalarSsa(Gen5ShaderState state) => + _scalarSsaCache.GetValue( + state.Program, + program => Ir.Gen5ScalarSsa.Build(program.Instructions, state.UserData)); + + /// + /// The byte offset comes from an SGPR. When the instruction that produced that + /// register is one the scalar evaluator cannot reproduce — a vector compare + /// writing VCC, say, whose value depends on per-lane data — the register still + /// holds whatever the linear walk left in it. Adding that to an otherwise valid + /// base address is how descriptors turned into addresses far out of range. + /// + private static bool IsOffsetFromUnmodelledWriter( + Gen5ShaderState state, + Gen5ShaderInstruction instruction, + Gen5ScalarMemoryControl control) + { + if (!_divergentDescriptorGuard || control.DynamicOffsetRegister is not { } offsetRegister) + { + return false; + } + + var ssa = GetScalarSsa(state); + var reaching = ssa.GetReachingDefinitionAt(instruction.Pc, offsetRegister); + if (reaching.State == Ir.IrReachingState.Multiple) + { + return true; + } + + if (reaching.State != Ir.IrReachingState.Single || + reaching.DefinitionPc == uint.MaxValue) + { + return false; + } + + var writer = state.Program.Instructions + .FirstOrDefault(candidate => candidate.Pc == reaching.DefinitionPc); + return writer is not null && Ir.Gen5ScalarSsa.WritesVccImplicitly(writer); + } + + /// + /// A descriptor assembled from registers that differ per incoming path is not a + /// descriptor, it is whichever path the linear walk happened to take last. + /// + private static bool IsDescriptorFromDivergentMerge( + Gen5ShaderState state, + uint pc, + uint scalarBase, + uint registerCount) + { + if (!_divergentDescriptorGuard) + { + return false; + } + + var ssa = GetScalarSsa(state); + if (!ssa.Graph.HasControlFlow) + { + return false; + } + + for (var offset = 0u; offset < registerCount; offset++) + { + var reaching = ssa.GetReachingDefinitionAt(pc, scalarBase + offset); + if (reaching.State == Ir.IrReachingState.Multiple) + { + return true; + } + + if (ssa.GetScalarAt(pc, scalarBase + offset).State == Ir.IrScalarState.Merged) + { + return true; + } + } + + return false; + } + + private static void TraceDivergentDescriptor( + Gen5ShaderState state, + Gen5ShaderInstruction instruction, + uint scalarBase, + ulong baseAddress) + { + lock (_scalarFallbackTraceGate) + { + if (!_tracedDivergentDescriptors.Add((state.Program.Address, instruction.Pc))) + { + return; + } + } + + Console.Error.WriteLine( + $"[LOADER][WARN] agc.descriptor_divergent " + + $"shader=0x{state.Program.Address:X16} pc=0x{instruction.Pc:X} " + + $"op={instruction.Opcode} base=s{scalarBase} " + + $"linear_base_addr=0x{baseAddress:X16} (unbound instead of dereferenced)"); + } // Shaders whose empty SRT/EUD caused a null-base scalar pointer load. // Host submit of those translations has lost the Vulkan device; Agc skips // them before QueueSubmit. @@ -1898,19 +2006,32 @@ public static class Gen5ShaderScalarEvaluator var address = unchecked( baseAddress + byteOffset) & ~3UL; + var descriptorDiverged = IsDescriptorFromDivergentMerge( + state, + instruction.Pc, + scalarBase.Value, + isBufferLoad ? 4u : 2u) || + IsOffsetFromUnmodelledWriter(state, instruction, control); + if (descriptorDiverged) + { + TraceDivergentDescriptor(state, instruction, scalarBase.Value, baseAddress); + } + var bufferUnbound = isBufferLoad && - (!hasBufferDescriptor || + (descriptorDiverged || + !hasBufferDescriptor || bufferDescriptor.SizeBytes == 0 || (scalarRegisters[scalarBase.Value] == 0 && scalarRegisters[scalarBase.Value + 1] == 0 && scalarBase.Value + 3 < ScalarRegisterCount && scalarRegisters[scalarBase.Value + 2] == 0 && scalarRegisters[scalarBase.Value + 3] == 0)); - var scalarPointerUnbound = ShouldTreatScalarPointerAsUnbound( - isBufferLoad, - address, - _strictScalarLoad); + var scalarPointerUnbound = descriptorDiverged && !isBufferLoad || + ShouldTreatScalarPointerAsUnbound( + isBufferLoad, + address, + _strictScalarLoad); if (scalarPointerUnbound) { TraceScalarPointerFallback( diff --git a/src/SharpEmu.ShaderCompiler/Ir/Gen5IrBranchResolver.cs b/src/SharpEmu.ShaderCompiler/Ir/Gen5IrBranchResolver.cs new file mode 100644 index 00000000..c723ab3c --- /dev/null +++ b/src/SharpEmu.ShaderCompiler/Ir/Gen5IrBranchResolver.cs @@ -0,0 +1,67 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System; + +namespace SharpEmu.ShaderCompiler.Ir; + +public sealed class Gen5IrBranchResolver : IIrBranchResolver +{ + public static Gen5IrBranchResolver Instance { get; } = new(); + + public bool IsBranch(Gen5ShaderInstruction instruction) => + IsUnconditionalBranch(instruction) || + IsConditional(instruction) || + IsTerminator(instruction); + + public bool IsConditional(Gen5ShaderInstruction instruction) => instruction.Opcode switch + { + "SCbranchScc0" or + "SCbranchScc1" or + "SCbranchVccz" or + "SCbranchVccnz" or + "SCbranchExecz" or + "SCbranchExecnz" or + "SCbranchCdbgsys" or + "SCbranchCdbguser" or + "SCbranchCdbgsysOrUser" or + "SCbranchCdbgsysAndUser" => true, + _ => false, + }; + + public bool TryGetBranchTarget(Gen5ShaderInstruction instruction, out uint targetPc) + { + targetPc = 0; + if (IsTerminator(instruction)) + { + return false; + } + + if (!IsUnconditionalBranch(instruction) && !IsConditional(instruction)) + { + return false; + } + + if (instruction.Encoding != Gen5ShaderEncoding.Sopp || instruction.Words.Count == 0) + { + return false; + } + + var offset = unchecked((short)(instruction.Words[0] & 0xFFFF)); + var nextPc = (long)instruction.Pc + instruction.Words.Count * sizeof(uint); + var target = nextPc + offset * sizeof(uint); + if (target < 0 || target > uint.MaxValue) + { + return false; + } + + targetPc = (uint)target; + return true; + } + + public static bool IsUnconditionalBranch(Gen5ShaderInstruction instruction) => + string.Equals(instruction.Opcode, "SBranch", StringComparison.Ordinal); + + public static bool IsTerminator(Gen5ShaderInstruction instruction) => + instruction.Opcode is "SEndpgm" or "SEndpgmSaved"; +} diff --git a/src/SharpEmu.ShaderCompiler/Ir/Gen5ScalarSsa.cs b/src/SharpEmu.ShaderCompiler/Ir/Gen5ScalarSsa.cs new file mode 100644 index 00000000..7b5d715e --- /dev/null +++ b/src/SharpEmu.ShaderCompiler/Ir/Gen5ScalarSsa.cs @@ -0,0 +1,498 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections.Generic; +using System.Linq; + +namespace SharpEmu.ShaderCompiler.Ir; + +public enum IrScalarState +{ + Unknown, + Constant, + Merged, +} + +public enum IrReachingState +{ + None, + Single, + Multiple, +} + +public readonly record struct IrReachingDefinition(IrReachingState State, uint DefinitionPc) +{ + public static readonly IrReachingDefinition None = new(IrReachingState.None, 0); + + public static readonly IrReachingDefinition Multiple = new(IrReachingState.Multiple, 0); + + public static IrReachingDefinition At(uint pc) => new(IrReachingState.Single, pc); + + public IrReachingDefinition Join(IrReachingDefinition other) + { + if (State == IrReachingState.None) + { + return other; + } + + if (other.State == IrReachingState.None) + { + return this; + } + + if (State == IrReachingState.Single && + other.State == IrReachingState.Single && + DefinitionPc == other.DefinitionPc) + { + return this; + } + + return Multiple; + } +} + +public readonly record struct IrScalarValue(IrScalarState State, uint Constant) +{ + public static readonly IrScalarValue Unknown = new(IrScalarState.Unknown, 0); + + public static readonly IrScalarValue Merged = new(IrScalarState.Merged, 0); + + public static IrScalarValue FromConstant(uint value) => new(IrScalarState.Constant, value); + + public bool IsResolved => State == IrScalarState.Constant; + + public IrScalarValue Join(IrScalarValue other) + { + if (State == IrScalarState.Unknown) + { + return other; + } + + if (other.State == IrScalarState.Unknown) + { + return this; + } + + if (State == IrScalarState.Constant && + other.State == IrScalarState.Constant && + Constant == other.Constant) + { + return this; + } + + return Merged; + } +} + +public sealed class Gen5ScalarSsa +{ + public const int ScalarRegisterCount = 256; + + private Gen5ScalarSsa( + IrControlFlowGraph graph, + IReadOnlyList entryState, + IReadOnlyList exitState, + IReadOnlyList entryDefinitions, + IReadOnlyDictionary blockByPc, + IReadOnlyList instructions) + { + Graph = graph; + _entryState = entryState; + _exitState = exitState; + _entryDefinitions = entryDefinitions; + _blockByPc = blockByPc; + _instructions = instructions; + } + + private readonly IReadOnlyList _entryDefinitions; + + public IrControlFlowGraph Graph { get; } + + private readonly IReadOnlyList _entryState; + private readonly IReadOnlyList _exitState; + private readonly IReadOnlyDictionary _blockByPc; + private readonly IReadOnlyList _instructions; + + public static Gen5ScalarSsa Build( + IReadOnlyList instructions, + IReadOnlyList userData, + IIrBranchResolver? resolver = null) + { + resolver ??= Gen5IrBranchResolver.Instance; + var graph = IrControlFlowGraph.Build(instructions, resolver); + var blockCount = graph.Blocks.Count; + + var entry = new List(blockCount); + var exit = new List(blockCount); + var defEntry = new List(blockCount); + var defExit = new List(blockCount); + for (var index = 0; index < blockCount; index++) + { + entry.Add(NewState()); + exit.Add(NewState()); + defEntry.Add(NewDefinitions()); + defExit.Add(NewDefinitions()); + } + + if (blockCount > 0) + { + var initial = entry[0]; + for (var index = 0; index < userData.Count && index < ScalarRegisterCount; index++) + { + initial[index] = IrScalarValue.FromConstant(userData[index]); + } + } + + var blockByPc = new Dictionary(); + for (var blockIndex = 0; blockIndex < blockCount; blockIndex++) + { + var range = graph.Blocks[blockIndex]; + foreach (var instruction in instructions) + { + if (instruction.Pc >= range.StartPc && instruction.Pc < range.EndPc) + { + blockByPc[instruction.Pc] = blockIndex; + } + } + } + + var worklist = new Queue(); + for (var index = 0; index < blockCount; index++) + { + worklist.Enqueue(index); + } + + var visits = new int[blockCount]; + const int visitLimit = 8; + while (worklist.Count > 0) + { + var blockIndex = worklist.Dequeue(); + if (visits[blockIndex]++ > visitLimit) + { + continue; + } + + var state = (IrScalarValue[])entry[blockIndex].Clone(); + if (graph.Predecessors[blockIndex].Count > 0) + { + state = NewState(); + var first = true; + foreach (var predecessor in graph.Predecessors[blockIndex]) + { + var incoming = exit[predecessor]; + for (var register = 0; register < ScalarRegisterCount; register++) + { + state[register] = first + ? incoming[register] + : state[register].Join(incoming[register]); + } + + first = false; + } + + if (blockIndex == 0) + { + for (var register = 0; register < userData.Count && register < ScalarRegisterCount; register++) + { + state[register] = state[register].Join( + IrScalarValue.FromConstant(userData[register])); + } + } + } + + entry[blockIndex] = state; + + var definitions = NewDefinitions(); + if (graph.Predecessors[blockIndex].Count == 0) + { + for (var register = 0; register < userData.Count && register < ScalarRegisterCount; register++) + { + definitions[register] = IrReachingDefinition.At(uint.MaxValue); + } + } + else + { + var first = true; + foreach (var predecessor in graph.Predecessors[blockIndex]) + { + var incoming = defExit[predecessor]; + for (var register = 0; register < ScalarRegisterCount; register++) + { + definitions[register] = first + ? incoming[register] + : definitions[register].Join(incoming[register]); + } + + first = false; + } + } + + defEntry[blockIndex] = definitions; + + var computed = Transfer(instructions, graph.Blocks[blockIndex], state); + var computedDefinitions = TransferDefinitions( + instructions, + graph.Blocks[blockIndex], + definitions); + var changed = !SameState(exit[blockIndex], computed) || + !SameDefinitions(defExit[blockIndex], computedDefinitions); + exit[blockIndex] = computed; + defExit[blockIndex] = computedDefinitions; + if (changed) + { + foreach (var successor in graph.Successors[blockIndex]) + { + worklist.Enqueue(successor); + } + } + } + + return new Gen5ScalarSsa(graph, entry, exit, defEntry, blockByPc, instructions); + } + + public IrScalarValue GetScalarAt(uint pc, uint register) + { + if (register >= ScalarRegisterCount || !_blockByPc.TryGetValue(pc, out var blockIndex)) + { + return IrScalarValue.Unknown; + } + + var state = (IrScalarValue[])_entryState[blockIndex].Clone(); + var range = _graphRange(blockIndex); + foreach (var instruction in _instructions) + { + if (instruction.Pc < range.StartPc || instruction.Pc >= range.EndPc) + { + continue; + } + + if (instruction.Pc >= pc) + { + break; + } + + Apply(instruction, state); + } + + return state[register]; + } + + public IrReachingDefinition GetReachingDefinitionAt(uint pc, uint register) + { + if (register >= ScalarRegisterCount || !_blockByPc.TryGetValue(pc, out var blockIndex)) + { + return IrReachingDefinition.None; + } + + var definitions = (IrReachingDefinition[])_entryDefinitions[blockIndex].Clone(); + var range = _graphRange(blockIndex); + foreach (var instruction in _instructions) + { + if (instruction.Pc < range.StartPc || instruction.Pc >= range.EndPc) + { + continue; + } + + if (instruction.Pc >= pc) + { + break; + } + + ApplyDefinitions(instruction, definitions); + } + + return definitions[register]; + } + + public bool IsInsideDivergentMerge(uint pc) => + _blockByPc.TryGetValue(pc, out var blockIndex) && + _graphPredecessorCount(blockIndex) > 1; + + private IrBlockRange _graphRange(int blockIndex) => Graph.Blocks[blockIndex]; + + private int _graphPredecessorCount(int blockIndex) => Graph.Predecessors[blockIndex].Count; + + private static IrScalarValue[] NewState() + { + var state = new IrScalarValue[ScalarRegisterCount]; + for (var index = 0; index < state.Length; index++) + { + state[index] = IrScalarValue.Unknown; + } + + return state; + } + + private static IrReachingDefinition[] NewDefinitions() + { + var definitions = new IrReachingDefinition[ScalarRegisterCount]; + for (var index = 0; index < definitions.Length; index++) + { + definitions[index] = IrReachingDefinition.None; + } + + return definitions; + } + + private static bool SameDefinitions(IrReachingDefinition[] left, IrReachingDefinition[] right) + { + for (var index = 0; index < left.Length; index++) + { + if (!left[index].Equals(right[index])) + { + return false; + } + } + + return true; + } + + private static IrReachingDefinition[] TransferDefinitions( + IReadOnlyList instructions, + IrBlockRange range, + IrReachingDefinition[] entry) + { + var definitions = (IrReachingDefinition[])entry.Clone(); + foreach (var instruction in instructions) + { + if (instruction.Pc < range.StartPc || instruction.Pc >= range.EndPc) + { + continue; + } + + ApplyDefinitions(instruction, definitions); + } + + return definitions; + } + + public const uint VccLo = 106; + + public const uint VccHi = 107; + + /// + /// VOPC compares and the VOP2 carry forms write VCC without naming it: the ISA + /// makes the destination implicit in the encoding, so the decoded instruction + /// carries no destination operand for it. Modelling that here (rather than in + /// the shared decoder) keeps the linear evaluator's behaviour untouched while + /// letting the dataflow see that VCC was written. + /// + public static bool WritesVccImplicitly(Gen5ShaderInstruction instruction) + { + if (instruction.Encoding == Gen5ShaderEncoding.Vopc) + { + return true; + } + + return instruction.Encoding == Gen5ShaderEncoding.Vop2 && + instruction.Opcode is + "VAddCoCiU32" or + "VSubCoCiU32" or + "VSubrevCoCiU32"; + } + + private static void ApplyDefinitions( + Gen5ShaderInstruction instruction, + IrReachingDefinition[] definitions) + { + foreach (var destination in instruction.Destinations) + { + if (destination.Kind != Gen5OperandKind.ScalarRegister || + destination.Value >= ScalarRegisterCount) + { + continue; + } + + definitions[destination.Value] = IrReachingDefinition.At(instruction.Pc); + } + + if (WritesVccImplicitly(instruction)) + { + definitions[VccLo] = IrReachingDefinition.At(instruction.Pc); + definitions[VccHi] = IrReachingDefinition.At(instruction.Pc); + } + } + + private static bool SameState(IrScalarValue[] left, IrScalarValue[] right) + { + for (var index = 0; index < left.Length; index++) + { + if (!left[index].Equals(right[index])) + { + return false; + } + } + + return true; + } + + private static IrScalarValue[] Transfer( + IReadOnlyList instructions, + IrBlockRange range, + IrScalarValue[] entry) + { + var state = (IrScalarValue[])entry.Clone(); + foreach (var instruction in instructions) + { + if (instruction.Pc < range.StartPc || instruction.Pc >= range.EndPc) + { + continue; + } + + Apply(instruction, state); + } + + return state; + } + + private static void Apply(Gen5ShaderInstruction instruction, IrScalarValue[] state) + { + var resolved = ResolveResult(instruction, state); + foreach (var destination in instruction.Destinations) + { + if (destination.Kind != Gen5OperandKind.ScalarRegister || + destination.Value >= ScalarRegisterCount) + { + continue; + } + + state[destination.Value] = resolved; + } + } + + private static IrScalarValue ResolveResult( + Gen5ShaderInstruction instruction, + IrScalarValue[] state) + { + if (instruction.Destinations.Count != 1) + { + return IrScalarValue.Unknown; + } + + return instruction.Opcode switch + { + "SMov" or "SMovB32" => Source(instruction, state, 0), + _ => IrScalarValue.Unknown, + }; + } + + private static IrScalarValue Source( + Gen5ShaderInstruction instruction, + IrScalarValue[] state, + int index) + { + if (index >= instruction.Sources.Count) + { + return IrScalarValue.Unknown; + } + + var source = instruction.Sources[index]; + return source.Kind switch + { + Gen5OperandKind.ScalarRegister when source.Value < ScalarRegisterCount => + state[source.Value], + Gen5OperandKind.LiteralConstant => IrScalarValue.FromConstant(source.Value), + _ => IrScalarValue.Unknown, + }; + } +} diff --git a/src/SharpEmu.ShaderCompiler/Ir/IrControlFlow.cs b/src/SharpEmu.ShaderCompiler/Ir/IrControlFlow.cs new file mode 100644 index 00000000..4a3a495d --- /dev/null +++ b/src/SharpEmu.ShaderCompiler/Ir/IrControlFlow.cs @@ -0,0 +1,161 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections.Generic; +using System.Linq; + +namespace SharpEmu.ShaderCompiler.Ir; + +public readonly record struct IrBlockRange(uint StartPc, uint EndPc); + +public sealed class IrControlFlowGraph +{ + private IrControlFlowGraph( + IReadOnlyList blocks, + IReadOnlyDictionary blockByStartPc, + IReadOnlyList> successors, + IReadOnlyList> predecessors, + IReadOnlySet loopHeaders) + { + Blocks = blocks; + BlockByStartPc = blockByStartPc; + Successors = successors; + Predecessors = predecessors; + LoopHeaders = loopHeaders; + } + + public IReadOnlyList Blocks { get; } + + public IReadOnlyDictionary BlockByStartPc { get; } + + public IReadOnlyList> Successors { get; } + + public IReadOnlyList> Predecessors { get; } + + public IReadOnlySet LoopHeaders { get; } + + public bool HasControlFlow => Blocks.Count > 1; + + public static IrControlFlowGraph Build( + IReadOnlyList instructions, + IIrBranchResolver resolver) + { + var leaders = new SortedSet(); + if (instructions.Count > 0) + { + leaders.Add(instructions[0].Pc); + } + + for (var index = 0; index < instructions.Count; index++) + { + var instruction = instructions[index]; + if (!resolver.IsBranch(instruction)) + { + continue; + } + + if (resolver.TryGetBranchTarget(instruction, out var target)) + { + leaders.Add(target); + } + + if (index + 1 < instructions.Count) + { + leaders.Add(instructions[index + 1].Pc); + } + } + + var ordered = leaders.ToList(); + var ranges = new List(ordered.Count); + var byStart = new Dictionary(); + for (var index = 0; index < ordered.Count; index++) + { + var start = ordered[index]; + var end = index + 1 < ordered.Count + ? ordered[index + 1] + : instructions.Count > 0 ? instructions[^1].Pc + 1 : start; + byStart[start] = ranges.Count; + ranges.Add(new IrBlockRange(start, end)); + } + + var successors = new List>(ranges.Count); + var predecessors = new List>(ranges.Count); + for (var index = 0; index < ranges.Count; index++) + { + successors.Add([]); + predecessors.Add([]); + } + + for (var blockIndex = 0; blockIndex < ranges.Count; blockIndex++) + { + var range = ranges[blockIndex]; + var last = instructions + .Where(candidate => candidate.Pc >= range.StartPc && candidate.Pc < range.EndPc) + .LastOrDefault(); + if (last is null) + { + continue; + } + + var isBranch = resolver.IsBranch(last); + var hasTarget = isBranch && resolver.TryGetBranchTarget(last, out var target) && + byStart.TryGetValue(target, out var targetIndex); + if (hasTarget) + { + _ = resolver.TryGetBranchTarget(last, out var resolved); + Link(successors, predecessors, blockIndex, byStart[resolved]); + } + + var fallsThrough = !isBranch || resolver.IsConditional(last); + if (fallsThrough && blockIndex + 1 < ranges.Count) + { + Link(successors, predecessors, blockIndex, blockIndex + 1); + } + } + + var headers = new HashSet(); + for (var blockIndex = 0; blockIndex < ranges.Count; blockIndex++) + { + foreach (var successor in successors[blockIndex]) + { + if (successor <= blockIndex) + { + headers.Add(successor); + } + } + } + + return new IrControlFlowGraph( + ranges, + byStart, + successors.Select(list => (IReadOnlyList)list).ToList(), + predecessors.Select(list => (IReadOnlyList)list).ToList(), + headers); + } + + private static void Link( + List> successors, + List> predecessors, + int from, + int to) + { + if (!successors[from].Contains(to)) + { + successors[from].Add(to); + } + + if (!predecessors[to].Contains(from)) + { + predecessors[to].Add(from); + } + } +} + +public interface IIrBranchResolver +{ + bool IsBranch(Gen5ShaderInstruction instruction); + + bool IsConditional(Gen5ShaderInstruction instruction); + + bool TryGetBranchTarget(Gen5ShaderInstruction instruction, out uint targetPc); +} 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); + } +}