diff --git a/src/SharpEmu.ShaderCompiler/Resources/ScalarValue.cs b/src/SharpEmu.ShaderCompiler/Resources/ScalarValue.cs index f48d7f3..72211ce 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ScalarValue.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ScalarValue.cs @@ -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 }; diff --git a/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.cs b/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.cs index b640e29..d0aa865 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ScalarValueGraph.cs @@ -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 _interned = []; + private readonly Dictionary _interned = []; + private readonly List _internedValues = []; + private readonly record struct InternedValue(ScalarValue Value, ulong Payload, ulong Identity, int Next); private readonly List _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); /// /// 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 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 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 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 operands, out ScalarValue folded) { folded = null!; Span 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 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 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) diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ScalarValueInterningTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ScalarValueInterningTests.cs new file mode 100644 index 0000000..9ddc81f --- /dev/null +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ScalarValueInterningTests.cs @@ -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); + } +}