mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00

This commit refactors RepeatablePath to contain a list of tagged unions containing "optional" and "required" variants. Both variants have a null terminated file path as their payload, but the tag dictates whether the path must exist or not. This implemenation is used to force consumers to handle the optional vs. required distinction. This also moves the parsing of optional file paths into RepeatablePath's parseCLI function. This allows the code to be better unit tested. Since RepeatablePath no longer contains a simple list of RepeatableStrings, many other of its methods needed to be reimplemented as well. Because all of this functionality is built into the RepeatablePath type, other config options which also use RepeatablePath gain the ability to specify optional paths as well. Right now this is only the "custom-shaders" option. The code paths in the renderer to load shader files has been updated accordingly. In the original optional config file parsing, the leading ? character was removed when paths were expanded. Thus, when config files were actually loaded recursively, they appeared to be regular (required) config files and an error occurred if the file did not exist. **This issue was not found during testing because the presence of the "theme" option masks the error**. I am not sure why the presence of "theme" does this, I did not dig into that. Now because the "optional" or "required" state of each path is tracked in the enum tag the "optional" status of the path is preserved after being expanded to an absolute path. Finally, this commit fixes a bug where missing "config-file" files were not included in the +show-config command (i.e. if a user had `config-file = foo.conf` and `foo.conf` did not exist, then `ghostty +show-config` would only display `config-file =`). This bug applied to `custom-shaders` too, where it has also been fixed.
378 lines
12 KiB
Zig
378 lines
12 KiB
Zig
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 configpkg = @import("../config.zig");
|
|
|
|
const log = std.log.scoped(.shadertoy);
|
|
|
|
/// 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).init(alloc_gpa);
|
|
defer list.deinit();
|
|
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(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 file
|
|
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: {
|
|
// SpirV pointer must be aligned to 4 bytes since we expect
|
|
// a slice of words.
|
|
var list = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc);
|
|
var errlog: SpirvLog = .{ .alloc = alloc };
|
|
defer errlog.deinit();
|
|
spirvFromGlsl(list.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;
|
|
};
|
|
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: anytype, 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: anytype,
|
|
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 {
|
|
return try spvCross(alloc, spvcross.c.SPVC_BACKEND_MSL, spv, null);
|
|
}
|
|
|
|
/// Convert SPIR-V binary to GLSL..
|
|
pub fn glslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 {
|
|
// Our minimum version for shadertoy shaders is OpenGL 4.2 because
|
|
// Spirv-Cross generates binding locations for uniforms which is
|
|
// only supported in OpenGL 4.2 and above.
|
|
//
|
|
// If we can figure out a way to NOT do this then we can lower this
|
|
// version.
|
|
const GLSL_VERSION = 420;
|
|
|
|
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 list = std.ArrayList(u8).init(alloc);
|
|
defer list.deinit();
|
|
try glslFromShader(list.writer(), src);
|
|
return try list.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 buf_stream = std.io.fixedBufferStream(&buf);
|
|
const writer = buf_stream.writer();
|
|
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 buf_stream = std.io.fixedBufferStream(&buf);
|
|
const writer = buf_stream.writer();
|
|
|
|
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 spvlist = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc);
|
|
defer spvlist.deinit();
|
|
try spirvFromGlsl(spvlist.writer(), null, src);
|
|
|
|
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 spvlist = std.ArrayListAligned(u8, @alignOf(u32)).init(alloc);
|
|
defer spvlist.deinit();
|
|
try spirvFromGlsl(spvlist.writer(), null, src);
|
|
|
|
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");
|