sharpemu/tests/SharpEmu.ShaderCompiler.Tests/Resources/DirectImageTableTests.cs

666 lines
33 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.ShaderCompiler.Metal;
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 DirectImageTableTests
{
[Theory]
[InlineData(0x80000000u, true)]
[InlineData(0x10000000u, false)]
public void ImageCandidatesDistinguishResourceLevelFromReservedBits(uint bit, bool valid)
{
var plan = ShaderResourcePlan.Extract(CreateWaveIndexedDescriptorProgram(), ShaderStage.Compute, Hash, 0, 2);
bool Read(ulong address, out uint word)
{
if (!ReadWaveIndexedMemory(address, out word)) return false;
if (address >= 0x1100 && (address - 0x1100) % 32 == 8) word |= bit;
return true;
}
ResourceSnapshot snapshot = new(); ResourceSpecialization specialization = new();
Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization));
Assert.Equal(valid, snapshot.Images.Any(image => image[0] != 0));
if (valid) Assert.All(snapshot.Images, image => Assert.Equal(bit, image[2] & bit));
}
internal static Gen5ShaderProgram CreateWaveIndexedDescriptorProgram()
{
return Program(
ScalarLoad(0, 0, 16, immediateOffset: 0x80),
Sop1(8, "SFF1I32B32", 18, Gen5Operand.Scalar(16)),
Sop2(12, "SMulI32", 106, Gen5Operand.Scalar(18), new Gen5Operand(Gen5OperandKind.LiteralConstant, 0x90)),
MoveVectorFromScalar(16, 34, 18),
Vop2(20, "VLshlrevB32", 15, Operand(4), Gen5Operand.Vector(34)),
Vop3(24, "VLshlAddU32", 16, Gen5Operand.Vector(15), Operand(3), Gen5Operand.Vector(15)),
Sop1(32, "SBitset0B32", 16, Gen5Operand.Scalar(18)),
Vop2(36, "VAddI32", 15, new Gen5Operand(Gen5OperandKind.LiteralConstant, 0x40), Gen5Operand.Vector(16)),
GlobalMemory(40, "GlobalLoadDword", 0, 15, 22, 0),
ReadFirstLane(48, 106, 22),
Sop2(52, "SLshlB32", 106, Gen5Operand.Scalar(106), Operand(5)),
ScalarLoad(56, 0, 4, 8, immediateOffset: 0x100, dynamicOffsetRegister: 106),
Image(64, "ImageLoad", 4, dmask: 1, vectorAddress: 1),
EndProgram(72));
}
internal static Gen5ShaderProgram CreateWaveIndexedReadLaneProgram(bool selfAddressed, bool restoreExec = true)
{
var instructions = new List<Gen5ShaderInstruction>
{
ScalarLoad(0, 0, 16, immediateOffset: 0x80),
Sop1(8, "SFF1I32B32", 18, Gen5Operand.Scalar(16)),
Sop2(12, "SMulI32", 19, Gen5Operand.Scalar(18), new Gen5Operand(Gen5OperandKind.LiteralConstant, 0x90)),
MoveVectorFromScalar(16, 34, 18),
Vop2(20, "VLshlrevB32", 15, Operand(4), Gen5Operand.Vector(34)),
Vop3(24, "VLshlAddU32", selfAddressed ? 15u : 16u, Gen5Operand.Vector(15), Operand(3), Gen5Operand.Vector(15)),
Sop2(32, "SLshlB32", 20, Operand(1), Gen5Operand.Scalar(18)),
Sop2(36, "SXorB32", 16, Gen5Operand.Scalar(20), Gen5Operand.Scalar(16)),
Vop2(40, "VAddI32", 15, new Gen5Operand(Gen5OperandKind.LiteralConstant, 0x40), Gen5Operand.Vector(selfAddressed ? 15u : 16u)),
GlobalMemory(44, "GlobalLoadDword", 0, 15, 22, 0),
Sop1(52, "SMovB64", 12, Gen5Operand.Scalar(126)),
Sop1(56, "SFF1I32B64", 24, Gen5Operand.Scalar(12)),
new(60, Gen5ShaderEncoding.Vop3, "VReadlaneB32", [0u, 0u],
[Gen5Operand.Vector(22), Gen5Operand.Scalar(24), Gen5Operand.Scalar(24)], [Gen5Operand.Scalar(106)], null),
new(68, Gen5ShaderEncoding.Vop3, "VCmpEqU32", [0u, 0u],
[Gen5Operand.Scalar(106), Gen5Operand.Vector(22)], [Gen5Operand.Scalar(14)], new Gen5Vop3Control(0, 0, 0, false, 0, 14)),
Sop1(76, "SAndSaveexecB64", 28, Gen5Operand.Scalar(14)),
Branch(80, "SCbranchExecz", 8),
Sop2(84, "SLshlB32", 106, Gen5Operand.Scalar(106), Operand(5)),
Sop2(88, "SAddI32", 107, Gen5Operand.Scalar(106), Operand(16)),
ScalarLoad(92, 0, 4, 4, immediateOffset: 0x100, dynamicOffsetRegister: 106),
ScalarLoad(100, 0, 8, 4, immediateOffset: 0x100, dynamicOffsetRegister: 107),
Image(108, "ImageLoad", 4, dmask: 1, vectorAddress: 1),
Sop2(116, "SAndn2B64", 12, Gen5Operand.Scalar(12), Gen5Operand.Scalar(14)),
restoreExec ? Sop1(120, "SMovB64", 126, Gen5Operand.Scalar(28)) : Nop(120),
Branch(124, "SCbranchScc1", -18),
EndProgram(128),
};
return Program([.. instructions]);
}
public static Gen5ShaderProgram CreateProgram(uint mask = 1, bool split = true, bool bitScan = true)
{
var instructions = new List<Gen5ShaderInstruction>
{
Vop2(0, "VAddU32", 12, Operand(mask), Gen5Operand.Vector(0)),
ReadFirstLane(4, 18, 12),
Sop1(8, bitScan ? "SFF1I32B32" : "SMovB32", 19, Gen5Operand.Scalar(18)),
Sop2(12, "SLshlB32", 106, Gen5Operand.Scalar(19), Operand(5)),
new(16, Gen5ShaderEncoding.Sopk, "SAddkI32", [0xB7EA0158], [new(Gen5OperandKind.LiteralConstant, 344)], [Gen5Operand.Scalar(106)], null),
Sop2(20, "SAddI32", 107, Gen5Operand.Scalar(106), Operand(16)),
ScalarLoad(24, 0, 4, split ? 4u : 8u, dynamicOffsetRegister: 106),
};
if (split) instructions.Add(ScalarLoad(32, 0, 8, 4, dynamicOffsetRegister: 107));
instructions.AddRange([
MoveScalar(40, 106, 999),
MoveVector(44, 1, 0), MoveVector(48, 2, 0),
Image(52, "ImageLoad", 4, dmask: 1, vectorAddress: 1),
MoveScalar(60, 20, 0), MoveScalar(64, 21, 0), MoveScalar(68, 22, 64), MoveScalar(72, 23, 0),
BufferAccess(76, "BufferStoreDword", 20, vectorData: 4), EndProgram(84),
]);
return Program([.. instructions]);
}
public static bool ReadDescriptor(ulong address, out uint word)
{
word = 0;
if (address < 0x1000 + 312 || address >= 0x1000 + 344 + 32 * 32) return false;
var offset = address - 0x1000 - 312;
var record = offset / 32;
var component = offset % 32 / 4;
word = component switch
{
0 => (record & 1) == 0 ? 0x2000u : 0x1000u,
1 => 20u << 20,
3 => 0xFACu | (9u << 28),
_ => 0,
};
return true;
}
public static Gen5ShaderProgram CreateGuardedProgram(uint mask = 1, bool split = true)
{
var program = CreateProgram(mask, split);
return program with
{
Instructions = program.Instructions.Where(instruction => instruction.Pc < 8)
.Concat([
Sopc(8, "SCmpLgU32", Operand(0), Gen5Operand.Scalar(18)),
Branch(12, "SCbranchScc0", 19),
])
.Concat(program.Instructions.Where(instruction => instruction.Pc >= 8)
.Select(instruction => instruction with { Pc = instruction.Pc + 8 })).ToArray(),
};
}
public static (ShaderResourcePlan Plan, ResourceSnapshot Snapshot, ShaderCompileRequest Request) PrepareDirect(uint mask = 1, bool split = true, bool guarded = false)
{
var program = guarded ? CreateGuardedProgram(mask, split) : CreateProgram(mask, split);
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2);
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: ReadDescriptor), ref snapshot, ref specialization));
var resources = ResourceMaterializer.ApplyTo(plan, specialization);
var layout = BindingLayout.Allocate(resources.Info, BindingLayout.CollectUserDataRegisters(program, 0, 2),
false, ShaderCompileRequest.RequiresFlattenedTable(plan, resources), false);
return (plan, snapshot, new ShaderCompileRequest(plan, resources, layout) { LocalSizeX = 1, ThreadCountX = 1 });
}
[Fact]
public void WaveIndexedDescriptorTableMaterializesOnlyActiveMaskKeys()
{
var plan = ShaderResourcePlan.Extract(CreateWaveIndexedDescriptorProgram(), ShaderStage.Compute, Hash, 0, 2);
var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!;
Assert.True(selector.Dense);
Assert.Equal(0u, selector.KeyBound);
Assert.Equal(new WaveIndexedImageSelector(0x80, 0x40, 0x90), selector.WaveIndexed);
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: ReadWaveIndexedMemory), ref snapshot, ref specialization));
Assert.Equal(2, snapshot.Images.Length);
}
[Fact]
public void CandidateWithAnUnsupportedFormatIsMaterializedAsNull()
{
var plan = ShaderResourcePlan.Extract(CreateWaveIndexedDescriptorProgram(), ShaderStage.Compute, Hash, 0, 2);
bool Read(ulong address, out uint word)
{
var success = ReadWaveIndexedMemory(address, out word);
if (address == 0x1000 + 0x100 + 5 * 32 + 4) word = 139u << 20;
return success;
}
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization, out var failure), $"materialize {failure}");
Assert.Equal(2, snapshot.Images.Length);
Assert.Single(snapshot.Images, image => image.All(word => word == 0));
}
[Fact]
public void IndirectTableWithOnlyIncompatibleDimensionsBindsTypedNullCandidates()
{
var program = CreateWaveIndexedDescriptorProgram();
program = program with
{
Instructions = program.Instructions.Select(instruction =>
instruction.Pc == 64
? Image(64, "ImageLoad", 4, dimension: 6, dmask: 1, vectorAddress: 1)
: instruction).ToArray(),
};
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2);
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(
plan, Inputs([0x1000, 0], readCleanMemory: ReadWaveIndexedMemory),
ref snapshot, ref specialization, out var failure), $"materialize {failure}");
Assert.Equal(2, snapshot.Images.Length);
Assert.All(snapshot.Images, image => Assert.All(image, word => Assert.Equal(0u, word)));
Assert.All(specialization.Images, image => Assert.Equal(ImageDimension.Dim2DMsaa, image.Dimension));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ReadLaneWaterfallSelectsWaveIndexedDescriptors(bool selfAddressed)
{
var program = CreateWaveIndexedReadLaneProgram(selfAddressed);
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2);
var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!;
Assert.True(selector.Dense, "dense");
Assert.Equal(0x100u, selector.TableOffset);
Assert.Equal(new WaveIndexedImageSelector(0x80, 0x40, 0x90), selector.WaveIndexed);
Assert.True(Assert.Single(plan.IndirectImages).KeyIsAddressOffset, "address-offset key");
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: ReadWaveIndexedMemory), ref snapshot, ref specialization, out var failure), $"materialize {failure}");
Assert.Equal(2, snapshot.Images.Length);
var resources = ResourceMaterializer.ApplyTo(plan, specialization);
var layout = BindingLayout.Allocate(resources.Info, BindingLayout.CollectUserDataRegisters(program, 0, 2),
false, ShaderCompileRequest.RequiresFlattenedTable(plan, resources), false);
var request = new ShaderCompileRequest(plan, resources, layout) { LocalSizeX = 64, ThreadCountX = 64 };
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out _, out var error), error);
}
// Without the restored EXEC the loop is no waterfall: no wave-indexed table is planned,
// and the load reads the descriptor its registers hold at run time.
[Fact]
public void ReadLaneWithoutRestoredExecutionIsNotWaveIndexed()
{
var plan = ShaderResourcePlan.Extract(CreateWaveIndexedReadLaneProgram(selfAddressed: true, restoreExec: false), ShaderStage.Compute, Hash, 0, 2);
Assert.Empty(plan.IndirectImages);
Assert.Contains(plan.Memory.Entries, entry => entry.RuntimeDescriptor);
Assert.Empty(plan.Info.NullDescriptorFallbacks);
}
private static bool ReadWaveIndexedMemory(ulong address, out uint word)
{
word = 0;
if (address == 0x1000 + 0x80)
{
word = (1u << 1) | (1u << 4);
return true;
}
if (address == 0x1000 + 0x40 + 0x90 || address == 0x1000 + 0x40 + 4 * 0x90)
{
word = address == 0x1000 + 0x40 + 0x90 ? 2u : 5u;
return true;
}
if (address < 0x1000 + 0x100 || address >= 0x1000 + 0x100 + 6 * 32)
return false;
var relative = address - 0x1000 - 0x100;
var record = relative / 32;
if (record is not (2 or 5))
return false;
word = (relative % 32 / 4) switch
{
0 => (record & 1) == 0 ? 0x2000u : 0x1000u,
1 => 20u << 20,
3 => 0xFACu | (9u << 28),
_ => 0,
};
return true;
}
public static (ResourceSnapshot Snapshot, ShaderCompileRequest Request) PrepareMixedDimensions(uint mask, bool arrayFirst)
{
var program = CreateGuardedProgram(mask);
program = program with
{
Instructions = program.Instructions.Select(instruction => instruction.Pc switch
{
48 => MoveVector(48, 3, 1),
60 => Image(60, "ImageLoad", 4, dimension: 5, dmask: 1, vectorAddress: 1),
_ => instruction,
}).ToArray(),
};
bool Read(ulong address, out uint word)
{
word = 0;
if (address < 0x1000 + 344 || address >= 0x1000 + 344 + 32 * 32) return false;
var offset = address - 0x1000 - 344;
var record = offset / 32;
if (record is not (0 or 31)) return true;
var array = (record == 0) == arrayFirst;
word = (offset % 32 / 4) switch
{
0 => array ? 0x1000u : 0x2000u,
1 => 20u << 20,
3 => 0xFACu | ((array ? 13u : 9u) << 28),
4 => array ? 1u : 0u,
_ => 0,
};
return true;
}
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2);
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization));
var resources = ResourceMaterializer.ApplyTo(plan, specialization);
var layout = BindingLayout.Allocate(resources.Info, BindingLayout.CollectUserDataRegisters(program, 0, 2),
false, ShaderCompileRequest.RequiresFlattenedTable(plan, resources), false);
return (snapshot, new ShaderCompileRequest(plan, resources, layout) { LocalSizeX = 1, ThreadCountX = 1 });
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void MixedDimensionsRetainEveryCandidateAndUseSeparateBindings(bool arrayFirst)
{
var (snapshot, request) = PrepareMixedDimensions(1, arrayFirst);
Assert.Equal(3, snapshot.Images.Length);
var images = request.Resources.Info.Images;
Assert.Equal(3, images[0].IndirectResources.Count);
Assert.Contains(images, image => image.Dimension == ImageDimension.Dim2D);
Assert.Contains(images, image => image.Dimension == ImageDimension.Dim2DArray);
var mapping = (int)images[0].IndirectMappingOffset;
Assert.Equal(32u, snapshot.FlattenedResourceTable[mapping]);
Assert.Equal(344u + 31 * 32, snapshot.FlattenedResourceTable[mapping + 63]);
Assert.Equal(2u, snapshot.FlattenedResourceTable[mapping + 64]);
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out _, out var error), error);
Assert.False(Gen5MslTranslator.TryCompileProgram(request, out _, out error));
Assert.Contains("not supported on Metal", error);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void DirectTableRetainsAllBitScanResultsAndCompiles(bool split)
{
var (plan, snapshot, request) = PrepareDirect(split: split);
Assert.Single(plan.IndirectImages);
Assert.True(plan.IndirectImages[0].KeyIsAddressOffset);
Assert.False(plan.Info.UsesDeviceAddresses);
Assert.Empty(plan.DynamicReads);
var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!;
Assert.Equal(33, selector.DirectCandidates!.Count);
Assert.Contains(selector.DirectCandidates, candidate => candidate.Offset == 312);
Assert.Contains(selector.DirectCandidates, candidate => candidate.Offset == 344 + 31 * 32);
Assert.Equal(2, snapshot.Images.Length);
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out _, out var spirvError), spirvError);
Assert.True(Gen5MslTranslator.TryCompileProgram(request, out _, out var metalError), metalError);
}
[Theory]
[InlineData(32)]
[InlineData(33)]
public void DirectImageCapacityPreservesTheLimitAndPublishedState(int distinctCount)
{
var (plan, snapshot, _) = PrepareDirect();
var specialization = new ResourceSpecialization();
var previousSnapshot = snapshot;
var previousSpecialization = specialization;
bool Read(ulong address, out uint word)
{
var success = ReadDescriptor(address, out word);
if (success && (address - 0x1000 - 312) % 32 == 0)
word = 0x2000 + (uint)(((address - 0x1000 - 312) / 32) % (ulong)distinctCount);
return success;
}
var success = ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read),
ref snapshot, ref specialization, out var failure);
Assert.Equal(distinctCount <= ShaderResourceInfo.MaxImages, success);
if (success)
{
Assert.Equal(ResourceMaterializationFailure.None, failure);
Assert.Equal(distinctCount, snapshot.Images.Length);
}
else
{
Assert.Equal(ResourceMaterializationFailure.ImageCapacityExceeded, failure);
Assert.Same(previousSnapshot, snapshot);
Assert.Same(previousSpecialization, specialization);
}
}
// An unbounded index still plans no direct table; the load reads its descriptor at run time.
[Fact]
public void UnboundedDirectTableIsReadAtRuntime()
{
var plan = ShaderResourcePlan.Extract(CreateProgram(bitScan: false), ShaderStage.Compute, Hash, 0, 2);
Assert.Empty(plan.IndirectImages);
Assert.Contains(plan.Memory.Entries, entry => entry.RuntimeDescriptor);
Assert.Empty(plan.Info.NullDescriptorFallbacks);
}
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public void NonZeroGuardExcludesTheUnreachableDescriptor(bool split, bool equality)
{
var program = CreateGuardedProgram(split: split);
if (equality)
program = program with
{
Instructions = program.Instructions.Select(instruction => instruction.Pc switch
{
8 => Sopc(8, "SCmpEqI32", Gen5Operand.Scalar(18), Operand(0)),
12 => Branch(12, "SCbranchScc1", 19),
_ => instruction,
}).ToArray(),
};
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2);
var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!;
Assert.True(selector.Dense);
Assert.Equal(32u, selector.KeyBound);
Assert.Equal(344u, selector.TableOffset);
Assert.True(Assert.Single(plan.IndirectImages).KeyIsAddressOffset);
bool Read(ulong address, out uint word)
{
Assert.True(address >= 0x1000 + 344);
return ReadDescriptor(address, out word);
}
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization));
var (_, _, request) = PrepareDirect(split: split, guarded: true);
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out _, out var spirvError), spirvError);
Assert.True(Gen5MslTranslator.TryCompileProgram(request, out _, out var metalError), metalError);
}
[Theory]
[InlineData(600u, 8u)]
[InlineData(444u, 3u)]
[InlineData(312u, 32u)]
[InlineData(1400u, 32u)]
public void DenseTableEndsBeforeASamplerLoadedFromTheSameBase(uint samplerOffset, uint expectedBound)
{
var program = CreateGuardedProgram();
program = program with
{
Instructions = program.Instructions.Where(instruction => instruction.Pc != 52).Select(instruction => instruction.Pc switch
{
48 => ScalarLoad(48, 0, 12, 4, immediateOffset: (int)samplerOffset),
60 => Image(60, "ImageSample", 4, samplerRegister: 12, dmask: 1, vectorAddress: 1),
_ => instruction,
}).ToArray(),
};
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2);
var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!;
Assert.True(selector.Dense);
Assert.Equal(344u, selector.TableOffset);
Assert.Equal(expectedBound, selector.KeyBound);
var highest = 0ul;
bool Read(ulong address, out uint word)
{
if (address >= 0x1000 + samplerOffset && address < 0x1000 + samplerOffset + 16)
{
word = 0;
return true;
}
if (address >= 0x1000 + 344) highest = Math.Max(highest, address);
return ReadDescriptor(address, out word);
}
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], Read, Read), ref snapshot, ref specialization, out var failure), $"materialize {failure}");
Assert.True(highest < 0x1000 + 344 + expectedBound * 32, $"read 0x{highest:X} beyond the table");
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void LoopMustRecheckTheBitScanInput(bool bypassGuard)
{
var program = CreateGuardedProgram();
program = program with
{
Instructions = program.Instructions.Where(instruction => instruction.Pc < 92)
.Select(instruction => instruction.Pc == 12 ? Branch(12, "SCbranchScc0", 21) : instruction)
.Concat([
ReadFirstLane(92, 18, 13),
Branch(96, "SBranch", bypassGuard ? (short)-21 : (short)-23),
EndProgram(100),
]).ToArray(),
};
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2);
var selector = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!;
if (bypassGuard)
{
Assert.Equal(33, selector.DirectCandidates!.Count);
Assert.Contains(selector.DirectCandidates, candidate => candidate.Offset == 312);
}
else
{
Assert.True(selector.Dense);
Assert.Equal(32u, selector.KeyBound);
Assert.Equal(344u, selector.TableOffset);
}
}
[Theory]
[InlineData("opposite-branch")]
[InlineData("different-register")]
[InlineData("guard-bypass")]
[InlineData("condition-write")]
[InlineData("input-write")]
public void UnprovenGuardRetainsTheZeroInputDescriptor(string variation)
{
var program = CreateGuardedProgram();
var instructions = program.Instructions.ToList();
switch (variation)
{
case "opposite-branch":
instructions[instructions.FindIndex(instruction => instruction.Pc == 12)] = Branch(12, "SCbranchScc1", 19);
break;
case "different-register":
instructions[instructions.FindIndex(instruction => instruction.Pc == 8)] =
Sopc(8, "SCmpLgU32", Operand(0), Gen5Operand.Scalar(17));
break;
case "guard-bypass":
instructions[0] = Branch(0, "SCbranchScc1", 3);
break;
case "condition-write":
instructions = instructions.Select(instruction => instruction.Pc >= 12
? instruction with { Pc = instruction.Pc + 4 } : instruction).ToList();
instructions.Add(Sopc(12, "SCmpEqU32", Operand(0), Operand(0)));
break;
case "input-write":
instructions = instructions.Select(instruction => instruction.Pc >= 16
? instruction with { Pc = instruction.Pc + 4 } : instruction).ToList();
instructions[instructions.FindIndex(instruction => instruction.Pc == 12)] = Branch(12, "SCbranchScc0", 20);
instructions.Add(ReadFirstLane(16, 18, 13));
break;
}
program = program with { Instructions = instructions.OrderBy(instruction => instruction.Pc).ToArray() };
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2);
var candidates = plan.DescriptorSources[(int)plan.Info.Images[0].Source].IndirectImage!.DirectCandidates!;
Assert.Equal(33, candidates.Count);
Assert.Contains(candidates, candidate => candidate.Offset == 312);
}
[Fact]
public void DescriptorWordsUsedByArithmeticAreNotRemoved()
{
var program = CreateProgram();
program = program with
{
Instructions = program.Instructions.Select(instruction => instruction.Pc == 40
? Vop1(40, "VMovB32", 15, Gen5Operand.Scalar(4)) : instruction).ToArray(),
};
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2);
Assert.Single(plan.IndirectImages);
Assert.True(plan.Info.UsesDeviceAddresses);
Assert.True(plan.Memory.TryGetIndex(24, 0, out var memoryIndex));
Assert.False(plan.Memory[memoryIndex].PlanningOnly);
}
[Fact]
public void DirectTableSlotWithAnUnsupportedFormatIsMaterializedAsNull()
{
var program = CreateProgram();
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, Hash, 0, 2);
var unsupported = true;
bool Read(ulong address, out uint word)
{
var success = ReadDescriptor(address, out word);
if (unsupported && address == 0x1000 + 344 + 3 * 32 + 4) word = 139u << 20;
return success;
}
var clean = new ResourceSnapshot();
var cleanSpecialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: ReadDescriptor), ref clean, ref cleanSpecialization));
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization, out var failure), $"materialize {failure}");
Assert.Equal(clean.Images.Length + 1, snapshot.Images.Length);
Assert.Single(snapshot.Images, image => image.All(word => word == 0));
unsupported = false;
Assert.True(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization, out failure), $"materialize {failure}");
Assert.Equal(clean.Images.Length, snapshot.Images.Length);
}
[Theory]
[InlineData(false, false)]
[InlineData(true, false)]
[InlineData(false, true)]
[InlineData(true, true)]
public void FailedReadOrIncompatibleCandidateDoesNotPublish(bool incompatible, bool guarded)
{
var (plan, original, _) = PrepareDirect(guarded: guarded);
var snapshot = original;
var specialization = new ResourceSpecialization();
var originalSpecialization = specialization;
bool Read(ulong address, out uint word)
{
var success = ReadDescriptor(address, out word);
// A converted format cannot share a case with a directly sampled one.
if (incompatible && address == 0x1000 + 344 + 4)
word = (word & ~(0x1FFu << 20)) | (GuestImageFormat.Format11x2x10Uint << 20);
if (!incompatible && address == 0x1000 + 344 + 12)
return false;
return success;
}
Assert.False(ResourceMaterializer.Materialize(plan, Inputs([0x1000, 0], readCleanMemory: Read), ref snapshot, ref specialization,
out var failure));
Assert.Equal(incompatible ? ResourceMaterializationFailure.IncompatibleImageCandidates : ResourceMaterializationFailure.Other, failure);
Assert.Same(original, snapshot);
Assert.Same(originalSpecialization, specialization);
}
// Vulkan cannot sample a multisample image. A sample instruction reads sample 0 of the texel
// its coordinate falls in (as shadPS4 lowers it); a gather has no such reading and is refused.
[Theory]
[InlineData("ImageSampleA", true)]
[InlineData("ImageSampleLz", true)]
[InlineData("ImageGather4Lz", false)]
[InlineData("ImageLoad", true)]
public void MultisampleDescriptorsRequireLoadsInsteadOfInvalidSamplingModules(string opcode, bool accepted)
{
var gather = opcode.StartsWith("ImageGather", StringComparison.Ordinal);
var program = Program([Image(0, opcode, 0, samplerRegister: 8, dmask: 1), EndProgram(8)]);
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Pixel, Hash, 0, 12);
var inputs = new ResourceRuntimeInputs { UserData = [0x1000, GuestImageFormat.Format32Float << 20, 0,
0xFAC | (1u << 16) | (GuestImageFormat.ImageType2DMsaa << 28), 0, 0x10, 0, 0,
0, 0xFFF000, 0, 0] };
ResourceSnapshot snapshot = new(); ResourceSpecialization specialization = new();
Assert.True(ResourceMaterializer.Materialize(plan, inputs, ref snapshot, ref specialization));
var resources = ResourceMaterializer.ApplyTo(plan, specialization);
Assert.Equal(ImageDimension.Dim2DMsaa, resources.Info.Images[0].Dimension);
var layout = BindingLayout.Allocate(resources.Info, BindingLayout.CollectUserDataRegisters(program, 0, 12),
false, ShaderCompileRequest.RequiresFlattenedTable(plan, resources), false);
Assert.Equal(accepted, Gen5SpirvTranslator.TryCompileProgram(new ShaderCompileRequest(plan, resources, layout), out var shader, out var error));
if (gather) Assert.Contains("multisample sampling semantics", error);
if (accepted) Gen5LargeDispatcherValidationTests.ValidateWithSpirvToolsWhenAvailable(shader.Spirv);
}
// A table in a scalar buffer, indexed by a lane value: planned as a dense table it binds
// every record of the buffer on each draw. An access the runtime table can read binds only
// the records it reads; one it cannot (a storage write) keeps the dense table.
[Theory]
[InlineData("ImageLoad", true)]
[InlineData("ImageStore", false)]
public void BufferTableImagesAreReadAtRuntimeWhenTheAccessAllowsIt(string opcode, bool runtime)
{
var program = Program(
ReadFirstLane(0, 106, 0),
Sop2(4, "SMulI32", 106, Gen5Operand.Scalar(106), Operand(384)),
ScalarBufferLoad(12, 0, 16, 8, dynamicOffsetRegister: 106),
Image(20, opcode, 16, dmask: 1, vectorAddress: 4),
EndProgram(28));
var plan = Extract(program, userDataCount: 4);
Assert.Equal(runtime, plan.Info.UsesRuntimeDescriptors);
Assert.Equal(!runtime, plan.DescriptorSources.Any(source => source.IndirectImage?.BufferTableStride == 384));
}
}