a73x

ee1a63bd

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

a73x   2026-04-08 08:52

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

diff --git a/build.zig b/build.zig
index 7ed1acd..06dde99 100644
--- a/build.zig
+++ b/build.zig
@@ -144,4 +144,50 @@ pub fn build(b: *std.Build) void {
        .root_module = font_test_mod,
    });
    test_step.dependOn(&b.addRunArtifact(font_tests).step);

    // vulkan-zig — generate Vulkan bindings from vk.xml
    const vulkan_headers_dep = b.dependency("vulkan_headers", .{});
    const vulkan_zig_dep = b.dependency("vulkan", .{
        .registry = vulkan_headers_dep.path("registry/vk.xml"),
    });
    const vulkan_module = vulkan_zig_dep.module("vulkan-zig");

    // Compile cell shaders to SPIR-V via glslc
    const glslc_vert = b.addSystemCommand(&.{ "glslc", "--target-env=vulkan1.2" });
    glslc_vert.addFileArg(b.path("shaders/cell.vert"));
    glslc_vert.addArg("-o");
    const cell_vert_spv = glslc_vert.addOutputFileArg("cell.vert.spv");

    const glslc_frag = b.addSystemCommand(&.{ "glslc", "--target-env=vulkan1.2" });
    glslc_frag.addFileArg(b.path("shaders/cell.frag"));
    glslc_frag.addArg("-o");
    const cell_frag_spv = glslc_frag.addOutputFileArg("cell.frag.spv");

    // Collect renderer.zig + both SPV blobs into one WriteFiles directory so
    // that @embedFile("cell.vert.spv") resolves correctly relative to renderer.zig.
    const renderer_dir = b.addWriteFiles();
    const renderer_zig_path = renderer_dir.addCopyFile(b.path("src/renderer.zig"), "renderer.zig");
    _ = renderer_dir.addCopyFile(cell_vert_spv, "cell.vert.spv");
    _ = renderer_dir.addCopyFile(cell_frag_spv, "cell.frag.spv");

    // renderer module
    const renderer_mod = b.createModule(.{
        .root_source_file = renderer_zig_path,
        .target = target,
        .optimize = optimize,
    });
    renderer_mod.addImport("vulkan", vulkan_module);
    exe_mod.addImport("renderer", renderer_mod);

    // Test renderer.zig
    const renderer_test_mod = b.createModule(.{
        .root_source_file = renderer_zig_path,
        .target = target,
        .optimize = optimize,
    });
    renderer_test_mod.addImport("vulkan", vulkan_module);
    const renderer_tests = b.addTest(.{
        .root_module = renderer_test_mod,
    });
    test_step.dependOn(&b.addRunArtifact(renderer_tests).step);
}
diff --git a/build.zig.zon b/build.zig.zon
index d8cf78e..1752ccb 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -15,8 +15,8 @@
            .hash = "wayland-0.6.0-dev-lQa1koD8AQDA1Ez_XdLOqA8QPvwKyxsAanpCIsZEb3OS",
        },
        .vulkan = .{
            .url = "git+https://github.com/Snektron/vulkan-zig#b3086a867a47bb10e66a987a38115a43056b60f7",
            .hash = "vulkan-0.0.0-r7Ytx99oAwDmJu0XtvukW_bSYnAv1PpbPO9ZFwsjcPPO",
            .url = "git+https://github.com/Snektron/vulkan-zig?ref=zig-0.15-compat#3ada9e2989bab70090a55f0f6fac19ea90d06357",
            .hash = "vulkan-0.0.0-r7Ytx6FIAwD3QyrrJhvObO0YSZ6WEkMGSCPKbhM__HFd",
        },
        .vulkan_headers = .{
            .url = "git+https://github.com/KhronosGroup/Vulkan-Headers#afe9eb980aa928a66d1c9c06f38c55dd59868720",
diff --git a/shaders/cell.frag b/shaders/cell.frag
new file mode 100644
index 0000000..7de861c
--- /dev/null
+++ b/shaders/cell.frag
@@ -0,0 +1,14 @@
#version 450

layout(binding = 0) uniform sampler2D glyph_atlas;

layout(location = 0) in vec2 in_uv;
layout(location = 1) in vec4 in_fg;
layout(location = 2) in vec4 in_bg;

layout(location = 0) out vec4 out_color;

void main() {
    float alpha = texture(glyph_atlas, in_uv).r;
    out_color = mix(in_bg, in_fg, alpha);
}
diff --git a/shaders/cell.vert b/shaders/cell.vert
new file mode 100644
index 0000000..e82987b
--- /dev/null
+++ b/shaders/cell.vert
@@ -0,0 +1,27 @@
#version 450

layout(push_constant) uniform PushConstants {
    vec2 viewport_size;
    vec2 cell_size;
} pc;

layout(location = 0) in vec2 in_unit_pos;

layout(location = 1) in vec2 in_cell_pos;
layout(location = 2) in vec4 in_uv_rect;
layout(location = 3) in vec4 in_fg_color;
layout(location = 4) in vec4 in_bg_color;

layout(location = 0) out vec2 out_uv;
layout(location = 1) out vec4 out_fg;
layout(location = 2) out vec4 out_bg;

void main() {
    vec2 pixel_pos = (in_cell_pos + in_unit_pos) * pc.cell_size;
    vec2 ndc = (pixel_pos / pc.viewport_size) * 2.0 - 1.0;
    gl_Position = vec4(ndc, 0.0, 1.0);

    out_uv = mix(in_uv_rect.xy, in_uv_rect.zw, in_unit_pos);
    out_fg = in_fg_color;
    out_bg = in_bg_color;
}
diff --git a/src/renderer.zig b/src/renderer.zig
new file mode 100644
index 0000000..5942c5c
--- /dev/null
+++ b/src/renderer.zig
@@ -0,0 +1,21 @@
const std = @import("std");
const vk = @import("vulkan");

pub const cell_vert_spv: []const u8 = @embedFile("cell.vert.spv");
pub const cell_frag_spv: []const u8 = @embedFile("cell.frag.spv");

test "vulkan module imports" {
    _ = vk;
}

test "shaders are embedded with SPIR-V magic" {
    try std.testing.expect(cell_vert_spv.len > 0);
    try std.testing.expect(cell_frag_spv.len > 0);

    // SPIR-V magic number (0x07230203), little-endian first 4 bytes
    const magic: u32 = std.mem.readInt(u32, cell_vert_spv[0..4], .little);
    try std.testing.expectEqual(@as(u32, 0x07230203), magic);

    const magic2: u32 = std.mem.readInt(u32, cell_frag_spv[0..4], .little);
    try std.testing.expectEqual(@as(u32, 0x07230203), magic2);
}