Compare commits

...

1 Commits

Author SHA1 Message Date
ParantezTech fd038d22cd [SRT] Compile shader resource plans in the background
Each shader resource plan is compiled once to a DynamicMethod that evaluates
its resource table graph with plan-local value slots instead of the
interpreter's hashed cache. ASTRO's PLAYROOM goes from about 20 to 60 fps.

Compilation runs on a low-priority "SRT compiler" thread; until a plan is
ready, the interpreter evaluates it, so a new shader no longer stalls the
render thread while its plan is JIT-compiled. A plan whose compilation fails
stays on the interpreter. SHARPEMU_SRT_NATIVE=0 turns the compiled path off;
it is also off when the runtime does not support dynamic code.
2026-10-04 23:44:16 +03:00
7 changed files with 694 additions and 32 deletions

View File

@ -14,7 +14,7 @@ The GUI shows 14 variables on its **Options** and **Game options** pages. This l
`SHARPEMU_LOG_NP`, `SHARPEMU_PROFILE_PERFORMANCE`, `SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE`,
`SHARPEMU_RENDERDOC`, `SHARPEMU_VK_DISABLE_IMPLICITS`, `SHARPEMU_STRICT_COMPUTE`, `SHARPEMU_VK_VALIDATION`, `SHARPEMU_WRITABLE_APP0`.
The list contains 268 variables. The source was examined on 2026-09-19, branch `dev`.
The list contains 269 variables. The source was examined on 2026-09-19, branch `dev`.
The descriptions come from the code that reads each variable. The emulator was not started for this list.
## How to use a variable
@ -30,7 +30,7 @@ The descriptions come from the code that reads each variable. The emulator was n
| Group | Variables |
| --- | --- |
| Behavior switches | 19 |
| GPU and Vulkan | 58 |
| GPU and Vulkan | 59 |
| Metal | 10 |
| Audio and video | 6 |
| Input | 5 |
@ -127,6 +127,7 @@ These variables change the GPU path or the Vulkan presenter.
| `SHARPEMU_SHADER_PREWARM` | `0` | `0` turns off the shader prewarm list. By default, each compute program that a game compiles is added at once to `shader-prewarm.bin` in `user/pipeline_cache/<title id>/`, so a stopped game keeps the list. At the next launch after a change of the shader translator or of the GPU driver, background threads translate the listed programs again and create their pipelines, so that the driver compiles them before the game asks for them. The `shader-prewarm.stamp` file names the build and driver that last did this; with the same build and driver, the launch does no prewarm work. The log shows `Shader prewarm done` and `Shader prewarm hits`. On Windows it replaces the cache file by default; on Linux it works beside the cache file. It has no effect with the per-shader cache files of `SHARPEMU_VK_PIPELINE_CACHE_SHARDS`. | `VulkanVideoPresenter.ShaderPrewarm.cs`, `ShaderPrewarmList.cs`, `ShaderProgramCache.cs` |
| `SHARPEMU_SKIP_ALL_COMPUTE` | `1` | Set to `1` to make the Metal presenter skip all compute dispatches. The Vulkan presenter reads the value but does not use it. The default is off. | `MetalVideoPresenter.Compute.cs`, `VulkanVideoPresenter.cs` |
| `SHARPEMU_SKIP_TALL_COMPUTE_Z` | number | Effect not clear from the code. The Vulkan presenter reads the number into a field for a minimum Z group count. No code uses that field. The default is 0. | `VulkanVideoPresenter.cs` |
| `SHARPEMU_SRT_NATIVE` | `0` | `0` turns off the compiled shader resource (SRT) evaluator. By default, the resource plan of each shader is compiled once to .NET code (`DynamicMethod`) on a low-priority background thread named `SRT compiler`; until it is ready, the plan is evaluated by the interpreter, so the first use of a new shader does not wait for the compilation. With `0`, every plan always uses the interpreter. The compiled path is also off when the runtime does not support dynamic code, for example in a Native AOT build. | `CompiledResourceEvaluator.cs`, `ShaderResourcePlan.cs`, `RuntimeValueEvaluator.cs` |
| `SHARPEMU_SUSPEND_POINTS_IN_FLIGHT` | `0` | `0` makes `sceAgcSuspendPoint` wait until the frame boundary it queued is processed. The default lets one boundary wait behind the graphics commands, so the guest CPU can run one frame ahead. A wait that follows its own queue's store to the same label still sees that value when the title rewrites the label for the next frame (Dead Cells). | `CommandStreamQueue.cs`, `GpuCommandInterpreter.cs` |
| `SHARPEMU_VK_DEBUG_LABELS` | `1` | Turns on Vulkan object names and command labels for capture tools. `SHARPEMU_VK_VALIDATION=1` also turns them on. Default is off because the labels add overhead to each draw. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_VK_DEVICE` | text (part of a device name) | Selects the Vulkan device whose name contains this text. The comparison ignores case. When unset, the presenter gives a score to each device and prefers a discrete GPU. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanVideoPresenter.cs` |

View File

