Merge pull request #903 from mitchellh/macos-update

Custom Shaders (Metal and OpenGL)
This commit is contained in:
Mitchell Hashimoto
2023-11-17 22:06:38 -08:00
committed by GitHub
60 changed files with 2844 additions and 488 deletions

View File

@ -10,3 +10,6 @@ macos/
# website dev run
website/.next
# shaders
*.frag

View File

@ -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(""));

View File

@ -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" },
},

137
pkg/glslang/build.zig Normal file
View File

@ -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;
}

11
pkg/glslang/build.zig.zon Normal file
View File

@ -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",
},
},
}

4
pkg/glslang/c.zig Normal file
View File

@ -0,0 +1,4 @@
pub usingnamespace @cImport({
@cInclude("glslang/Include/glslang_c_interface.h");
@cInclude("glslang/Public/resource_limits_c.h");
});

9
pkg/glslang/init.zig Normal file
View File

@ -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();
}

9
pkg/glslang/main.zig Normal file
View File

@ -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());
}

View File

@ -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

60
pkg/glslang/program.zig Normal file
View File

@ -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();
}

58
pkg/glslang/shader.zig Normal file
View File

@ -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);
}

10
pkg/glslang/test.zig Normal file
View File

@ -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();
}

View File

@ -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.;i<counter;i++){
v = mat2(v,-v.y,v.x) * v + vec2(2.*f+cos(0.5*t*(exp(-0.2* r))),-cos(t*r*r)*cos(0.5*t));
if(length(v)>2.){
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) ) );
}

View File

@ -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);

View File

@ -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());

6
pkg/macos/video.zig Normal file
View File

@ -0,0 +1,6 @@
pub const c = @import("video/c.zig");
pub usingnamespace @import("video/display_link.zig");
test {
@import("std").testing.refAllDecls(@This());
}

3
pkg/macos/video/c.zig Normal file
View File

@ -0,0 +1,3 @@
pub usingnamespace @cImport({
@cInclude("CoreVideo/CoreVideo.h");
});

View File

@ -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;
}
};

View File

@ -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,
_,
};

View File

@ -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, &current);
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)));
}
};

View File

@ -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});
}

View File

@ -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);
}

View File

@ -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);
}
};

5
pkg/opengl/build.zig Normal file
View File

@ -0,0 +1,5 @@
const std = @import("std");
pub fn build(b: *std.Build) !void {
_ = b.addModule("opengl", .{ .source_file = .{ .path = "main.zig" } });
}

View File

@ -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");

85
pkg/spirv-cross/build.zig Normal file
View File

@ -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;
}

View File

@ -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",
},
},
}

3
pkg/spirv-cross/c.zig Normal file
View File

@ -0,0 +1,3 @@
pub usingnamespace @cImport({
@cInclude("spirv_cross_c.h");
});

1
pkg/spirv-cross/main.zig Normal file
View File

@ -0,0 +1 @@
pub const c = @import("c.zig");

View File

@ -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

View File

@ -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);

View File

@ -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.

View File

@ -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(
inline for (@typeInfo(Config).Struct.fields) |field| {
if (field.type == RepeatablePath) {
try @field(self, field.name).expand(
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;
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

View File

@ -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

View File

@ -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;
};

View File

@ -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 },

View File

@ -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;
};

View File

@ -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");

View File

@ -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;

View File

@ -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();
}
};

View File

@ -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;
}

View File

@ -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<D-j>
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;

View File

@ -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;

View File

@ -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"), .{});
}
};

View File

@ -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);

View File

@ -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();
}
};

View File

@ -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,
);
}
};
};

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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); }

View File

@ -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);
}

View File

@ -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!

365
src/renderer/shadertoy.zig Normal file
View File

@ -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");