From 1bd92619b19da249a3d2c6399dc442536d74abc6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 16 Nov 2023 09:22:36 -0800 Subject: [PATCH] renderer: shadertoy functions --- pkg/glslang/main.zig | 1 + pkg/glslang/program.zig | 2 +- src/renderer.zig | 1 + src/renderer/shaders/shadertoy_prefix.glsl | 27 ++++ src/renderer/shaders/test_shadertoy_crt.glsl | 59 ++++++++ .../shaders/test_shadertoy_invalid.glsl | 12 ++ src/renderer/shadertoy.zig | 142 ++++++++++++++++++ 7 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 src/renderer/shaders/shadertoy_prefix.glsl create mode 100644 src/renderer/shaders/test_shadertoy_crt.glsl create mode 100644 src/renderer/shaders/test_shadertoy_invalid.glsl create mode 100644 src/renderer/shadertoy.zig diff --git a/pkg/glslang/main.zig b/pkg/glslang/main.zig index 9b13662c0..1a93e52be 100644 --- a/pkg/glslang/main.zig +++ b/pkg/glslang/main.zig @@ -1,4 +1,5 @@ pub const c = @import("c.zig"); +pub const testing = @import("test.zig"); pub usingnamespace @import("init.zig"); pub usingnamespace @import("program.zig"); pub usingnamespace @import("shader.zig"); diff --git a/pkg/glslang/program.zig b/pkg/glslang/program.zig index 03b4a2aeb..28fc8ab9b 100644 --- a/pkg/glslang/program.zig +++ b/pkg/glslang/program.zig @@ -35,7 +35,7 @@ pub const Program = opaque { } pub fn spirvGetPtr(self: *Program) ![*]u8 { - return c.glslang_program_SPIRV_get_ptr(@ptrCast(self)); + return @ptrCast(c.glslang_program_SPIRV_get_ptr(@ptrCast(self))); } pub fn spirvGetMessages(self: *Program) ![:0]const u8 { diff --git a/src/renderer.zig b/src/renderer.zig index b2a75ae36..65ad458b6 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -15,6 +15,7 @@ const WasmTarget = @import("os/wasm/target.zig").Target; pub usingnamespace @import("renderer/cursor.zig"); pub usingnamespace @import("renderer/message.zig"); pub usingnamespace @import("renderer/size.zig"); +pub const shadertoy = @import("renderer/shadertoy.zig"); pub const Metal = @import("renderer/Metal.zig"); pub const OpenGL = @import("renderer/OpenGL.zig"); pub const WebGL = @import("renderer/WebGL.zig"); diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl new file mode 100644 index 000000000..ff3749ff2 --- /dev/null +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -0,0 +1,27 @@ +#version 430 core + +layout(binding = 0) uniform Globals { + uniform vec3 iResolution; + uniform float iTime; + uniform float iTimeDelta; + uniform float iFrameRate; + uniform int iFrame; + uniform float iChannelTime[4]; + uniform vec3 iChannelResolution[4]; + uniform vec4 iMouse; + uniform vec4 iDate; + uniform float iSampleRate; +}; + +layout(binding = 0) uniform sampler2D iChannel0; +layout(binding = 1) uniform sampler2D iChannel1; +layout(binding = 2) uniform sampler2D iChannel2; +layout(binding = 3) uniform sampler2D iChannel3; + +layout(location = 0) in vec4 gl_FragCoord; +layout(location = 0) out vec4 _fragColor; + +#define texture2D texture + +void mainImage( out vec4 fragColor, in vec2 fragCoord ); +void main() { mainImage (_fragColor, gl_FragCoord.xy); } diff --git a/src/renderer/shaders/test_shadertoy_crt.glsl b/src/renderer/shaders/test_shadertoy_crt.glsl new file mode 100644 index 000000000..b8d6dbb49 --- /dev/null +++ b/src/renderer/shaders/test_shadertoy_crt.glsl @@ -0,0 +1,59 @@ +// This shader is NOT BUILD INTO Ghostty. This is only here for unit tests. + +// Loosely based on postprocessing shader by inigo quilez, License Creative +// Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. + +vec2 curve(vec2 uv) +{ + uv = (uv - 0.5) * 2.0; + uv *= 1.1; + uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0); + uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0); + uv = (uv / 2.0) + 0.5; + uv = uv *0.92 + 0.04; + return uv; +} +void mainImage( out vec4 fragColor, in vec2 fragCoord ) +{ + vec2 q = fragCoord.xy / iResolution.xy; + vec2 uv = q; + uv = curve( uv ); + vec3 oricol = texture( iChannel0, vec2(q.x,q.y) ).xyz; + vec3 col; + float x = sin(0.3*iTime+uv.y*21.0)*sin(0.7*iTime+uv.y*29.0)*sin(0.3+0.33*iTime+uv.y*31.0)*0.0017; + + col.r = texture(iChannel0,vec2(x+uv.x+0.001,uv.y+0.001)).x+0.05; + col.g = texture(iChannel0,vec2(x+uv.x+0.000,uv.y-0.002)).y+0.05; + col.b = texture(iChannel0,vec2(x+uv.x-0.002,uv.y+0.000)).z+0.05; + col.r += 0.08*texture(iChannel0,0.75*vec2(x+0.025, -0.027)+vec2(uv.x+0.001,uv.y+0.001)).x; + col.g += 0.05*texture(iChannel0,0.75*vec2(x+-0.022, -0.02)+vec2(uv.x+0.000,uv.y-0.002)).y; + col.b += 0.08*texture(iChannel0,0.75*vec2(x+-0.02, -0.018)+vec2(uv.x-0.002,uv.y+0.000)).z; + + col = clamp(col*0.6+0.4*col*col*1.0,0.0,1.0); + + float vig = (0.0 + 1.0*16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y)); + col *= vec3(pow(vig,0.3)); + + col *= vec3(0.95,1.05,0.95); + col *= 2.8; + + float scans = clamp( 0.35+0.35*sin(3.5*iTime+uv.y*iResolution.y*1.5), 0.0, 1.0); + + float s = pow(scans,1.7); + col = col*vec3( 0.4+0.7*s) ; + + col *= 1.0+0.01*sin(110.0*iTime); + if (uv.x < 0.0 || uv.x > 1.0) + col *= 0.0; + if (uv.y < 0.0 || uv.y > 1.0) + col *= 0.0; + + col*=1.0-0.65*vec3(clamp((mod(fragCoord.x, 2.0)-1.0)*2.0,0.0,1.0)); + + float comp = smoothstep( 0.1, 0.9, sin(iTime) ); + + // Remove the next line to stop cross-fade between original and postprocess +// col = mix( col, oricol, comp ); + + fragColor = vec4(col,1.0); +} diff --git a/src/renderer/shaders/test_shadertoy_invalid.glsl b/src/renderer/shaders/test_shadertoy_invalid.glsl new file mode 100644 index 000000000..1cff1c7cf --- /dev/null +++ b/src/renderer/shaders/test_shadertoy_invalid.glsl @@ -0,0 +1,12 @@ +vec2 curve(vec2 uv) +{ + uv = (uv - 0.5) * 2.0; + uv *= 1.1; + uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0); + uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0); + uv = (uv / 2.0) + 0.5; + uv = uv *0.92 + 0.04; + return uv; +} + +// Missing mainImage! diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig new file mode 100644 index 000000000..00a2443e5 --- /dev/null +++ b/src/renderer/shadertoy.zig @@ -0,0 +1,142 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; +const glslang = @import("glslang"); + +/// 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(); + try writer.writeAll(ptr[0..size]); +} + +/// 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 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]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]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); +} + +const test_crt = @embedFile("shaders/test_shadertoy_crt.glsl"); +const test_invalid = @embedFile("shaders/test_shadertoy_invalid.glsl");