@ -0,0 +1,419 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
namespace SharpEmu.ShaderCompiler.Resources;
// Compile the immutable SRT graph once, not the draw's pointers or memory contents.
// Small methods bound JIT cost; dependency calls use constant indices and a dense cache.
internal sealed class CompiledResourceEvaluator
{
private const int NodesPerMethod = 64;
private delegate bool EvaluateFunction(RuntimeValueEvaluator evaluator, int index, out ulong result);
private readonly Dictionary<ScalarValue, int> _indices;
private readonly EvaluateFunction[] _functions;
internal ScalarValue[] Values { get; }
internal int[][] SourceWords { get; }
internal int[] TableWords { get; }
internal int Count => Values.Length;
private static readonly Dictionary<string, MethodInfo> Helpers = new[]
{
nameof(RuntimeValueEvaluator.BeginCompiled), nameof(RuntimeValueEvaluator.EndCompiled),
nameof(RuntimeValueEvaluator.StoreCompiled), nameof(RuntimeValueEvaluator.EvaluateCompiled),
nameof(RuntimeValueEvaluator.EvaluateCompiledSpecial), nameof(RuntimeValueEvaluator.IsCompiledActiveMask),
nameof(RuntimeValueEvaluator.ReadUserData), nameof(RuntimeValueEvaluator.ReadShaderBase), nameof(RuntimeValueEvaluator.ReadRawWord),
}.ToDictionary(name => name, name => typeof(RuntimeValueEvaluator).GetMethod(name, BindingFlags.NonPublic | BindingFlags.Instance)!);
private static readonly MethodInfo EvaluateOperation = typeof(ScalarOperationSemantics)
.GetMethod(nameof(ScalarOperationSemantics.TryEvaluateFixed), BindingFlags.NonPublic | BindingFlags.Static)!;
private CompiledResourceEvaluator(ShaderResourcePlan plan, List<ScalarValue> values, Dictionary<ScalarValue, int> indices,
Dictionary<ScalarValue, ScalarValue?> phis)
{
Values = [.. values];
_indices = indices;
SourceWords = plan.DescriptorSources.Select(source => source.Dwords.Select(word => indices[word]).ToArray()).ToArray();
TableWords = plan.TableReads.Select(read => indices[read.Value]).ToArray();
_functions = new EvaluateFunction[(Count + NodesPerMethod - 1) / NodesPerMethod];
for (var chunk = 0; chunk < _functions.Length; chunk++)
_functions[chunk] = Compile(plan, chunk * NodesPerMethod, phis);
}
private static readonly System.Collections.Concurrent.ConcurrentQueue<ShaderResourcePlan> Pending = new();
private static readonly SemaphoreSlim PendingSignal = new(0);
private static int _compilerStarted;
internal static void Enqueue(ShaderResourcePlan plan)
{
Pending.Enqueue(plan);
if (Interlocked.Exchange(ref _compilerStarted, 1) == 0)
{
new Thread(CompilePending) { IsBackground = true, Priority = ThreadPriority.BelowNormal, Name = "SRT compiler" }.Start();
}
PendingSignal.Release();
}
private static void CompilePending()
{
while (true)
{
PendingSignal.Wait();
while (Pending.TryDequeue(out var plan)) plan.CompileEvaluatorNow();
}
}
internal bool TryGetIndex(ScalarValue value, out int index) => _indices.TryGetValue(value, out index);
internal bool Evaluate(RuntimeValueEvaluator evaluator, int index, out ulong result) =>
_functions[index / NodesPerMethod](evaluator, index, out result);
internal static CompiledResourceEvaluator? Build(ShaderResourcePlan plan)
{
if (!RuntimeFeature.IsDynamicCodeSupported || Environment.GetEnvironmentVariable("SHARPEMU_SRT_NATIVE") == "0") return null;
var values = new List<ScalarValue>();
var indices = new Dictionary<ScalarValue, int>();
var phis = new Dictionary<ScalarValue, ScalarValue?>();
void Add(ScalarValue value)
{
if (indices.TryAdd(value, values.Count)) values.Add(value);
}
foreach (var source in plan.DescriptorSources)
foreach (var word in source.Dwords) Add(word);
foreach (var read in plan.TableReads) Add(read.Value);
foreach (var read in plan.DynamicReads) Add(read);
foreach (var branch in plan.ResourceBranches)
if (branch.Condition is { } condition) Add(condition);
foreach (var range in plan.DeviceAddressRanges)
{
Add(range.BaseLow);
Add(range.BaseHigh);
}
foreach (var image in plan.IndirectImages) Add(image.Key);
foreach (var candidates in plan.BufferCandidateTables)
if (candidates.Limit is { } limit) Add(limit);
// An iterative walk also admits cycles. At run time, an in-progress dense slot
// fails just like the interpreter's visiting set, without recursing forever.
for (var index = 0; index < values.Count; index++)
{
var value = values[index];
if (value.Kind == ScalarValueKind.Phi)
{
var invariant = plan.Graph.ResolveInvariantPhi(value);
phis[value] = invariant;
if (invariant is not null) Add(invariant);
}
else if (value.Kind == ScalarValueKind.FirstLane)
{
Add(value.Operands[0]);
}
else if (value.Kind == ScalarValueKind.ResourceTableWord)
{
if (value.Payload < (ulong)plan.TableReads.Count) Add(plan.TableReads[(int)value.Payload].Value);
}
else if (value.Kind is ScalarValueKind.ScalarAddressWord or ScalarValueKind.ScalarBufferWord)
{
if ((uint)value.MemoryIndex >= (uint)plan.Memory.Count || value.Operands.Length < 2) continue;
foreach (var word in value.Operands[0].Operands) Add(word);
Add(value.Operands[1]);
}
else if (value.Kind == ScalarValueKind.Select ||
(value.Kind == ScalarValueKind.Operation && RuntimeValueValidator.IsUniformOperation(value.Operation)))
{
foreach (var operand in value.Operands) Add(operand);
}
}
return values.Count == 0 ? null : new(plan, values, indices, phis);
}
private EvaluateFunction Compile(ShaderResourcePlan plan, int first, Dictionary<ScalarValue, ScalarValue?> phis)
{
var count = Math.Min(NodesPerMethod, Count - first);
var method = new DynamicMethod($"Srt_{plan.Hash:X16}_{first}", typeof(bool),
[typeof(RuntimeValueEvaluator), typeof(int), typeof(ulong).MakeByRefType()], typeof(CompiledResourceEvaluator).Module, true);
var il = method.GetILGenerator();
var result = il.DeclareLocal(typeof(ulong));
var success = il.DeclareLocal(typeof(bool));
var status = il.DeclareLocal(typeof(int));
var operands = Enumerable.Range(0, 5).Select(_ => il.DeclareLocal(typeof(ulong))).ToArray();
var failed = il.DefineLabel();
var store = il.DefineLabel();
var exit = il.DefineLabel();
var cases = Enumerable.Range(0, count).Select(_ => il.DefineLabel()).ToArray();
void Call(string helper) => il.Emit(OpCodes.Call, Helpers[helper]);
void Constant(ulong value) => il.Emit(OpCodes.Ldc_I8, unchecked((long)value));
void Operand(ScalarValue value, LocalBuilder destination)
{
if (value.IsConstant)
{
Constant(value.Payload);
il.Emit(OpCodes.Stloc, destination);
return;
}
var index = _indices[value];
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, index);
il.Emit(OpCodes.Ldloca, destination);
if (index >= first && index < first + count) il.Emit(OpCodes.Call, method);
else Call(nameof(RuntimeValueEvaluator.EvaluateCompiled));
il.Emit(OpCodes.Brfalse, failed);
}
void Load(int index, bool narrow = false)
{
il.Emit(OpCodes.Ldloc, operands[index]);
if (narrow) il.Emit(OpCodes.Conv_U4);
}
void Special(int index)
{
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, index);
il.Emit(OpCodes.Ldloca, result);
Call(nameof(RuntimeValueEvaluator.EvaluateCompiledSpecial));
il.Emit(OpCodes.Brfalse, failed);
}
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
Call(nameof(RuntimeValueEvaluator.BeginCompiled));
il.Emit(OpCodes.Stloc, status);
var compute = il.DefineLabel();
il.Emit(OpCodes.Ldloc, status);
il.Emit(OpCodes.Brfalse, compute);
il.Emit(OpCodes.Ldloc, status);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Cgt);
il.Emit(OpCodes.Ret);
il.MarkLabel(compute);
il.BeginExceptionBlock();
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldc_I4, first);
il.Emit(OpCodes.Sub);
il.Emit(OpCodes.Switch, cases);
il.Emit(OpCodes.Br, failed);
for (var offset = 0; offset < count; offset++)
{
var index = first + offset;
var value = Values[index];
il.MarkLabel(cases[offset]);
switch (value.Kind)
{
case ScalarValueKind.Constant:
Constant(value.Payload);
il.Emit(OpCodes.Stloc, result);
break;
case ScalarValueKind.MemoryAperture:
Constant(Gen5InlineConstants.DecodeAperture64((uint)value.Payload) >> 32);
il.Emit(OpCodes.Stloc, result);
break;
case ScalarValueKind.UserData:
if (value.UserDataRegister < plan.UserDataBase)
{
il.Emit(OpCodes.Br, failed);
break;
}
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, unchecked((int)(value.UserDataRegister - plan.UserDataBase)));
il.Emit(OpCodes.Ldloca, result);
Call(nameof(RuntimeValueEvaluator.ReadUserData));
il.Emit(OpCodes.Brfalse, failed);
break;
case ScalarValueKind.ShaderBase:
il.Emit(OpCodes.Ldarg_0);
Call(nameof(RuntimeValueEvaluator.ReadShaderBase));
il.Emit(OpCodes.Stloc, result);
break;
case ScalarValueKind.Phi:
if (phis[value] is { } invariant) Operand(invariant, result);
else il.Emit(OpCodes.Br, failed);
break;
case ScalarValueKind.FirstLane:
case ScalarValueKind.ResourceTableWord:
// These cross evaluator contexts: active-lane cache or clean reader.
Special(index);
break;
case ScalarValueKind.Select:
{
var ordinary = il.DefineLabel();
var chosen = il.DefineLabel();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, _indices[value.Operands[0]]);
Call(nameof(RuntimeValueEvaluator.IsCompiledActiveMask));
il.Emit(OpCodes.Brfalse, ordinary);
Operand(value.Operands[1], result);
il.Emit(OpCodes.Br, store);
il.MarkLabel(ordinary);
for (var operand = 0; operand < 3; operand++) Operand(value.Operands[operand], operands[operand]);
// Ordinary selects are eager, including reads on the inactive arm.
Load(0);
il.Emit(OpCodes.Brfalse, chosen);
Load(1);
il.Emit(OpCodes.Stloc, result);
il.Emit(OpCodes.Br, store);
il.MarkLabel(chosen);
Load(2);
il.Emit(OpCodes.Stloc, result);
break;
}
case ScalarValueKind.ScalarAddressWord:
case ScalarValueKind.ScalarBufferWord:
{
var buffer = value.Kind == ScalarValueKind.ScalarBufferWord;
if ((uint)value.MemoryIndex >= (uint)plan.Memory.Count || value.Operands.Length < 2 ||
value.Operands[0].Operands.Length < 2 || (buffer && value.Operands[0].Operands.Length != 4))
{
il.Emit(OpCodes.Br, failed);
break;
}
var handle = value.Operands[0];
Operand(handle.Operands[0], operands[0]);
Operand(handle.Operands[1], operands[1]);
Operand(value.Operands[1], operands[2]);
if (buffer)
{
Operand(handle.Operands[2], operands[3]);
Operand(handle.Operands[3], operands[4]);
}
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldc_I4, (int)value.Kind);
Load(0);
Load(1);
Load(2, narrow: true);
if (buffer) Load(3);
else Constant(0);
Constant(unchecked((ulong)(long)(int)plan.Memory[value.MemoryIndex].Offset));
il.Emit(OpCodes.Ldloca, result);
Call(nameof(RuntimeValueEvaluator.ReadRawWord));
il.Emit(OpCodes.Brfalse, failed);
break;
}
case ScalarValueKind.Operation:
if (!RuntimeValueValidator.IsUniformOperation(value.Operation))
{
il.Emit(OpCodes.Br, failed);
break;
}
if (value.Operands.Length > 4)
{
Special(index);
break;
}
for (var operand = 0; operand < 4; operand++)
{
if (operand < value.Operands.Length) Operand(value.Operands[operand], operands[operand]);
else
{
Constant(0);
il.Emit(OpCodes.Stloc, operands[operand]);
}
}
if (EmitArithmetic(il, value.Operation, operands)) il.Emit(OpCodes.Stloc, result);
else
{
il.Emit(OpCodes.Ldc_I4, (int)value.Operation);
for (var operand = 0; operand < 4; operand++) Load(operand);
il.Emit(OpCodes.Ldloca, result);
il.Emit(OpCodes.Call, EvaluateOperation);
il.Emit(OpCodes.Brfalse, failed);
}
break;
default:
il.Emit(OpCodes.Br, failed);
break;
}
il.Emit(OpCodes.Br, store);
}
il.MarkLabel(store);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldloc, result);
Call(nameof(RuntimeValueEvaluator.StoreCompiled));
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Ldloc, result);
il.Emit(OpCodes.Stind_I8);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Stloc, success);
il.Emit(OpCodes.Leave, exit);
il.MarkLabel(failed);
il.Emit(OpCodes.Leave, exit);
il.BeginFinallyBlock();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
Call(nameof(RuntimeValueEvaluator.EndCompiled));
il.EndExceptionBlock();
il.MarkLabel(exit);
il.Emit(OpCodes.Ldloc, success);
il.Emit(OpCodes.Ret);
var function = method.CreateDelegate<EvaluateFunction>();
RuntimeHelpers.PrepareDelegate(function);
return function;
}
private static bool EmitArithmetic(ILGenerator il, ScalarOperation operation, LocalBuilder[] operands)
{
if (operation is ScalarOperation.Construct64 or ScalarOperation.AddCarry32 or ScalarOperation.UMulHi32)
{
for (var operand = 0; operand < 2; operand++)
{
il.Emit(OpCodes.Ldloc, operands[operand]);
il.Emit(OpCodes.Conv_U4);
il.Emit(OpCodes.Conv_U8);
if (operand == 1 && operation == ScalarOperation.Construct64)
{
il.Emit(OpCodes.Ldc_I4, 32);
il.Emit(OpCodes.Shl);
}
}
il.Emit(operation == ScalarOperation.Construct64 ? OpCodes.Or :
operation == ScalarOperation.AddCarry32 ? OpCodes.Add : OpCodes.Mul);
if (operation == ScalarOperation.UMulHi32)
{
il.Emit(OpCodes.Ldc_I4, 32);
il.Emit(OpCodes.Shr_Un);
}
return true;
}
var narrow = operation is ScalarOperation.IAdd32 or ScalarOperation.ISub32 or ScalarOperation.IMul32 or
ScalarOperation.And32 or ScalarOperation.Or32 or ScalarOperation.Xor32 or ScalarOperation.Not32 or
ScalarOperation.ShiftLeft32 or ScalarOperation.ShiftRightLogical32 or ScalarOperation.ShiftRightArithmetic32;
OpCode opcode;
switch (operation)
{
case ScalarOperation.IAdd32: case ScalarOperation.IAdd64: opcode = OpCodes.Add; break;
case ScalarOperation.ISub32: case ScalarOperation.ISub64: opcode = OpCodes.Sub; break;
case ScalarOperation.IMul32: case ScalarOperation.IMul64: opcode = OpCodes.Mul; break;
case ScalarOperation.And32: case ScalarOperation.And64: opcode = OpCodes.And; break;
case ScalarOperation.Or32: opcode = OpCodes.Or; break;
case ScalarOperation.Xor32: opcode = OpCodes.Xor; break;
case ScalarOperation.Not32: opcode = OpCodes.Not; break;
case ScalarOperation.ShiftLeft32: case ScalarOperation.ShiftLeft64: opcode = OpCodes.Shl; break;
case ScalarOperation.ShiftRightLogical32: case ScalarOperation.ShiftRightLogical64: opcode = OpCodes.Shr_Un; break;
case ScalarOperation.ShiftRightArithmetic32: case ScalarOperation.ShiftRightArithmetic64: opcode = OpCodes.Shr; break;
default: return false;
}
il.Emit(OpCodes.Ldloc, operands[0]);
if (narrow) il.Emit(OpCodes.Conv_U4);
if (operation != ScalarOperation.Not32)
{
il.Emit(OpCodes.Ldloc, operands[1]);
if (opcode == OpCodes.Shl || opcode == OpCodes.Shr || opcode == OpCodes.Shr_Un)
{
il.Emit(OpCodes.Conv_I4);
il.Emit(OpCodes.Ldc_I4, narrow ? 31 : 63);
il.Emit(OpCodes.And);
}
else if (narrow) il.Emit(OpCodes.Conv_U4);
}
il.Emit(opcode);
if (narrow) il.Emit(OpCodes.Conv_U4);
il.Emit(OpCodes.Conv_U8);
return true;
}
}

