[Tests] Cover the shader cache, static precompile, portable buffer words and fast pipeline builds

This commit is contained in:
ParantezTech 2026-10-08 02:53:32 +03:00
parent a46c92ab1d
commit 4a1e99d2fa
14 changed files with 1451 additions and 262 deletions

View File

@ -151,6 +151,40 @@ public sealed class PerGameSettingsTests
Assert.Equal(["SHARPEMU_VK_VALIDATION"], perGame.EnvironmentToggles); Assert.Equal(["SHARPEMU_VK_VALIDATION"], perGame.EnvironmentToggles);
} }
[Fact]
public void ShaderCache_FollowsTheGlobalChoiceUnlessTheGameOverridesIt()
{
var global = new GuiSettings { ShaderCache = true };
var inherited = new PerGameSettings { ShaderCache = true };
inherited.RemoveInheritedValues(global);
Assert.Null(inherited.ShaderCache);
Assert.True(inherited.IsEmpty);
Assert.True(EffectiveLaunchSettings.Resolve(global, inherited).ShaderCache);
var disabled = new PerGameSettings { ShaderCache = false };
disabled.RemoveInheritedValues(global);
Assert.False(disabled.ShaderCache);
Assert.False(EffectiveLaunchSettings.Resolve(global, disabled).ShaderCache);
Assert.False(EffectiveLaunchSettings.Resolve(new GuiSettings { ShaderCache = false }, null).ShaderCache);
}
[Fact]
public void ShaderLearn_IsOffByDefaultAndFollowsTheGameOverride()
{
var global = new GuiSettings();
Assert.False(EffectiveLaunchSettings.Resolve(global, null).ShaderLearn);
var inherited = new PerGameSettings { ShaderLearn = false };
inherited.RemoveInheritedValues(global);
Assert.Null(inherited.ShaderLearn);
Assert.True(inherited.IsEmpty);
var enabled = new PerGameSettings { ShaderLearn = true };
enabled.RemoveInheritedValues(global);
Assert.True(enabled.ShaderLearn);
Assert.True(EffectiveLaunchSettings.Resolve(global, enabled).ShaderLearn);
}
[Fact] [Fact]
public void RemoveInheritedValues_DisabledEnvironmentEntry_MatchesMissingEntry() public void RemoveInheritedValues_DisabledEnvironmentEntry_MatchesMissingEntry()
{ {

View File

@ -46,7 +46,9 @@ internal sealed class FakePipelineHost(ICpuMemory memory) : IShaderPipelineHost
public bool SharedInt64AtomicsEnabled => false; public bool SharedInt64AtomicsEnabled => false;
public ShaderPrewarmList? ShaderPrewarm { get; set; } public bool ShaderSignedZeroInfNanPreserveFloat32Supported { get; set; }
public SharpEmu.Libs.Gpu.ShaderCache.ShaderCacheFile? ShaderCache { get; set; }
public RenderHostLimits Limits => new(16384, 16384, 16384, 16384); public RenderHostLimits Limits => new(16384, 16384, 16384, 16384);

View File

@ -1,188 +0,0 @@
// 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.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.Libs.Tests.Gpu.Pipelines;
public sealed class ShaderPrewarmListTests : IDisposable
{
private const ulong CodeAddress = PipelineTestGuest.MemoryBase + 0x1_0000;
private const ulong HeaderAddress = PipelineTestGuest.MemoryBase + 0x8000;
private const ulong BufferAddress = PipelineTestGuest.MemoryBase + 0x4_0000;
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 static (IShaderPipelineHost Host, byte[] Spirv) CompileAtRuntime(ShaderPrewarmList list, uint format)
{
var guest = new PipelineTestGuest(Compile);
guest.Host.ShaderPrewarm = list;
guest.RegisterProgram(CodeAddress, HeaderAddress, PipelineTestGuest.FormatLoadProgram);
var userData = PipelineTestGuest.BufferDescriptor(BufferAddress, 4, 64, format);
var cursor = 0u;
guest.Programs.GetOrCompile(
guest.Source(CodeAddress, ShaderStage.Compute, userData), PipelineTestGuest.ComputeOptions(threadsX: 64), ref cursor, out _);
return (guest.Host, Assert.Single(guest.Compiler.Shaders).Spirv);
}
private ShaderPrewarmList Open() => ShaderPrewarmList.Open(_directory) ?? throw new InvalidOperationException("The list did not open.");
[Fact]
public void ARecordedComputeProgramCompilesToTheSameSpirvAfterReload()
{
byte[] runtime;
IShaderPipelineHost host;
using (var list = Open())
{
(host, runtime) = CompileAtRuntime(list, BufferDescriptorWords.Format32UInt);
}
using var reloaded = Open();
var (record, code) = Assert.Single(reloaded.LoadedComputes());
Assert.True(
ShaderProgramCache.TryCompilePrewarm(
record, code, new FakeShaderCompiler(Compile), host.SharedInt64AtomicsEnabled, host.ExecGuardElisionEnabled,
host.ShaderSignedZeroInfNanPreserveFloat32Supported, out var compiled, out var layout, out var error),
error);
Assert.NotNull(layout);
Assert.Equal(runtime, Assert.IsType<FakeCompiledShader>(compiled).Spirv);
}
[Fact]
public void TheSameCompileIsRecordedOnceAndAnotherSpecializationAddsARecord()
{
using (var list = Open())
{
CompileAtRuntime(list, BufferDescriptorWords.Format32UInt);
CompileAtRuntime(list, BufferDescriptorWords.Format32UInt);
CompileAtRuntime(list, BufferDescriptorWords.Format32x4UInt);
}
using var reloaded = Open();
var loaded = reloaded.LoadedComputes();
Assert.Equal(2, loaded.Count);
Assert.Same(loaded[0].Code.Ranges, loaded[1].Code.Ranges);
}
[Fact]
public void ATruncatedLastRecordIsDroppedAndTheListKeepsAppending()
{
using (var list = Open())
{
CompileAtRuntime(list, BufferDescriptorWords.Format32UInt);
}
var path = Path.Combine(_directory, ShaderPrewarmList.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 list = Open())
{
Assert.Equal(1, list.LoadedComputeCount);
Assert.Equal(intactLength, new FileInfo(path).Length);
CompileAtRuntime(list, BufferDescriptorWords.Format32x4UInt);
}
using var reloaded = Open();
Assert.Equal(2, reloaded.LoadedComputeCount);
}
[Fact]
public void TheStampMatchesOnlyTheTextLastWritten()
{
using var list = Open();
Assert.False(list.IsStampCurrent("build-a"));
list.WriteStamp("build-a");
Assert.True(list.IsStampCurrent("build-a"));
Assert.False(list.IsStampCurrent("build-b"));
}
[Fact]
public void ProgressResumesUnderTheSameStampAndClears()
{
using (var list = Open())
{
Assert.Empty(list.ReadProgress("build-a"));
list.AppendProgress("build-a", [1UL, 2UL]);
list.AppendProgress("build-a", [0xFEDC_BA98_7654_3210UL]);
}
using (var reopened = Open())
{
Assert.Equal(new HashSet<ulong> { 1UL, 2UL, 0xFEDC_BA98_7654_3210UL }, reopened.ReadProgress("build-a"));
reopened.ClearProgress();
Assert.Empty(reopened.ReadProgress("build-a"));
}
}
[Fact]
public void ProgressOfAnotherStampIsDiscarded()
{
using var list = Open();
list.AppendProgress("build-a", [7UL]);
Assert.Empty(list.ReadProgress("build-b"));
Assert.Empty(list.ReadProgress("build-a"));
list.AppendProgress("build-b", [8UL]);
Assert.Equal(new HashSet<ulong> { 8UL }, list.ReadProgress("build-b"));
}
[Fact]
public void ARecordIdentityDependsOnItsSpecialization()
{
using (var list = Open())
{
_ = CompileAtRuntime(list, BufferDescriptorWords.Format32UInt);
_ = CompileAtRuntime(list, BufferDescriptorWords.Format32x4UInt);
}
using var reloaded = Open();
var identities = reloaded.LoadedComputes().Select(item => ShaderPrewarmList.Identity(item.Record)).ToArray();
Assert.Equal(2, identities.Length);
Assert.NotEqual(identities[0], identities[1]);
Assert.Equal(identities[0], ShaderPrewarmList.Identity(reloaded.LoadedComputes()[0].Record));
}
[Fact]
public void RecordedReadsReplayAsMergedRanges()
{
var memory = new FakeCpuMemory(PipelineTestGuest.MemoryBase, 0x1000);
Assert.True(memory.TryWrite(PipelineTestGuest.MemoryBase + 0x100, [1, 2, 3, 4, 5, 6, 7, 8]));
var recording = new RecordingCpuMemory(memory);
Span<byte> word = stackalloc byte[4];
Assert.True(recording.TryRead(PipelineTestGuest.MemoryBase + 0x104, word));
Assert.True(recording.TryRead(PipelineTestGuest.MemoryBase + 0x100, word));
Assert.True(recording.TryRead(PipelineTestGuest.MemoryBase + 0x102, word));
var range = Assert.Single(recording.TakeRanges());
Assert.Equal(PipelineTestGuest.MemoryBase + 0x100, range.Address);
Assert.Equal(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }, range.Bytes);
var replay = new ReplayCpuMemory([range]);
Assert.True(replay.TryRead(PipelineTestGuest.MemoryBase + 0x102, word));
Assert.Equal(new byte[] { 3, 4, 5, 6 }, word.ToArray());
Assert.False(replay.TryRead(PipelineTestGuest.MemoryBase + 0x106, word));
}
}

View File

@ -0,0 +1,110 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.Libs.Gpu.ShaderCache;
namespace SharpEmu.Libs.Tests.Gpu.ShaderCache;
internal static class AgcHeaderBuilder
{
private const int SpecialsBytes = 48;
public static readonly int[] PointerFields = [24, 32, 40, 48, 56];
public static byte[] Build(
byte type,
int codeSize,
(uint Offset, uint Value)[]? shaderRegisters = null,
(uint Offset, uint Value)[]? contextRegisters = null,
uint[]? inputSemantics = null,
uint[]? outputSemantics = null,
uint dispatchModifier = 0,
ushort scratchDwords = 0)
{
shaderRegisters ??= [];
contextRegisters ??= [];
inputSemantics ??= [];
outputSemantics ??= [];
var shaderStart = AgcShaderHeader.FixedBytes;
var contextStart = shaderStart + shaderRegisters.Length * 8;
var specialsStart = contextStart + contextRegisters.Length * 8;
var inputStart = specialsStart + SpecialsBytes;
var outputStart = inputStart + inputSemantics.Length * 4;
var size = outputStart + outputSemantics.Length * 4;
var header = new byte[size];
AgcShaderHeader.Signature.CopyTo(header);
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(64), (uint)size);
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(68), (uint)codeSize);
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(80), (uint)inputSemantics.Length);
BinaryPrimitives.WriteUInt16LittleEndian(header.AsSpan(84), scratchDwords);
BinaryPrimitives.WriteUInt16LittleEndian(header.AsSpan(86), (ushort)outputSemantics.Length);
BinaryPrimitives.WriteUInt16LittleEndian(header.AsSpan(88), SpecialsBytes);
header[90] = type;
header[91] = (byte)contextRegisters.Length;
header[92] = (byte)shaderRegisters.Length;
void Pointer(int field, int target, bool present)
{
if (present)
{
BinaryPrimitives.WriteInt64LittleEndian(header.AsSpan(field), target - field);
}
}
Pointer(32, shaderStart, shaderRegisters.Length != 0);
Pointer(24, contextStart, contextRegisters.Length != 0);
Pointer(40, specialsStart, true);
Pointer(48, inputStart, inputSemantics.Length != 0);
Pointer(56, outputStart, outputSemantics.Length != 0);
for (var index = 0; index < shaderRegisters.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(shaderStart + index * 8), shaderRegisters[index].Offset);
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(shaderStart + index * 8 + 4), shaderRegisters[index].Value);
}
for (var index = 0; index < contextRegisters.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(contextStart + index * 8), contextRegisters[index].Offset);
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(contextStart + index * 8 + 4), contextRegisters[index].Value);
}
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(specialsStart + 16), dispatchModifier);
for (var index = 0; index < inputSemantics.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(inputStart + index * 4), inputSemantics[index]);
}
for (var index = 0; index < outputSemantics.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(outputStart + index * 4), outputSemantics[index]);
}
return header;
}
public static byte[] WithAbsolutePointers(byte[] header, ulong headerAddress)
{
var absolute = (byte[])header.Clone();
foreach (var field in PointerFields)
{
var relative = BinaryPrimitives.ReadInt64LittleEndian(absolute.AsSpan(field));
if (relative != 0)
{
BinaryPrimitives.WriteUInt64LittleEndian(absolute.AsSpan(field), headerAddress + (ulong)(field + relative));
}
}
return absolute;
}
public static byte[] Code(uint[] words)
{
var code = new byte[words.Length * sizeof(uint)];
for (var index = 0; index < words.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(code.AsSpan(index * sizeof(uint)), words[index]);
}
return code;
}
}

