442 lines
18 KiB
C#
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()));
|
|
}
|
|
}
|