mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
Metal Renderer
This implements a pure Metal renderer for macOS targets. Performance: - Average frame time: 0.7ms (Metal) vs. 1.5ms (OpenGL) - Average fps while `cat`-ing a 1GB file (vsync disabled): 100 (Metal) vs. 70 (OpenGL) * Note: while the frame time is 2x faster in Metal, the FPS is not 2x for what I assume to be lock contention on terminal state. Why? - OpenGL has been deprecated on macOS since 2018. - All OpenGL has to go through a Metal translation layer anyways, which has a non-zero cost. - There is a bug on Mac where rendering OpenGL on a separate thread from the windowing thread can cause crashes, so most OpenGL software just don't multi-thread render on Mac. - Metal is more explicit about resource management compared to OpenGL, so we gain performance. - Metal is much more multi-thread friendly, so our multi-threaded renderer works great! (with resizes!)
This commit is contained in:
3
.github/workflows/test.yml
vendored
3
.github/workflows/test.yml
vendored
@ -58,9 +58,6 @@ jobs:
|
|||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
run: nix develop -c zig build test -fstage1
|
|
||||||
|
|
||||||
- name: test stage2
|
|
||||||
run: nix develop -c zig build test
|
run: nix develop -c zig build test
|
||||||
|
|
||||||
- name: Test Dynamic Build
|
- name: Test Dynamic Build
|
||||||
|
12
build.zig
12
build.zig
@ -223,13 +223,19 @@ fn addDeps(
|
|||||||
_ = try utf8proc.link(b, step);
|
_ = try utf8proc.link(b, step);
|
||||||
|
|
||||||
// Glfw
|
// Glfw
|
||||||
const glfw_opts: glfw.Options = .{ .metal = false, .opengl = false };
|
const glfw_opts: glfw.Options = .{
|
||||||
|
.metal = step.target.isDarwin(),
|
||||||
|
.opengl = false,
|
||||||
|
};
|
||||||
try glfw.link(b, step, glfw_opts);
|
try glfw.link(b, step, glfw_opts);
|
||||||
|
|
||||||
// Imgui, we have to do this later since we need some information
|
// Imgui, we have to do this later since we need some information
|
||||||
const imgui_backends = [_][]const u8{ "glfw", "opengl3" };
|
const imgui_backends = if (step.target.isDarwin())
|
||||||
|
&[_][]const u8{ "glfw", "opengl3", "metal" }
|
||||||
|
else
|
||||||
|
&[_][]const u8{ "glfw", "opengl3" };
|
||||||
var imgui_opts: imgui.Options = .{
|
var imgui_opts: imgui.Options = .{
|
||||||
.backends = &imgui_backends,
|
.backends = imgui_backends,
|
||||||
.freetype = .{ .enabled = true },
|
.freetype = .{ .enabled = true },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -101,11 +101,17 @@ pub fn buildImgui(
|
|||||||
lib.addCSourceFiles(srcs, flags.items);
|
lib.addCSourceFiles(srcs, flags.items);
|
||||||
if (opt.backends) |backends| {
|
if (opt.backends) |backends| {
|
||||||
for (backends) |backend| {
|
for (backends) |backend| {
|
||||||
|
const ext = if (std.mem.eql(u8, "metal", backend)) ext: {
|
||||||
|
// Metal requires some extra frameworks
|
||||||
|
step.linkFramework("QuartzCore");
|
||||||
|
break :ext "mm";
|
||||||
|
} else "cpp";
|
||||||
|
|
||||||
var buf: [4096]u8 = undefined;
|
var buf: [4096]u8 = undefined;
|
||||||
const path = try std.fmt.bufPrint(
|
const path = try std.fmt.bufPrint(
|
||||||
&buf,
|
&buf,
|
||||||
"{s}imgui/backends/imgui_impl_{s}.cpp",
|
"{s}imgui/backends/imgui_impl_{s}.{s}",
|
||||||
.{ root, backend },
|
.{ root, backend, ext },
|
||||||
);
|
);
|
||||||
|
|
||||||
lib.addCSourceFile(path, flags.items);
|
lib.addCSourceFile(path, flags.items);
|
||||||
|
@ -13,6 +13,10 @@ pub const ImplGlfw = struct {
|
|||||||
return ImGui_ImplGlfw_InitForOpenGL(win, install_callbacks);
|
return ImGui_ImplGlfw_InitForOpenGL(win, install_callbacks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn initForOther(win: *GLFWWindow, install_callbacks: bool) bool {
|
||||||
|
return ImGui_ImplGlfw_InitForOther(win, install_callbacks);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn shutdown() void {
|
pub fn shutdown() void {
|
||||||
return ImGui_ImplGlfw_Shutdown();
|
return ImGui_ImplGlfw_Shutdown();
|
||||||
}
|
}
|
||||||
@ -23,6 +27,7 @@ pub const ImplGlfw = struct {
|
|||||||
|
|
||||||
extern "c" fn glfwGetError(?*const anyopaque) c_int;
|
extern "c" fn glfwGetError(?*const anyopaque) c_int;
|
||||||
extern "c" fn ImGui_ImplGlfw_InitForOpenGL(*GLFWWindow, bool) bool;
|
extern "c" fn ImGui_ImplGlfw_InitForOpenGL(*GLFWWindow, bool) bool;
|
||||||
|
extern "c" fn ImGui_ImplGlfw_InitForOther(*GLFWWindow, bool) bool;
|
||||||
extern "c" fn ImGui_ImplGlfw_Shutdown() void;
|
extern "c" fn ImGui_ImplGlfw_Shutdown() void;
|
||||||
extern "c" fn ImGui_ImplGlfw_NewFrame() void;
|
extern "c" fn ImGui_ImplGlfw_NewFrame() void;
|
||||||
};
|
};
|
||||||
|
31
pkg/imgui/impl_metal.zig
Normal file
31
pkg/imgui/impl_metal.zig
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const c = @import("c.zig");
|
||||||
|
const imgui = @import("main.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
pub const ImplMetal = struct {
|
||||||
|
pub fn init(device: ?*anyopaque) bool {
|
||||||
|
return ImGui_ImplMetal_Init(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown() void {
|
||||||
|
return ImGui_ImplMetal_Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn newFrame(render_pass_desc: ?*anyopaque) void {
|
||||||
|
return ImGui_ImplMetal_NewFrame(render_pass_desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn renderDrawData(
|
||||||
|
data: *imgui.DrawData,
|
||||||
|
command_buffer: ?*anyopaque,
|
||||||
|
command_encoder: ?*anyopaque,
|
||||||
|
) void {
|
||||||
|
ImGui_ImplMetal_RenderDrawData(data, command_buffer, command_encoder);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "c" fn ImGui_ImplMetal_Init(?*anyopaque) bool;
|
||||||
|
extern "c" fn ImGui_ImplMetal_Shutdown() void;
|
||||||
|
extern "c" fn ImGui_ImplMetal_NewFrame(?*anyopaque) void;
|
||||||
|
extern "c" fn ImGui_ImplMetal_RenderDrawData(*imgui.DrawData, ?*anyopaque, ?*anyopaque) void;
|
||||||
|
};
|
@ -7,6 +7,7 @@ pub usingnamespace @import("io.zig");
|
|||||||
pub usingnamespace @import("style.zig");
|
pub usingnamespace @import("style.zig");
|
||||||
|
|
||||||
pub usingnamespace @import("impl_glfw.zig");
|
pub usingnamespace @import("impl_glfw.zig");
|
||||||
|
pub usingnamespace @import("impl_metal.zig");
|
||||||
pub usingnamespace @import("impl_opengl3.zig");
|
pub usingnamespace @import("impl_opengl3.zig");
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
@ -54,7 +54,7 @@ pub const Object = struct {
|
|||||||
break :getter objc.sel(val);
|
break :getter objc.sel(val);
|
||||||
} else objc.sel(n);
|
} else objc.sel(n);
|
||||||
|
|
||||||
self.msgSend(T, getter, .{});
|
return self.msgSend(T, getter, .{});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ const assert = std.debug.assert;
|
|||||||
|
|
||||||
const Atlas = @import("Atlas.zig");
|
const Atlas = @import("Atlas.zig");
|
||||||
const Window = @import("Window.zig");
|
const Window = @import("Window.zig");
|
||||||
|
const renderer = @import("renderer.zig");
|
||||||
|
|
||||||
/// If this is false, the rest of the terminal will be compiled without
|
/// If this is false, the rest of the terminal will be compiled without
|
||||||
/// dev mode support at all.
|
/// dev mode support at all.
|
||||||
@ -27,9 +28,10 @@ window: ?*Window = null,
|
|||||||
/// Update the state associated with the dev mode. This should generally
|
/// Update the state associated with the dev mode. This should generally
|
||||||
/// only be called paired with a render since it otherwise wastes CPU
|
/// only be called paired with a render since it otherwise wastes CPU
|
||||||
/// cycles.
|
/// cycles.
|
||||||
|
///
|
||||||
|
/// Note: renderers should call their implementation "newFrame" functions
|
||||||
|
/// prior to this.
|
||||||
pub fn update(self: *const DevMode) !void {
|
pub fn update(self: *const DevMode) !void {
|
||||||
imgui.ImplOpenGL3.newFrame();
|
|
||||||
imgui.ImplGlfw.newFrame();
|
|
||||||
imgui.newFrame();
|
imgui.newFrame();
|
||||||
|
|
||||||
if (imgui.begin("dev mode", null, .{})) {
|
if (imgui.begin("dev mode", null, .{})) {
|
||||||
@ -42,16 +44,27 @@ pub fn update(self: *const DevMode) !void {
|
|||||||
helpMarker("The number of glyphs loaded and rendered into a " ++
|
helpMarker("The number of glyphs loaded and rendered into a " ++
|
||||||
"font atlas currently.");
|
"font atlas currently.");
|
||||||
|
|
||||||
|
const Renderer = @TypeOf(window.renderer);
|
||||||
if (imgui.treeNode("Atlas: Greyscale", .{ .default_open = true })) {
|
if (imgui.treeNode("Atlas: Greyscale", .{ .default_open = true })) {
|
||||||
defer imgui.treePop();
|
defer imgui.treePop();
|
||||||
const atlas = &window.font_group.atlas_greyscale;
|
const atlas = &window.font_group.atlas_greyscale;
|
||||||
try self.atlasInfo(atlas, @intCast(usize, window.renderer.texture.id));
|
const tex = switch (Renderer) {
|
||||||
|
renderer.OpenGL => @intCast(usize, window.renderer.texture.id),
|
||||||
|
renderer.Metal => @ptrToInt(window.renderer.texture_greyscale.value),
|
||||||
|
else => @compileError("renderer unsupported, add it!"),
|
||||||
|
};
|
||||||
|
try self.atlasInfo(atlas, tex);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imgui.treeNode("Atlas: Color (Emoji)", .{ .default_open = true })) {
|
if (imgui.treeNode("Atlas: Color (Emoji)", .{ .default_open = true })) {
|
||||||
defer imgui.treePop();
|
defer imgui.treePop();
|
||||||
const atlas = &window.font_group.atlas_color;
|
const atlas = &window.font_group.atlas_color;
|
||||||
try self.atlasInfo(atlas, @intCast(usize, window.renderer.texture_color.id));
|
const tex = switch (Renderer) {
|
||||||
|
renderer.OpenGL => @intCast(usize, window.renderer.texture_color.id),
|
||||||
|
renderer.Metal => @ptrToInt(window.renderer.texture_color.value),
|
||||||
|
else => @compileError("renderer unsupported, add it!"),
|
||||||
|
};
|
||||||
|
try self.atlasInfo(atlas, tex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ const log = std.log.scoped(.window);
|
|||||||
const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
|
const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
|
||||||
|
|
||||||
// The renderer implementation to use.
|
// The renderer implementation to use.
|
||||||
const Renderer = renderer.OpenGL;
|
const Renderer = renderer.Renderer;
|
||||||
|
|
||||||
/// Allocator
|
/// Allocator
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
|
@ -6,6 +6,7 @@ const fontconfig = @import("fontconfig");
|
|||||||
const freetype = @import("freetype");
|
const freetype = @import("freetype");
|
||||||
const harfbuzz = @import("harfbuzz");
|
const harfbuzz = @import("harfbuzz");
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
|
const renderer = @import("renderer.zig");
|
||||||
|
|
||||||
const App = @import("App.zig");
|
const App = @import("App.zig");
|
||||||
const cli_args = @import("cli_args.zig");
|
const cli_args = @import("cli_args.zig");
|
||||||
@ -19,6 +20,7 @@ pub fn main() !void {
|
|||||||
if (options.fontconfig) {
|
if (options.fontconfig) {
|
||||||
log.info("dependency fontconfig={d}", .{fontconfig.version()});
|
log.info("dependency fontconfig={d}", .{fontconfig.version()});
|
||||||
}
|
}
|
||||||
|
log.info("renderer={}", .{renderer.Renderer});
|
||||||
|
|
||||||
const GPA = std.heap.GeneralPurposeAllocator(.{});
|
const GPA = std.heap.GeneralPurposeAllocator(.{});
|
||||||
var gpa: ?GPA = gpa: {
|
var gpa: ?GPA = gpa: {
|
||||||
|
@ -7,11 +7,22 @@
|
|||||||
//! APIs. The renderers in this package assume that the renderer is already
|
//! APIs. The renderers in this package assume that the renderer is already
|
||||||
//! setup (OpenGL has a context, Vulkan has a surface, etc.)
|
//! setup (OpenGL has a context, Vulkan has a surface, etc.)
|
||||||
|
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
pub usingnamespace @import("renderer/cursor.zig");
|
||||||
pub usingnamespace @import("renderer/size.zig");
|
pub usingnamespace @import("renderer/size.zig");
|
||||||
|
pub const Metal = @import("renderer/Metal.zig");
|
||||||
pub const OpenGL = @import("renderer/OpenGL.zig");
|
pub const OpenGL = @import("renderer/OpenGL.zig");
|
||||||
pub const Thread = @import("renderer/Thread.zig");
|
pub const Thread = @import("renderer/Thread.zig");
|
||||||
pub const State = @import("renderer/State.zig");
|
pub const State = @import("renderer/State.zig");
|
||||||
|
|
||||||
|
/// The implementation to use for the renderer. This is comptime chosen
|
||||||
|
/// so that every build has exactly one renderer implementation.
|
||||||
|
pub const Renderer = switch (builtin.os.tag) {
|
||||||
|
.macos => Metal,
|
||||||
|
else => OpenGL,
|
||||||
|
};
|
||||||
|
|
||||||
test {
|
test {
|
||||||
@import("std").testing.refAllDecls(@This());
|
@import("std").testing.refAllDecls(@This());
|
||||||
}
|
}
|
||||||
|
1207
src/renderer/Metal.zig
Normal file
1207
src/renderer/Metal.zig
Normal file
File diff suppressed because it is too large
Load Diff
@ -66,7 +66,7 @@ font_shaper: font.Shaper,
|
|||||||
/// Whether the cursor is visible or not. This is used to control cursor
|
/// Whether the cursor is visible or not. This is used to control cursor
|
||||||
/// blinking.
|
/// blinking.
|
||||||
cursor_visible: bool,
|
cursor_visible: bool,
|
||||||
cursor_style: CursorStyle,
|
cursor_style: renderer.CursorStyle,
|
||||||
|
|
||||||
/// Default foreground color
|
/// Default foreground color
|
||||||
foreground: terminal.color.RGB,
|
foreground: terminal.color.RGB,
|
||||||
@ -74,25 +74,6 @@ foreground: terminal.color.RGB,
|
|||||||
/// Default background color
|
/// Default background color
|
||||||
background: terminal.color.RGB,
|
background: terminal.color.RGB,
|
||||||
|
|
||||||
/// Available cursor styles for drawing. The values represents the mode value
|
|
||||||
/// in the shader.
|
|
||||||
pub const CursorStyle = enum(u8) {
|
|
||||||
box = 3,
|
|
||||||
box_hollow = 4,
|
|
||||||
bar = 5,
|
|
||||||
|
|
||||||
/// Create a cursor style from the terminal style request.
|
|
||||||
pub fn fromTerminal(style: terminal.CursorStyle) ?CursorStyle {
|
|
||||||
return switch (style) {
|
|
||||||
.blinking_block, .steady_block => .box,
|
|
||||||
.blinking_bar, .steady_bar => .bar,
|
|
||||||
.blinking_underline, .steady_underline => null, // TODO
|
|
||||||
.default => .box,
|
|
||||||
else => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The raw structure that maps directly to the buffer sent to the vertex shader.
|
/// 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
|
/// This must be "extern" so that the field order is not reordered by the
|
||||||
/// Zig compiler.
|
/// Zig compiler.
|
||||||
@ -145,6 +126,14 @@ const GPUCellMode = enum(u8) {
|
|||||||
// Non-exhaustive because masks change it
|
// Non-exhaustive because masks change it
|
||||||
_,
|
_,
|
||||||
|
|
||||||
|
pub fn fromCursor(cursor: renderer.CursorStyle) GPUCellMode {
|
||||||
|
return switch (cursor) {
|
||||||
|
.box => .cursor_rect,
|
||||||
|
.box_hollow => .cursor_rect_hollow,
|
||||||
|
.bar => .cursor_bar,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply a mask to the mode.
|
/// Apply a mask to the mode.
|
||||||
pub fn mask(self: GPUCellMode, m: GPUCellMode) GPUCellMode {
|
pub fn mask(self: GPUCellMode, m: GPUCellMode) GPUCellMode {
|
||||||
return @intToEnum(
|
return @intToEnum(
|
||||||
@ -468,7 +457,7 @@ pub fn render(
|
|||||||
// Setup our cursor state
|
// Setup our cursor state
|
||||||
if (state.focused) {
|
if (state.focused) {
|
||||||
self.cursor_visible = state.cursor.visible and !state.cursor.blink;
|
self.cursor_visible = state.cursor.visible and !state.cursor.blink;
|
||||||
self.cursor_style = CursorStyle.fromTerminal(state.cursor.style) orelse .box;
|
self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box;
|
||||||
} else {
|
} else {
|
||||||
self.cursor_visible = true;
|
self.cursor_visible = true;
|
||||||
self.cursor_style = .box_hollow;
|
self.cursor_style = .box_hollow;
|
||||||
@ -494,6 +483,8 @@ pub fn render(
|
|||||||
const devmode_data = devmode_data: {
|
const devmode_data = devmode_data: {
|
||||||
if (state.devmode) |dm| {
|
if (state.devmode) |dm| {
|
||||||
if (dm.visible) {
|
if (dm.visible) {
|
||||||
|
imgui.ImplOpenGL3.newFrame();
|
||||||
|
imgui.ImplGlfw.newFrame();
|
||||||
try dm.update();
|
try dm.update();
|
||||||
break :devmode_data try dm.render();
|
break :devmode_data try dm.render();
|
||||||
}
|
}
|
||||||
@ -701,13 +692,8 @@ fn addCursor(self: *OpenGL, term: *Terminal) void {
|
|||||||
term.screen.cursor.x,
|
term.screen.cursor.x,
|
||||||
);
|
);
|
||||||
|
|
||||||
var mode: GPUCellMode = @intToEnum(
|
|
||||||
GPUCellMode,
|
|
||||||
@enumToInt(self.cursor_style),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.cells.appendAssumeCapacity(.{
|
self.cells.appendAssumeCapacity(.{
|
||||||
.mode = mode,
|
.mode = GPUCellMode.fromCursor(self.cursor_style),
|
||||||
.grid_col = @intCast(u16, term.screen.cursor.x),
|
.grid_col = @intCast(u16, term.screen.cursor.x),
|
||||||
.grid_row = @intCast(u16, term.screen.cursor.y),
|
.grid_row = @intCast(u16, term.screen.cursor.y),
|
||||||
.grid_width = if (cell.attrs.wide) 2 else 1,
|
.grid_width = if (cell.attrs.wide) 2 else 1,
|
||||||
|
@ -31,7 +31,7 @@ render_h: libuv.Timer,
|
|||||||
window: glfw.Window,
|
window: glfw.Window,
|
||||||
|
|
||||||
/// The underlying renderer implementation.
|
/// The underlying renderer implementation.
|
||||||
renderer: *renderer.OpenGL,
|
renderer: *renderer.Renderer,
|
||||||
|
|
||||||
/// Pointer to the shared state that is used to generate the final render.
|
/// Pointer to the shared state that is used to generate the final render.
|
||||||
state: *renderer.State,
|
state: *renderer.State,
|
||||||
@ -42,7 +42,7 @@ state: *renderer.State,
|
|||||||
pub fn init(
|
pub fn init(
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
window: glfw.Window,
|
window: glfw.Window,
|
||||||
renderer_impl: *renderer.OpenGL,
|
renderer_impl: *renderer.Renderer,
|
||||||
state: *renderer.State,
|
state: *renderer.State,
|
||||||
) !Thread {
|
) !Thread {
|
||||||
// We always store allocator pointer on the loop data so that
|
// We always store allocator pointer on the loop data so that
|
||||||
@ -143,16 +143,11 @@ pub fn threadMain(self: *Thread) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn threadMain_(self: *Thread) !void {
|
fn threadMain_(self: *Thread) !void {
|
||||||
// Get a copy to our allocator
|
|
||||||
// const alloc_ptr = self.loop.getData(Allocator).?;
|
|
||||||
// const alloc = alloc_ptr.*;
|
|
||||||
|
|
||||||
// Run our thread start/end callbacks. This is important because some
|
// Run our thread start/end callbacks. This is important because some
|
||||||
// renderers have to do per-thread setup. For example, OpenGL has to set
|
// renderers have to do per-thread setup. For example, OpenGL has to set
|
||||||
// some thread-local state since that is how it works.
|
// some thread-local state since that is how it works.
|
||||||
const Renderer = RendererType();
|
try self.renderer.threadEnter(self.window);
|
||||||
if (@hasDecl(Renderer, "threadEnter")) try self.renderer.threadEnter(self.window);
|
defer self.renderer.threadExit();
|
||||||
defer if (@hasDecl(Renderer, "threadExit")) self.renderer.threadExit();
|
|
||||||
|
|
||||||
// Set up our async handler to support rendering
|
// Set up our async handler to support rendering
|
||||||
self.wakeup.setData(self);
|
self.wakeup.setData(self);
|
||||||
@ -199,14 +194,3 @@ fn renderCallback(h: *libuv.Timer) void {
|
|||||||
fn stopCallback(h: *libuv.Async) void {
|
fn stopCallback(h: *libuv.Async) void {
|
||||||
h.loop().stop();
|
h.loop().stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is unnecessary right now but is logic we'll need for when we
|
|
||||||
// abstract renderers out.
|
|
||||||
fn RendererType() type {
|
|
||||||
const self: Thread = undefined;
|
|
||||||
return switch (@typeInfo(@TypeOf(self.renderer))) {
|
|
||||||
.Pointer => |p| p.child,
|
|
||||||
.Struct => |s| s,
|
|
||||||
else => unreachable,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
19
src/renderer/cursor.zig
Normal file
19
src/renderer/cursor.zig
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const terminal = @import("../terminal/main.zig");
|
||||||
|
|
||||||
|
/// Available cursor styles for drawing that renderers must support.
|
||||||
|
pub const CursorStyle = enum {
|
||||||
|
box,
|
||||||
|
box_hollow,
|
||||||
|
bar,
|
||||||
|
|
||||||
|
/// Create a cursor style from the terminal style request.
|
||||||
|
pub fn fromTerminal(style: terminal.CursorStyle) ?CursorStyle {
|
||||||
|
return switch (style) {
|
||||||
|
.blinking_block, .steady_block => .box,
|
||||||
|
.blinking_bar, .steady_bar => .bar,
|
||||||
|
.blinking_underline, .steady_underline => null, // TODO
|
||||||
|
.default => .box,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
268
src/shaders/cell.metal
Normal file
268
src/shaders/cell.metal
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
using namespace metal;
|
||||||
|
|
||||||
|
// The possible modes that a shader can take.
|
||||||
|
enum Mode : uint8_t {
|
||||||
|
MODE_BG = 1u,
|
||||||
|
MODE_FG = 2u,
|
||||||
|
MODE_FG_COLOR = 7u,
|
||||||
|
MODE_CURSOR_RECT = 3u,
|
||||||
|
MODE_CURSOR_RECT_HOLLOW = 4u,
|
||||||
|
MODE_CURSOR_BAR = 5u,
|
||||||
|
MODE_UNDERLINE = 6u,
|
||||||
|
MODE_STRIKETHROUGH = 8u,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Uniforms {
|
||||||
|
float4x4 projection_matrix;
|
||||||
|
float2 cell_size;
|
||||||
|
float underline_position;
|
||||||
|
float underline_thickness;
|
||||||
|
float strikethrough_position;
|
||||||
|
float strikethrough_thickness;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VertexIn {
|
||||||
|
// The mode for this cell.
|
||||||
|
uint8_t mode [[ attribute(0) ]];
|
||||||
|
|
||||||
|
// The grid coordinates (x, y) where x < columns and y < rows
|
||||||
|
float2 grid_pos [[ attribute(1) ]];
|
||||||
|
|
||||||
|
// The width of the cell in cells (i.e. 2 for double-wide).
|
||||||
|
uint8_t cell_width [[ attribute(6) ]];
|
||||||
|
|
||||||
|
// The color. For BG modes, this is the bg color, for FG modes this is
|
||||||
|
// the text color. For styles, this is the color of the style.
|
||||||
|
uchar4 color [[ attribute(5) ]];
|
||||||
|
|
||||||
|
// The fields below are present only when rendering text.
|
||||||
|
|
||||||
|
// The position of the glyph in the texture (x,y)
|
||||||
|
uint2 glyph_pos [[ attribute(2) ]];
|
||||||
|
|
||||||
|
// The size of the glyph in the texture (w,h)
|
||||||
|
uint2 glyph_size [[ attribute(3) ]];
|
||||||
|
|
||||||
|
// The left and top bearings for the glyph (x,y)
|
||||||
|
int2 glyph_offset [[ attribute(4) ]];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VertexOut {
|
||||||
|
float4 position [[ position ]];
|
||||||
|
float2 cell_size;
|
||||||
|
uint8_t mode;
|
||||||
|
float4 color;
|
||||||
|
float2 tex_coord;
|
||||||
|
};
|
||||||
|
|
||||||
|
vertex VertexOut uber_vertex(
|
||||||
|
unsigned int vid [[ vertex_id ]],
|
||||||
|
VertexIn input [[ stage_in ]],
|
||||||
|
constant Uniforms &uniforms [[ buffer(1) ]]
|
||||||
|
) {
|
||||||
|
// Convert the grid x,y into world space x, y by accounting for cell size
|
||||||
|
float2 cell_pos = uniforms.cell_size * input.grid_pos;
|
||||||
|
|
||||||
|
// Scaled cell size for the cell width
|
||||||
|
float2 cell_size_scaled = uniforms.cell_size;
|
||||||
|
cell_size_scaled.x = cell_size_scaled.x * input.cell_width;
|
||||||
|
|
||||||
|
// Turn the cell position into a vertex point depending on the
|
||||||
|
// vertex ID. Since we use instanced drawing, we have 4 vertices
|
||||||
|
// for each corner of the cell. We can use vertex ID to determine
|
||||||
|
// which one we're looking at. Using this, we can use 1 or 0 to keep
|
||||||
|
// or discard the value for the vertex.
|
||||||
|
//
|
||||||
|
// 0 = top-right
|
||||||
|
// 1 = bot-right
|
||||||
|
// 2 = bot-left
|
||||||
|
// 3 = top-left
|
||||||
|
float2 position;
|
||||||
|
position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f;
|
||||||
|
position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f;
|
||||||
|
|
||||||
|
VertexOut out;
|
||||||
|
out.mode = input.mode;
|
||||||
|
out.cell_size = uniforms.cell_size;
|
||||||
|
out.color = float4(input.color) / 255.0f;
|
||||||
|
switch (input.mode) {
|
||||||
|
case MODE_BG:
|
||||||
|
// Calculate the final position of our cell in world space.
|
||||||
|
// We have to add our cell size since our vertices are offset
|
||||||
|
// one cell up and to the left. (Do the math to verify yourself)
|
||||||
|
cell_pos = cell_pos + cell_size_scaled * position;
|
||||||
|
|
||||||
|
out.position = uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MODE_FG:
|
||||||
|
case MODE_FG_COLOR: {
|
||||||
|
float2 glyph_size = float2(input.glyph_size);
|
||||||
|
float2 glyph_offset = float2(input.glyph_offset);
|
||||||
|
|
||||||
|
// If the glyph is larger than our cell, we need to downsample it.
|
||||||
|
// The "+ 3" here is to give some wiggle room for fonts that are
|
||||||
|
// BARELY over it.
|
||||||
|
float2 glyph_size_downsampled = glyph_size;
|
||||||
|
if (glyph_size_downsampled.y > cell_size_scaled.y + 2) {
|
||||||
|
// Magic 0.9 and 1.1 are padding to make emoji look better
|
||||||
|
glyph_size_downsampled.y = cell_size_scaled.y * 0.9;
|
||||||
|
glyph_size_downsampled.x = glyph_size.x * (glyph_size_downsampled.y / glyph_size.y);
|
||||||
|
glyph_offset.y = glyph_offset.y * 1.1 * (glyph_size_downsampled.y / glyph_size.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The glyph_offset.y is the y bearing, a y value that when added
|
||||||
|
// to the baseline is the offset (+y is up). Our grid goes down.
|
||||||
|
// So we flip it with `cell_size.y - glyph_offset.y`.
|
||||||
|
glyph_offset.y = cell_size_scaled.y - glyph_offset.y;
|
||||||
|
|
||||||
|
// Calculate the final position of the cell which uses our glyph size
|
||||||
|
// and glyph offset to create the correct bounding box for the glyph.
|
||||||
|
cell_pos = cell_pos + glyph_size_downsampled * position + glyph_offset;
|
||||||
|
out.position = uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
// Calculate the texture coordinate in pixels. This is NOT normalized
|
||||||
|
// (between 0.0 and 1.0) and must be done in the fragment shader.
|
||||||
|
out.tex_coord = float2(input.glyph_pos) + float2(input.glyph_size) * position;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MODE_CURSOR_RECT:
|
||||||
|
// Same as background since we're taking up the whole cell.
|
||||||
|
cell_pos = cell_pos + cell_size_scaled * position;
|
||||||
|
|
||||||
|
out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MODE_CURSOR_RECT_HOLLOW:
|
||||||
|
// Top-left position of this cell is needed for the hollow rect.
|
||||||
|
out.tex_coord = cell_pos;
|
||||||
|
|
||||||
|
// Same as background since we're taking up the whole cell.
|
||||||
|
cell_pos = cell_pos + cell_size_scaled * position;
|
||||||
|
out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MODE_CURSOR_BAR: {
|
||||||
|
// Make the bar a smaller version of our cell
|
||||||
|
float2 bar_size = float2(uniforms.cell_size.x * 0.2, uniforms.cell_size.y);
|
||||||
|
|
||||||
|
// Same as background since we're taking up the whole cell.
|
||||||
|
cell_pos = cell_pos + bar_size * position;
|
||||||
|
|
||||||
|
out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MODE_UNDERLINE: {
|
||||||
|
// Underline Y value is just our thickness
|
||||||
|
float2 underline_size = float2(cell_size_scaled.x, uniforms.underline_thickness);
|
||||||
|
|
||||||
|
// Position the underline where we are told to
|
||||||
|
float2 underline_offset = float2(cell_size_scaled.x, uniforms.underline_position);
|
||||||
|
|
||||||
|
// Go to the bottom of the cell, take away the size of the
|
||||||
|
// underline, and that is our position. We also float it slightly
|
||||||
|
// above the bottom.
|
||||||
|
cell_pos = cell_pos + underline_offset - (underline_size * position);
|
||||||
|
|
||||||
|
out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MODE_STRIKETHROUGH: {
|
||||||
|
// Strikethrough Y value is just our thickness
|
||||||
|
float2 strikethrough_size = float2(cell_size_scaled.x, uniforms.strikethrough_thickness);
|
||||||
|
|
||||||
|
// Position the strikethrough where we are told to
|
||||||
|
float2 strikethrough_offset = float2(cell_size_scaled.x, uniforms.strikethrough_position);
|
||||||
|
|
||||||
|
// Go to the bottom of the cell, take away the size of the
|
||||||
|
// strikethrough, and that is our position. We also float it slightly
|
||||||
|
// above the bottom.
|
||||||
|
cell_pos = cell_pos + strikethrough_offset - (strikethrough_size * position);
|
||||||
|
|
||||||
|
out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment float4 uber_fragment(
|
||||||
|
VertexOut in [[ stage_in ]],
|
||||||
|
texture2d<float> textureGreyscale [[ texture(0) ]],
|
||||||
|
texture2d<float> textureColor [[ texture(1) ]]
|
||||||
|
) {
|
||||||
|
constexpr sampler textureSampler(address::clamp_to_edge, filter::linear);
|
||||||
|
|
||||||
|
switch (in.mode) {
|
||||||
|
case MODE_BG:
|
||||||
|
return in.color;
|
||||||
|
|
||||||
|
case MODE_FG: {
|
||||||
|
// Normalize the texture coordinates to [0,1]
|
||||||
|
float2 size = float2(textureGreyscale.get_width(), textureGreyscale.get_height());
|
||||||
|
float2 coord = in.tex_coord / size;
|
||||||
|
|
||||||
|
float a = textureGreyscale.sample(textureSampler, coord).r;
|
||||||
|
return float4(in.color.rgb, in.color.a * a);
|
||||||
|
}
|
||||||
|
|
||||||
|
case MODE_FG_COLOR: {
|
||||||
|
// Normalize the texture coordinates to [0,1]
|
||||||
|
float2 size = float2(textureColor.get_width(), textureColor.get_height());
|
||||||
|
float2 coord = in.tex_coord / size;
|
||||||
|
return textureColor.sample(textureSampler, coord);
|
||||||
|
}
|
||||||
|
|
||||||
|
case MODE_CURSOR_RECT:
|
||||||
|
return in.color;
|
||||||
|
|
||||||
|
case MODE_CURSOR_RECT_HOLLOW: {
|
||||||
|
// Okay so yeah this is probably horrendously slow and a shader
|
||||||
|
// should never do this, but we only ever render a cursor for ONE
|
||||||
|
// rectangle so we take the slowdown for that one.
|
||||||
|
|
||||||
|
// We subtracted one from cell size because our coordinates start at 0.
|
||||||
|
// So a width of 50 means max pixel of 49.
|
||||||
|
float2 cell_size_coords = in.cell_size - 1;
|
||||||
|
|
||||||
|
// Apply padding
|
||||||
|
float2 padding = float2(1.0f, 1.0f);
|
||||||
|
cell_size_coords = cell_size_coords - (padding * 2);
|
||||||
|
float2 screen_cell_pos_padded = in.tex_coord + padding;
|
||||||
|
|
||||||
|
// Convert our frag coord to offset of this cell. We have to subtract
|
||||||
|
// 0.5 because the frag coord is in center pixels.
|
||||||
|
float2 cell_frag_coord = in.position.xy - screen_cell_pos_padded - 0.5;
|
||||||
|
|
||||||
|
// If the frag coords are in the bounds, then we color it.
|
||||||
|
const float eps = 0.1;
|
||||||
|
if (cell_frag_coord.x >= 0 && cell_frag_coord.y >= 0 &&
|
||||||
|
cell_frag_coord.x <= cell_size_coords.x &&
|
||||||
|
cell_frag_coord.y <= cell_size_coords.y) {
|
||||||
|
if (abs(cell_frag_coord.x) < eps ||
|
||||||
|
abs(cell_frag_coord.x - cell_size_coords.x) < eps ||
|
||||||
|
abs(cell_frag_coord.y) < eps ||
|
||||||
|
abs(cell_frag_coord.y - cell_size_coords.y) < eps) {
|
||||||
|
return in.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to no color.
|
||||||
|
return float4(0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
case MODE_CURSOR_BAR:
|
||||||
|
return in.color;
|
||||||
|
|
||||||
|
case MODE_UNDERLINE:
|
||||||
|
return in.color;
|
||||||
|
|
||||||
|
case MODE_STRIKETHROUGH:
|
||||||
|
return in.color;
|
||||||
|
}
|
||||||
|
}
|
@ -147,7 +147,7 @@ void main() {
|
|||||||
glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y;
|
glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y;
|
||||||
|
|
||||||
// Calculate the final position of the cell.
|
// Calculate the final position of the cell.
|
||||||
cell_pos = cell_pos + glyph_size_downsampled * position + glyph_offset_calc;
|
cell_pos = cell_pos + (glyph_size_downsampled * position) + glyph_offset_calc;
|
||||||
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
gl_Position = projection * vec4(cell_pos, cell_z, 1.0);
|
||||||
|
|
||||||
// We need to convert our texture position and size to normalized
|
// We need to convert our texture position and size to normalized
|
||||||
|
Reference in New Issue
Block a user