mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 06:59:45 +08:00
Shader cfg ssa (#707)
* [shader] add scalar control flow graph * [shader] add gen5 branch resolver * [shader] add scalar reaching definitions * [shader] add cfg and dataflow tests * [shader] guard descriptors from unresolved registers
This commit is contained in:
@@ -7,6 +7,7 @@ using System.Buffers.Binary;
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace SharpEmu.ShaderCompiler;
|
namespace SharpEmu.ShaderCompiler;
|
||||||
|
|
||||||
@@ -36,6 +37,113 @@ public static class Gen5ShaderScalarEvaluator
|
|||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
private static readonly object _scalarFallbackTraceGate = new();
|
private static readonly object _scalarFallbackTraceGate = new();
|
||||||
private static readonly HashSet<(ulong Shader, uint Pc)> _tracedScalarFallbacks = [];
|
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.
|
// Shaders whose empty SRT/EUD caused a null-base scalar pointer load.
|
||||||
// Host submit of those translations has lost the Vulkan device; Agc skips
|
// Host submit of those translations has lost the Vulkan device; Agc skips
|
||||||
// them before QueueSubmit.
|
// them before QueueSubmit.
|
||||||
@@ -1898,19 +2006,32 @@ public static class Gen5ShaderScalarEvaluator
|
|||||||
var address = unchecked(
|
var address = unchecked(
|
||||||
baseAddress +
|
baseAddress +
|
||||||
byteOffset) & ~3UL;
|
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 =
|
var bufferUnbound =
|
||||||
isBufferLoad &&
|
isBufferLoad &&
|
||||||
(!hasBufferDescriptor ||
|
(descriptorDiverged ||
|
||||||
|
!hasBufferDescriptor ||
|
||||||
bufferDescriptor.SizeBytes == 0 ||
|
bufferDescriptor.SizeBytes == 0 ||
|
||||||
(scalarRegisters[scalarBase.Value] == 0 &&
|
(scalarRegisters[scalarBase.Value] == 0 &&
|
||||||
scalarRegisters[scalarBase.Value + 1] == 0 &&
|
scalarRegisters[scalarBase.Value + 1] == 0 &&
|
||||||
scalarBase.Value + 3 < ScalarRegisterCount &&
|
scalarBase.Value + 3 < ScalarRegisterCount &&
|
||||||
scalarRegisters[scalarBase.Value + 2] == 0 &&
|
scalarRegisters[scalarBase.Value + 2] == 0 &&
|
||||||
scalarRegisters[scalarBase.Value + 3] == 0));
|
scalarRegisters[scalarBase.Value + 3] == 0));
|
||||||
var scalarPointerUnbound = ShouldTreatScalarPointerAsUnbound(
|
var scalarPointerUnbound = descriptorDiverged && !isBufferLoad ||
|
||||||
isBufferLoad,
|
ShouldTreatScalarPointerAsUnbound(
|
||||||
address,
|
isBufferLoad,
|
||||||
_strictScalarLoad);
|
address,
|
||||||
|
_strictScalarLoad);
|
||||||
if (scalarPointerUnbound)
|
if (scalarPointerUnbound)
|
||||||
{
|
{
|
||||||
TraceScalarPointerFallback(
|
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