Skip to content

Instantly share code, notes, and snippets.

@alichraghi
Last active September 2, 2025 11:43
Show Gist options
  • Save alichraghi/cc4b1db0a0a556de4f85cf06f0e7a400 to your computer and use it in GitHub Desktop.
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.

@Kbz-8
Copy link

Kbz-8 commented Sep 2, 2025

how do you create a 4x4 matrix type

I use zmath to do so, it works perfectly in shaders as it uses @Vector to implement basically anything. I even generated and uploaded documentation of zmath here.

Here's a dumb simple 2D vertex shader that uses zmath:

const std = @import("std");
const gpu = std.gpu;
const zm = @import("zmath");

const Vec2f = @Vector(2, f32);
const Vec4f = @Vector(4, f32);

extern var in_position: Vec4f addrspace(.input);
extern var in_uv: Vec2f addrspace(.input);

extern var out_color: Vec4f addrspace(.output);
extern var out_uv: Vec2f addrspace(.output);

const UniformBuffer = extern struct {
    mat: zm.Mat,
    color: Vec4f,
};

extern const ubo: UniformBuffer addrspace(.uniform);

export fn main() callconv(.spirv_vertex) void {
    gpu.location(&in_position, 0);
    gpu.location(&in_uv, 2);

    gpu.location(&out_color, 0);
    gpu.location(&out_uv, 1);

    gpu.binding(&ubo, 1, 0);

    out_color = ubo.color;
    out_uv = Vec2f{ -in_uv[0], in_uv[1] };

    const position = Vec4f{ in_position[0], in_position[1], 0.0, 1.0 };
    gpu.position_out.* = zm.mul(ubo.mat, position);
}

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