Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update SPIRV-Tools and fix new validation errors. #6511

Merged
merged 23 commits into from
Mar 6, 2025

Conversation

csyonghe
Copy link
Collaborator

@csyonghe csyonghe commented Mar 3, 2025

Closes #6508.

Issue

Recent SPIRV-Tools update adds a new verification rule that disallows explicit layouts (e.g. Offset decoration) on types used from Function/Private/Workgroup etc. address spaces after SPIRV 1.4.

Background

SPIRV makes a distinction of physical and logical types. A physical type is a type with explicit memory layout and can be stored in a user-transparent way in memory. A logical type in contrast does not have a defined layout, and cannot be decorated with any explicit layout deocrations, such as Offset, ArrayStride etc.

The SPIRV spec requires that all buffer elements or global input/outputs to be a physical type, and all variables in the function/private(i.e. ordinary global vars)/workgroup address spaces to be logical.

This means that if the user writes:

struct Foo { int x, y; }

StructuredBuffer<Foo> f;  // or ConstantBuffer, etc.

void main()
{
     Foo localVar;
     localVar = f[0];
     f[1] = localVar;
}

We need to represent Foo as two distinct types when generating SPIRV: the one used inside StructuredBuffer must be a physical type with explicit layout (specifying the offset of Foo::x and Foo::y), and the one used for localVar must be a logical type. Therefore we generate something like:

struct Foo { int x, y; }
struct Foo_std430 { [Offset(0)] int x;  [Offset(4)] int y; }
// If `Foo` is used in a `ConstantBuffer`, we would generate a `Foo_std140` type too.

StructuredBuffer<Foo_std430> f;  // or ConstantBuffer, etc.

Foo_std430 packStorage(Foo f) { ... }
Foo unpackStorage(Foo_Std430 f) {...}

void main()
{
     Foo localVar;
     localVar = unpackStorage(f[0]);
     f[1] = packStorage(localVar);
}

Where Foo_std430 is the physical type for Foo that can be placed in a buffer, and packStorage and unpackStorage functions to convert between physical and logical type when needed.

The above translation is what the existing lowerBufferElementTypeToStorageType is doing.

Currently, when we emit SPIRV, we always emit Offset decorations for both Foo and Foo_std430. However this is against the SPIRV rules, which disallows a type to be declared as a local variable, if the type has explicit layout.

Solution

To fix the SPIRV validation error complaining Foo cannot be used to declare localVar because it has explicit layout, we simply need to mark the lowered storage types (Foo_std430 in the above example) with a [PhysicalType] decoration, and make sure we only emit SPIRV layout decorations (ArrayStride, MatrixStride, ColMajor, RowMajor, Offset) for structs with the [PhyiscalType] decoration.

The rest of the PR is to deal with a bunch of consequences after changing the way we emit SPIRV explicit layout based on whether or not a type is marked [PhysicalType].

  • GLSLStorageBuffer type weren't lowered by lowerBufferElementTypeToStorageType previously, but now needs to be in order to produce valid SPIRV so that the element type is properly marked as physical.
  • This means that lowerBufferElementTypeToStorageType needs to generate pack/unpack functions for unsized array types that can appear in glsl storage buffers, which we weren't handling previously.
  • Array types without explicit stride are now treated as logical type and won't result in a stride being decorated in the resulting SPIRV. We need to be careful to never declare a local variable whose type is an array with explicit stride operand -- that will lead to invalid SPIRV. This leads to a change in the next bullet.
  • packStorage/unpackStorage functions can no longer operate on SSA values, and has to operate on memory pointers instead, because we are no longer to elementExtract from a SSA register of a physical array type (can't copy it to a OpVariable and then do AccessChain, because OpVariable must be a logical type). These functions are inlined before emitting final code, because SPIRV doesn't allow us to pass pointers to functions without using VariablePointers capability. Because we no longer need to lower ElementExtract(array, i) to first copy to local var then do access chain, the resulting code should be much simpler for pack/unpack of large arrays and should be faster to compile by the downstream compiler.
  • When packStorage/unpackStorage operate on pointers, we are effectively deferring the load of a composite to load of separate leaf fields, this means that loadAlign(ptr) is going to be split into a lot of smaller loads, so we must propagate the alignment on the load through the addresses, so that each individual load/store gets the correct alignment.
  • Reimplement ConstBufferType as a wrapper of native pointers instead of a special builtin type, because we no longer want to maintain that code that special cases for ConstBufferTypes with this change. This means we need to:
  • Implemented pointers for GLSL target. The only reason for ConstBufferPointer to exist as a builtin was that it provides some minimal coverage of buffer_reference when targeting GLSL. With pointers supported for GLSL, there is no reason to keep it as a compiler-builtin.

The updated lowerBufferElementTypeToStorageType pass now translates the above example code to the following:

struct Foo { int x, y; }
[PhysicalType] struct Foo_std430 { [Offset(0)] int x;  [Offset(4)] int y; }

StructuredBuffer<Foo_std430> f;  // or ConstantBuffer, etc.

// Pack/unpack function are now inlined by Slang, because they
// need to work on references instead of values.
[ForceInline] void packStorage(Foo_std430* dest, Foo f) { ... }
[ForceInline] Foo unpackStorage(Foo_Std430* src) {...}

void main()
{
     Foo localVar;

     // unpack function takes in a pointer to the source value, instead of an SSA value.
     localVar = unpackStorage(&f[0]);

     // destination driven packing function that writes directly into a pointer,
     // instead of returning an SSA value.
     packStorage(&f[1], localVar); 
}

Copilot Summary

This pull request includes significant changes to the handling of pointer types and various optimizations in the SPIR-V and GLSL code generation. The most important changes include the refactoring of ConstBufferPointerType, updates to GLSL source emission, and improvements to SPIR-V emission and optimization.

Refactoring and Simplification:

GLSL Source Emission:

SPIR-V Emission and Optimization:

These changes collectively enhance the handling of pointer types and improve the efficiency and correctness of the generated GLSL and SPIR-V code.

@csyonghe csyonghe added the pr: non-breaking PRs without breaking changes label Mar 3, 2025
@csyonghe csyonghe requested a review from a team as a code owner March 3, 2025 19:35
Copy link
Collaborator

@jkwak-work jkwak-work left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

We were talking about the logical storage and physical storage in WorkGraphs meeting today coincidently.
I guess this is a thing going around recently.

Copy link
Contributor

@kaizhangNV kaizhangNV left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@csyonghe csyonghe merged commit 4485cf3 into shader-slang:master Mar 6, 2025
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr: non-breaking PRs without breaking changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Invalid SPIR-V Explicit Layout on texture descriptor indexing
3 participants