321 lines
15 KiB
C#
321 lines
15 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.ShaderCompiler;
|
|
using SharpEmu.ShaderCompiler.Resources;
|
|
using Xunit;
|
|
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
|
|
|
|
namespace SharpEmu.ShaderCompiler.Tests.Resources;
|
|
|
|
public sealed class BindingLayoutTests
|
|
{
|
|
[Theory]
|
|
[InlineData(0u)]
|
|
[InlineData(30u)]
|
|
public void DispatchLimitsFollowPackedOffsetsAndTakePartInAllocation(uint cursor)
|
|
{
|
|
var info = new ShaderResourceInfo { Buffers = Enumerable.Range(0, 5).Select(_ => new BufferResource { Read = true }).ToList() };
|
|
var plain = BindingLayout.Allocate(info, [0u], false, false, true, cursor);
|
|
var limits = BindingLayout.Allocate(info, [0u], false, false, true, cursor, usesDispatchThreadLimits: true);
|
|
Assert.Equal(3u, limits.MemoryOffsetDword);
|
|
Assert.Equal(5u, limits.DispatchThreadLimitsDword);
|
|
Assert.Equal(8u, limits.ShaderDataDwordCount);
|
|
Assert.NotEqual(plain, limits);
|
|
Assert.Equal(cursor == 0, limits.UsesPushData);
|
|
BindingLayoutValidator.Validate(limits, info, [0u], false, false, true, Hash, ShaderStage.Compute);
|
|
Assert.Throws<ResourcePlanException>(() =>
|
|
BindingLayoutValidator.Validate(limits, info, [0u], false, false, true, Hash, ShaderStage.Pixel));
|
|
}
|
|
|
|
[Fact]
|
|
public void ShaderInfoAndBindingLayout()
|
|
{
|
|
var program = Program(
|
|
MoveScalarRegister(0, 8, 3),
|
|
MoveScalarRegister(4, 9, 4),
|
|
MoveScalar(8, 10, 64),
|
|
MoveScalar(12, 11, 0),
|
|
BufferLoad(16, 8),
|
|
DataShareWrite(24, gds: true),
|
|
EndProgram(32));
|
|
var plan = Extract(program);
|
|
var registers = BindingLayout.CollectUserDataRegisters(program, 0, 64);
|
|
var layout = BindingLayout.Allocate(plan.Info, registers, BindingLayout.UsesGlobalDataShare(program), plan.TableReads.Count != 0, false);
|
|
|
|
Assert.NotNull(layout.Find(DescriptorBindingKind.Buffers));
|
|
Assert.NotNull(layout.Find(DescriptorBindingKind.GlobalDataShare));
|
|
Assert.Null(layout.Find(DescriptorBindingKind.ShaderData));
|
|
Assert.True(layout.UsesPushData);
|
|
Assert.False(layout.UsesShaderBase);
|
|
Assert.Equal(2u, layout.MemoryOffsetDword);
|
|
Assert.Equal((uint)DescriptorBindingKind.Buffers, BindingLayout.NativeBindingIndex(ShaderStage.Compute, DescriptorBindingKind.Buffers));
|
|
Assert.Equal((uint)DescriptorBindingKind.Buffers, BindingLayout.NativeBindingIndex(ShaderStage.Vertex, DescriptorBindingKind.Buffers));
|
|
Assert.Equal((uint)DescriptorBindingKind.Count + (uint)DescriptorBindingKind.Buffers, BindingLayout.NativeBindingIndex(ShaderStage.Pixel, DescriptorBindingKind.Buffers));
|
|
Assert.Equal([3u, 4u], layout.UserDataRegisters);
|
|
BindingLayoutValidator.Validate(layout, plan.Info, registers, true, false, false, Hash, ShaderStage.Compute);
|
|
}
|
|
|
|
// s4 is written on one path and read on the other, so it is live at entry.
|
|
[Fact]
|
|
public void UserDataReadOnOneBranch_AfterAWriteOnTheOther_IsCollected()
|
|
{
|
|
var program = Program(
|
|
Branch(0, "SCbranchScc0", 1),
|
|
MoveScalar(4, 4, 0),
|
|
MoveScalar(8, 5, 0),
|
|
MoveScalar(12, 6, 64),
|
|
MoveScalar(16, 7, 0),
|
|
BufferLoad(20, 4),
|
|
EndProgram(28));
|
|
|
|
Assert.Equal([4u], BindingLayout.CollectUserDataRegisters(program, 0, 16));
|
|
}
|
|
|
|
[Fact]
|
|
public void UserDataWrittenOnEveryPathBeforeTheRead_IsNotCollected()
|
|
{
|
|
var program = Program(
|
|
Branch(0, "SCbranchScc0", 2),
|
|
MoveScalar(4, 4, 0x1000),
|
|
Branch(8, "SBranch", 1),
|
|
MoveScalar(12, 4, 0x2000),
|
|
MoveScalar(16, 5, 0),
|
|
MoveScalar(20, 6, 64),
|
|
MoveScalar(24, 7, 0),
|
|
BufferLoad(28, 4),
|
|
MoveScalarRegister(36, 8, 9),
|
|
EndProgram(40));
|
|
|
|
Assert.Equal([9u], BindingLayout.CollectUserDataRegisters(program, 0, 16));
|
|
}
|
|
|
|
// A four-dword load into s4..s7 defines those four only; s8..s11 stay user data.
|
|
[Fact]
|
|
public void MultiwordLoadDestinations_DefineOnlyTheLoadedRegisters()
|
|
{
|
|
var program = Program(
|
|
ScalarLoad(0, 0, destination: 4, count: 4),
|
|
BufferLoad(8, 8),
|
|
Sop2(16, "SLshlB64", 12, Gen5Operand.Scalar(14), Operand(4)),
|
|
MoveScalarRegister(20, 16, 13),
|
|
EndProgram(24));
|
|
|
|
Assert.Equal([0u, 1u, 8u, 9u, 10u, 11u, 14u, 15u], BindingLayout.CollectUserDataRegisters(program, 0, 32));
|
|
}
|
|
|
|
[Fact]
|
|
public void UserDataReadInsideALoop_AfterTheLoopWritesIt_IsCollected()
|
|
{
|
|
var program = Program(
|
|
MoveScalarRegister(0, 8, 4),
|
|
Sop2(4, "SAddU32", 4, Gen5Operand.Scalar(4), Operand(1)),
|
|
Sopc(8, "SCmpLgU32", Gen5Operand.Scalar(8), Operand(0)),
|
|
Branch(12, "SCbranchScc1", -4),
|
|
EndProgram(16));
|
|
|
|
Assert.Equal([4u], BindingLayout.CollectUserDataRegisters(program, 0, 16));
|
|
}
|
|
|
|
[Fact]
|
|
public void ShaderBaseReservation_FollowsTheUserDataRegisters()
|
|
{
|
|
var program = Program(
|
|
new Gen5ShaderInstruction(0, Gen5ShaderEncoding.Sop1, "SGetpcB64", [0u], [], [Gen5Operand.Scalar(4)], null),
|
|
Sop2(4, "SAddU32", 4, Gen5Operand.Scalar(4), Gen5Operand.Scalar(2)),
|
|
Sop2(8, "SAddcU32", 5, Gen5Operand.Scalar(5), Operand(0)),
|
|
MoveScalar(12, 6, 64),
|
|
MoveScalar(16, 7, 0),
|
|
BufferLoad(20, 4),
|
|
EndProgram(28));
|
|
var plan = Extract(program, userDataCount: 3);
|
|
var registers = BindingLayout.CollectUserDataRegisters(program, 0, 3);
|
|
|
|
Assert.True(BindingLayout.ReadsShaderBase(program));
|
|
var layout = BindingLayout.Allocate(plan.Info, registers, false, false, BindingLayout.ReadsShaderBase(program));
|
|
Assert.Equal([2u], registers);
|
|
Assert.True(layout.UsesShaderBase);
|
|
Assert.Equal(1u, layout.ShaderBaseDword);
|
|
Assert.Equal(3u, layout.MemoryOffsetDword);
|
|
Assert.Equal(4u, layout.ShaderDataDwordCount);
|
|
BindingLayoutValidator.Validate(layout, plan.Info, registers, false, false, true, Hash, ShaderStage.Compute);
|
|
|
|
var without = BindingLayout.Allocate(plan.Info, registers, false, false, false);
|
|
Assert.False(without.UsesShaderBase);
|
|
Assert.Equal(1u, without.MemoryOffsetDword);
|
|
Assert.NotEqual(layout, without);
|
|
Assert.False(BindingLayout.ReadsShaderBase(Program(BufferLoad(0, 4), EndProgram(8))));
|
|
}
|
|
|
|
[Fact]
|
|
public void ImageBindingAbi()
|
|
{
|
|
Assert.Equal(45u, BindingLayout.ImageBindingCount);
|
|
Assert.Equal(0u, (uint)DescriptorBindingKind.Buffers);
|
|
Assert.Equal(46u, (uint)DescriptorBindingKind.Samplers);
|
|
Assert.Equal(47u, (uint)DescriptorBindingKind.GlobalDataShare);
|
|
Assert.Equal(48u, (uint)DescriptorBindingKind.DeviceAddressPageTable);
|
|
Assert.Equal(49u, (uint)DescriptorBindingKind.FaultBuffer);
|
|
Assert.Equal(50u, (uint)DescriptorBindingKind.FlattenedResourceTable);
|
|
Assert.Equal(51u, (uint)DescriptorBindingKind.ShaderData);
|
|
Assert.Equal(52u, (uint)DescriptorBindingKind.Count);
|
|
|
|
ImageDimension[] sampledDimensions =
|
|
[
|
|
ImageDimension.Dim1D, ImageDimension.Dim1DArray, ImageDimension.Dim2D, ImageDimension.Dim2DArray,
|
|
ImageDimension.Dim2DMsaa, ImageDimension.Dim2DMsaaArray, ImageDimension.Dim3D,
|
|
];
|
|
ImageDimension[] storageDimensions =
|
|
[
|
|
ImageDimension.Dim1D, ImageDimension.Dim1DArray, ImageDimension.Dim2D, ImageDimension.Dim2DArray, ImageDimension.Dim3D,
|
|
];
|
|
var index = 0u;
|
|
void CheckBinding(ImageResourceClass resourceClass, ImageNumericClass numericClass, ImageDimension dimension, bool atomic, bool cube = false, bool depthCompare = false)
|
|
{
|
|
var image = new ImageResource { ResourceClass = resourceClass, NumericClass = numericClass, Dimension = dimension, Atomic = atomic, Cube = cube, DepthCompare = depthCompare };
|
|
var kind = ImageDescriptorBinding.ForImage(image);
|
|
Assert.NotNull(kind);
|
|
Assert.Equal(BindingLayout.FirstImageBinding + index, (uint)kind!.Value);
|
|
Assert.Equal(index, ImageDescriptorBinding.ArrayIndex(kind.Value));
|
|
Assert.Equal(resourceClass, ImageDescriptorBinding.ResourceClass(kind.Value));
|
|
Assert.Equal(BindingLayout.FirstImageBinding + index, BindingLayout.NativeBindingIndex(ShaderStage.Compute, kind.Value));
|
|
Assert.Equal((uint)DescriptorBindingKind.Count + BindingLayout.FirstImageBinding + index, BindingLayout.NativeBindingIndex(ShaderStage.Pixel, kind.Value));
|
|
index++;
|
|
}
|
|
|
|
foreach (var numericClass in new[] { ImageNumericClass.Float, ImageNumericClass.Uint, ImageNumericClass.Sint })
|
|
{
|
|
foreach (var dimension in sampledDimensions)
|
|
{
|
|
CheckBinding(ImageResourceClass.Sampled, numericClass, dimension, false);
|
|
}
|
|
}
|
|
|
|
foreach (var numericClass in new[] { ImageNumericClass.Float, ImageNumericClass.Uint })
|
|
{
|
|
foreach (var dimension in storageDimensions)
|
|
{
|
|
CheckBinding(ImageResourceClass.Storage, numericClass, dimension, false);
|
|
}
|
|
}
|
|
|
|
foreach (var dimension in storageDimensions)
|
|
{
|
|
CheckBinding(ImageResourceClass.Storage, ImageNumericClass.Uint, dimension, true);
|
|
}
|
|
|
|
foreach (var numericClass in new[] { ImageNumericClass.Float, ImageNumericClass.Uint, ImageNumericClass.Sint })
|
|
CheckBinding(ImageResourceClass.Sampled, numericClass, ImageDimension.Dim2D, false, cube: true);
|
|
foreach (var numericClass in new[] { ImageNumericClass.Float, ImageNumericClass.Uint })
|
|
CheckBinding(ImageResourceClass.Storage, numericClass, ImageDimension.Dim2D, false, cube: true);
|
|
CheckBinding(ImageResourceClass.Storage, ImageNumericClass.Uint, ImageDimension.Dim2D, true, cube: true);
|
|
// Depth-reference sampling never shares an array with ordinary images.
|
|
CheckBinding(ImageResourceClass.Sampled, ImageNumericClass.Float, ImageDimension.Dim2D, false, depthCompare: true);
|
|
CheckBinding(ImageResourceClass.Sampled, ImageNumericClass.Float, ImageDimension.Dim2DArray, false, depthCompare: true);
|
|
CheckBinding(ImageResourceClass.Sampled, ImageNumericClass.Float, ImageDimension.Dim2D, false, cube: true, depthCompare: true);
|
|
|
|
Assert.Equal(BindingLayout.ImageBindingCount, index);
|
|
|
|
static bool Invalid(ImageResource image) => ImageDescriptorBinding.ForImage(image) is null;
|
|
var invalid = new ImageResource();
|
|
Assert.True(Invalid(invalid));
|
|
invalid.ResourceClass = ImageResourceClass.Sampled;
|
|
invalid.NumericClass = ImageNumericClass.Float;
|
|
invalid.Dimension = ImageDimension.Unknown;
|
|
Assert.True(Invalid(invalid));
|
|
invalid.Dimension = ImageDimension.Dim2D;
|
|
invalid.NumericClass = ImageNumericClass.Unsupported;
|
|
Assert.True(Invalid(invalid));
|
|
invalid.NumericClass = (ImageNumericClass)255;
|
|
Assert.True(Invalid(invalid));
|
|
invalid.NumericClass = ImageNumericClass.Float;
|
|
invalid.Dimension = (ImageDimension)255;
|
|
Assert.True(Invalid(invalid));
|
|
invalid.Dimension = ImageDimension.Dim2D;
|
|
invalid.Atomic = true;
|
|
Assert.True(Invalid(invalid));
|
|
invalid.ResourceClass = ImageResourceClass.Storage;
|
|
invalid.Atomic = false;
|
|
invalid.NumericClass = ImageNumericClass.Sint;
|
|
Assert.True(Invalid(invalid));
|
|
invalid.NumericClass = ImageNumericClass.Float;
|
|
invalid.Dimension = ImageDimension.Dim2DMsaa;
|
|
Assert.True(Invalid(invalid));
|
|
invalid.Dimension = ImageDimension.Dim2D;
|
|
invalid.Atomic = true;
|
|
Assert.True(Invalid(invalid));
|
|
}
|
|
|
|
[Fact]
|
|
public void GraphicsPushConstantLayout()
|
|
{
|
|
static uint[] UserData(uint count) => Enumerable.Range(0, (int)count).Select(index => (uint)index).ToArray();
|
|
uint cursor = 0;
|
|
var pixel = BindingLayout.Allocate(new ShaderResourceInfo(), UserData(4), false, false, false, cursor);
|
|
Assert.True(pixel.UsesPushData);
|
|
Assert.Equal(0u, pixel.PushDataStartDword);
|
|
Assert.Null(pixel.Find(DescriptorBindingKind.ShaderData));
|
|
pixel.AdvancePushData(ref cursor);
|
|
|
|
var vertex = BindingLayout.Allocate(new ShaderResourceInfo(), UserData(9), false, false, false, cursor);
|
|
Assert.True(vertex.UsesPushData);
|
|
Assert.Equal(4u, vertex.PushDataStartDword);
|
|
vertex.AdvancePushData(ref cursor);
|
|
Assert.Equal(13u, cursor);
|
|
|
|
var edge = BindingLayout.Allocate(new ShaderResourceInfo(), UserData(PushData.DwordCount), false, false, false);
|
|
Assert.True(edge.UsesPushData);
|
|
Assert.Null(edge.Find(DescriptorBindingKind.ShaderData));
|
|
|
|
var spill = BindingLayout.Allocate(new ShaderResourceInfo(), UserData(20), false, false, false, cursor);
|
|
Assert.False(spill.UsesPushData);
|
|
Assert.Equal(PushData.NoStart, spill.PushDataStartDword);
|
|
Assert.NotNull(spill.Find(DescriptorBindingKind.ShaderData));
|
|
spill.AdvancePushData(ref cursor);
|
|
Assert.Equal(13u, cursor);
|
|
|
|
var repeatedSpill = BindingLayout.Allocate(new ShaderResourceInfo(), UserData(20), false, false, false, 20);
|
|
Assert.Equal(spill, repeatedSpill);
|
|
|
|
// The shader base takes two dwords after the user data and moves the cursor.
|
|
cursor = 0;
|
|
var withBase = BindingLayout.Allocate(new ShaderResourceInfo(), UserData(4), false, false, true, cursor);
|
|
Assert.Equal(4u, withBase.ShaderBaseDword);
|
|
Assert.Equal(6u, withBase.MemoryOffsetDword);
|
|
withBase.AdvancePushData(ref cursor);
|
|
Assert.Equal(6u, cursor);
|
|
}
|
|
|
|
// A stage that spilled at cursor 13 is valid; validation must not reallocate at zero.
|
|
[Fact]
|
|
public void ValidatorAcceptsAStageThatSpilledToShaderData()
|
|
{
|
|
static uint[] UserData(uint count) => Enumerable.Range(0, (int)count).Select(index => (uint)index).ToArray();
|
|
var info = new ShaderResourceInfo();
|
|
var spill = BindingLayout.Allocate(info, UserData(20), false, false, false, 13);
|
|
|
|
Assert.False(spill.UsesPushData);
|
|
Assert.NotNull(spill.Find(DescriptorBindingKind.ShaderData));
|
|
BindingLayoutValidator.Validate(spill, info, UserData(20), false, false, false, Hash, ShaderStage.Vertex);
|
|
|
|
var pushed = BindingLayout.Allocate(info, UserData(20), false, false, false, 0);
|
|
Assert.True(pushed.UsesPushData);
|
|
BindingLayoutValidator.Validate(pushed, info, UserData(20), false, false, false, Hash, ShaderStage.Vertex);
|
|
}
|
|
|
|
[Fact]
|
|
public void ValidatorRejectsALayoutThatDoesNotMatchItsResources()
|
|
{
|
|
var info = new ShaderResourceInfo();
|
|
info.Buffers.Add(new BufferResource());
|
|
var layout = BindingLayout.Allocate(info, [0], false, false, false);
|
|
var error = Assert.Throws<ResourcePlanException>(() =>
|
|
BindingLayoutValidator.Validate(layout, info, [0, 1], false, false, false, Hash, ShaderStage.Vertex));
|
|
Assert.Contains("does not match", error.Message);
|
|
|
|
var baseError = Assert.Throws<ResourcePlanException>(() =>
|
|
BindingLayoutValidator.Validate(layout, info, [0], false, false, true, Hash, ShaderStage.Vertex));
|
|
Assert.Contains("does not match", baseError.Message);
|
|
}
|
|
}
|