Skip to content

Instantly share code, notes, and snippets.

@alichraghi
Last active August 7, 2026 21:21
Show Gist options
  • Select an option

  • Save alichraghi/cc4b1db0a0a556de4f85cf06f0e7a400 to your computer and use it in GitHub Desktop.

Select an option

Save alichraghi/cc4b1db0a0a556de4f85cf06f0e7a400 to your computer and use it in GitHub Desktop.
Zig Shaders

What does it look like?

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.binding(&ubo, 0, 0);
    gpu.location(&frag_color, 0);

    frag_color = ubo.object_color * ubo.light_color;
}

How to build?

In CLI:

zig build-obj shader.zig -target spirv32-vulkan -ofmt=spirv -mcpu vulkan_v1_2 -fno-llvm

In build.zig:

const vulkan12_target = b.resolveTargetQuery(.{
    .cpu_arch = .spirv32,
    .cpu_model = .{ .explicit = &std.Target.spirv.cpu.vulkan_v1_2 },
    .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()`

GLSL/HLSL -> Zig mapping

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_in/gpu.position_out
gl_VertexIndex SV_VertexID gpu.vertex_index
gl_InstanceIndex SV_InstanceID gpu.instance_index
gl_FragCoord SV_Position gpu.fragment_coord
gl_FragDepth SV_Depth gpu.fragment_depth
layout(location=N) SV_Target gpu.location()
layout(binding=N) register() gpu.binding()
gl_GlobalInvocationID SV_DispatchThreadID gpu.global_invocation_id
gl_LocalInvocationID SV_GroupThreadID gpu.local_invocation_id

How does inline assembly look like?

You can directly write SPIR-V assembly using the inline assembly feature. As you probably have noitced, it requires 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.

How can i help?

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. If you have any questions, feel free to reach me (#alichraghi) or Snektron (#snektron) in Discord or ZSF's zulip.

@korabdoroci

Copy link
Copy Markdown

I'm having trouble adding pointers to the push constants, or in this case DispatchParams. If i remove the gbuffer member it compiles,
but if its included i get:

compile exe render.spv debug spirv64-vulkan failure
error: process terminated with signal SEGV
failed command: /home/korab/.zvm/master/zig build-exe -ODebug -target spirv64-vulkan -mcpu vulkan_v1_2+int64+shader_clock_khr+SPV_KHR_shader_clock -Mroot=shaders/render.zig --cache-dir .zig-cache --global-cache-dir /home/korab/.cache/zig --name render.spv --zig-lib-dir /home/korab/.zvm/master/lib/ --listen=-
const DispatchParams = extern struct {
    gbuffer: *GBufferUniforms,
    max_bounces: u32,
    stbn_texture: TextureHandle2D(u32),
};

pub export fn ComputeMain() callconv(.{ .spirv_kernel = .{ .x = 8, .y = 8, .z = 1 } }) void {
    const pc = @extern(*addrspace(.push_constant) const DispatchParams, .{ .name = "pc" });
    _ = pc;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment