renderer/metal: load custom shaders

This commit is contained in:
Mitchell Hashimoto
2023-11-16 15:19:54 -08:00
parent 2520bb3d07
commit 1e572fb10b
4 changed files with 134 additions and 7 deletions

View File

@ -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

View File

@ -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

View File

@ -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"),

View File

@ -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