renderer/vulkan: probe shader Vulkan-compatibility (diagnostic)

Adds `probeGhosttyShaders` to the smoke test: tries to compile each
of the renderer's nine real GLSL shaders (with the same #include
preprocessing the OpenGL backend uses) through `shaders.Module.init`
as Vulkan-targeted SPIR-V. Reports per-shader success or the first
glslang error message.

Results on the current shader source:

  ✓ bg_color.f.glsl       (UBO-only, no textures)
  ✓ cell_bg.f.glsl        (UBO-only, no textures)
  ✗ full_screen.v.glsl    → gl_VertexID undeclared
  ✗ cell_text.v.glsl      → gl_VertexID undeclared
  ✗ cell_text.f.glsl      → location missing on in/out
  ✗ image.v.glsl          → gl_VertexID undeclared
  ✗ image.f.glsl          → location missing on in/out
  ✗ bg_image.v.glsl       → location missing on in/out
  ✗ bg_image.f.glsl       → location missing on in/out

Two distinct incompatibilities surfaced:

  1. `gl_VertexID` (OpenGL) vs `gl_VertexIndex` (Vulkan SPIR-V):
     every vertex shader uses the OpenGL name.

  2. SPIR-V requires explicit `layout(location = N)` on every
     shader-stage in/out variable. OpenGL GLSL auto-assigns
     locations; the renderer's shaders rely on that.

The binding namespace conflict (UBO at binding=1 colliding with
`atlas_color` at binding=1 in `cell_text.f.glsl`) is the next
hurdle behind these two, but it's hidden by them today.

Implications for the "use glslang auto-bind/auto-map" path:
glslang's auto-bind features are C++-only (not exposed through the
C `glslang_input_t` struct we use), AND they only help with
bindings — they don't synthesize `gl_VertexIndex` or auto-place
`layout(location = N)` qualifiers. Making the existing GLSL
Vulkan-compatible requires source modification spanning all 9
shaders.

Co-Authored-By: claude-flow <ruv@ruv.net>
pull/12846/head
Nathan 2026-05-24 10:47:43 -05:00
parent 0070b90370
commit f51433c770
1 changed files with 92 additions and 0 deletions

View File

@ -383,6 +383,13 @@ test "smoke" {
// it + a sampler, render a quad sampling from it, save as PPM.
try renderTexturedToFile(&device, "/tmp/ghastty-vulkan-smoke-textured.ppm");
// ---- 7. Try compiling the real Ghostty shaders ---------------
// Tests whether the existing OpenGL GLSL sources compile cleanly
// through glslang to Vulkan SPIR-V, or whether they hit binding
// namespace conflicts (Vulkan shares one namespace per descriptor
// set; OpenGL has separate ones per resource type).
try probeGhosttyShaders(&device);
std.debug.print("\n All Vulkan smoke checks passed.\n", .{});
std.debug.print(
" Visual (gradient): /tmp/ghastty-vulkan-smoke.ppm\n",
@ -1156,6 +1163,91 @@ fn renderTexturedToFile(device: *const Device, path: []const u8) !void {
std.debug.print(" Textured: wrote {}x{} PPM to {s}\n", .{ out_w, out_h, path });
}
/// Compile each of the renderer's actual GLSL shaders (with the
/// existing `#include` preprocessor splicing in `common.glsl`) and
/// report which ones glslang accepts as Vulkan-targeted SPIR-V. The
/// expected failure mode is a binding namespace collision on the
/// shaders that combine the Globals UBO with texture samplers.
fn probeGhosttyShaders(device: *const Device) !void {
// The full source files post-include-preprocessing. Computed at
// comptime via the same `processIncludes` trick as
// `opengl/shaders.zig`'s `loadShaderCode`.
const common = @embedFile("../shaders/glsl/common.glsl");
inline for (&[_]struct { name: []const u8, src: [:0]const u8, stage: shaders.Stage }{
.{
.name = "bg_color.f.glsl",
.src = comptime spliceCommon(@embedFile("../shaders/glsl/bg_color.f.glsl")),
.stage = .fragment,
},
.{
.name = "cell_bg.f.glsl",
.src = comptime spliceCommon(@embedFile("../shaders/glsl/cell_bg.f.glsl")),
.stage = .fragment,
},
.{
.name = "full_screen.v.glsl",
.src = comptime spliceCommon(@embedFile("../shaders/glsl/full_screen.v.glsl")),
.stage = .vertex,
},
.{
.name = "cell_text.v.glsl",
.src = comptime spliceCommon(@embedFile("../shaders/glsl/cell_text.v.glsl")),
.stage = .vertex,
},
.{
.name = "cell_text.f.glsl",
.src = comptime spliceCommon(@embedFile("../shaders/glsl/cell_text.f.glsl")),
.stage = .fragment,
},
.{
.name = "image.v.glsl",
.src = comptime spliceCommon(@embedFile("../shaders/glsl/image.v.glsl")),
.stage = .vertex,
},
.{
.name = "image.f.glsl",
.src = comptime spliceCommon(@embedFile("../shaders/glsl/image.f.glsl")),
.stage = .fragment,
},
.{
.name = "bg_image.v.glsl",
.src = comptime spliceCommon(@embedFile("../shaders/glsl/bg_image.v.glsl")),
.stage = .vertex,
},
.{
.name = "bg_image.f.glsl",
.src = comptime spliceCommon(@embedFile("../shaders/glsl/bg_image.f.glsl")),
.stage = .fragment,
},
}) |entry| {
if (shaders.Module.init(device, entry.src, entry.stage)) |mod| {
defer mod.deinit();
std.debug.print(" Shader compile ✓ {s}\n", .{entry.name});
} else |err| {
std.debug.print(" Shader compile ✗ {s} → {}\n", .{ entry.name, err });
}
}
_ = common;
}
/// Tiny comptime preprocessor: replace `#include "common.glsl"` with
/// the contents of `common.glsl`. The real Ghostty shaders all use
/// exactly that one include, so this is a sufficient stub.
fn spliceCommon(comptime contents: [:0]const u8) [:0]const u8 {
const needle = "#include \"common.glsl\"";
if (std.mem.indexOf(u8, contents, needle)) |idx| {
const common = @embedFile("../shaders/glsl/common.glsl");
return std.fmt.comptimePrint("{s}{s}{s}", .{
contents[0..idx],
common,
contents[idx + needle.len ..],
});
} else {
return contents;
}
}
fn imageBarrier(
device: *const Device,
cb: vk.VkCommandBuffer,