aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/shadertoy.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/shadertoy.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/shadertoy.zig')
-rw-r--r--vendor/libghostty-vt/src/renderer/shadertoy.zig427
1 files changed, 427 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/shadertoy.zig b/vendor/libghostty-vt/src/renderer/shadertoy.zig
new file mode 100644
index 00000000..556c2829
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/shadertoy.zig
@@ -0,0 +1,427 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const Allocator = std.mem.Allocator;
+const ArenaAllocator = std.heap.ArenaAllocator;
+const glslang = @import("glslang");
+const spvcross = @import("spirv_cross");
+const configpkg = @import("../config.zig");
+
+const log = std.log.scoped(.shadertoy);
+
+/// The uniform struct used for shadertoy shaders.
+pub const Uniforms = extern struct {
+ resolution: [3]f32 align(16),
+ time: f32 align(4),
+ time_delta: f32 align(4),
+ frame_rate: f32 align(4),
+ frame: i32 align(4),
+ channel_time: [4][4]f32 align(16),
+ channel_resolution: [4][4]f32 align(16),
+ mouse: [4]f32 align(16),
+ date: [4]f32 align(16),
+ sample_rate: f32 align(4),
+ current_cursor: [4]f32 align(16),
+ previous_cursor: [4]f32 align(16),
+ current_cursor_color: [4]f32 align(16),
+ previous_cursor_color: [4]f32 align(16),
+ current_cursor_style: i32 align(4),
+ previous_cursor_style: i32 align(4),
+ cursor_visible: i32 align(4),
+ cursor_change_time: f32 align(4),
+ time_focus: f32 align(4),
+ focus: i32 align(4),
+ palette: [256][4]f32 align(16),
+ background_color: [4]f32 align(16),
+ foreground_color: [4]f32 align(16),
+ cursor_color: [4]f32 align(16),
+ cursor_text: [4]f32 align(16),
+ selection_background_color: [4]f32 align(16),
+ selection_foreground_color: [4]f32 align(16),
+};
+
+/// The target to load shaders for.
+pub const Target = enum { glsl, msl };
+
+/// Load a set of shaders from files and convert them to the target
+/// format. The shader order is preserved.
+pub fn loadFromFiles(
+ alloc_gpa: Allocator,
+ paths: configpkg.RepeatablePath,
+ target: Target,
+) ![]const [:0]const u8 {
+ var list: std.ArrayList([:0]const u8) = .empty;
+ defer list.deinit(alloc_gpa);
+ errdefer for (list.items) |shader| alloc_gpa.free(shader);
+
+ for (paths.value.items) |item| {
+ const path, const optional = switch (item) {
+ .optional => |path| .{ path, true },
+ .required => |path| .{ path, false },
+ };
+
+ const shader = loadFromFile(alloc_gpa, path, target) catch |err| {
+ if (err == error.FileNotFound and optional) {
+ continue;
+ }
+
+ return err;
+ };
+ log.info("loaded custom shader path={s}", .{path});
+ try list.append(alloc_gpa, shader);
+ }
+
+ return try list.toOwnedSlice(alloc_gpa);
+}
+
+/// Load a single shader from a file and convert it to the target language
+/// ready to be used with renderers.
+pub fn loadFromFile(
+ alloc_gpa: Allocator,
+ path: []const u8,
+ target: Target,
+) ![:0]const u8 {
+ var arena = ArenaAllocator.init(alloc_gpa);
+ defer arena.deinit();
+ const alloc = arena.allocator();
+
+ // Read it all into memory -- we don't expect shaders to be large.
+ const src = src: {
+ // Load the shader file
+ const cwd = std.fs.cwd();
+ const file = try cwd.openFile(path, .{});
+ defer file.close();
+
+ break :src try file.readToEndAlloc(
+ alloc,
+ 4 * 1024 * 1024, // 4MB
+ );
+ };
+
+ // Convert to full GLSL
+ const glsl: [:0]const u8 = glsl: {
+ var stream: std.Io.Writer.Allocating = .init(alloc);
+ try glslFromShader(&stream.writer, src);
+ try stream.writer.writeByte(0);
+ break :glsl stream.written()[0 .. stream.written().len - 1 :0];
+ };
+
+ // Convert to SPIR-V
+ const spirv: []const u8 = spirv: {
+ var stream: std.Io.Writer.Allocating = .init(alloc);
+ var errlog: SpirvLog = .{ .alloc = alloc };
+ defer errlog.deinit();
+ spirvFromGlsl(&stream.writer, &errlog, glsl) catch |err| {
+ if (errlog.info.len > 0 or errlog.debug.len > 0) {
+ log.warn("spirv error path={s} info={s} debug={s}", .{
+ path,
+ errlog.info,
+ errlog.debug,
+ });
+ }
+
+ return err;
+ };
+
+ // SpirV pointer must be aligned to 4 bytes since we expect
+ // a slice of words.
+ var list: std.ArrayListAligned(u8, .of(u32)) = .empty;
+ try list.appendSlice(alloc, stream.written());
+ break :spirv list.items;
+ };
+
+ // Convert to MSL
+ return switch (target) {
+ // Important: using the alloc_gpa here on purpose because this
+ // is the final result that will be returned to the caller.
+ .glsl => try glslFromSpv(alloc_gpa, spirv),
+ .msl => try mslFromSpv(alloc_gpa, spirv),
+ };
+}
+
+/// Convert a ShaderToy shader into valid GLSL.
+///
+/// ShaderToy shaders aren't full shaders, they're just implementing a
+/// mainImage function and don't define any of the uniforms. This function
+/// will convert the ShaderToy shader into a valid GLSL shader that can be
+/// compiled and linked.
+pub fn glslFromShader(writer: *std.Io.Writer, src: []const u8) !void {
+ const prefix = @embedFile("shaders/shadertoy_prefix.glsl");
+ try writer.writeAll(prefix);
+ try writer.writeAll("\n\n");
+ try writer.writeAll(src);
+}
+
+/// Convert a GLSL shader into SPIR-V assembly.
+pub fn spirvFromGlsl(
+ writer: *std.Io.Writer,
+ errlog: ?*SpirvLog,
+ src: [:0]const u8,
+) !void {
+ // So we can run unit tests without fear.
+ if (builtin.is_test) try glslang.testing.ensureInit();
+
+ const c = glslang.c;
+ const input: c.glslang_input_t = .{
+ .language = c.GLSLANG_SOURCE_GLSL,
+ .stage = c.GLSLANG_STAGE_FRAGMENT,
+ .client = c.GLSLANG_CLIENT_VULKAN,
+ .client_version = c.GLSLANG_TARGET_VULKAN_1_2,
+ .target_language = c.GLSLANG_TARGET_SPV,
+ .target_language_version = c.GLSLANG_TARGET_SPV_1_5,
+ .code = src.ptr,
+ .default_version = 100,
+ .default_profile = c.GLSLANG_NO_PROFILE,
+ .force_default_version_and_profile = 0,
+ .forward_compatible = 0,
+ .messages = c.GLSLANG_MSG_DEFAULT_BIT,
+ .resource = c.glslang_default_resource(),
+ };
+
+ const shader = try glslang.Shader.create(&input);
+ defer shader.delete();
+
+ shader.preprocess(&input) catch |err| {
+ if (errlog) |ptr| ptr.fromShader(shader) catch {};
+ return err;
+ };
+ shader.parse(&input) catch |err| {
+ if (errlog) |ptr| ptr.fromShader(shader) catch {};
+ return err;
+ };
+
+ const program = try glslang.Program.create();
+ defer program.delete();
+ program.addShader(shader);
+ program.link(
+ c.GLSLANG_MSG_SPV_RULES_BIT |
+ c.GLSLANG_MSG_VULKAN_RULES_BIT,
+ ) catch |err| {
+ if (errlog) |ptr| ptr.fromProgram(program) catch {};
+ return err;
+ };
+ program.spirvGenerate(c.GLSLANG_STAGE_FRAGMENT);
+ const size = program.spirvGetSize();
+ const ptr = try program.spirvGetPtr();
+ const ptr_u8: [*]u8 = @ptrCast(ptr);
+ const slice_u8: []u8 = ptr_u8[0 .. size * 4];
+ try writer.writeAll(slice_u8);
+}
+
+/// Retrieve errors from spirv compilation.
+pub const SpirvLog = struct {
+ alloc: Allocator,
+ info: [:0]const u8 = "",
+ debug: [:0]const u8 = "",
+
+ pub fn deinit(self: *const SpirvLog) void {
+ if (self.info.len > 0) self.alloc.free(self.info);
+ if (self.debug.len > 0) self.alloc.free(self.debug);
+ }
+
+ fn fromShader(self: *SpirvLog, shader: *glslang.Shader) !void {
+ const info = try shader.getInfoLog();
+ const debug = try shader.getDebugInfoLog();
+ self.info = "";
+ self.debug = "";
+ if (info.len > 0) self.info = try self.alloc.dupeZ(u8, info);
+ if (debug.len > 0) self.debug = try self.alloc.dupeZ(u8, debug);
+ }
+
+ fn fromProgram(self: *SpirvLog, program: *glslang.Program) !void {
+ const info = try program.getInfoLog();
+ const debug = try program.getDebugInfoLog();
+ self.info = "";
+ self.debug = "";
+ if (info.len > 0) self.info = try self.alloc.dupeZ(u8, info);
+ if (debug.len > 0) self.debug = try self.alloc.dupeZ(u8, debug);
+ }
+};
+
+/// Convert SPIR-V binary to MSL.
+pub fn mslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 {
+ const c = spvcross.c;
+ return try spvCross(alloc, spvcross.c.SPVC_BACKEND_MSL, spv, (struct {
+ fn setOptions(options: c.spvc_compiler_options) error{SpvcFailed}!void {
+ // We enable decoration binding, because we need this
+ // to properly locate the uniform block to index 1.
+ if (c.spvc_compiler_options_set_bool(
+ options,
+ c.SPVC_COMPILER_OPTION_MSL_ENABLE_DECORATION_BINDING,
+ c.SPVC_TRUE,
+ ) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+ }
+ }).setOptions);
+}
+
+/// Convert SPIR-V binary to GLSL.
+pub fn glslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 {
+ const GLSL_VERSION = 430;
+
+ const c = spvcross.c;
+ return try spvCross(alloc, c.SPVC_BACKEND_GLSL, spv, (struct {
+ fn setOptions(options: c.spvc_compiler_options) error{SpvcFailed}!void {
+ if (c.spvc_compiler_options_set_uint(
+ options,
+ c.SPVC_COMPILER_OPTION_GLSL_VERSION,
+ GLSL_VERSION,
+ ) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+ }
+ }).setOptions);
+}
+
+fn spvCross(
+ alloc: Allocator,
+ backend: spvcross.c.spvc_backend,
+ spv: []const u8,
+ comptime optionsFn_: ?*const fn (c: spvcross.c.spvc_compiler_options) error{SpvcFailed}!void,
+) ![:0]const u8 {
+ // Spir-V is always a multiple of 4 because it is written as a series of words
+ if (@mod(spv.len, 4) != 0) return error.SpirvInvalid;
+
+ // Compiler context
+ const c = spvcross.c;
+ var ctx: c.spvc_context = undefined;
+ if (c.spvc_context_create(&ctx) != c.SPVC_SUCCESS) return error.SpvcFailed;
+ defer c.spvc_context_destroy(ctx);
+
+ // It would be better to get this out into an output parameter to
+ // show users but for now we can just log it.
+ c.spvc_context_set_error_callback(ctx, @ptrCast(&(struct {
+ fn callback(_: ?*anyopaque, msg_ptr: [*c]const u8) callconv(.c) void {
+ const msg = std.mem.sliceTo(msg_ptr, 0);
+ std.log.warn("spirv-cross error message={s}", .{msg});
+ }
+ }).callback), null);
+
+ // Parse the Spir-V binary to an IR
+ var ir: c.spvc_parsed_ir = undefined;
+ if (c.spvc_context_parse_spirv(
+ ctx,
+ @ptrCast(@alignCast(spv.ptr)),
+ spv.len / 4,
+ &ir,
+ ) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+
+ // Build our compiler to GLSL
+ var compiler: c.spvc_compiler = undefined;
+ if (c.spvc_context_create_compiler(
+ ctx,
+ backend,
+ ir,
+ c.SPVC_CAPTURE_MODE_TAKE_OWNERSHIP,
+ &compiler,
+ ) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+
+ // Setup our options if we have any
+ if (optionsFn_) |optionsFn| {
+ var options: c.spvc_compiler_options = undefined;
+ if (c.spvc_compiler_create_compiler_options(compiler, &options) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+
+ try optionsFn(options);
+
+ if (c.spvc_compiler_install_compiler_options(compiler, options) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+ }
+
+ // Compile the resulting string. This string pointer is owned by the
+ // context so we don't need to free it.
+ var result: [*:0]const u8 = undefined;
+ if (c.spvc_compiler_compile(compiler, @ptrCast(&result)) != c.SPVC_SUCCESS) {
+ return error.SpvcFailed;
+ }
+
+ return try alloc.dupeZ(u8, std.mem.sliceTo(result, 0));
+}
+
+/// Convert ShaderToy shader to null-terminated glsl for testing.
+fn testGlslZ(alloc: Allocator, src: []const u8) ![:0]const u8 {
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ defer buf.deinit();
+ try glslFromShader(&buf.writer, src);
+ return try buf.toOwnedSliceSentinel(0);
+}
+
+test "spirv" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const src = try testGlslZ(alloc, test_crt);
+ defer alloc.free(src);
+
+ var buf: [4096 * 4]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+ try spirvFromGlsl(&writer, null, src);
+}
+
+test "spirv invalid" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const src = try testGlslZ(alloc, test_invalid);
+ defer alloc.free(src);
+
+ var buf: [4096 * 4]u8 = undefined;
+ var writer: std.Io.Writer = .fixed(&buf);
+
+ var errlog: SpirvLog = .{ .alloc = alloc };
+ defer errlog.deinit();
+ try testing.expectError(error.GlslangFailed, spirvFromGlsl(&writer, &errlog, src));
+ try testing.expect(errlog.info.len > 0);
+}
+
+test "shadertoy to msl" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const src = try testGlslZ(alloc, test_crt);
+ defer alloc.free(src);
+
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ defer buf.deinit();
+ try spirvFromGlsl(&buf.writer, null, src);
+
+ // TODO: Replace this with an aligned version of Writer.Allocating
+ var spvlist: std.ArrayListAligned(u8, .of(u32)) = .empty;
+ defer spvlist.deinit(alloc);
+ try spvlist.appendSlice(alloc, buf.written());
+
+ const msl = try mslFromSpv(alloc, spvlist.items);
+ defer alloc.free(msl);
+}
+
+test "shadertoy to glsl" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const src = try testGlslZ(alloc, test_crt);
+ defer alloc.free(src);
+
+ var buf: std.Io.Writer.Allocating = .init(alloc);
+ defer buf.deinit();
+ try spirvFromGlsl(&buf.writer, null, src);
+
+ // TODO: Replace this with an aligned version of Writer.Allocating
+ var spvlist: std.ArrayListAligned(u8, .of(u32)) = .empty;
+ defer spvlist.deinit(alloc);
+ try spvlist.appendSlice(alloc, buf.written());
+
+ const glsl = try glslFromSpv(alloc, spvlist.items);
+ defer alloc.free(glsl);
+
+ // log.warn("glsl={s}", .{glsl});
+}
+
+const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl");
+const test_invalid = @embedFile("shaders/test_shadertoy_invalid.glsl");
+const test_focus = @embedFile("shaders/test_shadertoy_focus.glsl");