Here is a simple fragment shader with uniform buffers:
const std = @import("std");
const gpu = std.gpu;
const UBO = extern struct {
object_color: @Vector(4, f32),
light_color: @Vector(4, f32),
};
extern const ubo: UBO addrspace(.uniform);
extern var frag_color: Vec4 addrspace(.output);
export fn fragmentMain() callconv(.spirv_fragment) void {
// Annotation
gpu.fragmentOrigin(fragmentMain, .upper_left);
gpu.binding(&ubo, 0, 0);
gpu.location(&frag_color, 0);
frag_color = ubo.object_color * ubo.light_color;
}
Ugly? Well, consider how much the language grammer would've to change for just moving stuff next to variable declaration or put-your-bikeshedding-here.
Also SPIR-V is a large and growing format and adding keywords for all those decorations and types is an insane idea.
At some point @builtin
s were considered for some types but flexible user-space solutions like std.gpu
are usually prefered.
In CLI:
zig build-obj shader.zig -target spirv64-vulkan -ofmt=spirv -mcpu vulkan_v1_2+int64 -fno-llvm
In build.zig
:
const vulkan12_target = b.resolveTargetQuery(.{
.cpu_arch = .spirv64,
.cpu_model = .{ .explicit = &std.Target.spirv.cpu.vulkan_v1_2 },
.cpu_features_add = std.Target.spirv.featureSet(&.{.int64}),
.os_tag = .vulkan,
.ofmt = .spirv,
});
const shader = b.addObject(.{
.name = "shader",
.root_source_file = b.path("shader.zig"),
.target = vulkan12_target,
.optimize = .ReleaseFast,
.use_llvm = false,
.use_lld = false,
});
// Use the emited SPIR-V with `shader.getEmitedBin()`
Note
int64
Feature is currently neseccary due to its use in builtin
module. See ziglang/zig#21827
This is by no means complete, but it's a good starting point when you're looking to port some shaders between GLSL/HLSL to Zig.
GLSL | HLSL | Zig |
---|---|---|
gl_Position |
SV_Position |
gpu.position() |
gl_VertexIndex |
SV_VertexID |
gpu.vertexIndex() |
gl_InstanceIndex |
SV_InstanceID |
gpu.instanceIndex() |
gl_FragCoord |
SV_Position |
gpu.fragmentCoord() |
gl_FragDepth |
SV_Depth |
gpu.fragmentDepth() |
layout(location=N) |
SV_Target |
gpu.location() |
layout(binding=N) |
register() |
gpu.binding() |
gl_GlobalInvocationID |
SV_DispatchThreadID |
gpu.globalInvocationId() |
gl_LocalInvocationID |
SV_GroupThreadID |
gpu.localInvocationId() |
You can directly write SPIR-V assembly using the inline assembly feature. As it seems you must have a basic knowledge in both Zig's inline assembly syntax and SPIR-V so make sure to read Zig's inline assembly, SPIR-V Assembly Syntax and SPIR-V Specification docs.
Here's how std.gpu.binding()
is implemented:
pub fn binding(comptime ptr: anytype, comptime set: u32, comptime bind: u32) void {
asm volatile (
\\OpDecorate %ptr DescriptorSet $set
\\OpDecorate %ptr Binding $bind
:
: [ptr] "" (ptr),
[set] "c" (set),
[bind] "c" (bind),
);
}
OpDecorate
is an instruction
with no result-id which means it has no output. normal input constraints are declared by an empty string and a %
sign in the code.
there's also constant constraints ("c"
) which takes a comptime known value and are determined with a $
sign.
for more examples checkout std.gpu
.
Write code. SPIR-V backend is in early stages so we are eager to see how it works for real-world examples so
a reproducible bug in issue-tracker is appreciated. Btw, for a contributer friendly task, consider extending std.gpu
:)
If you have any further questions feel free to reach me (#alichraghi
) or
Snektron (#snektron
) in Zigcord's #spirv-backend
channel or ZSF's zulip.
I'm just wondering if you have at least the shaders for a full "Hello, Triangle" example in zig.
Also, I'm having some issues running the SPIRV output in vulkan, something about an unimplemented instruction. This very well might just be the fact that I'm running a 5700XT, but I doubt it since vulkaninfo tells me that my system can do vulkan version 1.4 and this is vulkan version 1.2.