mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-29 18:41:40 +00:00
Merge pull request #903 from mitchellh/macos-update
Custom Shaders (Metal and OpenGL)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
const Buffer = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig");
|
||||
const errors = @import("errors.zig");
|
||||
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,
|
||||
_,
|
||||
};
|
||||
|
||||
/// 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,
|
||||
_,
|
||||
};
|
||||
|
||||
/// 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 {
|
||||
target: Target,
|
||||
|
||||
/// 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(
|
||||
b: Binding,
|
||||
data: anytype,
|
||||
usage: Usage,
|
||||
) !void {
|
||||
const info = dataInfo(&data);
|
||||
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(
|
||||
b: Binding,
|
||||
offset: usize,
|
||||
data: anytype,
|
||||
) !void {
|
||||
const info = dataInfo(data);
|
||||
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(
|
||||
b: Binding,
|
||||
comptime T: type,
|
||||
usage: Usage,
|
||||
) !void {
|
||||
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(
|
||||
b: Binding,
|
||||
size: usize,
|
||||
usage: Usage,
|
||||
) !void {
|
||||
glad.context.BufferData.?(@intFromEnum(b.target), @intCast(size), null, @intFromEnum(usage));
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
fn dataInfo(data: anytype) struct {
|
||||
size: isize,
|
||||
ptr: *const anyopaque,
|
||||
} {
|
||||
return switch (@typeInfo(@TypeOf(data))) {
|
||||
.Pointer => |ptr| switch (ptr.size) {
|
||||
.One => .{
|
||||
.size = @sizeOf(ptr.child) * data.len,
|
||||
.ptr = data,
|
||||
},
|
||||
.Slice => .{
|
||||
.size = @intCast(@sizeOf(ptr.child) * data.len),
|
||||
.ptr = data.ptr,
|
||||
},
|
||||
else => {
|
||||
std.log.err("invalid buffer data pointer size: {}", .{ptr.size});
|
||||
unreachable;
|
||||
},
|
||||
},
|
||||
else => {
|
||||
std.log.err("invalid buffer data type: {s}", .{@tagName(@typeInfo(@TypeOf(data)))});
|
||||
unreachable;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub inline fn enableAttribArray(_: Binding, idx: c.GLuint) !void {
|
||||
glad.context.EnableVertexAttribArray.?(idx);
|
||||
}
|
||||
|
||||
/// Shorthand for vertexAttribPointer that is specialized towards the
|
||||
/// common use case of specifying an array of homogeneous types that
|
||||
/// don't need normalization. This also enables the attribute at idx.
|
||||
pub fn attribute(
|
||||
b: Binding,
|
||||
idx: c.GLuint,
|
||||
size: c.GLint,
|
||||
comptime T: type,
|
||||
offset: usize,
|
||||
) !void {
|
||||
const info: struct {
|
||||
// Type of the each component in the array.
|
||||
typ: c.GLenum,
|
||||
|
||||
// The byte offset between each full set of attributes.
|
||||
stride: c.GLsizei,
|
||||
|
||||
// The size of each component used in calculating the offset.
|
||||
offset: usize,
|
||||
} = switch (@typeInfo(T)) {
|
||||
.Array => |ary| .{
|
||||
.typ = switch (ary.child) {
|
||||
f32 => c.GL_FLOAT,
|
||||
else => @compileError("unsupported array child type"),
|
||||
},
|
||||
.offset = @sizeOf(ary.child),
|
||||
.stride = @sizeOf(T),
|
||||
},
|
||||
else => @compileError("unsupported type"),
|
||||
};
|
||||
|
||||
try b.attributeAdvanced(
|
||||
idx,
|
||||
size,
|
||||
info.typ,
|
||||
false,
|
||||
info.stride,
|
||||
offset * info.offset,
|
||||
);
|
||||
try b.enableAttribArray(idx);
|
||||
}
|
||||
|
||||
/// VertexAttribDivisor
|
||||
pub fn attributeDivisor(_: Binding, idx: c.GLuint, divisor: c.GLuint) !void {
|
||||
glad.context.VertexAttribDivisor.?(idx, divisor);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub inline fn attributeAdvanced(
|
||||
_: Binding,
|
||||
idx: c.GLuint,
|
||||
size: c.GLint,
|
||||
typ: c.GLenum,
|
||||
normalized: bool,
|
||||
stride: c.GLsizei,
|
||||
offset: usize,
|
||||
) !void {
|
||||
const normalized_c: c.GLboolean = if (normalized) c.GL_TRUE else c.GL_FALSE;
|
||||
const offsetPtr = if (offset > 0)
|
||||
@as(*const anyopaque, @ptrFromInt(offset))
|
||||
else
|
||||
null;
|
||||
|
||||
glad.context.VertexAttribPointer.?(idx, size, typ, normalized_c, stride, offsetPtr);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub inline fn attributeIAdvanced(
|
||||
_: Binding,
|
||||
idx: c.GLuint,
|
||||
size: c.GLint,
|
||||
typ: c.GLenum,
|
||||
stride: c.GLsizei,
|
||||
offset: usize,
|
||||
) !void {
|
||||
const offsetPtr = if (offset > 0)
|
||||
@as(*const anyopaque, @ptrFromInt(offset))
|
||||
else
|
||||
null;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
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();
|
||||
}
|
||||
};
|
||||
@@ -1,128 +0,0 @@
|
||||
const Program = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const log = std.log.scoped(.opengl);
|
||||
|
||||
const c = @import("c.zig");
|
||||
const Shader = @import("Shader.zig");
|
||||
const errors = @import("errors.zig");
|
||||
const glad = @import("glad.zig");
|
||||
|
||||
id: c.GLuint,
|
||||
|
||||
const Binding = struct {
|
||||
pub inline fn unbind(_: Binding) void {
|
||||
glad.context.UseProgram.?(0);
|
||||
}
|
||||
};
|
||||
|
||||
pub inline fn create() !Program {
|
||||
const id = glad.context.CreateProgram.?();
|
||||
if (id == 0) try errors.mustError();
|
||||
|
||||
log.debug("program created id={}", .{id});
|
||||
return Program{ .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 {
|
||||
const vs = try Shader.create(c.GL_VERTEX_SHADER);
|
||||
try vs.setSourceAndCompile(vsrc);
|
||||
defer vs.destroy();
|
||||
|
||||
const fs = try Shader.create(c.GL_FRAGMENT_SHADER);
|
||||
try fs.setSourceAndCompile(fsrc);
|
||||
defer fs.destroy();
|
||||
|
||||
const p = try create();
|
||||
try p.attachShader(vs);
|
||||
try p.attachShader(fs);
|
||||
try p.link();
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
pub inline fn attachShader(p: Program, s: Shader) !void {
|
||||
glad.context.AttachShader.?(p.id, s.id);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub inline fn link(p: Program) !void {
|
||||
glad.context.LinkProgram.?(p.id);
|
||||
|
||||
// Check if linking succeeded
|
||||
var success: c_int = undefined;
|
||||
glad.context.GetProgramiv.?(p.id, c.GL_LINK_STATUS, &success);
|
||||
if (success == c.GL_TRUE) {
|
||||
log.debug("program linked id={}", .{p.id});
|
||||
return;
|
||||
}
|
||||
|
||||
log.err("program link failure id={} message={s}", .{
|
||||
p.id,
|
||||
std.mem.sliceTo(&p.getInfoLog(), 0),
|
||||
});
|
||||
return error.CompileFailed;
|
||||
}
|
||||
|
||||
pub inline fn use(p: Program) !Binding {
|
||||
glad.context.UseProgram.?(p.id);
|
||||
try errors.getError();
|
||||
return Binding{};
|
||||
}
|
||||
|
||||
/// Requires the program is currently in use.
|
||||
pub inline fn setUniform(
|
||||
p: Program,
|
||||
n: [:0]const u8,
|
||||
value: anytype,
|
||||
) !void {
|
||||
const loc = glad.context.GetUniformLocation.?(
|
||||
p.id,
|
||||
@ptrCast(n.ptr),
|
||||
);
|
||||
if (loc < 0) {
|
||||
return error.UniformNameInvalid;
|
||||
}
|
||||
try errors.getError();
|
||||
|
||||
// Perform the correct call depending on the type of the value.
|
||||
switch (@TypeOf(value)) {
|
||||
comptime_int => glad.context.Uniform1i.?(loc, value),
|
||||
f32 => glad.context.Uniform1f.?(loc, value),
|
||||
@Vector(2, f32) => glad.context.Uniform2f.?(loc, value[0], value[1]),
|
||||
@Vector(3, f32) => glad.context.Uniform3f.?(loc, value[0], value[1], value[2]),
|
||||
@Vector(4, f32) => glad.context.Uniform4f.?(loc, value[0], value[1], value[2], value[3]),
|
||||
[4]@Vector(4, f32) => glad.context.UniformMatrix4fv.?(
|
||||
loc,
|
||||
1,
|
||||
c.GL_FALSE,
|
||||
@ptrCast(&value),
|
||||
),
|
||||
else => {
|
||||
log.warn("unsupported uniform type {}", .{@TypeOf(value)});
|
||||
unreachable;
|
||||
},
|
||||
}
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
/// getInfoLog returns the info log for this program. This attempts to
|
||||
/// keep the log fully stack allocated and is therefore limited to a max
|
||||
/// amount of elements.
|
||||
//
|
||||
// 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 {
|
||||
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});
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
const Shader = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const log = std.log.scoped(.opengl);
|
||||
|
||||
const c = @import("c.zig");
|
||||
const errors = @import("errors.zig");
|
||||
const glad = @import("glad.zig");
|
||||
|
||||
id: c.GLuint,
|
||||
|
||||
pub inline fn create(typ: c.GLenum) errors.Error!Shader {
|
||||
const id = glad.context.CreateShader.?(typ);
|
||||
if (id == 0) {
|
||||
try errors.mustError();
|
||||
unreachable;
|
||||
}
|
||||
|
||||
log.debug("shader created id={}", .{id});
|
||||
return Shader{ .id = id };
|
||||
}
|
||||
|
||||
/// Set the source and compile a shader.
|
||||
pub inline fn setSourceAndCompile(s: Shader, source: [:0]const u8) !void {
|
||||
glad.context.ShaderSource.?(s.id, 1, &@as([*c]const u8, @ptrCast(source)), null);
|
||||
glad.context.CompileShader.?(s.id);
|
||||
|
||||
// Check if compilation succeeded
|
||||
var success: c_int = undefined;
|
||||
glad.context.GetShaderiv.?(s.id, c.GL_COMPILE_STATUS, &success);
|
||||
if (success == c.GL_TRUE) return;
|
||||
log.err("shader compilation failure id={} message={s}", .{
|
||||
s.id,
|
||||
std.mem.sliceTo(&s.getInfoLog(), 0),
|
||||
});
|
||||
return error.CompileFailed;
|
||||
}
|
||||
|
||||
/// getInfoLog returns the info log for this shader. This attempts to
|
||||
/// keep the log fully stack allocated and is therefore limited to a max
|
||||
/// amount of elements.
|
||||
//
|
||||
// NOTE(mitchellh): we can add a dynamic version that uses an allocator
|
||||
// if we ever need it.
|
||||
pub inline fn getInfoLog(s: Shader) [512]u8 {
|
||||
var msg: [512]u8 = undefined;
|
||||
glad.context.GetShaderInfoLog.?(s.id, msg.len, null, &msg);
|
||||
return msg;
|
||||
}
|
||||
|
||||
pub inline fn destroy(s: Shader) void {
|
||||
assert(s.id != 0);
|
||||
glad.context.DeleteShader.?(s.id);
|
||||
log.debug("shader destroyed id={}", .{s.id});
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
const Texture = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig");
|
||||
const errors = @import("errors.zig");
|
||||
const glad = @import("glad.zig");
|
||||
|
||||
id: c.GLuint,
|
||||
|
||||
pub inline fn active(target: c.GLenum) !void {
|
||||
glad.context.ActiveTexture.?(target);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
/// Enun for possible texture binding targets.
|
||||
pub const Target = enum(c_uint) {
|
||||
@"1D" = c.GL_TEXTURE_1D,
|
||||
@"2D" = c.GL_TEXTURE_2D,
|
||||
@"3D" = c.GL_TEXTURE_3D,
|
||||
@"1DArray" = c.GL_TEXTURE_1D_ARRAY,
|
||||
@"2DArray" = c.GL_TEXTURE_2D_ARRAY,
|
||||
Rectangle = c.GL_TEXTURE_RECTANGLE,
|
||||
CubeMap = c.GL_TEXTURE_CUBE_MAP,
|
||||
Buffer = c.GL_TEXTURE_BUFFER,
|
||||
@"2DMultisample" = c.GL_TEXTURE_2D_MULTISAMPLE,
|
||||
@"2DMultisampleArray" = c.GL_TEXTURE_2D_MULTISAMPLE_ARRAY,
|
||||
};
|
||||
|
||||
/// Enum for possible texture parameters.
|
||||
pub const Parameter = enum(c_uint) {
|
||||
BaseLevel = c.GL_TEXTURE_BASE_LEVEL,
|
||||
CompareFunc = c.GL_TEXTURE_COMPARE_FUNC,
|
||||
CompareMode = c.GL_TEXTURE_COMPARE_MODE,
|
||||
LodBias = c.GL_TEXTURE_LOD_BIAS,
|
||||
MinFilter = c.GL_TEXTURE_MIN_FILTER,
|
||||
MagFilter = c.GL_TEXTURE_MAG_FILTER,
|
||||
MinLod = c.GL_TEXTURE_MIN_LOD,
|
||||
MaxLod = c.GL_TEXTURE_MAX_LOD,
|
||||
MaxLevel = c.GL_TEXTURE_MAX_LEVEL,
|
||||
SwizzleR = c.GL_TEXTURE_SWIZZLE_R,
|
||||
SwizzleG = c.GL_TEXTURE_SWIZZLE_G,
|
||||
SwizzleB = c.GL_TEXTURE_SWIZZLE_B,
|
||||
SwizzleA = c.GL_TEXTURE_SWIZZLE_A,
|
||||
WrapS = c.GL_TEXTURE_WRAP_S,
|
||||
WrapT = c.GL_TEXTURE_WRAP_T,
|
||||
WrapR = c.GL_TEXTURE_WRAP_R,
|
||||
};
|
||||
|
||||
/// Internal format enum for texture images.
|
||||
pub const InternalFormat = enum(c_int) {
|
||||
Red = c.GL_RED,
|
||||
RGBA = c.GL_RGBA,
|
||||
|
||||
// There are so many more that I haven't filled in.
|
||||
_,
|
||||
};
|
||||
|
||||
/// Format for texture images
|
||||
pub const Format = enum(c_uint) {
|
||||
Red = c.GL_RED,
|
||||
BGRA = c.GL_BGRA,
|
||||
|
||||
// There are so many more that I haven't filled in.
|
||||
_,
|
||||
};
|
||||
|
||||
/// Data type for texture images.
|
||||
pub const DataType = enum(c_uint) {
|
||||
UnsignedByte = c.GL_UNSIGNED_BYTE,
|
||||
|
||||
// There are so many more that I haven't filled in.
|
||||
_,
|
||||
};
|
||||
|
||||
pub const Binding = struct {
|
||||
target: Target,
|
||||
|
||||
pub inline fn unbind(b: *Binding) void {
|
||||
glad.context.BindTexture.?(@intFromEnum(b.target), 0);
|
||||
b.* = undefined;
|
||||
}
|
||||
|
||||
pub fn generateMipmap(b: Binding) void {
|
||||
glad.context.GenerateMipmap.?(@intFromEnum(b.target));
|
||||
}
|
||||
|
||||
pub fn parameter(b: Binding, name: Parameter, value: anytype) !void {
|
||||
switch (@TypeOf(value)) {
|
||||
c.GLint => glad.context.TexParameteri.?(
|
||||
@intFromEnum(b.target),
|
||||
@intFromEnum(name),
|
||||
value,
|
||||
),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn image2D(
|
||||
b: Binding,
|
||||
level: c.GLint,
|
||||
internal_format: InternalFormat,
|
||||
width: c.GLsizei,
|
||||
height: c.GLsizei,
|
||||
border: c.GLint,
|
||||
format: Format,
|
||||
typ: DataType,
|
||||
data: ?*const anyopaque,
|
||||
) !void {
|
||||
glad.context.TexImage2D.?(
|
||||
@intFromEnum(b.target),
|
||||
level,
|
||||
@intFromEnum(internal_format),
|
||||
width,
|
||||
height,
|
||||
border,
|
||||
@intFromEnum(format),
|
||||
@intFromEnum(typ),
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn subImage2D(
|
||||
b: Binding,
|
||||
level: c.GLint,
|
||||
xoffset: c.GLint,
|
||||
yoffset: c.GLint,
|
||||
width: c.GLsizei,
|
||||
height: c.GLsizei,
|
||||
format: Format,
|
||||
typ: DataType,
|
||||
data: ?*const anyopaque,
|
||||
) !void {
|
||||
glad.context.TexSubImage2D.?(
|
||||
@intFromEnum(b.target),
|
||||
level,
|
||||
xoffset,
|
||||
yoffset,
|
||||
width,
|
||||
height,
|
||||
@intFromEnum(format),
|
||||
@intFromEnum(typ),
|
||||
data,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/// 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);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
const VertexArray = @This();
|
||||
|
||||
const c = @import("c.zig");
|
||||
const glad = @import("glad.zig");
|
||||
const errors = @import("errors.zig");
|
||||
|
||||
id: c.GLuint,
|
||||
|
||||
/// Create a single vertex array object.
|
||||
pub inline 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 {
|
||||
glad.context.BindVertexArray.?(v.id);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub inline fn destroy(v: VertexArray) void {
|
||||
glad.context.DeleteVertexArrays.?(1, &v.id);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub usingnamespace @cImport({
|
||||
@cInclude("glad/gl.h");
|
||||
});
|
||||
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,
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
const c = @import("c.zig");
|
||||
const errors = @import("errors.zig");
|
||||
const glad = @import("glad.zig");
|
||||
|
||||
pub fn clearColor(r: f32, g: f32, b: f32, a: f32) void {
|
||||
glad.context.ClearColor.?(r, g, b, a);
|
||||
}
|
||||
|
||||
pub fn clear(mask: c.GLbitfield) void {
|
||||
glad.context.Clear.?(mask);
|
||||
}
|
||||
|
||||
pub fn drawArrays(mode: c.GLenum, first: c.GLint, count: c.GLsizei) !void {
|
||||
glad.context.DrawArrays.?(mode, first, count);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn drawElements(mode: c.GLenum, count: c.GLsizei, typ: c.GLenum, offset: usize) !void {
|
||||
const offsetPtr = if (offset == 0) null else @as(*const anyopaque, @ptrFromInt(offset));
|
||||
glad.context.DrawElements.?(mode, count, typ, offsetPtr);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn drawElementsInstanced(
|
||||
mode: c.GLenum,
|
||||
count: c.GLsizei,
|
||||
typ: c.GLenum,
|
||||
primcount: usize,
|
||||
) !void {
|
||||
glad.context.DrawElementsInstanced.?(mode, count, typ, null, @intCast(primcount));
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn enable(cap: c.GLenum) !void {
|
||||
glad.context.Enable.?(cap);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn frontFace(mode: c.GLenum) !void {
|
||||
glad.context.FrontFace.?(mode);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn blendFunc(sfactor: c.GLenum, dfactor: c.GLenum) !void {
|
||||
glad.context.BlendFunc.?(sfactor, dfactor);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn viewport(x: c.GLint, y: c.GLint, width: c.GLsizei, height: c.GLsizei) !void {
|
||||
glad.context.Viewport.?(x, y, width, height);
|
||||
}
|
||||
|
||||
pub fn pixelStore(mode: c.GLenum, value: anytype) !void {
|
||||
switch (@typeInfo(@TypeOf(value))) {
|
||||
.ComptimeInt, .Int => glad.context.PixelStorei.?(mode, value),
|
||||
else => unreachable,
|
||||
}
|
||||
try errors.getError();
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig");
|
||||
const glad = @import("glad.zig");
|
||||
|
||||
pub const Error = error{
|
||||
InvalidEnum,
|
||||
InvalidValue,
|
||||
InvalidOperation,
|
||||
InvalidFramebufferOperation,
|
||||
OutOfMemory,
|
||||
|
||||
Unknown,
|
||||
};
|
||||
|
||||
/// getError returns the error (if any) from the last OpenGL operation.
|
||||
pub fn getError() Error!void {
|
||||
return switch (glad.context.GetError.?()) {
|
||||
c.GL_NO_ERROR => {},
|
||||
c.GL_INVALID_ENUM => Error.InvalidEnum,
|
||||
c.GL_INVALID_VALUE => Error.InvalidValue,
|
||||
c.GL_INVALID_OPERATION => Error.InvalidOperation,
|
||||
c.GL_INVALID_FRAMEBUFFER_OPERATION => Error.InvalidFramebufferOperation,
|
||||
c.GL_OUT_OF_MEMORY => Error.OutOfMemory,
|
||||
else => Error.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
/// mustError just calls getError but always results in an error being returned.
|
||||
/// If getError has no error, then Unknown is returned.
|
||||
pub fn mustError() Error!void {
|
||||
try getError();
|
||||
return Error.Unknown;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig");
|
||||
const errors = @import("errors.zig");
|
||||
const glad = @import("glad.zig");
|
||||
|
||||
/// Returns the number of extensions.
|
||||
pub fn len() !u32 {
|
||||
var n: c.GLint = undefined;
|
||||
glad.context.GetIntegerv.?(c.GL_NUM_EXTENSIONS, &n);
|
||||
try errors.getError();
|
||||
return @intCast(n);
|
||||
}
|
||||
|
||||
/// Returns an iterator for the extensions.
|
||||
pub fn iterator() !Iterator {
|
||||
return Iterator{ .len = try len() };
|
||||
}
|
||||
|
||||
/// Iterator for the available extensions.
|
||||
pub const Iterator = struct {
|
||||
/// The total number of extensions.
|
||||
len: c.GLuint = 0,
|
||||
i: c.GLuint = 0,
|
||||
|
||||
pub fn next(self: *Iterator) !?[]const u8 {
|
||||
if (self.i >= self.len) return null;
|
||||
const res = glad.context.GetStringi.?(c.GL_EXTENSIONS, self.i);
|
||||
try errors.getError();
|
||||
self.i += 1;
|
||||
return std.mem.sliceTo(res, 0);
|
||||
}
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig");
|
||||
|
||||
pub const Context = c.GladGLContext;
|
||||
|
||||
/// This is the current context. Set this var manually prior to calling
|
||||
/// any of this package's functions. I know its nasty to have a global but
|
||||
/// this makes it match OpenGL API styles where it also operates on a
|
||||
/// threadlocal global.
|
||||
pub threadlocal var context: Context = undefined;
|
||||
|
||||
/// Initialize Glad. This is guaranteed to succeed if no errors are returned.
|
||||
/// The getProcAddress param is an anytype so that we can accept multiple
|
||||
/// forms of the function depending on what we're interfacing with.
|
||||
pub fn load(getProcAddress: anytype) !c_int {
|
||||
const GlProc = *const fn () callconv(.C) void;
|
||||
const GlfwFn = *const fn ([*:0]const u8) callconv(.C) ?GlProc;
|
||||
|
||||
const res = switch (@TypeOf(getProcAddress)) {
|
||||
// glfw
|
||||
GlfwFn => c.gladLoadGLContext(&context, @ptrCast(getProcAddress)),
|
||||
|
||||
// null proc address means that we are just loading the globally
|
||||
// pointed gl functions
|
||||
@TypeOf(null) => c.gladLoaderLoadGLContext(&context),
|
||||
|
||||
// try as-is. If this introduces a compiler error, then add a new case.
|
||||
else => c.gladLoadGLContext(&context, getProcAddress),
|
||||
};
|
||||
if (res == 0) return error.GLInitFailed;
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn unload() void {
|
||||
c.gladLoaderUnloadGLContext(&context);
|
||||
context = undefined;
|
||||
}
|
||||
|
||||
pub fn versionMajor(res: c_uint) c_uint {
|
||||
return c.GLAD_VERSION_MAJOR(res);
|
||||
}
|
||||
|
||||
pub fn versionMinor(res: c_uint) c_uint {
|
||||
return c.GLAD_VERSION_MINOR(res);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
//! OpenGL bindings.
|
||||
//!
|
||||
//! These are purpose-built for usage within this program. While they closely
|
||||
//! align with the OpenGL C APIs, they aren't meant to be general purpose,
|
||||
//! they aren't meant to have 100% API coverage, and they aren't meant to
|
||||
//! be hyper-performant.
|
||||
//!
|
||||
//! For performance-intensive or unsupported aspects of OpenGL, the C
|
||||
//! API is exposed via the `c` constant.
|
||||
//!
|
||||
//! WARNING: Lots of performance improvements that we can make with Zig
|
||||
//! comptime help. I'm deferring this until later but have some fun ideas.
|
||||
|
||||
pub const c = @import("c.zig");
|
||||
pub const glad = @import("glad.zig");
|
||||
pub usingnamespace @import("draw.zig");
|
||||
|
||||
pub const ext = @import("extensions.zig");
|
||||
pub const Buffer = @import("Buffer.zig");
|
||||
pub const Program = @import("Program.zig");
|
||||
pub const Shader = @import("Shader.zig");
|
||||
pub const Texture = @import("Texture.zig");
|
||||
pub const VertexArray = @import("VertexArray.zig");
|
||||
@@ -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