From 7ab6de1b93f29bf3e52fa1b2c38f5b71631bb062 Mon Sep 17 00:00:00 2001 From: kuba Date: Wed, 7 Oct 2026 10:48:57 +0200 Subject: [PATCH] [GPU] Cut per-draw command-worker cost in resource materialization and pixel input setup (#1039) * [ShaderCompiler] Materialize directly for plans whose cached inputs never repeat The materialization cache records every guest word a draw reads and keys the result on the draw's user data. Most Silent Hill draws point their user data at per-draw constants, so only 8% of lookups hit and every miss still paid for recording the reads and storing an entry. A plan now counts its lookups and hits; after 64 lookups with fewer than one hit in eight it walks its resources directly and probes the cache again on every 64th draw. On the SH title menu the command worker's busy time per draw fell from ~125 to ~111 us (cache hit rate of the remaining lookups 70%). * [GPU] Scan a pixel program's interpolated attributes once per decoded program Every draw scanned all instructions of its pixel program twice, once through a LINQ chain with Distinct/Order/ToArray, to count interpolated attributes and the vertex outputs they read. The sorted attribute set is a property of the decoded program, so it is computed once and kept beside it. Pixel input resolution on the SH menu dropped from ~14 us to a few us per draw. --- .../Gpu/Pipelines/ShaderPipelineCache.cs | 27 +- .../Resources/ResourceMaterializationCache.cs | 22 +- .../Resources/ShaderResourcePlan.cs | 4 + .../ResourceMaterializationCacheTests.cs | 591 +++++++++--------- 4 files changed, 350 insertions(+), 294 deletions(-) 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); + } +}