diff --git a/.prettierignore b/.prettierignore index 02fdc5b88..a0b692219 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,6 @@ macos/ # website dev run website/.next + +# shaders +*.frag diff --git a/build.zig b/build.zig index f6164675e..cb60aabb7 100644 --- a/build.zig +++ b/build.zig @@ -643,6 +643,14 @@ fn addDeps( .optimize = step.optimize, .@"enable-libpng" = true, }); + const glslang_dep = b.dependency("glslang", .{ + .target = step.target, + .optimize = step.optimize, + }); + const spirv_cross_dep = b.dependency("spirv_cross", .{ + .target = step.target, + .optimize = step.optimize, + }); const mach_glfw_dep = b.dependency("mach_glfw", .{ .target = step.target, .optimize = step.optimize, @@ -655,6 +663,7 @@ fn addDeps( .target = step.target, .optimize = step.optimize, }); + const opengl_dep = b.dependency("opengl", .{}); const pixman_dep = b.dependency("pixman", .{ .target = step.target, .optimize = step.optimize, @@ -718,8 +727,11 @@ fn addDeps( fontconfig_dep.module("fontconfig"), ); step.addModule("freetype", freetype_dep.module("freetype")); + step.addModule("glslang", glslang_dep.module("glslang")); + step.addModule("spirv_cross", spirv_cross_dep.module("spirv_cross")); step.addModule("harfbuzz", harfbuzz_dep.module("harfbuzz")); step.addModule("xev", libxev_dep.module("xev")); + step.addModule("opengl", opengl_dep.module("opengl")); step.addModule("pixman", pixman_dep.module("pixman")); step.addModule("ziglyph", ziglyph_dep.module("ziglyph")); @@ -743,6 +755,14 @@ fn addDeps( try static_libs.append(tracy_dep.artifact("tracy").getEmittedBin()); } + // Glslang + step.linkLibrary(glslang_dep.artifact("glslang")); + try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin()); + + // Spirv-Cross + step.linkLibrary(spirv_cross_dep.artifact("spirv_cross")); + try static_libs.append(spirv_cross_dep.artifact("spirv_cross").getEmittedBin()); + // Dynamic link if (!static) { step.addIncludePath(freetype_dep.path("")); diff --git a/build.zig.zon b/build.zig.zon index ed9fec81b..309df74d1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,8 +13,8 @@ .hash = "12202da6b8e9024c653f5d67f55a8065b401c42b3c08b69333d95400fe85d6019a59", }, .zig_objc = .{ - .url = "https://github.com/mitchellh/zig-objc/archive/146a50bb018d8e1ac5b9a1454d9db9a5eba5361f.tar.gz", - .hash = "12209f62dae4fccae478f5bd5670725c55308d8d985506110ba122ee2fb5e73122e0", + .url = "https://github.com/mitchellh/zig-objc/archive/a38331cb6ee366b3f22d0068297810ef14c0c400.tar.gz", + .hash = "1220dcb34ec79a9b02c46372a41a446212f2366e7c69c8eba68e88f0f25b5ddf475d", }, .zig_js = .{ .url = "https://github.com/mitchellh/zig-js/archive/60ac42ab137461cdba2b38cc6c5e16376470aae6.tar.gz", @@ -32,10 +32,15 @@ .harfbuzz = .{ .path = "./pkg/harfbuzz" }, .libpng = .{ .path = "./pkg/libpng" }, .macos = .{ .path = "./pkg/macos" }, + .opengl = .{ .path = "./pkg/opengl" }, .pixman = .{ .path = "./pkg/pixman" }, .tracy = .{ .path = "./pkg/tracy" }, .zlib = .{ .path = "./pkg/zlib" }, + // Shader translation + .glslang = .{ .path = "./pkg/glslang" }, + .spirv_cross = .{ .path = "./pkg/spirv-cross" }, + // System headers .apple_sdk = .{ .path = "./pkg/apple-sdk" }, }, diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig new file mode 100644 index 000000000..201c0743e --- /dev/null +++ b/pkg/glslang/build.zig @@ -0,0 +1,137 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + _ = b.addModule("glslang", .{ .source_file = .{ .path = "main.zig" } }); + + const upstream = b.dependency("glslang", .{}); + const lib = try buildGlslang(b, upstream, target, optimize); + b.installArtifact(lib); + + { + const test_exe = b.addTest(.{ + .name = "test", + .root_source_file = .{ .path = "main.zig" }, + .target = target, + .optimize = optimize, + }); + test_exe.linkLibrary(lib); + const tests_run = b.addRunArtifact(test_exe); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&tests_run.step); + + // Uncomment this if we're debugging tests + // b.installArtifact(test_exe); + } +} + +fn buildGlslang( + b: *std.Build, + upstream: *std.Build.Dependency, + target: std.zig.CrossTarget, + optimize: std.builtin.OptimizeMode, +) !*std.Build.Step.Compile { + const lib = b.addStaticLibrary(.{ + .name = "glslang", + .target = target, + .optimize = optimize, + }); + lib.linkLibC(); + lib.linkLibCpp(); + lib.addIncludePath(upstream.path("")); + lib.addIncludePath(.{ .path = "override" }); + + var flags = std.ArrayList([]const u8).init(b.allocator); + defer flags.deinit(); + try flags.appendSlice(&.{ + "-fno-sanitize=undefined", + "-fno-sanitize-trap=undefined", + }); + + lib.addCSourceFiles(.{ + .dependency = upstream, + .flags = flags.items, + .files = &.{ + // GenericCodeGen + "glslang/GenericCodeGen/CodeGen.cpp", + "glslang/GenericCodeGen/Link.cpp", + + // MachineIndependent + //"MachineIndependent/glslang.y", + "glslang/MachineIndependent/glslang_tab.cpp", + "glslang/MachineIndependent/attribute.cpp", + "glslang/MachineIndependent/Constant.cpp", + "glslang/MachineIndependent/iomapper.cpp", + "glslang/MachineIndependent/InfoSink.cpp", + "glslang/MachineIndependent/Initialize.cpp", + "glslang/MachineIndependent/IntermTraverse.cpp", + "glslang/MachineIndependent/Intermediate.cpp", + "glslang/MachineIndependent/ParseContextBase.cpp", + "glslang/MachineIndependent/ParseHelper.cpp", + "glslang/MachineIndependent/PoolAlloc.cpp", + "glslang/MachineIndependent/RemoveTree.cpp", + "glslang/MachineIndependent/Scan.cpp", + "glslang/MachineIndependent/ShaderLang.cpp", + "glslang/MachineIndependent/SpirvIntrinsics.cpp", + "glslang/MachineIndependent/SymbolTable.cpp", + "glslang/MachineIndependent/Versions.cpp", + "glslang/MachineIndependent/intermOut.cpp", + "glslang/MachineIndependent/limits.cpp", + "glslang/MachineIndependent/linkValidate.cpp", + "glslang/MachineIndependent/parseConst.cpp", + "glslang/MachineIndependent/reflection.cpp", + "glslang/MachineIndependent/preprocessor/Pp.cpp", + "glslang/MachineIndependent/preprocessor/PpAtom.cpp", + "glslang/MachineIndependent/preprocessor/PpContext.cpp", + "glslang/MachineIndependent/preprocessor/PpScanner.cpp", + "glslang/MachineIndependent/preprocessor/PpTokens.cpp", + "glslang/MachineIndependent/propagateNoContraction.cpp", + + // C Interface + "glslang/CInterface/glslang_c_interface.cpp", + + // ResourceLimits + "glslang/ResourceLimits/ResourceLimits.cpp", + "glslang/ResourceLimits/resource_limits_c.cpp", + + // SPIRV + "SPIRV/GlslangToSpv.cpp", + "SPIRV/InReadableOrder.cpp", + "SPIRV/Logger.cpp", + "SPIRV/SpvBuilder.cpp", + "SPIRV/SpvPostProcess.cpp", + "SPIRV/doc.cpp", + "SPIRV/disassemble.cpp", + "SPIRV/CInterface/spirv_c_interface.cpp", + }, + }); + + if (!target.isWindows()) { + lib.addCSourceFiles(.{ + .dependency = upstream, + .flags = flags.items, + .files = &.{ + "glslang/OSDependent/Unix/ossource.cpp", + }, + }); + } else { + lib.addCSourceFiles(.{ + .dependency = upstream, + .flags = flags.items, + .files = &.{ + "glslang/OSDependent/Windows/ossource.cpp", + }, + }); + } + + lib.installHeadersDirectoryOptions(.{ + .source_dir = upstream.path(""), + .install_dir = .header, + .install_subdir = "", + .include_extensions = &.{".h"}, + }); + + return lib; +} diff --git a/pkg/glslang/build.zig.zon b/pkg/glslang/build.zig.zon new file mode 100644 index 000000000..d1ffcfa5c --- /dev/null +++ b/pkg/glslang/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = "glslang", + .version = "13.1.1", + .paths = .{""}, + .dependencies = .{ + .glslang = .{ + .url = "https://github.com/KhronosGroup/glslang/archive/refs/tags/13.1.1.tar.gz", + .hash = "1220481fe19def1172cd0728743019c0f440181a6342b62d03e24d05c70141516799", + }, + }, +} diff --git a/pkg/glslang/c.zig b/pkg/glslang/c.zig new file mode 100644 index 000000000..97d9046a5 --- /dev/null +++ b/pkg/glslang/c.zig @@ -0,0 +1,4 @@ +pub usingnamespace @cImport({ + @cInclude("glslang/Include/glslang_c_interface.h"); + @cInclude("glslang/Public/resource_limits_c.h"); +}); diff --git a/pkg/glslang/init.zig b/pkg/glslang/init.zig new file mode 100644 index 000000000..33ddd081d --- /dev/null +++ b/pkg/glslang/init.zig @@ -0,0 +1,9 @@ +const c = @import("c.zig"); + +pub fn init() !void { + if (c.glslang_initialize_process() == 0) return error.GlslangInitFailed; +} + +pub fn finalize() void { + c.glslang_finalize_process(); +} diff --git a/pkg/glslang/main.zig b/pkg/glslang/main.zig new file mode 100644 index 000000000..1a93e52be --- /dev/null +++ b/pkg/glslang/main.zig @@ -0,0 +1,9 @@ +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"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/pkg/glslang/override/glslang/build_info.h b/pkg/glslang/override/glslang/build_info.h new file mode 100644 index 000000000..c25117eef --- /dev/null +++ b/pkg/glslang/override/glslang/build_info.h @@ -0,0 +1,62 @@ +// Copyright (C) 2020 The Khronos Group Inc. +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions +// are met: +// +// Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// +// Neither the name of The Khronos Group Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +// COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +#ifndef GLSLANG_BUILD_INFO +#define GLSLANG_BUILD_INFO + +#define GLSLANG_VERSION_MAJOR 13 +#define GLSLANG_VERSION_MINOR 1 +#define GLSLANG_VERSION_PATCH 1 +#define GLSLANG_VERSION_FLAVOR "" + +#define GLSLANG_VERSION_GREATER_THAN(major, minor, patch) \ + ((GLSLANG_VERSION_MAJOR) > (major) || ((major) == GLSLANG_VERSION_MAJOR && \ + ((GLSLANG_VERSION_MINOR) > (minor) || ((minor) == GLSLANG_VERSION_MINOR && \ + (GLSLANG_VERSION_PATCH) > (patch))))) + +#define GLSLANG_VERSION_GREATER_OR_EQUAL_TO(major, minor, patch) \ + ((GLSLANG_VERSION_MAJOR) > (major) || ((major) == GLSLANG_VERSION_MAJOR && \ + ((GLSLANG_VERSION_MINOR) > (minor) || ((minor) == GLSLANG_VERSION_MINOR && \ + (GLSLANG_VERSION_PATCH >= (patch)))))) + +#define GLSLANG_VERSION_LESS_THAN(major, minor, patch) \ + ((GLSLANG_VERSION_MAJOR) < (major) || ((major) == GLSLANG_VERSION_MAJOR && \ + ((GLSLANG_VERSION_MINOR) < (minor) || ((minor) == GLSLANG_VERSION_MINOR && \ + (GLSLANG_VERSION_PATCH) < (patch))))) + +#define GLSLANG_VERSION_LESS_OR_EQUAL_TO(major, minor, patch) \ + ((GLSLANG_VERSION_MAJOR) < (major) || ((major) == GLSLANG_VERSION_MAJOR && \ + ((GLSLANG_VERSION_MINOR) < (minor) || ((minor) == GLSLANG_VERSION_MINOR && \ + (GLSLANG_VERSION_PATCH <= (patch)))))) + +#endif // GLSLANG_BUILD_INFO diff --git a/pkg/glslang/program.zig b/pkg/glslang/program.zig new file mode 100644 index 000000000..70d3c88cd --- /dev/null +++ b/pkg/glslang/program.zig @@ -0,0 +1,60 @@ +const std = @import("std"); +const c = @import("c.zig"); +const testlib = @import("test.zig"); +const Shader = @import("shader.zig").Shader; + +pub const Program = opaque { + pub fn create() !*Program { + if (c.glslang_program_create()) |ptr| return @ptrCast(ptr); + return error.OutOfMemory; + } + + pub fn delete(self: *Program) void { + c.glslang_program_delete(@ptrCast(self)); + } + + pub fn addShader(self: *Program, shader: *Shader) void { + c.glslang_program_add_shader(@ptrCast(self), @ptrCast(shader)); + } + + pub fn link(self: *Program, messages: c_int) !void { + if (c.glslang_program_link(@ptrCast(self), messages) != 0) return; + return error.GlslangFailed; + } + + pub fn spirvGenerate(self: *Program, stage: c.glslang_stage_t) void { + c.glslang_program_SPIRV_generate(@ptrCast(self), stage); + } + + pub fn spirvGetSize(self: *Program) usize { + return @intCast(c.glslang_program_SPIRV_get_size(@ptrCast(self))); + } + + pub fn spirvGet(self: *Program, buf: []u32) void { + c.glslang_program_SPIRV_get(@ptrCast(self), buf.ptr); + } + + pub fn spirvGetPtr(self: *Program) ![*]u32 { + return @ptrCast(c.glslang_program_SPIRV_get_ptr(@ptrCast(self))); + } + + pub fn spirvGetMessages(self: *Program) ![:0]const u8 { + const ptr = c.glslang_program_SPIRV_get_messages(@ptrCast(self)); + return std.mem.sliceTo(ptr, 0); + } + + pub fn getInfoLog(self: *Program) ![:0]const u8 { + const ptr = c.glslang_program_get_info_log(@ptrCast(self)); + return std.mem.sliceTo(ptr, 0); + } + + pub fn getDebugInfoLog(self: *Program) ![:0]const u8 { + const ptr = c.glslang_program_get_info_debug_log(@ptrCast(self)); + return std.mem.sliceTo(ptr, 0); + } +}; + +test { + var program = try Program.create(); + defer program.delete(); +} diff --git a/pkg/glslang/shader.zig b/pkg/glslang/shader.zig new file mode 100644 index 000000000..90e5e192f --- /dev/null +++ b/pkg/glslang/shader.zig @@ -0,0 +1,58 @@ +const std = @import("std"); +const c = @import("c.zig"); +const testlib = @import("test.zig"); + +pub const Shader = opaque { + pub fn create(input: *const c.glslang_input_t) !*Shader { + if (c.glslang_shader_create(input)) |ptr| return @ptrCast(ptr); + return error.OutOfMemory; + } + + pub fn delete(self: *Shader) void { + c.glslang_shader_delete(@ptrCast(self)); + } + + pub fn preprocess(self: *Shader, input: *const c.glslang_input_t) !void { + if (c.glslang_shader_preprocess(@ptrCast(self), input) == 0) + return error.GlslangFailed; + } + + pub fn parse(self: *Shader, input: *const c.glslang_input_t) !void { + if (c.glslang_shader_parse(@ptrCast(self), input) == 0) + return error.GlslangFailed; + } + + pub fn getInfoLog(self: *Shader) ![:0]const u8 { + const ptr = c.glslang_shader_get_info_log(@ptrCast(self)); + return std.mem.sliceTo(ptr, 0); + } + + pub fn getDebugInfoLog(self: *Shader) ![:0]const u8 { + const ptr = c.glslang_shader_get_info_debug_log(@ptrCast(self)); + return std.mem.sliceTo(ptr, 0); + } +}; + +test { + 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 = @embedFile("test/simple.frag"), + .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(), + }; + + try testlib.ensureInit(); + const shader = try Shader.create(&input); + defer shader.delete(); + try shader.preprocess(&input); + try shader.parse(&input); +} diff --git a/pkg/glslang/test.zig b/pkg/glslang/test.zig new file mode 100644 index 000000000..8cdf98f75 --- /dev/null +++ b/pkg/glslang/test.zig @@ -0,0 +1,10 @@ +const glslang = @import("main.zig"); + +var initialized: bool = false; + +/// Call this function before any other tests in this package to ensure that +/// the glslang library is initialized. +pub fn ensureInit() !void { + if (initialized) return; + try glslang.init(); +} diff --git a/pkg/glslang/test/simple.frag b/pkg/glslang/test/simple.frag new file mode 100644 index 000000000..c1cd903ce --- /dev/null +++ b/pkg/glslang/test/simple.frag @@ -0,0 +1,56 @@ +#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); } + +#define t iTime + +void mainImage( out vec4 fragColor, in vec2 fragCoord ) +{ + // Normalized pixel coordinates (from 0 to 1) + vec2 uv = ( fragCoord - .5*iResolution.xy) / iResolution.y; + vec3 col = vec3(0.); + float a = atan(uv.y,uv.x); + float r = 0.5*length(uv); + float counter = 100.; + a = 4.*a+20.*r+50.*cos(r)*cos(.1*t)+abs(a*r); + float f = 0.02*abs(cos(a))/(r*r); + + + vec2 v = vec2(0.); + for(float i=0.;i2.){ + counter = i; + break; + } + } + + col=vec3(min(0.9,1.2*exp(-pow(f,0.45)*counter))); + + fragColor = min(0.9,1.2*exp(-pow(f,0.45)*counter) ) + * ( 0.7 + 0.3* cos(10.*r - 2.*t -vec4(.7,1.4,2.1,0) ) ); +} diff --git a/pkg/macos/build.zig b/pkg/macos/build.zig index 942100e45..cf8b4a541 100644 --- a/pkg/macos/build.zig +++ b/pkg/macos/build.zig @@ -28,6 +28,7 @@ pub fn build(b: *std.Build) !void { lib.linkFramework("CoreFoundation"); lib.linkFramework("CoreGraphics"); lib.linkFramework("CoreText"); + lib.linkFramework("CoreVideo"); if (!target.isNative()) try apple_sdk.addPaths(b, lib); b.installArtifact(lib); diff --git a/pkg/macos/main.zig b/pkg/macos/main.zig index 73bc5d3f6..46071d55b 100644 --- a/pkg/macos/main.zig +++ b/pkg/macos/main.zig @@ -2,6 +2,7 @@ pub const foundation = @import("foundation.zig"); pub const graphics = @import("graphics.zig"); pub const os = @import("os.zig"); pub const text = @import("text.zig"); +pub const video = @import("video.zig"); test { @import("std").testing.refAllDecls(@This()); diff --git a/pkg/macos/video.zig b/pkg/macos/video.zig new file mode 100644 index 000000000..c04f5cf27 --- /dev/null +++ b/pkg/macos/video.zig @@ -0,0 +1,6 @@ +pub const c = @import("video/c.zig"); +pub usingnamespace @import("video/display_link.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/pkg/macos/video/c.zig b/pkg/macos/video/c.zig new file mode 100644 index 000000000..46c5d9ef3 --- /dev/null +++ b/pkg/macos/video/c.zig @@ -0,0 +1,3 @@ +pub usingnamespace @cImport({ + @cInclude("CoreVideo/CoreVideo.h"); +}); diff --git a/pkg/macos/video/display_link.zig b/pkg/macos/video/display_link.zig new file mode 100644 index 000000000..e7f4844a0 --- /dev/null +++ b/pkg/macos/video/display_link.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const c = @import("c.zig"); + +pub const DisplayLink = opaque { + pub const Error = error{ + InvalidOperation, + }; + + pub fn createWithActiveCGDisplays() Allocator.Error!*DisplayLink { + var result: ?*DisplayLink = null; + if (c.CVDisplayLinkCreateWithActiveCGDisplays( + @ptrCast(&result), + ) != c.kCVReturnSuccess) + return error.OutOfMemory; + + return result orelse error.OutOfMemory; + } + + pub fn release(self: *DisplayLink) void { + c.CVDisplayLinkRelease(@ptrCast(self)); + } + + pub fn start(self: *DisplayLink) Error!void { + if (c.CVDisplayLinkStart(@ptrCast(self)) != c.kCVReturnSuccess) + return error.InvalidOperation; + } + + pub fn stop(self: *DisplayLink) Error!void { + if (c.CVDisplayLinkStop(@ptrCast(self)) != c.kCVReturnSuccess) + return error.InvalidOperation; + } + + pub fn isRunning(self: *DisplayLink) bool { + return c.CVDisplayLinkIsRunning(@ptrCast(self)) != 0; + } + + // Note: this purposely throws away a ton of arguments I didn't need. + // It would be trivial to refactor this into Zig types and properly + // pass this through. + pub fn setOutputCallback( + self: *DisplayLink, + comptime callbackFn: *const fn (*DisplayLink, ?*anyopaque) void, + userinfo: ?*anyopaque, + ) Error!void { + if (c.CVDisplayLinkSetOutputCallback( + @ptrCast(self), + @ptrCast(&(struct { + fn callback( + displayLink: *DisplayLink, + inNow: *const c.CVTimeStamp, + inOutputTime: *const c.CVTimeStamp, + flagsIn: c.CVOptionFlags, + flagsOut: *c.CVOptionFlags, + inner_userinfo: ?*anyopaque, + ) callconv(.C) c.CVReturn { + _ = inNow; + _ = inOutputTime; + _ = flagsIn; + _ = flagsOut; + + callbackFn(displayLink, inner_userinfo); + return c.kCVReturnSuccess; + } + }).callback), + userinfo, + ) != c.kCVReturnSuccess) + return error.InvalidOperation; + } +}; diff --git a/src/renderer/opengl/Buffer.zig b/pkg/opengl/Buffer.zig similarity index 73% rename from src/renderer/opengl/Buffer.zig rename to pkg/opengl/Buffer.zig index b794ca4f0..a8ba099d3 100644 --- a/src/renderer/opengl/Buffer.zig +++ b/pkg/opengl/Buffer.zig @@ -7,76 +7,106 @@ const glad = @import("glad.zig"); id: c.GLuint, -/// Enum for possible binding targets. -pub const Target = enum(c_uint) { - ArrayBuffer = c.GL_ARRAY_BUFFER, - ElementArrayBuffer = c.GL_ELEMENT_ARRAY_BUFFER, - _, -}; +/// Create a single buffer. +pub fn create() !Buffer { + var vbo: c.GLuint = undefined; + glad.context.GenBuffers.?(1, &vbo); + return Buffer{ .id = vbo }; +} -/// Enum for possible buffer usages. -pub const Usage = enum(c_uint) { - StreamDraw = c.GL_STREAM_DRAW, - StreamRead = c.GL_STREAM_READ, - StreamCopy = c.GL_STREAM_COPY, - StaticDraw = c.GL_STATIC_DRAW, - StaticRead = c.GL_STATIC_READ, - StaticCopy = c.GL_STATIC_COPY, - DynamicDraw = c.GL_DYNAMIC_DRAW, - DynamicRead = c.GL_DYNAMIC_READ, - DynamicCopy = c.GL_DYNAMIC_COPY, - _, -}; +/// glBindBuffer +pub fn bind(self: Buffer, target: Target) !Binding { + glad.context.BindBuffer.?(@intFromEnum(target), self.id); + return Binding{ .id = self.id, .target = target }; +} + +pub fn destroy(self: Buffer) void { + glad.context.DeleteBuffers.?(1, &self.id); +} + +pub fn bindBase(self: Buffer, target: Target, idx: c.GLuint) !void { + glad.context.BindBufferBase.?( + @intFromEnum(target), + idx, + self.id, + ); + try errors.getError(); +} /// Binding is a bound buffer. By using this for functions that operate /// on bound buffers, you can easily defer unbinding and in safety-enabled /// modes verify that unbound buffers are never accessed. pub const Binding = struct { + id: c.GLuint, target: Target, + pub fn unbind(b: Binding) void { + glad.context.BindBuffer.?(@intFromEnum(b.target), 0); + } + /// Sets the data of this bound buffer. The data can be any array-like /// type. The size of the data is automatically determined based on the type. - pub inline fn setData( + pub fn setData( b: Binding, data: anytype, usage: Usage, ) !void { const info = dataInfo(&data); - glad.context.BufferData.?(@intFromEnum(b.target), info.size, info.ptr, @intFromEnum(usage)); + glad.context.BufferData.?( + @intFromEnum(b.target), + info.size, + info.ptr, + @intFromEnum(usage), + ); try errors.getError(); } /// Sets the data of this bound buffer. The data can be any array-like /// type. The size of the data is automatically determined based on the type. - pub inline fn setSubData( + pub fn setSubData( b: Binding, offset: usize, data: anytype, ) !void { const info = dataInfo(data); - glad.context.BufferSubData.?(@intFromEnum(b.target), @intCast(offset), info.size, info.ptr); + glad.context.BufferSubData.?( + @intFromEnum(b.target), + @intCast(offset), + info.size, + info.ptr, + ); try errors.getError(); } /// Sets the buffer data with a null buffer that is expected to be /// filled in the future using subData. This requires the type just so /// we can setup the data size. - pub inline fn setDataNull( + pub fn setDataNull( b: Binding, comptime T: type, usage: Usage, ) !void { - glad.context.BufferData.?(@intFromEnum(b.target), @sizeOf(T), null, @intFromEnum(usage)); + glad.context.BufferData.?( + @intFromEnum(b.target), + @sizeOf(T), + null, + @intFromEnum(usage), + ); try errors.getError(); } /// Same as setDataNull but lets you manually specify the buffer size. - pub inline fn setDataNullManual( + pub fn setDataNullManual( b: Binding, size: usize, usage: Usage, ) !void { - glad.context.BufferData.?(@intFromEnum(b.target), @intCast(size), null, @intFromEnum(usage)); + glad.context.BufferData.?( + @intFromEnum(b.target), + @intCast(size), + null, + @intFromEnum(usage), + ); try errors.getError(); } @@ -87,7 +117,7 @@ pub const Binding = struct { return switch (@typeInfo(@TypeOf(data))) { .Pointer => |ptr| switch (ptr.size) { .One => .{ - .size = @sizeOf(ptr.child) * data.len, + .size = @sizeOf(ptr.child), .ptr = data, }, .Slice => .{ @@ -106,7 +136,7 @@ pub const Binding = struct { }; } - pub inline fn enableAttribArray(_: Binding, idx: c.GLuint) !void { + pub fn enableAttribArray(_: Binding, idx: c.GLuint) !void { glad.context.EnableVertexAttribArray.?(idx); } @@ -158,7 +188,7 @@ pub const Binding = struct { try errors.getError(); } - pub inline fn attributeAdvanced( + pub fn attributeAdvanced( _: Binding, idx: c.GLuint, size: c.GLint, @@ -177,7 +207,7 @@ pub const Binding = struct { try errors.getError(); } - pub inline fn attributeIAdvanced( + pub fn attributeIAdvanced( _: Binding, idx: c.GLuint, size: c.GLint, @@ -193,26 +223,26 @@ pub const Binding = struct { glad.context.VertexAttribIPointer.?(idx, size, typ, stride, offsetPtr); try errors.getError(); } - - pub inline fn unbind(b: *Binding) void { - glad.context.BindBuffer.?(@intFromEnum(b.target), 0); - b.* = undefined; - } }; -/// Create a single buffer. -pub inline fn create() !Buffer { - var vbo: c.GLuint = undefined; - glad.context.GenBuffers.?(1, &vbo); - return Buffer{ .id = vbo }; -} +/// Enum for possible binding targets. +pub const Target = enum(c_uint) { + array = c.GL_ARRAY_BUFFER, + element_array = c.GL_ELEMENT_ARRAY_BUFFER, + uniform = c.GL_UNIFORM_BUFFER, + _, +}; -/// glBindBuffer -pub inline fn bind(v: Buffer, target: Target) !Binding { - glad.context.BindBuffer.?(@intFromEnum(target), v.id); - return Binding{ .target = target }; -} - -pub inline fn destroy(v: Buffer) void { - glad.context.DeleteBuffers.?(1, &v.id); -} +/// Enum for possible buffer usages. +pub const Usage = enum(c_uint) { + stream_draw = c.GL_STREAM_DRAW, + stream_read = c.GL_STREAM_READ, + stream_copy = c.GL_STREAM_COPY, + static_draw = c.GL_STATIC_DRAW, + static_read = c.GL_STATIC_READ, + static_copy = c.GL_STATIC_COPY, + dynamic_draw = c.GL_DYNAMIC_DRAW, + dynamic_read = c.GL_DYNAMIC_READ, + dynamic_copy = c.GL_DYNAMIC_COPY, + _, +}; diff --git a/pkg/opengl/Framebuffer.zig b/pkg/opengl/Framebuffer.zig new file mode 100644 index 000000000..8ab07a238 --- /dev/null +++ b/pkg/opengl/Framebuffer.zig @@ -0,0 +1,92 @@ +const Framebuffer = @This(); + +const std = @import("std"); +const c = @import("c.zig"); +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); +const Texture = @import("Texture.zig"); + +id: c.GLuint, + +/// Create a single buffer. +pub fn create() !Framebuffer { + var fbo: c.GLuint = undefined; + glad.context.GenFramebuffers.?(1, &fbo); + return .{ .id = fbo }; +} + +pub fn destroy(v: Framebuffer) void { + glad.context.DeleteFramebuffers.?(1, &v.id); +} + +pub fn bind(v: Framebuffer, target: Target) !Binding { + // The default framebuffer is documented as being zero but + // on multiple OpenGL drivers its not zero, so we grab it + // at runtime. + var current: c.GLint = undefined; + glad.context.GetIntegerv.?(c.GL_FRAMEBUFFER_BINDING, ¤t); + glad.context.BindFramebuffer.?(@intFromEnum(target), v.id); + return .{ .target = target, .previous = @intCast(current) }; +} + +/// Enum for possible binding targets. +pub const Target = enum(c_uint) { + framebuffer = c.GL_FRAMEBUFFER, + draw = c.GL_DRAW_FRAMEBUFFER, + read = c.GL_READ_FRAMEBUFFER, + _, +}; + +pub const Attachment = enum(c_uint) { + color0 = c.GL_COLOR_ATTACHMENT0, + depth = c.GL_DEPTH_ATTACHMENT, + stencil = c.GL_STENCIL_ATTACHMENT, + depth_stencil = c.GL_DEPTH_STENCIL_ATTACHMENT, + _, +}; + +pub const Status = enum(c_uint) { + complete = c.GL_FRAMEBUFFER_COMPLETE, + undefined = c.GL_FRAMEBUFFER_UNDEFINED, + incomplete_attachment = c.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT, + incomplete_missing_attachment = c.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT, + incomplete_draw_buffer = c.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER, + incomplete_read_buffer = c.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER, + unsupported = c.GL_FRAMEBUFFER_UNSUPPORTED, + incomplete_multisample = c.GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE, + incomplete_layer_targets = c.GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS, + _, +}; + +pub const Binding = struct { + target: Target, + previous: c.GLuint, + + pub fn unbind(self: Binding) void { + glad.context.BindFramebuffer.?( + @intFromEnum(self.target), + self.previous, + ); + } + + pub fn texture2D( + self: Binding, + attachment: Attachment, + textarget: Texture.Target, + texture: Texture, + level: c.GLint, + ) !void { + glad.context.FramebufferTexture2D.?( + @intFromEnum(self.target), + @intFromEnum(attachment), + @intFromEnum(textarget), + texture.id, + level, + ); + try errors.getError(); + } + + pub fn checkStatus(self: Binding) Status { + return @enumFromInt(glad.context.CheckFramebufferStatus.?(@intFromEnum(self.target))); + } +}; diff --git a/src/renderer/opengl/Program.zig b/pkg/opengl/Program.zig similarity index 83% rename from src/renderer/opengl/Program.zig rename to pkg/opengl/Program.zig index d266bd226..3a2f2036a 100644 --- a/src/renderer/opengl/Program.zig +++ b/pkg/opengl/Program.zig @@ -11,23 +11,22 @@ const glad = @import("glad.zig"); id: c.GLuint, -const Binding = struct { - pub inline fn unbind(_: Binding) void { +pub const Binding = struct { + pub fn unbind(_: Binding) void { glad.context.UseProgram.?(0); } }; -pub inline fn create() !Program { +pub fn create() !Program { const id = glad.context.CreateProgram.?(); if (id == 0) try errors.mustError(); - log.debug("program created id={}", .{id}); - return Program{ .id = id }; + return .{ .id = id }; } /// Create a program from a vertex and fragment shader source. This will /// compile and link the vertex and fragment shader. -pub inline fn createVF(vsrc: [:0]const u8, fsrc: [:0]const u8) !Program { +pub fn createVF(vsrc: [:0]const u8, fsrc: [:0]const u8) !Program { const vs = try Shader.create(c.GL_VERTEX_SHADER); try vs.setSourceAndCompile(vsrc); defer vs.destroy(); @@ -44,12 +43,18 @@ pub inline fn createVF(vsrc: [:0]const u8, fsrc: [:0]const u8) !Program { return p; } -pub inline fn attachShader(p: Program, s: Shader) !void { +pub fn destroy(p: Program) void { + assert(p.id != 0); + glad.context.DeleteProgram.?(p.id); + log.debug("program destroyed id={}", .{p.id}); +} + +pub fn attachShader(p: Program, s: Shader) !void { glad.context.AttachShader.?(p.id, s.id); try errors.getError(); } -pub inline fn link(p: Program) !void { +pub fn link(p: Program) !void { glad.context.LinkProgram.?(p.id); // Check if linking succeeded @@ -67,14 +72,23 @@ pub inline fn link(p: Program) !void { return error.CompileFailed; } -pub inline fn use(p: Program) !Binding { +pub fn use(p: Program) !Binding { glad.context.UseProgram.?(p.id); try errors.getError(); - return Binding{}; + return .{}; +} + +pub fn uniformBlockBinding( + self: Program, + index: c.GLuint, + binding: c.GLuint, +) !void { + glad.context.UniformBlockBinding.?(self.id, index, binding); + try errors.getError(); } /// Requires the program is currently in use. -pub inline fn setUniform( +pub fn setUniform( p: Program, n: [:0]const u8, value: anytype, @@ -115,14 +129,8 @@ pub inline fn setUniform( // // NOTE(mitchellh): we can add a dynamic version that uses an allocator // if we ever need it. -pub inline fn getInfoLog(s: Program) [512]u8 { +pub fn getInfoLog(s: Program) [512]u8 { var msg: [512]u8 = undefined; glad.context.GetProgramInfoLog.?(s.id, msg.len, null, &msg); return msg; } - -pub inline fn destroy(p: Program) void { - assert(p.id != 0); - glad.context.DeleteProgram.?(p.id); - log.debug("program destroyed id={}", .{p.id}); -} diff --git a/src/renderer/opengl/Shader.zig b/pkg/opengl/Shader.zig similarity index 100% rename from src/renderer/opengl/Shader.zig rename to pkg/opengl/Shader.zig diff --git a/src/renderer/opengl/Texture.zig b/pkg/opengl/Texture.zig similarity index 90% rename from src/renderer/opengl/Texture.zig rename to pkg/opengl/Texture.zig index 91a65b565..afa22e926 100644 --- a/src/renderer/opengl/Texture.zig +++ b/pkg/opengl/Texture.zig @@ -7,11 +7,29 @@ const glad = @import("glad.zig"); id: c.GLuint, -pub inline fn active(target: c.GLenum) !void { +pub fn active(target: c.GLenum) !void { glad.context.ActiveTexture.?(target); try errors.getError(); } +/// Create a single texture. +pub fn create() !Texture { + var id: c.GLuint = undefined; + glad.context.GenTextures.?(1, &id); + return .{ .id = id }; +} + +/// glBindTexture +pub fn bind(v: Texture, target: Target) !Binding { + glad.context.BindTexture.?(@intFromEnum(target), v.id); + try errors.getError(); + return .{ .target = target }; +} + +pub fn destroy(v: Texture) void { + glad.context.DeleteTextures.?(1, &v.id); +} + /// Enun for possible texture binding targets. pub const Target = enum(c_uint) { @"1D" = c.GL_TEXTURE_1D, @@ -48,8 +66,9 @@ pub const Parameter = enum(c_uint) { /// Internal format enum for texture images. pub const InternalFormat = enum(c_int) { - Red = c.GL_RED, - RGBA = c.GL_RGBA, + red = c.GL_RED, + rgb = c.GL_RGB, + rgba = c.GL_RGBA, // There are so many more that I haven't filled in. _, @@ -57,8 +76,9 @@ pub const InternalFormat = enum(c_int) { /// Format for texture images pub const Format = enum(c_uint) { - Red = c.GL_RED, - BGRA = c.GL_BGRA, + red = c.GL_RED, + rgb = c.GL_RGB, + bgra = c.GL_BGRA, // There are so many more that I haven't filled in. _, @@ -75,9 +95,8 @@ pub const DataType = enum(c_uint) { pub const Binding = struct { target: Target, - pub inline fn unbind(b: *Binding) void { + pub fn unbind(b: *const Binding) void { glad.context.BindTexture.?(@intFromEnum(b.target), 0); - b.* = undefined; } pub fn generateMipmap(b: Binding) void { @@ -143,21 +162,3 @@ pub const Binding = struct { ); } }; - -/// Create a single texture. -pub inline fn create() !Texture { - var id: c.GLuint = undefined; - glad.context.GenTextures.?(1, &id); - return Texture{ .id = id }; -} - -/// glBindTexture -pub inline fn bind(v: Texture, target: Target) !Binding { - glad.context.BindTexture.?(@intFromEnum(target), v.id); - try errors.getError(); - return Binding{ .target = target }; -} - -pub inline fn destroy(v: Texture) void { - glad.context.DeleteTextures.?(1, &v.id); -} diff --git a/src/renderer/opengl/VertexArray.zig b/pkg/opengl/VertexArray.zig similarity index 63% rename from src/renderer/opengl/VertexArray.zig rename to pkg/opengl/VertexArray.zig index b86794042..4071c3a2a 100644 --- a/src/renderer/opengl/VertexArray.zig +++ b/pkg/opengl/VertexArray.zig @@ -7,23 +7,26 @@ const errors = @import("errors.zig"); id: c.GLuint, /// Create a single vertex array object. -pub inline fn create() !VertexArray { +pub fn create() !VertexArray { var vao: c.GLuint = undefined; glad.context.GenVertexArrays.?(1, &vao); return VertexArray{ .id = vao }; } -// Unbind any active vertex array. -pub inline fn unbind() !void { - glad.context.BindVertexArray.?(0); -} - /// glBindVertexArray -pub inline fn bind(v: VertexArray) !void { +pub fn bind(v: VertexArray) !Binding { glad.context.BindVertexArray.?(v.id); try errors.getError(); + return .{}; } -pub inline fn destroy(v: VertexArray) void { +pub fn destroy(v: VertexArray) void { glad.context.DeleteVertexArrays.?(1, &v.id); } + +pub const Binding = struct { + pub fn unbind(self: Binding) void { + _ = self; + glad.context.BindVertexArray.?(0); + } +}; diff --git a/pkg/opengl/build.zig b/pkg/opengl/build.zig new file mode 100644 index 000000000..34e5a8ab1 --- /dev/null +++ b/pkg/opengl/build.zig @@ -0,0 +1,5 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + _ = b.addModule("opengl", .{ .source_file = .{ .path = "main.zig" } }); +} diff --git a/src/renderer/opengl/c.zig b/pkg/opengl/c.zig similarity index 100% rename from src/renderer/opengl/c.zig rename to pkg/opengl/c.zig diff --git a/src/renderer/opengl/draw.zig b/pkg/opengl/draw.zig similarity index 100% rename from src/renderer/opengl/draw.zig rename to pkg/opengl/draw.zig diff --git a/src/renderer/opengl/errors.zig b/pkg/opengl/errors.zig similarity index 100% rename from src/renderer/opengl/errors.zig rename to pkg/opengl/errors.zig diff --git a/src/renderer/opengl/extensions.zig b/pkg/opengl/extensions.zig similarity index 100% rename from src/renderer/opengl/extensions.zig rename to pkg/opengl/extensions.zig diff --git a/src/renderer/opengl/glad.zig b/pkg/opengl/glad.zig similarity index 100% rename from src/renderer/opengl/glad.zig rename to pkg/opengl/glad.zig diff --git a/src/renderer/opengl/main.zig b/pkg/opengl/main.zig similarity index 94% rename from src/renderer/opengl/main.zig rename to pkg/opengl/main.zig index 79d32acea..9045beabf 100644 --- a/src/renderer/opengl/main.zig +++ b/pkg/opengl/main.zig @@ -17,6 +17,7 @@ pub usingnamespace @import("draw.zig"); pub const ext = @import("extensions.zig"); pub const Buffer = @import("Buffer.zig"); +pub const Framebuffer = @import("Framebuffer.zig"); pub const Program = @import("Program.zig"); pub const Shader = @import("Shader.zig"); pub const Texture = @import("Texture.zig"); diff --git a/pkg/spirv-cross/build.zig b/pkg/spirv-cross/build.zig new file mode 100644 index 000000000..13ff7d827 --- /dev/null +++ b/pkg/spirv-cross/build.zig @@ -0,0 +1,85 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + _ = b.addModule("spirv_cross", .{ .source_file = .{ .path = "main.zig" } }); + + const upstream = b.dependency("spirv_cross", .{}); + const lib = try buildSpirvCross(b, upstream, target, optimize); + b.installArtifact(lib); + + { + const test_exe = b.addTest(.{ + .name = "test", + .root_source_file = .{ .path = "main.zig" }, + .target = target, + .optimize = optimize, + }); + test_exe.linkLibrary(lib); + const tests_run = b.addRunArtifact(test_exe); + const test_step = b.step("test", "Run tests"); + test_step.dependOn(&tests_run.step); + + // Uncomment this if we're debugging tests + // b.installArtifact(test_exe); + } +} + +fn buildSpirvCross( + b: *std.Build, + upstream: *std.Build.Dependency, + target: std.zig.CrossTarget, + optimize: std.builtin.OptimizeMode, +) !*std.Build.Step.Compile { + const lib = b.addStaticLibrary(.{ + .name = "spirv_cross", + .target = target, + .optimize = optimize, + }); + lib.linkLibC(); + lib.linkLibCpp(); + //lib.addIncludePath(upstream.path("")); + //lib.addIncludePath(.{ .path = "override" }); + + var flags = std.ArrayList([]const u8).init(b.allocator); + defer flags.deinit(); + try flags.appendSlice(&.{ + "-DSPIRV_CROSS_C_API_GLSL=1", + "-DSPIRV_CROSS_C_API_MSL=1", + + "-fno-sanitize=undefined", + "-fno-sanitize-trap=undefined", + }); + + lib.addCSourceFiles(.{ + .dependency = upstream, + .flags = flags.items, + .files = &.{ + // Core + "spirv_cross.cpp", + "spirv_parser.cpp", + "spirv_cross_parsed_ir.cpp", + "spirv_cfg.cpp", + + // C + "spirv_cross_c.cpp", + + // GLSL + "spirv_glsl.cpp", + + // MSL + "spirv_msl.cpp", + }, + }); + + lib.installHeadersDirectoryOptions(.{ + .source_dir = upstream.path(""), + .install_dir = .header, + .install_subdir = "", + .include_extensions = &.{".h"}, + }); + + return lib; +} diff --git a/pkg/spirv-cross/build.zig.zon b/pkg/spirv-cross/build.zig.zon new file mode 100644 index 000000000..8338b7a61 --- /dev/null +++ b/pkg/spirv-cross/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = "spirv-cross", + .version = "13.1.1", + .paths = .{""}, + .dependencies = .{ + .spirv_cross = .{ + .url = "https://github.com/KhronosGroup/SPIRV-Cross/archive/4818f7e7ef7b7078a3a7a5a52c4a338e0dda22f4.tar.gz", + .hash = "1220b2d8a6cff1926ef28a29e312a0a503b555ebc2f082230b882410f49e672ac9c6", + }, + }, +} diff --git a/pkg/spirv-cross/c.zig b/pkg/spirv-cross/c.zig new file mode 100644 index 000000000..42ad77dab --- /dev/null +++ b/pkg/spirv-cross/c.zig @@ -0,0 +1,3 @@ +pub usingnamespace @cImport({ + @cInclude("spirv_cross_c.h"); +}); diff --git a/pkg/spirv-cross/main.zig b/pkg/spirv-cross/main.zig new file mode 100644 index 000000000..e66cd7094 --- /dev/null +++ b/pkg/spirv-cross/main.zig @@ -0,0 +1 @@ +pub const c = @import("c.zig"); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 53cd31c1d..c75e5a3ee 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -291,7 +291,7 @@ pub const App = struct { tabbing_id: *macos.foundation.String, pub fn init() !Darwin { - const NSWindow = objc.Class.getClass("NSWindow").?; + const NSWindow = objc.getClass("NSWindow").?; NSWindow.msgSend(void, objc.sel("setAllowsAutomaticWindowTabbing:"), .{true}); // Our tabbing ID allows all of our windows to group together diff --git a/src/apprt/gtk/ImguiWidget.zig b/src/apprt/gtk/ImguiWidget.zig index eb9b97f06..d0ce195f8 100644 --- a/src/apprt/gtk/ImguiWidget.zig +++ b/src/apprt/gtk/ImguiWidget.zig @@ -6,7 +6,7 @@ const assert = std.debug.assert; const cimgui = @import("cimgui"); const c = @import("c.zig"); const key = @import("key.zig"); -const gl = @import("../../renderer/opengl/main.zig"); +const gl = @import("opengl"); const input = @import("../../input.zig"); const log = std.log.scoped(.gtk_imgui_widget); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 06293e8e5..58e5a452d 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -252,7 +252,7 @@ pub fn deinit(self: *Surface) void { } fn render(self: *Surface) !void { - try self.core_surface.renderer.draw(); + try self.core_surface.renderer.drawFrame(self); } /// Queue the inspector to render if we have one. diff --git a/src/config/Config.zig b/src/config/Config.zig index af0d1d4af..341f78909 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -528,7 +528,7 @@ keybind: Keybinds = .{}, /// /// Cycles are not allowed. If a cycle is detected, an error will be logged /// and the configuration file will be ignored. -@"config-file": RepeatableString = .{}, +@"config-file": RepeatablePath = .{}, /// Confirms that a surface should be closed before closing it. This defaults /// to true. If set to false, surfaces will close without any confirmation. @@ -599,6 +599,46 @@ 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. +/// +/// On Linux, this requires OpenGL 4.2. Ghostty typically only requires +/// OpenGL 3.3, but custom shaders push that requirement up to 4.2. +/// +/// 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. 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. +/// +/// Changing this value at runtime and reloading the configuration will only +/// affect new windows, tabs, and splits. +@"custom-shader": RepeatablePath = .{}, + +/// If true (default), the focused terminal surface will run an animation +/// loop when custom shaders are used. This uses slightly more CPU (generally +/// less than 10%) but allows the shader to animate. This only runs if there +/// are custom shaders. +/// +/// If this is set to false, the terminal and custom shader will only render +/// when the terminal is updated. This is more efficient but the shader will +/// not animate. +/// +/// This value can be changed at runtime and will affect all currently +/// open terminals. +@"custom-shader-animation": bool = true, + /// 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 @@ -1143,7 +1183,7 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void { var buf_reader = std.io.bufferedReader(file.reader()); var iter = cli.args.lineIterator(buf_reader.reader()); try cli.args.parse(Config, alloc, self, &iter); - try self.expandConfigFiles(std.fs.path.dirname(config_path).?); + try self.expandPaths(std.fs.path.dirname(config_path).?); } else |err| switch (err) { error.FileNotFound => std.log.info( "homedir config not found, not loading path={s}", @@ -1172,15 +1212,15 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { try cli.args.parse(Config, alloc_gpa, self, &iter); // Config files loaded from the CLI args are relative to pwd - if (self.@"config-file".list.items.len > 0) { + if (self.@"config-file".value.list.items.len > 0) { var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; - try self.expandConfigFiles(try std.fs.cwd().realpath(".", &buf)); + try self.expandPaths(try std.fs.cwd().realpath(".", &buf)); } } /// Load and parse the config files that were added in the "config-file" key. pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { - if (self.@"config-file".list.items.len == 0) return; + if (self.@"config-file".value.list.items.len == 0) return; const arena_alloc = self._arena.?.allocator(); // Keeps track of loaded files to prevent cycles. @@ -1189,8 +1229,8 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { const cwd = std.fs.cwd(); var i: usize = 0; - while (i < self.@"config-file".list.items.len) : (i += 1) { - const path = self.@"config-file".list.items[i]; + while (i < self.@"config-file".value.list.items.len) : (i += 1) { + const path = self.@"config-file".value.list.items[i]; // Error paths if (path.len == 0) continue; @@ -1227,37 +1267,22 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { var buf_reader = std.io.bufferedReader(file.reader()); var iter = cli.args.lineIterator(buf_reader.reader()); try cli.args.parse(Config, alloc_gpa, self, &iter); - try self.expandConfigFiles(std.fs.path.dirname(path).?); + try self.expandPaths(std.fs.path.dirname(path).?); } } /// Expand the relative paths in config-files to be absolute paths /// relative to the base directory. -fn expandConfigFiles(self: *Config, base: []const u8) !void { - assert(std.fs.path.isAbsolute(base)); - var dir = try std.fs.cwd().openDir(base, .{}); - defer dir.close(); - +fn expandPaths(self: *Config, base: []const u8) !void { const arena_alloc = self._arena.?.allocator(); - for (self.@"config-file".list.items, 0..) |path, i| { - // If it is already absolute we can ignore it. - if (path.len == 0 or std.fs.path.isAbsolute(path)) continue; - - // If it isn't absolute, we need to make it absolute relative to the base. - const abs = dir.realpathAlloc(arena_alloc, path) catch |err| { - try self._errors.add(arena_alloc, .{ - .message = try std.fmt.allocPrintZ( - arena_alloc, - "error resolving config-file {s}: {}", - .{ path, err }, - ), - }); - self.@"config-file".list.items[i] = ""; - continue; - }; - - log.debug("expanding config-file path relative={s} abs={s}", .{ path, abs }); - self.@"config-file".list.items[i] = abs; + inline for (@typeInfo(Config).Struct.fields) |field| { + if (field.type == RepeatablePath) { + try @field(self, field.name).expand( + arena_alloc, + base, + &self._errors, + ); + } } } @@ -1889,6 +1914,69 @@ pub const RepeatableString = struct { } }; +/// RepeatablePath is like repeatable string but represents a path value. +/// The difference is that when loading the configuration any values for +/// this will be automatically expanded relative to the path of the config +/// file. +pub const RepeatablePath = struct { + const Self = @This(); + + value: RepeatableString = .{}, + + pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { + return self.value.parseCLI(alloc, input); + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const Self, alloc: Allocator) !Self { + return .{ + .value = try self.value.clone(alloc), + }; + } + + /// Compare if two of our value are requal. Required by Config. + pub fn equal(self: Self, other: Self) bool { + return self.value.equal(other.value); + } + + /// Expand all the paths relative to the base directory. + pub fn expand( + self: *Self, + alloc: Allocator, + base: []const u8, + errors: *ErrorList, + ) !void { + assert(std.fs.path.isAbsolute(base)); + var dir = try std.fs.cwd().openDir(base, .{}); + defer dir.close(); + + for (self.value.list.items, 0..) |path, i| { + // If it is already absolute we can ignore it. + if (path.len == 0 or std.fs.path.isAbsolute(path)) continue; + + // If it isn't absolute, we need to make it absolute relative + // to the base. + const abs = dir.realpathAlloc(alloc, path) catch |err| { + try errors.add(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "error resolving config-file {s}: {}", + .{ path, err }, + ), + }); + self.value.list.items[i] = ""; + continue; + }; + + log.debug( + "expanding config-file path relative={s} abs={s}", + .{ path, abs }, + ); + self.value.list.items[i] = abs; + } + } +}; + /// FontVariation is a repeatable configuration value that sets a single /// font variation value. Font variations are configurations for what /// are often called "variable fonts." The font files usually end in diff --git a/src/main.zig b/src/main.zig index 16ad70ef3..3ef42d136 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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 diff --git a/src/os/locale.zig b/src/os/locale.zig index 3c3f35fcf..361f4fe62 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -70,7 +70,7 @@ fn setLangFromCocoa() void { defer pool.deinit(); // The classes we're going to need. - const NSLocale = objc.Class.getClass("NSLocale") orelse { + const NSLocale = objc.getClass("NSLocale") orelse { log.err("NSLocale class not found. Locale may be incorrect.", .{}); return; }; diff --git a/src/os/macos_version.zig b/src/os/macos_version.zig index 575dd2b72..e0b21560e 100644 --- a/src/os/macos_version.zig +++ b/src/os/macos_version.zig @@ -7,7 +7,7 @@ const objc = @import("objc"); pub fn macosVersionAtLeast(major: i64, minor: i64, patch: i64) bool { assert(builtin.target.isDarwin()); - const NSProcessInfo = objc.Class.getClass("NSProcessInfo").?; + const NSProcessInfo = objc.getClass("NSProcessInfo").?; const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{}); return info.msgSend(bool, objc.sel("isOperatingSystemAtLeastVersion:"), .{ NSOperatingSystemVersion{ .major = major, .minor = minor, .patch = patch }, diff --git a/src/os/mouse.zig b/src/os/mouse.zig index e8b6f0f10..1774399c9 100644 --- a/src/os/mouse.zig +++ b/src/os/mouse.zig @@ -9,7 +9,7 @@ const log = std.log.scoped(.os); pub fn clickInterval() ?u32 { // On macOS, we can ask the system. if (comptime builtin.target.isDarwin()) { - const NSEvent = objc.Class.getClass("NSEvent") orelse { + const NSEvent = objc.getClass("NSEvent") orelse { log.err("NSEvent class not found. Can't get click interval.", .{}); return null; }; 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/Metal.zig b/src/renderer/Metal.zig index d3eb22a1f..1012d6bea 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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,13 +18,16 @@ 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"); const mtl_buffer = @import("metal/buffer.zig"); const mtl_image = @import("metal/image.zig"); +const mtl_sampler = @import("metal/sampler.zig"); const mtl_shaders = @import("metal/shaders.zig"); const Image = mtl_image.Image; const ImageMap = mtl_image.ImageMap; @@ -76,6 +80,10 @@ background_color: terminal.color.RGB, /// by a terminal application cursor_color: ?terminal.color.RGB, +/// The current frame background color. This is only updated during +/// the updateFrame method. +current_background_color: terminal.color.RGB, + /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. @@ -108,12 +116,31 @@ swapchain: objc.Object, // CAMetalLayer texture_greyscale: objc.Object, // MTLTexture texture_color: objc.Object, // MTLTexture +/// Custom shader state. This is only set if we have custom shaders. +custom_shader_state: ?CustomShaderState = null, + +pub const CustomShaderState = struct { + /// The screen texture that we render the terminal to. If we don't have + /// custom shaders, we render directly to the drawable. + screen_texture: objc.Object, // MTLTexture + sampler: mtl_sampler.Sampler, + uniforms: mtl_shaders.PostUniforms, + last_frame_time: std.time.Instant, + + pub fn deinit(self: *CustomShaderState) void { + deinitMTLResource(self.screen_texture); + self.sampler.deinit(); + } +}; + /// The configuration for this renderer that is derived from the main /// 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, @@ -124,17 +151,22 @@ 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), + custom_shader_animation: bool, 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); @@ -173,11 +205,16 @@ pub const DerivedConfig = struct { bg.toTerminalRGB() else null, + + .custom_shaders = custom_shaders, + .custom_shader_animation = config.@"custom-shader-animation", + + .arena = arena, }; } pub fn deinit(self: *DerivedConfig) void { - self.font_features.deinit(); + self.arena.deinit(); } }; @@ -199,11 +236,15 @@ 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"), .{}); const swapchain = swapchain: { - const CAMetalLayer = objc.Class.getClass("CAMetalLayer").?; + const CAMetalLayer = objc.getClass("CAMetalLayer").?; const swapchain = CAMetalLayer.msgSend(objc.Object, objc.sel("layer"), .{}); swapchain.setProperty("device", device.value); swapchain.setProperty("opaque", options.config.background_opacity >= 1); @@ -252,9 +293,50 @@ 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 &.{}; + }; + + // If we have custom shaders then setup our state + var custom_shader_state: ?CustomShaderState = state: { + if (custom_shaders.len == 0) break :state null; + + // Build our sampler for our texture + var sampler = try mtl_sampler.Sampler.init(device); + errdefer sampler.deinit(); + + break :state .{ + // Resolution and screen texture will be fixed up by first + // call to setScreenSize. This happens before any draw call. + .screen_texture = undefined, + .sampler = sampler, + .uniforms = .{ + .resolution = .{ 0, 0, 1 }, + .time = 1, + .time_delta = 1, + .frame_rate = 1, + .frame = 1, + .channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + .channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + .mouse = .{ 0, 0, 0, 0 }, + .date = .{ 0, 0, 0, 0 }, + .sample_rate = 1, + }, + + .last_frame_time = try std.time.Instant.now(), + }; + }; + errdefer if (custom_shader_state) |*state| state.deinit(); + // Initialize our shaders - var shaders = try Shaders.init(device); - errdefer shaders.deinit(); + var shaders = try Shaders.init(alloc, device, custom_shaders); + errdefer shaders.deinit(alloc); // Font atlas textures const texture_greyscale = try initAtlasTexture(device, &options.font_group.atlas_greyscale); @@ -271,6 +353,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .foreground_color = options.config.foreground, .background_color = options.config.background, .cursor_color = options.config.cursor_color, + .current_background_color = options.config.background, // Render state .cells_bg = .{}, @@ -298,6 +381,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .swapchain = swapchain, .texture_greyscale = texture_greyscale, .texture_color = texture_color, + .custom_shader_state = custom_shader_state, }; } @@ -323,7 +407,9 @@ pub fn deinit(self: *Metal) void { deinitMTLResource(self.texture_color); self.queue.msgSend(void, objc.sel("release"), .{}); - self.shaders.deinit(); + if (self.custom_shader_state) |*state| state.deinit(); + + self.shaders.deinit(self.alloc); self.* = undefined; } @@ -384,6 +470,13 @@ pub fn threadExit(self: *const Metal) void { // Metal requires no per-thread state. } +/// True if our renderer has animations so that a higher frequency +/// timer is used. +pub fn hasAnimations(self: *const Metal) bool { + return self.custom_shader_state != null and + self.config.custom_shader_animation; +} + /// Returns the grid size for a given screen size. This is safe to call /// on any thread. fn gridSize(self: *Metal) ?renderer.GridSize { @@ -448,8 +541,8 @@ pub fn setFontSize(self: *Metal, size: font.face.DesiredSize) !void { }, .{ .forever = {} }); } -/// The primary render callback that is completely thread-safe. -pub fn render( +/// Update the frame data. +pub fn updateFrame( self: *Metal, surface: *apprt.Surface, state: *renderer.State, @@ -535,10 +628,6 @@ pub fn render( }; defer critical.screen.deinit(); - // @autoreleasepool {} - const pool = objc.AutoreleasePool.init(); - defer pool.deinit(); - // Build our GPU cells try self.rebuildCells( critical.selection, @@ -547,18 +636,8 @@ pub fn render( critical.cursor_style, ); - // Get our drawable (CAMetalDrawable) - const drawable = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); - - // If our font atlas changed, sync the texture data - if (self.font_group.atlas_greyscale.modified) { - try syncAtlasTexture(self.device, &self.font_group.atlas_greyscale, &self.texture_greyscale); - self.font_group.atlas_greyscale.modified = false; - } - if (self.font_group.atlas_color.modified) { - try syncAtlasTexture(self.device, &self.font_group.atlas_color, &self.texture_color); - self.font_group.atlas_color.modified = false; - } + // Update our background color + self.current_background_color = critical.bg; // Go through our images and see if we need to setup any textures. { @@ -580,6 +659,45 @@ pub fn render( } } } +} + +/// Draw the frame to the screen. +pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { + _ = surface; + + // If we have custom shaders, update the animation time. + if (self.custom_shader_state) |*state| { + const now = std.time.Instant.now() catch state.last_frame_time; + const since_ns: f32 = @floatFromInt(now.since(state.last_frame_time)); + state.uniforms.time = since_ns / std.time.ns_per_s; + state.uniforms.time_delta = since_ns / std.time.ns_per_s; + } + + // @autoreleasepool {} + const pool = objc.AutoreleasePool.init(); + defer pool.deinit(); + + // Get our drawable (CAMetalDrawable) + const drawable = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); + + // Get our screen texture. If we don't have a dedicated screen texture + // then we just use the drawable texture. + const screen_texture = if (self.custom_shader_state) |state| + state.screen_texture + else tex: { + const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); + break :tex objc.Object.fromId(texture); + }; + + // If our font atlas changed, sync the texture data + if (self.font_group.atlas_greyscale.modified) { + try syncAtlasTexture(self.device, &self.font_group.atlas_greyscale, &self.texture_greyscale); + self.font_group.atlas_greyscale.modified = false; + } + if (self.font_group.atlas_color.modified) { + try syncAtlasTexture(self.device, &self.font_group.atlas_color, &self.texture_color); + self.font_group.atlas_color.modified = false; + } // Command buffer (MTLCommandBuffer) const buffer = self.queue.msgSend(objc.Object, objc.sel("commandBuffer"), .{}); @@ -587,7 +705,7 @@ pub fn render( { // MTLRenderPassDescriptor const desc = desc: { - const MTLRenderPassDescriptor = objc.Class.getClass("MTLRenderPassDescriptor").?; + const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?; const desc = MTLRenderPassDescriptor.msgSend( objc.Object, objc.sel("renderPassDescriptor"), @@ -607,14 +725,14 @@ pub fn render( // Ghostty in XCode in debug mode it returns a CaptureMTLDrawable // which ironically doesn't implement CAMetalDrawable as a // property so we just send a message. - const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); + //const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear)); attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store)); - attachment.setProperty("texture", texture); + attachment.setProperty("texture", screen_texture.value); attachment.setProperty("clearColor", mtl.MTLClearColor{ - .red = @as(f32, @floatFromInt(critical.bg.r)) / 255, - .green = @as(f32, @floatFromInt(critical.bg.g)) / 255, - .blue = @as(f32, @floatFromInt(critical.bg.b)) / 255, + .red = @as(f32, @floatFromInt(self.current_background_color.r)) / 255, + .green = @as(f32, @floatFromInt(self.current_background_color.g)) / 255, + .blue = @as(f32, @floatFromInt(self.current_background_color.b)) / 255, .alpha = self.config.background_opacity, }); } @@ -646,10 +764,118 @@ pub fn render( try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); } + // If we have custom shaders AND we have a screen texture, then we + // render the custom shaders. + if (self.custom_shader_state) |state| { + // MTLRenderPassDescriptor + const desc = desc: { + const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?; + const desc = MTLRenderPassDescriptor.msgSend( + objc.Object, + objc.sel("renderPassDescriptor"), + .{}, + ); + + // Set our color attachment to be our drawable surface. + const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); + { + const attachment = attachments.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + // Texture is a property of CAMetalDrawable but if you run + // Ghostty in XCode in debug mode it returns a CaptureMTLDrawable + // which ironically doesn't implement CAMetalDrawable as a + // property so we just send a message. + const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); + attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear)); + attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store)); + attachment.setProperty("texture", texture); + attachment.setProperty("clearColor", mtl.MTLClearColor{ + .red = 0, + .green = 0, + .blue = 0, + .alpha = 1, + }); + } + + break :desc desc; + }; + + // MTLRenderCommandEncoder + const encoder = buffer.msgSend( + objc.Object, + objc.sel("renderCommandEncoderWithDescriptor:"), + .{desc.value}, + ); + defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); + + for (self.shaders.post_pipelines) |pipeline| { + try self.drawPostShader(encoder, pipeline, &state); + } + } + buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value}); buffer.msgSend(void, objc.sel("commit"), .{}); } +fn drawPostShader( + self: *Metal, + encoder: objc.Object, + pipeline: objc.Object, + state: *const CustomShaderState, +) !void { + _ = self; + + // Use our custom shader pipeline + encoder.msgSend( + void, + objc.sel("setRenderPipelineState:"), + .{pipeline.value}, + ); + + // Set our sampler + encoder.msgSend( + void, + objc.sel("setFragmentSamplerState:atIndex:"), + .{ state.sampler.sampler.value, @as(c_ulong, 0) }, + ); + + // Set our uniforms + encoder.msgSend( + void, + objc.sel("setFragmentBytes:length:atIndex:"), + .{ + @as(*const anyopaque, @ptrCast(&state.uniforms)), + @as(c_ulong, @sizeOf(@TypeOf(state.uniforms))), + @as(c_ulong, 0), + }, + ); + + // Screen texture + encoder.msgSend( + void, + objc.sel("setFragmentTexture:atIndex:"), + .{ + state.screen_texture.value, + @as(c_ulong, 0), + }, + ); + + // Draw! + encoder.msgSend( + void, + objc.sel("drawPrimitives:vertexStart:vertexCount:"), + .{ + @intFromEnum(mtl.MTLPrimitiveType.triangle_strip), + @as(c_ulong, 0), + @as(c_ulong, 4), + }, + ); +} + fn drawImagePlacements( self: *Metal, encoder: objc.Object, @@ -1070,6 +1296,51 @@ pub fn setScreenSize( self.cells.clearAndFree(self.alloc); self.cells_bg.clearAndFree(self.alloc); + // If we have custom shaders then we update the state + if (self.custom_shader_state) |*state| { + // Only free our previous texture if this isn't our first + // time setting the custom shader state. + if (state.uniforms.resolution[0] > 0) { + deinitMTLResource(state.screen_texture); + } + + state.uniforms.resolution = .{ + @floatFromInt(dim.width), + @floatFromInt(dim.height), + 1, + }; + + state.screen_texture = screen_texture: { + // This texture is the size of our drawable but supports being a + // render target AND reading so that the custom shaders can read from it. + const desc = init: { + const Class = objc.getClass("MTLTextureDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.bgra8unorm)); + desc.setProperty("width", @as(c_ulong, @intCast(dim.width))); + desc.setProperty("height", @as(c_ulong, @intCast(dim.height))); + desc.setProperty( + "usage", + @intFromEnum(mtl.MTLTextureUsage.render_target) | + @intFromEnum(mtl.MTLTextureUsage.shader_read) | + @intFromEnum(mtl.MTLTextureUsage.shader_write), + ); + + // If we fail to create the texture, then we just don't have a screen + // texture and our custom shaders won't run. + const id = self.device.msgSend( + ?*anyopaque, + objc.sel("newTextureWithDescriptor:"), + .{desc}, + ) orelse return error.MetalFailed; + + break :screen_texture objc.Object.fromId(id); + }; + } + log.debug("screen size screen={} grid={}, cell={}", .{ dim, grid_size, self.cell_size }); } @@ -1625,7 +1896,7 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object // Create our descriptor const desc = init: { - const Class = objc.Class.getClass("MTLTextureDescriptor").?; + const Class = objc.getClass("MTLTextureDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 07e1d030c..3d7031c8d 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -7,6 +7,8 @@ const glfw = @import("glfw"); const assert = std.debug.assert; const testing = std.testing; const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const shadertoy = @import("shadertoy.zig"); const apprt = @import("../apprt.zig"); const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); @@ -14,11 +16,14 @@ const imgui = @import("imgui"); const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); const Terminal = terminal.Terminal; -const gl = @import("opengl/main.zig"); +const gl = @import("opengl"); const trace = @import("tracy").trace; const math = @import("../math.zig"); const Surface = @import("../Surface.zig"); +const CellProgram = @import("opengl/CellProgram.zig"); +const custom = @import("opengl/custom.zig"); + const log = std.log.scoped(.grid); /// The runtime can request a single-threaded draw by setting this boolean @@ -45,8 +50,8 @@ screen_size: ?renderer.ScreenSize, /// The current set of cells to render. Each set of cells goes into /// a separate shader call. -cells_bg: std.ArrayListUnmanaged(GPUCell), -cells: std.ArrayListUnmanaged(GPUCell), +cells_bg: std.ArrayListUnmanaged(CellProgram.Cell), +cells: std.ArrayListUnmanaged(CellProgram.Cell), /// The size of the cells list that was sent to the GPU. This is used /// to detect when the cells array was reallocated/resized and handle that @@ -102,8 +107,11 @@ draw_background: terminal.color.RGB, const SetScreenSize = struct { size: renderer.ScreenSize, - fn apply(self: SetScreenSize, r: *const OpenGL) !void { - const gl_state = r.gl_state orelse return error.OpenGLUninitialized; + fn apply(self: SetScreenSize, r: *OpenGL) !void { + const gl_state: *GLState = if (r.gl_state) |*v| + v + else + return error.OpenGLUninitialized; // Apply our padding const padding = if (r.padding.balance) @@ -130,7 +138,7 @@ const SetScreenSize = struct { ); // Update the projection uniform within our shader - try gl_state.program.setUniform( + try gl_state.cell_program.program.setUniform( "projection", // 2D orthographic projection with the full w/h @@ -141,6 +149,11 @@ const SetScreenSize = struct { -1 * @as(f32, @floatFromInt(padding.top)), ), ); + + // Update our custom shader resolution + if (gl_state.custom) |*custom_state| { + try custom_state.setScreenSize(self.size); + } } }; @@ -150,84 +163,32 @@ const SetFontSize = struct { fn apply(self: SetFontSize, r: *const OpenGL) !void { const gl_state = r.gl_state orelse return error.OpenGLUninitialized; - try gl_state.program.setUniform( + try gl_state.cell_program.program.setUniform( "cell_size", @Vector(2, f32){ @floatFromInt(self.metrics.cell_width), @floatFromInt(self.metrics.cell_height), }, ); - try gl_state.program.setUniform( + try gl_state.cell_program.program.setUniform( "strikethrough_position", @as(f32, @floatFromInt(self.metrics.strikethrough_position)), ); - try gl_state.program.setUniform( + try gl_state.cell_program.program.setUniform( "strikethrough_thickness", @as(f32, @floatFromInt(self.metrics.strikethrough_thickness)), ); } }; -/// The raw structure that maps directly to the buffer sent to the vertex shader. -/// This must be "extern" so that the field order is not reordered by the -/// Zig compiler. -const GPUCell = extern struct { - /// vec2 grid_coord - grid_col: u16, - grid_row: u16, - - /// vec2 glyph_pos - glyph_x: u32 = 0, - glyph_y: u32 = 0, - - /// vec2 glyph_size - glyph_width: u32 = 0, - glyph_height: u32 = 0, - - /// vec2 glyph_size - glyph_offset_x: i32 = 0, - glyph_offset_y: i32 = 0, - - /// vec4 fg_color_in - fg_r: u8, - fg_g: u8, - fg_b: u8, - fg_a: u8, - - /// vec4 bg_color_in - bg_r: u8, - bg_g: u8, - bg_b: u8, - bg_a: u8, - - /// uint mode - mode: GPUCellMode, - - /// The width in grid cells that a rendering takes. - grid_width: u8, -}; - -const GPUCellMode = enum(u8) { - bg = 1, - fg = 2, - fg_color = 7, - strikethrough = 8, - - // Non-exhaustive because masks change it - _, - - /// Apply a mask to the mode. - pub fn mask(self: GPUCellMode, m: GPUCellMode) GPUCellMode { - return @enumFromInt(@intFromEnum(self) | @intFromEnum(m)); - } -}; - /// The configuration for this renderer that is derived from the main /// 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_text: ?terminal.color.RGB, @@ -238,17 +199,22 @@ 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), + custom_shader_animation: bool, 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); @@ -287,11 +253,16 @@ pub const DerivedConfig = struct { bg.toTerminalRGB() else null, + + .custom_shaders = custom_shaders, + .custom_shader_animation = config.@"custom-shader-animation", + + .arena = arena, }; } pub fn deinit(self: *DerivedConfig) void { - self.font_features.deinit(); + self.arena.deinit(); } }; @@ -309,7 +280,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { options.config.font_thicken, ); - var gl_state = try GLState.init(options.font_group); + var gl_state = try GLState.init(alloc, options.config, options.font_group); errdefer gl_state.deinit(); return OpenGL{ @@ -336,7 +307,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { pub fn deinit(self: *OpenGL) void { self.font_shaper.deinit(); - if (self.gl_state) |*v| v.deinit(); + if (self.gl_state) |*v| v.deinit(self.alloc); self.cells.deinit(self.alloc); self.cells_bg.deinit(self.alloc); @@ -410,7 +381,7 @@ pub fn displayUnrealized(self: *OpenGL) void { defer if (single_threaded_draw) self.draw_mutex.unlock(); if (self.gl_state) |*v| { - v.deinit(); + v.deinit(self.alloc); self.gl_state = null; } } @@ -428,11 +399,11 @@ pub fn displayRealize(self: *OpenGL) !void { ); // Make our new state - var gl_state = try GLState.init(self.font_group); + var gl_state = try GLState.init(self.alloc, self.config, self.font_group); errdefer gl_state.deinit(); // Unrealize if we have to - if (self.gl_state) |*v| v.deinit(); + if (self.gl_state) |*v| v.deinit(self.alloc); // Set our new state self.gl_state = gl_state; @@ -506,6 +477,13 @@ pub fn threadExit(self: *const OpenGL) void { } } +/// True if our renderer has animations so that a higher frequency +/// timer is used. +pub fn hasAnimations(self: *const OpenGL) bool { + const state = self.gl_state orelse return false; + return state.custom != null and self.config.custom_shader_animation; +} + /// Callback when the focus changes for the terminal this is rendering. /// /// Must be called on the render thread. @@ -576,12 +554,14 @@ fn resetFontMetrics( } /// The primary render callback that is completely thread-safe. -pub fn render( +pub fn updateFrame( self: *OpenGL, surface: *apprt.Surface, state: *renderer.State, cursor_blink_visible: bool, ) !void { + _ = surface; + // Data we extract out of the critical area. const Critical = struct { gl_bg: terminal.color.RGB, @@ -669,19 +649,6 @@ pub fn render( critical.cursor_style, ); } - - // We're out of the critical path now. Let's render. We only render if - // we're not single threaded. If we're single threaded we expect the - // runtime to call draw. - if (single_threaded_draw) return; - - try self.draw(); - - // Swap our window buffers - switch (apprt.runtime) { - else => @compileError("unsupported runtime"), - apprt.glfw => surface.window.swapBuffers(), - } } /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a @@ -735,7 +702,7 @@ pub fn rebuildCells( // This is the cell that has [mode == .fg] and is underneath our cursor. // We keep track of it so that we can invert the colors so the character // remains visible. - var cursor_cell: ?GPUCell = null; + var cursor_cell: ?CellProgram.Cell = null; // Build each cell var rowIter = screen.rowIterator(.viewport); @@ -868,15 +835,15 @@ pub fn rebuildCells( if (cursor_cell) |*cell| { if (cell.mode == .fg) { if (self.config.cursor_text) |txt| { - cell.fg_r = txt.r; - cell.fg_g = txt.g; - cell.fg_b = txt.b; - cell.fg_a = 255; + cell.r = txt.r; + cell.g = txt.g; + cell.b = txt.b; + cell.a = 255; } else { - cell.fg_r = 0; - cell.fg_g = 0; - cell.fg_b = 0; - cell.fg_a = 255; + cell.r = 0; + cell.g = 0; + cell.b = 0; + cell.a = 255; } } self.cells.appendAssumeCapacity(cell.*); @@ -940,14 +907,10 @@ fn addPreeditCell( .glyph_height = 0, .glyph_offset_x = 0, .glyph_offset_y = 0, - .fg_r = 0, - .fg_g = 0, - .fg_b = 0, - .fg_a = 0, - .bg_r = bg.r, - .bg_g = bg.g, - .bg_b = bg.b, - .bg_a = 255, + .r = bg.r, + .g = bg.g, + .b = bg.b, + .a = 255, }); // Add our text @@ -962,14 +925,10 @@ fn addPreeditCell( .glyph_height = glyph.height, .glyph_offset_x = glyph.offset_x, .glyph_offset_y = glyph.offset_y, - .fg_r = fg.r, - .fg_g = fg.g, - .fg_b = fg.b, - .fg_a = 255, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, + .r = fg.r, + .g = fg.g, + .b = fg.b, + .a = 255, }); } @@ -977,7 +936,7 @@ fn addCursor( self: *OpenGL, screen: *terminal.Screen, cursor_style: renderer.CursorStyle, -) ?*const GPUCell { +) ?*const CellProgram.Cell { // Add the cursor. We render the cursor over the wide character if // we're on the wide characer tail. const wide, const x = cell: { @@ -1027,14 +986,10 @@ fn addCursor( .grid_col = @intCast(x), .grid_row = @intCast(screen.cursor.y), .grid_width = if (wide) 2 else 1, - .fg_r = color.r, - .fg_g = color.g, - .fg_b = color.b, - .fg_a = alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, + .r = color.r, + .g = color.g, + .b = color.b, + .a = alpha, .glyph_x = glyph.atlas_x, .glyph_y = glyph.atlas_y, .glyph_width = glyph.width, @@ -1182,14 +1137,10 @@ pub fn updateCell( .glyph_height = 0, .glyph_offset_x = 0, .glyph_offset_y = 0, - .fg_r = 0, - .fg_g = 0, - .fg_b = 0, - .fg_a = 0, - .bg_r = rgb.r, - .bg_g = rgb.g, - .bg_b = rgb.b, - .bg_a = bg_alpha, + .r = rgb.r, + .g = rgb.g, + .b = rgb.b, + .a = bg_alpha, }); } @@ -1208,7 +1159,7 @@ pub fn updateCell( // If we're rendering a color font, we use the color atlas const presentation = try self.font_group.group.presentationFromIndex(shaper_run.font_index); - const mode: GPUCellMode = switch (presentation) { + const mode: CellProgram.CellMode = switch (presentation) { .text => .fg, .emoji => .fg_color, }; @@ -1224,14 +1175,10 @@ pub fn updateCell( .glyph_height = glyph.height, .glyph_offset_x = glyph.offset_x, .glyph_offset_y = glyph.offset_y, - .fg_r = colors.fg.r, - .fg_g = colors.fg.g, - .fg_b = colors.fg.b, - .fg_a = alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, + .r = colors.fg.r, + .g = colors.fg.g, + .b = colors.fg.b, + .a = alpha, }); } @@ -1265,14 +1212,10 @@ pub fn updateCell( .glyph_height = underline_glyph.height, .glyph_offset_x = underline_glyph.offset_x, .glyph_offset_y = underline_glyph.offset_y, - .fg_r = color.r, - .fg_g = color.g, - .fg_b = color.b, - .fg_a = alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, + .r = color.r, + .g = color.g, + .b = color.b, + .a = alpha, }); } @@ -1288,14 +1231,10 @@ pub fn updateCell( .glyph_height = 0, .glyph_offset_x = 0, .glyph_offset_y = 0, - .fg_r = colors.fg.r, - .fg_g = colors.fg.g, - .fg_b = colors.fg.b, - .fg_a = alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, + .r = colors.fg.r, + .g = colors.fg.g, + .b = colors.fg.b, + .a = alpha, }); } @@ -1388,11 +1327,11 @@ fn flushAtlas(self: *OpenGL) !void { atlas.resized = false; try texbind.image2D( 0, - .Red, + .red, @intCast(atlas.size), @intCast(atlas.size), 0, - .Red, + .red, .UnsignedByte, atlas.data.ptr, ); @@ -1403,7 +1342,7 @@ fn flushAtlas(self: *OpenGL) !void { 0, @intCast(atlas.size), @intCast(atlas.size), - .Red, + .red, .UnsignedByte, atlas.data.ptr, ); @@ -1422,11 +1361,11 @@ fn flushAtlas(self: *OpenGL) !void { atlas.resized = false; try texbind.image2D( 0, - .RGBA, + .rgba, @intCast(atlas.size), @intCast(atlas.size), 0, - .BGRA, + .bgra, .UnsignedByte, atlas.data.ptr, ); @@ -1437,7 +1376,7 @@ fn flushAtlas(self: *OpenGL) !void { 0, @intCast(atlas.size), @intCast(atlas.size), - .BGRA, + .bgra, .UnsignedByte, atlas.data.ptr, ); @@ -1448,19 +1387,71 @@ fn flushAtlas(self: *OpenGL) !void { /// Render renders the current cell state. This will not modify any of /// the cells. -pub fn draw(self: *OpenGL) !void { +pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { const t = trace(@src()); defer t.end(); // If we're in single-threaded more we grab a lock since we use shared data. if (single_threaded_draw) self.draw_mutex.lock(); defer if (single_threaded_draw) self.draw_mutex.unlock(); - const gl_state = self.gl_state orelse return; + const gl_state: *GLState = if (self.gl_state) |*v| v else return; + // Draw our terminal cells + try self.drawCellProgram(gl_state); + + // Draw our custom shaders + if (gl_state.custom) |*custom_state| { + try self.drawCustomPrograms(custom_state); + } + + // Swap our window buffers + switch (apprt.runtime) { + apprt.glfw => surface.window.swapBuffers(), + apprt.gtk => {}, + else => @compileError("unsupported runtime"), + } +} + +/// Draw the custom shaders. +fn drawCustomPrograms( + self: *OpenGL, + custom_state: *custom.State, +) !void { + _ = self; + + // Bind our state that is global to all custom shaders + const custom_bind = try custom_state.bind(); + defer custom_bind.unbind(); + + // Setup the new frame + try custom_state.newFrame(); + + // Go through each custom shader and draw it. + for (custom_state.programs) |program| { + // Bind our cell program state, buffers + const bind = try program.bind(); + defer bind.unbind(); + try bind.draw(); + } +} + +/// Runs the cell program (shaders) to draw the terminal grid. +fn drawCellProgram( + self: *OpenGL, + gl_state: *const GLState, +) !void { // Try to flush our atlas, this will only do something if there // are changes to the atlas. try self.flushAtlas(); + // If we have custom shaders, then we draw to the custom + // shader framebuffer. + const fbobind: ?gl.Framebuffer.Binding = fbobind: { + const state = gl_state.custom orelse break :fbobind null; + break :fbobind try state.fbo.bind(.framebuffer); + }; + defer if (fbobind) |v| v.unbind(); + // Clear the surface gl.clearColor( @as(f32, @floatFromInt(self.draw_background.r)) / 255, @@ -1470,17 +1461,9 @@ pub fn draw(self: *OpenGL) !void { ); gl.clear(gl.c.GL_COLOR_BUFFER_BIT); - // Setup our VAO - try gl_state.vao.bind(); - defer gl.VertexArray.unbind() catch null; - - // Bind EBO - var ebobind = try gl_state.ebo.bind(.ElementArrayBuffer); - defer ebobind.unbind(); - - // Bind VBO and set data - var binding = try gl_state.vbo.bind(.ArrayBuffer); - defer binding.unbind(); + // Bind our cell program state, buffers + const bind = try gl_state.cell_program.bind(); + defer bind.unbind(); // Bind our textures try gl.Texture.active(gl.c.GL_TEXTURE0); @@ -1491,10 +1474,6 @@ pub fn draw(self: *OpenGL) !void { var texbind1 = try gl_state.texture_color.bind(.@"2D"); defer texbind1.unbind(); - // Pick our shader to use - const pbind = try gl_state.program.use(); - defer pbind.unbind(); - // If we have deferred operations, run them. if (self.deferred_screen_size) |v| { try v.apply(self); @@ -1505,8 +1484,9 @@ pub fn draw(self: *OpenGL) !void { self.deferred_font_size = null; } - try self.drawCells(binding, self.cells_bg); - try self.drawCells(binding, self.cells); + // Draw our background, then draw the fg on top of it. + try self.drawCells(bind.vbo, self.cells_bg); + try self.drawCells(bind.vbo, self.cells); } /// Loads some set of cell data into our buffer and issues a draw call. @@ -1517,7 +1497,7 @@ pub fn draw(self: *OpenGL) !void { fn drawCells( self: *OpenGL, binding: gl.Buffer.Binding, - cells: std.ArrayListUnmanaged(GPUCell), + cells: std.ArrayListUnmanaged(CellProgram.Cell), ) !void { // If we have no cells to render, then we render nothing. if (cells.items.len == 0) return; @@ -1534,8 +1514,8 @@ fn drawCells( }); try binding.setDataNullManual( - @sizeOf(GPUCell) * cells.capacity, - .StaticDraw, + @sizeOf(CellProgram.Cell) * cells.capacity, + .static_draw, ); self.gl_cells_size = cells.capacity; @@ -1546,7 +1526,7 @@ fn drawCells( if (self.gl_cells_written < cells.items.len) { const data = cells.items[self.gl_cells_written..]; // log.info("sending {} cells to GPU", .{data.len}); - try binding.setSubData(self.gl_cells_written * @sizeOf(GPUCell), data); + try binding.setSubData(self.gl_cells_written * @sizeOf(CellProgram.Cell), data); self.gl_cells_written += data.len; assert(data.len > 0); @@ -1565,14 +1545,41 @@ fn drawCells( /// easy to create/destroy these as a set in situations i.e. where the /// OpenGL context is replaced. const GLState = struct { - program: gl.Program, - vao: gl.VertexArray, - ebo: gl.Buffer, - vbo: gl.Buffer, + cell_program: CellProgram, texture: gl.Texture, texture_color: gl.Texture, + custom: ?custom.State, + + pub fn init( + alloc: Allocator, + config: DerivedConfig, + font_group: *font.GroupCache, + ) !GLState { + var arena = ArenaAllocator.init(alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + // Load our custom shaders + const custom_state: ?custom.State = custom: { + const shaders: []const [:0]const u8 = shadertoy.loadFromFiles( + arena_alloc, + config.custom_shaders.items, + .glsl, + ) catch |err| err: { + log.warn("error loading custom shaders err={}", .{err}); + break :err &.{}; + }; + if (shaders.len == 0) break :custom null; + + break :custom custom.State.init( + alloc, + shaders, + ) catch |err| err: { + log.warn("error initializing custom shaders err={}", .{err}); + break :err null; + }; + }; - pub fn init(font_group: *font.GroupCache) !GLState { // Blending for text. We use GL_ONE here because we should be using // premultiplied alpha for all our colors in our fragment shaders. // This avoids having a blurry border where transparency is expected on @@ -1580,74 +1587,6 @@ const GLState = struct { try gl.enable(gl.c.GL_BLEND); try gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA); - // Shader - const program = try gl.Program.createVF( - @embedFile("shaders/cell.v.glsl"), - @embedFile("shaders/cell.f.glsl"), - ); - - // Set our cell dimensions - const pbind = try program.use(); - defer pbind.unbind(); - - // Set all of our texture indexes - try program.setUniform("text", 0); - try program.setUniform("text_color", 1); - - // Setup our VAO - const vao = try gl.VertexArray.create(); - errdefer vao.destroy(); - try vao.bind(); - defer gl.VertexArray.unbind() catch null; - - // Element buffer (EBO) - const ebo = try gl.Buffer.create(); - errdefer ebo.destroy(); - var ebobind = try ebo.bind(.ElementArrayBuffer); - defer ebobind.unbind(); - try ebobind.setData([6]u8{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }, .StaticDraw); - - // Vertex buffer (VBO) - const vbo = try gl.Buffer.create(); - errdefer vbo.destroy(); - var vbobind = try vbo.bind(.ArrayBuffer); - defer vbobind.unbind(); - var offset: usize = 0; - try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(GPUCell), offset); - offset += 2 * @sizeOf(u16); - try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(GPUCell), offset); - offset += 2 * @sizeOf(u32); - try vbobind.attributeAdvanced(2, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(GPUCell), offset); - offset += 2 * @sizeOf(u32); - try vbobind.attributeAdvanced(3, 2, gl.c.GL_INT, false, @sizeOf(GPUCell), offset); - offset += 2 * @sizeOf(i32); - try vbobind.attributeAdvanced(4, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(GPUCell), offset); - offset += 4 * @sizeOf(u8); - try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(GPUCell), offset); - offset += 4 * @sizeOf(u8); - try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset); - offset += 1 * @sizeOf(u8); - try vbobind.attributeIAdvanced(7, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset); - try vbobind.enableAttribArray(0); - try vbobind.enableAttribArray(1); - try vbobind.enableAttribArray(2); - try vbobind.enableAttribArray(3); - try vbobind.enableAttribArray(4); - try vbobind.enableAttribArray(5); - try vbobind.enableAttribArray(6); - try vbobind.enableAttribArray(7); - try vbobind.attributeDivisor(0, 1); - try vbobind.attributeDivisor(1, 1); - try vbobind.attributeDivisor(2, 1); - try vbobind.attributeDivisor(3, 1); - try vbobind.attributeDivisor(4, 1); - try vbobind.attributeDivisor(5, 1); - try vbobind.attributeDivisor(6, 1); - try vbobind.attributeDivisor(7, 1); - // Build our texture const tex = try gl.Texture.create(); errdefer tex.destroy(); @@ -1659,11 +1598,11 @@ const GLState = struct { try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); try texbind.image2D( 0, - .Red, + .red, @intCast(font_group.atlas_greyscale.size), @intCast(font_group.atlas_greyscale.size), 0, - .Red, + .red, .UnsignedByte, font_group.atlas_greyscale.data.ptr, ); @@ -1680,32 +1619,32 @@ const GLState = struct { try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); try texbind.image2D( 0, - .RGBA, + .rgba, @intCast(font_group.atlas_color.size), @intCast(font_group.atlas_color.size), 0, - .BGRA, + .bgra, .UnsignedByte, font_group.atlas_color.data.ptr, ); } + // Build our cell renderer + const cell_program = try CellProgram.init(); + errdefer cell_program.deinit(); + return .{ - .program = program, - .vao = vao, - .ebo = ebo, - .vbo = vbo, + .cell_program = cell_program, .texture = tex, .texture_color = tex_color, + .custom = custom_state, }; } - pub fn deinit(self: *GLState) void { + pub fn deinit(self: *GLState, alloc: Allocator) void { + if (self.custom) |v| v.deinit(alloc); self.texture.destroy(); self.texture_color.destroy(); - self.vbo.destroy(); - self.ebo.destroy(); - self.vao.destroy(); - self.program.destroy(); + self.cell_program.deinit(); } }; diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 17abd6325..156d6cd6d 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -15,6 +15,7 @@ const App = @import("../App.zig"); const Allocator = std.mem.Allocator; const log = std.log.scoped(.renderer_thread); +const DRAW_INTERVAL = 33; // 30 FPS const CURSOR_BLINK_INTERVAL = 600; /// The type used for sending messages to the IO thread. For now this is @@ -43,6 +44,13 @@ stop_c: xev.Completion = .{}, render_h: xev.Timer, render_c: xev.Completion = .{}, +/// The timer used for draw calls. Draw calls don't update from the +/// terminal state so they're much cheaper. They're used for animation +/// and are paused when the terminal is not focused. +draw_h: xev.Timer, +draw_c: xev.Completion = .{}, +draw_active: bool = false, + /// The timer used for cursor blinking cursor_h: xev.Timer, cursor_c: xev.Completion = .{}, @@ -100,6 +108,10 @@ pub fn init( var render_h = try xev.Timer.init(); errdefer render_h.deinit(); + // Draw timer, see comments. + var draw_h = try xev.Timer.init(); + errdefer draw_h.deinit(); + // Setup a timer for blinking the cursor var cursor_timer = try xev.Timer.init(); errdefer cursor_timer.deinit(); @@ -114,6 +126,7 @@ pub fn init( .wakeup = wakeup_h, .stop = stop_h, .render_h = render_h, + .draw_h = draw_h, .cursor_h = cursor_timer, .surface = surface, .renderer = renderer_impl, @@ -129,6 +142,7 @@ pub fn deinit(self: *Thread) void { self.stop.deinit(); self.wakeup.deinit(); self.render_h.deinit(); + self.draw_h.deinit(); self.cursor_h.deinit(); self.loop.deinit(); @@ -172,27 +186,8 @@ fn threadMain_(self: *Thread) !void { cursorTimerCallback, ); - // If we are using tracy, then we setup a prepare handle so that - // we can mark the frame. - // TODO - // var frame_h: libuv.Prepare = if (!tracy.enabled) undefined else frame_h: { - // const alloc_ptr = self.loop.getData(Allocator).?; - // const alloc = alloc_ptr.*; - // const h = try libuv.Prepare.init(alloc, self.loop); - // h.setData(self); - // try h.start(prepFrameCallback); - // - // break :frame_h h; - // }; - // defer if (tracy.enabled) { - // frame_h.close((struct { - // fn callback(h: *libuv.Prepare) void { - // const alloc_h = h.loop().getData(Allocator).?.*; - // h.deinit(alloc_h); - // } - // }).callback); - // _ = self.loop.run(.nowait) catch {}; - // }; + // Start the draw timer + self.startDrawTimer(); // Run log.debug("starting renderer thread", .{}); @@ -200,6 +195,34 @@ fn threadMain_(self: *Thread) !void { _ = try self.loop.run(.until_done); } +fn startDrawTimer(self: *Thread) void { + // If our renderer doesn't suppoort animations then we never run this. + if (!@hasDecl(renderer.Renderer, "hasAnimations")) return; + if (!self.renderer.hasAnimations()) return; + + // Set our active state so it knows we're running. We set this before + // even checking the active state in case we have a pending shutdown. + self.draw_active = true; + + // If our draw timer is already active, then we don't have to do anything. + if (self.draw_c.state() == .active) return; + + // Start the timer which loops + self.draw_h.run( + &self.loop, + &self.draw_c, + DRAW_INTERVAL, + Thread, + self, + drawCallback, + ); +} + +fn stopDrawTimer(self: *Thread) void { + // This will stop the draw on the next iteration. + self.draw_active = false; +} + /// Drain the mailbox. fn drainMailbox(self: *Thread) !void { const zone = trace(@src()); @@ -213,6 +236,9 @@ fn drainMailbox(self: *Thread) !void { try self.renderer.setFocus(v); if (!v) { + // Stop the draw timer + self.stopDrawTimer(); + // If we're not focused, then we stop the cursor blink if (self.cursor_c.state() == .active and self.cursor_c_cancel.state() == .dead) @@ -227,6 +253,9 @@ fn drainMailbox(self: *Thread) !void { ); } } else { + // Start the draw timer + self.startDrawTimer(); + // If we're focused, we immediately show the cursor again // and then restart the timer. if (self.cursor_c.state() != .active) { @@ -281,6 +310,11 @@ fn drainMailbox(self: *Thread) !void { .change_config => |config| { defer config.alloc.destroy(config.ptr); try self.renderer.changeConfig(config.ptr); + + // Stop and start the draw timer to capture the new + // hasAnimations value. + self.stopDrawTimer(); + self.startDrawTimer(); }, .inspector => |v| self.flags.has_inspector = v, @@ -325,6 +359,41 @@ fn wakeupCallback( return .rearm; } +fn drawCallback( + self_: ?*Thread, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Timer.RunError!void, +) xev.CallbackAction { + _ = r catch unreachable; + const t = self_ orelse { + // This shouldn't happen so we log it. + log.warn("render callback fired without data set", .{}); + return .disarm; + }; + + // If we're doing single-threaded GPU calls then we just wake up the + // app thread to redraw at this point. + if (renderer.Renderer == renderer.OpenGL and + renderer.OpenGL.single_threaded_draw) + { + _ = t.app_mailbox.push( + .{ .redraw_surface = t.surface }, + .{ .instant = {} }, + ); + } else { + t.renderer.drawFrame(t.surface) catch |err| + log.warn("error drawing err={}", .{err}); + } + + // Only continue if we're still active + if (t.draw_active) { + t.draw_h.run(&t.loop, &t.draw_c, DRAW_INTERVAL, Thread, t, drawCallback); + } + + return .disarm; +} + fn renderCallback( self_: ?*Thread, _: *xev.Loop, @@ -346,7 +415,8 @@ fn renderCallback( _ = t.app_mailbox.push(.{ .redraw_inspector = t.surface }, .{ .instant = {} }); } - t.renderer.render( + // Update our frame data + t.renderer.updateFrame( t.surface, t.state, t.flags.cursor_blink_visible, @@ -359,8 +429,13 @@ fn renderCallback( renderer.OpenGL.single_threaded_draw) { _ = t.app_mailbox.push(.{ .redraw_surface = t.surface }, .{ .instant = {} }); + return .disarm; } + // Draw + t.renderer.drawFrame(t.surface) catch |err| + log.warn("error drawing err={}", .{err}); + return .disarm; } diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index f3dc2f835..f92489374 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -57,6 +57,7 @@ pub const MTLVertexStepFunction = enum(c_ulong) { /// https://developer.apple.com/documentation/metal/mtlpixelformat?language=objc pub const MTLPixelFormat = enum(c_ulong) { r8unorm = 10, + rgba8unorm = 70, rgba8uint = 73, bgra8unorm = 80, }; @@ -66,6 +67,22 @@ pub const MTLPurgeableState = enum(c_ulong) { empty = 4, }; +/// https://developer.apple.com/documentation/metal/mtlsamplerminmagfilter?language=objc +pub const MTLSamplerMinMagFilter = enum(c_ulong) { + nearest = 0, + linear = 1, +}; + +/// https://developer.apple.com/documentation/metal/mtlsampleraddressmode?language=objc +pub const MTLSamplerAddressMode = enum(c_ulong) { + clamp_to_edge = 0, + mirror_clamp_to_edge = 1, + repeat = 2, + mirror_repeat = 3, + clamp_to_zero = 4, + clamp_to_border_color = 5, +}; + /// https://developer.apple.com/documentation/metal/mtlblendfactor?language=objc pub const MTLBlendFactor = enum(c_ulong) { zero = 0, @@ -98,6 +115,15 @@ pub const MTLBlendOperation = enum(c_ulong) { max = 4, }; +/// https://developer.apple.com/documentation/metal/mtltextureusage?language=objc +pub const MTLTextureUsage = enum(c_ulong) { + unknown = 0, + shader_read = 1, + shader_write = 2, + render_target = 4, + pixel_format_view = 8, +}; + /// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc /// (incomplete, we only use this mode so we just hardcode it) pub const MTLResourceStorageModeShared: c_ulong = @intFromEnum(MTLStorageMode.shared) << 4; diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig index 88de3d27a..154174697 100644 --- a/src/renderer/metal/image.zig +++ b/src/renderer/metal/image.zig @@ -218,7 +218,7 @@ pub const Image = union(enum) { fn initTexture(p: Pending, device: objc.Object) !objc.Object { // Create our descriptor const desc = init: { - const Class = objc.Class.getClass("MTLTextureDescriptor").?; + const Class = objc.getClass("MTLTextureDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; diff --git a/src/renderer/metal/sampler.zig b/src/renderer/metal/sampler.zig new file mode 100644 index 000000000..c7a04df3a --- /dev/null +++ b/src/renderer/metal/sampler.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const objc = @import("objc"); + +const mtl = @import("api.zig"); + +pub const Sampler = struct { + sampler: objc.Object, + + pub fn init(device: objc.Object) !Sampler { + const desc = init: { + const Class = objc.getClass("MTLSamplerDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + defer desc.msgSend(void, objc.sel("release"), .{}); + desc.setProperty("rAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge)); + desc.setProperty("sAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge)); + desc.setProperty("tAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge)); + desc.setProperty("minFilter", @intFromEnum(mtl.MTLSamplerMinMagFilter.linear)); + desc.setProperty("magFilter", @intFromEnum(mtl.MTLSamplerMinMagFilter.linear)); + + const sampler = device.msgSend( + objc.Object, + objc.sel("newSamplerStateWithDescriptor:"), + .{desc}, + ); + errdefer sampler.msgSend(void, objc.sel("release"), .{}); + + return .{ .sampler = sampler }; + } + + pub fn deinit(self: *Sampler) void { + self.sampler.msgSend(void, objc.sel("release"), .{}); + } +}; diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 9001ede8f..030ae2b6c 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -12,10 +12,31 @@ const log = std.log.scoped(.metal); /// This contains the state for the shaders used by the Metal renderer. pub const Shaders = struct { library: objc.Object, + + /// The cell shader is the shader used to render the terminal cells. + /// It is a single shader that is used for both the background and + /// foreground. cell_pipeline: objc.Object, + + /// The image shader is the shader used to render images for things + /// like the Kitty image protocol. image_pipeline: objc.Object, - pub fn init(device: objc.Object) !Shaders { + /// Custom shaders to run against the final drawable texture. This + /// can be used to apply a lot of effects. Each shader is run in sequence + /// against the output of the previous shader. + post_pipelines: []const objc.Object, + + /// Initialize our shader set. + /// + /// "post_shaders" is an optional list of postprocess shaders to run + /// against the final drawable texture. This is an array of shader source + /// code, not file paths. + pub fn init( + alloc: Allocator, + device: objc.Object, + post_shaders: []const [:0]const u8, + ) !Shaders { const library = try initLibrary(device); errdefer library.msgSend(void, objc.sel("release"), .{}); @@ -25,17 +46,44 @@ pub const Shaders = struct { const image_pipeline = try initImagePipeline(device, library); errdefer image_pipeline.msgSend(void, objc.sel("release"), .{}); + const post_pipelines: []const objc.Object = initPostPipelines( + alloc, + device, + library, + post_shaders, + ) catch |err| err: { + // If an error happens while building postprocess shaders we + // want to just not use any postprocess shaders since we don't + // want to block Ghostty from working. + log.warn("error initializing postprocess shaders err={}", .{err}); + break :err &.{}; + }; + errdefer if (post_pipelines.len > 0) { + for (post_pipelines) |pipeline| pipeline.msgSend(void, objc.sel("release"), .{}); + alloc.free(post_pipelines); + }; + return .{ .library = library, .cell_pipeline = cell_pipeline, .image_pipeline = image_pipeline, + .post_pipelines = post_pipelines, }; } - pub fn deinit(self: *Shaders) void { + pub fn deinit(self: *Shaders, alloc: Allocator) void { + // Release our primary shaders self.cell_pipeline.msgSend(void, objc.sel("release"), .{}); self.image_pipeline.msgSend(void, objc.sel("release"), .{}); self.library.msgSend(void, objc.sel("release"), .{}); + + // Release our postprocess shaders + if (self.post_pipelines.len > 0) { + for (self.post_pipelines) |pipeline| { + pipeline.msgSend(void, objc.sel("release"), .{}); + } + alloc.free(self.post_pipelines); + } } }; @@ -79,6 +127,23 @@ pub const Uniforms = extern struct { strikethrough_thickness: f32, }; +/// The uniforms used for custom postprocess shaders. +pub const PostUniforms = extern struct { + // Note: all of the explicit aligmnments are copied from the + // MSL developer reference just so that we can be sure that we got + // it all exactly right. + 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), +}; + /// Initialize the MTLLibrary. A MTLLibrary is a collection of shaders. fn initLibrary(device: objc.Object) !objc.Object { // Hardcoded since this file isn't meant to be reusable. @@ -105,6 +170,129 @@ fn initLibrary(device: objc.Object) !objc.Object { return library; } +/// Initialize our custom shader pipelines. The shaders argument is a +/// set of shader source code, not file paths. +fn initPostPipelines( + alloc: Allocator, + device: objc.Object, + library: objc.Object, + shaders: []const [:0]const u8, +) ![]const objc.Object { + // If we have no shaders, do nothing. + if (shaders.len == 0) return &.{}; + + // Keeps track of how many shaders we successfully wrote. + var i: usize = 0; + + // Initialize our result set. If any error happens, we undo everything. + var pipelines = try alloc.alloc(objc.Object, shaders.len); + errdefer { + for (pipelines[0..i]) |pipeline| { + pipeline.msgSend(void, objc.sel("release"), .{}); + } + alloc.free(pipelines); + } + + // Build each shader. Note we don't use "0.." to build our index + // because we need to keep track of our length to clean up above. + for (shaders) |source| { + pipelines[i] = try initPostPipeline(device, library, source); + i += 1; + } + + return pipelines; +} + +/// Initialize a single custom shader pipeline from shader source. +fn initPostPipeline( + device: objc.Object, + library: objc.Object, + data: [:0]const u8, +) !objc.Object { + // Create our library which has the shader source + const post_library = library: { + const source = try macos.foundation.String.createWithBytes( + data, + .utf8, + false, + ); + defer source.release(); + + var err: ?*anyopaque = null; + const post_library = device.msgSend( + objc.Object, + objc.sel("newLibraryWithSource:options:error:"), + .{ source, @as(?*anyopaque, null), &err }, + ); + try checkError(err); + errdefer post_library.msgSend(void, objc.sel("release"), .{}); + + break :library post_library; + }; + defer post_library.msgSend(void, objc.sel("release"), .{}); + + // Get our vertex and fragment functions + const func_vert = func_vert: { + const str = try macos.foundation.String.createWithBytes( + "post_vertex", + .utf8, + false, + ); + defer str.release(); + + const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + break :func_vert objc.Object.fromId(ptr.?); + }; + const func_frag = func_frag: { + const str = try macos.foundation.String.createWithBytes( + "main0", + .utf8, + false, + ); + defer str.release(); + + const ptr = post_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + break :func_frag objc.Object.fromId(ptr.?); + }; + defer func_vert.msgSend(void, objc.sel("release"), .{}); + defer func_frag.msgSend(void, objc.sel("release"), .{}); + + // Create our descriptor + const desc = init: { + const Class = objc.getClass("MTLRenderPipelineDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + defer desc.msgSend(void, objc.sel("release"), .{}); + desc.setProperty("vertexFunction", func_vert); + desc.setProperty("fragmentFunction", func_frag); + + // Set our color attachment + const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); + { + const attachment = attachments.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + // Value is MTLPixelFormatBGRA8Unorm + attachment.setProperty("pixelFormat", @as(c_ulong, 80)); + } + + // Make our state + var err: ?*anyopaque = null; + const pipeline_state = device.msgSend( + objc.Object, + objc.sel("newRenderPipelineStateWithDescriptor:error:"), + .{ desc, &err }, + ); + try checkError(err); + + return pipeline_state; +} + /// Initialize the cell render pipeline for our shader library. fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { // Get our vertex and fragment functions @@ -130,6 +318,8 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); break :func_frag objc.Object.fromId(ptr.?); }; + defer func_vert.msgSend(void, objc.sel("release"), .{}); + defer func_frag.msgSend(void, objc.sel("release"), .{}); // Create the vertex descriptor. The vertex descriptor describes the // data layout of the vertex inputs. We use indexed (or "instanced") @@ -137,7 +327,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { // Cell as input. const vertex_desc = vertex_desc: { const desc = init: { - const Class = objc.Class.getClass("MTLVertexDescriptor").?; + const Class = objc.getClass("MTLVertexDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; @@ -239,14 +429,16 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { break :vertex_desc desc; }; + defer vertex_desc.msgSend(void, objc.sel("release"), .{}); // Create our descriptor const desc = init: { - const Class = objc.Class.getClass("MTLRenderPipelineDescriptor").?; + const Class = objc.getClass("MTLRenderPipelineDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; }; + defer desc.msgSend(void, objc.sel("release"), .{}); // Set our properties desc.setProperty("vertexFunction", func_vert); @@ -284,6 +476,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { .{ desc, &err }, ); try checkError(err); + errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); return pipeline_state; } @@ -313,6 +506,8 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); break :func_frag objc.Object.fromId(ptr.?); }; + defer func_vert.msgSend(void, objc.sel("release"), .{}); + defer func_frag.msgSend(void, objc.sel("release"), .{}); // Create the vertex descriptor. The vertex descriptor describes the // data layout of the vertex inputs. We use indexed (or "instanced") @@ -320,7 +515,7 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { // Image as input. const vertex_desc = vertex_desc: { const desc = init: { - const Class = objc.Class.getClass("MTLVertexDescriptor").?; + const Class = objc.getClass("MTLVertexDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; @@ -389,14 +584,16 @@ fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { break :vertex_desc desc; }; + defer vertex_desc.msgSend(void, objc.sel("release"), .{}); // Create our descriptor const desc = init: { - const Class = objc.Class.getClass("MTLRenderPipelineDescriptor").?; + const Class = objc.getClass("MTLRenderPipelineDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; }; + defer desc.msgSend(void, objc.sel("release"), .{}); // Set our properties desc.setProperty("vertexFunction", func_vert); diff --git a/src/renderer/opengl/CellProgram.zig b/src/renderer/opengl/CellProgram.zig new file mode 100644 index 000000000..d1ea969fe --- /dev/null +++ b/src/renderer/opengl/CellProgram.zig @@ -0,0 +1,174 @@ +/// The OpenGL program for rendering terminal cells. +const CellProgram = @This(); + +const std = @import("std"); +const gl = @import("opengl"); + +program: gl.Program, +vao: gl.VertexArray, +ebo: gl.Buffer, +vbo: gl.Buffer, + +/// The raw structure that maps directly to the buffer sent to the vertex shader. +/// This must be "extern" so that the field order is not reordered by the +/// Zig compiler. +pub const Cell = extern struct { + /// vec2 grid_coord + grid_col: u16, + grid_row: u16, + + /// vec2 glyph_pos + glyph_x: u32 = 0, + glyph_y: u32 = 0, + + /// vec2 glyph_size + glyph_width: u32 = 0, + glyph_height: u32 = 0, + + /// vec2 glyph_offset + glyph_offset_x: i32 = 0, + glyph_offset_y: i32 = 0, + + /// vec4 fg_color_in + r: u8, + g: u8, + b: u8, + a: u8, + + /// uint mode + mode: CellMode, + + /// The width in grid cells that a rendering takes. + grid_width: u8, +}; + +pub const CellMode = enum(u8) { + bg = 1, + fg = 2, + fg_color = 7, + strikethrough = 8, + + // Non-exhaustive because masks change it + _, + + /// Apply a mask to the mode. + pub fn mask(self: CellMode, m: CellMode) CellMode { + return @enumFromInt(@intFromEnum(self) | @intFromEnum(m)); + } +}; + +pub fn init() !CellProgram { + // Load and compile our shaders. + const program = try gl.Program.createVF( + @embedFile("../shaders/cell.v.glsl"), + @embedFile("../shaders/cell.f.glsl"), + ); + errdefer program.destroy(); + + // Set our cell dimensions + const pbind = try program.use(); + defer pbind.unbind(); + + // Set all of our texture indexes + try program.setUniform("text", 0); + try program.setUniform("text_color", 1); + + // Setup our VAO + const vao = try gl.VertexArray.create(); + errdefer vao.destroy(); + const vaobind = try vao.bind(); + defer vaobind.unbind(); + + // Element buffer (EBO) + const ebo = try gl.Buffer.create(); + errdefer ebo.destroy(); + var ebobind = try ebo.bind(.element_array); + defer ebobind.unbind(); + try ebobind.setData([6]u8{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }, .static_draw); + + // Vertex buffer (VBO) + const vbo = try gl.Buffer.create(); + errdefer vbo.destroy(); + var vbobind = try vbo.bind(.array); + defer vbobind.unbind(); + var offset: usize = 0; + try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(Cell), offset); + offset += 2 * @sizeOf(u16); + try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Cell), offset); + offset += 2 * @sizeOf(u32); + try vbobind.attributeAdvanced(2, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Cell), offset); + offset += 2 * @sizeOf(u32); + try vbobind.attributeAdvanced(3, 2, gl.c.GL_INT, false, @sizeOf(Cell), offset); + offset += 2 * @sizeOf(i32); + try vbobind.attributeAdvanced(4, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset); + offset += 4 * @sizeOf(u8); + try vbobind.attributeIAdvanced(5, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset); + offset += 1 * @sizeOf(u8); + try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset); + try vbobind.enableAttribArray(0); + try vbobind.enableAttribArray(1); + try vbobind.enableAttribArray(2); + try vbobind.enableAttribArray(3); + try vbobind.enableAttribArray(4); + try vbobind.enableAttribArray(5); + try vbobind.enableAttribArray(6); + try vbobind.attributeDivisor(0, 1); + try vbobind.attributeDivisor(1, 1); + try vbobind.attributeDivisor(2, 1); + try vbobind.attributeDivisor(3, 1); + try vbobind.attributeDivisor(4, 1); + try vbobind.attributeDivisor(5, 1); + try vbobind.attributeDivisor(6, 1); + + return .{ + .program = program, + .vao = vao, + .ebo = ebo, + .vbo = vbo, + }; +} + +pub fn bind(self: CellProgram) !Binding { + const program = try self.program.use(); + errdefer program.unbind(); + + const vao = try self.vao.bind(); + errdefer vao.unbind(); + + const ebo = try self.ebo.bind(.element_array); + errdefer ebo.unbind(); + + const vbo = try self.vbo.bind(.array); + errdefer vbo.unbind(); + + return .{ + .program = program, + .vao = vao, + .ebo = ebo, + .vbo = vbo, + }; +} + +pub fn deinit(self: CellProgram) void { + self.vbo.destroy(); + self.ebo.destroy(); + self.vao.destroy(); + self.program.destroy(); +} + +pub const Binding = struct { + program: gl.Program.Binding, + vao: gl.VertexArray.Binding, + ebo: gl.Buffer.Binding, + vbo: gl.Buffer.Binding, + + pub fn unbind(self: Binding) void { + self.vbo.unbind(); + self.ebo.unbind(); + self.vao.unbind(); + self.program.unbind(); + } +}; diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig new file mode 100644 index 000000000..c14ba3c5c --- /dev/null +++ b/src/renderer/opengl/custom.zig @@ -0,0 +1,289 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const gl = @import("opengl"); +const ScreenSize = @import("../size.zig").ScreenSize; + +const log = std.log.scoped(.opengl_custom); + +/// The "INDEX" is the index into the global GL state and the +/// "BINDING" is the binding location in the shader. +const UNIFORM_INDEX: gl.c.GLuint = 0; +const UNIFORM_BINDING: gl.c.GLuint = 0; + +/// Global uniforms for custom shaders. +pub const Uniforms = extern struct { + resolution: [3]f32 align(16) = .{ 0, 0, 0 }, + time: f32 align(4) = 1, + time_delta: f32 align(4) = 1, + frame_rate: f32 align(4) = 1, + frame: i32 align(4) = 1, + channel_time: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + channel_resolution: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, + mouse: [4]f32 align(16) = .{ 0, 0, 0, 0 }, + date: [4]f32 align(16) = .{ 0, 0, 0, 0 }, + sample_rate: f32 align(4) = 1, +}; + +/// The state associated with custom shaders. This should only be initialized +/// if there is at least one custom shader. +/// +/// To use this, the main terminal shader should render to the framebuffer +/// specified by "fbo". The resulting "fb_texture" will contain the color +/// attachment. This is then used as the iChannel0 input to the custom +/// shader. +pub const State = struct { + /// The uniform data + uniforms: Uniforms, + + /// The OpenGL buffers + fbo: gl.Framebuffer, + ubo: gl.Buffer, + vao: gl.VertexArray, + ebo: gl.Buffer, + fb_texture: gl.Texture, + + /// The set of programs for the custom shaders. + programs: []const Program, + + /// The last time the frame was drawn. This is used to update + /// the time uniform. + first_frame_time: std.time.Instant, + + pub fn init( + alloc: Allocator, + srcs: []const [:0]const u8, + ) !State { + if (srcs.len == 0) return error.OneCustomShaderRequired; + + // Create our programs + var programs = std.ArrayList(Program).init(alloc); + defer programs.deinit(); + errdefer for (programs.items) |p| p.deinit(); + for (srcs) |src| { + try programs.append(try Program.init(src)); + } + + // Create the texture for the framebuffer + const fb_tex = try gl.Texture.create(); + errdefer fb_tex.destroy(); + { + const texbind = try fb_tex.bind(.@"2D"); + try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE); + try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE); + try texbind.parameter(.MinFilter, gl.c.GL_LINEAR); + try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); + try texbind.image2D( + 0, + .rgb, + 1, + 1, + 0, + .rgb, + .UnsignedByte, + null, + ); + } + + // Create our framebuffer for rendering off screen. + // The shader prior to custom shaders should use this + // framebuffer. + const fbo = try gl.Framebuffer.create(); + errdefer fbo.destroy(); + const fbbind = try fbo.bind(.framebuffer); + defer fbbind.unbind(); + try fbbind.texture2D(.color0, .@"2D", fb_tex, 0); + const fbstatus = fbbind.checkStatus(); + if (fbstatus != .complete) { + log.warn( + "framebuffer is not complete state={}", + .{fbstatus}, + ); + return error.InvalidFramebuffer; + } + + // Create our uniform buffer that is shared across all + // custom shaders + const ubo = try gl.Buffer.create(); + errdefer ubo.destroy(); + { + var ubobind = try ubo.bind(.uniform); + defer ubobind.unbind(); + try ubobind.setDataNull(Uniforms, .static_draw); + } + + // Setup our VAO for the custom shader. + const vao = try gl.VertexArray.create(); + errdefer vao.destroy(); + const vaobind = try vao.bind(); + defer vaobind.unbind(); + + // Element buffer (EBO) + const ebo = try gl.Buffer.create(); + errdefer ebo.destroy(); + var ebobind = try ebo.bind(.element_array); + defer ebobind.unbind(); + try ebobind.setData([6]u8{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }, .static_draw); + + return .{ + .programs = try programs.toOwnedSlice(), + .uniforms = .{}, + .fbo = fbo, + .ubo = ubo, + .vao = vao, + .ebo = ebo, + .fb_texture = fb_tex, + .first_frame_time = try std.time.Instant.now(), + }; + } + + pub fn deinit(self: *const State, alloc: Allocator) void { + for (self.programs) |p| p.deinit(); + alloc.free(self.programs); + self.ubo.destroy(); + self.ebo.destroy(); + self.vao.destroy(); + self.fb_texture.destroy(); + self.fbo.destroy(); + } + + pub fn setScreenSize(self: *State, size: ScreenSize) !void { + // Update our uniforms + self.uniforms.resolution = .{ + @floatFromInt(size.width), + @floatFromInt(size.height), + 1, + }; + try self.syncUniforms(); + + // Update our texture + const texbind = try self.fb_texture.bind(.@"2D"); + try texbind.image2D( + 0, + .rgb, + @intCast(size.width), + @intCast(size.height), + 0, + .rgb, + .UnsignedByte, + null, + ); + } + + /// Call this prior to drawing a frame to update the time + /// and synchronize the uniforms. This synchronizes uniforms + /// so you should make changes to uniforms prior to calling + /// this. + pub fn newFrame(self: *State) !void { + // Update our frame time + const now = std.time.Instant.now() catch self.first_frame_time; + const since_ns: f32 = @floatFromInt(now.since(self.first_frame_time)); + self.uniforms.time = since_ns / std.time.ns_per_s; + self.uniforms.time_delta = since_ns / std.time.ns_per_s; + + // Sync our uniform changes + try self.syncUniforms(); + } + + fn syncUniforms(self: *State) !void { + var ubobind = try self.ubo.bind(.uniform); + defer ubobind.unbind(); + try ubobind.setData(self.uniforms, .static_draw); + } + + /// Call this to bind all the necessary OpenGL resources for + /// all custom shaders. Each individual shader needs to be bound + /// one at a time too. + pub fn bind(self: *const State) !Binding { + // Move our uniform buffer into proper global index. Note that + // in theory we can do this globally once and never worry about + // it again. I don't think we're high-performance enough at all + // to worry about that and this makes it so you can just move + // around CustomProgram usage without worrying about clobbering + // the global state. + try self.ubo.bindBase(.uniform, UNIFORM_INDEX); + + // Bind our texture that is shared amongst all + try gl.Texture.active(gl.c.GL_TEXTURE0); + var texbind = try self.fb_texture.bind(.@"2D"); + errdefer texbind.unbind(); + + const vao = try self.vao.bind(); + errdefer vao.unbind(); + + const ebo = try self.ebo.bind(.element_array); + errdefer ebo.unbind(); + + return .{ + .vao = vao, + .ebo = ebo, + .fb_texture = texbind, + }; + } + + pub const Binding = struct { + vao: gl.VertexArray.Binding, + ebo: gl.Buffer.Binding, + fb_texture: gl.Texture.Binding, + + pub fn unbind(self: Binding) void { + self.ebo.unbind(); + self.vao.unbind(); + self.fb_texture.unbind(); + } + }; +}; + +/// A single OpenGL program (combined shaders) for custom shaders. +pub const Program = struct { + program: gl.Program, + + pub fn init(src: [:0]const u8) !Program { + const program = try gl.Program.createVF( + @embedFile("../shaders/custom.v.glsl"), + src, + //@embedFile("../shaders/temp.f.glsl"), + ); + errdefer program.destroy(); + + // Map our uniform buffer to the global GL state + try program.uniformBlockBinding(UNIFORM_INDEX, UNIFORM_BINDING); + + return .{ .program = program }; + } + + pub fn deinit(self: *const Program) void { + self.program.destroy(); + } + + /// Bind the program for use. This should be called so that draw can + /// be called. + pub fn bind(self: *const Program) !Binding { + const program = try self.program.use(); + errdefer program.unbind(); + + return .{ + .program = program, + }; + } + + pub const Binding = struct { + program: gl.Program.Binding, + + pub fn unbind(self: Binding) void { + self.program.unbind(); + } + + pub fn draw(self: Binding) !void { + _ = self; + try gl.drawElementsInstanced( + gl.c.GL_TRIANGLES, + 6, + gl.c.GL_UNSIGNED_BYTE, + 1, + ); + } + }; +}; diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 8c1e0750a..e6ba3f7ac 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -253,3 +253,20 @@ fragment float4 image_fragment( uint4 rgba = image.sample(textureSampler, in.tex_coord); return float4(rgba) / 255.0f; } + +//------------------------------------------------------------------- +// Post Shader +//------------------------------------------------------------------- +#pragma mark - Post Shader + +struct PostVertexOut { + float4 position [[ position ]]; +}; + +constant float2 post_pos[4] = { {-1,-1}, {1,-1}, {-1,1}, {1,1 } }; + +vertex PostVertexOut post_vertex(uint id [[ vertex_id ]]) { + PostVertexOut out; + out.position = float4(post_pos[id], 0, 1); + return out; +} diff --git a/src/renderer/shaders/cell.v.glsl b/src/renderer/shaders/cell.v.glsl index a339358c5..ccca33982 100644 --- a/src/renderer/shaders/cell.v.glsl +++ b/src/renderer/shaders/cell.v.glsl @@ -21,20 +21,18 @@ layout (location = 2) in vec2 glyph_size; // Offset of the top-left corner of the glyph when rendered in a rect. layout (location = 3) in vec2 glyph_offset; -// The background color for this cell in RGBA (0 to 1.0) -layout (location = 4) in vec4 fg_color_in; - -// The background color for this cell in RGBA (0 to 1.0) -layout (location = 5) in vec4 bg_color_in; +// The color for this cell in RGBA (0 to 1.0). Background or foreground +// depends on mode. +layout (location = 4) in vec4 color_in; // The mode of this shader. The mode determines what fields are used, // what the output will be, etc. This shader is capable of executing in // multiple "modes" so that we can share some logic and so that we can draw // the entire terminal grid in a single GPU pass. -layout (location = 6) in uint mode_in; +layout (location = 5) in uint mode_in; // The width in cells of this item. -layout (location = 7) in uint grid_width; +layout (location = 6) in uint grid_width; // The background or foreground color for the fragment, depending on // whether this is a background or foreground pass. @@ -117,7 +115,7 @@ void main() { cell_pos = cell_pos + cell_size_scaled * position; gl_Position = projection * vec4(cell_pos, cell_z, 1.0); - color = bg_color_in / 255.0; + color = color_in / 255.0; break; case MODE_FG: @@ -150,7 +148,7 @@ void main() { glyph_tex_coords = glyph_tex_pos + glyph_tex_size * position; // Set our foreground color output - color = fg_color_in / 255.; + color = color_in / 255.; break; case MODE_STRIKETHROUGH: @@ -166,7 +164,7 @@ void main() { cell_pos = cell_pos + strikethrough_offset - (strikethrough_size * position); gl_Position = projection * vec4(cell_pos, cell_z, 1.0); - color = fg_color_in / 255.0; + color = color_in / 255.0; break; } } diff --git a/src/renderer/shaders/custom.v.glsl b/src/renderer/shaders/custom.v.glsl new file mode 100644 index 000000000..653e1800e --- /dev/null +++ b/src/renderer/shaders/custom.v.glsl @@ -0,0 +1,8 @@ +#version 330 core + +void main(){ + vec2 position; + position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? -1. : 1.; + position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 1. : -1.; + gl_Position = vec4(position.xy, 0.0f, 1.0f); +} diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl new file mode 100644 index 000000000..a1a220bd4 --- /dev/null +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -0,0 +1,29 @@ +#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; + +// These are unused currently by Ghostty: +// 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..6f7937102 --- /dev/null +++ b/src/renderer/shadertoy.zig @@ -0,0 +1,365 @@ +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 { 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: []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: { + // 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");