[Tests] Cover the shader cache, static precompile, portable buffer words and fast pipeline builds
This commit is contained in:
parent
a46c92ab1d
commit
4a1e99d2fa
|
|
@ -151,6 +151,40 @@ public sealed class PerGameSettingsTests
|
|||
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]
|
||||
public void RemoveInheritedValues_DisabledEnvironmentEntry_MatchesMissingEntry()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ internal sealed class FakePipelineHost(ICpuMemory memory) : IShaderPipelineHost
|
|||
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -181,7 +181,8 @@ internal sealed unsafe class LayoutComputeRunner : IDisposable
|
|||
uint[]? flattenedTable = null,
|
||||
IReadOnlyDictionary<DescriptorBindingKind, DescriptorImageInfo[]>? boundImages = null,
|
||||
ulong shaderBase = 0,
|
||||
uint[]? dispatchThreadLimits = null)
|
||||
uint[]? dispatchThreadLimits = null,
|
||||
uint[]? bufferWords = null)
|
||||
{
|
||||
var vk = _harness.Vk;
|
||||
var device = _harness.Device.Device;
|
||||
|
|
@ -199,6 +200,12 @@ internal sealed unsafe class LayoutComputeRunner : IDisposable
|
|||
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 (dispatchThreadLimits is not { Length: 3 }) throw new InvalidOperationException("The dispatch requires three thread limits.");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ using SharpEmu.Libs.Gpu.Images;
|
|||
using SharpEmu.Libs.Gpu.Pipelines;
|
||||
using SharpEmu.Libs.Gpu.Rendering;
|
||||
using SharpEmu.Libs.Gpu.Scheduling;
|
||||
using SharpEmu.Libs.Gpu.ShaderCache;
|
||||
using SharpEmu.Libs.Tests.Gpu.Buffers;
|
||||
using SharpEmu.Libs.Tests.Gpu.Vulkan;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
|
|
@ -79,6 +80,8 @@ internal sealed class PresenterUnderTest : IDisposable
|
|||
"_deferredGuestImageVersionDestroys",
|
||||
"_pipelineEntries", "_shaderModules", "_shaderModuleSpirvBytes",
|
||||
"_preparedTextures", "_barriersAfterRendering", "_feedbackSnapshotPool",
|
||||
"_warmStages", "_shaderCacheWorkers",
|
||||
"_pipelineOptimizationGate", "_pipelineOptimizationQueue", "_pipelineOptimizations",
|
||||
})
|
||||
{
|
||||
var field = PresenterType.GetField(name, InstanceMembers)!;
|
||||
|
|
@ -86,6 +89,8 @@ internal sealed class PresenterUnderTest : IDisposable
|
|||
}
|
||||
|
||||
SetField("_shaderModuleCacheIdentities", new Dictionary<ulong, string>());
|
||||
SetField("_shaderCacheCancel", new CancellationTokenSource());
|
||||
SetField("_pipelineStoreSources", Array.Empty<ShaderCacheFile>());
|
||||
var cacheShards = PresenterType.GetField("_pipelineCacheShards", InstanceMembers)!;
|
||||
cacheShards.SetValue(Instance, Activator.CreateInstance(cacheShards.FieldType, nonPublic: true));
|
||||
forwarder.Target = this;
|
||||
|
|
@ -156,6 +161,7 @@ internal sealed class PresenterUnderTest : IDisposable
|
|||
|
||||
private void DestroyPipelineResources()
|
||||
{
|
||||
InvokeMethod("StopPipelineOptimization");
|
||||
InvokeMethod("DestroyRenderPipelines");
|
||||
GetField<DescriptorHeap>("_descriptorHeap").Dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue