[ShaderCompiler] Intern scalar graphs without string keys or hit allocations

This commit is contained in:
kuba 2026-10-01 16:24:57 +02:00
parent 9c03cb0b0b
commit 99a19fa2fa
3 changed files with 139 additions and 45 deletions

View File

@ -153,6 +153,10 @@ public sealed class ScalarValue
public uint UserDataRegister => (uint)Payload;
public int MemoryIndex => (int)Payload;
internal static ScalarValue CreateInterned(ScalarValueKind kind, ScalarValueType type,
ScalarOperation operation, ulong payload, ScalarValue[] operands) =>
new(kind, type, operation, operands) { Payload = payload };
public static ScalarValue ConstantOf(uint value) =>
new(ScalarValueKind.Constant, ScalarValueType.U32, ScalarOperation.None, []) { Payload = value };

View File

@ -17,7 +17,9 @@ public sealed record MemoryAccessBinding(ScalarValue? Handle, ScalarValue? Sampl
// from, symbolic in user data and in the shader base.
public sealed partial class ScalarValueGraph
{
private readonly Dictionary<string, ScalarValue> _interned = [];
private readonly Dictionary<int, int> _interned = [];
private readonly List<InternedValue> _internedValues = [];
private readonly record struct InternedValue(ScalarValue Value, ulong Payload, ulong Identity, int Next);
private readonly List<ScalarValue> _values = [];
private ScalarValueGraph(Gen5ShaderProgram program, IrControlFlowGraph controlFlow, MemoryAccessTable memory, uint userDataBase, uint userDataCount,
@ -69,13 +71,13 @@ public sealed partial class ScalarValueGraph
// Node creation. Equal structure yields the same node, and operations over
// constants fold, so revisiting a block reproduces its values exactly.
internal ScalarValue Constant(uint value) => Intern($"c32:{value}", () => ScalarValue.ConstantOf(value));
internal ScalarValue Constant(uint value) => Intern(ScalarValueKind.Constant, ScalarValueType.U32, value);
internal ScalarValue MemoryAperture(uint operand) => Intern($"aperture:{operand}", () => ScalarValue.MemoryAperture(operand));
internal ScalarValue MemoryAperture(uint operand) => Intern(ScalarValueKind.MemoryAperture, ScalarValueType.U32, operand);
internal ScalarValue Constant(ulong value) => Intern($"c64:{value}", () => ScalarValue.ConstantOf(value));
internal ScalarValue Constant(ulong value) => Intern(ScalarValueKind.Constant, ScalarValueType.U64, value);
internal ScalarValue Constant(bool value) => Intern($"c1:{value}", () => ScalarValue.ConstantOf(value));
internal ScalarValue Constant(bool value) => Intern(ScalarValueKind.Constant, ScalarValueType.Bool, value ? 1ul : 0ul);
/// <summary>
/// The instruction the builder is translating, for diagnostics only.
@ -105,18 +107,18 @@ public sealed partial class ScalarValueGraph
{
if (BuilderInstruction.Opcode is null)
{
return Intern($"undef:{type}", () => ScalarValue.Undefined(type));
return Intern(ScalarValueKind.Undefined, type);
}
var instruction = BuilderInstruction;
var value = Intern($"undef:{type}:{instruction.Pc:X}", () => ScalarValue.Undefined(type));
var value = Intern(ScalarValueKind.Undefined, type, identity: (1ul << 32) | instruction.Pc);
_undefinedOrigins.TryAdd(value, instruction);
return value;
}
internal ScalarValue UserData(uint register) => Intern($"ud:{register}", () => ScalarValue.UserData(register));
internal ScalarValue UserData(uint register) => Intern(ScalarValueKind.UserData, ScalarValueType.U32, register);
internal ScalarValue ShaderBase() => Intern("base", ScalarValue.ShaderBase);
internal ScalarValue ShaderBase() => Intern(ScalarValueKind.ShaderBase, ScalarValueType.U64);
// Keep the finite result range even when the input cannot be resolved on the host.
internal ScalarValue FindLowestSetBit(ScalarValue value, uint instructionAddress)
@ -125,22 +127,22 @@ public sealed partial class ScalarValueGraph
return Operation(ScalarOperation.FindLowestBit32, ScalarValueType.U32, value);
// Keep each instruction separate because its input guard can differ from another scan's guard.
var result = Intern($"lowest-bit:{instructionAddress}:{value.Id}", () =>
ScalarValue.MakeOperation(ScalarOperation.FindLowestBit32, ScalarValueType.U32, value));
var result = Intern(ScalarValueKind.Operation, ScalarValueType.U32, operation: ScalarOperation.FindLowestBit32,
operands: [value], identity: (1ul << 32) | instructionAddress);
_bitScanInstructions[result] = instructionAddress;
return result;
}
internal ScalarValue ResourceTableWord(uint slot) => Intern($"table:{slot}", () => ScalarValue.ResourceTableWord(slot));
internal ScalarValue ResourceTableWord(uint slot) => Intern(ScalarValueKind.ResourceTableWord, ScalarValueType.U32, slot);
internal ScalarValue Handle(ScalarValueKind kind, params ScalarValue[] dwords) =>
Intern($"{kind}:{Ids(dwords)}", () => ScalarValue.Handle(kind, dwords));
internal ScalarValue Handle(ScalarValueKind kind, params ReadOnlySpan<ScalarValue> dwords) =>
Intern(kind, ScalarValueType.U32, operands: dwords);
internal ScalarValue MemoryRead(ScalarValueKind kind, ScalarValue handle, ScalarValue offset, int memoryIndex) =>
Intern($"{kind}:{memoryIndex}:{handle.Id}:{offset.Id}", () => ScalarValue.MemoryRead(kind, handle, offset, memoryIndex));
Intern(kind, ScalarValueType.U32, unchecked((ulong)memoryIndex), operands: [handle, offset]);
internal ScalarValue FirstLane(ScalarValue value, ScalarValue activeMask, uint instructionAddress) =>
Intern($"first:{instructionAddress}:{value.Id}:{activeMask.Id}", () => ScalarValue.FirstLane(value, activeMask, instructionAddress));
Intern(ScalarValueKind.FirstLane, ScalarValueType.U32, instructionAddress, operands: [value, activeMask]);
internal ScalarValue Phi(int block, ScalarValueType type) => Track(ScalarValue.Phi(block, type));
@ -156,10 +158,10 @@ public sealed partial class ScalarValueGraph
return whenTrue;
}
return Intern($"sel:{condition.Id}:{whenTrue.Id}:{whenFalse.Id}", () => ScalarValue.Select(condition, whenTrue, whenFalse));
return Intern(ScalarValueKind.Select, whenTrue.Type, operands: [condition, whenTrue, whenFalse]);
}
internal ScalarValue Operation(ScalarOperation operation, ScalarValueType type, params ScalarValue[] operands)
internal ScalarValue Operation(ScalarOperation operation, ScalarValueType type, params ReadOnlySpan<ScalarValue> operands)
{
// Some ALU identities produce a deterministic value even when the other
// operand has no tracked provenance. This matters for descriptor setup code
@ -179,7 +181,7 @@ public sealed partial class ScalarValueGraph
// preserving the node lets later symbolic identities eliminate
// only the bits that are provably independent of that input and
// keeps diagnostics attached to the real instruction graph.
return Intern($"op:{operation}:{type}:{Ids(operands)}", () => ScalarValue.MakeOperation(operation, type, operands));
return Intern(ScalarValueKind.Operation, type, operation: operation, operands: operands);
}
}
@ -203,10 +205,10 @@ public sealed partial class ScalarValueGraph
return folded;
}
return Intern($"op:{operation}:{type}:{Ids(operands)}", () => ScalarValue.MakeOperation(operation, type, operands));
return Intern(ScalarValueKind.Operation, type, operation: operation, operands: operands);
}
private bool TryFoldWithUndefined(ScalarOperation operation, ScalarValueType type, ScalarValue[] operands, out ScalarValue folded)
private bool TryFoldWithUndefined(ScalarOperation operation, ScalarValueType type, ReadOnlySpan<ScalarValue> operands, out ScalarValue folded)
{
folded = null!;
@ -216,35 +218,40 @@ public sealed partial class ScalarValueGraph
}
if (operation == ScalarOperation.And32 &&
operands.Any(operand => operand.IsConstant && operand.Type == ScalarValueType.U32 && operand.ConstantU32 == 0))
((operands[0].IsConstant && operands[0].Type == ScalarValueType.U32 && operands[0].ConstantU32 == 0) ||
(operands[1].IsConstant && operands[1].Type == ScalarValueType.U32 && operands[1].ConstantU32 == 0)))
{
folded = Constant(0u);
return true;
}
if (operation == ScalarOperation.And64 &&
operands.Any(operand => operand.IsConstant && operand.Type == ScalarValueType.U64 && operand.ConstantU64 == 0))
((operands[0].IsConstant && operands[0].Type == ScalarValueType.U64 && operands[0].ConstantU64 == 0) ||
(operands[1].IsConstant && operands[1].Type == ScalarValueType.U64 && operands[1].ConstantU64 == 0)))
{
folded = Constant(0ul);
return true;
}
if (operation == ScalarOperation.Or32 &&
operands.Any(operand => operand.IsConstant && operand.Type == ScalarValueType.U32 && operand.ConstantU32 == uint.MaxValue))
((operands[0].IsConstant && operands[0].Type == ScalarValueType.U32 && operands[0].ConstantU32 == uint.MaxValue) ||
(operands[1].IsConstant && operands[1].Type == ScalarValueType.U32 && operands[1].ConstantU32 == uint.MaxValue)))
{
folded = Constant(uint.MaxValue);
return true;
}
if (operation == ScalarOperation.LogicalAnd &&
operands.Any(operand => operand.IsConstant && operand.Type == ScalarValueType.Bool && !operand.ConstantBool))
((operands[0].IsConstant && operands[0].Type == ScalarValueType.Bool && !operands[0].ConstantBool) ||
(operands[1].IsConstant && operands[1].Type == ScalarValueType.Bool && !operands[1].ConstantBool)))
{
folded = Constant(false);
return true;
}
if (operation == ScalarOperation.LogicalOr &&
operands.Any(operand => operand.IsConstant && operand.Type == ScalarValueType.Bool && operand.ConstantBool))
((operands[0].IsConstant && operands[0].Type == ScalarValueType.Bool && operands[0].ConstantBool) ||
(operands[1].IsConstant && operands[1].Type == ScalarValueType.Bool && operands[1].ConstantBool)))
{
folded = Constant(true);
return true;
@ -253,7 +260,7 @@ public sealed partial class ScalarValueGraph
return false;
}
private bool TryFold(ScalarOperation operation, ScalarValueType type, ScalarValue[] operands, out ScalarValue folded)
private bool TryFold(ScalarOperation operation, ScalarValueType type, ReadOnlySpan<ScalarValue> operands, out ScalarValue folded)
{
folded = null!;
Span<ulong> values = stackalloc ulong[operands.Length];
@ -373,28 +380,36 @@ public sealed partial class ScalarValueGraph
return uses;
}
private static string Ids(ScalarValue[] operands)
// Hashes select a chain only. Full metadata and operand identity comparisons
// preserve correctness on collisions and avoid allocating anything on a hit.
// Keep the original payload in the entry: planning can retarget a memory node.
private ScalarValue Intern(ScalarValueKind kind, ScalarValueType type, ulong payload = 0,
ScalarOperation operation = ScalarOperation.None, ReadOnlySpan<ScalarValue> operands = default, ulong identity = 0)
{
var text = new System.Text.StringBuilder();
foreach (var operand in operands)
var hash = new HashCode();
hash.Add(kind);
hash.Add(type);
hash.Add(payload);
hash.Add(operation);
hash.Add(identity);
foreach (var operand in operands) hash.Add(operand.Id);
var key = hash.ToHashCode();
var head = _interned.TryGetValue(key, out var first) ? first : -1;
for (var index = head; index >= 0; index = _internedValues[index].Next)
{
text.Append(operand.Id).Append(',');
var entry = _internedValues[index];
var value = entry.Value;
if (value.Kind == kind && value.Type == type && value.Operation == operation &&
entry.Payload == payload && entry.Identity == identity &&
operands.SequenceEqual(value.Operands))
return value;
}
return text.ToString();
}
private ScalarValue Intern(string key, Func<ScalarValue> create)
{
if (_interned.TryGetValue(key, out var existing))
{
return existing;
}
var value = create();
_interned[key] = value;
_values.Add(value);
return value;
var created = ScalarValue.CreateInterned(kind, type, operation, payload, operands.ToArray());
_interned[key] = _internedValues.Count;
_internedValues.Add(new(created, payload, identity, head));
_values.Add(created);
return created;
}
private ScalarValue Track(ScalarValue value)

