blob: c9d693202d2409cdae7752ed81342c52153219c7 [file] [log] [blame] [view] [edit]
# SPIR-V translation of shader input and output variables
WGSL [MR 1315](https://github.com/gpuweb/gpuweb/issues/1315) changed WGSL so
that pipeline inputs and outputs are handled similar to HLSL:
- Shader pipeline inputs are the WGSL entry point function arguments.
- Shader pipeline outputs are the WGSL entry point return value.
Note: In both cases, a struct may be used to pack multiple values together.
In that case, I/O specific attributes appear on struct members at the struct declaration.
Resource variables, e.g. buffers, samplers, and textures, are still declared
as variables at module scope.
## Vulkan SPIR-V today
SPIR-V for Vulkan models inputs and outputs as module-scope variables in
the Input and Output address spaces, respectively.
The `OpEntryPoint` instruction has a list of module-scope variables that must
be a superset of all the input and output variables that are statically
accessed in the shader call tree.
From SPIR-V 1.4 onward, all interface variables that might be statically accessed
must appear on that list.
So that includes all resource variables that might be statically accessed
by the shader call tree.
## Translation scheme for SPIR-V to WGSL
A translation scheme from SPIR-V to WGSL is as follows:
Each SPIR-V entry point maps to a set of Private variables proxying the
inputs and outputs, and two functions:
- An inner function with no arguments or return values, and whose body
is the same as the original SPIR-V entry point.
- Original input variables are mapped to pseudo-in Private variables
with the same store types, but no other attributes or properties copied.
In Vulkan, Input variables don't have initalizers.
- Original output variables are mapped to pseudo-out Private variables
with the same store types and optional initializer, but no other attributes
or properties are copied.
- A wrapper entry point function whose arguments correspond in type, location
and builtin attributes the original input variables, and whose return type is
a structure containing members correspond in type, location, and builtin
attributes to the original output variables.
The body of the wrapper function the following phases:
- Copy formal parameter values into pseudo-in variables.
- Insert a bitcast if the WGSL builtin variable has different signedness
from the SPIR-V declared type.
- Execute the inner function.
- Copy pseudo-out variables into the return structure.
- Insert a bitcast if the WGSL builtin variable has different signedness
from the SPIR-V declared type.
- Return the return structure.
- Replace uses of the the original input/output variables to the pseudo-in and
pseudo-out variables, respectively.
- Remap pointer-to-Input with pointer-to-Private
- Remap pointer-to-Output with pointer-to-Private
We are not concerned with the cost of extra copying input/output values.
First, the pipeline inputs/outputs tend to be small.
Second, we expect the backend compiler in the driver will be able to see
through the copying and optimize the result.
### Example
```glsl
#version 450
layout(location = 0) out vec4 frag_colour;
layout(location = 0) in vec4 the_colour;
void bar() {
frag_colour = the_colour;
}
void main() {
bar();
}
```
Current translation, through SPIR-V, SPIR-V reader, WGSL writer:
```groovy
@location(0) var<out> frag_colour : vec4<f32>;
@location(0) var<in> the_colour : vec4<f32>;
fn bar_() -> void {
const x_14 : vec4<f32> = the_colour;
frag_colour = x_14;
return;
}
@fragment
fn main() -> void {
bar_();
return;
}
```
Proposed translation, through SPIR-V, SPIR-V reader, WGSL writer:
```groovy
// 'in' variables are now 'private'.
var<private> frag_colour : vec4<f32>;
var<private> the_colour : vec4<f32>;
fn bar_() -> void {
// Accesses to the module-scope variables do not change.
// This is a big simplifying advantage.
const x_14 : vec4<f32> = the_colour;
frag_colour = x_14;
return;
}
fn main_inner() -> void {
bar_();
return;
}
// Declare a structure type to collect the return values.
struct main_result_type {
@location(0) frag_color : vec4<f32>;
};
@fragment
fn main(
// 'in' variables are entry point parameters
@location(0) the_color_arg : vec4<f32>
) -> main_result_type {
// Save 'in' arguments to 'private' variables.
the_color = the_color_arg;
// Initialize 'out' variables.
// Use the zero value, since no initializer was specified.
frag_color = vec4<f32>();
// Invoke the original entry point.
main_inner();
// Collect outputs into a structure and return it.
var result : main_outer_result_type;
result.frag_color = frag_color;
return result;
}
```
Alternately, we could emit the body of the original entry point at
the point of invocation.
However that is more complex because the original entry point function
may return from multiple locations, and we would like to have only
a single exit path to construct and return the result value.
### Handling fragment discard
In SPIR-V `OpKill` causes immediate termination of the shader.
Is the shader obligated to write its outputs when `OpKill` is executed?
The Vulkan fragment operations are as follows:
(see [6. Fragment operations](https://www.khronos.org/registry/vulkan/specs/1.2/html/vkspec.html#fragops)).
* Scissor test
* Sample mask test
* Fragment shading
* Multisample coverage
* Depth bounds test
* Stencil test
* Depth test
* Sample counting
* Coverage reduction
After that, the fragment results are used to update output attachments, including
colour, depth, and stencil attachments.
Vulkan says:
> If a fragment operation results in all bits of the coverage mask being 0,
> the fragment is discarded, and no further operations are performed.
> Fragments can also be programmatically discarded in a fragment shader by executing one of
>
> OpKill.
I interpret this to mean that the outputs of a discarded fragment are ignored.
Therefore, `OpKill` does not require us to modify the basic scheme from the previous
section.
The `OpDemoteToHelperInvocationEXT`
instruction is an alternative way to throw away a fragment, but which
does not immediately terminate execution of the invocation.
It is introduced in the [`SPV_EXT_demote_to_helper_invocation](http://htmlpreview.github.io/?https://github.com/KhronosGroup/SPIRV-Registry/blob/master/extensions/EXT/SPV_EXT_demote_to_helper_invocation.html)
extension. WGSL does not have this feature, but we expect it will be introduced by a
future WGSL extension. The same analysis applies to demote-to-helper. When introduced,
it will not affect translation of pipeline outputs.
### Handling depth-replacing mode
A Vulkan fragment shader must write to the fragment depth builtin if and only if it
has a `DepthReplacing` execution mode. Otherwise behaviour is undefined.
We will ignore the case where the SPIR-V shader writes to the `FragDepth` builtin
and then discards the fragment.
This is justified because "no further operations" are performed by the pipeline
after the fragment is discarded, and that includes writing to depth output attachments.
Assuming the shader is valid, no special translation is required.
### Handling output sample mask
By the same reasoning as for depth-replacing, it is ok to incidentally not write
to the sample-mask builtin variable when the fragment is discarded.
### Handling clip distance and cull distance
Most builtin variables are scalars or vectors.
However, the `ClipDistance` and `CullDistance` builtin variables are arrays of 32-bit float values.
Each entry defines a clip half-plane (respectively cull half-plane)
A Vulkan implementation must support array sizes of up to 8 elements.
How prevalent are shaders that use these features?
These variables are supported when Vulkan features `shaderClipDistance` and `shaderCullDistance`
are supported.
According to gpuinfo.org as of this writing, those
Vulkan features appear to be nearly universally supported on Windows devices (>99%),
but by only 70% on Android.
It appears that Qualcomm devices support them, but Mali devices do not (e.g. Mali-G77).
The proposed translation scheme forces a copy of each array from private
variables into the return value of a vertex shader, or into a private
variable of a fragment shader.
In addition to the register pressure, there may be a performance degradation
due to the bulk copying of data.
We think this is an acceptable tradeoff for the gain in usability and
consistency with other pipeline inputs and outputs.
## Translation scheme for WGSL AST to SPIR-V
To translate from the WGSL AST to SPIR-V, do the following:
- Each entry point formal parameter is mapped to a SPIR-V `Input` variable.
- Struct and array inputs may have to be broken down into individual variables.
- The return of the entry point is broken down into fields, with one
`Output` variable per field.
- In the above, builtins must be separated from user attributes.
- Builtin attributes are moved to the corresponding variable.
- Location and interpolation attributes are moved to the corresponding
variables.
- This translation relies on the fact that pipeline inputs and pipeline
outputs are IO-shareable types. IO-shareable types are always storable,
and can be the store type of input/output variables.
- Input function parameters will be automatically initialized by the system
as part of setting up the pipeline inputs to the entry point.
- Replace each return statement in the entry point with a code sequence
which writes the return value components to the synthesized output variables,
and then executes an `OpReturn` (without value).
This translation is sufficient even for fragment shaders with discard.
In that case, outputs will be ignored because downstream pipeline
operations will not be performed.
This is the same rationale as for translation from SPIR-V to WGSL AST.