10 – Shaders Compilation
This step is a small detour I added to make my life easier with shader compilation. A shader has to be compiled to SPIR-V before it can be sent to the GPU. Several shader languages exist: GLSL, HLSL or Slang, among others. GLSL is the language OpenGL originally shipped with. Khronos created the newer Slang language to take the pain out of writing shaders: instead of one GLSL file per shader stage, Slang lets you keep everything in a single file and even import shared files, which GLSL is badly missing. The slangc compiler turns a .slang source file into a SPIR-V .spv output file.
So I wrote a small shader_compiler.odin that wraps the slangc executable bundled with the Vulkan SDK, letting us kick off compilation at runtime through a single os.process_exec call. The official Vulkan Tutorial wires the compiler call into the cmake build steps, but for a simpler setup, and to eventually allow hot-reloading shaders later on, compiling at runtime felt like the better choice here.
Compilation is fast for the shader sizes this tutorial deals with, and the error messages slangc prints are very readable. I could have linked against the slangc library instead of using the executable, but that would have meant wiring up a fair amount of C API for very little gain in this project.
The full source for this step lives in src/10_shaders_compilation/main.odin and src/10_shaders_compilation/shader_compiler.odin. We’ll reuse shader_compiler.odin unchanged from the next step onward.
Useful references:
- Slang language: https://shader-slang.org
- Slang compilation: https://docs.shader-slang.org/en/latest/external/slang/docs/user-guide/08-compiling.html
- Khronos Tutorial Shader modules: https://docs.vulkan.org/tutorial/latest/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.html
- vulkan-tutorial.com Shader Modules chapter, for context on what the SPIR-V bytes are for: https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipeline_basics/Shader_modules
What’s new, in one glance
Unlike the other steps, this one is stand-alone. Its only purpose is to write, test and demonstrate the slang compiler wrapper.
shader.slang- a single Slang source file that holds both the vertex and the fragment entry points. No separate.vert/.fragfiles.shader_compiler.odin- acompile_slang_shaderproc that findsslangc, runs it as a subprocess, and returns the compiled SPIR-V bytes.main.odin- just calls the wrapper and prints the SPIR-V size. No Vulkan calls at all in this step.
Nothing here touches the device, the swapchain or any handle from the previous steps. The file spits out a []u8 of SPIR-V; the next step will feed those bytes to vkCreateShaderModule.
A word about the shader itself
shader.slang is the same Vulkan Tutorial “vertex index → hard-coded position + hard-coded color” triangle, just written in Slang syntax:
[shader("vertex")]
VertexOutput vertMain(uint vid: SV_VertexID) { ... }
[shader("fragment")]
float4 fragMain(VertexOutput inVert) : SV_Target { ... }
The SV_VertexID semantic is what lets us avoid a vertex buffer for now - the vertex index is generated by the GPU, and we index into two static arrays for positions and colors. This matches the tutorial’s “vertex shader without vertex input” approach, so we’ll reuse this exact shader in the next steps until we add real vertex buffers.
Slang arguments
Nothing Vulkan-specific here, it’s just the slangc CLI. Three flags worth a sentence each:
-profile spirv_1_4- we explicitly request SPIR-V 1.4. That’s the versionvkGetPhysicalDeviceProperties2told us the device supports (step 04 required Vulkan 1.4 minimum). Asking slangc to target a specific profile avoids surprises where slangc hands us SPIR-V 1.6 the driver refuses.-emit-spirv-directly- skip the GLSL/HLSL intermediate. Faster, and the only way to get some of Slang’s newer features.-fvk-use-entrypoint-name- keep our function names (vertMain,fragMain) in the SPIR-V instead of renaming them tomain. The Vulkan pipeline cares about the entry point name; we’d rather it match what’s in the source.
The -entry flag is repeated once per entry point - Slang knows to pair them up with their [shader(...)] attribute in the source. That’s how one .slang file can produce a vert+frag pair in a single SPIR-V blob. We’ll see in the next step how to slice that single blob into two vk.ShaderModule handles.
Test it
After building the project, from src/10_shaders_compilation:
bin/debug/10_shaders_compilation
Expected output:
Shaders compilation
-------------------------------------------
Compiling...
Shaders compiled with success! SPIR-V size: 1476 bytes
If you see VULKAN_SDK environment variable is not set, your shell didn’t export the SDK path - source the SDK’s setup-env.sh (Linux/macOS) or set the variable manually (Windows).
To see what a failure looks like, put a typo in shader.slang (say, change vertMain to vrtMain). The proc prints slangc’s stderr to your terminal, with the file/line where Slang choked, and os.exit(1) shuts the program down.
What’s next
With a working compile_slang_shader and a SPIR-V blob in hand, the next step is to actually feed those bytes to Vulkan with vkCreateShaderModule and wire the modules into the graphics pipeline. That’s 11 - Shader Module.