sharpemu/tests/SharpEmu.ShaderCompiler.Tests/Resources/ResourceMaterializationCach...

313 lines
13 KiB
C#

// 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<ulong, uint> 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<byte> 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);
}
}