From 07271a6cfdcd96c8cc5abe3d75ffecd166a660d2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 28 Oct 2022 14:48:36 -0700 Subject: [PATCH 01/24] Initial metal abstraction (noop) --- src/Window.zig | 2 +- src/renderer.zig | 10 +++++ src/renderer/Metal.zig | 86 +++++++++++++++++++++++++++++++++++++++++ src/renderer/Thread.zig | 24 ++---------- 4 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 src/renderer/Metal.zig 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/renderer.zig b/src/renderer.zig index a5d62a725..8d3e899a4 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -7,11 +7,21 @@ //! 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/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..53765bb21 --- /dev/null +++ b/src/renderer/Metal.zig @@ -0,0 +1,86 @@ +//! Renderer implementation for Metal. +pub const Metal = @This(); + +const std = @import("std"); +const glfw = @import("glfw"); +const font = @import("../font/main.zig"); +const terminal = @import("../terminal/main.zig"); +const renderer = @import("../renderer.zig"); +const Allocator = std.mem.Allocator; + +const log = std.log.scoped(.metal); + +/// Current cell dimensions for this grid. +cell_size: renderer.CellSize, + +/// Default foreground color +foreground: terminal.color.RGB, + +/// Default background color +background: terminal.color.RGB, + +/// 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; +} + +pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { + // 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}); + + return Metal{ + .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, + .background = .{ .r = 0, .g = 0, .b = 0 }, + .foreground = .{ .r = 255, .g = 255, .b = 255 }, + }; +} + +pub fn deinit(self: *Metal) void { + 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 { + _ = self; + _ = window; +} + +/// Callback called by renderer.Thread when it begins. +pub fn threadEnter(self: *const Metal, window: glfw.Window) !void { + _ = self; + _ = window; +} + +/// Callback called by renderer.Thread when it exits. +pub fn threadExit(self: *const Metal) void { + _ = self; +} + +/// The primary render callback that is completely thread-safe. +pub fn render( + self: *Metal, + window: glfw.Window, + state: *renderer.State, +) !void { + _ = self; + _ = window; + _ = state; +} 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, - }; -} From 090bab6798ff106238b9d2a9942e6f0845e95427 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 28 Oct 2022 15:46:05 -0700 Subject: [PATCH 02/24] metal: setup the render loop, draw the background color --- build.zig | 5 +- pkg/objc/object.zig | 2 +- src/renderer/Metal.zig | 146 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 147 insertions(+), 6 deletions(-) diff --git a/build.zig b/build.zig index 882b04a79..4c77fb92a 100644 --- a/build.zig +++ b/build.zig @@ -223,7 +223,10 @@ 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 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/renderer/Metal.zig b/src/renderer/Metal.zig index 53765bb21..6b5f2ea7d 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -2,12 +2,19 @@ pub const Metal = @This(); const std = @import("std"); +const builtin = @import("builtin"); const glfw = @import("glfw"); +const objc = @import("objc"); const font = @import("../font/main.zig"); const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); const Allocator = std.mem.Allocator; +// 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); /// Current cell dimensions for this grid. @@ -19,6 +26,11 @@ foreground: terminal.color.RGB, /// Default background color background: terminal.color.RGB, +/// Metal objects +device: objc.Object, // MTLDevice +queue: objc.Object, // MTLCommandQueue +swapchain: objc.Object, // CAMetalLayer + /// Returns the hints that we want for this pub fn windowHints() glfw.Window.Hints { return .{ @@ -32,9 +44,23 @@ pub fn windowHints() glfw.Window.Hints { /// 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); + 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. @@ -49,6 +75,9 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, .background = .{ .r = 0, .g = 0, .b = 0 }, .foreground = .{ .r = 255, .g = 255, .b = 255 }, + .device = device, + .queue = queue, + .swapchain = swapchain, }; } @@ -59,19 +88,26 @@ pub fn deinit(self: *Metal) void { /// 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 { - _ = self; - _ = window; + // 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); } /// 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. @@ -80,7 +116,109 @@ pub fn render( window: glfw.Window, state: *renderer.State, ) !void { - _ = self; _ = window; - _ = state; + + // Data we extract out of the critical area. + const Critical = struct { + bg: terminal.color.RGB, + }; + + // Update all our data as tightly as possible within the mutex. + const critical: Critical = critical: { + state.mutex.lock(); + defer state.mutex.unlock(); + + // 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; + } + + break :critical .{ + .bg = self.background, + }; + }; + + // @autoreleasepool {} + const pool = objc_autoreleasePoolPush(); + defer objc_autoreleasePoolPop(pool); + + // Get our surface (CAMetalDrawable) + const surface = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); + + // MTLRenderPassDescriptor + const MTLRenderPassDescriptor = objc.Class.getClass("MTLRenderPassDescriptor").?; + const desc = desc: { + 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}, + ); + encoder.msgSend(void, objc.sel("endEncoding"), .{}); + + buffer.msgSend(void, objc.sel("presentDrawable:"), .{surface.value}); + buffer.msgSend(void, objc.sel("commit"), .{}); } + +/// 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, +}; + +const MTLClearColor = extern struct { + red: f64, + green: f64, + blue: f64, + alpha: f64, +}; + +extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque; +extern "c" fn objc_autoreleasePoolPush() ?*anyopaque; +extern "c" fn objc_autoreleasePoolPop(?*anyopaque) void; From 873afd042be616675421c19e9aac0e8a7a1fe9cd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 28 Oct 2022 15:52:25 -0700 Subject: [PATCH 03/24] don't test stage1 --- .github/workflows/test.yml | 3 --- 1 file changed, 3 deletions(-) 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 From 90a284e176f5db952c0ff736c8bb1dd330ee5307 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 29 Oct 2022 10:43:01 -0700 Subject: [PATCH 04/24] boilerplate for rendering --- src/renderer/Metal.zig | 214 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 213 insertions(+), 1 deletion(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 6b5f2ea7d..edb5f0aea 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -8,7 +8,9 @@ const objc = @import("objc"); const font = @import("../font/main.zig"); const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.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(.{ @@ -17,20 +19,38 @@ const glfwNative = glfw.Native(.{ 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, +/// The last screen size set. +screen_size: renderer.ScreenSize, + /// 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 font structures. +font_group: *font.GroupCache, +font_shaper: font.Shaper, + /// Metal objects device: objc.Object, // MTLDevice queue: objc.Object, // MTLCommandQueue swapchain: objc.Object, // CAMetalLayer +const GPUCell = extern struct { + foo: f64, +}; + /// Returns the hints that we want for this pub fn windowHints() glfw.Window.Hints { return .{ @@ -71,10 +91,29 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { }; 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(); + return Metal{ + .alloc = alloc, .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, + .screen_size = .{ .width = 0, .height = 0 }, .background = .{ .r = 0, .g = 0, .b = 0 }, .foreground = .{ .r = 255, .g = 255, .b = 255 }, + + // Render state + .cells = .{}, + + // Fonts + .font_group = font_group, + .font_shaper = font_shaper, + + // Metal stuff .device = device, .queue = queue, .swapchain = swapchain, @@ -82,6 +121,11 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { } pub fn deinit(self: *Metal) void { + self.cells.deinit(self.alloc); + + self.font_shaper.deinit(); + self.alloc.free(self.font_shaper.cell_buf); + self.* = undefined; } @@ -128,6 +172,10 @@ pub fn render( 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; + // Swap bg/fg if the terminal is reversed const bg = self.background; const fg = self.foreground; @@ -140,6 +188,9 @@ pub fn render( self.foreground = bg; } + // Build our GPU cells + try self.rebuildCells(state.terminal); + break :critical .{ .bg = self.background, }; @@ -193,12 +244,164 @@ pub fn render( objc.sel("renderCommandEncoderWithDescriptor:"), .{desc.value}, ); - encoder.msgSend(void, objc.sel("endEncoding"), .{}); + // If we are resizing we need to update the viewport + encoder.msgSend(void, objc.sel("setViewport:"), .{MTLViewport{ + .x = 0, + .y = 0, + .width = @intToFloat(f64, self.screen_size.width), + .height = @intToFloat(f64, self.screen_size.height), + .znear = 0, + .zfar = 1, + }}); + + // End our rendering and draw + encoder.msgSend(void, objc.sel("endEncoding"), .{}); 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 { + // Update our screen size + self.screen_size = dim; + + // Recalculate the rows/columns. + const grid_size = renderer.GridSize.init(self.screen_size, 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; +} + +/// 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, + ); + + // // Build each cell + // var rowIter = term.screen.rowIterator(.viewport); + // var y: usize = 0; + // while (rowIter.next()) |row| { + // defer y += 1; + // + // // 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); + // } +} + +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 { + _ = shaper_cell; + _ = shaper_run; + + 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(.{ + // .grid_col = @intCast(u16, x), + // .grid_row = @intCast(u16, y), + // .grid_width = cell.widthLegacy(), + // .fg_r = 0, + // .fg_g = 0, + // .fg_b = 0, + // .fg_a = 0, + // .bg_r = rgb.r, + // .bg_g = rgb.g, + // .bg_b = rgb.b, + // .bg_a = alpha, + // }); + // } + _ = alpha; + _ = colors; + + return true; +} + /// https://developer.apple.com/documentation/metal/mtlloadaction?language=objc const MTLLoadAction = enum(c_ulong) { dont_care = 0, @@ -219,6 +422,15 @@ const MTLClearColor = extern struct { alpha: f64, }; +const MTLViewport = extern struct { + x: f64, + y: f64, + width: f64, + height: f64, + znear: f64, + zfar: f64, +}; + extern "c" fn MTLCreateSystemDefaultDevice() ?*anyopaque; extern "c" fn objc_autoreleasePoolPush() ?*anyopaque; extern "c" fn objc_autoreleasePoolPop(?*anyopaque) void; From fc7e457098914dffe4bddc17534fb642d6ce1ea8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 29 Oct 2022 11:41:10 -0700 Subject: [PATCH 05/24] add first pass metal shader --- src/renderer/Metal.zig | 72 ++++++++++++++++++++++++++++++++++++++++++ src/shaders/cell.metal | 35 ++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/shaders/cell.metal diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index edb5f0aea..4d7e40dc0 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -5,6 +5,7 @@ const std = @import("std"); const builtin = @import("builtin"); const glfw = @import("glfw"); const objc = @import("objc"); +const macos = @import("macos"); const font = @import("../font/main.zig"); const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); @@ -46,6 +47,8 @@ font_shaper: font.Shaper, device: objc.Object, // MTLDevice queue: objc.Object, // MTLCommandQueue swapchain: objc.Object, // CAMetalLayer +library: objc.Object, // MTLLibrary +buf_instance: objc.Object, // MTLBuffer const GPUCell = extern struct { foo: f64, @@ -99,6 +102,61 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { var font_shaper = try font.Shaper.init(shape_buf); errdefer font_shaper.deinit(); + // Initialize our Metal buffers + const buf_instance = buffer: { + const data = [6]u8{ + 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(u8)), + MTLResourceStorageModeShared, + }, + ); + }; + + // Initialize our shader (MTLLibrary) + const library = library: { + // Load our source into a CFString + const source = try macos.foundation.String.createWithBytes( + @embedFile("../shaders/cell.metal"), + .utf8, + false, + ); + defer source.release(); + + // Compile + var err: ?*anyopaque = null; + const library = device.msgSend( + objc.Object, + objc.sel("newLibraryWithSource:options:error:"), + .{ + source, + @as(?*anyopaque, null), + &err, + }, + ); + + // If there is an error (shouldn't since we test), report it and exit. + if (err != null) { + const nserr = objc.Object.fromId(err); + const str = @ptrCast( + *macos.foundation.String, + nserr.getProperty(?*anyopaque, "localizedDescription").?, + ); + + log.err("shader error={s}", .{str.cstringPtr(.ascii).?}); + return error.MetalFailed; + } + + break :library library; + }; + return Metal{ .alloc = alloc, .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, @@ -117,6 +175,8 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { .device = device, .queue = queue, .swapchain = swapchain, + .library = library, + .buf_instance = buf_instance, }; } @@ -415,6 +475,18 @@ const MTLStoreAction = enum(c_ulong) { 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/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, diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal new file mode 100644 index 000000000..318e26dd8 --- /dev/null +++ b/src/shaders/cell.metal @@ -0,0 +1,35 @@ +vertex float4 basic_vertex(unsigned int vid [[ vertex_id ]]) { + // Where we are in the grid (x, y) where top-left is origin + float2 grid_coord = float2(0.0f, 0.0f); + + // The size of a single cell in pixels + float2 cell_size = float2(75.0f, 100.0f); + + // Convert the grid x,y into world space x, y by accounting for cell size + float2 cell_pos = cell_size * grid_coord; + + // 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; + + // 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 * position; + + return float4(cell_pos.x, cell_pos.y, 0.5f, 1.0f); +} + +fragment half4 basic_fragment() { + return half4(1.0); +} From 4d4c1790cbce4d28668be606963289cb22954928 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 29 Oct 2022 13:21:04 -0700 Subject: [PATCH 06/24] draw a triangle --- src/renderer/Metal.zig | 189 ++++++++++++++++++++++++++++++++++------- src/shaders/cell.metal | 15 +++- 2 files changed, 170 insertions(+), 34 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 4d7e40dc0..57e555863 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -26,9 +26,6 @@ alloc: std.mem.Allocator, /// Current cell dimensions for this grid. cell_size: renderer.CellSize, -/// The last screen size set. -screen_size: renderer.ScreenSize, - /// Default foreground color foreground: terminal.color.RGB, @@ -47,8 +44,9 @@ font_shaper: font.Shaper, device: objc.Object, // MTLDevice queue: objc.Object, // MTLCommandQueue swapchain: objc.Object, // CAMetalLayer -library: objc.Object, // MTLLibrary +buf_cells: objc.Object, // MTLBuffer buf_instance: objc.Object, // MTLBuffer +pipeline: objc.Object, // MTLRenderPipelineState const GPUCell = extern struct { foo: f64, @@ -104,7 +102,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { // Initialize our Metal buffers const buf_instance = buffer: { - const data = [6]u8{ + const data = [6]u16{ 0, 1, 3, // Top-left triangle 1, 2, 3, // Bottom-right triangle }; @@ -114,7 +112,25 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { objc.sel("newBufferWithBytes:length:options:"), .{ @ptrCast(*const anyopaque, &data), - @intCast(c_ulong, data.len * @sizeOf(u8)), + @intCast(c_ulong, data.len * @sizeOf(u16)), + MTLResourceStorageModeShared, + }, + ); + }; + + const buf_cells = buffer: { + const data = [9]f32{ + 0, 1, 0, + -1, -1, 0, + 1, -1, 0, + }; + + break :buffer device.msgSend( + objc.Object, + objc.sel("newBufferWithBytes:length:options:"), + .{ + @ptrCast(*const anyopaque, &data), + @intCast(c_ulong, data.len * @sizeOf(f32)), MTLResourceStorageModeShared, }, ); @@ -156,11 +172,70 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { break :library library; }; + const func_vert = func_vert: { + const str = try macos.foundation.String.createWithBytes( + "demo_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( + "basic_fragment", + .utf8, + false, + ); + defer str.release(); + + const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + break :func_frag objc.Object.fromId(ptr.?); + }; + + const pipeline_state = pipeline_state: { + // 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); + + // Set our color attachment + const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); + { + const attachment = attachments.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + // Value is MTLPixelFormatBGRA8Unorm + attachment.setProperty("pixelFormat", @as(c_ulong, 80)); + } + + // Make our state + var err: ?*anyopaque = null; + const pipeline_state = device.msgSend( + objc.Object, + objc.sel("newRenderPipelineStateWithDescriptor:error:"), + .{ desc, &err }, + ); + try checkError(err); + + break :pipeline_state pipeline_state; + }; return Metal{ .alloc = alloc, .cell_size = .{ .width = metrics.cell_width, .height = metrics.cell_height }, - .screen_size = .{ .width = 0, .height = 0 }, .background = .{ .r = 0, .g = 0, .b = 0 }, .foreground = .{ .r = 255, .g = 255, .b = 255 }, @@ -175,8 +250,9 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { .device = device, .queue = queue, .swapchain = swapchain, - .library = library, + .buf_cells = buf_cells, .buf_instance = buf_instance, + .pipeline = pipeline_state, }; } @@ -264,8 +340,8 @@ pub fn render( const surface = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); // MTLRenderPassDescriptor - const MTLRenderPassDescriptor = objc.Class.getClass("MTLRenderPassDescriptor").?; const desc = desc: { + const MTLRenderPassDescriptor = objc.Class.getClass("MTLRenderPassDescriptor").?; const desc = MTLRenderPassDescriptor.msgSend( objc.Object, objc.sel("renderPassDescriptor"), @@ -298,36 +374,59 @@ pub fn render( // 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}, - ); + { + // MTLRenderCommandEncoder + const encoder = buffer.msgSend( + objc.Object, + objc.sel("renderCommandEncoderWithDescriptor:"), + .{desc.value}, + ); + defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); - // If we are resizing we need to update the viewport - encoder.msgSend(void, objc.sel("setViewport:"), .{MTLViewport{ - .x = 0, - .y = 0, - .width = @intToFloat(f64, self.screen_size.width), - .height = @intToFloat(f64, self.screen_size.height), - .znear = 0, - .zfar = 1, - }}); + // 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) }, + ); + + // Draw + encoder.msgSend( + void, + objc.sel("drawPrimitives:vertexStart:vertexCount:instanceCount:"), + .{ + @enumToInt(MTLPrimitiveType.triangle), + @as(c_ulong, 0), + @as(c_ulong, 3), + @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, 1), + // }, + // ); + } - // End our rendering and draw - encoder.msgSend(void, objc.sel("endEncoding"), .{}); 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 { - // Update our screen size - self.screen_size = dim; - // Recalculate the rows/columns. - const grid_size = renderer.GridSize.init(self.screen_size, self.cell_size); + const grid_size = renderer.GridSize.init(dim, self.cell_size); // Update our shaper // TODO: don't reallocate if it is close enough (but bigger) @@ -335,6 +434,8 @@ fn setScreenSize(self: *Metal, dim: renderer.ScreenSize) !void { 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). @@ -462,6 +563,19 @@ pub fn updateCell( return true; } +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, @@ -483,6 +597,21 @@ const MTLStorageMode = enum(c_ulong) { 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/mtlresourceoptions?language=objc /// (incomplete, we only use this mode so we just hardcode it) const MTLResourceStorageModeShared: c_ulong = @enumToInt(MTLStorageMode.shared) << 4; diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index 318e26dd8..7a73cde0c 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -3,7 +3,8 @@ vertex float4 basic_vertex(unsigned int vid [[ vertex_id ]]) { float2 grid_coord = float2(0.0f, 0.0f); // The size of a single cell in pixels - float2 cell_size = float2(75.0f, 100.0f); + //float2 cell_size = float2(75.0f, 100.0f); + float2 cell_size = float2(1.0f, 1.0f); // Convert the grid x,y into world space x, y by accounting for cell size float2 cell_pos = cell_size * grid_coord; @@ -25,11 +26,17 @@ vertex float4 basic_vertex(unsigned int vid [[ vertex_id ]]) { // 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 * position; + cell_pos = cell_size * position; - return float4(cell_pos.x, cell_pos.y, 0.5f, 1.0f); + return float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); +} + +vertex float4 demo_vertex( + const device packed_float3* vertex_array [[ buffer(0) ]], + unsigned int vid [[ vertex_id ]]) { + return float4(vertex_array[vid], 1.0); } fragment half4 basic_fragment() { - return half4(1.0); + return half4(1.0, 0.0, 0.0, 1.0); } From a17a7426a3eeab240da2894aaac406d86f1590fe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 29 Oct 2022 13:55:52 -0700 Subject: [PATCH 07/24] draw a square, handle resize --- src/renderer/Metal.zig | 39 +++++++++++++++++++++++---------------- src/shaders/cell.metal | 8 +------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 57e555863..7dc2c1373 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -174,7 +174,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { }; const func_vert = func_vert: { const str = try macos.foundation.String.createWithBytes( - "demo_vertex", + "basic_vertex", .utf8, false, ); @@ -336,6 +336,10 @@ pub fn render( const pool = objc_autoreleasePoolPush(); defer objc_autoreleasePoolPop(pool); + // Ensure our layer size is always updated + const bounds = self.swapchain.getProperty(macos.graphics.Rect, "bounds"); + self.swapchain.setProperty("drawableSize", bounds.size); + // Get our surface (CAMetalDrawable) const surface = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); @@ -383,6 +387,9 @@ pub fn render( ); 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}); @@ -394,29 +401,29 @@ pub fn render( ); // Draw - encoder.msgSend( - void, - objc.sel("drawPrimitives:vertexStart:vertexCount:instanceCount:"), - .{ - @enumToInt(MTLPrimitiveType.triangle), - @as(c_ulong, 0), - @as(c_ulong, 3), - @as(c_ulong, 1), - }, - ); - // encoder.msgSend( // void, - // objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), + // objc.sel("drawPrimitives:vertexStart:vertexCount:instanceCount:"), // .{ // @enumToInt(MTLPrimitiveType.triangle), - // @as(c_ulong, 6), - // @enumToInt(MTLIndexType.uint16), - // self.buf_instance.value, // @as(c_ulong, 0), + // @as(c_ulong, 3), // @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, 1), + }, + ); } buffer.msgSend(void, objc.sel("presentDrawable:"), .{surface.value}); diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index 7a73cde0c..538018e04 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -26,17 +26,11 @@ vertex float4 basic_vertex(unsigned int vid [[ vertex_id ]]) { // 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_size * position; + cell_pos = cell_pos + cell_size * position; return float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); } -vertex float4 demo_vertex( - const device packed_float3* vertex_array [[ buffer(0) ]], - unsigned int vid [[ vertex_id ]]) { - return float4(vertex_array[vid], 1.0); -} - fragment half4 basic_fragment() { return half4(1.0, 0.0, 0.0, 1.0); } From ea0265f021ff14b816350d0412d795d47038590e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 29 Oct 2022 14:49:01 -0700 Subject: [PATCH 08/24] projection matrix, render cell --- src/renderer/Metal.zig | 28 ++++++++++++++++++++++++++++ src/shaders/cell.metal | 22 ++++++++++++++-------- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 7dc2c1373..80d4e3d02 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -9,6 +9,7 @@ const macos = @import("macos"); const font = @import("../font/main.zig"); const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); +const math = @import("../math.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Terminal = terminal.Terminal; @@ -52,6 +53,11 @@ const GPUCell = extern struct { foo: f64, }; +const GPUUniforms = extern struct { + projection_matrix: math.Mat, + cell_size: [2]f32, +}; + /// Returns the hints that we want for this pub fn windowHints() glfw.Window.Hints { return .{ @@ -340,6 +346,19 @@ pub fn render( const bounds = self.swapchain.getProperty(macos.graphics.Rect, "bounds"); self.swapchain.setProperty("drawableSize", bounds.size); + // Setup our uniforms + const uniforms: GPUUniforms = .{ + .projection_matrix = math.ortho2d( + 0, + @floatCast(f32, bounds.size.width), + @floatCast(f32, bounds.size.height), + 0, + ), + + // TODO: get content scale to scale these + .cell_size = .{ self.cell_size.width / 2, self.cell_size.height / 2 }, + }; + // Get our surface (CAMetalDrawable) const surface = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); @@ -399,6 +418,15 @@ pub fn render( 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, &uniforms), + @as(c_ulong, @sizeOf(@TypeOf(uniforms))), + @as(c_ulong, 1), + }, + ); // Draw // encoder.msgSend( diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index 538018e04..53ffc995c 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -1,13 +1,19 @@ -vertex float4 basic_vertex(unsigned int vid [[ vertex_id ]]) { +using namespace metal; + +struct Uniforms { + float4x4 projection_matrix; + float2 cell_size; +}; + +vertex float4 basic_vertex( + unsigned int vid [[ vertex_id ]], + constant Uniforms &uniforms [[ buffer(1) ]] +) { // Where we are in the grid (x, y) where top-left is origin float2 grid_coord = float2(0.0f, 0.0f); - // The size of a single cell in pixels - //float2 cell_size = float2(75.0f, 100.0f); - float2 cell_size = float2(1.0f, 1.0f); - // Convert the grid x,y into world space x, y by accounting for cell size - float2 cell_pos = cell_size * grid_coord; + float2 cell_pos = uniforms.cell_size * grid_coord; // Turn the cell position into a vertex point depending on the // vertex ID. Since we use instanced drawing, we have 4 vertices @@ -26,9 +32,9 @@ vertex float4 basic_vertex(unsigned int vid [[ vertex_id ]]) { // 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 * position; + cell_pos = cell_pos + uniforms.cell_size * position; - return float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); + return uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); } fragment half4 basic_fragment() { From 6b7ed3fefb66deb8dbec93ba6f977e05d9c2a677 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 29 Oct 2022 19:16:41 -0700 Subject: [PATCH 09/24] metal: use content scale when resizing --- src/renderer/Metal.zig | 49 ++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 80d4e3d02..aa30e815c 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -37,6 +37,9 @@ background: terminal.color.RGB, /// 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, @@ -247,6 +250,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { // Render state .cells = .{}, + .uniforms = undefined, // Fonts .font_group = font_group, @@ -307,6 +311,7 @@ pub fn render( // Data we extract out of the critical area. const Critical = struct { bg: terminal.color.RGB, + screen_size: ?renderer.ScreenSize, }; // Update all our data as tightly as possible within the mutex. @@ -335,6 +340,7 @@ pub fn render( break :critical .{ .bg = self.background, + .screen_size = state.resize_screen, }; }; @@ -342,22 +348,33 @@ pub fn render( const pool = objc_autoreleasePoolPush(); defer objc_autoreleasePoolPop(pool); - // Ensure our layer size is always updated - const bounds = self.swapchain.getProperty(macos.graphics.Rect, "bounds"); - self.swapchain.setProperty("drawableSize", bounds.size); + // 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"); - // Setup our uniforms - const uniforms: GPUUniforms = .{ - .projection_matrix = math.ortho2d( - 0, - @floatCast(f32, bounds.size.width), - @floatCast(f32, bounds.size.height), - 0, - ), + // Set the size of the drawable surface to the bounds of our surface. + self.swapchain.setProperty("drawableSize", bounds.size); - // TODO: get content scale to scale these - .cell_size = .{ self.cell_size.width / 2, self.cell_size.height / 2 }, - }; + // Our drawable surface is usually scaled so we need to figure + // out the scalem amount so our pixels are correct. + const scaleX = @floatCast(f32, bounds.size.width) / @intToFloat(f32, screen_size.width); + const scaleY = @floatCast(f32, bounds.size.height) / @intToFloat(f32, screen_size.height); + + // Setup our uniforms + self.uniforms = .{ + .projection_matrix = math.ortho2d( + 0, + @floatCast(f32, bounds.size.width), + @floatCast(f32, bounds.size.height), + 0, + ), + + .cell_size = .{ + self.cell_size.width * scaleX, + self.cell_size.height * scaleY, + }, + }; + } // Get our surface (CAMetalDrawable) const surface = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); @@ -422,8 +439,8 @@ pub fn render( void, objc.sel("setVertexBytes:length:atIndex:"), .{ - @ptrCast(*const anyopaque, &uniforms), - @as(c_ulong, @sizeOf(@TypeOf(uniforms))), + @ptrCast(*const anyopaque, &self.uniforms), + @as(c_ulong, @sizeOf(@TypeOf(self.uniforms))), @as(c_ulong, 1), }, ); From 89610f9b8d813fc749cbbbd7b18cb64278441e30 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 29 Oct 2022 20:10:16 -0700 Subject: [PATCH 10/24] metal: setup vertex data --- src/renderer/Metal.zig | 413 +++++++++++++++++++++++++---------------- src/shaders/cell.metal | 13 +- 2 files changed, 263 insertions(+), 163 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index aa30e815c..d2189e0f5 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -53,7 +53,7 @@ buf_instance: objc.Object, // MTLBuffer pipeline: objc.Object, // MTLRenderPipelineState const GPUCell = extern struct { - foo: f64, + grid_pos: [2]f32, }; const GPUUniforms = extern struct { @@ -128,119 +128,23 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { }; const buf_cells = buffer: { - const data = [9]f32{ - 0, 1, 0, - -1, -1, 0, - 1, -1, 0, - }; + // 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("newBufferWithBytes:length:options:"), + objc.sel("newBufferWithLength:options:"), .{ - @ptrCast(*const anyopaque, &data), - @intCast(c_ulong, data.len * @sizeOf(f32)), + @intCast(c_ulong, prealloc * @sizeOf(GPUCell)), MTLResourceStorageModeShared, }, ); }; // Initialize our shader (MTLLibrary) - const library = library: { - // Load our source into a CFString - const source = try macos.foundation.String.createWithBytes( - @embedFile("../shaders/cell.metal"), - .utf8, - false, - ); - defer source.release(); - - // Compile - var err: ?*anyopaque = null; - const library = device.msgSend( - objc.Object, - objc.sel("newLibraryWithSource:options:error:"), - .{ - source, - @as(?*anyopaque, null), - &err, - }, - ); - - // If there is an error (shouldn't since we test), report it and exit. - if (err != null) { - const nserr = objc.Object.fromId(err); - const str = @ptrCast( - *macos.foundation.String, - nserr.getProperty(?*anyopaque, "localizedDescription").?, - ); - - log.err("shader error={s}", .{str.cstringPtr(.ascii).?}); - return error.MetalFailed; - } - - break :library library; - }; - const func_vert = func_vert: { - const str = try macos.foundation.String.createWithBytes( - "basic_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( - "basic_fragment", - .utf8, - false, - ); - defer str.release(); - - const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); - break :func_frag objc.Object.fromId(ptr.?); - }; - - const pipeline_state = pipeline_state: { - // 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); - - // Set our color attachment - const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); - { - const attachment = attachments.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - - // Value is MTLPixelFormatBGRA8Unorm - attachment.setProperty("pixelFormat", @as(c_ulong, 80)); - } - - // Make our state - var err: ?*anyopaque = null; - const pipeline_state = device.msgSend( - objc.Object, - objc.sel("newRenderPipelineStateWithDescriptor:error:"), - .{ desc, &err }, - ); - try checkError(err); - - break :pipeline_state pipeline_state; - }; + const library = try initLibrary(device, @embedFile("../shaders/cell.metal")); + const pipeline_state = try initPipelineState(device, library); return Metal{ .alloc = alloc, @@ -379,6 +283,9 @@ pub fn render( // Get our surface (CAMetalDrawable) const surface = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); + // Setup our buffer + try self.syncCells(); + // MTLRenderPassDescriptor const desc = desc: { const MTLRenderPassDescriptor = objc.Class.getClass("MTLRenderPassDescriptor").?; @@ -445,18 +352,6 @@ pub fn render( }, ); - // Draw - // encoder.msgSend( - // void, - // objc.sel("drawPrimitives:vertexStart:vertexCount:instanceCount:"), - // .{ - // @enumToInt(MTLPrimitiveType.triangle), - // @as(c_ulong, 0), - // @as(c_ulong, 3), - // @as(c_ulong, 1), - // }, - // ); - encoder.msgSend( void, objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), @@ -466,7 +361,7 @@ pub fn render( @enumToInt(MTLIndexType.uint16), self.buf_instance.value, @as(c_ulong, 0), - @as(c_ulong, 1), + @as(c_ulong, self.cells.items.len), }, ); } @@ -504,30 +399,30 @@ fn rebuildCells(self: *Metal, term: *Terminal) !void { (term.screen.rows * term.screen.cols * 3) + 1, ); - // // Build each cell - // var rowIter = term.screen.rowIterator(.viewport); - // var y: usize = 0; - // while (rowIter.next()) |row| { - // defer y += 1; - // - // // 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); - // } + // Build each cell + var rowIter = term.screen.rowIterator(.viewport); + var y: usize = 0; + while (rowIter.next()) |row| { + defer y += 1; + + // 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); + } } pub fn updateCell( @@ -539,9 +434,6 @@ pub fn updateCell( x: usize, y: usize, ) !bool { - _ = shaper_cell; - _ = shaper_run; - const BgFg = struct { /// Background is optional because in un-inverted mode /// it may just be equivalent to the default background in @@ -594,27 +486,216 @@ pub fn updateCell( 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(.{ - // .grid_col = @intCast(u16, x), - // .grid_row = @intCast(u16, y), - // .grid_width = cell.widthLegacy(), - // .fg_r = 0, - // .fg_g = 0, - // .fg_b = 0, - // .fg_a = 0, - // .bg_r = rgb.r, - // .bg_g = rgb.g, - // .bg_b = rgb.b, - // .bg_a = alpha, - // }); - // } + if (colors.bg) |rgb| { + _ = rgb; + self.cells.appendAssumeCapacity(.{ + .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + // .grid_col = @intCast(u16, x), + // .grid_row = @intCast(u16, y), + // .grid_width = cell.widthLegacy(), + // .fg_r = 0, + // .fg_g = 0, + // .fg_b = 0, + // .fg_a = 0, + // .bg_r = rgb.r, + // .bg_g = rgb.g, + // .bg_b = rgb.b, + // .bg_a = alpha, + }); + } _ = alpha; - _ = colors; + + // If the cell has a character, draw it + if (cell.char > 0) { + // Render + const face = try self.font_group.group.faceFromIndex(shaper_run.font_index); + _ = face; + const glyph = try self.font_group.renderGlyph( + self.alloc, + shaper_run.font_index, + shaper_cell.glyph_index, + @floatToInt(u16, @ceil(self.cell_size.height)), + ); + _ = glyph; + + self.cells.appendAssumeCapacity(.{ + .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + // .mode = mode, + // .grid_col = @intCast(u16, x), + // .grid_row = @intCast(u16, y), + // .grid_width = cell.widthLegacy(), + // .glyph_x = glyph.atlas_x, + // .glyph_y = glyph.atlas_y, + // .glyph_width = glyph.width, + // .glyph_height = glyph.height, + // .glyph_offset_x = glyph.offset_x, + // .glyph_offset_y = glyph.offset_y, + // .fg_r = colors.fg.r, + // .fg_g = colors.fg.g, + // .fg_b = colors.fg.b, + // .fg_a = alpha, + // .bg_r = 0, + // .bg_g = 0, + // .bg_b = 0, + // .bg_a = 0, + }); + } return true; } +/// 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) { + @panic("TODO: reallocate buffer"); + } + + // 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); +} + +/// 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( + "basic_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( + "basic_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.float2)); + attr.setProperty("offset", @as(c_ulong, 0)); + 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)); + } + + // 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; +} + fn checkError(err_: ?*anyopaque) !void { if (err_) |err| { const nserr = objc.Object.fromId(err); @@ -664,6 +745,18 @@ const MTLIndexType = enum(c_ulong) { uint32 = 1, }; +/// https://developer.apple.com/documentation/metal/mtlvertexformat?language=objc +const MTLVertexFormat = enum(c_ulong) { + float2 = 29, +}; + +/// 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/mtlresourceoptions?language=objc /// (incomplete, we only use this mode so we just hardcode it) const MTLResourceStorageModeShared: c_ulong = @enumToInt(MTLStorageMode.shared) << 4; diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index 53ffc995c..1113faf11 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -1,16 +1,23 @@ using namespace metal; struct Uniforms { - float4x4 projection_matrix; - float2 cell_size; + float4x4 projection_matrix; + float2 cell_size; +}; + +struct VertexIn { + // The grid coordinates (x, y) where x < columns and y < rows + float2 grid_pos [[ attribute(0) ]]; }; vertex float4 basic_vertex( unsigned int vid [[ vertex_id ]], + VertexIn input [[ stage_in ]], constant Uniforms &uniforms [[ buffer(1) ]] ) { // Where we are in the grid (x, y) where top-left is origin - float2 grid_coord = float2(0.0f, 0.0f); + // float2 grid_coord = float2(5.0f, 0.0f); + float2 grid_coord = input.grid_pos; // Convert the grid x,y into world space x, y by accounting for cell size float2 cell_pos = uniforms.cell_size * grid_coord; From a7c1f63ad8628073b6913a722555d80faf18829c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Oct 2022 10:32:13 -0700 Subject: [PATCH 11/24] metal: populate the greyscale texture, prep ubershader --- src/renderer/Metal.zig | 196 ++++++++++++++++++++++++++++++++++++++--- src/shaders/cell.metal | 75 +++++++++++++--- 2 files changed, 245 insertions(+), 26 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index d2189e0f5..bd4fd9f00 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -6,6 +6,7 @@ const builtin = @import("builtin"); const glfw = @import("glfw"); const objc = @import("objc"); const macos = @import("macos"); +const Atlas = @import("../Atlas.zig"); const font = @import("../font/main.zig"); const terminal = @import("../terminal/main.zig"); const renderer = @import("../renderer.zig"); @@ -51,9 +52,14 @@ swapchain: objc.Object, // CAMetalLayer buf_cells: objc.Object, // MTLBuffer buf_instance: objc.Object, // MTLBuffer pipeline: objc.Object, // MTLRenderPipelineState +texture_greyscale: objc.Object, // MTLTexture const GPUCell = extern struct { + mode: GPUCellMode, grid_pos: [2]f32, + glyph_pos: [2]u32 = .{ 0, 0 }, + glyph_size: [2]u32 = .{ 0, 0 }, + glyph_offset: [2]i32 = .{ 0, 0 }, }; const GPUUniforms = extern struct { @@ -61,6 +67,17 @@ const GPUUniforms = extern struct { cell_size: [2]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, +}; + /// Returns the hints that we want for this pub fn windowHints() glfw.Window.Hints { return .{ @@ -145,6 +162,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { // 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); return Metal{ .alloc = alloc, @@ -167,6 +185,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { .buf_cells = buf_cells, .buf_instance = buf_instance, .pipeline = pipeline_state, + .texture_greyscale = texture_greyscale, }; } @@ -283,9 +302,15 @@ pub fn render( // Get our surface (CAMetalDrawable) const surface = self.swapchain.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); - // Setup our buffer + // 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.font_group.atlas_greyscale, &self.texture_greyscale); + self.font_group.atlas_greyscale.modified = false; + } + // MTLRenderPassDescriptor const desc = desc: { const MTLRenderPassDescriptor = objc.Class.getClass("MTLRenderPassDescriptor").?; @@ -351,6 +376,14 @@ pub fn render( @as(c_ulong, 1), }, ); + encoder.msgSend( + void, + objc.sel("setFragmentTexture:atIndex:"), + .{ + self.texture_greyscale.value, + @as(c_ulong, 0), + }, + ); encoder.msgSend( void, @@ -489,6 +522,7 @@ pub fn updateCell( if (colors.bg) |rgb| { _ = rgb; self.cells.appendAssumeCapacity(.{ + .mode = .bg, .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, // .grid_col = @intCast(u16, x), // .grid_row = @intCast(u16, y), @@ -516,20 +550,16 @@ pub fn updateCell( shaper_cell.glyph_index, @floatToInt(u16, @ceil(self.cell_size.height)), ); - _ = glyph; self.cells.appendAssumeCapacity(.{ + .mode = .fg, .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + .glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }, + .glyph_size = .{ glyph.width, glyph.height }, + .glyph_offset = .{ glyph.offset_x, glyph.offset_y }, + // .mode = mode, - // .grid_col = @intCast(u16, x), - // .grid_row = @intCast(u16, y), // .grid_width = cell.widthLegacy(), - // .glyph_x = glyph.atlas_x, - // .glyph_y = glyph.atlas_y, - // .glyph_width = glyph.width, - // .glyph_height = glyph.height, - // .glyph_offset_x = glyph.offset_x, - // .glyph_offset_y = glyph.offset_y, // .fg_r = colors.fg.r, // .fg_g = colors.fg.g, // .fg_b = colors.fg.b, @@ -565,6 +595,34 @@ fn syncCells(self: *Metal) !void { @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(atlas: *const Atlas, texture: *objc.Object) !void { + const width = texture.getProperty(c_ulong, "width"); + if (atlas.size > width) { + @panic("TODO: reallocate texture"); + } + + 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( @@ -594,7 +652,7 @@ 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( - "basic_vertex", + "uber_vertex", .utf8, false, ); @@ -605,7 +663,7 @@ fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object { }; const func_frag = func_frag: { const str = try macos.foundation.String.createWithBytes( - "basic_fragment", + "uber_fragment", .utf8, false, ); @@ -636,8 +694,52 @@ fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object { .{@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, 0)); + 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)); } @@ -696,6 +798,44 @@ fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object { 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, + 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); @@ -748,6 +888,9 @@ const MTLIndexType = enum(c_ulong) { /// https://developer.apple.com/documentation/metal/mtlvertexformat?language=objc const MTLVertexFormat = enum(c_ulong) { float2 = 29, + int2 = 33, + uint2 = 37, + uchar = 45, }; /// https://developer.apple.com/documentation/metal/mtlvertexstepfunction?language=objc @@ -757,6 +900,16 @@ const MTLVertexStepFunction = enum(c_ulong) { per_instance = 2, }; +/// https://developer.apple.com/documentation/metal/mtlpixelformat?language=objc +const MTLPixelFormat = enum(c_ulong) { + r8unorm = 10, +}; + +/// https://developer.apple.com/documentation/metal/mtlpurgeablestate?language=objc +const MTLPurgeableState = enum(c_ulong) { + empty = 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; @@ -777,6 +930,23 @@ const MTLViewport = extern struct { 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/shaders/cell.metal b/src/shaders/cell.metal index 1113faf11..157ab7d9c 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -1,26 +1,46 @@ using namespace metal; +// The possible modes that a shader can take. +enum Mode : uint8_t { + MODE_BG = 1u, + MODE_FG = 2u, +}; + struct Uniforms { float4x4 projection_matrix; float2 cell_size; }; 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(0) ]]; + float2 grid_pos [[ attribute(1) ]]; + + // 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) ]]; }; -vertex float4 basic_vertex( +struct VertexOut { + float4 position [[ position ]]; +}; + +vertex VertexOut uber_vertex( unsigned int vid [[ vertex_id ]], VertexIn input [[ stage_in ]], constant Uniforms &uniforms [[ buffer(1) ]] ) { - // Where we are in the grid (x, y) where top-left is origin - // float2 grid_coord = float2(5.0f, 0.0f); - float2 grid_coord = input.grid_pos; - // Convert the grid x,y into world space x, y by accounting for cell size - float2 cell_pos = uniforms.cell_size * grid_coord; + float2 cell_pos = uniforms.cell_size * input.grid_pos; // Turn the cell position into a vertex point depending on the // vertex ID. Since we use instanced drawing, we have 4 vertices @@ -36,14 +56,43 @@ vertex float4 basic_vertex( position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; - // 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 + uniforms.cell_size * position; + // TODO: scale + float2 cell_size = uniforms.cell_size; - return uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); + VertexOut out; + 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 + uniforms.cell_size * position; + + out.position = uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); + break; + + case MODE_FG: + float2 glyph_size = float2(input.glyph_size); + float2 glyph_offset = float2(input.glyph_offset); + + // TODO: downsampling + + // 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.y - glyph_offset.y; + + // Calculate the final position of the cell. + cell_pos = cell_pos + glyph_size * position + glyph_offset; + + out.position = uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); + break; + } + + return out; } -fragment half4 basic_fragment() { +fragment half4 uber_fragment( + VertexOut in [[ stage_in ]] +) { return half4(1.0, 0.0, 0.0, 1.0); } From fb49595904c81be39fd4e27c5739af60fa31bd42 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Oct 2022 10:47:04 -0700 Subject: [PATCH 12/24] metal: pass colors in --- src/renderer/Metal.zig | 18 ++++++++++++++++-- src/shaders/cell.metal | 23 ++++++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index bd4fd9f00..ff096fef0 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -57,6 +57,7 @@ texture_greyscale: objc.Object, // MTLTexture const GPUCell = extern struct { mode: GPUCellMode, grid_pos: [2]f32, + color: [4]u8, glyph_pos: [2]u32 = .{ 0, 0 }, glyph_size: [2]u32 = .{ 0, 0 }, glyph_offset: [2]i32 = .{ 0, 0 }, @@ -520,10 +521,11 @@ pub fn updateCell( // If the cell has a background, we always draw it. if (colors.bg) |rgb| { - _ = rgb; self.cells.appendAssumeCapacity(.{ .mode = .bg, .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + .color = .{ rgb.r, rgb.g, rgb.b, alpha }, + // .grid_col = @intCast(u16, x), // .grid_row = @intCast(u16, y), // .grid_width = cell.widthLegacy(), @@ -537,7 +539,6 @@ pub fn updateCell( // .bg_a = alpha, }); } - _ = alpha; // If the cell has a character, draw it if (cell.char > 0) { @@ -554,6 +555,7 @@ pub fn updateCell( self.cells.appendAssumeCapacity(.{ .mode = .fg, .grid_pos = .{ @intToFloat(f32, x), @intToFloat(f32, y) }, + .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 }, @@ -742,6 +744,17 @@ fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object { 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)); + } // The layout describes how and when we fetch the next vertex input. const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); @@ -887,6 +900,7 @@ const MTLIndexType = enum(c_ulong) { /// https://developer.apple.com/documentation/metal/mtlvertexformat?language=objc const MTLVertexFormat = enum(c_ulong) { + uchar4 = 3, float2 = 29, int2 = 33, uint2 = 37, diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index 157ab7d9c..560fffa3e 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -18,6 +18,10 @@ struct VertexIn { // The grid coordinates (x, y) where x < columns and y < rows float2 grid_pos [[ attribute(1) ]]; + // 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) @@ -32,6 +36,8 @@ struct VertexIn { struct VertexOut { float4 position [[ position ]]; + uint8_t mode; + float4 color; }; vertex VertexOut uber_vertex( @@ -60,6 +66,8 @@ vertex VertexOut uber_vertex( float2 cell_size = uniforms.cell_size; VertexOut out; + out.mode = input.mode; + out.color = float4(input.color) / 255.0f; switch (input.mode) { case MODE_BG: // Calculate the final position of our cell in world space. @@ -91,8 +99,17 @@ vertex VertexOut uber_vertex( return out; } -fragment half4 uber_fragment( - VertexOut in [[ stage_in ]] +fragment float4 uber_fragment( + VertexOut in [[ stage_in ]], + texture2d textureGreyscale [[ texture(0) ]] ) { - return half4(1.0, 0.0, 0.0, 1.0); + constexpr sampler textureSampler; + + switch (in.mode) { + case MODE_BG: + return in.color; + + case MODE_FG: + return in.color; + } } From 0058906035c0bad55fc0876380c37a2ab4350fa4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Oct 2022 11:04:15 -0700 Subject: [PATCH 13/24] metal: handle HiDPI scaling --- src/renderer/Metal.zig | 23 ++++++++++++++++++----- src/shaders/cell.metal | 15 ++++++++------- src/shaders/cell.v.glsl | 2 +- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index ff096fef0..cb9609e31 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1,4 +1,11 @@ //! Renderer implementation for Metal. +//! +//! Open questions: +//! +//! - This requires a "px_scale" uniform to account for pixel scaling +//! issues with Retina. I'm not 100% sure why this is necessary and why +//! this doesn't happen with OpenGL. +//! pub const Metal = @This(); const std = @import("std"); @@ -64,7 +71,16 @@ const GPUCell = extern struct { }; 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, + + /// A scale factor to apply to all pixels given as input (including + /// in this uniform i.e. cell_size). This is due to HiDPI screens (Retina) + /// mismatch with the window. + px_scale: [2]f32, + + /// Size of a single cell in pixels, unscaled. cell_size: [2]f32, }; @@ -292,11 +308,8 @@ pub fn render( @floatCast(f32, bounds.size.height), 0, ), - - .cell_size = .{ - self.cell_size.width * scaleX, - self.cell_size.height * scaleY, - }, + .px_scale = .{ scaleX, scaleY }, + .cell_size = .{ self.cell_size.width, self.cell_size.height }, }; } diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index 560fffa3e..6e7eeebcb 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -8,6 +8,7 @@ enum Mode : uint8_t { struct Uniforms { float4x4 projection_matrix; + float2 px_scale; float2 cell_size; }; @@ -45,8 +46,11 @@ vertex VertexOut uber_vertex( VertexIn input [[ stage_in ]], constant Uniforms &uniforms [[ buffer(1) ]] ) { + // TODO: scale with cell width + float2 cell_size = uniforms.cell_size * uniforms.px_scale; + // Convert the grid x,y into world space x, y by accounting for cell size - float2 cell_pos = uniforms.cell_size * input.grid_pos; + float2 cell_pos = cell_size * input.grid_pos; // Turn the cell position into a vertex point depending on the // vertex ID. Since we use instanced drawing, we have 4 vertices @@ -62,9 +66,6 @@ vertex VertexOut uber_vertex( position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; - // TODO: scale - float2 cell_size = uniforms.cell_size; - VertexOut out; out.mode = input.mode; out.color = float4(input.color) / 255.0f; @@ -73,14 +74,14 @@ vertex VertexOut uber_vertex( // 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 + uniforms.cell_size * position; + cell_pos = cell_pos + cell_size * position; out.position = uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); break; case MODE_FG: - float2 glyph_size = float2(input.glyph_size); - float2 glyph_offset = float2(input.glyph_offset); + float2 glyph_size = float2(input.glyph_size) * uniforms.px_scale; + float2 glyph_offset = float2(input.glyph_offset) * uniforms.px_scale; // TODO: downsampling 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 From 178418834c3bb54a8406a8f2dca3bfd00bf08f2d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Oct 2022 12:34:00 -0700 Subject: [PATCH 14/24] metal: glyphs show up (poorly) --- src/renderer/Metal.zig | 2 ++ src/shaders/cell.metal | 19 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index cb9609e31..f1a838d70 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -810,6 +810,7 @@ fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object { // Value is MTLPixelFormatBGRA8Unorm attachment.setProperty("pixelFormat", @as(c_ulong, 80)); + attachment.setProperty("blendingEnabled", true); } // Make our state @@ -930,6 +931,7 @@ const MTLVertexStepFunction = enum(c_ulong) { /// 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 diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index 6e7eeebcb..cddd427b5 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -39,6 +39,7 @@ struct VertexOut { float4 position [[ position ]]; uint8_t mode; float4 color; + float2 tex_coord; }; vertex VertexOut uber_vertex( @@ -90,10 +91,15 @@ vertex VertexOut uber_vertex( // So we flip it with `cell_size.y - glyph_offset.y`. glyph_offset.y = cell_size.y - glyph_offset.y; - // Calculate the final position of the cell. + // 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 * 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. + // TODO: do I need to px_scale? + out.tex_coord = float2(input.glyph_pos) + float2(input.glyph_size) * position; break; } @@ -104,13 +110,18 @@ fragment float4 uber_fragment( VertexOut in [[ stage_in ]], texture2d textureGreyscale [[ texture(0) ]] ) { - constexpr sampler textureSampler; + constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); switch (in.mode) { case MODE_BG: return in.color; case MODE_FG: - return in.color; + // 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); } } From 4b5174d2c6aa25933c20369e1737825070f37148 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Oct 2022 19:09:03 -0700 Subject: [PATCH 15/24] metal: blending --- src/renderer/Metal.zig | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index f1a838d70..45780ea8e 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -810,7 +810,16 @@ fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object { // 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 @@ -939,6 +948,38 @@ 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; From ee45d363a96077a7142ee72130df5d2f36d667bf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Oct 2022 19:47:15 -0700 Subject: [PATCH 16/24] metal: cursor and underline --- src/renderer.zig | 1 + src/renderer/Metal.zig | 151 ++++++++++++++++++++++++++++++++++------ src/renderer/OpenGL.zig | 38 +++------- src/shaders/cell.metal | 126 ++++++++++++++++++++++++++++++++- 4 files changed, 264 insertions(+), 52 deletions(-) diff --git a/src/renderer.zig b/src/renderer.zig index 8d3e899a4..2f3018558 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -9,6 +9,7 @@ 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"); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 45780ea8e..5c73a5fb4 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -35,6 +35,11 @@ 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, @@ -64,6 +69,7 @@ texture_greyscale: 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 }, @@ -82,6 +88,12 @@ const GPUUniforms = extern struct { /// 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) { @@ -93,6 +105,14 @@ const GPUCellMode = enum(u8) { 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 @@ -186,10 +206,20 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { .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 = undefined, + .uniforms = .{ + .projection_matrix = undefined, + .px_scale = 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, @@ -263,6 +293,15 @@ pub fn render( 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; @@ -301,6 +340,7 @@ pub fn render( const scaleY = @floatCast(f32, bounds.size.height) / @intToFloat(f32, screen_size.height); // Setup our uniforms + const old = self.uniforms; self.uniforms = .{ .projection_matrix = math.ortho2d( 0, @@ -310,6 +350,10 @@ pub fn render( ), .px_scale = .{ scaleX, scaleY }, .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, }; } @@ -446,12 +490,35 @@ fn rebuildCells(self: *Metal, term: *Terminal) !void { (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| { @@ -470,6 +537,15 @@ fn rebuildCells(self: *Metal, term: *Terminal) !void { // 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( @@ -537,19 +613,8 @@ pub fn updateCell( 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 }, - - // .grid_col = @intCast(u16, x), - // .grid_row = @intCast(u16, y), - // .grid_width = cell.widthLegacy(), - // .fg_r = 0, - // .fg_g = 0, - // .fg_b = 0, - // .fg_a = 0, - // .bg_r = rgb.r, - // .bg_g = rgb.g, - // .bg_b = rgb.b, - // .bg_a = alpha, }); } @@ -568,27 +633,58 @@ pub fn updateCell( self.cells.appendAssumeCapacity(.{ .mode = .fg, .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 }, // .mode = mode, - // .grid_width = cell.widthLegacy(), - // .fg_r = colors.fg.r, - // .fg_g = colors.fg.g, - // .fg_b = colors.fg.b, - // .fg_a = alpha, - // .bg_r = 0, - // .bg_g = 0, - // .bg_b = 0, - // .bg_a = 0, + }); + } + + 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. @@ -768,6 +864,17 @@ fn initPipelineState(device: objc.Object, library: objc.Object) !objc.Object { 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")); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index eaf2d319a..323406431 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; @@ -701,13 +690,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/shaders/cell.metal b/src/shaders/cell.metal index cddd427b5..1fc4ff9e1 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -4,12 +4,21 @@ using namespace metal; enum Mode : uint8_t { MODE_BG = 1u, MODE_FG = 2u, + 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 px_scale; float2 cell_size; + float underline_position; + float underline_thickness; + float strikethrough_position; + float strikethrough_thickness; }; struct VertexIn { @@ -19,6 +28,9 @@ struct VertexIn { // 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) ]]; @@ -47,8 +59,8 @@ vertex VertexOut uber_vertex( VertexIn input [[ stage_in ]], constant Uniforms &uniforms [[ buffer(1) ]] ) { - // TODO: scale with cell width float2 cell_size = uniforms.cell_size * uniforms.px_scale; + cell_size.x = cell_size.x * input.cell_width; // Convert the grid x,y into world space x, y by accounting for cell size float2 cell_pos = cell_size * input.grid_pos; @@ -80,7 +92,7 @@ vertex VertexOut uber_vertex( out.position = uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); break; - case MODE_FG: + case MODE_FG: { float2 glyph_size = float2(input.glyph_size) * uniforms.px_scale; float2 glyph_offset = float2(input.glyph_offset) * uniforms.px_scale; @@ -103,6 +115,67 @@ vertex VertexOut uber_vertex( break; } + case MODE_CURSOR_RECT: + // Same as background since we're taking up the whole cell. + cell_pos = cell_pos + cell_size * position; + + out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0); + break; + + case MODE_CURSOR_RECT_HOLLOW: + // Same as background since we're taking up the whole cell. + cell_pos = cell_pos + cell_size * position; + out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0); + + // Top-left position of this cell is needed for the hollow rect. + out.tex_coord = cell_pos; + break; + + case MODE_CURSOR_BAR: { + // Make the bar a smaller version of our cell + float2 bar_size = float2(cell_size.x * 0.2, 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.x, uniforms.underline_thickness); + + // Position the underline where we are told to + float2 underline_offset = float2(cell_size.x, uniforms.underline_position * uniforms.px_scale.y); + + // 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.x, uniforms.strikethrough_thickness); + + // Position the strikethrough where we are told to + float2 strikethrough_offset = float2(cell_size.x, uniforms.strikethrough_position * uniforms.px_scale.y); + + // 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; } @@ -116,7 +189,7 @@ fragment float4 uber_fragment( case MODE_BG: return in.color; - case MODE_FG: + 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; @@ -124,4 +197,51 @@ fragment float4 uber_fragment( float a = textureGreyscale.sample(textureSampler, coord).r; return float4(in.color.rgb, in.color.a * a); } + + 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. + // vec2 cell_size_coords = cell_size - 1; + // + // // Apply padding + // vec2 padding = vec2(1.,1.); + // cell_size_coords = cell_size_coords - (padding * 2); + // vec2 screen_cell_pos_padded = screen_cell_pos + padding; + // + // // Convert our frag coord to offset of this cell. We have to subtract + // // 0.5 because the frag coord is in center pixels. + // vec2 cell_frag_coord = gl_FragCoord.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) { + // out_FragColor = 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; + } } From 666833f12fa3b4d95335fa95a11fa80c1b4ec934 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Oct 2022 19:55:47 -0700 Subject: [PATCH 17/24] metal: color textures --- src/renderer/Metal.zig | 26 +++++++++++++++++++++----- src/shaders/cell.metal | 27 +++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 5c73a5fb4..d1eea57c7 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -65,6 +65,7 @@ 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, @@ -200,6 +201,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { 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, @@ -233,6 +235,7 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { .buf_instance = buf_instance, .pipeline = pipeline_state, .texture_greyscale = texture_greyscale, + .texture_color = texture_color, }; } @@ -368,6 +371,10 @@ pub fn render( try syncAtlasTexture(&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.font_group.atlas_color, &self.texture_color); + self.font_group.atlas_color.modified = false; + } // MTLRenderPassDescriptor const desc = desc: { @@ -442,6 +449,14 @@ pub fn render( @as(c_ulong, 0), }, ); + encoder.msgSend( + void, + objc.sel("setFragmentTexture:atIndex:"), + .{ + self.texture_color.value, + @as(c_ulong, 1), + }, + ); encoder.msgSend( void, @@ -621,8 +636,6 @@ pub fn updateCell( // If the cell has a character, draw it if (cell.char > 0) { // Render - const face = try self.font_group.group.faceFromIndex(shaper_run.font_index); - _ = face; const glyph = try self.font_group.renderGlyph( self.alloc, shaper_run.font_index, @@ -630,16 +643,18 @@ pub fn updateCell( @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 = .fg, + .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 }, - - // .mode = mode, }); } @@ -946,6 +961,7 @@ 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"), }; diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index 1fc4ff9e1..72bb29a6c 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -4,6 +4,7 @@ using namespace metal; 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, @@ -92,11 +93,21 @@ vertex VertexOut uber_vertex( out.position = uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); break; - case MODE_FG: { + case MODE_FG: + case MODE_FG_COLOR: { float2 glyph_size = float2(input.glyph_size) * uniforms.px_scale; float2 glyph_offset = float2(input.glyph_offset) * uniforms.px_scale; - // TODO: downsampling + // 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.y + 2) { + // Magic 0.9 and 1.1 are padding to make emoji look better + glyph_size_downsampled.y = cell_size.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. @@ -105,7 +116,7 @@ vertex VertexOut uber_vertex( // 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 * position + glyph_offset; + 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 @@ -181,7 +192,8 @@ vertex VertexOut uber_vertex( fragment float4 uber_fragment( VertexOut in [[ stage_in ]], - texture2d textureGreyscale [[ texture(0) ]] + texture2d textureGreyscale [[ texture(0) ]], + texture2d textureColor [[ texture(1) ]] ) { constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); @@ -198,6 +210,13 @@ fragment float4 uber_fragment( 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; From c1b70cb78892257fc19dd58ce23a4eeb16842806 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Oct 2022 20:24:11 -0700 Subject: [PATCH 18/24] metal: devmode --- build.zig | 7 +++++-- pkg/imgui/build.zig | 10 ++++++++-- pkg/imgui/impl_glfw.zig | 5 +++++ pkg/imgui/impl_metal.zig | 31 +++++++++++++++++++++++++++++++ pkg/imgui/main.zig | 1 + src/DevMode.zig | 21 +++++++++++++++++---- src/renderer/Metal.zig | 39 +++++++++++++++++++++++++++++++++++++++ src/renderer/OpenGL.zig | 2 ++ src/renderer/cursor.zig | 19 +++++++++++++++++++ 9 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 pkg/imgui/impl_metal.zig create mode 100644 src/renderer/cursor.zig diff --git a/build.zig b/build.zig index 4c77fb92a..424bf08f6 100644 --- a/build.zig +++ b/build.zig @@ -230,9 +230,12 @@ fn addDeps( 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/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/renderer/Metal.zig b/src/renderer/Metal.zig index d1eea57c7..63f19ee60 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -13,11 +13,13 @@ 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; @@ -240,6 +242,11 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { } pub fn deinit(self: *Metal) void { + if (DevMode.enabled) { + imgui.ImplMetal.shutdown(); + imgui.ImplGlfw.shutdown(); + } + self.cells.deinit(self.alloc); self.font_shaper.deinit(); @@ -256,6 +263,15 @@ pub fn finalizeInit(self: *const Metal, window: glfw.Window) !void { const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?); contentView.setProperty("layer", self.swapchain.value); contentView.setProperty("wantsLayer", true); + + 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. @@ -285,6 +301,7 @@ pub fn render( const Critical = struct { bg: terminal.color.RGB, screen_size: ?renderer.ScreenSize, + devmode: bool, }; // Update all our data as tightly as possible within the mutex. @@ -323,6 +340,7 @@ pub fn render( break :critical .{ .bg = self.background, .screen_size = state.resize_screen, + .devmode = if (state.devmode) |dm| dm.visible else false, }; }; @@ -470,6 +488,27 @@ pub fn render( @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}); diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 323406431..ee10e17a0 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -483,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(); } 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, + }; + } +}; From ddc0d60ea2103a3378cf9f0d9eaa4cf006bd6c21 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Oct 2022 22:04:37 -0700 Subject: [PATCH 19/24] metal: set contentsScale and handle screen scale factor for retina --- src/renderer/Metal.zig | 40 +++++++++++++++++++++------------------- src/shaders/cell.metal | 12 +++++------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 63f19ee60..1b34c93ba 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -2,10 +2,6 @@ //! //! Open questions: //! -//! - This requires a "px_scale" uniform to account for pixel scaling -//! issues with Retina. I'm not 100% sure why this is necessary and why -//! this doesn't happen with OpenGL. -//! pub const Metal = @This(); const std = @import("std"); @@ -84,11 +80,6 @@ const GPUUniforms = extern struct { /// This is calculated based on the size of the screen. projection_matrix: math.Mat, - /// A scale factor to apply to all pixels given as input (including - /// in this uniform i.e. cell_size). This is due to HiDPI screens (Retina) - /// mismatch with the window. - px_scale: [2]f32, - /// Size of a single cell in pixels, unscaled. cell_size: [2]f32, @@ -217,7 +208,6 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { .cells = .{}, .uniforms = .{ .projection_matrix = undefined, - .px_scale = undefined, .cell_size = undefined, .underline_position = metrics.underline_position, .underline_thickness = metrics.underline_thickness, @@ -264,6 +254,13 @@ pub fn finalizeInit(self: *const Metal, window: glfw.Window) !void { 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( @@ -352,24 +349,29 @@ pub fn render( if (critical.screen_size) |screen_size| { const bounds = self.swapchain.getProperty(macos.graphics.Rect, "bounds"); - // Set the size of the drawable surface to the bounds of our surface. - self.swapchain.setProperty("drawableSize", bounds.size); + // 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, + }; + }; - // Our drawable surface is usually scaled so we need to figure - // out the scalem amount so our pixels are correct. - const scaleX = @floatCast(f32, bounds.size.width) / @intToFloat(f32, screen_size.width); - const scaleY = @floatCast(f32, bounds.size.height) / @intToFloat(f32, screen_size.height); + // Set the size of the drawable surface to the scaled bounds + self.swapchain.setProperty("drawableSize", scaled); + 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, bounds.size.width), - @floatCast(f32, bounds.size.height), + @floatCast(f32, scaled.width), + @floatCast(f32, scaled.height), 0, ), - .px_scale = .{ scaleX, scaleY }, .cell_size = .{ self.cell_size.width, self.cell_size.height }, .underline_position = old.underline_position, .underline_thickness = old.underline_thickness, diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index 72bb29a6c..1519f60e6 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -14,7 +14,6 @@ enum Mode : uint8_t { struct Uniforms { float4x4 projection_matrix; - float2 px_scale; float2 cell_size; float underline_position; float underline_thickness; @@ -60,7 +59,7 @@ vertex VertexOut uber_vertex( VertexIn input [[ stage_in ]], constant Uniforms &uniforms [[ buffer(1) ]] ) { - float2 cell_size = uniforms.cell_size * uniforms.px_scale; + float2 cell_size = uniforms.cell_size; cell_size.x = cell_size.x * input.cell_width; // Convert the grid x,y into world space x, y by accounting for cell size @@ -95,8 +94,8 @@ vertex VertexOut uber_vertex( case MODE_FG: case MODE_FG_COLOR: { - float2 glyph_size = float2(input.glyph_size) * uniforms.px_scale; - float2 glyph_offset = float2(input.glyph_offset) * uniforms.px_scale; + 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 @@ -121,7 +120,6 @@ vertex VertexOut uber_vertex( // Calculate the texture coordinate in pixels. This is NOT normalized // (between 0.0 and 1.0) and must be done in the fragment shader. - // TODO: do I need to px_scale? out.tex_coord = float2(input.glyph_pos) + float2(input.glyph_size) * position; break; } @@ -158,7 +156,7 @@ vertex VertexOut uber_vertex( float2 underline_size = float2(cell_size.x, uniforms.underline_thickness); // Position the underline where we are told to - float2 underline_offset = float2(cell_size.x, uniforms.underline_position * uniforms.px_scale.y); + float2 underline_offset = float2(cell_size.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 @@ -174,7 +172,7 @@ vertex VertexOut uber_vertex( float2 strikethrough_size = float2(cell_size.x, uniforms.strikethrough_thickness); // Position the strikethrough where we are told to - float2 strikethrough_offset = float2(cell_size.x, uniforms.strikethrough_position * uniforms.px_scale.y); + float2 strikethrough_offset = float2(cell_size.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 From 901ff199c8c0bc771cdd18e8776d7a9da41402db Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 30 Oct 2022 22:20:17 -0700 Subject: [PATCH 20/24] log the renderer on startup --- src/main.zig | 2 ++ 1 file changed, 2 insertions(+) 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: { From 1d1f161b03794f83f0387d0689465f221073681f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 31 Oct 2022 08:54:23 -0700 Subject: [PATCH 21/24] metal: fix double wide cell width --- src/renderer/Metal.zig | 3 ++- src/shaders/cell.metal | 31 ++++++++++++++++--------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 1b34c93ba..a307ac826 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -361,7 +361,8 @@ pub fn render( // Set the size of the drawable surface to the scaled bounds self.swapchain.setProperty("drawableSize", scaled); - log.warn("bounds={} screen={} scaled={}", .{ bounds, screen_size, scaled }); + _ = screen_size; + //log.warn("bounds={} screen={} scaled={}", .{ bounds, screen_size, scaled }); // Setup our uniforms const old = self.uniforms; diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index 1519f60e6..ea3918f3c 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -59,11 +59,12 @@ vertex VertexOut uber_vertex( VertexIn input [[ stage_in ]], constant Uniforms &uniforms [[ buffer(1) ]] ) { - float2 cell_size = uniforms.cell_size; - cell_size.x = cell_size.x * input.cell_width; - // Convert the grid x,y into world space x, y by accounting for cell size - float2 cell_pos = cell_size * input.grid_pos; + 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 @@ -87,7 +88,7 @@ vertex VertexOut uber_vertex( // 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 * position; + 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; @@ -101,9 +102,9 @@ vertex VertexOut uber_vertex( // 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.y + 2) { + 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.y * 0.9; + 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); } @@ -111,7 +112,7 @@ vertex VertexOut uber_vertex( // 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.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. @@ -126,14 +127,14 @@ vertex VertexOut uber_vertex( case MODE_CURSOR_RECT: // Same as background since we're taking up the whole cell. - cell_pos = cell_pos + cell_size * position; + 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: // Same as background since we're taking up the whole cell. - cell_pos = cell_pos + cell_size * position; + cell_pos = cell_pos + cell_size_scaled * position; out.position = uniforms.projection_matrix * float4(cell_pos, 0.0f, 1.0); // Top-left position of this cell is needed for the hollow rect. @@ -142,7 +143,7 @@ vertex VertexOut uber_vertex( case MODE_CURSOR_BAR: { // Make the bar a smaller version of our cell - float2 bar_size = float2(cell_size.x * 0.2, cell_size.y); + 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; @@ -153,10 +154,10 @@ vertex VertexOut uber_vertex( case MODE_UNDERLINE: { // Underline Y value is just our thickness - float2 underline_size = float2(cell_size.x, uniforms.underline_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.x, uniforms.underline_position); + 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 @@ -169,10 +170,10 @@ vertex VertexOut uber_vertex( case MODE_STRIKETHROUGH: { // Strikethrough Y value is just our thickness - float2 strikethrough_size = float2(cell_size.x, uniforms.strikethrough_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.x, uniforms.strikethrough_position); + 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 From 20adaa7b66db8aa6c9458828fb61ea25faebf369 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 31 Oct 2022 10:25:49 -0700 Subject: [PATCH 22/24] metal: disable v-sync --- src/renderer/Metal.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index a307ac826..0d763cc88 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -136,6 +136,10 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal { 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; }; From 9e628635c23c04abfb4bce72df021cfd6c8caa85 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 31 Oct 2022 10:33:31 -0700 Subject: [PATCH 23/24] metal: hollow rect cursor works --- src/shaders/cell.metal | 61 ++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/src/shaders/cell.metal b/src/shaders/cell.metal index ea3918f3c..763e93ddf 100644 --- a/src/shaders/cell.metal +++ b/src/shaders/cell.metal @@ -49,6 +49,7 @@ struct VertexIn { struct VertexOut { float4 position [[ position ]]; + float2 cell_size; uint8_t mode; float4 color; float2 tex_coord; @@ -82,6 +83,7 @@ vertex VertexOut uber_vertex( 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: @@ -133,12 +135,12 @@ vertex VertexOut uber_vertex( 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); - - // Top-left position of this cell is needed for the hollow rect. - out.tex_coord = cell_pos; break; case MODE_CURSOR_BAR: { @@ -219,39 +221,40 @@ fragment float4 uber_fragment( case MODE_CURSOR_RECT: return in.color; - case MODE_CURSOR_RECT_HOLLOW: + 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. - // vec2 cell_size_coords = cell_size - 1; - // - // // Apply padding - // vec2 padding = vec2(1.,1.); - // cell_size_coords = cell_size_coords - (padding * 2); - // vec2 screen_cell_pos_padded = screen_cell_pos + padding; - // - // // Convert our frag coord to offset of this cell. We have to subtract - // // 0.5 because the frag coord is in center pixels. - // vec2 cell_frag_coord = gl_FragCoord.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) { - // out_FragColor = color; - // } - // } + // 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; From 8dd68ea5fe74496c83bf72aa2c37f8a2dce6fd8b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 31 Oct 2022 10:42:27 -0700 Subject: [PATCH 24/24] metal: reallocate textures if they grow --- src/renderer/Metal.zig | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 0d763cc88..168a171b1 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -393,11 +393,11 @@ pub fn render( // If our font atlas changed, sync the texture data if (self.font_group.atlas_greyscale.modified) { - try syncAtlasTexture(&self.font_group.atlas_greyscale, &self.texture_greyscale); + 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.font_group.atlas_color, &self.texture_color); + try syncAtlasTexture(self.device, &self.font_group.atlas_color, &self.texture_color); self.font_group.atlas_color.modified = false; } @@ -755,7 +755,19 @@ fn syncCells(self: *Metal) !void { // If we need more bytes than our buffer has, we need to reallocate. if (req_bytes > avail_bytes) { - @panic("TODO: reallocate buffer"); + // 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. @@ -770,10 +782,14 @@ fn syncCells(self: *Metal) !void { /// 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(atlas: *const Atlas, texture: *objc.Object) !void { +fn syncAtlasTexture(device: objc.Object, atlas: *const Atlas, texture: *objc.Object) !void { const width = texture.getProperty(c_ulong, "width"); if (atlas.size > width) { - @panic("TODO: reallocate texture"); + // Free our old texture + deinitMTLResource(texture.*); + + // Reallocate + texture.* = try initAtlasTexture(device, atlas); } texture.msgSend(