From 73afbd2015ee36a367d1413f198aea463c10569e Mon Sep 17 00:00:00 2001 From: kuba Date: Thu, 1 Oct 2026 13:12:09 +0200 Subject: [PATCH] [Shader] Reuse scratch storage for resource read recording --- .../Resources/ResourceMaterializationCache.cs | 213 +++++++++++------- .../ResourceMaterializationCacheTests.cs | 23 ++ 2 files changed, 159 insertions(+), 77 deletions(-) diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs index 12defee..45149d0 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs @@ -23,6 +23,7 @@ public sealed class ResourceMaterializationCache private Dictionary _young = new(); private Dictionary _old = new(); private byte[] _scratch = new byte[256]; + private ReadRecorder? _spareRecorder; public ResourceMaterializationCache(int generationCapacity = 16384) { @@ -82,28 +83,35 @@ public sealed class ResourceMaterializationCache Misses++; Interlocked.Increment(ref _totalMisses); - var recorder = new ReadRecorder(); - var recording = new ResourceRuntimeInputs + var recorder = TakeRecorder(); + try { - UserData = inputs.UserData, - ShaderBase = inputs.ShaderBase, - ReadMemory = recorder.Wrap(inputs.ReadMemory, clean: false), - ReadCleanMemory = recorder.Wrap(inputs.ReadCleanMemory, clean: true), - ComputeState = inputs.ComputeState, - TablePhase = recorder.SetTablePhase, - }; - if (!ResourceMaterializer.Materialize(plan, recording, ref snapshot, ref specialization, out failure)) - return false; + var recording = new ResourceRuntimeInputs + { + UserData = inputs.UserData, + ShaderBase = inputs.ShaderBase, + ReadMemory = recorder.Wrap(inputs.ReadMemory, clean: false), + ReadCleanMemory = recorder.Wrap(inputs.ReadCleanMemory, clean: true), + ComputeState = inputs.ComputeState, + TablePhase = recorder.TablePhase, + }; + if (!ResourceMaterializer.Materialize(plan, recording, ref snapshot, ref specialization, out failure)) + return false; - if (recorder.Failed) - { - Uncacheable++; - Interlocked.Increment(ref _totalUncacheable); + if (recorder.Failed) + { + Uncacheable++; + Interlocked.Increment(ref _totalUncacheable); + return true; + } + + Store(key, recorder.Build(plan, inputs, snapshot, specialization)); return true; } - - Store(key, recorder.Build(plan, inputs, snapshot, specialization)); - return true; + finally + { + ReturnRecorder(recorder); + } } // Most stale entries in Demon's Souls differ only in words the shader reads through scalar @@ -139,57 +147,77 @@ public sealed class ResourceMaterializationCache if (!changed) return false; - var recorder = new ReadRecorder(); - var recording = new ResourceRuntimeInputs + var recorder = TakeRecorder(); + try { - UserData = inputs.UserData, - ShaderBase = inputs.ShaderBase, - ReadMemory = recorder.Wrap(inputs.ReadMemory, clean: false), - ReadCleanMemory = recorder.Wrap(inputs.ReadCleanMemory, clean: true), - ComputeState = inputs.ComputeState, - }; - var cachedTable = cached.Snapshot.FlattenedResourceTable; - if (!ResourceMaterializer.TryEvaluateTable(plan, recording, out var table) || recorder.Failed || table.Length != cachedTable.Length) - return false; - - foreach (var (address, word, _, _) in recorder.Reads) - { - if (!TryFindWord(cached, address, out var offset) || - System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(current.AsSpan(offset, sizeof(uint))) != word) - return false; - } - - // The written device-address slots come from reads validated unchanged above. - foreach (var slot in plan.WrittenRangeSlotByHandle.Values) - cachedTable.AsSpan((int)slot, ShaderResourcePlan.WrittenRangeDwordCount).CopyTo(table.AsSpan((int)slot)); - - var previous = cached.Snapshot; - refreshed = new Entry - { - Plan = cached.Plan, - UserData = cached.UserData, - ShaderBase = cached.ShaderBase, - ComputeState = cached.ComputeState, - RangeAddresses = cached.RangeAddresses, - RangeOffsets = cached.RangeOffsets, - RangeLengths = cached.RangeLengths, - RangeClean = cached.RangeClean, - WordTableOnly = cached.WordTableOnly, - TableRefreshable = true, - Bytes = current, - Snapshot = new ResourceSnapshot + var recording = new ResourceRuntimeInputs { - Buffers = previous.Buffers, - Images = previous.Images, - Samplers = previous.Samplers, - FlattenedResourceTable = table, - UserData = previous.UserData, - DeviceAddressRanges = previous.DeviceAddressRanges, - }, - Specialization = cached.Specialization, - }; - Store(key, refreshed); - return true; + UserData = inputs.UserData, + ShaderBase = inputs.ShaderBase, + ReadMemory = recorder.Wrap(inputs.ReadMemory, clean: false), + ReadCleanMemory = recorder.Wrap(inputs.ReadCleanMemory, clean: true), + ComputeState = inputs.ComputeState, + }; + var cachedTable = cached.Snapshot.FlattenedResourceTable; + if (!ResourceMaterializer.TryEvaluateTable(plan, recording, out var table) || recorder.Failed || table.Length != cachedTable.Length) + return false; + + foreach (var (address, word, _, _) in recorder.Reads) + { + if (!TryFindWord(cached, address, out var offset) || + System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(current.AsSpan(offset, sizeof(uint))) != word) + return false; + } + + // The written device-address slots come from reads validated unchanged above. + foreach (var slot in plan.WrittenRangeSlotByHandle.Values) + cachedTable.AsSpan((int)slot, ShaderResourcePlan.WrittenRangeDwordCount).CopyTo(table.AsSpan((int)slot)); + + var previous = cached.Snapshot; + refreshed = new Entry + { + Plan = cached.Plan, + UserData = cached.UserData, + ShaderBase = cached.ShaderBase, + ComputeState = cached.ComputeState, + RangeAddresses = cached.RangeAddresses, + RangeOffsets = cached.RangeOffsets, + RangeLengths = cached.RangeLengths, + RangeClean = cached.RangeClean, + WordTableOnly = cached.WordTableOnly, + TableRefreshable = true, + Bytes = current, + Snapshot = new ResourceSnapshot + { + Buffers = previous.Buffers, + Images = previous.Images, + Samplers = previous.Samplers, + FlattenedResourceTable = table, + UserData = previous.UserData, + DeviceAddressRanges = previous.DeviceAddressRanges, + }, + Specialization = cached.Specialization, + }; + Store(key, refreshed); + return true; + } + finally + { + ReturnRecorder(recorder); + } + } + + private ReadRecorder TakeRecorder() + { + var recorder = _spareRecorder ?? new ReadRecorder(); + _spareRecorder = null; + return recorder; + } + + private void ReturnRecorder(ReadRecorder recorder) + { + recorder.Reset(); + _spareRecorder = recorder; } // The byte offset of a recorded dword in the entry's bytes, found by its address. @@ -311,28 +339,59 @@ public sealed class ResourceMaterializationCache { private readonly List<(ulong Address, uint Word, bool Clean, bool Table)> _reads = new(); private bool _inTable; + private GuestWordReader? _reader; + private GuestWordReader? _cleanReader; + private readonly GuestWordReader _recordRead; + private readonly GuestWordReader _recordCleanRead; + + public ReadRecorder() + { + _recordRead = (ulong address, out uint word) => Read(_reader!, address, out word, clean: false); + _recordCleanRead = (ulong address, out uint word) => Read(_cleanReader!, address, out word, clean: true); + TablePhase = inTable => _inTable = inTable; + } + + public void Reset() + { + _reads.Clear(); + // Retain ordinary descriptor walks without keeping unusually large + // tables alive for the lifetime of the renderer. + if (_reads.Capacity > 16384) + _reads.Capacity = 0; + _reader = null; + _cleanReader = null; + _inTable = false; + Failed = false; + } public bool Failed { get; private set; } public List<(ulong Address, uint Word, bool Clean, bool Table)> Reads => _reads; - public void SetTablePhase(bool inTable) => _inTable = inTable; + public Action TablePhase { get; } public GuestWordReader? Wrap(GuestWordReader? inner, bool clean) { if (inner is null) return null; - return (ulong address, out uint word) => + if (clean) { - if (!inner(address, out word)) - { - Failed = true; - return false; - } + _cleanReader = inner; + return _recordCleanRead; + } + _reader = inner; + return _recordRead; + } - _reads.Add((address, word, clean, _inTable)); - return true; - }; + private bool Read(GuestWordReader inner, ulong address, out uint word, bool clean) + { + if (!inner(address, out word)) + { + Failed = true; + return false; + } + _reads.Add((address, word, clean, _inTable)); + return true; } public Entry Build(ShaderResourcePlan plan, ResourceRuntimeInputs inputs, ResourceSnapshot snapshot, diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs index 3ba843b..842b7ee 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs @@ -160,6 +160,29 @@ public sealed class ResourceMaterializationCacheTests Assert.Equal(0, cache.Hits); } + [Fact] + public void AFailedRecordingDoesNotPoisonTheNextReader() + { + var plan = Plan(); + var incomplete = new Heap(); + incomplete.Words.Remove(HeapBase + 0x100 + 5 * 32 + 4); + var cache = new ResourceMaterializationCache(); + Run(cache, plan, incomplete, [0x1000, 0], out _, out _); + + var complete = new Heap(); + Assert.True(Run(cache, plan, complete, [0x1000, 0], out var first, out _)); + Assert.True(Run(cache, plan, complete, [0x1000, 0], out var second, out _)); + Assert.Same(first, second); + Assert.Equal(1, cache.Hits); + + // Retained entries must not refer to scratch reads reused for another key. + complete.Words[HeapBase + 0x80] = 1u << 1; + Assert.True(Run(cache, plan, complete, [0x1000, 0], out _, out _, shaderBase: 0x100)); + var original = new Heap(); + Assert.True(Run(cache, plan, original, [0x1000, 0], out var restored, out _)); + Assert.Same(first, restored); + } + // The table pointer in s[0:1]; the rest of the nine user-data registers the program declares. private static readonly uint[] TableUserData = [0x1000, 0, 0, 0, 0, 0, 0, 0, 0];