pkg/glslang: typed Zig wrapper for the Vulkan compile shim

Adds `pkg/glslang/vk.zig` exposing `compileToSpv(alloc, source, stage)`
with a `Stage` enum, owning the malloc/free dance for the shim's
out-pointers (separate free entry points for SPIR-V vs error string,
both optional, both have to be dropped on the right path).

Same shape step 1 used to promote the Vulkan binding: the renderer
should consume `glslang.vk.*` typed APIs, not poke `glslang.c.ghastty_*`
directly.

Removed the two near-identical 25-line blocks of raw shim plumbing
from `src/renderer/vulkan/shaders.zig` (production `Module.init` and
the test-side `compileToSpv` helper). Net: +23 / -76 in shaders.zig.
Local `Stage.glslangStage()` was dead and is dropped; new
`vkBindingStage()` maps the renderer's `Stage` to `glslang.vk.Stage`.

Verified via Docker (zig 0.15.2 linux-arm64):
  zig build -Drenderer=vulkan -Dapp-runtime=none → clean
  zig build -Drenderer=opengl -Dapp-runtime=none → clean

Step 2 of 6 in the PR-17 review refactor.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
ntomsic 2026-05-25 18:49:30 -05:00
parent 3ec5f35bd7
commit 2ee457d5ba
3 changed files with 111 additions and 76 deletions

View File

@ -4,6 +4,7 @@ const shader = @import("shader.zig");
pub const c = @import("c.zig").c;
pub const testing = @import("test.zig");
pub const vk = @import("vk.zig");
pub const init = initpkg.init;
pub const finalize = initpkg.finalize;

88
pkg/glslang/vk.zig Normal file
View File

@ -0,0 +1,88 @@
//! Typed Zig wrapper around the Ghastty Vulkan-friendly glslang
//! compile shim (`pkg/glslang/override/ghastty_vk_shim.h`). The shim
//! itself is a small C entry point that wraps glslang's C++-only
//! `setAutoMapBindings` / `setAutoMapLocations` / `setEnvInput` knobs
//! the upstream C ABI doesn't expose.
//!
//! Callers use this instead of poking `glslang.c.ghastty_*` directly:
//! the malloc/free dance for the shim's out-pointers is finicky
//! (separate free entry points for SPIR-V and error strings, both
//! optional, both have to be dropped on the right path) and was
//! previously open-coded across two near-identical 25-line blocks
//! in `src/renderer/vulkan/shaders.zig`. This module is the binding
//! layer; the renderer just calls `compileToSpv` and gets a Zig
//! `[]const u32` slice.
const std = @import("std");
const Allocator = std.mem.Allocator;
const c = @import("c.zig").c;
const log = std.log.scoped(.glslang);
pub const Stage = enum {
vertex,
fragment,
fn cValue(self: Stage) c.ghastty_glslang_stage_t {
return switch (self) {
.vertex => c.GHASTTY_GLSLANG_STAGE_VERTEX,
.fragment => c.GHASTTY_GLSLANG_STAGE_FRAGMENT,
};
}
};
pub const Error = error{
/// `glslang_shader_preprocess` / `_parse` / `_program_link` /
/// `_program_SPIRV_generate` failed. The shim's error message
/// is logged via `std.log.err` before this error is returned
/// no allocation is propagated to the caller.
GlslangFailed,
} || Allocator.Error;
/// Compile a null-terminated GLSL source string to a Vulkan-flavored
/// SPIR-V binary.
///
/// On success, returns a slice owned by `alloc`; the caller frees with
/// `alloc.free(spv)`. The shim hands back its own malloc'd buffer
/// which we copy into `alloc` so the caller's `defer alloc.free` works
/// without remembering a separate `ghastty_glslang_free_spirv` call.
///
/// On failure, the shim's error string is logged with `std.log.err`
/// and `error.GlslangFailed` is returned the C-side malloc'd error
/// buffer is freed before returning so callers don't have to.
pub fn compileToSpv(
alloc: Allocator,
source: [:0]const u8,
stage: Stage,
) Error![]const u32 {
var spv_ptr: [*c]u32 = undefined;
var spv_len: usize = 0;
var err_ptr: [*c]u8 = undefined;
const rc = c.ghastty_glslang_compile_vulkan(
source.ptr,
stage.cValue(),
&spv_ptr,
&spv_len,
&err_ptr,
);
if (rc != 0) {
if (err_ptr != null) {
log.err("ghastty_glslang_compile_vulkan: {s}", .{
std.mem.span(@as([*:0]const u8, @ptrCast(err_ptr))),
});
c.ghastty_glslang_free_error(err_ptr);
} else {
log.err("ghastty_glslang_compile_vulkan: unspecified failure", .{});
}
return error.GlslangFailed;
}
defer c.ghastty_glslang_free_spirv(spv_ptr);
// Copy out of the shim's malloc into `alloc` so the caller's
// free path is symmetric with every other allocator-owned slice.
const owned = try alloc.alloc(u32, spv_len);
@memcpy(owned, spv_ptr[0..spv_len]);
return owned;
}

View File