View File

@ -0,0 +1,441 @@
// 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()));
}
}

View File

@ -0,0 +1,53 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.ShaderCache;
using Xunit;
namespace SharpEmu.Libs.Tests.Gpu.ShaderCache;
public sealed class ShaderCacheSettingsTests
{
[Theory]
[InlineData("on", true)]
[InlineData("1", true)]
[InlineData("True", true)]
[InlineData("OFF", false)]
[InlineData("0", false)]
[InlineData("disabled", false)]
public void KnownValuesParse(string value, bool expected)
{
Assert.True(ShaderCacheSettings.TryParse(value, out var enabled));
Assert.Equal(expected, enabled);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("sometimes")]
public void UnknownValuesDoNotParse(string? value) => Assert.False(ShaderCacheSettings.TryParse(value, out _));
[Fact]
public void TheCacheIsOnUnlessAValueOrTheLegacySwitchTurnsItOff()
{
Assert.True(ShaderCacheSettings.Resolve(null));
Assert.False(ShaderCacheSettings.Resolve(null, "0"));
Assert.True(ShaderCacheSettings.Resolve("on", "0"));
Assert.False(ShaderCacheSettings.Resolve("off"));
}
[Fact]
public void TheFormatRoundTrips()
{
Assert.True(ShaderCacheSettings.TryParse(ShaderCacheSettings.Format(true), out var on) && on);
Assert.True(ShaderCacheSettings.TryParse(ShaderCacheSettings.Format(false), out var off) && !off);
}
[Fact]
public void TheCacheFileLivesUnderTheTitleFolder()
{
var path = ShaderCacheSettings.CacheFilePath("PPSA01341");
Assert.Equal(ShaderCacheFile.FileName, Path.GetFileName(path));
Assert.Equal("PPSA01341", Path.GetFileName(Path.GetDirectoryName(path)));
}
}

View File

@ -0,0 +1,345 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
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 Xunit;
namespace SharpEmu.Libs.Tests.Gpu.ShaderCache;
public sealed class ShaderPrecompileTests : IDisposable
{
private const byte ComputeType = 0;
private const byte PixelType = 1;
private const byte GeometryType = 2;
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 ShaderCacheFile Open() => ShaderCacheFile.Open(_directory) ?? throw new InvalidOperationException("The shader cache did not open.");
private static byte[] ProgramCode => AgcHeaderBuilder.Code(PipelineTestGuest.FormatLoadProgram);
private static CachedProgram Program(byte[] header, ulong address = GameShaderScanner.ScannedCodeAddress)
{
Assert.True(ShaderInventory.TryCreateCapture(ProgramCode, (uint)ProgramCode.Length, address, out var capture));
Assert.True(AgcShaderHeader.TryParse(header, 0, out var parsed));
var program = new InventoryProgram { Code = capture.Key, Source = InventorySource.Scanned, Header = parsed.Bytes };
return new CachedProgram(program, capture, ShaderCacheFile.ProgramIdentity(program));
}
[Fact]
public void AnAgcHeaderExposesItsRegistersSemanticsAndDispatchModifier()
{
var bytes = AgcHeaderBuilder.Build(
ComputeType, 12,
shaderRegisters: [(0x207, 64), (0x213, 0x90)],
contextRegisters: [(0x1B3, 2)],
inputSemantics: [0x11, 0x22],
outputSemantics: [0x33],
dispatchModifier: 0x8000);
Assert.True(AgcShaderHeader.TryParse(bytes, 0, out var header));
Assert.Equal(AgcShaderType.Compute, header.Type);
Assert.Equal(12u, header.CodeSize);
Assert.Equal([new AgcRegister(0x207, 64), new AgcRegister(0x213, 0x90)], header.ShaderRegisters);
Assert.Equal([new AgcRegister(0x1B3, 2)], header.ContextRegisters);
Assert.Equal(new uint[] { 0x11, 0x22 }, header.InputSemantics);
Assert.Equal(new uint[] { 0x33 }, header.OutputSemantics);
Assert.Equal(0x8000u, header.DispatchModifier);
}
[Fact]
public void ARelocatedHeaderNormalizesToTheFileLayout()
{
const ulong headerAddress = 0x7_0000_1000;
var bytes = AgcHeaderBuilder.Build(PixelType, 12, contextRegisters: [(0x1B3, 2)], inputSemantics: [0x11]);
var relocated = AgcHeaderBuilder.WithAbsolutePointers(bytes, headerAddress);
Assert.True(AgcShaderHeader.TryParse(relocated, headerAddress, out var header));
Assert.True(AgcShaderHeader.TryParse(bytes, 0, out var original));
Assert.Equal(original.Bytes, header.Bytes);
}
[Fact]
public void AHeaderWithAForeignSignatureOrSizeIsRejected()
{
var bytes = AgcHeaderBuilder.Build(ComputeType, 12);
bytes[0] = 0;
Assert.False(AgcShaderHeader.TryParse(bytes, 0, out _));
var wrongSize = AgcHeaderBuilder.Build(ComputeType, 12);
Assert.False(AgcShaderHeader.TryParse(wrongSize.AsSpan(0, wrongSize.Length - 4), 0, out _));
}
[Fact]
public void AStaticComputeRecordReadsTheDispatchShapeFromTheHeader()
{
var resource2 = (4u << 1) | (1u << 7) | (2u << 11) | (3u << 15);
var program = Program(AgcHeaderBuilder.Build(
ComputeType, ProgramCode.Length,
shaderRegisters: [(0x207, 64), (0x208, 2), (0x209, 1), (0x213, resource2)],
dispatchModifier: 1u << 15,
scratchDwords: 5));
Assert.True(StaticStageInputs.TryCompute(program, computeWave64Supported: false, out var record));
var info = Assert.IsType<ComputeInputInfo>(record.Compute);
Assert.Equal(ShaderStage.Compute, record.Stage);
Assert.Equal(4u, record.UserDataCount);
Assert.Null(record.Specialization);
Assert.Equal((64u, 2u, 1u), (info.ThreadsX, info.ThreadsY, info.ThreadsZ));
Assert.Equal(32u, info.WaveSize);
Assert.Equal(3u * 128u, info.LocalDataShareDwords);
Assert.Equal(5u, info.ScratchDwords);
Assert.Equal(3, info.ThreadIdCount);
Assert.True(info.GroupIdX);
Assert.True(info.NeedsLocalDataShareBarriers);
Assert.Equal(4u, record.ComputeSystemRegisters!.Value.WorkGroupXRegister);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void AStaticComputeRecordReplaysWithTheDefaultSpecialization(bool preserveFloat32)
{
var program = Program(AgcHeaderBuilder.Build(ComputeType, ProgramCode.Length, shaderRegisters: [(0x207, 64), (0x213, 4u << 1)]));
Assert.True(StaticStageInputs.TryCompute(program, computeWave64Supported: false, out var record));
var host = new FakePipelineHost(new ReplayCpuMemory([]))
{
ShaderSignedZeroInfNanPreserveFloat32Supported = preserveFloat32,
};
Assert.True(ShaderProgramCache.TryReplay(
record, program.Code, new FakeShaderCompiler(static _ => new byte[] { 1, 2, 3, 4 }), ShaderCompileHostFlags.From(host),
out var compiled, out var info, out var error), error);
Assert.Equal(preserveFloat32, Assert.IsType<FakeCompiledShader>(compiled).Request.ShaderSignedZeroInfNanPreserveFloat32Supported);
Assert.Single(info!.Buffers);
}
[Fact]
public void TheScannerFindsCodeBeforeEachHeaderAndPairsAdjacentStages()
{
var game = Path.Combine(_directory, "game");
Directory.CreateDirectory(game);
var code = ProgramCode;
var bundle = new MemoryStream();
foreach (var (type, semantics) in new[] { (ComputeType, 0u), (GeometryType, 1u), (PixelType, 2u) })
{
bundle.Write(code);
bundle.Write(AgcHeaderBuilder.Build(type, code.Length, inputSemantics: type == PixelType ? [semantics] : [], outputSemantics: type == GeometryType ? [semantics] : []));
}
File.WriteAllBytes(Path.Combine(game, "shaders.csdr"), bundle.ToArray());
File.WriteAllBytes(Path.Combine(game, "movie.mp4"), bundle.ToArray());
using (var file = Open())
{
var result = GameShaderScanner.Scan(game, file, null, CancellationToken.None);
Assert.Equal(1, result.FilesScanned);
Assert.Equal(3, result.Programs);
Assert.Equal(3, file.Programs.Count);
var (vertex, pixel) = Assert.Single(file.Pairs);
Assert.True(file.TryGetProgram(vertex, out var vertexProgram));
Assert.True(file.TryGetProgram(pixel, out var pixelProgram));
Assert.True(StaticStageInputs.TryParse(vertexProgram, out var vertexHeader) && vertexHeader.Type == AgcShaderType.Geometry);
Assert.True(StaticStageInputs.TryParse(pixelProgram, out var pixelHeader) && pixelHeader.Type == AgcShaderType.Pixel);
}
using var reopened = Open();
var again = GameShaderScanner.Scan(game, reopened, null, CancellationToken.None);
Assert.Equal(0, again.FilesScanned);
Assert.Equal(1, again.FilesSkipped);
Assert.Equal(3, reopened.Programs.Count);
}
[Fact]
public void InventoryPairsScansAndDoneMarkersSurviveAReopen()
{
var compute = Program(AgcHeaderBuilder.Build(ComputeType, ProgramCode.Length, shaderRegisters: [(0x207, 64)]));
using (var file = Open())
{
Assert.Equal(compute.Identity, file.RecordProgram(compute.Program, compute.Code));
Assert.Null(file.RecordProgram(compute.Program, compute.Code));
file.RecordPair(1, 2);
file.RecordPair(1, 2);
file.RecordScannedFile(new ScannedFile("a/b.csdr", 10, 20, 1, 1));
file.RecordScannedFile(new ScannedFile("a/b.csdr", 11, 21, 2, 1));
file.AddDone(7, 9);
}
using var reopened = Open();
Assert.True(reopened.TryGetProgram(compute.Identity, out var program));
Assert.Equal(compute.Program.Header, program.Program.Header);
Assert.Equal([(1ul, 2ul)], reopened.Pairs);
Assert.True(reopened.TryGetScannedFile("a/b.csdr", out var scanned));
Assert.Equal(new ScannedFile("a/b.csdr", 11, 21, 2, 1), scanned);
Assert.True(reopened.IsDone(7, 9));
Assert.False(reopened.IsDone(7, 10));
}
[Fact]
public void ASnapshotReadsUpToItsLengthAndNeverWrites()
{
long length;
string path;
using (var file = Open())
{
file.AddDone(1, 5);
length = file.Length;
file.AddDone(2, 5);
path = file.Path;
}
using (var snapshot = ShaderCacheFile.OpenSnapshot(path, length)!)
{
Assert.True(snapshot.IsDone(1, 5));
Assert.False(snapshot.IsDone(2, 5));
snapshot.AddDone(3, 5);
Assert.False(snapshot.IsDone(3, 5));
}
using var reopened = Open();
Assert.True(reopened.IsDone(2, 5));
Assert.False(reopened.IsDone(3, 5));
}
[Fact]
public void AWorkerPartMergesItsStagesBinariesAndDoneMarkers()
{
var stage = new CompiledStageRecord { Spirv = [1, 2], Bindings = [], VertexFetchComponents = [], PushDataEnd = 6 };
var partPath = Path.Combine(_directory, "worker.part");
Directory.CreateDirectory(_directory);
using (var part = ShaderCacheFile.OpenPart(partPath)!)
{
part.AddCompiledStage(3, 4, stage);
part.AddBinary([9], 42, [new PipelineBinaryPart([1], [7, 7])]);
part.AddDone(5, 6);
}
using (var file = Open())
{
Assert.Equal(3, file.MergePart(partPath));
Assert.Equal(0, file.MergePart(partPath));
}
using var reopened = Open();
Assert.True(reopened.TryGetCompiledStage(3, 4, out var merged));
Assert.Equal(6u, merged.PushDataEnd);
Assert.True(reopened.HasBinary([9], 42));
Assert.True(reopened.TryReadBinary([9], 42, out var parts));
Assert.Equal(new byte[] { 7, 7 }, Assert.Single(parts).Data);
Assert.True(reopened.IsDone(5, 6));
}
[Fact]
public void ADamagedBinaryIsNotHandedOut()
{
string path;
using (var file = Open())
{
file.AddBinary([9], 42, [new PipelineBinaryPart([1], [7, 7, 7, 7])]);
path = file.Path;
}
var bytes = File.ReadAllBytes(path);
bytes[^1] ^= 0xFF;
File.WriteAllBytes(path, bytes);
using var reopened = Open();
Assert.True(reopened.HasBinary([9], 42));
Assert.False(reopened.TryReadBinary([9], 42, out _));
}
[Fact]
public void APixelExportTargetSurvivesTheStageRecord()
{
var record = new StageRecord
{
Stage = ShaderStage.Pixel,
Hash = 1,
CodeSize = 12,
Address = 0x1000,
UserDataBase = 0,
UserDataCount = 0,
PushDataCursor = 0,
Pixel = new PixelStageInputs(0, [new Gen5PixelOutputBinding(2, 0, Gen5PixelOutputKind.Float) { ExportTarget = 0 }], 2, 0, 2, [0]),
};
var payload = ShaderCacheSerializer.Serialize(writer => ShaderCacheSerializer.WriteStage(writer, record));
var read = ShaderCacheSerializer.Deserialize(payload, ShaderCacheSerializer.ReadStage);
var output = Assert.Single(read.Pixel!.Outputs);
Assert.Equal(2u, output.GuestSlot);
Assert.Equal(0u, output.ExportTarget);
}
[Fact]
public void AShaderSeedKeepsStatesAndPairsWithoutShaderCode()
{
var state = ShaderSeed.CreateState(
new PipelineRenderingState { ColorCount = 1 },
new PipelineStaticParameters { ColorCount = 1 },
[new Gen5PixelOutputBinding(0, 0, Gen5PixelOutputKind.Float)],
default);
var seedPath = Path.Combine(_directory, "shader_seeds", "PPSA00001.seed");
var pixel = Program(AgcHeaderBuilder.Build(PixelType, ProgramCode.Length, contextRegisters: [(0x1C5, 4)]));
Assert.True(StaticStageInputs.TryParse(pixel, out var pixelHeader));
var empty = new ShaderSeed();
Assert.True(empty.IsEmpty);
using (var file = Open())
{
file.RecordPair(11, 22);
var learned = ShaderSeed.Learn(file);
Assert.Equal(new[] { (11ul, 22ul) }, learned.Pairs);
Assert.True(learned.TrySave(seedPath));
}
var loaded = ShaderSeed.Load(seedPath);
Assert.Equal(new[] { (11ul, 22ul) }, loaded.Pairs);
Assert.Empty(loaded.Palette(StaticStageInputs.PixelSignature(pixelHeader), 8));
Assert.NotEqual(0ul, state.Identity);
Assert.Equal(StaticStageInputs.PixelSignature(pixelHeader), StaticStageInputs.PixelSignature(pixelHeader));
}
[Fact]
public void PortableWordsCarryTheStrideAndFormatAndDestinationSelect()
{
var descriptor = PipelineTestGuest.BufferDescriptor(0x1000, 48, 4, BufferDescriptorWords.Format32x4UInt);
var word = PortableBufferWord.Pack(descriptor);
Assert.Equal(48u, word.Stride);
Assert.Equal(BufferDescriptorWords.Format32x4UInt, (word.DescriptorWord3 >> 12) & 0x7F);
Assert.Equal(descriptor[3] & 0xFFF, word.DescriptorWord3 & 0xFFF);
descriptor[3] |= 1u << 30;
Assert.Equal((0u, 0u), PortableBufferWord.Pack(descriptor));
}
[Fact]
public void APortableLayoutPlacesTwoWordsPerBufferAfterTheMemoryOffsets()
{
var info = new ShaderResourceInfo();
for (var index = 0; index < 5; index++)
{
info.Buffers.Add(new BufferResource { Read = true });
}
var plain = BindingLayout.Allocate(info, [], false, false, false, usesDispatchThreadLimits: true);
var portable = BindingLayout.Allocate(info, [], false, false, false, usesDispatchThreadLimits: true, portableBuffers: true);
Assert.Equal(0u, plain.BufferWordCount);
Assert.Equal(10u, portable.BufferWordCount);
Assert.Equal(plain.DispatchThreadLimitsDword, portable.BufferWordDword);
Assert.Equal(portable.BufferWordDword + 10, portable.DispatchThreadLimitsDword);
Assert.Equal(plain.ShaderDataDwordCount + 10, portable.ShaderDataDwordCount);
Assert.NotEqual(plain, portable);
}
}

View File

@ -181,7 +181,8 @@ internal sealed unsafe class LayoutComputeRunner : IDisposable
uint[]? flattenedTable = null, uint[]? flattenedTable = null,
IReadOnlyDictionary<DescriptorBindingKind, DescriptorImageInfo[]>? boundImages = null, IReadOnlyDictionary<DescriptorBindingKind, DescriptorImageInfo[]>? boundImages = null,
ulong shaderBase = 0, ulong shaderBase = 0,
uint[]? dispatchThreadLimits = null) uint[]? dispatchThreadLimits = null,
uint[]? bufferWords = null)
{ {
var vk = _harness.Vk; var vk = _harness.Vk;
var device = _harness.Device.Device; var device = _harness.Device.Device;
@ -199,6 +200,12 @@ internal sealed unsafe class LayoutComputeRunner : IDisposable
shaderData[layout.ShaderBaseDword + 1] = (uint)(shaderBase >> 32); shaderData[layout.ShaderBaseDword + 1] = (uint)(shaderBase >> 32);
} }
if (bufferWords is not null)
{
if (bufferWords.Length != layout.BufferWordCount) throw new InvalidOperationException($"The dispatch requires {layout.BufferWordCount} buffer words.");
bufferWords.CopyTo(shaderData, (int)layout.BufferWordDword);
}
if (layout.UsesDispatchThreadLimits) if (layout.UsesDispatchThreadLimits)
{ {
if (dispatchThreadLimits is not { Length: 3 }) throw new InvalidOperationException("The dispatch requires three thread limits."); if (dispatchThreadLimits is not { Length: 3 }) throw new InvalidOperationException("The dispatch requires three thread limits.");

View File

@ -0,0 +1,99 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
namespace SharpEmu.Libs.Tests.Gpu.Vulkan;
public sealed class PortableBufferFormatDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture<HeadlessVulkanFixture>
{
private const uint MaxFloatUlps = 4;
private static readonly uint[] Swizzles = [0xFAC, 0x3AC, 0xA04, 0xF2E, 0x249];
[Fact]
public void RuntimeDescriptorWordsReadEveryFormatLikeABakedDescriptor()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan)) return;
var program = Program(BufferLoad(0, 0, dwords: 4, formatted: true), BufferStore(8, 4, dwords: 4), EndProgram(16));
var plan = Extract(program);
var portable = Compile(plan, program, ResourceSpecialization.Default(plan.Info), portableBuffers: true);
var source = Enumerable.Range(0, 64).Select(static index => (byte)(index * 37 + 11)).ToArray();
using var harness = new ImageTestHarness(vulkan!);
for (uint format = 1; format < 128; format++)
{
if (!Gfx10UnifiedFormat.TryDecode(format, out var dataFormat, out var numberFormat) || dataFormat == 0)
{
continue;
}
foreach (var swizzle in Swizzles)
{
var specialization = ResourceSpecialization.Default(plan.Info);
specialization.Buffers[0] = new BufferSpecialization(0, format, swizzle);
var baked = Run(harness, Compile(plan, program, specialization, portableBuffers: false), source, null);
var runtime = Run(harness, portable, source, [0, (format << 12) | swizzle, 0, 0]);
for (var component = 0; component < 4; component++)
{
var expected = BinaryPrimitives.ReadUInt32LittleEndian(baked.AsSpan(component * sizeof(uint)));
var actual = BinaryPrimitives.ReadUInt32LittleEndian(runtime.AsSpan(component * sizeof(uint)));
Assert.True(Matches(expected, actual, numberFormat),
$"format {format} swizzle 0x{swizzle:X3} component {component}: baked 0x{expected:X8}, runtime 0x{actual:X8}");
}
}
}
harness.AssertNoValidationMessages();
}
private static bool Matches(uint expected, uint actual, uint numberFormat)
{
if (expected == actual)
{
return true;
}
if (numberFormat is 4 or 5 || float.IsNaN(BitConverter.UInt32BitsToSingle(expected)) != float.IsNaN(BitConverter.UInt32BitsToSingle(actual)))
{
return false;
}
static long Ordered(uint bits) => (bits & 0x8000_0000) != 0 ? -(long)(bits & 0x7FFF_FFFF) : bits;
return Math.Abs(Ordered(expected) - Ordered(actual)) <= MaxFloatUlps;
}
private static (ShaderCompileRequest Request, byte[] Spirv) Compile(
ShaderResourcePlan plan, Gen5ShaderProgram program, ResourceSpecialization specialization, bool portableBuffers)
{
var resources = ResourceMaterializer.ApplyTo(plan, specialization);
var layout = BindingLayout.Allocate(
resources.Info,
BindingLayout.CollectUserDataRegisters(program, 0, 64),
BindingLayout.UsesGlobalDataShare(program),
ShaderCompileRequest.RequiresFlattenedTable(plan, resources),
BindingLayout.ReadsShaderBase(program),
portableBuffers: portableBuffers);
var request = new ShaderCompileRequest(plan, resources, layout) { LocalSizeX = 1, ThreadCountX = 1, WaveSize = 32 };
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
return (request, shader.Spirv);
}
private static byte[] Run(ImageTestHarness harness, (ShaderCompileRequest Request, byte[] Spirv) shader, byte[] source, uint[]? bufferWords)
{
using var runner = new LayoutComputeRunner(harness, shader.Request, shader.Spirv);
var input = runner.CreateBuffer(source);
var output = runner.CreateBuffer(16);
harness.Run(() => runner.Dispatch(new uint[64],
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [input, output] }, 1,
bufferWords: bufferWords));
return runner.ReadBack(output, 0, 16);
}
}