View File

@ -13,7 +13,18 @@ internal sealed class RuntimeEvaluationScratch : IDisposable
private bool _rented;
internal ScalarValueCache Values { get; } = new();
internal CompiledValueCache CompiledValues { get; } = new();
internal List<ScalarValue> Visiting { get; } = [];
internal Stack<int> PendingBranches { get; } = new();
private bool[] _visitedBranches = [];
internal bool[] PrepareBranches(int count)
{
if (_visitedBranches.Length < count) _visitedBranches = new bool[count];
else Array.Clear(_visitedBranches, 0, count);
PendingBranches.Clear();
return _visitedBranches;
}
internal static RuntimeEvaluationScratch Rent()
{
@ -32,9 +43,60 @@ internal sealed class RuntimeEvaluationScratch : IDisposable
{
if (!_rented) return;
Values.Reset();
CompiledValues.Reset();
Visiting.Clear();
PendingBranches.Clear();
_rented = false;
_nextAvailable = _available;
_available = this;
}
}
// Plan-local indices avoid hashing graph nodes on every dependency evaluation.
internal sealed class CompiledValueCache
{
private ulong[] _values = [];
private uint[] _stamps = [];
private uint _generation = 2;
internal void EnsureCapacity(int count)
{
if (_values.Length >= count) return;
Array.Resize(ref _values, count);
Array.Resize(ref _stamps, count);
}
internal int Begin(int index, out ulong result)
{
result = 0;
if (_stamps[index] == _generation)
{
result = _values[index];
return 1;
}
if (_stamps[index] == _generation + 1) return -1;
_stamps[index] = _generation + 1;
return 0;
}
internal bool Contains(int index) => _stamps[index] == _generation;
internal void Store(int index, ulong value)
{
_values[index] = value;
_stamps[index] = _generation;
}
internal void End(int index)
{
if (_stamps[index] == _generation + 1) _stamps[index] = 0;
}
internal void Reset()
{
_generation = unchecked(_generation + 2);
if (_generation != 0) return;
Array.Clear(_stamps);
_generation = 2;
}
}

