715 lines
28 KiB
C#
715 lines
28 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using SharpEmu.ShaderCompiler.Ir;
|
|
|
|
namespace SharpEmu.ShaderCompiler.Resources;
|
|
|
|
// The fixed binding numbers of one stage: buffers, image classes, samplers,
|
|
// and the shared buffers. A pixel stage adds Count to every number.
|
|
public enum DescriptorBindingKind : uint
|
|
{
|
|
Buffers = 0,
|
|
Samplers = BindingLayout.FirstImageBinding + BindingLayout.ImageBindingCount,
|
|
GlobalDataShare,
|
|
DeviceAddressPageTable,
|
|
FaultBuffer,
|
|
FlattenedResourceTable,
|
|
ShaderData,
|
|
Count,
|
|
}
|
|
|
|
// Thirty-two dwords of push data shared by the stages of one pipeline.
|
|
public static class PushData
|
|
{
|
|
public const uint DwordCount = 32;
|
|
public const uint ByteSize = DwordCount * sizeof(uint);
|
|
public const uint NoStart = uint.MaxValue;
|
|
|
|
public static bool CanFit(uint start, uint size) => size != 0 && start <= DwordCount && size <= DwordCount - start;
|
|
|
|
public static uint StartFor(uint cursor, uint size) => CanFit(cursor, size) ? cursor : NoStart;
|
|
}
|
|
|
|
public sealed record DescriptorBinding(DescriptorBindingKind Kind, IReadOnlyList<uint> Resources);
|
|
|
|
// Which binding an image resource occupies: its class, numeric class and dimension.
|
|
public static class ImageDescriptorBinding
|
|
{
|
|
private const uint SampledFloatBinding = 1;
|
|
private const uint SampledUintBinding = 8;
|
|
private const uint SampledSintBinding = 15;
|
|
private const uint StorageFloatBinding = 22;
|
|
private const uint StorageUintBinding = 27;
|
|
private const uint AtomicUintBinding = 32;
|
|
private const uint SampledCubeFloatBinding = 37;
|
|
private const uint SampledCubeUintBinding = 38;
|
|
private const uint SampledCubeSintBinding = 39;
|
|
private const uint StorageCubeFloatBinding = 40;
|
|
private const uint StorageCubeUintBinding = 41;
|
|
private const uint AtomicCubeUintBinding = 42;
|
|
// Depth-reference sampling gets its own arrays: validation checks every element
|
|
// of an array sampled with Dref, so sharing one with color images reports them.
|
|
private const uint SampledCompare2DBinding = 43;
|
|
private const uint SampledCompare2DArrayBinding = 44;
|
|
private const uint SampledCompareCubeBinding = 45;
|
|
|
|
public static DescriptorBindingKind? ForImage(ImageResource image)
|
|
{
|
|
if (image.DepthCompare && image.ResourceClass == ImageResourceClass.Sampled && !image.Atomic &&
|
|
image.NumericClass == ImageNumericClass.Float)
|
|
{
|
|
if (image.Cube)
|
|
{
|
|
return image.Dimension is ImageDimension.Dim2D or ImageDimension.Dim2DArray
|
|
? (DescriptorBindingKind)SampledCompareCubeBinding
|
|
: null;
|
|
}
|
|
|
|
if (image.Dimension == ImageDimension.Dim2D)
|
|
{
|
|
return (DescriptorBindingKind)SampledCompare2DBinding;
|
|
}
|
|
|
|
if (image.Dimension == ImageDimension.Dim2DArray)
|
|
{
|
|
return (DescriptorBindingKind)SampledCompare2DArrayBinding;
|
|
}
|
|
}
|
|
|
|
if (image.Cube)
|
|
{
|
|
if (image.Dimension is not (ImageDimension.Dim2D or ImageDimension.Dim2DArray))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (image.ResourceClass == ImageResourceClass.Sampled)
|
|
{
|
|
if (image.Atomic)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return image.NumericClass switch
|
|
{
|
|
ImageNumericClass.Float => (DescriptorBindingKind)SampledCubeFloatBinding,
|
|
ImageNumericClass.Uint => (DescriptorBindingKind)SampledCubeUintBinding,
|
|
ImageNumericClass.Sint => (DescriptorBindingKind)SampledCubeSintBinding,
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
if (image.ResourceClass == ImageResourceClass.Storage)
|
|
{
|
|
if (image.Atomic)
|
|
{
|
|
return image.NumericClass == ImageNumericClass.Uint
|
|
? (DescriptorBindingKind)AtomicCubeUintBinding
|
|
: null;
|
|
}
|
|
|
|
return image.NumericClass switch
|
|
{
|
|
ImageNumericClass.Float => (DescriptorBindingKind)StorageCubeFloatBinding,
|
|
ImageNumericClass.Uint => (DescriptorBindingKind)StorageCubeUintBinding,
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
uint baseBinding;
|
|
var sampled = false;
|
|
if (image.ResourceClass == ImageResourceClass.Sampled)
|
|
{
|
|
if (image.Atomic)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
sampled = true;
|
|
switch (image.NumericClass)
|
|
{
|
|
case ImageNumericClass.Float:
|
|
baseBinding = SampledFloatBinding;
|
|
break;
|
|
case ImageNumericClass.Uint:
|
|
baseBinding = SampledUintBinding;
|
|
break;
|
|
case ImageNumericClass.Sint:
|
|
baseBinding = SampledSintBinding;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
else if (image.ResourceClass == ImageResourceClass.Storage)
|
|
{
|
|
if (image.Atomic)
|
|
{
|
|
if (image.NumericClass != ImageNumericClass.Uint)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
baseBinding = AtomicUintBinding;
|
|
}
|
|
else
|
|
{
|
|
switch (image.NumericClass)
|
|
{
|
|
case ImageNumericClass.Float:
|
|
baseBinding = StorageFloatBinding;
|
|
break;
|
|
case ImageNumericClass.Uint:
|
|
baseBinding = StorageUintBinding;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
|
|
uint dimension;
|
|
switch (image.Dimension)
|
|
{
|
|
case ImageDimension.Dim1D:
|
|
dimension = 0;
|
|
break;
|
|
case ImageDimension.Dim1DArray:
|
|
dimension = 1;
|
|
break;
|
|
case ImageDimension.Dim2D:
|
|
dimension = 2;
|
|
break;
|
|
case ImageDimension.Dim2DArray:
|
|
dimension = 3;
|
|
break;
|
|
case ImageDimension.Dim2DMsaa:
|
|
if (!sampled)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
dimension = 4;
|
|
break;
|
|
case ImageDimension.Dim2DMsaaArray:
|
|
if (!sampled)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
dimension = 5;
|
|
break;
|
|
case ImageDimension.Dim3D:
|
|
dimension = sampled ? 6u : 4u;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
return (DescriptorBindingKind)(baseBinding + dimension);
|
|
}
|
|
|
|
public static ImageResourceClass ResourceClass(DescriptorBindingKind kind)
|
|
{
|
|
var value = (uint)kind;
|
|
if (value is >= SampledCubeFloatBinding and <= SampledCubeSintBinding or
|
|
>= SampledCompare2DBinding and <= SampledCompareCubeBinding)
|
|
{
|
|
return ImageResourceClass.Sampled;
|
|
}
|
|
|
|
if (value is >= StorageCubeFloatBinding and <= AtomicCubeUintBinding)
|
|
{
|
|
return ImageResourceClass.Storage;
|
|
}
|
|
|
|
if (value >= BindingLayout.FirstImageBinding && value < BindingLayout.FirstStorageImageBinding)
|
|
{
|
|
return ImageResourceClass.Sampled;
|
|
}
|
|
|
|
if (value >= BindingLayout.FirstStorageImageBinding && value < (uint)DescriptorBindingKind.Samplers)
|
|
{
|
|
return ImageResourceClass.Storage;
|
|
}
|
|
|
|
return ImageResourceClass.None;
|
|
}
|
|
|
|
public static uint ArrayIndex(DescriptorBindingKind kind) => (uint)kind - BindingLayout.FirstImageBinding;
|
|
|
|
public static bool IsCube(DescriptorBindingKind kind) =>
|
|
(uint)kind is >= SampledCubeFloatBinding and <= AtomicCubeUintBinding or SampledCompareCubeBinding;
|
|
|
|
private static readonly ImageDimension[] SampledDimensions =
|
|
[
|
|
ImageDimension.Dim1D, ImageDimension.Dim1DArray, ImageDimension.Dim2D, ImageDimension.Dim2DArray,
|
|
ImageDimension.Dim2DMsaa, ImageDimension.Dim2DMsaaArray, ImageDimension.Dim3D,
|
|
];
|
|
|
|
private static readonly ImageDimension[] StorageDimensions =
|
|
[
|
|
ImageDimension.Dim1D, ImageDimension.Dim1DArray, ImageDimension.Dim2D, ImageDimension.Dim2DArray, ImageDimension.Dim3D,
|
|
];
|
|
|
|
// The class, numeric class and dimension an image binding kind declares.
|
|
public static (ImageResourceClass ResourceClass, ImageNumericClass NumericClass, ImageDimension Dimension, bool Atomic) Describe(DescriptorBindingKind kind)
|
|
{
|
|
var index = (uint)kind;
|
|
switch (index)
|
|
{
|
|
case SampledCompare2DBinding:
|
|
return (ImageResourceClass.Sampled, ImageNumericClass.Float, ImageDimension.Dim2D, false);
|
|
case SampledCompare2DArrayBinding:
|
|
case SampledCompareCubeBinding:
|
|
return (ImageResourceClass.Sampled, ImageNumericClass.Float, ImageDimension.Dim2DArray, false);
|
|
}
|
|
|
|
if (index is >= SampledCubeFloatBinding and <= SampledCubeSintBinding)
|
|
{
|
|
var numericClass = index switch
|
|
{
|
|
SampledCubeFloatBinding => ImageNumericClass.Float,
|
|
SampledCubeUintBinding => ImageNumericClass.Uint,
|
|
_ => ImageNumericClass.Sint,
|
|
};
|
|
return (ImageResourceClass.Sampled, numericClass, ImageDimension.Dim2DArray, false);
|
|
}
|
|
|
|
if (index is >= StorageCubeFloatBinding and <= StorageCubeUintBinding)
|
|
{
|
|
return (ImageResourceClass.Storage,
|
|
index == StorageCubeFloatBinding ? ImageNumericClass.Float : ImageNumericClass.Uint,
|
|
ImageDimension.Dim2DArray, false);
|
|
}
|
|
|
|
if (index == AtomicCubeUintBinding)
|
|
{
|
|
return (ImageResourceClass.Storage, ImageNumericClass.Uint, ImageDimension.Dim2DArray, true);
|
|
}
|
|
|
|
if (index >= SampledFloatBinding && index < StorageFloatBinding)
|
|
{
|
|
var offset = index - SampledFloatBinding;
|
|
var numericClass = (offset / 7) switch { 0 => ImageNumericClass.Float, 1 => ImageNumericClass.Uint, _ => ImageNumericClass.Sint };
|
|
return (ImageResourceClass.Sampled, numericClass, SampledDimensions[offset % 7], false);
|
|
}
|
|
|
|
if (index >= StorageFloatBinding && index < AtomicUintBinding)
|
|
{
|
|
var offset = index - StorageFloatBinding;
|
|
return (ImageResourceClass.Storage, offset / 5 == 0 ? ImageNumericClass.Float : ImageNumericClass.Uint, StorageDimensions[offset % 5], false);
|
|
}
|
|
|
|
if (index >= AtomicUintBinding && index < (uint)DescriptorBindingKind.Samplers)
|
|
{
|
|
return (ImageResourceClass.Storage, ImageNumericClass.Uint, StorageDimensions[index - AtomicUintBinding], true);
|
|
}
|
|
|
|
return (ImageResourceClass.None, ImageNumericClass.Unsupported, ImageDimension.Unknown, false);
|
|
}
|
|
}
|
|
|
|
// The descriptor set layout of one compiled program and its push data: user-data
|
|
// registers, the shader base, packed memory offsets, and optional dispatch limits.
|
|
public sealed class BindingLayout : IEquatable<BindingLayout>
|
|
{
|
|
public const uint FirstImageBinding = 1;
|
|
public const uint FirstStorageImageBinding = 22;
|
|
public const uint ImageBindingCount = 45;
|
|
public const uint NoShaderBase = uint.MaxValue;
|
|
public const uint ShaderBaseDwordCount = 2;
|
|
private const int ScalarRegisterCount = 256;
|
|
|
|
public uint PushDataStartDword { get; init; } = PushData.NoStart;
|
|
public uint AllocationCursor { get; init; }
|
|
public uint ShaderBaseDword { get; init; } = NoShaderBase;
|
|
public uint MemoryOffsetDword { get; init; }
|
|
public uint MemoryOffsetCount { get; init; }
|
|
public bool UsesDispatchThreadLimits { get; init; }
|
|
|
|
// The module reads the internal-resolution dwords: the factor that maps a guest pixel
|
|
// onto host texels, its reciprocal, the mask of scaled image resources and the factor
|
|
// the pixel position is divided by.
|
|
public bool UsesRenderScale { get; init; }
|
|
public IReadOnlyList<uint> UserDataRegisters { get; init; } = [];
|
|
public IReadOnlyList<DescriptorBinding> Descriptors { get; init; } = [];
|
|
|
|
public const uint RenderScaleDwordCount = 5;
|
|
|
|
public uint DispatchThreadLimitsDword => MemoryOffsetDword + (MemoryOffsetCount + 3) / 4;
|
|
|
|
public uint RenderScaleDword => DispatchThreadLimitsDword + (UsesDispatchThreadLimits ? 3u : 0u);
|
|
|
|
public uint ShaderDataDwordCount => RenderScaleDword + (UsesRenderScale ? RenderScaleDwordCount : 0u);
|
|
|
|
public bool UsesPushData => PushDataStartDword != PushData.NoStart;
|
|
|
|
public bool UsesShaderBase => ShaderBaseDword != NoShaderBase;
|
|
|
|
public void AdvancePushData(ref uint cursor)
|
|
{
|
|
if (UsesPushData)
|
|
{
|
|
cursor = PushDataStartDword + ShaderDataDwordCount;
|
|
}
|
|
}
|
|
|
|
public static uint NativeBindingIndex(ShaderStage stage, DescriptorBindingKind kind) =>
|
|
(uint)kind + (stage == ShaderStage.Pixel ? (uint)DescriptorBindingKind.Count : 0);
|
|
|
|
public DescriptorBinding? Find(DescriptorBindingKind kind) => Descriptors.FirstOrDefault(binding => binding.Kind == kind);
|
|
|
|
// The user-data registers live at program entry: those some path reads before it
|
|
// writes them, found by liveness over the control-flow graph, in ascending order.
|
|
public static IReadOnlyList<uint> CollectUserDataRegisters(Gen5ShaderProgram program, uint userDataBase, uint userDataCount)
|
|
{
|
|
var controlFlow = IrControlFlowGraph.Build(program.Instructions, Gen5IrBranchResolver.Instance);
|
|
var blockCount = controlFlow.Blocks.Count;
|
|
var uses = new bool[blockCount][];
|
|
var definitions = new bool[blockCount][];
|
|
for (var block = 0; block < blockCount; block++)
|
|
{
|
|
uses[block] = new bool[ScalarRegisterCount];
|
|
definitions[block] = new bool[ScalarRegisterCount];
|
|
}
|
|
|
|
foreach (var instruction in program.Instructions)
|
|
{
|
|
var block = controlFlow.BlockOf(instruction.Pc);
|
|
if (block >= 0)
|
|
{
|
|
RecordUsesAndDefinitions(instruction, uses[block], definitions[block]);
|
|
}
|
|
}
|
|
|
|
// live-in = uses | (live-out & registers the block does not define), to a fixpoint on bit sets.
|
|
const int Words = ScalarRegisterCount / 64;
|
|
var useBits = new ulong[blockCount * Words];
|
|
var passBits = new ulong[blockCount * Words];
|
|
var liveIn = new ulong[blockCount * Words];
|
|
for (var block = 0; block < blockCount; block++)
|
|
{
|
|
for (var register = 0; register < ScalarRegisterCount; register++)
|
|
{
|
|
var bit = 1UL << (register & 63);
|
|
if (uses[block][register])
|
|
useBits[block * Words + (register >> 6)] |= bit;
|
|
if (!definitions[block][register])
|
|
passBits[block * Words + (register >> 6)] |= bit;
|
|
}
|
|
}
|
|
|
|
var changed = blockCount != 0;
|
|
while (changed)
|
|
{
|
|
changed = false;
|
|
for (var block = blockCount - 1; block >= 0; block--)
|
|
{
|
|
for (var word = 0; word < Words; word++)
|
|
{
|
|
var liveOut = 0UL;
|
|
foreach (var successor in controlFlow.Successors[block])
|
|
{
|
|
liveOut |= liveIn[successor * Words + word];
|
|
}
|
|
|
|
var index = block * Words + word;
|
|
var live = useBits[index] | (liveOut & passBits[index]);
|
|
if (live != liveIn[index])
|
|
{
|
|
liveIn[index] = live;
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var registers = new List<uint>();
|
|
for (uint index = 0; index < userDataCount && blockCount != 0; index++)
|
|
{
|
|
var register = userDataBase + index;
|
|
if (register < ScalarRegisterCount && (liveIn[register >> 6] & (1UL << (int)(register & 63))) != 0)
|
|
{
|
|
registers.Add(register);
|
|
}
|
|
}
|
|
|
|
return registers;
|
|
}
|
|
|
|
// Reads count as uses only before the block defines the register: scalar sources at
|
|
// their width plus the descriptor, address and offset registers of memory instructions.
|
|
private static void RecordUsesAndDefinitions(Gen5ShaderInstruction instruction, bool[] uses, bool[] definitions)
|
|
{
|
|
void Use(uint register, uint count)
|
|
{
|
|
for (uint index = 0; index < count; index++)
|
|
{
|
|
if (register + index < ScalarRegisterCount && !definitions[register + index])
|
|
{
|
|
uses[register + index] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
var width = instruction.Opcode.Contains("64", StringComparison.Ordinal) ? 2u : 1u;
|
|
foreach (var source in instruction.Sources)
|
|
{
|
|
if (source.Kind == Gen5OperandKind.ScalarRegister)
|
|
{
|
|
Use(source.Value, width);
|
|
}
|
|
}
|
|
|
|
// These read the register their destination field names: the bit sets and the SOPK
|
|
// accumulations modify it, and the SOPK compares only read it.
|
|
var comparesDestination = instruction.Encoding == Gen5ShaderEncoding.Sopk &&
|
|
instruction.Opcode.StartsWith("SCmpk", StringComparison.Ordinal);
|
|
if (comparesDestination ||
|
|
instruction.Opcode is "SBitset0B32" or "SBitset1B32" ||
|
|
instruction.Encoding == Gen5ShaderEncoding.Sopk && instruction.Opcode is "SAddkI32" or "SMulkI32" or "SCmovkI32")
|
|
{
|
|
foreach (var destination in instruction.Destinations)
|
|
{
|
|
if (destination.Kind == Gen5OperandKind.ScalarRegister)
|
|
{
|
|
Use(destination.Value, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (comparesDestination)
|
|
{
|
|
return;
|
|
}
|
|
|
|
switch (instruction.Control)
|
|
{
|
|
case Gen5ImageControl image:
|
|
Use(image.ScalarResource, 8);
|
|
if (instruction.Opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
|
|
instruction.Opcode.StartsWith("ImageGather", StringComparison.Ordinal))
|
|
{
|
|
Use(image.ScalarSampler, 4);
|
|
}
|
|
|
|
break;
|
|
case Gen5ScalarMemoryControl scalar:
|
|
if (instruction.Sources.Count != 0 && instruction.Sources[0].Kind == Gen5OperandKind.ScalarRegister)
|
|
{
|
|
Use(instruction.Sources[0].Value, instruction.Opcode.StartsWith("SBufferLoad", StringComparison.Ordinal) ? 4u : 2u);
|
|
}
|
|
|
|
if (scalar.DynamicOffsetRegister is { } offsetRegister)
|
|
{
|
|
Use(offsetRegister, 1);
|
|
}
|
|
|
|
break;
|
|
case Gen5GlobalMemoryControl global when global.ScalarAddress < 255:
|
|
Use(global.ScalarAddress, 2);
|
|
break;
|
|
case Gen5BufferMemoryControl buffer:
|
|
Use(buffer.ScalarResource, 4);
|
|
break;
|
|
}
|
|
|
|
// A record that lists every written register defines each once; a single listed
|
|
// register of a multiword instruction defines the whole width.
|
|
var scalarDestinations = instruction.Destinations.Where(destination => destination.Kind == Gen5OperandKind.ScalarRegister).ToList();
|
|
if (scalarDestinations.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (instruction.Control is Gen5ScalarMemoryControl memory)
|
|
{
|
|
var first = scalarDestinations.Min(destination => destination.Value);
|
|
var count = Math.Max(memory.DestinationCount, (uint)scalarDestinations.Count);
|
|
for (uint index = 0; index < count && first + index < ScalarRegisterCount; index++)
|
|
{
|
|
definitions[first + index] = true;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var perDestination = scalarDestinations.Count > 1 ? 1u : width;
|
|
foreach (var destination in scalarDestinations)
|
|
{
|
|
for (uint index = 0; index < perDestination && destination.Value + index < ScalarRegisterCount; index++)
|
|
{
|
|
definitions[destination.Value + index] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static bool UsesGlobalDataShare(Gen5ShaderProgram program) =>
|
|
program.Instructions.Any(instruction => instruction.Control is Gen5DataShareControl { Gds: true });
|
|
|
|
public static bool ReadsShaderBase(Gen5ShaderProgram program) =>
|
|
program.Instructions.Any(instruction => instruction.Opcode == "SGetpcB64");
|
|
|
|
// Allocates one program's bindings: buffers, image groups (one element per mip for a
|
|
// dynamic-mip storage image), samplers, shared buffers, push data or its fallback.
|
|
public static BindingLayout Allocate(
|
|
ShaderResourceInfo info,
|
|
IReadOnlyList<uint> userDataRegisters,
|
|
bool usesGlobalDataShare,
|
|
bool usesFlattenedTable,
|
|
bool usesShaderBase,
|
|
uint pushDataStartDword = 0,
|
|
bool usesDispatchThreadLimits = false,
|
|
bool usesRenderScale = false)
|
|
{
|
|
var shaderBaseDword = usesShaderBase ? (uint)userDataRegisters.Count : NoShaderBase;
|
|
var memoryOffsetDword = (uint)userDataRegisters.Count + (usesShaderBase ? ShaderBaseDwordCount : 0);
|
|
var memoryOffsetCount = (uint)info.Buffers.Count;
|
|
var shaderDataDwords = memoryOffsetDword + (memoryOffsetCount + 3) / 4 + (usesDispatchThreadLimits ? 3u : 0u) +
|
|
(usesRenderScale ? RenderScaleDwordCount : 0u);
|
|
var pushStart = PushData.StartFor(pushDataStartDword, shaderDataDwords);
|
|
var descriptors = new List<DescriptorBinding>();
|
|
if (info.Buffers.Count != 0)
|
|
{
|
|
descriptors.Add(new DescriptorBinding(DescriptorBindingKind.Buffers, Enumerable.Range(0, info.Buffers.Count).Select(index => (uint)index).ToArray()));
|
|
}
|
|
|
|
var imageGroups = new List<uint>[ImageBindingCount];
|
|
for (var index = 0; index < info.Images.Count; index++)
|
|
{
|
|
var image = info.Images[index];
|
|
var kind = ImageDescriptorBinding.ForImage(image);
|
|
if (kind is null)
|
|
{
|
|
throw new ResourcePlanException($"shader binding layout failed: image {index} has an invalid binding class");
|
|
}
|
|
|
|
var group = ImageDescriptorBinding.ArrayIndex(kind.Value);
|
|
if (group >= ImageBindingCount)
|
|
{
|
|
throw new ResourcePlanException($"shader binding layout failed: image {index} has an unmapped binding class");
|
|
}
|
|
|
|
var dynamic = image.MipMode == ImageMipMode.DynamicStorage;
|
|
var count = dynamic ? image.MipCount : 1;
|
|
if (count == 0 || (!dynamic && image.MipCount != 1))
|
|
{
|
|
throw new ResourcePlanException($"shader binding layout failed: image {index} has invalid specialized mip count {image.MipCount}");
|
|
}
|
|
|
|
imageGroups[group] ??= [];
|
|
imageGroups[group].AddRange(Enumerable.Repeat((uint)index, (int)count));
|
|
}
|
|
|
|
for (var group = 0; group < imageGroups.Length; group++)
|
|
{
|
|
if (imageGroups[group] is { Count: > 0 } resources)
|
|
{
|
|
descriptors.Add(new DescriptorBinding((DescriptorBindingKind)(FirstImageBinding + group), resources));
|
|
}
|
|
}
|
|
|
|
if (info.Samplers.Count != 0)
|
|
{
|
|
descriptors.Add(new DescriptorBinding(DescriptorBindingKind.Samplers, Enumerable.Range(0, info.Samplers.Count).Select(index => (uint)index).ToArray()));
|
|
}
|
|
|
|
if (usesGlobalDataShare)
|
|
{
|
|
descriptors.Add(new DescriptorBinding(DescriptorBindingKind.GlobalDataShare, []));
|
|
}
|
|
|
|
if (info.UsesDeviceAddresses)
|
|
{
|
|
descriptors.Add(new DescriptorBinding(DescriptorBindingKind.DeviceAddressPageTable, []));
|
|
descriptors.Add(new DescriptorBinding(DescriptorBindingKind.FaultBuffer, []));
|
|
}
|
|
|
|
var flattened = usesFlattenedTable || info.Images.Any(image => image.IndirectSearchIterations != 0);
|
|
if (flattened)
|
|
{
|
|
descriptors.Add(new DescriptorBinding(DescriptorBindingKind.FlattenedResourceTable, []));
|
|
}
|
|
|
|
if (shaderDataDwords != 0 && pushStart == PushData.NoStart)
|
|
{
|
|
descriptors.Add(new DescriptorBinding(DescriptorBindingKind.ShaderData, []));
|
|
}
|
|
|
|
return new BindingLayout
|
|
{
|
|
PushDataStartDword = pushStart,
|
|
AllocationCursor = pushDataStartDword,
|
|
ShaderBaseDword = shaderBaseDword,
|
|
MemoryOffsetDword = memoryOffsetDword,
|
|
MemoryOffsetCount = memoryOffsetCount,
|
|
UsesDispatchThreadLimits = usesDispatchThreadLimits,
|
|
UsesRenderScale = usesRenderScale,
|
|
UserDataRegisters = userDataRegisters,
|
|
Descriptors = descriptors,
|
|
};
|
|
}
|
|
|
|
// The allocation cursor is context, not layout: two spills from different cursors are equal.
|
|
public bool Equals(BindingLayout? other) =>
|
|
other is not null &&
|
|
PushDataStartDword == other.PushDataStartDword &&
|
|
ShaderBaseDword == other.ShaderBaseDword &&
|
|
MemoryOffsetDword == other.MemoryOffsetDword &&
|
|
MemoryOffsetCount == other.MemoryOffsetCount &&
|
|
UsesDispatchThreadLimits == other.UsesDispatchThreadLimits &&
|
|
UsesRenderScale == other.UsesRenderScale &&
|
|
UserDataRegisters.SequenceEqual(other.UserDataRegisters) &&
|
|
Descriptors.Count == other.Descriptors.Count &&
|
|
Descriptors.Zip(other.Descriptors).All(pair => pair.First.Kind == pair.Second.Kind && pair.First.Resources.SequenceEqual(pair.Second.Resources));
|
|
|
|
public override bool Equals(object? obj) => Equals(obj as BindingLayout);
|
|
|
|
public override int GetHashCode() => HashCode.Combine(PushDataStartDword, MemoryOffsetDword, MemoryOffsetCount, Descriptors.Count, UsesDispatchThreadLimits, UsesRenderScale);
|
|
}
|
|
|
|
// Recomputes the layout an emitter was given from the same inputs and the same push
|
|
// cursor and fails when they differ, so declarations and descriptor writes never drift.
|
|
public static class BindingLayoutValidator
|
|
{
|
|
public static void Validate(
|
|
BindingLayout layout,
|
|
ShaderResourceInfo info,
|
|
IReadOnlyList<uint> userDataRegisters,
|
|
bool usesGlobalDataShare,
|
|
bool usesFlattenedTable,
|
|
bool usesShaderBase,
|
|
ulong hash,
|
|
ShaderStage stage)
|
|
{
|
|
if (layout.UsesDispatchThreadLimits && stage != ShaderStage.Compute)
|
|
{
|
|
throw new ResourcePlanException("Only a compute shader can use dispatch thread limits.");
|
|
}
|
|
|
|
var expected = BindingLayout.Allocate(info, userDataRegisters, usesGlobalDataShare, usesFlattenedTable, usesShaderBase, layout.AllocationCursor, layout.UsesDispatchThreadLimits, layout.UsesRenderScale);
|
|
if (!expected.Equals(layout))
|
|
{
|
|
throw new ResourcePlanException(
|
|
$"shader binding layout does not match its resources: hash=0x{hash:X16} stage={stage} " +
|
|
$"expected=[{Describe(expected)}] actual=[{Describe(layout)}]");
|
|
}
|
|
}
|
|
|
|
private static string Describe(BindingLayout layout) =>
|
|
$"push={layout.PushDataStartDword} base={layout.ShaderBaseDword} offsets={layout.MemoryOffsetDword}+{layout.MemoryOffsetCount} " +
|
|
$"user=[{string.Join(",", layout.UserDataRegisters)}] " +
|
|
string.Join(" ", layout.Descriptors.Select(binding => $"{binding.Kind}:{string.Join(",", binding.Resources)}"));
|
|
}
|