View File

@ -10,6 +10,7 @@ using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Gpu.Pipelines; using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering; using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Gpu.Scheduling; using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.Libs.Gpu.ShaderCache;
using SharpEmu.Libs.Tests.Gpu.Buffers; using SharpEmu.Libs.Tests.Gpu.Buffers;
using SharpEmu.Libs.Tests.Gpu.Vulkan; using SharpEmu.Libs.Tests.Gpu.Vulkan;
using SharpEmu.Libs.VideoOut; using SharpEmu.Libs.VideoOut;
@ -79,6 +80,8 @@ internal sealed class PresenterUnderTest : IDisposable
"_deferredGuestImageVersionDestroys", "_deferredGuestImageVersionDestroys",
"_pipelineEntries", "_shaderModules", "_shaderModuleSpirvBytes", "_pipelineEntries", "_shaderModules", "_shaderModuleSpirvBytes",
"_preparedTextures", "_barriersAfterRendering", "_feedbackSnapshotPool", "_preparedTextures", "_barriersAfterRendering", "_feedbackSnapshotPool",
"_warmStages", "_shaderCacheWorkers",
"_pipelineOptimizationGate", "_pipelineOptimizationQueue", "_pipelineOptimizations",
}) })
{ {
var field = PresenterType.GetField(name, InstanceMembers)!; var field = PresenterType.GetField(name, InstanceMembers)!;
@ -86,6 +89,8 @@ internal sealed class PresenterUnderTest : IDisposable
} }
SetField("_shaderModuleCacheIdentities", new Dictionary<ulong, string>()); SetField("_shaderModuleCacheIdentities", new Dictionary<ulong, string>());
SetField("_shaderCacheCancel", new CancellationTokenSource());
SetField("_pipelineStoreSources", Array.Empty<ShaderCacheFile>());
var cacheShards = PresenterType.GetField("_pipelineCacheShards", InstanceMembers)!; var cacheShards = PresenterType.GetField("_pipelineCacheShards", InstanceMembers)!;
cacheShards.SetValue(Instance, Activator.CreateInstance(cacheShards.FieldType, nonPublic: true)); cacheShards.SetValue(Instance, Activator.CreateInstance(cacheShards.FieldType, nonPublic: true));
forwarder.Target = this; forwarder.Target = this;
@ -156,6 +161,7 @@ internal sealed class PresenterUnderTest : IDisposable
private void DestroyPipelineResources() private void DestroyPipelineResources()
{ {
InvokeMethod("StopPipelineOptimization");
InvokeMethod("DestroyRenderPipelines"); InvokeMethod("DestroyRenderPipelines");
GetField<DescriptorHeap>("_descriptorHeap").Dispose(); GetField<DescriptorHeap>("_descriptorHeap").Dispose();
} }

