Skip to content

Instantly share code, notes, and snippets.

@alichraghi
Last active May 14, 2025 15:38
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.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 @builtins were considered for some types but flexible user-space solutions like std.gpu are usually prefered.

How to build?

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

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()
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()

How does inline assembly look like?

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.

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. 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.

@VictorSohier
Copy link

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.

@alichraghi
Copy link
Author

alichraghi commented Apr 24, 2025

I will try to get a working vulkan triangle example soon. perhaps just contribute it as an option to vulkan-zig.

something about an unimplemented instruction

Can you send the full validator message?

EDIT: See Snektron/vulkan-zig#181

@VictorSohier
Copy link

VictorSohier commented Apr 24, 2025

Sure, here's the full error:

ACO ERROR:
    In file ../src/amd/compiler/aco_instruction_selection.cpp:9022
    Unimplemented intrinsic instr: @store_deref (%1, %6) (wrmask=xyz, access=none)

This is being run through an Odin platform layer, but I can't imagine that being the problem since these are just passed into vulkan as shader code.

Either way, thank you dearly for all the resources both you and Robin have created regarding Zig on GPU. I will be very happy when this is fully fleshed out. Might try doing GCN for other things in the meantime.

EDIT: I figured out my issue, apparently, GPUs don't like concatenating arrays (++ operator)

@EtienneParmentier
Copy link

EtienneParmentier commented May 13, 2025

EDIT: I figured out my issue, apparently, GPUs don't like concatenating arrays (++ operator)

This is a compile time operation, it should be allowed ? I'd consider this a bug. There are no memory allocator on the GPU.

@EtienneParmentier
Copy link

EtienneParmentier commented May 13, 2025

How does one accesses the compiled shader code ? when using installArtifact on zig 14, it fails because object files can't be installed.

inside: lib\std\Build\Step\InstallArtifact.zig line 56:

    const dest_dir: ?InstallDir = switch (options.dest_dir) {
        .disabled => null,
        .default => switch (artifact.kind) {
            .obj => @panic("object files have no standard installation procedure"),
            .exe, .@"test" => .bin,
            .lib => if (artifact.isDll()) .bin else .lib,
        },
        .override => |o| o,
    };

@VictorSohier
Copy link

I get no such issues on Linux. I suspect it is a bug in the Windows build. I tried this kind of thing on Windows and it basically told me that it has no permission to write.

@alichraghi
Copy link
Author

@EtienneParmentier use getEmittedBin(). example

@EtienneParmentier
Copy link

EtienneParmentier commented May 14, 2025

@EtienneParmentier use getEmittedBin(). example

Cool ! could we update the example at the top ? Actually, I want to save the binary file somewhere, not import it inside another application, to allow swapping shaders after the app is compiled.
I tried using addObjCopy(), but I can't figure out how it works.

I tried using your embedfile approach, and it doesn't work for me either; here is my code (it uses your fragment shader)

Wait it's the same error as @VictorSohier ! I believe submitting an issue to zig repo is valuable at this point, using the shader here and my build.zig code as minimal reproducible example. here it is

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