sharpemu/tests/SharpEmu.Libs.Tests/Gpu/ShaderCache/ShaderCacheFileTests.cs

442 lines
18 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Gpu.ShaderCache;
using SharpEmu.Libs.Tests.Gpu.Pipelines;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Silk.NET.Vulkan;
using Xunit;
namespace SharpEmu.Libs.Tests.Gpu.ShaderCache;
public sealed class ShaderCacheFileTests : IDisposable
{
private const ulong CodeAddress = PipelineTestGuest.MemoryBase + 0x1_0000;
private const ulong HeaderAddress = PipelineTestGuest.MemoryBase + 0x8000;
private const ulong VertexCodeAddress = PipelineTestGuest.MemoryBase + 0x2_0000;
private const ulong VertexHeaderAddress = PipelineTestGuest.MemoryBase + 0x9000;
private const ulong PixelCodeAddress = PipelineTestGuest.MemoryBase + 0x3_0000;
private const ulong PixelHeaderAddress = PipelineTestGuest.MemoryBase + 0xA000;
private const ulong BufferAddress = PipelineTestGuest.MemoryBase + 0x4_0000;
private const uint Format32x4Float = 77;
private static readonly uint[] EmbeddedFetchVertexProgram = [0xF4080303, 0xFA000000, 0x02020B08, 0xE00C2000, 0x80030401, 0xBF810000];
private readonly string _directory = Path.Combine(Path.GetTempPath(), "SharpEmuTests", Guid.NewGuid().ToString("N"));
public void Dispose()
{
if (Directory.Exists(_directory))
{
Directory.Delete(_directory, recursive: true);
}
}
private static byte[] Compile(ShaderCompileRequest request)
{
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
return shader.Spirv;
}
private ShaderCacheFile Open() => ShaderCacheFile.Open(_directory) ?? throw new InvalidOperationException("The shader cache did not open.");
private static (PipelineTestGuest Guest, byte[] Spirv) CompileCompute(ShaderCacheFile file, uint format)
{
var guest = new PipelineTestGuest(Compile);
guest.Host.ShaderCache = file;
guest.RegisterProgram(CodeAddress, HeaderAddress, PipelineTestGuest.FormatLoadProgram);
var cursor = 0u;
guest.Programs.GetOrCompile(
guest.Source(CodeAddress, ShaderStage.Compute, PipelineTestGuest.BufferDescriptor(BufferAddress, 4, 64, format)),
PipelineTestGuest.ComputeOptions(threadsX: 64),
ref cursor,
out _);
return (guest, Assert.Single(guest.Compiler.Shaders).Spirv);
}
private static byte[] Replay(StageRecord record, ShaderCodeCapture code, IShaderPipelineHost host)
{
Assert.True(
ShaderProgramCache.TryReplay(
record, code, new FakeShaderCompiler(Compile), ShaderCompileHostFlags.From(host), out var compiled, out var info, out var error),
error);
Assert.NotNull(info);
return Assert.IsType<FakeCompiledShader>(compiled).Spirv;
}
[Fact]
public void ARecordedComputeProgramReplaysToTheSameSpirv()
{
PipelineTestGuest guest;
byte[] runtime;
using (var file = Open())
{
(guest, runtime) = CompileCompute(file, BufferDescriptorWords.Format32UInt);
}
using var reloaded = Open();
var item = Assert.Single(reloaded.Computes);
Assert.Equal(runtime, Replay(item.Record, item.Code, guest.Host));
}
[Fact]
public void ARecordedGraphicsPipelineReplaysBothStagesToTheSameSpirv()
{
var descriptor = PipelineTestGuest.BufferDescriptor(BufferAddress, 16, 4, Format32x4Float);
var descriptorWords = new BufferDescriptorWords(descriptor[0], descriptor[1], descriptor[2], descriptor[3]);
PipelineTestGuest guest;
byte[] vertexRuntime;
byte[] pixelRuntime;
using (var file = Open())
{
guest = new PipelineTestGuest(Compile);
guest.Host.ShaderCache = file;
guest.RegisterProgram(VertexCodeAddress, VertexHeaderAddress, EmbeddedFetchVertexProgram);
guest.RegisterProgram(PixelCodeAddress, PixelHeaderAddress, PipelineTestGuest.EndProgram);
var vertexOptions = new StageCompileOptions
{
VertexInfo = new VertexInputInfo
{
FetchEmbedded = true,
FetchAttributeRegister = 4,
FetchBufferRegister = 6,
Attributes = [new VertexAttributeResource(descriptorWords, 4, 4, 0, 0, 0, 0)],
Buffers = [new VertexInputBuffer(BufferAddress, 16, 4)],
},
};
var cursor = 0u;
var vertex = guest.Programs.GetOrCompile(
guest.Source(VertexCodeAddress, ShaderStage.Vertex, new uint[8]), vertexOptions, ref cursor, out _);
var pixel = guest.Programs.GetOrCompile(
guest.Source(PixelCodeAddress, ShaderStage.Pixel, []), new StageCompileOptions { PixelInfo = new PixelInputInfo() }, ref cursor, out _);
vertexRuntime = guest.Compiler.Shaders[0].Spirv;
pixelRuntime = guest.Compiler.Shaders[1].Spirv;
Assert.True(guest.Programs.TryGetStageRecord(vertex.Id, out var vertexRecord, out var vertexCode));
Assert.True(guest.Programs.TryGetStageRecord(pixel.Id, out var pixelRecord, out var pixelCode));
var rendering = new PipelineRenderingState { ColorCount = 1, DepthFormat = Format.D32Sfloat };
rendering.ColorFormats[0] = Format.R8G8B8A8Unorm;
var vertexInput = new PipelineVertexInputState { BindingCount = 1, AttributeCount = 1 };
vertexInput.Bindings[0] = new PipelineVertexBinding(16, false);
vertexInput.Attributes[0] = new PipelineVertexAttribute(0, 0);
var parameters = new PipelineStaticParameters { Topology = PrimitiveTopology.TriangleList, WithDepth = true };
file.RecordGraphics(
new GraphicsPipelineRecord
{
Vertex = vertexRecord,
Pixel = pixelRecord,
Rendering = rendering,
VertexInput = vertexInput,
StaticParameters = parameters,
Attributes = [new VertexAttributeFormat(descriptorWords, 4)],
},
vertexCode,
pixelCode);
}
using var reloaded = Open();
var item = Assert.Single(reloaded.Graphics);
Assert.Equal(vertexRuntime, Replay(item.Record.Vertex, item.VertexCode, guest.Host));
Assert.Equal(pixelRuntime, Replay(item.Record.Pixel!, item.PixelCode!, guest.Host));
Assert.Equal(Format.R8G8B8A8Unorm, item.Record.Rendering.ColorFormats[0]);
Assert.Equal(Format.D32Sfloat, item.Record.Rendering.DepthFormat);
Assert.Equal(16u, item.Record.VertexInput.Bindings[0].Stride);
Assert.True(item.Record.StaticParameters.WithDepth);
Assert.Equal(descriptorWords, Assert.Single(item.Record.Attributes).Descriptor);
Assert.Equal(ShaderCacheFile.StageIdentity(item.Record.Vertex), item.VertexIdentity);
}
[Fact]
public void TheSameCompileIsRecordedOnceAndAnotherSpecializationAddsARecord()
{
using (var file = Open())
{
CompileCompute(file, BufferDescriptorWords.Format32UInt);
CompileCompute(file, BufferDescriptorWords.Format32UInt);
CompileCompute(file, BufferDescriptorWords.Format32x4UInt);
}
using var reloaded = Open();
Assert.Equal(2, reloaded.Computes.Count);
Assert.Same(reloaded.Computes[0].Code, reloaded.Computes[1].Code);
}
[Fact]
public void ATruncatedLastRecordIsDroppedAndTheFileKeepsAppending()
{
using (var file = Open())
{
CompileCompute(file, BufferDescriptorWords.Format32UInt);
}
var path = Path.Combine(_directory, ShaderCacheFile.FileName);
var intactLength = new FileInfo(path).Length;
using (var stream = new FileStream(path, FileMode.Append))
{
stream.Write([0x40, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 2, 9, 9]);
}
using (var file = Open())
{
Assert.Single(file.Computes);
Assert.Equal(intactLength, new FileInfo(path).Length);
CompileCompute(file, BufferDescriptorWords.Format32x4UInt);
}
using var reloaded = Open();
Assert.Equal(2, reloaded.Computes.Count);
}
[Fact]
public void CompiledStagesAndBinariesAreFoundOnlyUnderTheirKeys()
{
var stage = new CompiledStageRecord
{
Spirv = [1, 2, 3, 4],
Bindings = [new LayoutBindingRecord(3, 7, 1, 32)],
VertexFetchComponents = new byte[VertexInputInfo.MaxBuffers],
};
byte[] driver = [0xAA, 0xBB];
using (var file = Open())
{
file.AddCompiledStage(11, 100, stage);
file.AddBinary(driver, 42, [new PipelineBinaryPart([9, 9], [5, 6, 7])]);
}
using var reloaded = Open();
Assert.True(reloaded.TryGetCompiledStage(11, 100, out var loaded));
Assert.Equal(stage.Spirv, loaded.Spirv);
Assert.Equal(stage.Bindings, loaded.Bindings);
Assert.False(reloaded.TryGetCompiledStage(11, 101, out _));
Assert.True(reloaded.TryReadBinary(driver, 42, out var parts));
Assert.Equal(new byte[] { 5, 6, 7 }, Assert.Single(parts).Data);
Assert.False(reloaded.TryReadBinary([0xAA, 0xBC], 42, out _));
Assert.False(reloaded.TryReadBinary(driver, 43, out _));
}
[Fact]
public void CompactionDropsStagesForeignBinariesAndStaleDoneMarkersAndKeepsPipelineRecords()
{
byte[] driver = [1];
var stage = new CompiledStageRecord { Spirv = [1], Bindings = [], VertexFetchComponents = [] };
using (var file = Open())
{
CompileCompute(file, BufferDescriptorWords.Format32UInt);
file.AddCompiledStage(1, 100, stage);
file.AddCompiledStage(2, 99, stage);
file.AddBinary(driver, 10, [new PipelineBinaryPart([1], [1])]);
file.AddBinary(driver, 11, [new PipelineBinaryPart([1], [1])]);
file.AddBinary([2], 10, [new PipelineBinaryPart([1], [1])]);
file.AddDone(5, 7);
file.AddDone(6, 8);
Assert.Null(file.Compact(100, driver, 7));
var result = file.Compact(100, driver, 7, force: true);
Assert.Equal(4, result?.RecordsDropped);
Assert.True(result?.BytesAfter < result?.BytesBefore);
Assert.Null(file.Compact(100, driver, 7, force: true));
}
using var reloaded = Open();
Assert.Single(reloaded.Computes);
Assert.False(reloaded.TryGetCompiledStage(1, 100, out _));
Assert.False(reloaded.TryGetCompiledStage(2, 99, out _));
Assert.True(reloaded.TryReadBinary(driver, 10, out _));
Assert.True(reloaded.TryReadBinary(driver, 11, out _));
Assert.False(reloaded.TryReadBinary([2], 10, out _));
Assert.Equal(2, reloaded.BinaryCount);
Assert.True(reloaded.IsDone(5, 7));
Assert.False(reloaded.IsDone(6, 8));
}
[Fact]
public void UnreferencedBinariesAreDroppedOnlyWhenTheCompactionAsksForIt()
{
byte[] driver = [1];
using (var file = Open())
{
file.AddBinary(driver, 10, [new PipelineBinaryPart([1], [1])]);
file.AddBinary(driver, 11, [new PipelineBinaryPart([1], [1])]);
file.AddBinary(driver, 12, [new PipelineBinaryPart([1], [1])]);
file.AddDone(5, 7, [10]);
file.AddDone(6, 8, [11]);
Assert.Equal(1, file.Compact(100, driver, 7, force: true)?.RecordsDropped);
Assert.Equal(3, file.BinaryCount);
}
using (var reopened = Open())
{
Assert.Equal(2, reopened.Compact(100, driver, 7, force: true, dropUnreferencedBinaries: true)?.RecordsDropped);
}
using var reloaded = Open();
Assert.True(reloaded.TryReadBinary(driver, 10, out _));
Assert.False(reloaded.TryReadBinary(driver, 11, out _));
Assert.False(reloaded.TryReadBinary(driver, 12, out _));
Assert.True(reloaded.IsDone(5, 7));
Assert.Null(reloaded.Compact(100, driver, 7, force: true, dropUnreferencedBinaries: true));
}
[Fact]
public void AShaderSeedCarriesThePlayedPipelinesWithoutTheirAddresses()
{
var seedPath = Path.Combine(_directory, "shader_seeds", "PPSA00001.seed");
StageRecord recorded;
using (var file = Open())
{
CompileCompute(file, BufferDescriptorWords.Format32UInt);
recorded = Assert.Single(file.Computes).Record;
var learned = ShaderSeed.Learn(file);
Assert.Equal(0ul, Assert.Single(learned.Computes).Address);
Assert.True(learned.TrySave(seedPath));
var items = ShaderWorkList.Build(file, true, ShaderSeed.Load(seedPath)).Items;
Assert.DoesNotContain(items, static item => item.Kind == ShaderWorkKind.SeedCompute);
}
var loaded = Assert.Single(ShaderSeed.Load(seedPath).Computes);
Assert.NotEqual(0ul, recorded.Address);
Assert.Equal(ShaderSeed.ComputeIdentity(recorded), ShaderSeed.ComputeIdentity(loaded));
Assert.Equal(recorded.Specialization, loaded.Specialization);
}
[Fact]
public void ADifferenceNamesTheFirstChangedFieldAndItsCategory()
{
var recorded = new ResourceSpecialization
{
Images = [new ImageSpecialization(ImageNumericClass.Float, ImageDimension.Dim2D, 1, 0, 0xA04, 0, 0, 0, false)],
};
var derived = new ResourceSpecialization
{
Images = [new ImageSpecialization(ImageNumericClass.Float, ImageDimension.Dim2D, 1, 0, 0xFAC, 0, 0, 0, false)],
};
var difference = new RecordDifference().Compare("Specialization", recorded, derived).ToString();
Assert.Equal("Specialization.Images[0].ShaderSwizzle 0xA04 -> 0xFAC", difference);
Assert.Equal("Specialization.Images[*].ShaderSwizzle", RecordDifference.Category(difference));
Assert.False(new RecordDifference().Compare("Specialization", recorded, recorded.Clone()).Any);
}
[Fact]
public void AFileWithAnotherHeaderStartsEmpty()
{
Directory.CreateDirectory(_directory);
File.WriteAllBytes(Path.Combine(_directory, ShaderCacheFile.FileName), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
using var file = Open();
Assert.Empty(file.Computes);
Assert.Equal(12, file.Length);
}
[Fact]
public void AnOldPrewarmListIsImportedOnceAndRemoved()
{
var source = Path.Combine(_directory, "source");
PipelineTestGuest guest;
byte[] runtime;
using (var file = ShaderCacheFile.Open(source)!)
{
(guest, runtime) = CompileCompute(file, BufferDescriptorWords.Format32UInt);
}
CachedCompute recorded;
using (var file = ShaderCacheFile.Open(source)!)
{
recorded = Assert.Single(file.Computes);
}
var legacy = Path.Combine(_directory, "shader-prewarm.bin");
var stamp = Path.Combine(_directory, "shader-prewarm.stamp");
File.WriteAllBytes(legacy, LegacyPrewarmList(recorded.Record, recorded.Code));
File.WriteAllText(stamp, "build");
using (var file = Open())
{
Assert.Equal(1, file.ImportedLegacyComputes);
var imported = Assert.Single(file.Computes);
Assert.Equal(recorded.Identity, imported.Identity);
Assert.Equal(runtime, Replay(imported.Record, imported.Code, guest.Host));
}
Assert.False(File.Exists(legacy));
Assert.False(File.Exists(stamp));
using var reloaded = Open();
Assert.Equal(0, reloaded.ImportedLegacyComputes);
Assert.Equal(recorded.Identity, Assert.Single(reloaded.Computes).Identity);
}
[Fact]
public void AnOldPrewarmListWithAnotherLayoutIsRemovedWithoutImporting()
{
Directory.CreateDirectory(_directory);
var legacy = Path.Combine(_directory, "shader-prewarm.bin");
File.WriteAllBytes(legacy, [0x53, 0x45, 0x50, 0x57, 1, 0, 0, 0, 0, 0, 0, 0, 1]);
using var file = Open();
Assert.Equal(0, file.ImportedLegacyComputes);
Assert.Empty(file.Computes);
Assert.False(File.Exists(legacy));
}
private static byte[] LegacyPrewarmList(StageRecord record, ShaderCodeCapture code)
{
using var stream = new MemoryStream();
using var writer = new BinaryWriter(stream);
writer.Write(0x57504553u);
writer.Write(1u);
writer.Write(LegacySchemaFingerprint());
WriteLegacyRecord(writer, 1, ShaderCacheSerializer.Serialize(body => ShaderCacheSerializer.WriteCode(body, code)));
WriteLegacyRecord(writer, 2, ShaderCacheSerializer.Serialize(body =>
{
body.Write(record.Hash);
body.Write(record.CodeSize);
body.Write(record.Address);
body.Write(record.UserDataBase);
body.Write(record.UserDataCount);
body.Write(record.PushDataCursor);
ShaderCacheSerializer.WriteCompute(body, record.Compute!, record.ComputeSystemRegisters);
ShaderCacheSerializer.WriteSpecialization(body, record.Specialization!);
}));
writer.Flush();
return stream.ToArray();
}
private static void WriteLegacyRecord(BinaryWriter writer, byte kind, byte[] payload)
{
var body = new byte[payload.Length + 1];
body[0] = kind;
payload.CopyTo(body, 1);
writer.Write((uint)body.Length);
writer.Write(System.IO.Hashing.XxHash3.HashToUInt64(body));
writer.Write(body);
}
private static uint LegacySchemaFingerprint()
{
var builder = new System.Text.StringBuilder();
foreach (var type in new[]
{
typeof(ComputeInputInfo), typeof(Gen5ComputeSystemRegisters), typeof(ResourceSpecialization),
typeof(BufferSpecialization), typeof(ImageSpecialization), typeof(BufferCandidateTableSpecialization),
})
{
builder.Append(type.FullName).Append('{');
foreach (var property in type.GetProperties().OrderBy(static property => property.Name, StringComparer.Ordinal))
{
builder.Append(property.PropertyType.FullName).Append(' ').Append(property.Name).Append(';');
}
builder.Append('}');
}
return (uint)System.IO.Hashing.XxHash3.HashToUInt64(System.Text.Encoding.UTF8.GetBytes(builder.ToString()));
}
}