View File

@ -0,0 +1,140 @@
// 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.Images;
using SharpEmu.Libs.Tests.Gpu.ShaderCache;
using SharpEmu.Libs.Tests.Gpu.Pipelines;
using SharpEmu.Libs.Tests.Gpu.Vulkan;
using SharpEmu.Libs.VideoOut;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using System.Collections.Concurrent;
using Silk.NET.Vulkan;
using Xunit;
namespace SharpEmu.Libs.Tests.VideoOut;
public sealed class ShaderCachePresenterTests(HeadlessVulkanFixture fixture) : IClassFixture<HeadlessVulkanFixture>, IDisposable
{
private const ulong CodeAddress = PipelineTestGuest.MemoryBase + 0x1_0000;
private const ulong HeaderAddress = PipelineTestGuest.MemoryBase + 0x8000;
private const ulong BufferAddress = PipelineTestGuest.MemoryBase + 0x4_0000;
private const ulong Stamp = 42;
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 PipelineTestGuest RecordCompute()
{
using var file = ShaderCacheFile.Open(_directory)!;
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, BufferDescriptorWords.Format32UInt)),
PipelineTestGuest.ComputeOptions(threadsX: 64), ref cursor, out _);
return guest;
}
private static PresenterUnderTest Presenter(HeadlessVulkan vulkan, ShaderCacheFile file, PipelineTestGuest guest)
{
var presenter = new PresenterUnderTest(vulkan);
presenter.SetField("_shaderCacheStamp", Stamp);
presenter.SetField("_shaderCacheFlags", ShaderCompileHostFlags.From(guest.Host));
presenter.InvokeMethod("UseShaderCacheStores", file, new[] { file });
return presenter;
}
[Fact]
public async Task AFastComputeBuildIsReplacedByACompletedNativeOptimization()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan)) return;
var guest = RecordCompute();
using var file = ShaderCacheFile.Open(_directory)!;
var item = Assert.Single(file.Computes);
using var presenter = Presenter(vulkan!, file, guest);
presenter.SetField("_physicalDeviceVendorId", 0x10DEu);
presenter.SetField("_fastPipelineBuild", true);
Assert.True(ShaderProgramCache.TryReplay(item.Record, item.Code, guest.Compiler, ShaderCompileHostFlags.From(guest.Host),
out var compiled, out var info, out var error), error);
presenter.Run(() =>
{
var host = (IShaderPipelineHost)presenter.Instance;
var module = host.CreateShaderModule(compiled!, ShaderStage.Compute, item.Record.Hash, 1);
host.CreateComputePipeline(new ComputePipelineDescription
{
Input = item.Record.Compute!,
Program = new ShaderProgram(1, module),
Stage = info!,
});
});
var pending = Assert.Single(presenter.GetField<ConcurrentDictionary<ulong, Task<Pipeline>>>("_pipelineOptimizations"));
var optimized = await pending.Value.WaitAsync(TimeSpan.FromSeconds(10));
Assert.NotEqual(0ul, optimized.Handle);
Assert.NotEqual(pending.Key, optimized.Handle);
Assert.Equal(optimized.Handle, ((Pipeline)presenter.InvokeMethod("ResolveOptimizedPipeline", new Pipeline(pending.Key))!).Handle);
vulkan!.AssertNoValidationMessages();
}
[Fact]
public void ARecordedComputeIsPrecompiledWithoutGrowingTheFile()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan)) return;
var guest = RecordCompute();
using var file = ShaderCacheFile.Open(_directory)!;
var item = Assert.Single(file.Computes);
var lengthBefore = file.Length;
using var presenter = Presenter(vulkan!, file, guest);
Assert.Equal(ShaderCacheOutcome.Compiled, presenter.InvokeMethod("CompileComputeItem", item.Record, item.Code));
Assert.Equal(ShaderCacheOutcome.Compiled, presenter.InvokeMethod("CompileComputeItem", item.Record, item.Code));
Assert.Equal(lengthBefore, file.Length);
presenter.InvokeMethod("StopShaderCache");
vulkan!.AssertNoValidationMessages();
}
[Fact]
public void AStaticComputeFromItsHeaderCompilesOnTheDevice()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan)) return;
var guest = RecordCompute();
using var file = ShaderCacheFile.Open(_directory)!;
var code = AgcHeaderBuilder.Code(PipelineTestGuest.FormatLoadProgram);
Assert.True(ShaderInventory.TryCreateCapture(code, (uint)code.Length, GameShaderScanner.ScannedCodeAddress, out var capture));
var header = AgcHeaderBuilder.Build(0, code.Length, shaderRegisters: [(0x207, 64), (0x213, 4u << 1)]);
var program = new InventoryProgram { Code = capture.Key, Source = InventorySource.Scanned, Header = header };
var identity = file.RecordProgram(program, capture)!.Value;
Assert.True(file.TryGetProgram(identity, out var cached));
Assert.True(StaticStageInputs.TryCompute(cached, computeWave64Supported: false, out var record));
using var presenter = Presenter(vulkan!, file, guest);
Assert.Equal(ShaderCacheOutcome.Compiled, presenter.InvokeMethod("CompileComputeItem", record, capture));
vulkan!.AssertNoValidationMessages();
}
}

