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.
Is this available on the master branch? Currently
std.Target.spirv.cpu
doesn't seem to have avulkan_v1_2
field.