diff --git a/src/config/Config.zig b/src/config/Config.zig index 4f19f0bb6..563b9627f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -599,6 +599,29 @@ keybind: Keybinds = .{}, /// need KAM, you don't need it. @"vt-kam-allowed": bool = false, +/// Custom shaders to run after the default shaders. This is a file path +/// to a GLSL-syntax shader for all platforms. +/// +/// WARNING: Invalid shaders can cause Ghostty to become unusable such as by +/// causing the window to be completely black. If this happens, you can +/// unset this configuration to disable the shader. +/// +/// The shader API is identical to the ShaderToy API: you specify a `mainImage` +/// function and the available uniforms match ShaderToy. The iChannel0 uniform +/// is a texture containing the rendered terminal screen. +/// +/// If the shader fails to compile, the shader will be ignored. Any errors +/// related to shader compilation will not show up as configuration errors +/// and only show up in the log, since shader compilation happens after +/// configuration loading on the dedicated render thread. If your shader is +/// not working, another way to debug is to run the `ghostty +/// +custom-shader-compile` command which will compile the shader and show any +/// errors. For interactive development, use ShaderToy.com. +/// +/// This can be repeated multiple times to load multiple shaders. The shaders +/// will be run in the order they are specified. +@"custom-shader": RepeatablePath = .{}, + /// If anything other than false, fullscreen mode on macOS will not use the /// native fullscreen, but make the window fullscreen without animations and /// using a new space. It's faster than the native fullscreen mode since it diff --git a/src/main.zig b/src/main.zig index 16ad70ef3..3ef42d136 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator; const build_config = @import("build_config.zig"); const options = @import("build_options"); const glfw = @import("glfw"); +const glslang = @import("glslang"); const macos = @import("macos"); const tracy = @import("tracy"); const cli = @import("cli.zig"); @@ -267,6 +268,9 @@ pub const GlobalState = struct { // We need to make sure the process locale is set properly. Locale // affects a lot of behaviors in a shell. try internal_os.ensureLocale(self.alloc); + + // Initialize glslang for shader compilation + try glslang.init(); } /// Cleans up the global state. This doesn't _need_ to be called but diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 1bc4bfd1d..0b9d3ac70 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -10,6 +10,7 @@ const glfw = @import("glfw"); const objc = @import("objc"); const macos = @import("macos"); const imgui = @import("imgui"); +const glslang = @import("glslang"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); @@ -17,8 +18,10 @@ const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); +const shadertoy = @import("shadertoy.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; const Terminal = terminal.Terminal; const mtl = @import("metal/api.zig"); @@ -116,8 +119,10 @@ texture_color: objc.Object, // MTLTexture /// configuration. This must be exported so that we don't need to /// pass around Config pointers which makes memory management a pain. pub const DerivedConfig = struct { + arena: ArenaAllocator, + font_thicken: bool, - font_features: std.ArrayList([]const u8), + font_features: std.ArrayListUnmanaged([]const u8), font_styles: font.Group.StyleStatus, cursor_color: ?terminal.color.RGB, cursor_opacity: f64, @@ -128,17 +133,21 @@ pub const DerivedConfig = struct { selection_background: ?terminal.color.RGB, selection_foreground: ?terminal.color.RGB, invert_selection_fg_bg: bool, + custom_shaders: std.ArrayListUnmanaged([]const u8), pub fn init( alloc_gpa: Allocator, config: *const configpkg.Config, ) !DerivedConfig { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Copy our shaders + const custom_shaders = try config.@"custom-shader".value.list.clone(alloc); + // Copy our font features - var font_features = features: { - var clone = try config.@"font-feature".list.clone(alloc_gpa); - break :features clone.toManaged(alloc_gpa); - }; - errdefer font_features.deinit(); + const font_features = try config.@"font-feature".list.clone(alloc); // Get our font styles var font_styles = font.Group.StyleStatus.initFill(true); @@ -177,11 +186,15 @@ pub const DerivedConfig = struct { bg.toTerminalRGB() else null, + + .custom_shaders = custom_shaders, + + .arena = arena, }; } pub fn deinit(self: *DerivedConfig) void { - self.font_features.deinit(); + self.arena.deinit(); } }; @@ -203,6 +216,10 @@ pub fn surfaceInit(surface: *apprt.Surface) !void { } pub fn init(alloc: Allocator, options: renderer.Options) !Metal { + var arena = ArenaAllocator.init(alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + // Initialize our metal stuff const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); @@ -256,6 +273,17 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }); errdefer buf_instance.deinit(); + // Load our custom shaders + const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( + arena_alloc, + options.config.custom_shaders.items, + .msl, + ) catch |err| err: { + log.warn("error loading custom shaders err={}", .{err}); + break :err &.{}; + }; + _ = custom_shaders; + // Initialize our shaders var shaders = try Shaders.init(alloc, device, &.{ @embedFile("shaders/temp3.metal"), diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 3cef46e2d..1d5610c7a 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -2,9 +2,81 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; const glslang = @import("glslang"); const spvcross = @import("spirv_cross"); +const log = std.log.scoped(.shadertoy); + +/// The target to load shaders for. +pub const Target = enum { 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: []const []const u8, + target: Target, +) ![]const [:0]const u8 { + var list = std.ArrayList([:0]const u8).init(alloc_gpa); + defer list.deinit(); + errdefer for (list.items) |shader| alloc_gpa.free(shader); + + for (paths) |path| { + const shader = try loadFromFile(alloc_gpa, path, target); + log.info("loaded custom shader path={s}", .{path}); + try list.append(shader); + } + + return try list.toOwnedSlice(); +} + +/// 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(); + + // Load the shader fiel + const cwd = std.fs.cwd(); + const file = try cwd.openFile(path, .{}); + defer file.close(); + + // Read it all into memory -- we don't expect shaders to be large. + var buf_reader = std.io.bufferedReader(file.reader()); + const src = try buf_reader.reader().readAllAlloc( + alloc, + 4 * 1024 * 1024, // 4MB + ); + + // Convert to full GLSL + const glsl: [:0]const u8 = glsl: { + var list = std.ArrayList(u8).init(alloc); + try glslFromShader(list.writer(), src); + try list.append(0); + break :glsl list.items[0 .. list.items.len - 1 :0]; + }; + + // Convert to SPIR-V + const spirv: []const u8 = spirv: { + var list = std.ArrayList(u8).init(alloc); + try spirvFromGlsl(list.writer(), null, glsl); + 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. + .msl => try mslFromSpv(alloc_gpa, spirv), + }; +} + /// Convert a ShaderToy shader into valid GLSL. /// /// ShaderToy shaders aren't full shaders, they're just implementing a