View File

@ -1,72 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.Libs.Tests.Gpu.Pipelines;
using SharpEmu.Libs.Tests.Gpu.Vulkan;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
namespace SharpEmu.Libs.Tests.VideoOut;
public sealed class ShaderPrewarmPresenterTests(HeadlessVulkanFixture fixture) : IClassFixture<HeadlessVulkanFixture>, IDisposable
{
private const ulong CodeAddress = PipelineTestGuest.MemoryBase + 0x1_0000;
private const ulong HeaderAddress = PipelineTestGuest.MemoryBase + 0x8000;
private const ulong BufferAddress = PipelineTestGuest.MemoryBase + 0x4_0000;
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;
}
[Fact]
public void APrewarmRecordBuildsTheDevicePipeline()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan)) return;
byte[] runtime;
using (var list = ShaderPrewarmList.Open(_directory)!)
{
var guest = new PipelineTestGuest(Compile);
guest.Host.ShaderPrewarm = list;
guest.RegisterProgram(CodeAddress, HeaderAddress, PipelineTestGuest.FormatLoadProgram);
var cursor = 0u;
guest.Programs.GetOrCompile(
guest.Source(CodeAddress, ShaderStage.Compute,
PipelineTestGuest.BufferDescriptor(BufferAddress, 4, 64, BufferDescriptorWords.Format32UInt)),
PipelineTestGuest.ComputeOptions(threadsX: 64), ref cursor, out _);
runtime = Assert.Single(guest.Compiler.Shaders).Spirv;
}
using var reloaded = ShaderPrewarmList.Open(_directory)!;
var (record, code) = Assert.Single(reloaded.LoadedComputes());
using var presenter = new PresenterUnderTest(vulkan);
var prewarmed = new ConcurrentDictionary<string, byte>(StringComparer.Ordinal);
presenter.SetField("_prewarmedShaderIdentities", prewarmed);
presenter.InvokeMethod("PrewarmComputePipeline", record, code, new FakeShaderCompiler(Compile), false, true);
Assert.Equal(0, presenter.GetField<int>("_shaderPrewarmFailed"));
Assert.Equal(1, presenter.GetField<int>("_shaderPrewarmCompiled"));
Assert.True(prewarmed.ContainsKey(SharpEmu.Libs.VideoOut.VulkanPipelineCacheStorage.CompiledShaderIdentity(runtime)));
vulkan.AssertNoValidationMessages();
}
}

