mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 06:59:45 +08:00
Compare commits
11 Commits
main
...
shader-cfg-ssa
| Author | SHA1 | Date | |
|---|---|---|---|
| 7019fe14cd | |||
| 6871510c1a | |||
| 9074ca200b | |||
| 71ace8af88 | |||
| 20dcb791dc | |||
| 8833847111 | |||
| 166152e99c | |||
| 8bcbc7c7d4 | |||
| 9608569613 | |||
| 1be0d06d0d | |||
| ae736136d4 |
@@ -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<Gen5ShaderProgram, Ir.Gen5ScalarSsa> _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));
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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(
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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<IrScalarValue[]> entryState,
|
||||
IReadOnlyList<IrScalarValue[]> exitState,
|
||||
IReadOnlyList<IrReachingDefinition[]> entryDefinitions,
|
||||
IReadOnlyDictionary<uint, int> blockByPc,
|
||||
IReadOnlyList<Gen5ShaderInstruction> instructions)
|
||||
{
|
||||
Graph = graph;
|
||||
_entryState = entryState;
|
||||
_exitState = exitState;
|
||||
_entryDefinitions = entryDefinitions;
|
||||
_blockByPc = blockByPc;
|
||||
_instructions = instructions;
|
||||
}
|
||||
|
||||
private readonly IReadOnlyList<IrReachingDefinition[]> _entryDefinitions;
|
||||
|
||||
public IrControlFlowGraph Graph { get; }
|
||||
|
||||
private readonly IReadOnlyList<IrScalarValue[]> _entryState;
|
||||
private readonly IReadOnlyList<IrScalarValue[]> _exitState;
|
||||
private readonly IReadOnlyDictionary<uint, int> _blockByPc;
|
||||
private readonly IReadOnlyList<Gen5ShaderInstruction> _instructions;
|
||||
|
||||
public static Gen5ScalarSsa Build(
|
||||
IReadOnlyList<Gen5ShaderInstruction> instructions,
|
||||
IReadOnlyList<uint> userData,
|
||||
IIrBranchResolver? resolver = null)
|
||||
{
|
||||
resolver ??= Gen5IrBranchResolver.Instance;
|
||||
var graph = IrControlFlowGraph.Build(instructions, resolver);
|
||||
var blockCount = graph.Blocks.Count;
|
||||
|
||||
var entry = new List<IrScalarValue[]>(blockCount);
|
||||
var exit = new List<IrScalarValue[]>(blockCount);
|
||||
var defEntry = new List<IrReachingDefinition[]>(blockCount);
|
||||
var defExit = new List<IrReachingDefinition[]>(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<uint, int>();
|
||||
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<int>();
|
||||
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<Gen5ShaderInstruction> 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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<Gen5ShaderInstruction> 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<IrBlockRange> blocks,
|
||||
IReadOnlyDictionary<uint, int> blockByStartPc,
|
||||
IReadOnlyList<IReadOnlyList<int>> successors,
|
||||
IReadOnlyList<IReadOnlyList<int>> predecessors,
|
||||
IReadOnlySet<int> loopHeaders)
|
||||
{
|
||||
Blocks = blocks;
|
||||
BlockByStartPc = blockByStartPc;
|
||||
Successors = successors;
|
||||
Predecessors = predecessors;
|
||||
LoopHeaders = loopHeaders;
|
||||
}
|
||||
|
||||
public IReadOnlyList<IrBlockRange> Blocks { get; }
|
||||
|
||||
public IReadOnlyDictionary<uint, int> BlockByStartPc { get; }
|
||||
|
||||
public IReadOnlyList<IReadOnlyList<int>> Successors { get; }
|
||||
|
||||
public IReadOnlyList<IReadOnlyList<int>> Predecessors { get; }
|
||||
|
||||
public IReadOnlySet<int> LoopHeaders { get; }
|
||||
|
||||
public bool HasControlFlow => Blocks.Count > 1;
|
||||
|
||||
public static IrControlFlowGraph Build(
|
||||
IReadOnlyList<Gen5ShaderInstruction> instructions,
|
||||
IIrBranchResolver resolver)
|
||||
{
|
||||
var leaders = new SortedSet<uint>();
|
||||
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<IrBlockRange>(ordered.Count);
|
||||
var byStart = new Dictionary<uint, int>();
|
||||
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<List<int>>(ranges.Count);
|
||||
var predecessors = new List<List<int>>(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<int>();
|
||||
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<int>)list).ToList(),
|
||||
predecessors.Select(list => (IReadOnlyList<int>)list).ToList(),
|
||||
headers);
|
||||
}
|
||||
|
||||
private static void Link(
|
||||
List<List<int>> successors,
|
||||
List<List<int>> 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);
|
||||
}
|
||||
@@ -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