mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #903 from mitchellh/macos-update
Custom Shaders (Metal and OpenGL)
This commit is contained in:
@ -10,3 +10,6 @@ macos/
|
||||
|
||||
# website dev run
|
||||
website/.next
|
||||
|
||||
# shaders
|
||||
*.frag
|
||||
|
20
build.zig
20
build.zig
@ -643,6 +643,14 @@ fn addDeps(
|
||||
.optimize = step.optimize,
|
||||
.@"enable-libpng" = true,
|
||||
});
|
||||
const glslang_dep = b.dependency("glslang", .{
|
||||
.target = step.target,
|
||||
.optimize = step.optimize,
|
||||
});
|
||||
const spirv_cross_dep = b.dependency("spirv_cross", .{
|
||||
.target = step.target,
|
||||
.optimize = step.optimize,
|
||||
});
|
||||
const mach_glfw_dep = b.dependency("mach_glfw", .{
|
||||
.target = step.target,
|
||||
.optimize = step.optimize,
|
||||
@ -655,6 +663,7 @@ fn addDeps(
|
||||
.target = step.target,
|
||||
.optimize = step.optimize,
|
||||
});
|
||||
const opengl_dep = b.dependency("opengl", .{});
|
||||
const pixman_dep = b.dependency("pixman", .{
|
||||
.target = step.target,
|
||||
.optimize = step.optimize,
|
||||
@ -718,8 +727,11 @@ fn addDeps(
|
||||
fontconfig_dep.module("fontconfig"),
|
||||
);
|
||||
step.addModule("freetype", freetype_dep.module("freetype"));
|
||||
step.addModule("glslang", glslang_dep.module("glslang"));
|
||||
step.addModule("spirv_cross", spirv_cross_dep.module("spirv_cross"));
|
||||
step.addModule("harfbuzz", harfbuzz_dep.module("harfbuzz"));
|
||||
step.addModule("xev", libxev_dep.module("xev"));
|
||||
step.addModule("opengl", opengl_dep.module("opengl"));
|
||||
step.addModule("pixman", pixman_dep.module("pixman"));
|
||||
step.addModule("ziglyph", ziglyph_dep.module("ziglyph"));
|
||||
|
||||
@ -743,6 +755,14 @@ fn addDeps(
|
||||
try static_libs.append(tracy_dep.artifact("tracy").getEmittedBin());
|
||||
}
|
||||
|
||||
// Glslang
|
||||
step.linkLibrary(glslang_dep.artifact("glslang"));
|
||||
try static_libs.append(glslang_dep.artifact("glslang").getEmittedBin());
|
||||
|
||||
// Spirv-Cross
|
||||
step.linkLibrary(spirv_cross_dep.artifact("spirv_cross"));
|
||||
try static_libs.append(spirv_cross_dep.artifact("spirv_cross").getEmittedBin());
|
||||
|
||||
// Dynamic link
|
||||
if (!static) {
|
||||
step.addIncludePath(freetype_dep.path(""));
|
||||
|
@ -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
137
pkg/glslang/build.zig
Normal 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
11
pkg/glslang/build.zig.zon
Normal 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
4
pkg/glslang/c.zig
Normal 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
9
pkg/glslang/init.zig
Normal 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
9
pkg/glslang/main.zig
Normal 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());
|
||||
}
|
62
pkg/glslang/override/glslang/build_info.h
Normal file
62
pkg/glslang/override/glslang/build_info.h
Normal 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
60
pkg/glslang/program.zig
Normal 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
58
pkg/glslang/shader.zig
Normal 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
10
pkg/glslang/test.zig
Normal 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();
|
||||
}
|
56
pkg/glslang/test/simple.frag
Normal file
56
pkg/glslang/test/simple.frag
Normal 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) ) );
|
||||
}
|
@ -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);
|
||||
|
@ -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
6
pkg/macos/video.zig
Normal 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
3
pkg/macos/video/c.zig
Normal file
@ -0,0 +1,3 @@
|
||||
pub usingnamespace @cImport({
|
||||
@cInclude("CoreVideo/CoreVideo.h");
|
||||
});
|
71
pkg/macos/video/display_link.zig
Normal file
71
pkg/macos/video/display_link.zig
Normal 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;
|
||||
}
|
||||
};
|
@ -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,
|
||||
_,
|
||||
};
|
92
pkg/opengl/Framebuffer.zig
Normal file
92
pkg/opengl/Framebuffer.zig
Normal 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, ¤t);
|
||||
glad.context.BindFramebuffer.?(@intFromEnum(target), v.id);
|
||||
return .{ .target = target, .previous = @intCast(current) };
|
||||
}
|
||||
|
||||
/// Enum for possible binding targets.
|
||||
pub const Target = enum(c_uint) {
|
||||
framebuffer = c.GL_FRAMEBUFFER,
|
||||
draw = c.GL_DRAW_FRAMEBUFFER,
|
||||
read = c.GL_READ_FRAMEBUFFER,
|
||||
_,
|
||||
};
|
||||
|
||||
pub const Attachment = enum(c_uint) {
|
||||
color0 = c.GL_COLOR_ATTACHMENT0,
|
||||
depth = c.GL_DEPTH_ATTACHMENT,
|
||||
stencil = c.GL_STENCIL_ATTACHMENT,
|
||||
depth_stencil = c.GL_DEPTH_STENCIL_ATTACHMENT,
|
||||
_,
|
||||
};
|
||||
|
||||
pub const Status = enum(c_uint) {
|
||||
complete = c.GL_FRAMEBUFFER_COMPLETE,
|
||||
undefined = c.GL_FRAMEBUFFER_UNDEFINED,
|
||||
incomplete_attachment = c.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT,
|
||||
incomplete_missing_attachment = c.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT,
|
||||
incomplete_draw_buffer = c.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER,
|
||||
incomplete_read_buffer = c.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER,
|
||||
unsupported = c.GL_FRAMEBUFFER_UNSUPPORTED,
|
||||
incomplete_multisample = c.GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE,
|
||||
incomplete_layer_targets = c.GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS,
|
||||
_,
|
||||
};
|
||||
|
||||
pub const Binding = struct {
|
||||
target: Target,
|
||||
previous: c.GLuint,
|
||||
|
||||
pub fn unbind(self: Binding) void {
|
||||
glad.context.BindFramebuffer.?(
|
||||
@intFromEnum(self.target),
|
||||
self.previous,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn texture2D(
|
||||
self: Binding,
|
||||
attachment: Attachment,
|
||||
textarget: Texture.Target,
|
||||
texture: Texture,
|
||||
level: c.GLint,
|
||||
) !void {
|
||||
glad.context.FramebufferTexture2D.?(
|
||||
@intFromEnum(self.target),
|
||||
@intFromEnum(attachment),
|
||||
@intFromEnum(textarget),
|
||||
texture.id,
|
||||
level,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn checkStatus(self: Binding) Status {
|
||||
return @enumFromInt(glad.context.CheckFramebufferStatus.?(@intFromEnum(self.target)));
|
||||
}
|
||||
};
|
@ -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});
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
5
pkg/opengl/build.zig
Normal file
@ -0,0 +1,5 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
_ = b.addModule("opengl", .{ .source_file = .{ .path = "main.zig" } });
|
||||
}
|
@ -17,6 +17,7 @@ pub usingnamespace @import("draw.zig");
|
||||
|
||||
pub const ext = @import("extensions.zig");
|
||||
pub const Buffer = @import("Buffer.zig");
|
||||
pub const Framebuffer = @import("Framebuffer.zig");
|
||||
pub const Program = @import("Program.zig");
|
||||
pub const Shader = @import("Shader.zig");
|
||||
pub const Texture = @import("Texture.zig");
|
85
pkg/spirv-cross/build.zig
Normal file
85
pkg/spirv-cross/build.zig
Normal 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;
|
||||
}
|
11
pkg/spirv-cross/build.zig.zon
Normal file
11
pkg/spirv-cross/build.zig.zon
Normal 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
3
pkg/spirv-cross/c.zig
Normal file
@ -0,0 +1,3 @@
|
||||
pub usingnamespace @cImport({
|
||||
@cInclude("spirv_cross_c.h");
|
||||
});
|
1
pkg/spirv-cross/main.zig
Normal file
1
pkg/spirv-cross/main.zig
Normal file
@ -0,0 +1 @@
|
||||
pub const c = @import("c.zig");
|
@ -291,7 +291,7 @@ pub const App = struct {
|
||||
tabbing_id: *macos.foundation.String,
|
||||
|
||||
pub fn init() !Darwin {
|
||||
const NSWindow = objc.Class.getClass("NSWindow").?;
|
||||
const NSWindow = objc.getClass("NSWindow").?;
|
||||
NSWindow.msgSend(void, objc.sel("setAllowsAutomaticWindowTabbing:"), .{true});
|
||||
|
||||
// Our tabbing ID allows all of our windows to group together
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -528,7 +528,7 @@ keybind: Keybinds = .{},
|
||||
///
|
||||
/// Cycles are not allowed. If a cycle is detected, an error will be logged
|
||||
/// and the configuration file will be ignored.
|
||||
@"config-file": RepeatableString = .{},
|
||||
@"config-file": RepeatablePath = .{},
|
||||
|
||||
/// Confirms that a surface should be closed before closing it. This defaults
|
||||
/// to true. If set to false, surfaces will close without any confirmation.
|
||||
@ -599,6 +599,46 @@ keybind: Keybinds = .{},
|
||||
/// need KAM, you don't need it.
|
||||
@"vt-kam-allowed": bool = false,
|
||||
|
||||
/// Custom shaders to run after the default shaders. This is a file path
|
||||
/// to a GLSL-syntax shader for all platforms.
|
||||
///
|
||||
/// WARNING: Invalid shaders can cause Ghostty to become unusable such as by
|
||||
/// causing the window to be completely black. If this happens, you can
|
||||
/// unset this configuration to disable the shader.
|
||||
///
|
||||
/// On Linux, this requires OpenGL 4.2. Ghostty typically only requires
|
||||
/// OpenGL 3.3, but custom shaders push that requirement up to 4.2.
|
||||
///
|
||||
/// The shader API is identical to the Shadertoy API: you specify a `mainImage`
|
||||
/// function and the available uniforms match Shadertoy. The iChannel0 uniform
|
||||
/// is a texture containing the rendered terminal screen.
|
||||
///
|
||||
/// If the shader fails to compile, the shader will be ignored. Any errors
|
||||
/// related to shader compilation will not show up as configuration errors
|
||||
/// and only show up in the log, since shader compilation happens after
|
||||
/// configuration loading on the dedicated render thread. For interactive
|
||||
/// development, use Shadertoy.com.
|
||||
///
|
||||
/// This can be repeated multiple times to load multiple shaders. The shaders
|
||||
/// will be run in the order they are specified.
|
||||
///
|
||||
/// Changing this value at runtime and reloading the configuration will only
|
||||
/// affect new windows, tabs, and splits.
|
||||
@"custom-shader": RepeatablePath = .{},
|
||||
|
||||
/// If true (default), the focused terminal surface will run an animation
|
||||
/// loop when custom shaders are used. This uses slightly more CPU (generally
|
||||
/// less than 10%) but allows the shader to animate. This only runs if there
|
||||
/// are custom shaders.
|
||||
///
|
||||
/// If this is set to false, the terminal and custom shader will only render
|
||||
/// when the terminal is updated. This is more efficient but the shader will
|
||||
/// not animate.
|
||||
///
|
||||
/// This value can be changed at runtime and will affect all currently
|
||||
/// open terminals.
|
||||
@"custom-shader-animation": bool = true,
|
||||
|
||||
/// If anything other than false, fullscreen mode on macOS will not use the
|
||||
/// native fullscreen, but make the window fullscreen without animations and
|
||||
/// using a new space. It's faster than the native fullscreen mode since it
|
||||
@ -1143,7 +1183,7 @@ pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
|
||||
var buf_reader = std.io.bufferedReader(file.reader());
|
||||
var iter = cli.args.lineIterator(buf_reader.reader());
|
||||
try cli.args.parse(Config, alloc, self, &iter);
|
||||
try self.expandConfigFiles(std.fs.path.dirname(config_path).?);
|
||||
try self.expandPaths(std.fs.path.dirname(config_path).?);
|
||||
} else |err| switch (err) {
|
||||
error.FileNotFound => std.log.info(
|
||||
"homedir config not found, not loading path={s}",
|
||||
@ -1172,15 +1212,15 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
|
||||
try cli.args.parse(Config, alloc_gpa, self, &iter);
|
||||
|
||||
// Config files loaded from the CLI args are relative to pwd
|
||||
if (self.@"config-file".list.items.len > 0) {
|
||||
if (self.@"config-file".value.list.items.len > 0) {
|
||||
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||
try self.expandConfigFiles(try std.fs.cwd().realpath(".", &buf));
|
||||
try self.expandPaths(try std.fs.cwd().realpath(".", &buf));
|
||||
}
|
||||
}
|
||||
|
||||
/// Load and parse the config files that were added in the "config-file" key.
|
||||
pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
|
||||
if (self.@"config-file".list.items.len == 0) return;
|
||||
if (self.@"config-file".value.list.items.len == 0) return;
|
||||
const arena_alloc = self._arena.?.allocator();
|
||||
|
||||
// Keeps track of loaded files to prevent cycles.
|
||||
@ -1189,8 +1229,8 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
|
||||
|
||||
const cwd = std.fs.cwd();
|
||||
var i: usize = 0;
|
||||
while (i < self.@"config-file".list.items.len) : (i += 1) {
|
||||
const path = self.@"config-file".list.items[i];
|
||||
while (i < self.@"config-file".value.list.items.len) : (i += 1) {
|
||||
const path = self.@"config-file".value.list.items[i];
|
||||
|
||||
// Error paths
|
||||
if (path.len == 0) continue;
|
||||
@ -1227,37 +1267,22 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
|
||||
var buf_reader = std.io.bufferedReader(file.reader());
|
||||
var iter = cli.args.lineIterator(buf_reader.reader());
|
||||
try cli.args.parse(Config, alloc_gpa, self, &iter);
|
||||
try self.expandConfigFiles(std.fs.path.dirname(path).?);
|
||||
try self.expandPaths(std.fs.path.dirname(path).?);
|
||||
}
|
||||
}
|
||||
|
||||
/// Expand the relative paths in config-files to be absolute paths
|
||||
/// relative to the base directory.
|
||||
fn expandConfigFiles(self: *Config, base: []const u8) !void {
|
||||
assert(std.fs.path.isAbsolute(base));
|
||||
var dir = try std.fs.cwd().openDir(base, .{});
|
||||
defer dir.close();
|
||||
|
||||
fn expandPaths(self: *Config, base: []const u8) !void {
|
||||
const arena_alloc = self._arena.?.allocator();
|
||||
for (self.@"config-file".list.items, 0..) |path, i| {
|
||||
// If it is already absolute we can ignore it.
|
||||
if (path.len == 0 or std.fs.path.isAbsolute(path)) continue;
|
||||
|
||||
// If it isn't absolute, we need to make it absolute relative to the base.
|
||||
const abs = dir.realpathAlloc(arena_alloc, path) catch |err| {
|
||||
try self._errors.add(arena_alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
arena_alloc,
|
||||
"error resolving config-file {s}: {}",
|
||||
.{ path, err },
|
||||
),
|
||||
});
|
||||
self.@"config-file".list.items[i] = "";
|
||||
continue;
|
||||
};
|
||||
|
||||
log.debug("expanding config-file path relative={s} abs={s}", .{ path, abs });
|
||||
self.@"config-file".list.items[i] = abs;
|
||||
inline for (@typeInfo(Config).Struct.fields) |field| {
|
||||
if (field.type == RepeatablePath) {
|
||||
try @field(self, field.name).expand(
|
||||
arena_alloc,
|
||||
base,
|
||||
&self._errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1889,6 +1914,69 @@ pub const RepeatableString = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// RepeatablePath is like repeatable string but represents a path value.
|
||||
/// The difference is that when loading the configuration any values for
|
||||
/// this will be automatically expanded relative to the path of the config
|
||||
/// file.
|
||||
pub const RepeatablePath = struct {
|
||||
const Self = @This();
|
||||
|
||||
value: RepeatableString = .{},
|
||||
|
||||
pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void {
|
||||
return self.value.parseCLI(alloc, input);
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
pub fn clone(self: *const Self, alloc: Allocator) !Self {
|
||||
return .{
|
||||
.value = try self.value.clone(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
/// Compare if two of our value are requal. Required by Config.
|
||||
pub fn equal(self: Self, other: Self) bool {
|
||||
return self.value.equal(other.value);
|
||||
}
|
||||
|
||||
/// Expand all the paths relative to the base directory.
|
||||
pub fn expand(
|
||||
self: *Self,
|
||||
alloc: Allocator,
|
||||
base: []const u8,
|
||||
errors: *ErrorList,
|
||||
) !void {
|
||||
assert(std.fs.path.isAbsolute(base));
|
||||
var dir = try std.fs.cwd().openDir(base, .{});
|
||||
defer dir.close();
|
||||
|
||||
for (self.value.list.items, 0..) |path, i| {
|
||||
// If it is already absolute we can ignore it.
|
||||
if (path.len == 0 or std.fs.path.isAbsolute(path)) continue;
|
||||
|
||||
// If it isn't absolute, we need to make it absolute relative
|
||||
// to the base.
|
||||
const abs = dir.realpathAlloc(alloc, path) catch |err| {
|
||||
try errors.add(alloc, .{
|
||||
.message = try std.fmt.allocPrintZ(
|
||||
alloc,
|
||||
"error resolving config-file {s}: {}",
|
||||
.{ path, err },
|
||||
),
|
||||
});
|
||||
self.value.list.items[i] = "";
|
||||
continue;
|
||||
};
|
||||
|
||||
log.debug(
|
||||
"expanding config-file path relative={s} abs={s}",
|
||||
.{ path, abs },
|
||||
);
|
||||
self.value.list.items[i] = abs;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// FontVariation is a repeatable configuration value that sets a single
|
||||
/// font variation value. Font variations are configurations for what
|
||||
/// are often called "variable fonts." The font files usually end in
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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 },
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
38
src/renderer/metal/sampler.zig
Normal file
38
src/renderer/metal/sampler.zig
Normal 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"), .{});
|
||||
}
|
||||
};
|
@ -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);
|
||||
|
174
src/renderer/opengl/CellProgram.zig
Normal file
174
src/renderer/opengl/CellProgram.zig
Normal 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();
|
||||
}
|
||||
};
|
289
src/renderer/opengl/custom.zig
Normal file
289
src/renderer/opengl/custom.zig
Normal 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,
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
8
src/renderer/shaders/custom.v.glsl
Normal file
8
src/renderer/shaders/custom.v.glsl
Normal 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);
|
||||
}
|
29
src/renderer/shaders/shadertoy_prefix.glsl
Normal file
29
src/renderer/shaders/shadertoy_prefix.glsl
Normal 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); }
|
59
src/renderer/shaders/test_shadertoy_crt.glsl
Normal file
59
src/renderer/shaders/test_shadertoy_crt.glsl
Normal 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);
|
||||
}
|
12
src/renderer/shaders/test_shadertoy_invalid.glsl
Normal file
12
src/renderer/shaders/test_shadertoy_invalid.glsl
Normal 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
365
src/renderer/shadertoy.zig
Normal 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");
|
Reference in New Issue
Block a user