a73x

ee1a63bd

build(renderer): wire vulkan-zig + glslc + embed SPIR-V

a73x   2026-04-08 08:52

Commit message
build(renderer): wire vulkan-zig + glslc + embed SPIR-V

- Update vulkan dep to zig-0.15-compat branch (was master, which targets
  Zig nightly and uses std.process.Init / std.Io APIs absent in 0.15.2)
- Add glslc build step that compiles shaders/cell.{vert,frag} to SPIR-V
- Collect renderer.zig + both SPV blobs into a WriteFiles directory so
  that @embedFile("cell.vert.spv") resolves relative to renderer.zig
- Add renderer module with vulkan import; wire into exe and test step
- All 12 tests pass including the SPIR-V magic-number check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

build.zig
Old New
@@ -144,4 +144,50 @@ pub fn build(b: *std.Build) void {
144 .root_module = font_test_mod, 144 .root_module = font_test_mod,
145 }); 145 });
146 test_step.dependOn(&b.addRunArtifact(font_tests).step); 146 test_step.dependOn(&b.addRunArtifact(font_tests).step);
147
148 // vulkan-zig — generate Vulkan bindings from vk.xml
149 const vulkan_headers_dep = b.dependency("vulkan_headers", .{});
150 const vulkan_zig_dep = b.dependency("vulkan", .{
151 .registry = vulkan_headers_dep.path("registry/vk.xml"),
152 });
153 const vulkan_module = vulkan_zig_dep.module("vulkan-zig");
154
155 // Compile cell shaders to SPIR-V via glslc
156 const glslc_vert = b.addSystemCommand(&.{ "glslc", "--target-env=vulkan1.2" });
157 glslc_vert.addFileArg(b.path("shaders/cell.vert"));
158 glslc_vert.addArg("-o");
159 const cell_vert_spv = glslc_vert.addOutputFileArg("cell.vert.spv");
160
161 const glslc_frag = b.addSystemCommand(&.{ "glslc", "--target-env=vulkan1.2" });
162 glslc_frag.addFileArg(b.path("shaders/cell.frag"));
163 glslc_frag.addArg("-o");
164 const cell_frag_spv = glslc_frag.addOutputFileArg("cell.frag.spv");
165
166 // Collect renderer.zig + both SPV blobs into one WriteFiles directory so
167 // that @embedFile("cell.vert.spv") resolves correctly relative to renderer.zig.
168 const renderer_dir = b.addWriteFiles();
169 const renderer_zig_path = renderer_dir.addCopyFile(b.path("src/renderer.zig"), "renderer.zig");
170 _ = renderer_dir.addCopyFile(cell_vert_spv, "cell.vert.spv");
171 _ = renderer_dir.addCopyFile(cell_frag_spv, "cell.frag.spv");
172
173 // renderer module
174 const renderer_mod = b.createModule(.{
175 .root_source_file = renderer_zig_path,
176 .target = target,
177 .optimize = optimize,
178 });
179 renderer_mod.addImport("vulkan", vulkan_module);
180 exe_mod.addImport("renderer", renderer_mod);
181
182 // Test renderer.zig
183 const renderer_test_mod = b.createModule(.{
184 .root_source_file = renderer_zig_path,
185 .target = target,
186 .optimize = optimize,
187 });
188 renderer_test_mod.addImport("vulkan", vulkan_module);
189 const renderer_tests = b.addTest(.{
190 .root_module = renderer_test_mod,
191 });
192 test_step.dependOn(&b.addRunArtifact(renderer_tests).step);
147 } 193 }
build.zig.zon
Old New
@@ -15,8 +15,8 @@
15 .hash = "wayland-0.6.0-dev-lQa1koD8AQDA1Ez_XdLOqA8QPvwKyxsAanpCIsZEb3OS", 15 .hash = "wayland-0.6.0-dev-lQa1koD8AQDA1Ez_XdLOqA8QPvwKyxsAanpCIsZEb3OS",
16 }, 16 },
17 .vulkan = .{ 17 .vulkan = .{
18 .url = "git+https://github.com/Snektron/vulkan-zig#b3086a867a47bb10e66a987a38115a43056b60f7", 18 .url = "git+https://github.com/Snektron/vulkan-zig?ref=zig-0.15-compat#3ada9e2989bab70090a55f0f6fac19ea90d06357",
19 .hash = "vulkan-0.0.0-r7Ytx99oAwDmJu0XtvukW_bSYnAv1PpbPO9ZFwsjcPPO", 19 .hash = "vulkan-0.0.0-r7Ytx6FIAwD3QyrrJhvObO0YSZ6WEkMGSCPKbhM__HFd",
20 }, 20 },
21 .vulkan_headers = .{ 21 .vulkan_headers = .{
22 .url = "git+https://github.com/KhronosGroup/Vulkan-Headers#afe9eb980aa928a66d1c9c06f38c55dd59868720", 22 .url = "git+https://github.com/KhronosGroup/Vulkan-Headers#afe9eb980aa928a66d1c9c06f38c55dd59868720",
shaders/cell.frag
Old New
@@ -0,0 +1,14 @@
1 #version 450
2
3 layout(binding = 0) uniform sampler2D glyph_atlas;
4
5 layout(location = 0) in vec2 in_uv;
6 layout(location = 1) in vec4 in_fg;
7 layout(location = 2) in vec4 in_bg;
8
9 layout(location = 0) out vec4 out_color;
10
11 void main() {
12 float alpha = texture(glyph_atlas, in_uv).r;
13 out_color = mix(in_bg, in_fg, alpha);
14 }
shaders/cell.vert
Old New
@@ -0,0 +1,27 @@
1 #version 450
2
3 layout(push_constant) uniform PushConstants {
4 vec2 viewport_size;
5 vec2 cell_size;
6 } pc;
7
8 layout(location = 0) in vec2 in_unit_pos;
9
10 layout(location = 1) in vec2 in_cell_pos;
11 layout(location = 2) in vec4 in_uv_rect;
12 layout(location = 3) in vec4 in_fg_color;
13 layout(location = 4) in vec4 in_bg_color;
14
15 layout(location = 0) out vec2 out_uv;
16 layout(location = 1) out vec4 out_fg;
17 layout(location = 2) out vec4 out_bg;
18
19 void main() {
20 vec2 pixel_pos = (in_cell_pos + in_unit_pos) * pc.cell_size;
21 vec2 ndc = (pixel_pos / pc.viewport_size) * 2.0 - 1.0;
22 gl_Position = vec4(ndc, 0.0, 1.0);
23
24 out_uv = mix(in_uv_rect.xy, in_uv_rect.zw, in_unit_pos);
25 out_fg = in_fg_color;
26 out_bg = in_bg_color;
27 }
src/renderer.zig
Old New
@@ -0,0 +1,21 @@
1 const std = @import("std");
2 const vk = @import("vulkan");
3
4 pub const cell_vert_spv: []const u8 = @embedFile("cell.vert.spv");
5 pub const cell_frag_spv: []const u8 = @embedFile("cell.frag.spv");
6
7 test "vulkan module imports" {
8 _ = vk;
9 }
10
11 test "shaders are embedded with SPIR-V magic" {
12 try std.testing.expect(cell_vert_spv.len > 0);
13 try std.testing.expect(cell_frag_spv.len > 0);
14
15 // SPIR-V magic number (0x07230203), little-endian first 4 bytes
16 const magic: u32 = std.mem.readInt(u32, cell_vert_spv[0..4], .little);
17 try std.testing.expectEqual(@as(u32, 0x07230203), magic);
18
19 const magic2: u32 = std.mem.readInt(u32, cell_frag_spv[0..4], .little);
20 try std.testing.expectEqual(@as(u32, 0x07230203), magic2);
21 }