Skip to content

Instantly share code, notes, and snippets.

@alichraghi
Last active January 4, 2026 15:11
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.

@EtienneParmentier
Copy link

Aaah we can't 'return' a sampler, because zig doesn't know how to represent them in the type system.

@Ben-Miller0
Copy link

how do you create a 4x4 matrix type

@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);
}

@jedugsem
Copy link

jedugsem commented Jan 4, 2026

any idear why i get:
'''
Invalid explicit layout decorations on type for operand '106[%106]'
%107 = OpVariable %_ptr_Function__arr_v4f32_u32_4 Function
The Vulkan spec states: All variables must have valid explicit layout decorations as described in Shader Interfaces
'''
when using zm.mul()

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