View File

@ -0,0 +1,59 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
using Silk.NET.Vulkan;
using Xunit;
namespace SharpEmu.Libs.Tests.VideoOut;
public sealed class VulkanPipelineOptimizationTests
{
[Fact]
public async Task PendingAndFailedBuildsKeepTheInitialPipelineWhileCompletedBuildsArePromoted()
{
var type = PresenterUnderTest.PresenterType;
var presenter = RuntimeHelpers.GetUninitializedObject(type);
foreach (var name in new[] { "_pipelineOptimizationGate", "_pipelineOptimizationQueue", "_pipelineOptimizations" })
{
var field = type.GetField(name, PresenterUnderTest.InstanceMembers)!;
field.SetValue(presenter, Activator.CreateInstance(field.FieldType));
}
var tasks = (ConcurrentDictionary<ulong, Task<Pipeline>>)type.GetField("_pipelineOptimizations", PresenterUnderTest.InstanceMembers)!.GetValue(presenter)!;
object? Invoke(string name, params object[] arguments) => type.GetMethod(name, PresenterUnderTest.InstanceMembers)!.Invoke(presenter, arguments);
using var release = new ManualResetEventSlim();
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var initial = new Pipeline(11);
var optimized = new Pipeline(22);
try
{
Invoke("QueuePipelineOptimization", initial, (Func<Pipeline>)(() =>
{
entered.SetResult();
if (!release.Wait(TimeSpan.FromSeconds(10))) throw new TimeoutException();
return optimized;
}));
await entered.Task.WaitAsync(TimeSpan.FromSeconds(10));
Assert.Equal(initial.Handle, ((Pipeline)Invoke("ResolveOptimizedPipeline", initial)!).Handle);
release.Set();
Assert.Equal(optimized.Handle, (await tasks[initial.Handle].WaitAsync(TimeSpan.FromSeconds(10))).Handle);
Assert.Equal(optimized.Handle, ((Pipeline)Invoke("ResolveOptimizedPipeline", initial)!).Handle);
var failed = new Pipeline(33);
Invoke("QueuePipelineOptimization", failed, (Func<Pipeline>)(() => throw new InvalidOperationException("test compile failure")));
await tasks[failed.Handle].WaitAsync(TimeSpan.FromSeconds(10));
Assert.Equal(failed.Handle, ((Pipeline)Invoke("ResolveOptimizedPipeline", failed)!).Handle);
}
finally
{
release.Set();
if (tasks.TryGetValue(initial.Handle, out var pending)) await pending.WaitAsync(TimeSpan.FromSeconds(10));
tasks.TryRemove(initial.Handle, out _);
Invoke("StopPipelineOptimization");
}
}
}

View File

