blob: dc9031bf7dda952c920f9a80e3a5d890e89c0051 [file] [log] [blame] [view]
David Neto0e6d95b2022-11-01 03:54:42 +00001# Converting SPIR-V to WGSL
2
3This document describes the challenges in converting SPIR-V into WGSL.
4
5Note: Unless otherwise specified, the namespace for C++ code is
dan sinclaireb664312023-07-27 21:29:56 +00006`tint::spirv::reader::`.
David Neto0e6d95b2022-11-01 03:54:42 +00007
8## Overall flow
9
101. Validate the SPIR-V input.
11
12 The SPIR-V module (binary blob) is validated against rules for
13 Vulkan 1.1, using the SPIRV-Tools validator.
14
15 This allows the rest of the flow to ignore invalid inputs.
16 However, the SPIR-V might still be rejected in a later step because:
17
18 - it uses features unavailable in WGSL, or
19 - the SPIR-V Reader is insufficiently smart, or
20 - the translated program tries to do something rejected by WGSL's rules
21 (which are checked by Tint's Resolver).
22
232. Load the SPIR-V binary into an in-memory representation.
24
25 The SPIR-V reader uses the in-memory representation of the SPIR-V
26 module defined by the SPIRV-Tools optimizer. That provides
27 convenient representation of basic structures such as:
28
29 - instructions
30 - types
31 - constants
32 - functions
33 - basic blocks
34
35 and provides analyses for:
36
37 - relating definitions to uses (spvtools::opt::analysis::DefUseMgr)
38 - types (spvtools::opt::analysis:TypeManager)
39 - constants (spvtools::opt::analysis:ConstantManager)
40
41 Note: The SPIR-V is not modified by the SPIR-V Reader.
42
433. Translate the SPIR-V module into Tint's AST.
44
45 The AST is valid for WGSL except for some small exceptions which are
46 cleaned up by transformations.
47
484. Post-process the AST to make it valid for WGSL.
49
50 Example:
51 - Rewrite strided arrays and matrices (remove `@stride` attribute)
52 - Rewrite atomic functions
53 - Remove unreachable statements, to satisfy WGSL's behaviour analysis.
54
55
56## Overcoming mismatches between SPIR-V and WGSL
57
58### Remapping builtin inputs and outputs
59
60SPIR-V for Vulkan models builtin inputs and outputs as variables
61in Input and Output storage classes.
62
63WGSL builtin inputs are parameters to the entry point, and
64builtin outputs are result values of the entry point.
65
66See [spirv-input-output-variables.md](spirv-input-output-variables.md)
67
68### We only care about `gl_Position` from `gl_PerVertex`
69
70Glslang SPIR-V output for a vertex shader has a `gl_PerVertex`
71output variable with four members:
72
73- `gl_Position`
74- `gl_PointSize`
75- `gl_ClipDistance`
76- `gl_CullDistance`
77
78WGSL only supports the `position` builtin variable.
79
80The SPIR-V Reader has a bunch of carveouts so it only generates the
81position variable. In partcular, it tracks which expressions are actually
82accesses into the per-vertex variable, and ignores accesses to other
83parts of the structure, and remaps accesses of the position member.
84
85### `gl_PointSize` must be 1.0
86
87It's a WGSL rule. SPIR-V is more flexible, and the SPIR-V Reader
88checks that any assignment to (the equivalent of) `gl_PointSize`
89must the constant value 1.0.
90
91### Remapping sample mask inputs and outputs
92
93There's some shenanigans here I don't recall.
94See the SkipReason enum.
95
96### Integer signedness
97
98In SPIR-V, the instruction determines the signedness of an operation,
99not the types of its operands.
100
101For example:
102
103 %uint = OpTypeInt 32 0 ; u32 type
104 %int = OpTypeInt 32 1 ; i32 type
105
106 %int_1 = OpConstant %int 1 ; WGSL 1i
107 %uint_2 = OpConstant %uint 2 ; WGSL 2u
108
109 ; You can mix signs of an operand, and the instruction
110 ; tells you the result type.
111 %sum_uint = OpIAdd %uint %int %int_1 %uint_2
112 %sum_int = OpIAdd %int %int %int_1 %uint_2
113
114However, WGSL arithmetic tends to require the operands and
115result type for an operation to all have the same signedness.
116
117So the above might translate to WGSL as:
118
119 let sum_uint: u32 = bitcast<u32>(1i) + 2u;
120 let sum_int: i32 = 1i + bitcast<i32>(2u);
121
122See:
123* ParserImpl::RectifyOperandSignedness
124* ParserImpl::RectifySecondOperandSignedness
125* ParserImpl::RectifyForcedResultType
126
127### Translating textures and samplers
128
129SPIR-V textures and samplers are module-scope variables
130in UniformConstant storage class.
131These map directly to WGSL variables.
132
133For a sampled-image operation, SPIR-V will:
134- load the image value from a texture variable
135- load the sampler value from a sampler variable
136- form a "sampled image" value using `SpvOpSampledImage`
137- then use that sampled image value in a image operation
138 such as `SpvOpImageSampleImplicitLod`
139
140For an image operation that is not a sampled-image operation
141(e.g. OpImageLoad or OpImageWrite), then the steps are similar
142except without a sampler (clearly), and without invoking
143`OpSampledImage`.
144
145In contrast to the SPIR-V code pattern, the WGSL builtin requires
146the texture and sampler value to be passed in as separate parameters.
147Secondly, they are passed in by value, by naming the variables
148themselves and relying on WGSL's "Load Rule" to pass the handle
149value into the callee.
150
151When the SPIR-V Reader translates a texture builtin, it traces
152backward through the `OpSampledImage` operation (if any),
153back through the load, and all the way back to the `OpVariable`
154declaration. It does this for both the image/texture variable and
155the sampler variable (if applicable). It then uses the names
156of those variables as the corresponding arguments to the WGSL
157texture builtin.
158
159### Passing textures and samplers into helper functions
160
David Neto0e6d95b2022-11-01 03:54:42 +0000161Glslang generates SPIR-V where texture and sampler formal parameters
162are as pointer-to-UniformConstant.
163
164WGSL models them as passing texture and sampler values themselves,
165conceptually as opaque handles. This is similar to GLSL, but unlike
166SPIR-V.
167
David Neto24c84402022-12-09 19:53:27 +0000168To support textures and samplers as arguments to user-defined functions,
169we extend the tracing logic so it knows to bottom out at OpFunctionParameter.
170
171Also, code that generates function declarations now understands formal
172parameters declared as a pointer to uniform-constant as
173well as direct image and sampler values.
David Neto0e6d95b2022-11-01 03:54:42 +0000174
175Example GLSL compute shader:
176
177 #version 450
178
179 layout(set=0,binding=0) uniform texture2D im;
180 layout(set=0,binding=1) uniform sampler s;
181
182 vec4 helper(texture2D imparam, sampler sparam) {
183 return texture(sampler2D(imparam,sparam),vec2(0));
184 }
185
186 void main() {
187 vec4 v = helper(im,s);
188 }
189
190SPIR-V generated by Glslang (Shaderc's glslc):
191
192 ; SPIR-V
193 ; Version: 1.0
194 ; Generator: Google Shaderc over Glslang; 10
195 ; Bound: 32
196 ; Schema: 0
197 OpCapability Shader
198 %1 = OpExtInstImport "GLSL.std.450"
199 OpMemoryModel Logical GLSL450
200 OpEntryPoint GLCompute %main "main"
201 OpExecutionMode %main LocalSize 1 1 1
202 OpSource GLSL 450
203 OpSourceExtension "GL_GOOGLE_cpp_style_line_directive"
204 OpSourceExtension "GL_GOOGLE_include_directive"
205 OpName %main "main"
206 OpName %helper_t21_p1_ "helper(t21;p1;"
207 OpName %imparam "imparam"
208 OpName %sparam "sparam"
209 OpName %v "v"
210 OpName %im "im"
211 OpName %s "s"
212 OpDecorate %im DescriptorSet 0
213 OpDecorate %im Binding 0
214 OpDecorate %s DescriptorSet 0
215 OpDecorate %s Binding 1
216 %void = OpTypeVoid
217 %3 = OpTypeFunction %void
218 %float = OpTypeFloat 32
219 %7 = OpTypeImage %float 2D 0 0 0 1 Unknown
220 %_ptr_UniformConstant_7 = OpTypePointer UniformConstant %7
221 %9 = OpTypeSampler
222 %_ptr_UniformConstant_9 = OpTypePointer UniformConstant %9
223 %v4float = OpTypeVector %float 4
224 %12 = OpTypeFunction %v4float %_ptr_UniformConstant_7 %_ptr_UniformConstant_9
225 %19 = OpTypeSampledImage %7
226 %v2float = OpTypeVector %float 2
227 %float_0 = OpConstant %float 0
228 %23 = OpConstantComposite %v2float %float_0 %float_0
229 %_ptr_Function_v4float = OpTypePointer Function %v4float
230 %im = OpVariable %_ptr_UniformConstant_7 UniformConstant
231 %s = OpVariable %_ptr_UniformConstant_9 UniformConstant
232 %main = OpFunction %void None %3
233 %5 = OpLabel
234 %v = OpVariable %_ptr_Function_v4float Function
235 %31 = OpFunctionCall %v4float %helper_t21_p1_ %im %s
236 OpStore %v %31
237 OpReturn
238 OpFunctionEnd
239 %helper_t21_p1_ = OpFunction %v4float None %12
240 %imparam = OpFunctionParameter %_ptr_UniformConstant_7
241 %sparam = OpFunctionParameter %_ptr_UniformConstant_9
242 %16 = OpLabel
243 %17 = OpLoad %7 %imparam
244 %18 = OpLoad %9 %sparam
245 %20 = OpSampledImage %19 %17 %18
246 %24 = OpImageSampleExplicitLod %v4float %20 %23 Lod %float_0
247 OpReturnValue %24
248 OpFunctionEnd
249
250What the SPIR-V Reader currently generates:
251
252 @group(0) @binding(0) var im : texture_2d<f32>;
253
254 @group(0) @binding(1) var s : sampler;
255
David Neto0e6d95b2022-11-01 03:54:42 +0000256 fn helper_t21_p1_(imparam : texture_2d<f32>, sparam : sampler) -> vec4<f32> {
257 let x_24 : vec4<f32> = textureSampleLevel(imparam, sparam, vec2<f32>(0.0f, 0.0f), 0.0f);
258 return x_24;
259 }
260
261 fn main_1() {
262 var v : vec4<f32>;
263 let x_31 : vec4<f32> = helper_t21_p1_(im, s);
264 v = x_31;
265 return;
266 }
267
268 @compute @workgroup_size(1i, 1i, 1i)
269 fn main() {
270 main_1();
271 }
272
273### Dimensionality mismatch in texture builtins
274
275Vulkan SPIR-V is fairly forgiving in the dimensionality
276of input coordinates and result values of texturing operations.
277There is some localized rewriting of values to satisfy the overloads
278of WGSL's texture builtin functions.
279
280### Reconstructing structured control flow
281
282This is subtle.
283
284- Use structural dominance (but we didn't have the name at the time).
285 See SPIR-V 1.6 Rev 2 for updated definitions.
286- See the big comment at the start of reader/spirv/function.cc
287- See internal presentations.
288
289Basically:
290* Compute a "structured order" for structurally reachable basic blocks.
291* Traversing in structured order, use a stack-based algorithn to
292 identify intervals of blocks corresponding to structured constructs.
293 For example, loop construct, continue construct, if-selection,
294 switch-selection, and case-construct. Constructs can be nested,
295 hence the need for a stack. This is akin to "drawing braces"
296 around statements, to form block-statements that will appear in
297 the output. This step performs some validation, which may now be
298 redundant with the SPIRV-Tools validator. This is defensive
299 programming, and some tests skip use of the SPIRV-Tools validator.
300* Traversing in structured order, identify structured exits from the
301 constructs identified in the previous step. This determines what
302 control flow edges correspond to `break`, `continue`, and `return`,
303 as needed.
304* Traversing in structured order, generate statements for instructions.
305 This uses a stack corresponding to nested constructs. The kind of
306 each construct being entered or exited determines emission of control
307 flow constructs (WGSL's `if`, `loop`, `continuing`, `switch`, `case`).
308
309### Preserving execution order
310
311An instruction inside a SPIR-V instruction is one of:
312
313- control flow: see the previous section
314- combinatorial: think of this as an ALU operation, i.e. the effect
315 is purely to evaluate a result value from the values of its operands.
316 It has no side effects, and is not affected by external state such
317 as memory or the actions of other invocations in its subgroup.
318 Examples: arithmetic, OpCopyObject
319- interacts with memory or other invocations in some way.
320 Examples: load, store, atomics, barriers, (subgroup operations when we
321 get them)
322- function calls: functions are not analyzed to see if they are pure,
323 so we assume function calls are non-combinatorial.
324
325To preserve execution order, all non-combinatorial instructions must
326be translated as their own separate statement. For example, an OpStore
327maps to an assignment statement.
328
329However, combinatorial instructions can be emitted at any point
330in evaluation, provided data flow constraints are satisfied: input
331values are available, and such that the resulting value is generated
332in time for consumption by downstream uses.
333
334The SPIR-V Reader uses a heuristic to choose when to emit combinatorial
335values:
336- if a combinatorial expression only has one use, *and*
337- its use is in the same structured construct as its definition, *then*
338- emit the expression at the place where it is consumed.
339
340Otherwise, make a `let` declaration for the value.
341
342Why:
343- If a value has many uses, then computing it once can save effort.
344 Preserve that choice if it was made by an upstream optimizing compiler.
345- If a value is consumed in a different structured construct, then the
346 site of its consumption may be inside a loop, and we don't want to
347 sink the computation into the loop, thereby causing spurious extra
348 evaluation.
349
350This heuristic generates halfway-readable code, greatly reducing the
351varbosity of code in the common case.
352
353### Hoisting and phis
354
355SPIR-V uses SSA (static single assignment). The key requirement is
356that the definition of a value must dominate its uses.
357
358WGSL uses lexical scoping.
359
360It is easy enough for a human or an optimizing compiler to generate
361SSA cases which do not map cleanly to a lexically scoped value.
362
363Example pseudo-GLSL:
364
365 void main() {
366 if (cond) {
367 const uint x = 1;
368 } else {
369 return;
370 }
371 const uint y = x; // x's definition dominates this use.
372 }
373
374This isn't valid GLSL and its analog would not be a valid WGSL
375program because x is used outside the scope of its declaration.
376
377Additionally, SSA uses `phi` nodes to transmit values from predecessor
378basic blocks that would otherwise not be visible (because the
379parent does not dominate the consuming basic block). An example
380is sending the updated value of a loop induction variable back to
381the top of the loop.
382
383The SPIR-V reader handles these cases by tracking:
384- where a value definition occurs
385- the span of basic blocks, in structured order, where there
386 are uses of the value.
387
388If the uses of a value span structured contructs which are not
389contained by the construct containing the definition (or
390if the value is a `phi` node), then we "hoist" the value
391into a variable:
392
393- create a function-scope variable at the top of the structured
394 construct that spans all the uses, so that all the uses
395 are in scope of that variable declaration.
396
397- for a non-phi: generate an assignment to that variable corresponding
398 to the value definition in the original SPIR-V.
399
400- for a phi: generate an assigment to that variable at the end of
401 each predecessor block for that phi, assigning the value to be
402 transmitted from that phi.
403
404This scheme works for values which can be the stored in a variable.
405
406It does not work for pointers. However, we don't think we need
407to solve this case any time soon as it is uncommon or hard/impossible
408to generate via standard tooling.
409See https://crbug.com/tint/98 and https://crbug.com/tint/837
410
411## Mapping types
412
413SPIR-V has a recursive type system. Types are defined, given result IDs,
414before any functions are defined, and before any constant values using
415the corresponding types.
416
417WGSL also has a recursive type system. However, except for structure types,
418types are spelled inline at their uses.
419
420## Texture and sampler types
421
422SPIR-V image types map to WGSL types, but the WGSL type is determined
423more by usage (what operations are performed on it) than by declaration.
424
425For example, Vulkan ignores the "Depth" operand of the image type
426declaration (OpTypeImage).
427See [16.1 Image Operations Overview](https://registry.khronos.org/vulkan/specs/1.3/html/vkspec.html#_image_operations_overview).
428Instead, we must infer that a texture is a depth texture because
429it is used by image instructions using a depth-reference, e.g.
430OpImageSampleDrefImplicitLod vs. OpImageSampleImplicitLod.
431
432Similarly, SPIR-V only has one sampler type. The use of the
433sampler determines whether it maps to a WGSL `sampler` or
434`sampler_comparison` (for depth sampling).
435
436The SPIR-V Reader scans uses of each texture and sampler
437in the module to infer the appropriate target WGSL type.
438See ParserImpl::RegisterHandleUsage
439
440In Vulkan SPIR-V it is possible to use the same sampler for regular
441sampling and depth-reference sampling. In this case the SPIR-V Reader
442will infer a depth texture, but then the generated program will fail WGSL
443validation.
444
445For example, this GLSL fragment shader:
446
447 #version 450
448
449 layout(set=1,binding=0) uniform texture2D tInput;
450 layout(set=1,binding=1) uniform sampler s;
451
452 void main() {
453 vec4 v = texture(sampler2D(tInput,s),vec2(0));
454 float f = texture(sampler2DShadow(tInput,s),vec3(0));
455 }
456
457Converts to this WGSL shader:
458
459 @group(1) @binding(0) var tInput : texture_depth_2d;
460
461 @group(1) @binding(1) var s : sampler_comparison;
462
463 fn main_1() {
464 var v : vec4<f32>;
465 var f : f32;
466 let x_23 : vec4<f32> = vec4<f32>(textureSample(tInput, s, vec2<f32>(0.0f, 0.0f)), 0.0f, 0.0f, 0.0f);
467 v = x_23;
468 let x_34 : f32 = textureSampleCompare(tInput, s, vec3<f32>(0.0f, 0.0f, 0.0f).xy, vec3<f32>(0.0f, 0.0f, 0.0f).z);
469 f = x_34;
470 return;
471 }
472
473 @fragment
474 fn main() {
475 main_1();
476 }
477
478But then this fails validation:
479
480 error: no matching call to textureSample(texture_depth_2d, sampler_comparison, vec2<f32>)
481 15 candidate functions: ...
482
483## References and pointers
484
485SPIR-V has a pointer type.
486
487A SPIR-V pointer type corresponds to a WGSL memory view. WGSL has two
488memory view types: a reference type, and a pointer type.
489
490See [spirv-ptr-ref.md](spirv-ptr-ref.md) for details on the translation.
491
492## Mapping buffer types
493
494Vulkan SPIR-V expresses a Uniform Buffer Object (UBO), or
495a WGSL 'uniform buffer' as:
496
497- an OpVariable in Uniform storage class
498- its pointee type (store type) is a Block-decorated structure type
499
500Vulkan SPIR-V has two ways to express a Shader Storage Buffer Object (SSBO),
501or a WGSL 'storage buffer' as either deprecated-style:
502
503- an OpVariable in Uniform storage class
504- its pointee type (store type) is a BufferBlock-decorated structure type
505
506or as new-style:
507
508- an OpVariable in StorageBuffer storage class
509- its pointee type (store type) is a Block-decorated structure type
510
511Deprecated-style storage buffer was the only option in un-extended
512Vulkan 1.0. It is generated by tools that want to generate code for
513the broadest reach. This includes DXC.
514
515New-style storage buffer requires the use of the `OpExtension
516"SPV_KHR_storage_buffer_storage_class"` or SPIR-V 1.3 or later
517(Vulkan 1.1 or later).
518
519Additionally, a storage buffer in SPIR-V may be marked as NonWritable.
520Perhaps surprisingly, this is typically done by marking *all* the
521members of the top-level (Buffer)Block-decorated structure as NonWritable.
522(This is the common translation because that's what Glslang does.)
523
524Translation of uniform buffers is straightforward.
525
526However, the SPIR-V Reader must support both the deprecated and the new
527styles of storage buffers.
528
529Additionally:
530- a storage buffer with all NonWritable members is translated with `read`
531 access mode. This becomes a part of its WGSL reference type (and hence
532 corresponding pointer type).
533- a storage buffer without all NonWritable members is translated with
534 an explicit `read_write` access mode. This becomes a part of its
535 WGSL reference type (and hence corresponding pointer type).
536
537Note that read-only vs. read-write is a property of the pointee-type in SPIR-V,
538but in WGSL it's part of the reference type (not the store type).
539
540To handle this mismatch, the SPIR-V Reader has bookkeeping to map
541each pointer value (inside a function) back to through to the originating
542variable. This originating variable may be a buffer variable which then
543tells us which address space and access mode to use for a locally-defined
544pointer value.
545
546Since baseline SPIR-V does not allow passing pointers to buffers into
547user-defined helper functions, we don't need to handle this buffer type
548remapping into function formal parameters.
549
550## Mapping OpArrayLength
551
552The OpArrayLength instruction takes a pointer to the enclosing
553structure (the pointee type of the storage buffer variable).
554
555But the WGSL arrayLength builtin variable takes a pointer to the
556member inside that structure.
557
558A small local adjustment is sufficient here.