We want to improve support for authoring SPIR-V shaders in Zig. The accepted proposal #32032 suggests adding vec2, vec3, and vec4 types to the language to support this goal. This proposal is my suggestion for how exactly we do that.
Happy to work on the implementation if accepted.
Problem Statement
SPIR-V supports vectors of two or more scalars, as well as a number of instructions that operate on vectors.
Popular languages that target SPIR-V (GLSL, HLSL, and Slang) have language level support for vectors. This allows programmers to write readable code that lowers to vector instructions. Shader authors lean heavily on this functionality.
IIUC these instructions typically end up scalarized when compiled by the graphics driver from SPIR-V to the ISA for a particular GPU (e.g. RDNA4). However, playing nice with the IR keeps shaders small, likely makes the optimizer's job easier, and gives us an easy path forward on the accepted proposal to add a matrix type to the language since these types will need to interact with vectors.
(Relying on the optimizer to vectorize scalar code is probably not ideal here, as an example Khronos' SPIR-V optimizer fails to autovecotrize scalar code in seemingly easy cases. I am not criticizing their implementation, seemingly easy does not necessarily mean actually easy.)
Additionally, by providing a built-in way for users to access vector functionality:
- One of the main motivations for asking for operator overloading is resolved
- Libraries with public APIs using vectors don't have to agree on a math library
Point 2. comes up a lot in game dev libraries. I have about 20 different Zig game dev libraries, I counted and about half of them want to use vectors in their public APIs, often just for a vec2. Each of those needs to make the tough decision of whether to accept vectors as [2]f32, or bite the bullet and pull in my whole math library to get a type with field names.
Proposal
Zig's SPIR-V backend already compiles @Vector(...) types to SPIR-V vectors today. This status quo will continue to serve us well when we add a matrix type to the language, as LLVM represents matrices as vectors.
I propose that instead of adding new types for vectors, we extend the existing vector type with two goals:
- Make it easier to lower to optimal SPIR-V operations
- Make it easier to read and write the type of vectorized code often seen in shaders, whether targeting a CPU or a GPU
This means that we would reject gh/7305 rename SIMD type from @Vector to @Simd since we would be embracing SPIR-V as a use case for this type, and SPIR-V vectors are not SIMD vectors. Additionally, as a side effect of this proposal, the builtin vectors will become usable as general purpose vectors, so it is not as important that we distinguish between these use cases.
That being said, this proposal is only suggesting that we add operations actually available on targets we support. Higher level operations like quaternion multiplication are out of scope and can easily be provided by users in library code.
New Functionality
@Vector Takes A Layout Argument
Currently, @Vector is sized and aligned for aligned loads into SIMD registers. We don't recommend that users store these types long term, since this results in space wasted to padding. Instead, we suggest that they coerce vectors to arrays for this purpose.
From a processing perspective, this is the best of both worlds: tightly packed data in long term storage, aligned data in the hot loop.
As vectors gain support for field access and swizzles, there's increased temptation to store them long term to gain access to convenient syntax. Rather than shoo people away from using these convenient features, we should accept this use case as valid by adding a layout argument to the @Vector builtin:
@Vector(len:comptime_int,Element:type,layout:enum{auto,array})type
The auto layout is the current layout. It tells the compiler to pick the most efficient layout for vector operations on the target, and to pass the vector via SIMD registers to functions when possible.
The array layout tells the compiler to layout and pass the vector like an array of its components.
In GLSL (and I believe HLSL/Slang), vectors may or may not have extra padding depending on the struct layout (scalar, std140, std430, etc). This is essentially a more flexible version of that feature.
Note that while this makes declaring vector types more verbose, vector types were already very verbose if you used the builtin every time you needed to name them. We can suggest that users alias frequently used vector types under short names for themselves:
constvec2=@Vector(2,f32,.array);constvec3=@Vector(3,f32,.array);constvec4=@Vector(4,f32,.array);constsimd2=@Vector(2,f32,.auto);constsimd3=@Vector(3,f32,.auto);constsimd4=@Vector(4,f32,.auto);
Vector Length
We should accept gh/#17886 add .len to vector values.
Component Access
In GLSL, the components of a vec4 v can be accessed in the following ways:
v.x v.y, v.z, v.w
v.r v.g, v.b, v.a
v.s v.t, v.p, v.q (rarely used)
v[0] v[1], v[2], v[3]
In HLSL and Slang, the components of a floatx4 v can be accessed in the following ways:
v.x v.y, v.z, v.w
v.r v.g, v.b, v.a
v[0] v[1], v[2], v[3]
If you haven't worked with a shading language before, this might seem odd. Why not just use indices? They're necessary for vectors with more than four components either way.
However, supporting only indexing makes a lot of common vector operations hard to read/write. For example, here's a snippet of a random ShaderToy with formatting cleaned up. I did not look hard for this example, I just picked a shader at random from the home-page:
vec3 rain = pow(texture(iChannel0, uv2 + iTime * 7.25468).rgb, vec3(1.5));
color = mix(rain, color, clamp(iTime * 0.5 - 0.5, 0.0, 1.0));
color *= 1.0 - pow(length(uv2 * uv2 * uv2 * uv2) * 1.1, 6.0);
uv2.y *= iResolution.y / 360.0;
color.r *= (0.5 + abs(0.5 - mod(uv2.y, 0.021) / 0.021) * 0.5) * 1.5;
color.g *= (0.5 + abs(0.5 - mod(uv2.y + 0.007, 0.021) / 0.021) * 0.5) * 1.5;
color.b *= (0.5 + abs(0.5 - mod(uv2.y + 0.014, 0.021) / 0.021) * 0.5) * 1.5;
color *= 0.9 + rain * 0.35;
I encourage the reader to rewrite this snippet to use only indices. Whenever you see swizzle syntax (e.g. .xyz), you'll have to replace it with a shuffle, or invent a swizzle syntax that doesn't rely on named fields. When you're done, I encourage you to do a separate pass where you instead rewrite it to keep xyz, but never use rgb.
We should support indexing vectors with xyzw, and rgba. We should not support stpq.
A downside of this feature is that it encourages component access which is efficient in shaders, but is likely inefficient if done in between SIMD operations on a typical CPU. Users will need to understand the hardware they're targeting to make optimal choices.
Vector literals do not need to be changed. Initializing vectors with named fields creates visual noise without clarifying meaning, feel free to convert some of the linked shader to this form to see for yourself.
Swizzle Syntax
Languages like HLSL/GLSL/Slang support swizzling vectors. This lowers to a shuffle, but is given special syntax since it's a very common operation.
For example, in GLSL v.xxy is logically equivalent to vec3(v.x, v.x, v.y).
Here's an example of swizzles in practice to demonstrate why having a shorthand is important. This is from the same shader as earlier randomly picked from the ShaderToy home page. Rewriting this snippet to use shuffles is left as an exercise for the reader:
uv.y *= iResolution.y / iResolution.x;
vec2 mouse = (iMouse.xy / iResolution.xy - 0.5) * 3.0;
if (iMouse.z < 1.0) mouse = vec2(0.0);
mat2 rotview1, rotview2;
vec3 from = origin + move(rotview1, rotview2);
vec3 dir = normalize(vec3(uv * 0.8, 1.0));
dir.yz *= rot(mouse.y);
dir.xz *= rot(mouse.x);
dir.yz *= rotview2;
dir.xz *= rotview1;
Notice that swizzles can be both l-values and r-values.
I propose that we support the same syntax as HLSL, GLSL, and Slang for swizzles. Pointers to swizzles should result in a compiler error since it's unclear how to lower these. This syntax only works for up to 4 components, above that and the user must use @shuffle.
Vector Concatenation
I propose that, similarly to arrays, vectors can be concatenated with ++. This should also work on a mix of arrays and vectors.
constresult:@Vector(f32,4)=color.rgb*b++.{mask.a};
This allows flexible construction of vectors, without needing to add support for the slightly more complex construction syntax offered by GLSL/HLSL/Slang.
Without this feature, the example from above generates worse code (more scalar ops) and is harder to read:
constresult:@Vector(f32,4)=.{b*color.r,b*color.g,b*color.b,mask.a};
You could avoid the extra scalar ops by creating a local, but it would still easy to miss the fact that the last component is handled differently:
constrgb=b*color.rgb;constresult:@Vector(f32,4)=.{rgb.r,rgb.g,rgb.b,mask.a};
* Operator
Currently, the * operator supports scalar x scalar multiplication, and vector x vector multiplication. It does not support mixing scalars and vectors. This makes sense, since the current design is targeting SIMD and AFAIK there are no SIMD instructions that multiply scalars by vectors.
However, SPIR-V does have a OpVectorTimesScalar instruction. As such, it would make sense for Zig to support this operation. On targets where it's unsupported it would lower two a vector vector multiplication with a splat.
This also has the advantage of making these operations much easier to read and write. You may or may not have noticed that I've been assuming this feature exists in my previous snippets to keep them from becoming overly verbose.
This code:
particle.position+=particle.velocity*@as(@Vector(3,f32),@splat(dt));particle.acceleration+=particle.velocity*@as(@Vector(3,f32),@splat(dt));
Becomes this:
particle.position+=particle.velocity*dt;particle.acceleration+=particle.velocity*dt;
Many of the other examples in this proposal assume this syntax exists to avoid becoming hard to read due to the splat + cast.
Builtins
We should split @mulAdd into @mulAdd and @fma. Other than that, our existing vector builtins for the most part map well to what SPIR-V offers. There are some additional arithmetic operations we may want to consider supporting.
I'll need to go through these one by one. I suspect that many of them don't map to actual hardware instructions, if the implementations are short and straightforward it may be okay to leave them out. See also the next section on extended instruction sets.
Extended Instruction Sets
SPIR-V supports extended instruction sets via OpExtInstImport. GLSL.std.450 is an extended instruction set that provides additional, commonly used functionality.
I don't think that we want to adopt builtins for everything in GLSL.std.450. However, we also don't want to re-implement (or ask users to re-implement) all of this functionality since that would inflate the size of the generated shaders.
I think the path forward here is to call into extended instruction sets via inline assembly. Zig modules can be created for common extended instruction sets such as GLSL.std.450, and they can be provided either as part of std or as third party libraries. There may be existing work already done along these lines, I'm not sure, if not this is something I'll need to dig into further.
Thanks for reading!
@Snektron @mlugg @SpexGuy
We want to improve support for authoring SPIR-V shaders in Zig. The accepted proposal #32032 suggests adding `vec2`, `vec3`, and `vec4` types to the language to support this goal. This proposal is my suggestion for how exactly we do that.
Happy to work on the implementation if accepted.
## Problem Statement
SPIR-V supports vectors of [two or more scalars](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#_types), as well as a number of [instructions](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#_instructions) that operate on vectors.
Popular languages that target SPIR-V ([GLSL](https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.html), [HLSL](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl), and [Slang](https://shader-slang.org/)) have language level support for vectors. This allows programmers to write readable code that lowers to vector instructions. Shader authors lean heavily on this functionality.
IIUC these instructions typically end up scalarized when compiled by the graphics driver from SPIR-V to the ISA for a particular GPU (e.g. [RDNA4](https://docs.amd.com/v/u/en-US/rdna4-instruction-set-architecture)). However, playing nice with the IR keeps shaders small, likely makes the optimizer's job easier, and gives us an easy path forward on the accepted proposal to [add a matrix type to the language](https://github.com/ziglang/zig/issues/4960) since these types will need to interact with vectors.
(Relying on the optimizer to vectorize scalar code is probably not ideal here, as an example Khronos' SPIR-V optimizer fails to autovecotrize scalar code in [seemingly easy cases](https://godbolt.org/z/jz6zoY7Ye). I am not criticizing their implementation, seemingly easy does not necessarily mean actually easy.)
Additionally, by providing a built-in way for users to access vector functionality:
1. One of the main motivations for asking for operator overloading is resolved
2. Libraries with public APIs using vectors don't have to agree on a math library
*Point 2. comes up a lot in game dev libraries. I have about [20 different Zig game dev libraries](https://codeberg.org/Games-by-Mason), I counted and about half of them want to use vectors in their public APIs, often just for a vec2. Each of those needs to make the tough decision of whether to accept vectors as `[2]f32`, or bite the bullet and pull in my whole math library to get a type with field names.*
## Proposal
**Zig's SPIR-V backend already compiles `@Vector(...)` types to SPIR-V vectors today.** This status quo will continue to serve us well when we [add a matrix type to the language](https://github.com/ziglang/zig/issues/4960), as [LLVM represents matrices as vectors](https://llvm.org/docs/LangRef.html#matrix-intrinsics).
I propose that instead of adding new types for vectors, we extend the existing vector type with two goals:
1. Make it easier to lower to optimal SPIR-V operations
2. Make it easier to read and write the type of vectorized code often seen in shaders, whether targeting a CPU or a GPU
This means that we would reject [gh/7305 rename SIMD type from @Vector to @Simd](https://github.com/ziglang/zig/issues/7305) since we would be embracing SPIR-V as a use case for this type, and SPIR-V vectors are not SIMD vectors. Additionally, as a side effect of this proposal, the builtin vectors will become usable as general purpose vectors, so it is not as important that we distinguish between these use cases.
*That being said, this proposal is only suggesting that we add operations actually available on targets we support. Higher level operations like quaternion multiplication are out of scope and can easily be provided by users in library code.*
## New Functionality
### `@Vector` Takes A Layout Argument
Currently, `@Vector` is sized and aligned for aligned loads into SIMD registers. We don't recommend that users store these types long term, since this results in space wasted to padding. Instead, we suggest that they coerce vectors to arrays for this purpose.
From a processing perspective, this is the best of both worlds: tightly packed data in long term storage, aligned data in the hot loop.
As vectors gain support for field access and swizzles, there's increased temptation to store them long term to gain access to convenient syntax. Rather than shoo people away from using these convenient features, we should accept this use case as valid by adding a `layout` argument to the `@Vector` builtin:
```zig
@Vector(len: comptime_int, Element: type, layout: enum { auto, array }) type
```
The auto layout is the current layout. It tells the compiler to pick the most efficient layout for vector operations on the target, and to pass the vector via SIMD registers to functions when possible.
The array layout tells the compiler to layout and pass the vector like an array of its components.
In GLSL (and I believe HLSL/Slang), vectors may or may not have extra padding depending on the struct layout (scalar, std140, std430, etc). This is essentially a more flexible version of that feature.
Note that while this makes declaring vector types more verbose, vector types were **already** very verbose if you used the builtin every time you needed to name them. We can suggest that users alias frequently used vector types under short names for themselves:
```zig
const vec2 = @Vector(2, f32, .array);
const vec3 = @Vector(3, f32, .array);
const vec4 = @Vector(4, f32, .array);
const simd2 = @Vector(2, f32, .auto);
const simd3 = @Vector(3, f32, .auto);
const simd4 = @Vector(4, f32, .auto);
```
### Vector Length
We should accept [gh/#17886 add .len to vector values](https://github.com/ziglang/zig/issues/17886).
### Component Access
In GLSL, the components of a `vec4 v` can be accessed in the following ways:
* `v.x` `v.y`, `v.z`, `v.w`
* `v.r` `v.g`, `v.b`, `v.a`
* `v.s` `v.t`, `v.p`, `v.q` (rarely used)
* `v[0]` `v[1]`, `v[2]`, `v[3]`
In HLSL and Slang, the components of a `floatx4 v` can be accessed in the following ways:
* `v.x` `v.y`, `v.z`, `v.w`
* `v.r` `v.g`, `v.b`, `v.a`
* `v[0]` `v[1]`, `v[2]`, `v[3]`
If you haven't worked with a shading language before, this might seem odd. Why not just use indices? They're necessary for vectors with more than four components either way.
However, supporting only indexing makes a lot of common vector operations hard to read/write. For example, here's a snippet of [a random ShaderToy](https://www.shadertoy.com/view/Xtf3Rn) with formatting cleaned up. I did not look hard for this example, I just picked a shader at random from the home-page:
```glsl
vec3 rain = pow(texture(iChannel0, uv2 + iTime * 7.25468).rgb, vec3(1.5));
color = mix(rain, color, clamp(iTime * 0.5 - 0.5, 0.0, 1.0));
color *= 1.0 - pow(length(uv2 * uv2 * uv2 * uv2) * 1.1, 6.0);
uv2.y *= iResolution.y / 360.0;
color.r *= (0.5 + abs(0.5 - mod(uv2.y, 0.021) / 0.021) * 0.5) * 1.5;
color.g *= (0.5 + abs(0.5 - mod(uv2.y + 0.007, 0.021) / 0.021) * 0.5) * 1.5;
color.b *= (0.5 + abs(0.5 - mod(uv2.y + 0.014, 0.021) / 0.021) * 0.5) * 1.5;
color *= 0.9 + rain * 0.35;
```
I encourage the reader to rewrite this snippet to use only indices. Whenever you see swizzle syntax (e.g. `.xyz`), you'll have to replace it with a shuffle, or invent a swizzle syntax that doesn't rely on named fields. When you're done, I encourage you to do a separate pass where you instead rewrite it to keep `xyz`, but never use `rgb`.
**We should support indexing vectors with `xyzw`, and `rgba`.** We should *not* support `stpq`.
*A downside of this feature is that it encourages component access which is efficient in shaders, but is likely inefficient if done in between SIMD operations on a typical CPU. Users will need to understand the hardware they're targeting to make optimal choices.*
*Vector literals do not need to be changed. Initializing vectors with named fields creates visual noise without clarifying meaning, feel free to convert some of the linked shader to this form to see for yourself.*
### Swizzle Syntax
Languages like HLSL/GLSL/Slang support swizzling vectors. This lowers to a shuffle, but is given special syntax since it's a very common operation.
For example, in GLSL `v.xxy` is logically equivalent to `vec3(v.x, v.x, v.y)`.
Here's an example of swizzles in practice to demonstrate why having a shorthand is important. This is from the [same shader as earlier](https://www.shadertoy.com/view/Xtf3Rn) randomly picked from the ShaderToy home page. Rewriting this snippet to use shuffles is left as an exercise for the reader:
```glsl
uv.y *= iResolution.y / iResolution.x;
vec2 mouse = (iMouse.xy / iResolution.xy - 0.5) * 3.0;
if (iMouse.z < 1.0) mouse = vec2(0.0);
mat2 rotview1, rotview2;
vec3 from = origin + move(rotview1, rotview2);
vec3 dir = normalize(vec3(uv * 0.8, 1.0));
dir.yz *= rot(mouse.y);
dir.xz *= rot(mouse.x);
dir.yz *= rotview2;
dir.xz *= rotview1;
```
Notice that swizzles can be both l-values and r-values.
**I propose that we support the same syntax as HLSL, GLSL, and Slang for swizzles.** Pointers to swizzles should result in a compiler error since it's unclear how to lower these. This syntax only works for up to 4 components, above that and the user must use `@shuffle`.
### Vector Concatenation
I propose that, similarly to arrays, vectors can be concatenated with `++`. This should also work on a mix of arrays and vectors.
```zig
const result: @Vector(f32, 4) = color.rgb * b ++ .{ mask.a };
```
This allows flexible construction of vectors, without needing to add support for the slightly more complex construction syntax offered by GLSL/HLSL/Slang.
Without this feature, the example from above generates worse code (more scalar ops) and is harder to read:
```zig
const result: @Vector(f32, 4) = .{ b * color.r, b * color.g, b * color.b, mask.a };
```
You could avoid the extra scalar ops by creating a local, but it would still easy to miss the fact that the last component is handled differently:
```zig
const rgb = b * color.rgb;
const result: @Vector(f32, 4) = .{ rgb.r, rgb.g, rgb.b, mask.a };
```
## `*` Operator
Currently, the `*` operator supports scalar x scalar multiplication, and vector x vector multiplication. It does not support mixing scalars and vectors. This makes sense, since the current design is targeting SIMD and AFAIK there are no SIMD instructions that multiply scalars by vectors.
However, SPIR-V *does* have a [`OpVectorTimesScalar`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#Arithmetic) instruction. As such, it would make sense for Zig to support this operation. On targets where it's unsupported it would lower two a vector vector multiplication with a splat.
This also has the advantage of making these operations much easier to read and write. You may or may not have noticed that I've been assuming this feature exists in my previous snippets to keep them from becoming overly verbose.
This code:
```zig
particle.position += particle.velocity * @as(@Vector(3, f32), @splat(dt));
particle.acceleration += particle.velocity * @as(@Vector(3, f32), @splat(dt));
```
Becomes this:
```zig
particle.position += particle.velocity * dt;
particle.acceleration += particle.velocity * dt;
```
Many of the other examples in this proposal assume this syntax exists to avoid becoming hard to read due to the splat + cast.
## Builtins
We should [split `@mulAdd` into `@mulAdd` and `@fma`](https://codeberg.org/ziglang/zig/issues/35312). Other than that, our existing vector builtins for the most part map well to what SPIR-V offers. There are some additional [arithmetic operations](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#Arithmetic) we may want to consider supporting.
I'll need to go through these one by one. I suspect that many of them don't map to actual hardware instructions, if the implementations are short and straightforward it may be okay to leave them out. See also the next section on extended instruction sets.
## Extended Instruction Sets
SPIR-V supports extended instruction sets via `OpExtInstImport`. [`GLSL.std.450`](https://registry.khronos.org/SPIR-V/specs/unified1/GLSL.std.450.html) is an extended instruction set that provides additional, commonly used functionality.
I don't think that we want to adopt builtins for everything in `GLSL.std.450`. However, we also don't want to re-implement (or ask users to re-implement) all of this functionality since that would inflate the size of the generated shaders.
I think the path forward here is to call into extended instruction sets via inline assembly. Zig modules can be created for common extended instruction sets such as `GLSL.std.450`, and they can be provided either as part of `std` or as third party libraries. There may be existing work already done along these lines, I'm not sure, if not this is something I'll need to dig into further.
***
Thanks for reading!
@Snektron @mlugg @SpexGuy