ghostty/src/renderer/shadertoy.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");