@ -103,10 +103,15 @@ pub const Stage = enum {
vertex,
fragment,
fn glslangStage(self: Stage) c_uint {
/// Map to the binding-layer enum that `glslang.vk.compileToSpv`
/// accepts. Same shape, different module keeping the enum at
/// this level so the renderer's `.vertex` / `.fragment` literals
/// stay backend-flavored (the `vk_*` field on the struct also
/// reads off this enum).
fn vkBindingStage(self: Stage) glslang.vk.Stage {
return switch (self) {
.vertex => glslang.c.GLSLANG_STAGE_VERTEX,
.fragment => glslang.c.GLSLANG_STAGE_FRAGMENT,
.vertex => .vertex,
.fragment => .fragment,
};
}
@ -515,12 +520,12 @@ pub const Module = struct {
/// The source is run through `vulkanizeGlsl` to swap OpenGL-only
/// builtins for their Vulkan equivalents (`gl_VertexID`
/// `gl_VertexIndex`, `gl_InstanceID` `gl_InstanceIndex`); then
/// the Ghastty Vulkan compile shim
/// (`pkg/glslang/override/ghastty_vk_shim.cpp`) finishes the job
/// with auto-map bindings / locations enabled. Same path covers
/// the renderer's built-in shaders AND user-supplied custom
/// shaders, so the OpenGL-flavored GLSL Ghostty already speaks
/// keeps working.
/// `glslang.vk.compileToSpv` (typed wrapper around the Vulkan
/// compile shim in `pkg/glslang/override/ghastty_vk_shim.cpp`)
/// finishes the job with auto-map bindings / locations enabled.
/// Same path covers the renderer's built-in shaders AND
/// user-supplied custom shaders, so the OpenGL-flavored GLSL
/// Ghostty already speaks keeps working.
pub fn init(
alloc: std.mem.Allocator,
device: *const Device,
@ -540,36 +545,8 @@ pub const Module = struct {
const translated = try vulkanizeGlsl(alloc, src);
defer alloc.free(translated);
const c = glslang.c;
const c_stage: c.ghastty_glslang_stage_t = switch (stage) {
.vertex => c.GHASTTY_GLSLANG_STAGE_VERTEX,
.fragment => c.GHASTTY_GLSLANG_STAGE_FRAGMENT,
};
var spv_ptr: [*c]u32 = undefined;
var spv_len: usize = 0;
var err_ptr: [*c]u8 = undefined;
const rc = c.ghastty_glslang_compile_vulkan(
translated.ptr,
c_stage,
&spv_ptr,
&spv_len,
&err_ptr,
);
if (rc != 0) {
if (err_ptr != null) {
log.err("ghastty_glslang_compile_vulkan: {s}", .{
std.mem.span(@as([*:0]const u8, @ptrCast(err_ptr))),
});
c.ghastty_glslang_free_error(err_ptr);
} else {
log.err("ghastty_glslang_compile_vulkan: unspecified failure", .{});
}
return error.GlslangFailed;
}
defer c.ghastty_glslang_free_spirv(spv_ptr);
const spv: []const u32 = spv_ptr[0..spv_len];
const spv = try glslang.vk.compileToSpv(alloc, translated, stage.vkBindingStage());
defer alloc.free(spv);
return try initFromSpirv(device, spv, stage);
}
@ -1587,11 +1564,11 @@ test "vulkanizeGlsl: layout with pre-existing set qualifier is unchanged" {
//
// `vulkanizeGlsl` unit tests above exercise the textual rewrite in
// isolation. The integration tests below feed the rewriter's output
// through glslang via `ghastty_glslang_compile_vulkan` and assert
// the result is a valid SPIR-V binary. That covers the seam where
// a syntactically-fine rewrite still produces something glslang
// rejects (e.g. a `set = N` on a declaration glslang's
// `--auto-map-bindings` is also trying to assign).
// through `glslang.vk.compileToSpv` and assert the result is a valid
// SPIR-V binary. That covers the seam where a syntactically-fine
// rewrite still produces something glslang rejects (e.g. a `set = N`
// on a declaration glslang's `--auto-map-bindings` is also trying
// to assign).
fn compileToSpv(
alloc: std.mem.Allocator,
@ -1599,40 +1576,9 @@ fn compileToSpv(
stage: Stage,
) ![]const u32 {
glslang.testing.ensureInit() catch return error.GlslangFailed;
const translated = try vulkanizeGlsl(alloc, src);
defer alloc.free(translated);
var spv_ptr: [*c]u32 = undefined;
var spv_len: usize = 0;
var err_ptr: [*c]u8 = undefined;
const c_stage: glslang.c.ghastty_glslang_stage_t = switch (stage) {
.vertex => glslang.c.GHASTTY_GLSLANG_STAGE_VERTEX,
.fragment => glslang.c.GHASTTY_GLSLANG_STAGE_FRAGMENT,
};
const rc = glslang.c.ghastty_glslang_compile_vulkan(
translated.ptr,
c_stage,
&spv_ptr,
&spv_len,
&err_ptr,
);
if (rc != 0) {
if (err_ptr != null) {
std.log.err("compileToSpv: {s}", .{
std.mem.span(@as([*:0]const u8, @ptrCast(err_ptr))),
});
glslang.c.ghastty_glslang_free_error(err_ptr);
}
return error.GlslangFailed;
}
// Caller owns; copy out of glslang's malloc into the test allocator
// so cleanup is symmetric (the caller `defer alloc.free(out)`s).
const spv_words = spv_ptr[0..spv_len];
const owned = try alloc.alloc(u32, spv_len);
@memcpy(owned, spv_words);
glslang.c.ghastty_glslang_free_spirv(spv_ptr);
return owned;
return try glslang.vk.compileToSpv(alloc, translated, stage.vkBindingStage());
}
test "glslang integration: built-in bg_color fragment compiles" {