Skip to content

Commit 06e89f3

Browse files
committed
[naga spv-out] Handle 2-row matrices in uniform buffers
Two-row matrices in uniform buffers in Vulkan/SPIR-V's default "extended" (std140) layout do not adhere to WGSL/Naga IR's layout rules - each column is aligned to a minimum of 16 bytes rather than just the vector size. To work around this, we emit additional std140 compatible type declarations for each type used in a uniform buffer that requires one. Any two-row matrix struct member is decomposed into the containing struct as separate vectors for each column. Two-row matrices used directly as uniform buffers are wrapped in a struct with a vector member for each column, and arrays (of arrays, etc) of two-row matrices are declared as arrays (of arrays) of structs containing a vector member for each column. When loading such a value from a uniform buffer, we convert from the std140 compatible type to the regular type immediately after loading. Accesses of a uniform two-row matrix's columns using constant indices are rewritten to access the containing struct's vector members directly. Accesses using dynamic indices are implemented by loading and converting the matrix, extracting each column then `switch`ing on the index to select the correct column. We can now remove the expected failure annotation for the Vulkan backend on the uniform_input struct_layout test, as it now passes. We additionally make the hlsl_mat_cx*.wgsl snapshot tests run for the SPIR-V backend too, and remove the "hlsl" prefix from the name, as they are no longer just relevant for HLSL.
1 parent 8ef1922 commit 06e89f3

21 files changed

+3781
-1379
lines changed

naga/src/back/spv/block.rs

Lines changed: 485 additions & 73 deletions
Large diffs are not rendered by default.

naga/src/back/spv/helpers.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,44 @@ pub fn global_needs_wrapper(ir_module: &crate::Module, var: &crate::GlobalVariab
122122
}
123123
}
124124