View File

@ -18,6 +18,8 @@ public sealed class RuntimeValueEvaluator
private readonly ScalarValue? _activeMask;
private readonly ScalarValueCache _cache;
private readonly List<ScalarValue> _visiting;
private readonly CompiledResourceEvaluator? _compiled;
private readonly CompiledValueCache _compiledValues;
public RuntimeValueEvaluator(
ShaderResourcePlan plan,
@ -25,7 +27,7 @@ public sealed class RuntimeValueEvaluator
IReadOnlyList<byte>? cleanFlatSlots = null,
RuntimeValueEvaluator? cleanEvaluator = null,
ScalarValue? activeMask = null)
: this(plan, inputs, cleanFlatSlots, cleanEvaluator, activeMask, new ScalarValueCache(), [])
: this(plan, inputs, cleanFlatSlots, cleanEvaluator, activeMask, new ScalarValueCache(), [], new CompiledValueCache(), true)
{
}
@ -35,8 +37,9 @@ public sealed class RuntimeValueEvaluator
ResourceRuntimeInputs inputs,
IReadOnlyList<byte>? cleanFlatSlots = null,
RuntimeValueEvaluator? cleanEvaluator = null,
ScalarValue? activeMask = null)
: this(plan, inputs, cleanFlatSlots, cleanEvaluator, activeMask, scratch.Values, scratch.Visiting)
ScalarValue? activeMask = null,
bool useCompiled = true)
: this(plan, inputs, cleanFlatSlots, cleanEvaluator, activeMask, scratch.Values, scratch.Visiting, scratch.CompiledValues, useCompiled)
{
}
@ -47,7 +50,9 @@ public sealed class RuntimeValueEvaluator
RuntimeValueEvaluator? cleanEvaluator,
ScalarValue? activeMask,
ScalarValueCache cache,
List<ScalarValue> visiting)
List<ScalarValue> visiting,
CompiledValueCache compiledValues,
bool useCompiled)
{
_cache = cache;
_visiting = visiting;
@ -56,6 +61,9 @@ public sealed class RuntimeValueEvaluator
_cleanFlatSlots = cleanFlatSlots ?? [];
_cleanEvaluator = cleanEvaluator;
_activeMask = activeMask;
_compiled = useCompiled ? plan.CompiledEvaluator : null;
_compiledValues = compiledValues;
if (_compiled is not null) _compiledValues.EnsureCapacity(_compiled.Count);
}
public bool Evaluate(ScalarValue value, out uint result)
@ -84,6 +92,11 @@ public sealed class RuntimeValueEvaluator
return EvaluateWide(value.Operands[1], out result);
}
if (_compiled is not null && _compiled.TryGetIndex(value, out var compiledIndex))
{
return EvaluateCompiled(compiledIndex, out result);
}
if (_cache.TryGetValue(value, out result))
{
return true;
@ -141,24 +154,11 @@ public sealed class RuntimeValueEvaluator
case ScalarValueKind.FirstLane:
{
using var scratch = RuntimeEvaluationScratch.Rent();
return new RuntimeValueEvaluator(scratch, _plan, _inputs, _cleanFlatSlots, _cleanEvaluator, value.Operands[1])
return new RuntimeValueEvaluator(scratch, _plan, _inputs, _cleanFlatSlots, _cleanEvaluator, value.Operands[1], useCompiled: _compiled is not null)
.EvaluateWide(value.Operands[0], out result);
}
case ScalarValueKind.ResourceTableWord:
{
var slot = (int)value.Payload;
if (slot >= _plan.TableReads.Count)
{
return false;
}
if (slot < _cleanFlatSlots.Count && _cleanFlatSlots[slot] != 0 && _cleanEvaluator is not null)
{
return _cleanEvaluator.EvaluateWide(_plan.TableReads[slot].Value, out result);
}
return EvaluateWide(_plan.TableReads[slot].Value, out result);
}
return EvaluateTableReference((int)value.Payload, out result);
case ScalarValueKind.ScalarAddressWord:
case ScalarValueKind.ScalarBufferWord:
return EvaluateRawRead(value, out result);
@ -215,7 +215,6 @@ public sealed class RuntimeValueEvaluator
return false;
}
var baseAddress = ((high << 32) | (uint)low) & AddressMask;
ulong records = 0;
if (value.Kind == ScalarValueKind.ScalarBufferWord &&
(handle.Operands.Length != 4 ||
@ -225,7 +224,14 @@ public sealed class RuntimeValueEvaluator
return false;
}
switch (ResolveRawAddress(value.Kind, baseAddress, high, records, (long)(int)memory.Offset, (uint)offset, out var address))
return ReadRawWord(value.Kind, low, high, (uint)offset, records, (int)memory.Offset, out result);
}
internal bool ReadRawWord(ScalarValueKind kind, ulong low, ulong high, uint offset, ulong records, long immediate, out ulong result)
{
result = 0;
var baseAddress = ((high << 32) | (uint)low) & AddressMask;
switch (ResolveRawAddress(kind, baseAddress, high, records, immediate, offset, out var address))
{
case RawAddress.Failed:
return false;
@ -312,9 +318,59 @@ public sealed class RuntimeValueEvaluator
return true;
}
internal bool IsEvaluated(ScalarValue value) => _cache.TryGetValue(value, out _);
internal bool IsEvaluated(ScalarValue value) =>
_compiled is not null && _compiled.TryGetIndex(value, out var index)
? _compiledValues.Contains(index) : _cache.TryGetValue(value, out _);
internal void Seed(ScalarValue value, ulong result) => _cache[value] = result;
internal void Seed(ScalarValue value, ulong result)
{
if (_compiled is not null && _compiled.TryGetIndex(value, out var index)) _compiledValues.Store(index, result);
else _cache[value] = result;
}
internal int BeginCompiled(int index, out ulong result) => _compiledValues.Begin(index, out result);
internal void StoreCompiled(int index, ulong result) => _compiledValues.Store(index, result);
internal void EndCompiled(int index) => _compiledValues.End(index);
internal bool EvaluateCompiled(int index, out ulong result) => _compiled!.Evaluate(this, index, out result);
internal bool EvaluateCompiledSpecial(int index, out ulong result) => EvaluateNode(_compiled!.Values[index], out result);
internal bool IsCompiledActiveMask(int index) => ReferenceEquals(_activeMask, _compiled!.Values[index]);
internal bool ReadUserData(int index, out ulong result)
{
result = 0;
if ((uint)index >= (uint)_inputs.UserData.Count) return false;
result = _inputs.UserData[index];
return true;
}
internal ulong ReadShaderBase() => _inputs.ShaderBase;
private bool EvaluateTableReference(int slot, out ulong result)
{
result = 0;
if ((uint)slot >= (uint)_plan.TableReads.Count) return false;
var selected = slot < _cleanFlatSlots.Count && _cleanFlatSlots[slot] != 0 && _cleanEvaluator is not null ? _cleanEvaluator : this;
return selected._compiled is { } compiled
? selected.EvaluateCompiled(compiled.TableWords[slot], out result)
: selected.EvaluateWide(_plan.TableReads[slot].Value, out result);
}
private bool EvaluateSourceWord(int source, int word, out uint result)
{
var value = _plan.DescriptorSources[source].Dwords[word];
if (_compiled is null || value.IsConstant) return Evaluate(value, out result);
var succeeded = EvaluateCompiled(_compiled.SourceWords[source][word], out var wide);
result = (uint)wide;
return succeeded;
}
private bool EvaluateTableRead(int index, out uint result)
{
if (_compiled is null) return Evaluate(_plan.TableReads[index].Value, out result);
var succeeded = EvaluateCompiled(_compiled.TableWords[index], out var wide);
result = (uint)wide;
return succeeded;
}
internal ResourceRuntimeInputs Inputs => _inputs;
@ -390,7 +446,7 @@ public sealed class RuntimeValueEvaluator
var cleanEvaluator = new RuntimeValueEvaluator(cleanScratch, plan, inputs.WithReader(inputs.ReadCleanMemory));
var evaluator = new RuntimeValueEvaluator(scratch, plan, inputs, cleanFlatSlots, cleanEvaluator);
if (evaluateTable && plan.ResourceBranches.Count != 0)
activeSources = EvaluateActiveSources(plan, inputs, cleanEvaluator);
activeSources = EvaluateActiveSources(plan, inputs, cleanEvaluator, cleanScratch);
var evaluated = new List<DescriptorWords>(sources.Count);
RawReadPrefetch.PrefetchSources(plan, sources, activeSources, evaluator);
foreach (var sourceIndex in sources)
@ -406,7 +462,7 @@ public sealed class RuntimeValueEvaluator
{
for (var index = 0; index < words.Length; index++)
{
if (!evaluator.Evaluate(source.Dwords[index], out words[index]))
if (!evaluator.EvaluateSourceWord((int)sourceIndex, index, out words[index]))
{
return false;
}
@ -424,11 +480,12 @@ public sealed class RuntimeValueEvaluator
try
{
RawReadPrefetch.PrefetchTable(plan, evaluator, cleanEvaluator, cleanFlatSlots);
foreach (var read in plan.TableReads)
for (var index = 0; index < plan.TableReads.Count; index++)
{
var read = plan.TableReads[index];
var clean = read.FlatOffset < cleanFlatSlots.Count && cleanFlatSlots[(int)read.FlatOffset] != 0;
var selected = clean ? cleanEvaluator : evaluator;
if (read.FlatOffset >= plan.TableReads.Count || !selected.Evaluate(read.Value, out var word))
if (read.FlatOffset >= plan.TableReads.Count || !selected.EvaluateTableRead(index, out var word))
{
return false;
}
@ -447,15 +504,16 @@ public sealed class RuntimeValueEvaluator
return true;
}
private static bool[] EvaluateActiveSources(ShaderResourcePlan plan, ResourceRuntimeInputs inputs, RuntimeValueEvaluator cleanEvaluator)
private static bool[] EvaluateActiveSources(ShaderResourcePlan plan, ResourceRuntimeInputs inputs, RuntimeValueEvaluator cleanEvaluator,
RuntimeEvaluationScratch scratch)
{
var activeSources = new bool[plan.DescriptorSources.Count];
Array.Fill(activeSources, true);
foreach (var block in plan.ResourceBranches)
foreach (var source in block.Sources) activeSources[source] = false;
var visited = new bool[plan.ResourceBranches.Count];
var pending = new Stack<int>();
var visited = scratch.PrepareBranches(plan.ResourceBranches.Count);
var pending = scratch.PendingBranches;
pending.Push(0);
while (pending.TryPop(out var blockIndex))
{

View File

@ -9,6 +9,9 @@ namespace SharpEmu.ShaderCompiler.Resources;
// values use the low dword, F32 values are the bits of the low dword.
public static class ScalarOperationSemantics
{
internal static bool TryEvaluateFixed(ScalarOperation operation, ulong first, ulong second, ulong third, ulong fourth, out ulong result) =>
TryEvaluate(operation, [first, second, third, fourth], out result);
public static bool TryEvaluate(ScalarOperation operation, ReadOnlySpan<ulong> operands, out ulong result)
{
result = 0;

View File

@ -43,6 +43,40 @@ public sealed class ShaderResourcePlan
public ShaderResourceInfo Info { get; private set; } = new();
internal RawReadPrefetchPlan? RawReadPrefetch { get; set; }
private readonly object _compileGate = new();
private CompiledResourceEvaluator? _compiledEvaluator;
private int _compiledEvaluatorState;
internal CompiledResourceEvaluator? CompiledEvaluator
{
get
{
if (Volatile.Read(ref _compiledEvaluatorState) == 2) return _compiledEvaluator;
if (Interlocked.CompareExchange(ref _compiledEvaluatorState, 1, 0) == 0) CompiledResourceEvaluator.Enqueue(this);
return null;
}
}
internal CompiledResourceEvaluator? CompileEvaluatorNow()
{
lock (_compileGate)
{
if (Volatile.Read(ref _compiledEvaluatorState) == 2) return _compiledEvaluator;
CompiledResourceEvaluator? compiled;
try
{
compiled = CompiledResourceEvaluator.Build(this);
}
catch (Exception exception) when (exception is not OutOfMemoryException)
{
compiled = null;
}
_compiledEvaluator = compiled;
Volatile.Write(ref _compiledEvaluatorState, 2);
return compiled;
}
}
// The handles each memory access reads, after flattened reads were replaced.
public MemoryAccessBinding?[] Accesses { get; private set; } = [];

View File

@ -0,0 +1,85 @@
// 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 CompiledResourceEvaluatorTests
{
[Fact]
public void UniformOperationsMatchTheInterpreterWithFullWidthOperands()
{
ulong[] cases = [0, 1, 31, 32, 63, 64, uint.MaxValue, 0x80000000, 0x7FC00000, 0xFFFFFFFFFFFFFFFF, 0x8000000000000000];
foreach (var operation in Enum.GetValues<ScalarOperation>().Where(RuntimeValueValidator.IsUniformOperation))
{
var plan = Extract(Program(BufferLoad(0, 0), EndProgram(8)), userDataCount: 8);
var operands = Enumerable.Range(0, 4).Select(index => ScalarValue.MakeOperation(ScalarOperation.Construct64, ScalarValueType.U64,
ScalarValue.UserData((uint)(index * 2)), ScalarValue.UserData((uint)(index * 2 + 1)))).ToArray();
var value = ScalarValue.MakeOperation(operation, ScalarValueType.U64, operands);
plan.DescriptorSources[0].Dwords[0] = value;
Assert.NotNull(plan.CompileEvaluatorNow());
Assert.True(plan.CompiledEvaluator.TryGetIndex(value, out _));
for (var iteration = 0; iteration < cases.Length; iteration++)
{
var input = Enumerable.Range(0, 4).Select(index => cases[(iteration + index * 3) % cases.Length]).ToArray();
uint[] registers = input.SelectMany(word => new[] { (uint)word, (uint)(word >> 32) }).ToArray();
using var compiledScratch = RuntimeEvaluationScratch.Rent();
using var interpretedScratch = RuntimeEvaluationScratch.Rent();
var compiled = new RuntimeValueEvaluator(compiledScratch, plan, Inputs(registers));
var interpreted = new RuntimeValueEvaluator(interpretedScratch, plan, Inputs(registers), useCompiled: false);
var expectedOk = interpreted.EvaluateWide(value, out var expected);
var actualOk = compiled.EvaluateWide(value, out var actual);
Assert.Equal(expectedOk, actualOk);
Assert.Equal(expected, actual);
}
}
}
[Fact]
public void ActiveLaneSkipsAnUndefinedArmWithoutChangingTheOrdinaryCache()
{
var plan = Extract(Program(BufferLoad(0, 0), EndProgram(8)));
var mask = ScalarValue.UserData(0);
var select = ScalarValue.Select(mask, ScalarValue.UserData(1), ScalarValue.Undefined(ScalarValueType.U32));
var lane = ScalarValue.FirstLane(select, mask);
plan.DescriptorSources[0].Dwords[0] = lane;
Assert.NotNull(plan.CompileEvaluatorNow());
Assert.True(plan.CompiledEvaluator.TryGetIndex(lane, out _));
using var scratch = RuntimeEvaluationScratch.Rent();
var evaluator = new RuntimeValueEvaluator(scratch, plan, Inputs([0, 42]));
Assert.False(evaluator.Evaluate(select, out _));
Assert.True(evaluator.Evaluate(lane, out var result));
Assert.Equal(42u, result);
Assert.False(evaluator.Evaluate(select, out _));
}
[Fact]
public void CompiledAndInterpretedTableReadsHaveIdenticalOrderAndFailure()
{
var plan = Extract(Program(ScalarLoad(0, 0, 4, 4), BufferLoad(8, 4), EndProgram(16)));
Assert.NotNull(plan.CompileEvaluatorNow());
foreach (var failAt in new ulong[] { 0, 0x1000, 0x1008 })
{
var reads = new List<ulong>();
bool Read(ulong address, out uint word)
{
reads.Add(address);
word = (uint)address;
return address != failAt;
}
using var compiledScratch = RuntimeEvaluationScratch.Rent();
using var interpretedScratch = RuntimeEvaluationScratch.Rent();
var compiled = new RuntimeValueEvaluator(compiledScratch, plan, Inputs([0x1000, 0], Read));
var interpreted = new RuntimeValueEvaluator(interpretedScratch, plan, Inputs([0x1000, 0], Read), useCompiled: false);
var actual = plan.DescriptorSources[0].Dwords.Select(value => (compiled.EvaluateWide(value, out var word), word)).ToArray();
var actualReads = reads.ToArray();
reads.Clear();
var expected = plan.DescriptorSources[0].Dwords.Select(value => (interpreted.EvaluateWide(value, out var word), word)).ToArray();
Assert.Equal(expected, actual);
Assert.Equal(reads, actualReads);
}
}
}