@ -0,0 +1,153 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
namespace SharpEmu.ShaderCompiler.Tests.Resources;
public sealed class PortableBufferWordTests
{
[Theory]
[InlineData(0x3FFFu, 4012u)]
[InlineData(0x2000u, 940u)]
[InlineData(0x1000u, 556u)]
public void PackingPreservesFullStrideAndDescriptorWord3(uint stride, uint swizzle)
{
uint[] descriptor = [0x1000, 0xC0000000 | (stride << 16), 1, (22u << 12) | swizzle | (1u << 23)];
var packed = PortableBufferWord.Pack(descriptor);
Assert.Equal(2, PortableBufferWord.DwordCount);
Assert.Equal(stride, packed.Stride);
Assert.Equal(descriptor[3], packed.DescriptorWord3);
Assert.Equal(swizzle, packed.DescriptorWord3 & 0xFFF);
}
[Fact]
public void NullAndInvalidDescriptorsPackAsZero()
{
uint[][] descriptors = [[], [0, 0, 0], [0, 0, 0, 0], [0x1000, 0x3FFF0000, 1, 0xC0016FAC], [0, 0, 0, 0, 0]];
foreach (var descriptor in descriptors)
Assert.Equal((0u, 0u), PortableBufferWord.Pack(descriptor));
}
[Theory]
[InlineData(0u)]
[InlineData(20u)]
public void TwoDwordsPerBufferPrecedeDispatchLimitsAndAffectAllocation(uint cursor)
{
var info = new ShaderResourceInfo { Buffers = Enumerable.Range(0, 5).Select(_ => new BufferResource { Read = true }).ToList() };
var layout = BindingLayout.Allocate(info, [0u], false, false, true, cursor, usesDispatchThreadLimits: true, portableBuffers: true);
Assert.Equal(10u, layout.BufferWordCount);
Assert.Equal(5u, layout.BufferWordDword);
Assert.Equal(15u, layout.DispatchThreadLimitsDword);
Assert.Equal(18u, layout.ShaderDataDwordCount);
Assert.Equal(cursor == 0, layout.UsesPushData);
Assert.Equal(cursor != 0, layout.Find(DescriptorBindingKind.ShaderData) is not null);
Assert.NotEqual(layout, BindingLayout.Allocate(info, [0u], false, false, true, cursor, usesDispatchThreadLimits: true));
BindingLayoutValidator.Validate(layout, info, [0u], false, false, true, Hash, ShaderStage.Compute);
}
[Theory]
[InlineData(true, false)]
[InlineData(true, true)]
[InlineData(false, false)]
[InlineData(false, true)]
public void OnlyReadOnlyPortableBuffersShareSwizzleSpecializationsAndModules(bool portable, bool written)
{
var program = written
? Program(BufferLoad(0, 0, dwords: 4, formatted: true), BufferStore(8, 0, formatted: true), EndProgram(16))
: Program(BufferLoad(0, 0, dwords: 4, formatted: true), EndProgram(8));
var plan = Extract(program);
var runtime = portable && !written;
var specializations = new List<ResourceSpecialization>();
var modules = new List<byte[]>();
foreach (var swizzle in new uint[] { 4012, 940, 556 })
{
uint[] descriptor = [0x1000, 0x3FFF0000, 1, (22u << 12) | swizzle];
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(plan, new ResourceRuntimeInputs { UserData = descriptor, PortableBuffers = portable },
ref snapshot, ref specialization));
Assert.Equal(descriptor, Assert.Single(snapshot.Buffers));
var buffer = Assert.Single(specialization.Buffers);
Assert.Equal(portable ? 0u : 0x3FFFu, buffer.PackedStride);
Assert.Equal(runtime ? DescriptorConstants.InvalidFormat : 22u, buffer.DescriptorFormat);
Assert.Equal(runtime ? DescriptorConstants.IdentityDestinationSelect : swizzle, buffer.DescriptorSwizzle);
specializations.Add(specialization);
modules.Add(Compile(specialization));
if (runtime)
{
var recorded = specialization.Clone();
recorded.Buffers[0] = new BufferSpecialization(0x3FFF, 22, swizzle);
Assert.Equal(modules[^1], Compile(recorded));
}
}
if (runtime)
{
Assert.All(specializations, specialization => Assert.Equal(ResourceSpecialization.Default(plan.Info), specialization));
Assert.All(modules, module => Assert.Equal(Compile(ResourceSpecialization.Default(plan.Info)), module));
AssertPortableDwordsLoaded(modules[0]);
}
else
{
Assert.NotEqual(specializations[0], specializations[1]);
Assert.NotEqual(modules[0], modules[1]);
}
byte[] Compile(ResourceSpecialization specialization)
{
var resources = ResourceMaterializer.ApplyTo(plan, specialization);
var layout = BindingLayout.Allocate(resources.Info, BindingLayout.CollectUserDataRegisters(program, 0, 64),
false, false, false, portableBuffers: portable);
Assert.True(Gen5SpirvTranslator.TryCompileProgram(new ShaderCompileRequest(plan, resources, layout), out var shader, out var error), error);
return shader.Spirv;
}
}
[Fact]
public void MaterializationCacheSeparatesPortableAndBakedModes()
{
var plan = Extract(Program(BufferLoad(0, 0, formatted: true), EndProgram(8)));
var cache = new ResourceMaterializationCache();
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
foreach (var portable in new[] { false, true, false, true })
{
var inputs = new ResourceRuntimeInputs { UserData = [0x1000, 16u << 16, 1, (22u << 12) | 940], PortableBuffers = portable };
Assert.True(cache.Materialize(plan, inputs, (ulong _, Span<byte> _, bool _) => false, ref snapshot, ref specialization, out _));
Assert.Equal(portable ? DescriptorConstants.IdentityDestinationSelect : 940u, specialization.Buffers[0].DescriptorSwizzle);
}
Assert.Equal(2, cache.Misses);
Assert.Equal(2, cache.Hits);
}
private static void AssertPortableDwordsLoaded(byte[] spirv)
{
var array = new SpirvModuleInspector(spirv).Names.Single(pair => pair.Value == "guestBufferWords").Key;
var words = MemoryMarshal.Cast<byte, uint>(spirv).ToArray();
var constants = new Dictionary<uint, uint>();
var pointers = new Dictionary<uint, uint>();
var loads = new HashSet<uint>();
for (var offset = 5; offset < words.Length; offset += (int)(words[offset] >> 16))
{
switch ((SpirvOp)(words[offset] & 0xFFFF))
{
case SpirvOp.Constant when (words[offset] >> 16) == 4:
constants[words[offset + 2]] = words[offset + 3];
break;
case SpirvOp.AccessChain when words[offset + 3] == array:
pointers[words[offset + 2]] = constants[words[offset + 4]];
break;
case SpirvOp.Load when pointers.TryGetValue(words[offset + 3], out var dword):
loads.Add(dword);
break;
}
}
Assert.True(loads.SetEquals([0u, 1u]), "Stride and descriptor word3 must both be loaded at runtime.");
}
}