From f51433c77020f64879ca33429d8196460a9a7930 Mon Sep 17 00:00:00 2001 From: Nathan Date: Sun, 24 May 2026 10:47:43 -0500 Subject: [PATCH] renderer/vulkan: probe shader Vulkan-compatibility (diagnostic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/renderer/vulkan/smoke.zig | 92 +++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/src/renderer/vulkan/smoke.zig b/src/renderer/vulkan/smoke.zig index ec287770f..8f2a4f8f4 100644 --- a/src/renderer/vulkan/smoke.zig +++ b/src/renderer/vulkan/smoke.zig @@ -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,