namespace AcDream.App.Tests.Rendering.Gpu.Vk;
///
/// S5 issue #470: inspects the committed production module, not GLSL text, so
/// shader compilation cannot silently fold the shared-transform/local-sidecar
/// address split into a different lookup contract.
///
public sealed class MeshModernSharedIndexSpirvTests
{
private const ushort OpConstant = 43;
private const ushort OpVariable = 59;
private const ushort OpLoad = 61;
private const ushort OpAccessChain = 65;
private const ushort OpDecorate = 71;
private const ushort OpBitcast = 124;
private const ushort OpIAdd = 128;
private const ushort OpISub = 130;
private const ushort OpIMul = 132;
private const ushort OpPhi = 245;
private const uint StorageClassInput = 1;
private const uint StorageClassPushConstant = 9;
private const uint DecorationBuiltIn = 11;
private const uint DecorationBinding = 33;
private const uint DecorationDescriptorSet = 34;
private const uint BuiltInInstanceIndex = 43;
[Fact]
public void ProductionModule_UsesAbsoluteTransformAndLocalOrdinarySidecars()
{
string path = Path.Combine(
RepositoryRoot(),
"src", "AcDream.App", "Rendering", "Shaders", "spv", "mesh_modern.vert.spv");
Spirv module = Spirv.Read(path);
uint instanceInput = Assert.Single(
module.Variables,
variable => variable.Value == StorageClassInput
&& module.Decoration(variable.Key, DecorationBuiltIn) == BuiltInInstanceIndex).Key;
uint absoluteIndex = Assert.Single(
module.Instructions,
instruction => instruction.OpCode == OpLoad && instruction.Operands[0] == instanceInput).ResultId;
uint pushBlock = Assert.Single(
module.Variables,
variable => variable.Value == StorageClassPushConstant).Key;
uint prefixPointer = Assert.Single(
module.Instructions,
instruction => instruction.OpCode == OpAccessChain
&& instruction.Operands[0] == pushBlock
&& instruction.Operands.Skip(1).Any(id => module.Constant(id) == 6u)).ResultId;
uint prefixUnsigned = Assert.Single(
module.Instructions,
instruction => instruction.OpCode == OpLoad
&& instruction.Operands[0] == prefixPointer).ResultId;
uint prefixSigned = Assert.Single(
module.Instructions,
instruction => instruction.OpCode == OpBitcast
&& instruction.Operands[0] == prefixUnsigned).ResultId;
Instruction subtraction = Assert.Single(
module.Instructions,
instruction => instruction.OpCode == OpISub
&& instruction.Operands.SequenceEqual([absoluteIndex, prefixSigned]));
uint localIndex = subtraction.ResultId;
Instruction transformLookup = Assert.Single(module.RootAccessChains(set: 0, binding: 0));
Assert.Contains(absoluteIndex, transformLookup.Operands.Skip(1));
Assert.DoesNotContain(localIndex, transformLookup.Operands.Skip(1));
foreach (uint sidecarBinding in new uint[] { 5, 6, 7, 8, 9 })
{
Instruction[] lookups = module.RootAccessChains(set: 0, binding: sidecarBinding).ToArray();
Assert.NotEmpty(lookups);
Assert.All(
lookups,
lookup => Assert.Contains(
lookup.Operands.Skip(1),
index => module.DependsOn(index, localIndex)));
}
}
private readonly record struct Instruction(ushort OpCode, uint ResultId, uint[] Operands);
private sealed class Spirv
{
private readonly Dictionary> _decorations = [];
private readonly Dictionary _constants = [];
private readonly Dictionary _results = [];
private Spirv()
{
}
internal Dictionary Variables { get; } = [];
internal List Instructions { get; } = [];
internal static Spirv Read(string path)
{
byte[] bytes = File.ReadAllBytes(path);
Assert.True(bytes.Length >= 20 && bytes.Length % sizeof(uint) == 0, $"{path} is not SPIR-V.");
uint[] words = new uint[bytes.Length / sizeof(uint)];
Buffer.BlockCopy(bytes, 0, words, 0, bytes.Length);
Assert.Equal(0x07230203u, words[0]);
var module = new Spirv();
for (int index = 5; index < words.Length;)
{
int wordCount = checked((int)(words[index] >> 16));
ushort opCode = checked((ushort)(words[index] & 0xFFFFu));
Assert.True(wordCount > 0 && index + wordCount <= words.Length);
if (opCode == OpDecorate)
{
uint target = words[index + 1];
uint decoration = words[index + 2];
if (wordCount >= 4)
{
if (!module._decorations.TryGetValue(target, out Dictionary? values))
module._decorations[target] = values = [];
values[decoration] = words[index + 3];
}
}
else if (opCode == OpConstant && wordCount >= 4)
{
module._constants[words[index + 2]] = words[index + 3];
}
else if (opCode == OpVariable && wordCount >= 4)
{
module.Variables[words[index + 2]] = words[index + 3];
}
if (TryReadResult(opCode, words.AsSpan(index, wordCount), out Instruction instruction))
{
module.Instructions.Add(instruction);
module._results[instruction.ResultId] = instruction;
}
index += wordCount;
}
return module;
}
internal uint? Decoration(uint id, uint decoration) =>
_decorations.TryGetValue(id, out Dictionary? values)
&& values.TryGetValue(decoration, out uint value)
? value
: null;
internal uint? Constant(uint id) => _constants.TryGetValue(id, out uint value) ? value : null;
internal IEnumerable RootAccessChains(uint set, uint binding)
{
HashSet roots = Variables.Keys
.Where(id => Decoration(id, DecorationDescriptorSet) == set)
.Where(id => Decoration(id, DecorationBinding) == binding)
.ToHashSet();
Assert.Single(roots);
return Instructions.Where(instruction =>
instruction.OpCode == OpAccessChain && roots.Contains(instruction.Operands[0]));
}
internal bool DependsOn(uint value, uint dependency)
{
if (value == dependency)
return true;
var visited = new HashSet();
return Visit(value);
bool Visit(uint current)
{
if (!visited.Add(current) || !_results.TryGetValue(current, out Instruction instruction))
return false;
foreach (uint operand in instruction.Operands)
{
if (operand == dependency || Visit(operand))
return true;
}
return false;
}
}
private static bool TryReadResult(
ushort opCode,
ReadOnlySpan words,
out Instruction instruction)
{
switch (opCode)
{
case OpLoad:
case OpAccessChain:
case OpBitcast:
case OpIAdd:
case OpISub:
case OpIMul:
case OpPhi:
instruction = new Instruction(opCode, words[2], words[3..].ToArray());
return true;
default:
instruction = default;
return false;
}
}
}
private static string RepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
directory = directory.Parent;
return directory?.FullName
?? throw new InvalidOperationException("Could not locate the repository root.");
}
}