You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
If SPIR-V mandates a distinct type exists for Structs with different Layouts then the HLSL structs with different layouts should be distinct types
#387
In Vulkan (and historically inherited from GL) there exist layout rules which need to be respected for using structs for different resources, for example in unextended Vulkan 1.1:
UBOs (HLSL Constant Buffers) need to follow std140
SSBOs (HLSL ByteAddress and Structured Buffers) can follow std430 or std140
Push Constants need to follow std140 (IIRC)
Then extensions which since got promoted to core features in 1.3, which are now also on the 2024 Roadmap introduce relaxations such as:
uniform buffer Standard Layout, can use std430 for UBOs
scalar layout which relaxes all strange vector constraints (such as alignments of 16 but size of 12 on float32_t3) essentially making your C++ and HLSL structures match in layout
Scalar Layout is almost ubiquitous across Vulkan 1.3 platforms, while the devices that DX12 targets and also run Vulkan 1.3, 100% support scalar layout.
The most important caveat
In SPIR-V GL/VK layout information in the form of std140, std430, or scalar does not exist. Only the member byte offsets for every member of a struct are present in the SPIR-V and they can be validated against the enabled features and extensions.
In fact, this is why Renderdoc shows std140, or std430 for Constant Buffers using types which would have satisfied those constraints anyway. Because it has to guess and eliminate the possible layouts.
Caveat on a caveat - layoutless objects
Private, Function and Shared variables don't even need (or in fact without extensions, shouldn't have) member byte offset SPIR-V decorations on their OpTypeStruct.
Important Baseline Limit of SPIR-V 1.4
If SPIR-V 1.4 is the lowest emitted version, we have a OpLogicalCopy which can be used to massively simplify the codegen when one needs to convert between identical structs of different types with different layouts. https://godbolt.org/z/9qYezeWaW
Problem Statement
Inline SPIR-V
As inline SPIR-V gets more extensively used, interactions when argument, return or pointers to types of structs come up.
A prime example is microsoft/DirectXShaderCompiler#6541 when attempting to use vk::SpirvType to implement Buffer Device Address loads and stores.
Currently HLSL does not distinguish between the types of the result of a load from a ConstantBuffer<T> and StructuredBuffer<T> even if they use different layouts, or those and private or groupshared variables as those don't have a layout specified.
DXC's SPIR-V codegen then has to deduce which SPIR-V struct declaration to use, and if an OpCopyLogical needs to be performed when e.g. assigning a UBO load result to a groupshared variable. This deduction has to be inferred from context which is missing for SPIR-V and is a general hindrance for other cases.
Implementability of offsetof and correctness of sizeof
Currently sizeof(T) and sizeof(var) works, but there's no meaningful definition of what a "layoutless" structure variable should report. Sizeof currenlty reports values but there's no concrete spec saying which layout it reports for.
Also offsetof(type,member) is not implemented and exactly this is one of the roadblocks preventing its implementation.
Possible Solution
Different Member Offsets == Different Types
Let the user assign member offsets with [[vk::byte_offset(N)]] (basically expanding to [[vk::ext_decorate(/*Offset*/ 35,N)]]) or using existing syntax microsoft/DirectXShaderCompiler#6541 (comment)
Also to ensure ergonomics there should be a [[vk::layout(none/std140/std430/scalar)]] which you can put on a struct to "initialize" the member offsets to easy defaults which you can then override for members you need.
This way if you want the same struct logically (same members) you need to pick a different identifier, and it won't result in is_same_v<T_layout_A,T_layout_B>==true
Allow conversion assignments of structs with logically identical members and use OpLogicalCopy for that
This will allow a = b; without issue.
This compatibility with OpLogicalCopy does not need to be expensive to check if this can be performed as the structs member and type definitions can be Markle Tree hashed with a large enough hash (e.g. 256bit - blake3).
Scalar Layout as the new default, don't support other layouts as defaults via compile flags
This way any default struct definition will work with ConstantBuffer, ByteAddressBuffer, StructuredBuffer and vk:BufferPointer/vk::RawBuffer.../vk::SpirvType with storage class that can have a layout.
If the user requires backward compatible structure definitions, then let them define their type T as vk::NamedLayout<T,LayoutEnum>, example
template<typename T>
using ConstantBufferStd140 = vk::NamedDefaultLayout<T,LayoutEnum::STD140>;
This moves compile flag galore, to annotating every type usage in a descriptor which deviates from the new default.
With C++ not settling on reflection, we want to keep such magic to a minimum.
The offsetless_v trait would require that all member's types are also layoutless. Fundamental types would report as layoutless.
Define the old default ConstantBuffer<T>, StrcturedBuffer<T> and friends in terms of the above
Example
template<typename T>
using ConstantBuffer = /*implementation via struct partial specs*/;
This way if the particular layouts are allowed for a UBO (via the compile options you pass to the compiler to determine features which can be relied on), a partial spec or an enable_if can check e.g. satifies_block_layout_v<T,BlockLayout::std430>.
Open Problems
How to handle sizeof and offsetof for Layoutless types
@Hugobros3 suggests to "pretend" the layoutless structs follow scalar layout.
Another option is just to refuse to compile sizeof and addressof on a struct where all byte offsets of members and sizes of members are not known.
Allow the re-laying of an offserless_v<T> type so that old ConstantBuffer and friends can be used
Basically a magic spirv::laid_out_t<T,BlockLayout> produces a derived type from T.
It probably not a good idea because needing to recurse through the members, and a huge implementation burden.
Alternatives Considered
All of the below don't really lend themselves to making more "user-space" traits for laid out types and partial specs depending on the layout.
Simple Attributes
[[a]] int x;
[[b]] int y;
static_assert(!std::is_same_v<decltype(x),decltype(y)>,"attributes don't actually differ the type");
Template Alias to apply layouts
template<typename T, Layout _layout>
using LaidOut = [[vk::layout(_layout)]] T;
=
structpair
{
float x,y;
};
static_assert(!std::is_same_v<LaidOut<pair,STD140>,LaidOut<pair,STD430> >,"attribute applied from a template does not a new type make");
The text was updated successfully, but these errors were encountered:
devshgraphicsprogramming
changed the title
[WIP] If SPIR-V mandates a distinct type exists for Structs with different Layouts then the HLSL structs with different layouts should be distinct types
If SPIR-V mandates a distinct type exists for Structs with different Layouts then the HLSL structs with different layouts should be distinct types
Feb 19, 2025
P.s. it seems to only be the Address Space attribute that currently clang understands and template matches properly https://godbolt.org/z/zrEPaW8qv
So theoretically the "Template Alias to apply layouts" which would work same as clang::address_space could be an option at the cost of maintaining another "special attribute" that doesn't get lost when type and template matching
Background
In Vulkan (and historically inherited from GL) there exist layout rules which need to be respected for using structs for different resources, for example in unextended Vulkan 1.1:
Then extensions which since got promoted to core features in 1.3, which are now also on the 2024 Roadmap introduce relaxations such as:
float32_t3
) essentially making your C++ and HLSL structures match in layoutScalar Layout is almost ubiquitous across Vulkan 1.3 platforms, while the devices that DX12 targets and also run Vulkan 1.3, 100% support scalar layout.
The most important caveat
In SPIR-V GL/VK layout information in the form of std140, std430, or scalar does not exist.
Only the member byte offsets for every member of a struct are present in the SPIR-V and they can be validated against the enabled features and extensions.
In fact, this is why Renderdoc shows std140, or std430 for Constant Buffers using types which would have satisfied those constraints anyway. Because it has to guess and eliminate the possible layouts.
Caveat on a caveat - layoutless objects
Private, Function and Shared variables don't even need (or in fact without extensions, shouldn't have) member byte offset SPIR-V decorations on their
OpTypeStruct
.Important Baseline Limit of SPIR-V 1.4
If SPIR-V 1.4 is the lowest emitted version, we have a
OpLogicalCopy
which can be used to massively simplify the codegen when one needs to convert between identical structs of different types with different layouts.https://godbolt.org/z/9qYezeWaW
Problem Statement
Inline SPIR-V
As inline SPIR-V gets more extensively used, interactions when argument, return or pointers to types of structs come up.
A prime example is microsoft/DirectXShaderCompiler#6541 when attempting to use
vk::SpirvType
to implement Buffer Device Address loads and stores.Currently HLSL does not distinguish between the types of the result of a load from a
ConstantBuffer<T>
andStructuredBuffer<T>
even if they use different layouts, or those and private orgroupshared
variables as those don't have a layout specified.DXC's SPIR-V codegen then has to deduce which SPIR-V struct declaration to use, and if an
OpCopyLogical
needs to be performed when e.g. assigning a UBO load result to agroupshared
variable. This deduction has to be inferred from context which is missing for SPIR-V and is a general hindrance for other cases.Implementability of
offsetof
and correctness ofsizeof
Currently
sizeof(T)
andsizeof(var)
works, but there's no meaningful definition of what a "layoutless" structure variable should report. Sizeof currenlty reports values but there's no concrete spec saying which layout it reports for.Also
offsetof(type,member)
is not implemented and exactly this is one of the roadblocks preventing its implementation.Possible Solution
Different Member Offsets == Different Types
Let the user assign member offsets with
[[vk::byte_offset(N)]]
(basically expanding to[[vk::ext_decorate(/*Offset*/ 35,N)]]
) or using existing syntax microsoft/DirectXShaderCompiler#6541 (comment)Also to ensure ergonomics there should be a
[[vk::layout(none/std140/std430/scalar)]]
which you can put on a struct to "initialize" the member offsets to easy defaults which you can then override for members you need.This way if you want the same struct logically (same members) you need to pick a different identifier, and it won't result in
is_same_v<T_layout_A,T_layout_B>==true
Allow conversion assignments of structs with logically identical members and use
OpLogicalCopy
for thatThis will allow
a = b;
without issue.This compatibility with
OpLogicalCopy
does not need to be expensive to check if this can be performed as the structs member and type definitions can be Markle Tree hashed with a large enough hash (e.g. 256bit - blake3).Scalar Layout as the new default, don't support other layouts as defaults via compile flags
This way any default struct definition will work with
ConstantBuffer
,ByteAddressBuffer
,StructuredBuffer
andvk:BufferPointer
/vk::RawBuffer...
/vk::SpirvType
with storage class that can have a layout.If the user requires backward compatible structure definitions, then let them define their type
T
asvk::NamedLayout<T,LayoutEnum>
, exampleThis moves compile flag galore, to annotating every type usage in a descriptor which deviates from the new default.
Traits to determine layout compatibility
With C++ not settling on reflection, we want to keep such magic to a minimum.
The
offsetless_v
trait would require that all member's types are also layoutless. Fundamental types would report as layoutless.Define the old default
ConstantBuffer<T>
,StrcturedBuffer<T>
and friends in terms of the aboveExample
This way if the particular layouts are allowed for a UBO (via the compile options you pass to the compiler to determine features which can be relied on), a partial spec or an
enable_if
can check e.g.satifies_block_layout_v<T,BlockLayout::std430>
.Open Problems
How to handle
sizeof
andoffsetof
for Layoutless types@Hugobros3 suggests to "pretend" the layoutless structs follow scalar layout.
Another option is just to refuse to compile
sizeof
andaddressof
on a struct where all byte offsets of members and sizes of members are not known.Allow the re-laying of an
offserless_v<T>
type so that oldConstantBuffer
and friends can be usedBasically a magic
spirv::laid_out_t<T,BlockLayout>
produces a derived type fromT
.It probably not a good idea because needing to recurse through the members, and a huge implementation burden.
Alternatives Considered
All of the below don't really lend themselves to making more "user-space" traits for laid out types and partial specs depending on the layout.
Simple Attributes
Template Alias to apply layouts
The text was updated successfully, but these errors were encountered: