diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1ac39933..1e9b01889 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,9 +58,6 @@ jobs: nix_path: nixpkgs=channel:nixos-unstable - name: test - run: nix develop -c zig build test -fstage1 - - - name: test stage2 run: nix develop -c zig build test - name: Test Dynamic Build diff --git a/build.zig b/build.zig index 882b04a79..424bf08f6 100644 --- a/build.zig +++ b/build.zig @@ -223,13 +223,19 @@ fn addDeps( _ = try utf8proc.link(b, step); // 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); // 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 = .{ - .backends = &imgui_backends, + .backends = imgui_backends, .freetype = .{ .enabled = true }, }; diff --git a/pkg/imgui/build.zig b/pkg/imgui/build.zig index ddb0f4fb0..c67fd70d5 100644 --- a/pkg/imgui/build.zig +++ b/pkg/imgui/build.zig @@ -101,11 +101,17 @@ pub fn buildImgui( lib.addCSourceFiles(srcs, flags.items); if (opt.backends) |backends| { 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; const path = try std.fmt.bufPrint( &buf, - "{s}imgui/backends/imgui_impl_{s}.cpp", - .{ root, backend }, + "{s}imgui/backends/imgui_impl_{s}.{s}", + .{ root, backend, ext }, ); lib.addCSourceFile(path, flags.items); diff --git a/pkg/imgui/impl_glfw.zig b/pkg/imgui/impl_glfw.zig index cb27b86fb..d8dd00a37 100644 --- a/pkg/imgui/impl_glfw.zig +++ b/pkg/imgui/impl_glfw.zig @@ -13,6 +13,10 @@ pub const ImplGlfw = struct { 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 { return ImGui_ImplGlfw_Shutdown(); } @@ -23,6 +27,7 @@ pub const ImplGlfw = struct { extern "c" fn glfwGetError(?*const anyopaque) c_int; 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_NewFrame() void; }; diff --git a/pkg/imgui/impl_metal.zig b/pkg/imgui/impl_metal.zig new file mode 100644 index 000000000..3b228f611 --- /dev/null +++ b/pkg/imgui/impl_metal.zig @@ -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; +}; diff --git a/pkg/imgui/main.zig b/pkg/imgui/main.zig index 3a071a380..424e015cb 100644 --- a/pkg/imgui/main.zig +++ b/pkg/imgui/main.zig @@ -7,6 +7,7 @@ pub usingnamespace @import("io.zig"); pub usingnamespace @import("style.zig"); pub usingnamespace @import("impl_glfw.zig"); +pub usingnamespace @import("impl_metal.zig"); pub usingnamespace @import("impl_opengl3.zig"); test { diff --git a/pkg/objc/object.zig b/pkg/objc/object.zig index 11f152b67..b69b0ee22 100644 --- a/pkg/objc/object.zig +++ b/pkg/objc/object.zig @@ -54,7 +54,7 @@ pub const Object = struct { break :getter objc.sel(val); } else objc.sel(n); - self.msgSend(T, getter, .{}); + return self.msgSend(T, getter, .{}); } }; diff --git a/src/DevMode.zig b/src/DevMode.zig index 7e1e73842..7698aedcb 100644 --- a/src/DevMode.zig +++ b/src/DevMode.zig @@ -9,6 +9,7 @@ const assert = std.debug.assert; const Atlas = @import("Atlas.zig"); const Window = @import("Window.zig"); +const renderer = @import("renderer.zig"); /// If this is false, the rest of the terminal will be compiled without /// dev mode support at all. @@ -27,9 +28,10 @@ window: ?*Window = null, /// Update the state associated with the dev mode. This should generally /// only be called paired with a render since it otherwise wastes CPU /// cycles. +/// +/// Note: renderers should call their implementation "newFrame" functions +/// prior to this. pub fn update(self: *const DevMode) !void { - imgui.ImplOpenGL3.newFrame(); - imgui.ImplGlfw.newFrame(); imgui.newFrame(); 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 " ++ "font atlas currently."); + const Renderer = @TypeOf(window.renderer); if (imgui.treeNode("Atlas: Greyscale", .{ .default_open = true })) { defer imgui.treePop(); 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 })) { defer imgui.treePop(); 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); } } } diff --git a/src/Window.zig b/src/Window.zig index 02e8adc5c..41f493bc4 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -36,7 +36,7 @@ const log = std.log.scoped(.window); const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5); // The renderer implementation to use. -const Renderer = renderer.OpenGL; +const Renderer = renderer.Renderer; /// Allocator alloc: Allocator, diff --git a/src/main.zig b/src/main.zig index dd543d710..ebfc68dac 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ const fontconfig = @import("fontconfig"); const freetype = @import("freetype"); const harfbuzz = @import("harfbuzz"); const tracy = @import("tracy"); +const renderer = @import("renderer.zig"); const App = @import("App.zig"); const cli_args = @import("cli_args.zig"); @@ -19,6 +20,7 @@ pub fn main() !void { if (options.fontconfig) { log.info("dependency fontconfig={d}", .{fontconfig.version()}); } + log.info("renderer={}", .{renderer.Renderer}); const GPA = std.heap.GeneralPurposeAllocator(.{}); var gpa: ?GPA = gpa: { diff --git a/src/renderer.zig b/src/renderer.zig index a5d62a725..2f3018558 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -7,11 +7,22 @@ //! APIs. The renderers in this package assume that the renderer is already //! 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 const Metal = @import("renderer/Metal.zig"); pub const OpenGL = @import("renderer/OpenGL.zig"); pub const Thread = @import("renderer/Thread.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 { @import("std").testing.refAllDecls(@This()); } diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig new file mode 100644 index 000000000..168a171b1 --- /dev/null +++ b/src/renderer/Metal.zig @@ -0,0 +1,1207 @@ +//! Renderer implementation for Metal. +//! +//! Open questions: +//! +pub const Metal = @This(); + +const std = @import("std"); +const builtin = @import("builtin"); +const glfw = @import("glfw"); +const objc = @import("objc"); +const macos = @import("macos"); +const imgui = @import("imgui"); +const Atlas = @import("../Atlas.zig"); +const font = @import("../font/main.zig"); +const terminal = @import("../terminal/main.zig"); +const renderer = @import("../renderer.zig"); +const math = @import("../math.zig"); +const DevMode = @import("../DevMode.zig"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Terminal = terminal.Terminal; + +// Get native API access on certain platforms so we can do more customization. +const glfwNative = glfw.Native(.{ + .cocoa = builtin.os.tag == .macos, +}); + +const log = std.log.scoped(.metal); + +/// Allocator that can be used +alloc: std.mem.Allocator, + +/// Current cell dimensions for this grid. +cell_size: renderer.CellSize, + +/// Whether the cursor is visible or not. This is used to control cursor +/// blinking. +cursor_visible: bool, +cursor_style: renderer.CursorStyle, + +/// Default foreground color +foreground: terminal.color.RGB, + +/// Default background color +background: 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. +cells: std.ArrayListUnmanaged(GPUCell), + +/// The current GPU uniform values. +uniforms: GPUUniforms, + +/// The font structures. +font_group: *font.GroupCache, +font_shaper: font.Shaper, + +/// Metal objects +device: objc.Object, // MTLDevice +queue: objc.Object, // MTLCommandQueue +swapchain: objc.Object, // CAMetalLayer +buf_cells: objc.Object, // MTLBuffer +buf_instance: objc.Object, // MTLBuffer +pipeline: objc.Object, // MTLRenderPipelineState +texture_greyscale: objc.Object, // MTLTexture +texture_color: objc.Object, // MTLTexture + +const GPUCell = extern struct { + mode: GPUCellMode, + grid_pos: [2]f32, + cell_width: u8, + color: [4]u8, + glyph_pos: [2]u32 = .{ 0, 0 }, + glyph_size: [2]u32 = .{ 0, 0 }, + glyph_offset: [2]i32 = .{ 0, 0 }, +}; + +const GPUUniforms = extern struct { + /// The projection matrix for turning world coordinates to normalized. + /// This is calculated based on the size of the screen. + projection_matrix: math.Mat, + + /// Size of a single cell in pixels, unscaled. + cell_size: [2]f32, + + /// Metrics for underline/strikethrough + underline_position: f32, + underline_thickness: f32, + strikethrough_position: f32, + strikethrough_thickness: f32, +}; + +const GPUCellMode = enum(u8) { + bg = 1, + fg = 2, + fg_color = 7, + cursor_rect = 3, + cursor_rect_hollow = 4, + cursor_bar = 5, + underline = 6, + strikethrough = 8, + + pub fn fromCursor(cursor: renderer.CursorStyle) GPUCellMode { + return switch (cursor) { + .box => .cursor_rect, + .box_hollow => .cursor_rect_hollow, + .bar => .cursor_bar, + }; + } +}; + +/// Returns the hints that we want for this +pub fn windowHints() glfw.Window.Hints { + return .{ + .client_api = .no_api, + // .cocoa_graphics_switching = builtin.os.tag == .macos, + // .cocoa_retina_framebuffer = true, + }; +} + +/// This is called early right after window creation to setup our +/// window surface as necessary. +pub fn windowInit(window: glfw.Window) !void { + _ = window; + + // We don't do anything else here because we want to set everything + // else up during actual initialization. +} + +pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { + // Initialize our metal stuff + const device = objc.Object.fromId(MTLCreateSystemDefaultDevice()); + const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); + const swapchain = swapchain: { + const CAMetalLayer = objc.Class.getClass("CAMetalLayer").?; + const swapchain = CAMetalLayer.msgSend(objc.Object, objc.sel("layer"), .{}); + swapchain.setProperty("device", device.value); + swapchain.setProperty("opaque", true); + + // disable v-sync + swapchain.setProperty("displaySyncEnabled", false); + + break :swapchain swapchain; + }; + + // Get our cell metrics based on a regular font ascii 'M'. Why 'M'? + // Doesn't matter, any normal ASCII will do we're just trying to make + // sure we use the regular font. + const metrics = metrics: { + const index = (try font_group.indexForCodepoint(alloc, 'M', .regular, .text)).?; + const face = try font_group.group.faceFromIndex(index); + break :metrics face.metrics; + }; + log.debug("cell dimensions={}", .{metrics}); + + // Create the font shaper. We initially create a shaper that can support + // a width of 160 which is a common width for modern screens to help + // avoid allocations later. + var shape_buf = try alloc.alloc(font.Shaper.Cell, 160); + errdefer alloc.free(shape_buf); + var font_shaper = try font.Shaper.init(shape_buf); + errdefer font_shaper.deinit(); + + // Initialize our Metal buffers + const buf_instance = buffer: { + const data = [6]u16{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }; + + break :buffer device.msgSend( + objc.Object, + objc.sel("newBufferWithBytes:length:options:"), + .{ + @ptrCast(*const anyopaque, &data), + @intCast(c_ulong, data.len * @sizeOf(u16)), + MTLResourceStorageModeShared, + }, + ); + }; + + const buf_cells = buffer: { + // Preallocate for 160x160 grid with 3 modes (bg, fg, text). This + // should handle most terminals well, and we can avoid a resize later. + const prealloc = 160 * 160 * 3; + + break :buffer device.msgSend( + objc.Object, + objc.sel("newBufferWithLength:options:"), + .{ + @intCast(c_ulong, prealloc * @sizeOf(GPUCell)), + MTLResourceStorageModeShared, + }, + ); + }; + + // Initialize our shader (MTLLibrary) + const library = try initLibrary(device, @embedFile("../shaders/cell.metal")); + const pipeline_state = try initPipelineState(device, library); + const texture_greyscale = try initAtlasTexture(device, &font_group.atlas_greyscale); + const texture_color = try initAtlasTexture(device, &font_group.atlas_color); + + return Metal{ + .alloc = alloc, + .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, + .background = .{ .r = 0, .g = 0, .b = 0 }, + .foreground = .{ .r = 255, .g = 255, .b = 255 }, + .cursor_visible = true, + .cursor_style = .box, + + // Render state + .cells = .{}, + .uniforms = .{ + .projection_matrix = undefined, + .cell_size = undefined, + .underline_position = metrics.underline_position, + .underline_thickness = metrics.underline_thickness, + .strikethrough_position = metrics.strikethrough_position, + .strikethrough_thickness = metrics.strikethrough_thickness, + }, + + // Fonts + .font_group = font_group, + .font_shaper = font_shaper, + + // Metal stuff + .device = device, + .queue = queue, + .swapchain = swapchain, + .buf_cells = buf_cells, + .buf_instance = buf_instance, + .pipeline = pipeline_state, + .texture_greyscale = texture_greyscale, + .texture_color = texture_color, + }; +} + +pub fn deinit(self: *Metal) void { + if (DevMode.enabled) { + imgui.ImplMetal.shutdown(); + imgui.ImplGlfw.shutdown(); + } + + self.cells.deinit(self.alloc); + + self.font_shaper.deinit(); + self.alloc.free(self.font_shaper.cell_buf); + + self.* = undefined; +} + +/// This is called just prior to spinning up the renderer thread for +/// final main thread setup requirements. +pub fn finalizeInit(self: *const Metal, window: glfw.Window) !void { + // Set our window backing layer to be our swapchain + const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(window).?); + const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?); + contentView.setProperty("layer", self.swapchain.value); + contentView.setProperty("wantsLayer", true); + + // Ensure that our metal layer has a content scale set to match the + // scale factor of the window. This avoids magnification issues leading + // to blurry rendering. + const layer = contentView.getProperty(objc.Object, "layer"); + const scaleFactor = nswindow.getProperty(macos.graphics.c.CGFloat, "backingScaleFactor"); + layer.setProperty("contentsScale", scaleFactor); + + if (DevMode.enabled) { + // Initialize for our window + assert(imgui.ImplGlfw.initForOther( + @ptrCast(*imgui.ImplGlfw.GLFWWindow, window.handle), + true, + )); + assert(imgui.ImplMetal.init(self.device.value)); + } +} + +/// Callback called by renderer.Thread when it begins. +pub fn threadEnter(self: *const Metal, window: glfw.Window) !void { + _ = self; + _ = window; + + // Metal requires no per-thread state. +} + +/// Callback called by renderer.Thread when it exits. +pub fn threadExit(self: *const Metal) void { + _ = self; + + // Metal requires no per-thread state. +} + +/// The primary render callback that is completely thread-safe. +pub fn render( + self: *Metal, + window: glfw.Window, + state: *renderer.State, +) !void { + _ = window; + + // Data we extract out of the critical area. + const Critical = struct { + bg: terminal.color.RGB, + screen_size: ?renderer.ScreenSize, + devmode: bool, + }; + + // Update all our data as tightly as possible within the mutex. + const critical: Critical = critical: { + state.mutex.lock(); + defer state.mutex.unlock(); + + // If we're resizing, then handle that now. + if (state.resize_screen) |size| try self.setScreenSize(size); + defer state.resize_screen = null; + + // Setup our cursor state + if (state.focused) { + self.cursor_visible = state.cursor.visible and !state.cursor.blink; + self.cursor_style = renderer.CursorStyle.fromTerminal(state.cursor.style) orelse .box; + } else { + self.cursor_visible = true; + self.cursor_style = .box_hollow; + } + + // Swap bg/fg if the terminal is reversed + const bg = self.background; + const fg = self.foreground; + defer { + self.background = bg; + self.foreground = fg; + } + if (state.terminal.modes.reverse_colors) { + self.background = fg; + self.foreground = bg; + } + + // Build our GPU cells + try self.rebuildCells(state.terminal); + + break :critical .{ + .bg = self.background, + .screen_size = state.resize_screen, + .devmode = if (state.devmode) |dm| dm.visible else false, + }; + }; + + // @autoreleasepool {} + const pool = objc_autoreleasePoolPush(); + defer objc_autoreleasePoolPop(pool); + + // If we're resizing, then we have to update a bunch of things... + if (critical.screen_size) |screen_size| { + const bounds = self.swapchain.getProperty(macos.graphics.Rect, "bounds"); + + // Scale the bounds based on the layer content scale so that we + // properly handle Retina. + const scaled: macos.graphics.Size = scaled: { + const scaleFactor = self.swapchain.getProperty(macos.graphics.c.CGFloat, "contentsScale"); + break :scaled .{ + .width = bounds.size.width * scaleFactor, + .height = bounds.size.height * scaleFactor, + }; + }; + + // Set the size of the drawable surface to the scaled bounds + self.swapchain.setProperty("drawableSize", scaled); + _ = screen_size; + //log.warn("bounds={} screen={} scaled={}", .{ bounds, screen_size, scaled }); + + // Setup our uniforms + const old = self.uniforms; + self.uniforms = .{ + .projection_matrix = math.ortho2d( + 0, + @floatCast(f32, scaled.width), + @floatCast(f32, scaled.height), + 0, + ), + .cell_size = .{ self.cell_size.width, self.cell_size.height }, + .underline_position = old.underline_position, + .underline_thickness = old.underline_thickness, + .strikethrough_position = old.strikethrough_position, + .strikethrough_thickness = old.strikethrough_thickness, + }; + } + + // Get our surface (CAMetalDrawable) + const surface = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); + + // Setup our buffers + try self.syncCells(); + + // 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; + } + + // MTLRenderPassDescriptor + const desc = desc: { + const MTLRenderPassDescriptor = objc.Class.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)}, + ); + + attachment.setProperty("loadAction", @enumToInt(MTLLoadAction.clear)); + attachment.setProperty("storeAction", @enumToInt(MTLStoreAction.store)); + attachment.setProperty("texture", surface.getProperty(objc.c.id, "texture").?); + attachment.setProperty("clearColor", MTLClearColor{ + .red = @intToFloat(f32, critical.bg.r) / 255, + .green = @intToFloat(f32, critical.bg.g) / 255, + .blue = @intToFloat(f32, critical.bg.b) / 255, + .alpha = 1.0, + }); + } + + break :desc desc; + }; + + // Command buffer (MTLCommandBuffer) + const buffer = self.queue.msgSend(objc.Object, objc.sel("commandBuffer"), .{}); + + { + // MTLRenderCommandEncoder + const encoder = buffer.msgSend( + objc.Object, + objc.sel("renderCommandEncoderWithDescriptor:"), + .{desc.value}, + ); + defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); + + //do we need to do this? + //encoder.msgSend(void, objc.sel("setViewport:"), .{viewport}); + + // Use our shader pipeline + encoder.msgSend(void, objc.sel("setRenderPipelineState:"), .{self.pipeline.value}); + + // Set our buffers + encoder.msgSend( + void, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ self.buf_cells.value, @as(c_ulong, 0), @as(c_ulong, 0) }, + ); + encoder.msgSend( + void, + objc.sel("setVertexBytes:length:atIndex:"), + .{ + @ptrCast(*const anyopaque, &self.uniforms), + @as(c_ulong, @sizeOf(@TypeOf(self.uniforms))), + @as(c_ulong, 1), + }, + ); + encoder.msgSend( + void, + objc.sel("setFragmentTexture:atIndex:"), + .{ + self.texture_greyscale.value, + @as(c_ulong, 0), + }, + ); + encoder.msgSend( + void, + objc.sel("setFragmentTexture:atIndex:"), + .{ + self.texture_color.value, + @as(c_ulong, 1), + }, + ); + + encoder.msgSend( + void, + objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), + .{ + @enumToInt(MTLPrimitiveType.triangle), + @as(c_ulong, 6), + @enumToInt(MTLIndexType.uint16), + self.buf_instance.value, + @as(c_ulong, 0), + @as(c_ulong, self.cells.items.len), + }, + ); + + // Build our devmode draw data. This sucks because it requires we + // lock our state mutex but the metal imgui implementation requires + // access to all this stuff. + if (critical.devmode) { + state.mutex.lock(); + defer state.mutex.unlock(); + + if (state.devmode) |dm| { + if (dm.visible) { + imgui.ImplMetal.newFrame(desc.value); + imgui.ImplGlfw.newFrame(); + try dm.update(); + imgui.ImplMetal.renderDrawData( + try dm.render(), + buffer.value, + encoder.value, + ); + } + } + } + } + + buffer.msgSend(void, objc.sel("presentDrawable:"), .{surface.value}); + buffer.msgSend(void, objc.sel("commit"), .{}); +} + +/// Resize the screen. +fn setScreenSize(self: *Metal, dim: renderer.ScreenSize) !void { + // Recalculate the rows/columns. + const grid_size = renderer.GridSize.init(dim, self.cell_size); + + // Update our shaper + // TODO: don't reallocate if it is close enough (but bigger) + var shape_buf = try self.alloc.alloc(font.Shaper.Cell, grid_size.columns * 2); + errdefer self.alloc.free(shape_buf); + self.alloc.free(self.font_shaper.cell_buf); + self.font_shaper.cell_buf = shape_buf; + + log.debug("screen size screen={} grid={}, cell={}", .{ dim, grid_size, self.cell_size }); +} + +/// Sync all the CPU cells with the GPU state (but still on the CPU here). +/// This builds all our "GPUCells" on this struct, but doesn't send them +/// down to the GPU yet. +fn rebuildCells(self: *Metal, term: *Terminal) !void { + // Over-allocate just to ensure we don't allocate again during loops. + self.cells.clearRetainingCapacity(); + try self.cells.ensureTotalCapacity( + self.alloc, + + // * 3 for background modes and cursor and underlines + // + 1 for cursor + (term.screen.rows * term.screen.cols * 3) + 1, + ); + + // 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; + + // Build each cell + var rowIter = term.screen.rowIterator(.viewport); + var y: usize = 0; + while (rowIter.next()) |row| { + defer y += 1; + + // If this is the row with our cursor, then we may have to modify + // the cell with the cursor. + const start_i: usize = self.cells.items.len; + defer if (self.cursor_visible and + self.cursor_style == .box and + term.screen.viewportIsBottom() and + y == term.screen.cursor.y) + { + for (self.cells.items[start_i..]) |cell| { + if (cell.grid_pos[0] == @intToFloat(f32, term.screen.cursor.x) and + cell.mode == .fg) + { + cursor_cell = cell; + break; + } + } + }; + + // Split our row into runs and shape each one. + var iter = self.font_shaper.runIterator(self.font_group, row); + while (try iter.next(self.alloc)) |run| { + for (try self.font_shaper.shape(run)) |shaper_cell| { + assert(try self.updateCell( + term, + row.getCell(shaper_cell.x), + shaper_cell, + run, + shaper_cell.x, + y, + )); + } + } + + // Set row is not dirty anymore + row.setDirty(false); + } + + // Add the cursor at the end so that it overlays everything. If we have + // a cursor cell then we invert the colors on that and add it in so + // that we can always see it. + self.addCursor(term); + if (cursor_cell) |*cell| { + cell.color = .{ 0, 0, 0, 255 }; + self.cells.appendAssumeCapacity(cell.*); + } +} + +pub fn updateCell( + self: *Metal, + term: *Terminal, + cell: terminal.Screen.Cell, + shaper_cell: font.Shaper.Cell, + shaper_run: font.Shaper.TextRun, + x: usize, + y: usize, +) !bool { + const BgFg = struct { + /// Background is optional because in un-inverted mode + /// it may just be equivalent to the default background in + /// which case we do nothing to save on GPU render time. + bg: ?terminal.color.RGB, + + /// Fg is always set to some color, though we may not render + /// any fg if the cell is empty or has no attributes like + /// underline. + fg: terminal.color.RGB, + }; + + // The colors for the cell. + const colors: BgFg = colors: { + // If we have a selection, then we need to check if this + // cell is selected. + // TODO(perf): we can check in advance if selection is in + // our viewport at all and not run this on every point. + if (term.selection) |sel| { + const screen_point = (terminal.point.Viewport{ + .x = x, + .y = y, + }).toScreen(&term.screen); + + // If we are selected, we our colors are just inverted fg/bg + if (sel.contains(screen_point)) { + break :colors BgFg{ + .bg = self.foreground, + .fg = self.background, + }; + } + } + + const res: BgFg = if (!cell.attrs.inverse) .{ + // In normal mode, background and fg match the cell. We + // un-optionalize the fg by defaulting to our fg color. + .bg = if (cell.attrs.has_bg) cell.bg else null, + .fg = if (cell.attrs.has_fg) cell.fg else self.foreground, + } else .{ + // In inverted mode, the background MUST be set to something + // (is never null) so it is either the fg or default fg. The + // fg is either the bg or default background. + .bg = if (cell.attrs.has_fg) cell.fg else self.foreground, + .fg = if (cell.attrs.has_bg) cell.bg else self.background, + }; + break :colors res; + }; + + // Alpha multiplier + const alpha: u8 = if (cell.attrs.faint) 175 else 255; + + // If the cell has a background, we always draw it. + if (colors.bg) |rgb| { + self.cells.appendAssumeCapacity(.{ + .mode = .bg, + .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + .cell_width = cell.widthLegacy(), + .color = .{ rgb.r, rgb.g, rgb.b, alpha }, + }); + } + + // If the cell has a character, draw it + if (cell.char > 0) { + // Render + const glyph = try self.font_group.renderGlyph( + self.alloc, + shaper_run.font_index, + shaper_cell.glyph_index, + @floatToInt(u16, @ceil(self.cell_size.height)), + ); + + // If we're rendering a color font, we use the color atlas + const face = try self.font_group.group.faceFromIndex(shaper_run.font_index); + const mode: GPUCellMode = if (face.presentation == .emoji) .fg_color else .fg; + + self.cells.appendAssumeCapacity(.{ + .mode = mode, + .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + .cell_width = cell.widthLegacy(), + .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, + .glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }, + .glyph_size = .{ glyph.width, glyph.height }, + .glyph_offset = .{ glyph.offset_x, glyph.offset_y }, + }); + } + + if (cell.attrs.underline) { + self.cells.appendAssumeCapacity(.{ + .mode = .underline, + .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + .cell_width = cell.widthLegacy(), + .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, + }); + } + + if (cell.attrs.strikethrough) { + self.cells.appendAssumeCapacity(.{ + .mode = .strikethrough, + .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + .cell_width = cell.widthLegacy(), + .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, + }); + } + + return true; +} + +fn addCursor(self: *Metal, term: *Terminal) void { + // Add the cursor + if (self.cursor_visible and term.screen.viewportIsBottom()) { + const cell = term.screen.getCell( + .active, + term.screen.cursor.y, + term.screen.cursor.x, + ); + + self.cells.appendAssumeCapacity(.{ + .mode = GPUCellMode.fromCursor(self.cursor_style), + .grid_pos = .{ + @intToFloat(f32, term.screen.cursor.x), + @intToFloat(f32, term.screen.cursor.y), + }, + .cell_width = if (cell.attrs.wide) 2 else 1, + .color = .{ 0xFF, 0xFF, 0xFF, 0xFF }, + }); + } +} + +/// Sync the vertex buffer inputs to the GPU. This will attempt to reuse +/// the existing buffer (of course!) but will allocate a new buffer if +/// our cells don't fit in it. +fn syncCells(self: *Metal) !void { + const req_bytes = self.cells.items.len * @sizeOf(GPUCell); + const avail_bytes = self.buf_cells.getProperty(c_ulong, "length"); + + // If we need more bytes than our buffer has, we need to reallocate. + if (req_bytes > avail_bytes) { + // Deallocate previous buffer + deinitMTLResource(self.buf_cells); + + // Allocate a new buffer with enough to hold double what we require. + const size = req_bytes * 2; + self.buf_cells = self.device.msgSend( + objc.Object, + objc.sel("newBufferWithLength:options:"), + .{ + @intCast(c_ulong, size * @sizeOf(GPUCell)), + MTLResourceStorageModeShared, + }, + ); + } + + // We can fit within the vertex buffer so we can just replace bytes. + const ptr = self.buf_cells.msgSend(?[*]u8, objc.sel("contents"), .{}) orelse { + log.warn("buf_cells contents ptr is null", .{}); + return error.MetalFailed; + }; + + @memcpy(ptr, @ptrCast([*]const u8, self.cells.items.ptr), req_bytes); +} + +/// Sync the atlas data to the given texture. This copies the bytes +/// associated with the atlas to the given texture. If the atlas no longer +/// fits into the texture, the texture will be resized. +fn syncAtlasTexture(device: objc.Object, atlas: *const Atlas, texture: *objc.Object) !void { + const width = texture.getProperty(c_ulong, "width"); + if (atlas.size > width) { + // Free our old texture + deinitMTLResource(texture.*); + + // Reallocate + texture.* = try initAtlasTexture(device, atlas); + } + + texture.msgSend( + void, + objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"), + .{ + MTLRegion{ + .origin = .{ .x = 0, .y = 0, .z = 0 }, + .size = .{ + .width = @intCast(c_ulong, atlas.size), + .height = @intCast(c_ulong, atlas.size), + .depth = 1, + }, + }, + @as(c_ulong, 0), + atlas.data.ptr, + @as(c_ulong, atlas.format.depth() * atlas.size), + }, + ); +} + +/// Initialize the shader library. +fn initLibrary(device: objc.Object, data: []const u8) !objc.Object { + const source = try macos.foundation.String.createWithBytes( + data, + .utf8, + false, + ); + defer source.release(); + + var err: ?*anyopaque = null; + const library = device.msgSend( + objc.Object, + objc.sel("newLibraryWithSource:options:error:"), + .{ + source, + @as(?*anyopaque, null), + &err, + }, + ); + try checkError(err); + + return library; +} + +/// Initialize the render pipeline for our shader library. +fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object { + // Get our vertex and fragment functions + const func_vert = func_vert: { + const str = try macos.foundation.String.createWithBytes( + "uber_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( + "uber_fragment", + .utf8, + false, + ); + defer str.release(); + + const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + break :func_frag objc.Object.fromId(ptr.?); + }; + + // Create the vertex descriptor. The vertex descriptor describves the + // data layout of the vertex inputs. We use indexed (or "instanced") + // rendering, so this makes it so that each instance gets a single + // GPUCell as input. + const vertex_desc = vertex_desc: { + const desc = init: { + const Class = objc.Class.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; + }; + + // Our attributes are the fields of the input + const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes")); + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + attr.setProperty("format", @enumToInt(MTLVertexFormat.uchar)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "mode"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 1)}, + ); + + attr.setProperty("format", @enumToInt(MTLVertexFormat.float2)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "grid_pos"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 2)}, + ); + + attr.setProperty("format", @enumToInt(MTLVertexFormat.uint2)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "glyph_pos"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 3)}, + ); + + attr.setProperty("format", @enumToInt(MTLVertexFormat.uint2)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "glyph_size"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 4)}, + ); + + attr.setProperty("format", @enumToInt(MTLVertexFormat.int2)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "glyph_offset"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 5)}, + ); + + attr.setProperty("format", @enumToInt(MTLVertexFormat.uchar4)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "color"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 6)}, + ); + + attr.setProperty("format", @enumToInt(MTLVertexFormat.uchar)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(GPUCell, "cell_width"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + + // The layout describes how and when we fetch the next vertex input. + const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); + { + const layout = layouts.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + // Access each GPUCell per instance, not per vertex. + layout.setProperty("stepFunction", @enumToInt(MTLVertexStepFunction.per_instance)); + layout.setProperty("stride", @as(c_ulong, @sizeOf(GPUCell))); + } + + break :vertex_desc desc; + }; + + // Create our descriptor + const desc = init: { + const Class = objc.Class.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; + }; + + // Set our properties + desc.setProperty("vertexFunction", func_vert); + desc.setProperty("fragmentFunction", func_frag); + desc.setProperty("vertexDescriptor", vertex_desc); + + // 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)); + + // Blending. This is required so that our text we render on top + // of our drawable properly blends into the bg. + attachment.setProperty("blendingEnabled", true); + attachment.setProperty("rgbBlendOperation", @enumToInt(MTLBlendOperation.add)); + attachment.setProperty("alphaBlendOperation", @enumToInt(MTLBlendOperation.add)); + attachment.setProperty("sourceRGBBlendFactor", @enumToInt(MTLBlendFactor.source_alpha)); + attachment.setProperty("sourceAlphaBlendFactor", @enumToInt(MTLBlendFactor.source_alpha)); + attachment.setProperty("destinationRGBBlendFactor", @enumToInt(MTLBlendFactor.one_minus_source_alpha)); + attachment.setProperty("destinationAlphaBlendFactor", @enumToInt(MTLBlendFactor.one_minus_source_alpha)); + } + + // 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 a MTLTexture object for the given atlas. +fn initAtlasTexture(device: objc.Object, atlas: *const Atlas) !objc.Object { + // Determine our pixel format + const pixel_format: MTLPixelFormat = switch (atlas.format) { + .greyscale => .r8unorm, + .rgba => .bgra8unorm, + else => @panic("unsupported atlas format for Metal texture"), + }; + + // Create our descriptor + const desc = init: { + const Class = objc.Class.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; + }; + + // Set our properties + desc.setProperty("pixelFormat", @enumToInt(pixel_format)); + desc.setProperty("width", @intCast(c_ulong, atlas.size)); + desc.setProperty("height", @intCast(c_ulong, atlas.size)); + + // Initialize + const id = device.msgSend( + ?*anyopaque, + objc.sel("newTextureWithDescriptor:"), + .{desc}, + ) orelse return error.MetalFailed; + + return objc.Object.fromId(id); +} + +/// Deinitialize a metal resource (buffer, texture, etc.) and free the +/// memory associated with it. +fn deinitMTLResource(obj: objc.Object) void { + obj.msgSend(void, objc.sel("setPurgeableState:"), .{@enumToInt(MTLPurgeableState.empty)}); + obj.msgSend(void, objc.sel("release"), .{}); +} + +fn checkError(err_: ?*anyopaque) !void { + if (err_) |err| { + const nserr = objc.Object.fromId(err); + const str = @ptrCast( + *macos.foundation.String, + nserr.getProperty(?*anyopaque, "localizedDescription").?, + ); + + log.err("metal error={s}", .{str.cstringPtr(.ascii).?}); + return error.MetalFailed; + } +} + +/// https://developer.apple.com/documentation/metal/mtlloadaction?language=objc +const MTLLoadAction = enum(c_ulong) { + dont_care = 0, + load = 1, + clear = 2, +}; + +/// https://developer.apple.com/documentation/metal/mtlstoreaction?language=objc +const MTLStoreAction = enum(c_ulong) { + dont_care = 0, + store = 1, +}; + +/// https://developer.apple.com/documentation/metal/mtlstoragemode?language=objc +const MTLStorageMode = enum(c_ulong) { + shared = 0, + managed = 1, + private = 2, + memoryless = 3, +}; + +/// https://developer.apple.com/documentation/metal/mtlprimitivetype?language=objc +const MTLPrimitiveType = enum(c_ulong) { + point = 0, + line = 1, + line_strip = 2, + triangle = 3, + triangle_strip = 4, +}; + +/// https://developer.apple.com/documentation/metal/mtlindextype?language=objc +const MTLIndexType = enum(c_ulong) { + uint16 = 0, + uint32 = 1, +}; + +/// https://developer.apple.com/documentation/metal/mtlvertexformat?language=objc +const MTLVertexFormat = enum(c_ulong) { + uchar4 = 3, + float2 = 29, + int2 = 33, + uint2 = 37, + uchar = 45, +}; + +/// https://developer.apple.com/documentation/metal/mtlvertexstepfunction?language=objc +const MTLVertexStepFunction = enum(c_ulong) { + constant = 0, + per_vertex = 1, + per_instance = 2, +}; + +/// https://developer.apple.com/documentation/metal/mtlpixelformat?language=objc +const MTLPixelFormat = enum(c_ulong) { + r8unorm = 10, + bgra8unorm = 80, +}; + +/// https://developer.apple.com/documentation/metal/mtlpurgeablestate?language=objc +const MTLPurgeableState = enum(c_ulong) { + empty = 4, +}; + +/// https://developer.apple.com/documentation/metal/mtlblendfactor?language=objc +const MTLBlendFactor = enum(c_ulong) { + zero = 0, + one = 1, + source_color = 2, + one_minus_source_color = 3, + source_alpha = 4, + one_minus_source_alpha = 5, + dest_color = 6, + one_minus_dest_color = 7, + dest_alpha = 8, + one_minus_dest_alpha = 9, + source_alpha_saturated = 10, + blend_color = 11, + one_minus_blend_color = 12, + blend_alpha = 13, + one_minus_blend_alpha = 14, + source_1_color = 15, + one_minus_source_1_color = 16, + source_1_alpha = 17, + one_minus_source_1_alpha = 18, +}; + +/// https://developer.apple.com/documentation/metal/mtlblendoperation?language=objc +const MTLBlendOperation = enum(c_ulong) { + add = 0, + subtract = 1, + reverse_subtract = 2, + min = 3, + max = 4, +}; + +/// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc +/// (incomplete, we only use this mode so we just hardcode it) +const MTLResourceStorageModeShared: c_ulong = @enumToInt(MTLStorageMode.shared) << 4; + +const MTLClearColor = extern struct { + red: f64, + green: f64, + blue: f64, + alpha: f64, +}; + +const MTLViewport = extern struct { + x: f64, + y: f64, + width: f64, + height: f64, + znear: f64, + zfar: f64, +}; + +const MTLRegion = extern struct { + origin: MTLOrigin, + size: MTLSize, +}; + +const MTLOrigin = extern struct { + x: c_ulong, + y: c_ulong, + z: c_ulong, +}; + +const MTLSize = extern struct { + width: c_ulong, + height: c_ulong, + depth: c_ulong, +}; + +extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque; +extern "c" fn objc_autoreleasePoolPush() ?*anyopaque; +extern "c" fn objc_autoreleasePoolPop(?*anyopaque) void; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index eaf2d319a..ee10e17a0 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -66,7 +66,7 @@ font_shaper: font.Shaper, /// Whether the cursor is visible or not. This is used to control cursor /// blinking. cursor_visible: bool, -cursor_style: CursorStyle, +cursor_style: renderer.CursorStyle, /// Default foreground color foreground: terminal.color.RGB, @@ -74,25 +74,6 @@ foreground: terminal.color.RGB, /// Default background color 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. /// This must be "extern" so that the field order is not reordered by the /// Zig compiler. @@ -145,6 +126,14 @@ const GPUCellMode = enum(u8) { // 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. pub fn mask(self: GPUCellMode, m: GPUCellMode) GPUCellMode { return @intToEnum( @@ -468,7 +457,7 @@ pub fn render( // Setup our cursor state if (state.focused) { 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 { self.cursor_visible = true; self.cursor_style = .box_hollow; @@ -494,6 +483,8 @@ pub fn render( const devmode_data = devmode_data: { if (state.devmode) |dm| { if (dm.visible) { + imgui.ImplOpenGL3.newFrame(); + imgui.ImplGlfw.newFrame(); try dm.update(); break :devmode_data try dm.render(); } @@ -701,13 +692,8 @@ fn addCursor(self: *OpenGL, term: *Terminal) void { term.screen.cursor.x, ); - var mode: GPUCellMode = @intToEnum( - GPUCellMode, - @enumToInt(self.cursor_style), - ); - self.cells.appendAssumeCapacity(.{ - .mode = mode, + .mode = GPUCellMode.fromCursor(self.cursor_style), .grid_col = @intCast(u16, term.screen.cursor.x), .grid_row = @intCast(u16, term.screen.cursor.y), .grid_width = if (cell.attrs.wide) 2 else 1, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 9a6b71605..5fb10420a 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -31,7 +31,7 @@ render_h: libuv.Timer, window: glfw.Window, /// The underlying renderer implementation. -renderer: *renderer.OpenGL, +renderer: *renderer.Renderer, /// Pointer to the shared state that is used to generate the final render. state: *renderer.State, @@ -42,7 +42,7 @@ state: *renderer.State, pub fn init( alloc: Allocator, window: glfw.Window, - renderer_impl: *renderer.OpenGL, + renderer_impl: *renderer.Renderer, state: *renderer.State, ) !Thread { // 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 { - // 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 // renderers have to do per-thread setup. For example, OpenGL has to set // some thread-local state since that is how it works. - const Renderer = RendererType(); - if (@hasDecl(Renderer, "threadEnter")) try self.renderer.threadEnter(self.window); - defer if (@hasDecl(Renderer, "threadExit")) self.renderer.threadExit(); + try self.renderer.threadEnter(self.window); + defer self.renderer.threadExit(); // Set up our async handler to support rendering self.wakeup.setData(self); @@ -199,14 +194,3 @@ fn renderCallback(h: *libuv.Timer) void { fn stopCallback(h: *libuv.Async) void { 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, - }; -} diff --git a/src/renderer/cursor.zig b/src/renderer/cursor.zig new file mode 100644 index 000000000..df0ab1bc4 --- /dev/null +++ b/src/renderer/cursor.zig @@ -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, + }; + } +}; diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal new file mode 100644 index 000000000..763e93ddf --- /dev/null +++ b/src/shaders/cell.metal @@ -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 textureGreyscale [[ texture(0) ]], + texture2d 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; + } +} diff --git a/src/shaders/cell.v.glsl b/src/shaders/cell.v.glsl index 756b0ecd7..b0c94a31f 100644 --- a/src/shaders/cell.v.glsl +++ b/src/shaders/cell.v.glsl @@ -147,7 +147,7 @@ void main() { glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y; // 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); // We need to convert our texture position and size to normalized