125+
/// Returns true if `pointer` refers to two-row matrix which is a member of a
126+
/// struct in the [`crate::AddressSpace::Uniform`] address space.
127+
pub fn is_uniform_matcx2_struct_member_access(
128+
ir_function: &crate::Function,
129+
fun_info: &crate::valid::FunctionInfo,
130+
ir_module: &crate::Module,
131+
pointer: Handle<crate::Expression>,
132+
) -> bool {
133+
if let crate::TypeInner::Pointer {
134+
base: pointer_base_type,
135+
space: crate::AddressSpace::Uniform,
136+
} = *fun_info[pointer].ty.inner_with(&ir_module.types)
137+
{
138+
if let crate::TypeInner::Matrix {
139+
rows: crate::VectorSize::Bi,
140+
..
141+
} = ir_module.types[pointer_base_type].inner
142+
{
143+
if let crate::Expression::AccessIndex {
144+
base: parent_pointer,
145+
..
146+
} = ir_function.expressions[pointer]
147+
{
148+
if let crate::TypeInner::Pointer {
149+
base: parent_type, ..
150+
} = *fun_info[parent_pointer].ty.inner_with(&ir_module.types)
151+
{
152+
if let crate::TypeInner::Struct { .. } = ir_module.types[parent_type].inner {
153+
return true;
154+
}
155+
}
156+
}
157+
}
158+
}
159+
160+
false
161+
}
162+
125163
///HACK: this is taken from std unstable, remove it when std's floor_char_boundary is stable
126164
trait U8Internal {
127165
fn is_utf8_char_boundary(&self) -> bool;

naga/src/back/spv/index.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -536,17 +536,18 @@ impl BlockContext<'_> {
536536
/// Emit code to subscript a vector by value with a computed index.
537537
///
538538
/// Return the id of the element value.
539+
///
540+
/// If `base_id_override` is provided, it is used as the vector expression
541+
/// to be subscripted into, rather than the cached value of `base`.
539542
pub(super) fn write_vector_access(
540543
&mut self,
541-
expr_handle: Handle<crate::Expression>,
544+
result_type_id: Word,
542545
base: Handle<crate::Expression>,
543-
index: Handle<crate::Expression>,
546+
base_id_override: Option<Word>,
547+
index: GuardedIndex,
544548
block: &mut Block,
545549
) -> Result<Word, Error> {
546-
let result_type_id = self.get_expression_type_id(&self.fun_info[expr_handle].ty);
547-
548-
let base_id = self.cached[base];
549-
let index = GuardedIndex::Expression(index);
550+
let base_id = base_id_override.unwrap_or_else(|| self.cached[base]);
550551

551552
let result_id = match self.write_bounds_check(base, index, block)? {
552553
BoundsCheckResult::KnownInBounds(known_index) => {

naga/src/back/spv/mod.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,91 @@
11
/*!
22
Backend for [SPIR-V][spv] (Standard Portable Intermediate Representation).
33
4+
# Layout of values in `uniform` buffers
5+
6+
WGSL's ["Internal Layout of Values"][ilov] rules specify how each WGSL type
7+
should be stored in `uniform` and `storage` buffers, and Naga IR adheres to
8+
these rules. The SPIR-V we generate must access values in that form, even when
9+
it is not what Vulkan would use normally. Fortunately the rules for `storage`
10+
buffers match Vulkan's, but some adjustments must be made when emitting SPIR-V
11+
for `uniform` buffers.
12+
13+
## Padding in two-row matrices
14+
15+
In Vulkan's ["extended layout"][extended-layout] (also known as std140) used
16+
for `uniform` buffers, matrices are defined in terms of arrays of their vector
17+
type, and arrays are defined to have an alignment equal to the alignment of
18+
their element type rounded up to a multiple of 16. This means that each column
19+
of the vector has a minimum alignment of 16. WGSL, and consequently Naga IR, on
20+
the other hand defines each column to have an alignment equal to the alignment
21+
of the vector type, without being rounded up to 16.
22+
23+
To compensate for this, for any `struct` used as a `uniform` buffer which
24+
contains a two-row matrix, we declare an additional "std140 compatible" type
25+
in which each column of the matrix has been decomposed into the containing
26+
struct. For example, the following WGSL struct type:
27+
28+
```ignore
29+
struct Baz {
30+
m: mat3x2<f32>,
31+
}
32+
```
33+
34+
is rendered as the SPIR-V struct type:
35+
36+
```ignore
37+
OpTypeStruct %v2float %v2float %v2float
38+
```
39+
40+
This has the effect that struct indices in Naga IR for such types do not
41+
correspond to the struct indices used in SPIR-V. A mapping of struct indices
42+
for these types is maintained in [`Std140CompatTypeInfo`].
43+
44+
Additionally, any two-row matrices that are declared directly as uniform
45+
buffers without being wrapped in a struct are declared as a struct containing a
46+
vector member for each column. Any array of a two-row matrix in a uniform
47+
buffer is declared as an array of a struct containing a vector member for each
48+
column. Any struct or array within a uniform buffer which contains a member or
49+
whose base type requires requires a std140 compatible type declaration, itself
50+
requires a std140 compatible type declaration.
51+
52+
Whenever a value of such a type is [`loaded`] we insert code to convert the
53+
loaded value from the std140 compatible type to the regular type. This occurs
54+
in `BlockContext::write_checked_load`, making use of the wrapper function
55+
defined by `Writer::write_wrapped_convert_from_std140_compat_type`. For matrices
56+
that have been decomposed as separate columns in the containing struct, we load
57+
each column separately then composite the matrix type in
58+
`BlockContext::maybe_write_load_uniform_matcx2_struct_member`.
59+
60+
Whenever a column of a matrix that has been decomposed into its containing
61+
struct is [`accessed`] with a constant index we adjust the emitted access chain
62+
to access from the containing struct instead, in `BlockContext::write_access_chain`.
63+
64+
Whenever a column of a uniform buffer two-row matrix is [`dynamically accessed`]
65+
we must first load the matrix type, converting it from its std140 compatible
66+
type as described above, then access the column using the wrapper function
67+
defined by `Writer::write_wrapped_matcx2_get_column`. This is handled by
68+
`BlockContext::maybe_write_uniform_matcx2_dynamic_access`.
69+
70+
Note that this approach differs somewhat from the equivalent code in the HLSL
71+
backend. For HLSL all structs containing two-row matrices (or arrays of such)
72+
have their declarations modified, not just those used as uniform buffers.
73+
Two-row matrices and arrays of such only use modified type declarations when
74+
used as uniform buffers, or additionally when used as struct member in any
75+
context. This avoids the need to convert struct values when loading from uniform
76+
buffers, but when loading arrays and matrices from uniform buffers or from any
77+
struct the conversion is still required. In contrast, the approach used here
78+
always requires converting *any* affected type when loading from a uniform
79+
buffer, but consistently *only* when loading from a uniform buffer. As a result
80+
this also means we only have to handle loads and not stores, as uniform buffers
81+
are read-only.
82+
483
[spv]: https://www.khronos.org/registry/SPIR-V/
84+
[ilov]: https://gpuweb.github.io/gpuweb/wgsl/#internal-value-layout
85+
[extended-layout]: https://docs.vulkan.org/spec/latest/chapters/interfaces.html#interfaces-resources-layout
86+
[`loaded`]: crate::Expression::Load
87+
[`accessed`]: crate::Expression::AccessIndex
88+
[`dynamically accessed`]: crate::Expression::Access
589
*/
690

791
mod block;
@@ -462,6 +546,12 @@ enum WrappedFunction {
462546
left_type_id: Word,
463547
right_type_id: Word,
464548
},
549+
ConvertFromStd140CompatType {
550+
r#type: Handle<crate::Type>,
551+
},
552+
MatCx2GetColumn {
553+
r#type: Handle<crate::Type>,
554+
},
465555
}
466556

467557
/// A map from evaluated [`Expression`](crate::Expression)s to their SPIR-V ids.
@@ -722,6 +812,20 @@ impl BlockContext<'_> {
722812
}
723813
}
724814

815+
/// Information about a type for which we have declared a std140 layout
816+
/// compatible variant, because the type is used in a uniform but does not
817+
/// adhere to std140 requirements. The uniform will be declared using the
818+
/// type `type_id`, and the result of any `Load` will be immediately converted
819+
/// to the base type. This is used for matrices with 2 rows, as well as any
820+
/// arrays or structs containing such matrices.
821+
pub struct Std140CompatTypeInfo {
822+
/// ID of the std140 compatible type declaration.
823+
type_id: Word,
824+
/// For structs, a mapping of Naga IR struct member indices to the indices
825+
/// used in the generated SPIR-V. For non-struct types this will be empty.
826+
member_indices: Vec<u32>,
827+
}
828+
725829
pub struct Writer {
726830
physical_layout: PhysicalLayout,
727831
logical_layout: LogicalLayout,
@@ -761,6 +865,7 @@ pub struct Writer {
761865
constant_ids: HandleVec<crate::Expression, Word>,
762866
cached_constants: crate::FastHashMap<CachedConstant, Word>,
763867
global_variables: HandleVec<crate::GlobalVariable, GlobalVariable>,
868+
std140_compat_uniform_types: crate::FastHashMap<Handle<crate::Type>, Std140CompatTypeInfo>,
764869
fake_missing_bindings: bool,
765870
binding_map: BindingMap,
766871

0 commit comments

Comments
 (0)