297 lines
12 KiB
C#
297 lines
12 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.Libs.Gpu.Pipelines;
|
|
using SharpEmu.Libs.Gpu.Rendering;
|
|
using SharpEmu.Libs.Tests.Gpu.Scheduling;
|
|
using SharpEmu.ShaderCompiler.Resources;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Gpu.Pipelines;
|
|
|
|
// Program identity is the content hash or the declared hash; entries key on static state, permutations on specialization.
|
|
[Collection(SchedulingStateCollection.Name)]
|
|
public sealed class ShaderProgramCacheTests : IDisposable
|
|
{
|
|
private const ulong CodeA = PipelineTestGuest.MemoryBase + 0x1000;
|
|
private const ulong CodeB = PipelineTestGuest.MemoryBase + 0x2000;
|
|
private const ulong HeaderA = PipelineTestGuest.MemoryBase + 0x8000;
|
|
private const ulong HeaderB = PipelineTestGuest.MemoryBase + 0x8100;
|
|
private const ulong DataBase = PipelineTestGuest.MemoryBase + 0x4_0000;
|
|
private const uint Format32x4Uint = 75;
|
|
private const uint Format32x4Float = 77;
|
|
|
|
[Fact]
|
|
public void EmbeddedFetchIsReplacedBeforeTheProgramRequestsResourceTables()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA,
|
|
[0xF4080303, 0xFA000000, 0x02020B08, 0xE00C2000, 0x80030401, 0xBF810000]);
|
|
var source = _guest.Source(CodeA, ShaderStage.Vertex, new uint[8]);
|
|
var descriptor = PipelineTestGuest.BufferDescriptor(DataBase, 16, 4, Format32x4Float);
|
|
var options = new StageCompileOptions
|
|
{
|
|
VertexInfo = new VertexInputInfo
|
|
{
|
|
FetchEmbedded = true,
|
|
FetchAttributeRegister = 4,
|
|
FetchBufferRegister = 6,
|
|
Attributes = [new VertexAttributeResource(new(descriptor[0], descriptor[1], descriptor[2], descriptor[3]), 4, 4, 0, 0, 0, 0)],
|
|
Buffers = [new VertexInputBuffer(DataBase, 16, 4)],
|
|
},
|
|
};
|
|
var cursor = 0u;
|
|
_ = _guest.Programs.GetOrCompile(source, options, ref cursor, out var stage);
|
|
var request = Assert.Single(_guest.Compiler.Requests);
|
|
Assert.Single(request.VertexInputs);
|
|
Assert.Empty(request.Memory.Entries);
|
|
Assert.Empty(stage.Resources.FlattenedResourceTable);
|
|
Assert.False(stage.Program!.UsesDeviceAddresses);
|
|
Assert.Equal("SNop", request.Program.Instructions[0].Opcode);
|
|
}
|
|
|
|
private readonly FatalScope _fatal = new();
|
|
private readonly PipelineTestGuest _guest = new();
|
|
|
|
public void Dispose() => _fatal.Dispose();
|
|
|
|
private ShaderProgram Compile(ulong code, uint[] userData, ref uint cursor, out ShaderStageResources stage, uint threadsX = 64) =>
|
|
_guest.Programs.GetOrCompile(_guest.Source(code, ShaderStage.Compute, userData), PipelineTestGuest.ComputeOptions(threadsX), ref cursor, out stage);
|
|
|
|
private ShaderProgram Compile(ulong code, uint[]? userData = null, uint threadsX = 64)
|
|
{
|
|
var cursor = 0u;
|
|
return Compile(code, userData ?? [], ref cursor, out _, threadsX);
|
|
}
|
|
|
|
// The marker, the block offset, the code, padding, then the binary information block with its hash.
|
|
private static uint[] DeclaredHashProgram(uint hash0, uint hash1) =>
|
|
[
|
|
ShaderBinaryInfo.MarkerWord, 1, 0xBF810000, 0,
|
|
0, 0, 0, 0, hash0, hash1, 0,
|
|
];
|
|
|
|
[Fact]
|
|
public void UndeclaredHash_IsTheContentHash()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, PipelineTestGuest.EndProgram);
|
|
var registered = _guest.Registry.Require(CodeA, "compute");
|
|
|
|
var hash = ShaderIdentity.Compute(_guest.Memory, CodeA, registered.CodeRanges, "compute");
|
|
|
|
Assert.Equal(System.IO.Hashing.XxHash3.HashToUInt64([0x00, 0x00, 0x81, 0xBF]), hash);
|
|
Assert.NotEqual(0UL, hash);
|
|
}
|
|
|
|
[Fact]
|
|
public void DeclaredHash_IsTakenFromTheBinaryInfoBlock()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, DeclaredHashProgram(0xC0DE0001, 0xBEEF0002));
|
|
|
|
Assert.True(ShaderIdentity.TryReadDeclaredHash(_guest.Memory, CodeA, out var declared));
|
|
Assert.Equal(0xBEEF0002_C0DE0001UL, declared);
|
|
var source = _guest.Source(CodeA, ShaderStage.Compute, []);
|
|
Assert.Equal(0xBEEF0002_C0DE0001UL, source.Hash);
|
|
}
|
|
|
|
[Fact]
|
|
public void ZeroDeclaredHash_UsesContentHash()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, DeclaredHashProgram(0, 0));
|
|
_guest.RegisterProgram(CodeB, HeaderB, [.. DeclaredHashProgram(0, 0)[..10], 1]);
|
|
|
|
var first = Compile(CodeA);
|
|
var second = Compile(CodeB);
|
|
|
|
Assert.NotEqual(first.Id, second.Id);
|
|
Assert.Equal(2, _guest.Programs.ProgramCount);
|
|
Assert.NotEqual(_guest.Source(CodeA, ShaderStage.Compute, []).Hash, _guest.Source(CodeB, ShaderStage.Compute, []).Hash);
|
|
}
|
|
|
|
[Fact]
|
|
public void RewriteAtTheSameAddress_CompilesASecondProgram()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, PipelineTestGuest.EndProgram);
|
|
var first = Compile(CodeA);
|
|
|
|
_guest.RegisterProgram(CodeA, HeaderA, [0xBF800000, 0xBF810000]);
|
|
var second = Compile(CodeA);
|
|
|
|
Assert.NotEqual(first.Id, second.Id);
|
|
Assert.Equal(2, _guest.Programs.ProgramCount);
|
|
Assert.Equal(2, _guest.Host.Modules.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void SameCodeAtTwoAddresses_SharesOneEntry()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, PipelineTestGuest.EndProgram);
|
|
_guest.RegisterProgram(CodeB, HeaderB, PipelineTestGuest.EndProgram);
|
|
|
|
var first = Compile(CodeA);
|
|
var second = Compile(CodeB);
|
|
|
|
Assert.Equal(first.Id, second.Id);
|
|
Assert.Equal(1, _guest.Programs.ProgramCount);
|
|
Assert.Single(_guest.Host.Modules);
|
|
Assert.Equal(1, _guest.Compiler.Compilations);
|
|
}
|
|
|
|
[Fact]
|
|
public void StaticStateChange_IsANewEntryNotAPermutation()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, PipelineTestGuest.EndProgram);
|
|
|
|
var first = Compile(CodeA, threadsX: 64);
|
|
var second = Compile(CodeA, threadsX: 32);
|
|
|
|
Assert.NotEqual(first.Id, second.Id);
|
|
Assert.Equal(2, _guest.Programs.ProgramCount);
|
|
Assert.All(_guest.Programs.Entries, entry => Assert.Single(entry.Permutations));
|
|
}
|
|
|
|
[Fact]
|
|
public void SpecializationChange_IsANewPermutationOfTheSameEntry()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, PipelineTestGuest.FormatLoadProgram);
|
|
|
|
var first = Compile(CodeA, PipelineTestGuest.BufferDescriptor(DataBase, 16, 4, Format32x4Uint));
|
|
var same = Compile(CodeA, PipelineTestGuest.BufferDescriptor(DataBase + 0x100, 16, 8, Format32x4Uint));
|
|
var second = Compile(CodeA, PipelineTestGuest.BufferDescriptor(DataBase, 16, 4, Format32x4Float));
|
|
|
|
Assert.Equal(first.Id, same.Id);
|
|
Assert.NotEqual(first.Id, second.Id);
|
|
var entry = Assert.Single(_guest.Programs.Entries);
|
|
Assert.Equal(2, entry.Permutations.Count);
|
|
Assert.Equal(2, _guest.Compiler.Compilations);
|
|
Assert.Same(entry.Plan, entry.Plan);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0u)]
|
|
[InlineData(32u)]
|
|
public void DispatchLimitsChangeWithoutRecompilingAndStayWithEachStage(uint startCursor)
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, PipelineTestGuest.EndProgram);
|
|
var source = _guest.Source(CodeA, ShaderStage.Compute, []);
|
|
ShaderStageResources Obtain(uint count, out ShaderProgram program)
|
|
{
|
|
var options = new StageCompileOptions
|
|
{
|
|
ComputeInfo = new ComputeInputInfo
|
|
{
|
|
ThreadsX = 64, ThreadsY = 1, ThreadsZ = 1, WaveSize = 32,
|
|
GroupIdX = true, ThreadIdCount = 1, DispatchThreadDimensions = true,
|
|
DispatchThreadsX = count, DispatchThreadsY = 1, DispatchThreadsZ = 1,
|
|
},
|
|
};
|
|
var cursor = startCursor;
|
|
program = _guest.Programs.GetOrCompile(source, options, ref cursor, out var stage);
|
|
return stage;
|
|
}
|
|
|
|
var first = Obtain(100, out var firstProgram);
|
|
var second = Obtain(4, out var secondProgram);
|
|
Assert.Equal(firstProgram, secondProgram);
|
|
Assert.Single(_guest.Compiler.Requests);
|
|
Assert.Single(_guest.Host.Modules);
|
|
var layout = first.Program!.Bindings!;
|
|
Assert.True(layout.UsesDispatchThreadLimits);
|
|
Assert.Equal(startCursor == 0, layout.UsesPushData);
|
|
var firstData = new uint[layout.ShaderDataDwordCount];
|
|
var secondData = new uint[layout.ShaderDataDwordCount];
|
|
first.WriteDispatchThreadLimits(firstData);
|
|
second.WriteDispatchThreadLimits(secondData);
|
|
Assert.Equal(new uint[] { 100, 1, 1 }, firstData);
|
|
Assert.Equal(new uint[] { 4, 1, 1 }, secondData);
|
|
var missing = first with { ThreadLimits = null };
|
|
Assert.Throws<SchedulerFatalException>(() => missing.WriteDispatchThreadLimits(firstData));
|
|
}
|
|
|
|
[Fact]
|
|
public void UserDataChangesBetweenDraws_ChangeTheSnapshotWithoutRecompiling()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, PipelineTestGuest.FormatLoadProgram);
|
|
var cursor = 0u;
|
|
|
|
Compile(CodeA, PipelineTestGuest.BufferDescriptor(DataBase, 16, 4, Format32x4Uint), ref cursor, out var firstStage);
|
|
cursor = 0;
|
|
Compile(CodeA, PipelineTestGuest.BufferDescriptor(DataBase + 0x200, 16, 9, Format32x4Uint), ref cursor, out var secondStage);
|
|
|
|
Assert.Equal(1, _guest.Compiler.Compilations);
|
|
Assert.Equal(unchecked((uint)DataBase), firstStage.Resources.Buffers[0][0]);
|
|
Assert.Equal(unchecked((uint)(DataBase + 0x200)), secondStage.Resources.Buffers[0][0]);
|
|
Assert.Equal(9u, secondStage.Resources.Buffers[0][2]);
|
|
Assert.Same(firstStage.Program, secondStage.Program);
|
|
}
|
|
|
|
[Fact]
|
|
public void PushCursorChange_IsANewPermutation()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, PipelineTestGuest.FormatLoadProgram);
|
|
var descriptor = PipelineTestGuest.BufferDescriptor(DataBase, 16, 4, Format32x4Uint);
|
|
var cursor = 0u;
|
|
var first = Compile(CodeA, descriptor, ref cursor, out var firstStage);
|
|
var firstEnd = cursor;
|
|
|
|
cursor = 3;
|
|
var second = Compile(CodeA, descriptor, ref cursor, out var secondStage);
|
|
|
|
Assert.NotEqual(first.Id, second.Id);
|
|
Assert.Equal(2, Assert.Single(_guest.Programs.Entries).Permutations.Count);
|
|
Assert.Equal(0u, firstStage.Program!.Bindings!.PushDataStartDword);
|
|
Assert.Equal(3u, secondStage.Program!.Bindings!.PushDataStartDword);
|
|
Assert.Equal(firstStage.Program.Bindings.ShaderDataDwordCount, firstEnd);
|
|
Assert.Equal(3u + secondStage.Program.Bindings.ShaderDataDwordCount, cursor);
|
|
}
|
|
|
|
[Fact]
|
|
public void SamePushCursor_ReusesThePermutation()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, PipelineTestGuest.FormatLoadProgram);
|
|
var descriptor = PipelineTestGuest.BufferDescriptor(DataBase, 16, 4, Format32x4Uint);
|
|
var cursor = 2u;
|
|
var first = Compile(CodeA, descriptor, ref cursor, out _);
|
|
|
|
cursor = 2;
|
|
var second = Compile(CodeA, descriptor, ref cursor, out _);
|
|
|
|
Assert.Equal(first.Id, second.Id);
|
|
Assert.Single(Assert.Single(_guest.Programs.Entries).Permutations);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingHeader_IsFatalWithStageAndAddress()
|
|
{
|
|
var fatal = Assert.Throws<SchedulerFatalException>(() => _guest.Registry.Require(CodeA, "pixel"));
|
|
|
|
Assert.Contains("label=pixel", fatal.Message);
|
|
Assert.Contains($"shader=0x{CodeA:X16}", fatal.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void ZeroOrUnalignedCodeSize_IsFatal()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, PipelineTestGuest.EndProgram);
|
|
_guest.Write(HeaderA + 0x44, [0x02, 0x00, 0x00, 0x00]);
|
|
|
|
var fatal = Assert.Throws<SchedulerFatalException>(() => _guest.Registry.Require(CodeA, "compute"));
|
|
|
|
Assert.Contains("invalid code size", fatal.Message);
|
|
Assert.Contains("size=0x00000002", fatal.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProgramIds_AreMonotonicAcrossEntries()
|
|
{
|
|
_guest.RegisterProgram(CodeA, HeaderA, PipelineTestGuest.EndProgram);
|
|
_guest.RegisterProgram(CodeB, HeaderB, [0xBF800000, 0xBF810000]);
|
|
|
|
var first = Compile(CodeA);
|
|
var second = Compile(CodeB);
|
|
|
|
Assert.Equal(1UL, first.Id);
|
|
Assert.Equal(2UL, second.Id);
|
|
Assert.Equal([(ShaderStage.Compute, _guest.Source(CodeA, ShaderStage.Compute, []).Hash, 1UL), (ShaderStage.Compute, _guest.Source(CodeB, ShaderStage.Compute, []).Hash, 2UL)], _guest.Host.Modules);
|
|
}
|
|
}
|