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

176 lines
6.8 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 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 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 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);
}
}