diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs index 96a8f8d..8e5b32f 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs @@ -210,15 +210,22 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider } // One past the highest parameter location the pixel program reads, resolved as its translator does. - private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info) - { - var attributes = pixelProgram.Instructions + // The attributes a pixel program interpolates, ascending; scanned once per decoded program + // instead of on every draw. + private static readonly System.Runtime.CompilerServices.ConditionalWeakTable _interpolatedAttributes = new(); + + private static uint[] InterpolatedAttributes(Gen5ShaderProgram program) => + _interpolatedAttributes.GetValue(program, static program => program.Instructions .Select(static instruction => instruction.Control) .OfType() .Select(static control => control.Attribute) .Distinct() .Order() - .ToArray(); + .ToArray()); + + private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info) + { + var attributes = InterpolatedAttributes(pixelProgram); if (attributes.Length == 0) { return 0; @@ -237,16 +244,8 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider private static uint InterpolatedAttributeCount(Gen5ShaderProgram program) { - var maxAttribute = -1; - foreach (var instruction in program.Instructions) - { - if (instruction.Control is Gen5InterpolationControl interpolation) - { - maxAttribute = Math.Max(maxAttribute, (int)interpolation.Attribute); - } - } - - return (uint)(maxAttribute + 1); + var attributes = InterpolatedAttributes(program); + return attributes.Length == 0 ? 0u : attributes[^1] + 1; } // The bound colour slots in order; each output mode names the kind the pixel program exports. diff --git a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs index 759e5bc..1308be6 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ResourceMaterializationCache.cs @@ -37,6 +37,7 @@ public sealed class ResourceMaterializationCache private static long _totalStale; private static long _totalStaleUnreadable; private static long _totalRefreshes; + private static long _totalBypassed; [ThreadStatic] private static bool _readingTable; @@ -47,6 +48,11 @@ public sealed class ResourceMaterializationCache private set => _readingTable = value; } + private const int ProbationLookups = 64; + // A plan keeps using the cache while at least one lookup in this many hits. + private const int MinimumHitShare = 8; + + public long Bypassed { get; private set; } public long Hits { get; private set; } public long Misses { get; private set; } public long Uncacheable { get; private set; } @@ -61,9 +67,10 @@ public sealed class ResourceMaterializationCache var stale = Interlocked.Exchange(ref _totalStale, 0); var staleUnreadable = Interlocked.Exchange(ref _totalStaleUnreadable, 0); var refreshes = Interlocked.Exchange(ref _totalRefreshes, 0); + var bypassed = Interlocked.Exchange(ref _totalBypassed, 0); var total = hits + misses; return FormattableString.Invariant( - $"[PERF][RESOURCE_CACHE] hits={hits} misses={misses} stale={stale} stale_unreadable={staleUnreadable} refreshes={refreshes} uncacheable={uncacheable} hit_rate={(total == 0 ? 0 : hits * 100.0 / total):F1}% {RawReadPrefetch.TakeReport()}"); + $"[PERF][RESOURCE_CACHE] hits={hits} misses={misses} stale={stale} stale_unreadable={staleUnreadable} refreshes={refreshes} bypassed={bypassed} uncacheable={uncacheable} hit_rate={(total == 0 ? 0 : hits * 100.0 / total):F1}% {RawReadPrefetch.TakeReport()}"); } public bool Materialize( @@ -74,6 +81,17 @@ public sealed class ResourceMaterializationCache ref ResourceSpecialization specialization, out ResourceMaterializationFailure failure) { + // Most draws of a plan whose user data points at per-draw constants never repeat their + // inputs; recording their reads and storing entries then costs more than a plain walk. + // Such a plan materializes directly and probes the cache again on every 64th draw. + var lookups = ++plan.CacheLookups; + if (lookups > ProbationLookups && plan.CacheHits * MinimumHitShare < lookups && (lookups & 63) != 0) + { + Bypassed++; + Interlocked.Increment(ref _totalBypassed); + return ResourceMaterializer.Materialize(plan, inputs, ref snapshot, ref specialization, out failure); + } + var key = KeyOf(plan, inputs); var found = TryFind(key, plan, inputs, out var cached); if (found) @@ -94,6 +112,7 @@ public sealed class ResourceMaterializationCache } Hits++; + plan.CacheHits++; Interlocked.Increment(ref _totalHits); snapshot = variant.Snapshot; specialization = variant.Specialization; @@ -107,6 +126,7 @@ public sealed class ResourceMaterializationCache if (TryRefreshTable(key, cached, plan, inputs, residentReader, out var refreshed)) { TableRefreshes++; + plan.CacheHits++; Interlocked.Increment(ref _totalRefreshes); snapshot = refreshed.Snapshot; specialization = refreshed.Specialization; diff --git a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs index 985b04f..3214514 100644 --- a/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs +++ b/src/SharpEmu.ShaderCompiler/Resources/ShaderResourcePlan.cs @@ -27,6 +27,10 @@ public sealed class ShaderResourcePlan public ScalarValueGraph Graph { get; } public ShaderStage Stage { get; } public ulong Hash { get; } + + // How often ResourceMaterializationCache looked this plan up and found a reusable entry. + internal int CacheLookups; + internal int CacheHits; public uint UserDataBase => Graph.UserDataBase; public uint UserDataCount => Graph.UserDataCount; public MemoryAccessTable Memory => Graph.Memory; diff --git a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs index e1f7354..a348123 100644 --- a/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs +++ b/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCacheTests.cs @@ -1,279 +1,312 @@ -// 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 ResourceMaterializationCacheTests -{ - private const ulong HeapBase = 0x1000; - - // A mask word, a two-entry index table and six 32-byte image records. - private sealed class Heap - { - public readonly Dictionary Words = new(); - public int Reads; - public bool GpuOwned; - - public Heap() - { - Words[HeapBase + 0x80] = (1u << 1) | (1u << 4); - Words[HeapBase + 0x40 + 0x90] = 2; - Words[HeapBase + 0x40 + 4 * 0x90] = 5; - foreach (var record in new uint[] { 2, 5 }) - { - var entry = HeapBase + 0x100 + record * 32; - for (uint dword = 0; dword < 8; dword++) - Words[entry + dword * 4] = dword switch - { - 0 => (record & 1) == 0 ? 0x2000u : 0x1000u, - 1 => 20u << 20, - 3 => 0xFACu | (9u << 28), - _ => 0, - }; - } - } - - public bool Read(ulong address, out uint word) - { - Reads++; - return Words.TryGetValue(address, out word); - } - - public bool ReadResident(ulong address, Span destination, bool clean) - { - if (GpuOwned) - return false; - for (var offset = 0; offset < destination.Length; offset += 4) - { - if (!Words.TryGetValue(address + (ulong)offset, out var word)) - return false; - BitConverter.TryWriteBytes(destination[offset..], word); - } - - return true; - } - } - - private static ShaderResourcePlan Plan() => - ShaderResourcePlan.Extract(DirectImageTableTests.CreateWaveIndexedDescriptorProgram(), ShaderStage.Compute, Hash, 0, 2); - - private static bool Run(ResourceMaterializationCache cache, ShaderResourcePlan plan, Heap heap, uint[] userData, - out ResourceSnapshot snapshot, out ResourceSpecialization specialization, ulong shaderBase = 0) - { - snapshot = new ResourceSnapshot(); - specialization = new ResourceSpecialization(); - return cache.Materialize(plan, Inputs(userData, readCleanMemory: heap.Read, shaderBase: shaderBase), heap.ReadResident, - ref snapshot, ref specialization, out _); - } - - [Fact] - public void UnchangedMemoryReusesTheMaterialization() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out var firstSpecialization)); - var readsAfterFirst = heap.Reads; - Assert.True(readsAfterFirst > 0); - - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out var secondSpecialization)); - Assert.Equal(readsAfterFirst, heap.Reads); - Assert.Same(first, second); - Assert.Same(firstSpecialization, secondSpecialization); - Assert.Equal((1, 1), (cache.Hits, cache.Misses)); - } - - [Fact] - public void AChangedDescriptorWordMaterializesAgain() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); - - heap.Words[HeapBase + 0x100 + 5 * 32] = 0x3000; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _)); - Assert.NotSame(first, second); - Assert.Equal((0, 2), (cache.Hits, cache.Misses)); - - // An uncached full walk gives the same result as the re-materialization. - var reference = new ResourceSnapshot(); - var referenceSpecialization = new ResourceSpecialization(); - Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: heap.Read), - ref reference, ref referenceSpecialization)); - Assert.Equal(reference.Images.Select(image => image.ToArray()), second.Images.Select(image => image.ToArray())); - } - - [Fact] - public void AlternatingDescriptorsReuseEveryRecentVariant() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - var word = HeapBase + 0x100 + 5 * 32; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); - heap.Words[word] = 0x3000; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _)); - Assert.Equal((0, 2), (cache.Hits, cache.Misses)); - - heap.Words[word] = 0x1000; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var firstAgain, out _)); - heap.Words[word] = 0x3000; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var secondAgain, out _)); - Assert.Same(first, firstAgain); - Assert.Same(second, secondAgain); - Assert.Equal((2, 2), (cache.Hits, cache.Misses)); - } - - [Fact] - public void AChangedMaskMaterializesAgain() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - heap.Words[HeapBase + 0x80] = 1u << 1; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - Assert.Equal(0, cache.Hits); - } - - [Fact] - public void GpuOwnedMemoryIsNeverTrusted() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - heap.GpuOwned = true; - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - Assert.Equal(0, cache.Hits); - } - - [Fact] - public void ADifferentDrawKeyIsADifferentEntry() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _, shaderBase: 0x100)); - Assert.NotSame(first, second); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out var third, out _)); - Assert.Same(first, third); - Assert.Equal((1, 2), (cache.Hits, cache.Misses)); - } - - [Fact] - public void AFailedReadIsNotCached() - { - var plan = Plan(); - var heap = new Heap(); - heap.Words.Remove(HeapBase + 0x100 + 5 * 32 + 4); - var cache = new ResourceMaterializationCache(); - var ok = Run(cache, plan, heap, [0x1000, 0], out _, out _); - var reads = heap.Reads; - Assert.Equal(ok, Run(cache, plan, heap, [0x1000, 0], out _, out _)); - Assert.True(heap.Reads > reads); - 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]; - - // Reads the four constants of FlattenedReadReuseTests.RepeatedReadProgram from a word memory. - private static bool RunTable(ResourceMaterializationCache cache, ShaderResourcePlan plan, TestWordMemory memory, - out ResourceSnapshot snapshot) - { - snapshot = new ResourceSnapshot(); - var specialization = new ResourceSpecialization(); - return cache.Materialize(plan, Inputs(TableUserData, memory.Read, memory.Read), (address, destination, _) => - { - for (var offset = 0; offset < destination.Length; offset += 4) - { - if (!memory.Read(address + (ulong)offset, out var word)) - return false; - BitConverter.TryWriteBytes(destination[offset..], word); - } - - return true; - }, ref snapshot, ref specialization, out _); - } - - [Fact] - public void AChangedTableOnlyWordRefreshesOnlyTheTable() - { - var (plan, _, _) = Prepare(FlattenedReadReuseTests.RepeatedReadProgram(4), userDataCount: 9); - var memory = new TestWordMemory { Words = [11, 22, 33, 44] }; - var cache = new ResourceMaterializationCache(); - Assert.True(RunTable(cache, plan, memory, out var first)); - Assert.Equal([11u, 22, 33, 44], first.FlattenedResourceTable); - - memory.Words[2] = 99; - Assert.True(RunTable(cache, plan, memory, out var second)); - Assert.Equal((0, 1, 1), (cache.Hits, cache.Misses, cache.TableRefreshes)); - Assert.Equal([11u, 22, 99, 44], second.FlattenedResourceTable); - Assert.Same(first.Buffers, second.Buffers); - - // The refreshed table matches an uncached full walk, and the refreshed entry is then a hit. - var reference = new ResourceSnapshot(); - var referenceSpecialization = new ResourceSpecialization(); - Assert.True(ResourceMaterializer.Materialize(plan, Inputs(TableUserData, memory.Read, memory.Read), ref reference, ref referenceSpecialization)); - Assert.Equal(reference.FlattenedResourceTable, second.FlattenedResourceTable); - Assert.True(RunTable(cache, plan, memory, out var third)); - Assert.Same(second, third); - Assert.Equal((1, 1, 1), (cache.Hits, cache.Misses, cache.TableRefreshes)); - } - - [Fact] - public void AnUnreadableTableWordFallsBackToAFullWalk() - { - var (plan, _, _) = Prepare(FlattenedReadReuseTests.RepeatedReadProgram(4), userDataCount: 9); - var memory = new TestWordMemory { Words = [11, 22, 33, 44] }; - var cache = new ResourceMaterializationCache(); - Assert.True(RunTable(cache, plan, memory, out _)); - memory.Words[1] = 7; - memory.FailAddress = 0x1000 + 3 * 4; - RunTable(cache, plan, memory, out _); - Assert.Equal(0, cache.TableRefreshes); - } - - [Fact] - public void AFullGenerationKeepsRecentEntries() - { - var plan = Plan(); - var heap = new Heap(); - var cache = new ResourceMaterializationCache(generationCapacity: 1); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _, shaderBase: 0x100)); - // The first entry moved to the old generation and is still found. - Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); - Assert.Equal(1, cache.Hits); - } -} +// 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 ResourceMaterializationCacheTests +{ + private const ulong HeapBase = 0x1000; + + // A mask word, a two-entry index table and six 32-byte image records. + private sealed class Heap + { + public readonly Dictionary Words = new(); + public int Reads; + public bool GpuOwned; + + public Heap() + { + Words[HeapBase + 0x80] = (1u << 1) | (1u << 4); + Words[HeapBase + 0x40 + 0x90] = 2; + Words[HeapBase + 0x40 + 4 * 0x90] = 5; + foreach (var record in new uint[] { 2, 5 }) + { + var entry = HeapBase + 0x100 + record * 32; + for (uint dword = 0; dword < 8; dword++) + Words[entry + dword * 4] = dword switch + { + 0 => (record & 1) == 0 ? 0x2000u : 0x1000u, + 1 => 20u << 20, + 3 => 0xFACu | (9u << 28), + _ => 0, + }; + } + } + + public bool Read(ulong address, out uint word) + { + Reads++; + return Words.TryGetValue(address, out word); + } + + public bool ReadResident(ulong address, Span destination, bool clean) + { + if (GpuOwned) + return false; + for (var offset = 0; offset < destination.Length; offset += 4) + { + if (!Words.TryGetValue(address + (ulong)offset, out var word)) + return false; + BitConverter.TryWriteBytes(destination[offset..], word); + } + + return true; + } + } + + private static ShaderResourcePlan Plan() => + ShaderResourcePlan.Extract(DirectImageTableTests.CreateWaveIndexedDescriptorProgram(), ShaderStage.Compute, Hash, 0, 2); + + private static bool Run(ResourceMaterializationCache cache, ShaderResourcePlan plan, Heap heap, uint[] userData, + out ResourceSnapshot snapshot, out ResourceSpecialization specialization, ulong shaderBase = 0) + { + snapshot = new ResourceSnapshot(); + specialization = new ResourceSpecialization(); + return cache.Materialize(plan, Inputs(userData, readCleanMemory: heap.Read, shaderBase: shaderBase), heap.ReadResident, + ref snapshot, ref specialization, out _); + } + + [Fact] + public void UnchangedMemoryReusesTheMaterialization() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out var firstSpecialization)); + var readsAfterFirst = heap.Reads; + Assert.True(readsAfterFirst > 0); + + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out var secondSpecialization)); + Assert.Equal(readsAfterFirst, heap.Reads); + Assert.Same(first, second); + Assert.Same(firstSpecialization, secondSpecialization); + Assert.Equal((1, 1), (cache.Hits, cache.Misses)); + } + + [Fact] + public void APlanWhoseInputsNeverRepeatStopsRecordingButKeepsItsResults() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + for (var draw = 0u; draw < 200; draw++) + { + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var snapshot, out _, shaderBase: 0x10000 + draw * 0x100)); + var reference = new ResourceSnapshot(); + var referenceSpecialization = new ResourceSpecialization(); + Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: heap.Read, shaderBase: 0x10000 + draw * 0x100), + ref reference, ref referenceSpecialization)); + Assert.Equal(reference.Images.Select(image => image.ToArray()), snapshot.Images.Select(image => image.ToArray())); + } + + Assert.True(cache.Bypassed > 100); + Assert.Equal(0, cache.Hits); + } + + [Fact] + public void APlanThatKeepsHittingKeepsTheCache() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + for (var draw = 0; draw < 200; draw++) + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + + Assert.Equal(0, cache.Bypassed); + Assert.Equal(199, cache.Hits); + } + + [Fact] + public void AChangedDescriptorWordMaterializesAgain() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); + + heap.Words[HeapBase + 0x100 + 5 * 32] = 0x3000; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _)); + Assert.NotSame(first, second); + Assert.Equal((0, 2), (cache.Hits, cache.Misses)); + + // An uncached full walk gives the same result as the re-materialization. + var reference = new ResourceSnapshot(); + var referenceSpecialization = new ResourceSpecialization(); + Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: heap.Read), + ref reference, ref referenceSpecialization)); + Assert.Equal(reference.Images.Select(image => image.ToArray()), second.Images.Select(image => image.ToArray())); + } + + [Fact] + public void AlternatingDescriptorsReuseEveryRecentVariant() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + var word = HeapBase + 0x100 + 5 * 32; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); + heap.Words[word] = 0x3000; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _)); + Assert.Equal((0, 2), (cache.Hits, cache.Misses)); + + heap.Words[word] = 0x1000; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var firstAgain, out _)); + heap.Words[word] = 0x3000; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var secondAgain, out _)); + Assert.Same(first, firstAgain); + Assert.Same(second, secondAgain); + Assert.Equal((2, 2), (cache.Hits, cache.Misses)); + } + + [Fact] + public void AChangedMaskMaterializesAgain() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + heap.Words[HeapBase + 0x80] = 1u << 1; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + Assert.Equal(0, cache.Hits); + } + + [Fact] + public void GpuOwnedMemoryIsNeverTrusted() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + heap.GpuOwned = true; + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + Assert.Equal(0, cache.Hits); + } + + [Fact] + public void ADifferentDrawKeyIsADifferentEntry() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var first, out _)); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var second, out _, shaderBase: 0x100)); + Assert.NotSame(first, second); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out var third, out _)); + Assert.Same(first, third); + Assert.Equal((1, 2), (cache.Hits, cache.Misses)); + } + + [Fact] + public void AFailedReadIsNotCached() + { + var plan = Plan(); + var heap = new Heap(); + heap.Words.Remove(HeapBase + 0x100 + 5 * 32 + 4); + var cache = new ResourceMaterializationCache(); + var ok = Run(cache, plan, heap, [0x1000, 0], out _, out _); + var reads = heap.Reads; + Assert.Equal(ok, Run(cache, plan, heap, [0x1000, 0], out _, out _)); + Assert.True(heap.Reads > reads); + 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]; + + // Reads the four constants of FlattenedReadReuseTests.RepeatedReadProgram from a word memory. + private static bool RunTable(ResourceMaterializationCache cache, ShaderResourcePlan plan, TestWordMemory memory, + out ResourceSnapshot snapshot) + { + snapshot = new ResourceSnapshot(); + var specialization = new ResourceSpecialization(); + return cache.Materialize(plan, Inputs(TableUserData, memory.Read, memory.Read), (address, destination, _) => + { + for (var offset = 0; offset < destination.Length; offset += 4) + { + if (!memory.Read(address + (ulong)offset, out var word)) + return false; + BitConverter.TryWriteBytes(destination[offset..], word); + } + + return true; + }, ref snapshot, ref specialization, out _); + } + + [Fact] + public void AChangedTableOnlyWordRefreshesOnlyTheTable() + { + var (plan, _, _) = Prepare(FlattenedReadReuseTests.RepeatedReadProgram(4), userDataCount: 9); + var memory = new TestWordMemory { Words = [11, 22, 33, 44] }; + var cache = new ResourceMaterializationCache(); + Assert.True(RunTable(cache, plan, memory, out var first)); + Assert.Equal([11u, 22, 33, 44], first.FlattenedResourceTable); + + memory.Words[2] = 99; + Assert.True(RunTable(cache, plan, memory, out var second)); + Assert.Equal((0, 1, 1), (cache.Hits, cache.Misses, cache.TableRefreshes)); + Assert.Equal([11u, 22, 99, 44], second.FlattenedResourceTable); + Assert.Same(first.Buffers, second.Buffers); + + // The refreshed table matches an uncached full walk, and the refreshed entry is then a hit. + var reference = new ResourceSnapshot(); + var referenceSpecialization = new ResourceSpecialization(); + Assert.True(ResourceMaterializer.Materialize(plan, Inputs(TableUserData, memory.Read, memory.Read), ref reference, ref referenceSpecialization)); + Assert.Equal(reference.FlattenedResourceTable, second.FlattenedResourceTable); + Assert.True(RunTable(cache, plan, memory, out var third)); + Assert.Same(second, third); + Assert.Equal((1, 1, 1), (cache.Hits, cache.Misses, cache.TableRefreshes)); + } + + [Fact] + public void AnUnreadableTableWordFallsBackToAFullWalk() + { + var (plan, _, _) = Prepare(FlattenedReadReuseTests.RepeatedReadProgram(4), userDataCount: 9); + var memory = new TestWordMemory { Words = [11, 22, 33, 44] }; + var cache = new ResourceMaterializationCache(); + Assert.True(RunTable(cache, plan, memory, out _)); + memory.Words[1] = 7; + memory.FailAddress = 0x1000 + 3 * 4; + RunTable(cache, plan, memory, out _); + Assert.Equal(0, cache.TableRefreshes); + } + + [Fact] + public void AFullGenerationKeepsRecentEntries() + { + var plan = Plan(); + var heap = new Heap(); + var cache = new ResourceMaterializationCache(generationCapacity: 1); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _, shaderBase: 0x100)); + // The first entry moved to the old generation and is still found. + Assert.True(Run(cache, plan, heap, [0x1000, 0], out _, out _)); + Assert.Equal(1, cache.Hits); + } +}