mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 08:16:13 +03:00
404 lines
13 KiB
Zig
404 lines
13 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 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),
|
|
cursor_change_time: f32 align(4),
|
|
};
|
|
|
|
/// 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 {
|
|
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 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");
|