View File

@ -0,0 +1,75 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.ShaderCompiler.Resources;
using Xunit;
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
namespace SharpEmu.ShaderCompiler.Tests.Resources;
public sealed class ScalarValueInterningTests
{
private static ScalarValueGraph Graph() => ScalarValueGraph.Build(Program(EndProgram(0)), 0, 0);
[Fact]
public void IdentityIncludesTypesOriginsAndOperandOrder()
{
var graph = Graph();
Assert.NotSame(graph.Constant(1u), graph.Constant(1ul));
Assert.NotSame(graph.Constant(1u), graph.Constant(true));
var left = graph.UserData(0);
var right = graph.UserData(1);
var pair = graph.Handle(ScalarValueKind.AddressHandle, left, right);
Assert.Same(pair, graph.Handle(ScalarValueKind.AddressHandle, left, right));
Assert.NotSame(pair, graph.Handle(ScalarValueKind.AddressHandle, right, left));
Assert.NotSame(pair, graph.Handle(ScalarValueKind.BufferHandle, left, right));
Assert.NotSame(graph.MemoryRead(ScalarValueKind.ScalarAddressWord, pair, left, 0),
graph.MemoryRead(ScalarValueKind.ScalarAddressWord, pair, left, 1));
graph.BuilderInstruction = default;
var unknown = graph.Undefined(ScalarValueType.U32);
graph.BuilderInstruction = (0, "unknown");
var atZero = graph.Undefined(ScalarValueType.U32);
Assert.NotSame(unknown, atZero);
graph.BuilderInstruction = (4, "unknown");
Assert.NotSame(atZero, graph.Undefined(ScalarValueType.U32));
graph.BuilderInstruction = (0, "unknown");
Assert.Same(atZero, graph.Undefined(ScalarValueType.U32));
Assert.NotSame(graph.FindLowestSetBit(left, 0), graph.FindLowestSetBit(left, 4));
Assert.NotSame(graph.FindLowestSetBit(left, 0),
graph.Operation(ScalarOperation.FindLowestBit32, ScalarValueType.U32, left));
}
[Fact]
public void RetainedOperandsAreIndependentOfCallerScratch()
{
var graph = Graph();
var operands = Enumerable.Range(0, 12).Select(i => graph.UserData((uint)i)).ToArray();
var saved = operands.ToArray();
var handle = graph.Handle(ScalarValueKind.ImageHandle, operands);
operands[11] = graph.Constant(99u);
Assert.Same(handle, graph.Handle(ScalarValueKind.ImageHandle, saved));
Assert.NotSame(handle, graph.Handle(ScalarValueKind.ImageHandle, operands));
Assert.Same(saved[11], handle.Operands[11]);
}
[Fact]
public void RepeatedLookupsAndFoldedOperationsAllocateNothing()
{
var graph = Graph();
void Lookup()
{
var left = graph.UserData(0);
var right = graph.Constant(12u);
var sum = graph.Operation(ScalarOperation.IAdd32, ScalarValueType.U32, left, right);
var address = graph.Handle(ScalarValueKind.AddressHandle, sum, graph.Constant(0u));
_ = graph.MemoryRead(ScalarValueKind.ScalarAddressWord, address, right, 7);
_ = graph.Operation(ScalarOperation.And32, ScalarValueType.U32, sum, graph.Constant(0u));
}
for (var i = 0; i < 1000; i++) Lookup();
var bytes = GC.GetAllocatedBytesForCurrentThread();
for (var i = 0; i < 10000; i++) Lookup();
bytes = GC.GetAllocatedBytesForCurrentThread() - bytes;
Assert.Equal(0, bytes);
}
}