From 1d41a3ccff04dfaf6dd31aa2127000463192cd53 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Apr 2024 19:12:25 -0700 Subject: [PATCH 01/26] renderer/metal: start setting up per-frame state --- src/renderer/Metal.zig | 144 +++++++++++++++++++++++++++------- src/renderer/metal/buffer.zig | 15 ++++ 2 files changed, 129 insertions(+), 30 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 0afc363a8..2999d9d32 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -108,8 +108,6 @@ image_text_end: u32 = 0, /// Metal state shaders: Shaders, // Compiled shaders -buf_cells: CellBuffer, // Vertex buffer for cells -buf_cells_bg: CellBuffer, // Vertex buffer for background cells buf_instance: InstanceBuffer, // MTLBuffer /// Metal objects @@ -133,6 +131,93 @@ health: std.atomic.Value(Health) = .{ .raw = .healthy }, /// will be incremented. inflight: std.Thread.Semaphore = .{ .permits = 1 }, +/// Our GPU state +gpu_state: GPUState, + +/// State we need for the GPU that is shared between all frames. +pub const GPUState = struct { + // The count of buffers we use for double/triple buffering. If + // this is one then we don't do any double+ buffering at all. This + // is comptime because there isn't a good reason to change this at + // runtime and there is a lot of complexity to support it. For comptime, + // this is useful for debugging. + const BufferCount = 3; + + /// The frame data, the current frame index, and the semaphore protecting + /// the frame data. This is used to implement double/triple/etc. buffering. + frames: [BufferCount]FrameState, + frame_index: std.math.IntFittingRange(0, BufferCount - 1) = 0, + frame_sema: std.Thread.Semaphore = .{ .permits = BufferCount }, + + device: objc.Object, // MTLDevice + + pub fn init() !GPUState { + var result: GPUState = .{ + .device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()), + .frames = undefined, + }; + + // Initialize all of our frame state. + for (&result.frames) |*frame| { + frame.* = try FrameState.init(result.device); + } + + return result; + } + + pub fn deinit(self: *GPUState) void { + // Wait for all of our inflight draws to complete so that + // we can cleanly deinit our GPU state. + for (0..BufferCount) |_| self.frame_sema.wait(); + for (&self.frames) |*frame| frame.deinit(); + } +}; + +/// State we need duplicated for every frame. Any state that could be +/// in a data race between the GPU and CPU while a frame is being +/// drawn should be in this struct. +/// +/// While a draw is in-process, we "lock" the state (via a semaphore) +/// and prevent the CPU from updating the state until Metal reports +/// that the frame is complete. +/// +/// This is used to implement double/triple buffering. +pub const FrameState = struct { + uniforms: UniformBuffer, + cells: CellBuffer, + cells_bg: CellBuffer, + + /// A buffer containing the uniform data. + const UniformBuffer = mtl_buffer.Buffer(mtl_shaders.Uniforms); + + pub fn init(device: objc.Object) !FrameState { + // Uniform buffer contains exactly 1 uniform struct. The + // uniform data will be undefined so this must be set before + // a frame is drawn. + var uniforms = try UniformBuffer.init(device, 1); + errdefer uniforms.deinit(); + + // Create the buffers for our vertex data. The preallocation size + // is likely too small but our first frame update will resize it. + var cells = try CellBuffer.init(device, 10 * 10); + errdefer cells.deinit(); + var cells_bg = try CellBuffer.init(device, 10 * 10); + errdefer cells_bg.deinit(); + + return .{ + .uniforms = uniforms, + .cells = cells, + .cells_bg = cells_bg, + }; + } + + pub fn deinit(self: *FrameState) void { + self.uniforms.deinit(); + self.cells.deinit(); + self.cells_bg.deinit(); + } +}; + pub const CustomShaderState = struct { /// The screen texture that we render the terminal to. If we don't have /// custom shaders, we render directly to the drawable. @@ -354,10 +439,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { errdefer font_shaper.deinit(); // Vertex buffers - var buf_cells = try CellBuffer.init(device, 160 * 160); - errdefer buf_cells.deinit(); - var buf_cells_bg = try CellBuffer.init(device, 160 * 160); - errdefer buf_cells_bg.deinit(); var buf_instance = try InstanceBuffer.initFill(device, &.{ 0, 1, 3, // Top-left triangle 1, 2, 3, // Bottom-right triangle @@ -433,6 +514,10 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }; }; + // Build our GPU state + var gpu_state = try GPUState.init(); + errdefer gpu_state.deinit(); + return Metal{ .alloc = alloc, .config = options.config, @@ -461,8 +546,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // Shaders .shaders = shaders, - .buf_cells = buf_cells, - .buf_cells_bg = buf_cells_bg, .buf_instance = buf_instance, // Metal stuff @@ -472,6 +555,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .texture_greyscale = font_critical.texture_greyscale, .texture_color = font_critical.texture_color, .custom_shader_state = custom_shader_state, + .gpu_state = gpu_state, }; } @@ -482,6 +566,9 @@ pub fn deinit(self: *Metal) void { // "self" self.inflight.wait(); + // All inflight frames are done, deinit our GPU state. + self.gpu_state.deinit(); + self.cells.deinit(self.alloc); self.cells_bg.deinit(self.alloc); @@ -496,8 +583,6 @@ pub fn deinit(self: *Metal) void { } self.image_placements.deinit(self.alloc); - self.buf_cells_bg.deinit(); - self.buf_cells.deinit(); self.buf_instance.deinit(); deinitMTLResource(self.texture_greyscale); deinitMTLResource(self.texture_color); @@ -721,6 +806,12 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { self.inflight.wait(); errdefer self.inflight.post(); + // Setup our frame data + const frame = &self.gpu_state.frames[self.gpu_state.frame_index]; + try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); + try frame.cells_bg.sync(self.gpu_state.device, self.cells_bg.items); + try frame.cells.sync(self.gpu_state.device, self.cells.items); + // If we have custom shaders, update the animation time. if (self.custom_shader_state) |*state| { const now = std.time.Instant.now() catch state.first_frame_time; @@ -818,13 +909,13 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells - try self.drawCells(encoder, &self.buf_cells_bg, self.cells_bg); + try self.drawCells(encoder, frame, frame.cells_bg, self.cells_bg.items.len); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells - try self.drawCells(encoder, &self.buf_cells, self.cells); + try self.drawCells(encoder, frame, frame.cells, self.cells.items.len); // Then draw remaining images try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); @@ -1121,13 +1212,10 @@ fn drawImagePlacement( fn drawCells( self: *Metal, encoder: objc.Object, - buf: *CellBuffer, - cells: std.ArrayListUnmanaged(mtl_shaders.Cell), + frame: *const FrameState, + buf: CellBuffer, + len: usize, ) !void { - if (cells.items.len == 0) return; - - try buf.sync(self.device, cells.items); - // Use our shader pipeline encoder.msgSend( void, @@ -1138,12 +1226,13 @@ fn drawCells( // Set our buffers encoder.msgSend( void, - objc.sel("setVertexBytes:length:atIndex:"), - .{ - @as(*const anyopaque, @ptrCast(&self.uniforms)), - @as(c_ulong, @sizeOf(@TypeOf(self.uniforms))), - @as(c_ulong, 1), - }, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, + ); + encoder.msgSend( + void, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, ); encoder.msgSend( void, @@ -1161,11 +1250,6 @@ fn drawCells( @as(c_ulong, 1), }, ); - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, - ); encoder.msgSend( void, @@ -1176,7 +1260,7 @@ fn drawCells( @intFromEnum(mtl.MTLIndexType.uint16), self.buf_instance.buffer.value, @as(c_ulong, 0), - @as(c_ulong, cells.items.len), + @as(c_ulong, len), }, ); } diff --git a/src/renderer/metal/buffer.zig b/src/renderer/metal/buffer.zig index eb5c1d193..b11861817 100644 --- a/src/renderer/metal/buffer.zig +++ b/src/renderer/metal/buffer.zig @@ -49,6 +49,21 @@ pub fn Buffer(comptime T: type) type { self.buffer.msgSend(void, objc.sel("release"), .{}); } + /// Get the buffer contents as a slice of T. The contents are + /// mutable. The contents may or may not be automatically synced + /// depending on the buffer storage mode. See the Metal docs. + pub fn contents(self: *Self) ![]T { + const len_bytes = self.buffer.getProperty(c_ulong, "length"); + assert(@mod(len_bytes, @sizeOf(T)) == 0); + const len = @divExact(len_bytes, @sizeOf(T)); + const ptr = self.buffer.msgSend( + ?[*]T, + objc.sel("contents"), + .{}, + ).?; + return ptr[0..len]; + } + /// Sync new contents to the buffer. pub fn sync(self: *Self, device: objc.Object, data: []const T) !void { // If we need more bytes than our buffer has, we need to reallocate. From 2dc8ae2ed77081d33634b7be0abf2c224c5711c8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Apr 2024 19:21:56 -0700 Subject: [PATCH 02/26] renderer/metal: move more frame state to the frame --- src/renderer/Metal.zig | 68 ++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 2999d9d32..50be3ebd8 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -114,10 +114,6 @@ buf_instance: InstanceBuffer, // MTLBuffer device: objc.Object, // MTLDevice queue: objc.Object, // MTLCommandQueue layer: objc.Object, // CAMetalLayer -texture_greyscale: objc.Object, // MTLTexture -texture_color: objc.Object, // MTLTexture -texture_greyscale_modified: usize = 0, -texture_color_modified: usize = 0, /// Custom shader state. This is only set if we have custom shaders. custom_shader_state: ?CustomShaderState = null, @@ -187,6 +183,11 @@ pub const FrameState = struct { cells: CellBuffer, cells_bg: CellBuffer, + greyscale: objc.Object, // MTLTexture + greyscale_modified: usize = 0, + color: objc.Object, // MTLTexture + color_modified: usize = 0, + /// A buffer containing the uniform data. const UniformBuffer = mtl_buffer.Buffer(mtl_shaders.Uniforms); @@ -204,10 +205,26 @@ pub const FrameState = struct { var cells_bg = try CellBuffer.init(device, 10 * 10); errdefer cells_bg.deinit(); + // Initialize our textures for our font atlas. + const greyscale = try initAtlasTexture(device, &.{ + .data = undefined, + .size = 8, + .format = .greyscale, + }); + errdefer deinitMTLResource(greyscale); + const color = try initAtlasTexture(device, &.{ + .data = undefined, + .size = 8, + .format = .rgba, + }); + errdefer deinitMTLResource(color); + return .{ .uniforms = uniforms, .cells = cells, .cells_bg = cells_bg, + .greyscale = greyscale, + .color = color, }; } @@ -215,6 +232,8 @@ pub const FrameState = struct { self.uniforms.deinit(); self.cells.deinit(); self.cells_bg.deinit(); + deinitMTLResource(self.greyscale); + deinitMTLResource(self.color); } }; @@ -494,23 +513,12 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // Initialize all the data that requires a critical font section. const font_critical: struct { metrics: font.Metrics, - texture_greyscale: objc.Object, - texture_color: objc.Object, } = font_critical: { const grid = options.font_grid; grid.lock.lockShared(); defer grid.lock.unlockShared(); - - // Font atlas textures - const greyscale = try initAtlasTexture(device, &grid.atlas_greyscale); - errdefer deinitMTLResource(greyscale); - const color = try initAtlasTexture(device, &grid.atlas_color); - errdefer deinitMTLResource(color); - break :font_critical .{ .metrics = grid.metrics, - .texture_greyscale = greyscale, - .texture_color = color, }; }; @@ -552,8 +560,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .device = device, .queue = queue, .layer = layer, - .texture_greyscale = font_critical.texture_greyscale, - .texture_color = font_critical.texture_color, .custom_shader_state = custom_shader_state, .gpu_state = gpu_state, }; @@ -584,8 +590,6 @@ pub fn deinit(self: *Metal) void { self.image_placements.deinit(self.alloc); self.buf_instance.deinit(); - deinitMTLResource(self.texture_greyscale); - deinitMTLResource(self.texture_color); self.queue.msgSend(void, objc.sel("release"), .{}); if (self.custom_shader_state) |*state| state.deinit(); @@ -652,8 +656,14 @@ pub fn setFocus(self: *Metal, focus: bool) !void { pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) void { // Update our grid self.font_grid = grid; - self.texture_greyscale_modified = 0; - self.texture_color_modified = 0; + + // Update all our textures so that they sync on the next frame. + // We can modify this without a lock because the GPU does not + // touch this data. + for (&self.gpu_state.frames) |*frame| { + frame.greyscale_modified = 0; + frame.color_modified = 0; + } // Get our metrics from the grid. This doesn't require a lock because // the metrics are never recalculated. @@ -841,19 +851,19 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // If our font atlas changed, sync the texture data texture: { const modified = self.font_grid.atlas_greyscale.modified.load(.monotonic); - if (modified <= self.texture_greyscale_modified) break :texture; + if (modified <= frame.greyscale_modified) break :texture; self.font_grid.lock.lockShared(); defer self.font_grid.lock.unlockShared(); - self.texture_greyscale_modified = self.font_grid.atlas_greyscale.modified.load(.monotonic); - try syncAtlasTexture(self.device, &self.font_grid.atlas_greyscale, &self.texture_greyscale); + frame.greyscale_modified = self.font_grid.atlas_greyscale.modified.load(.monotonic); + try syncAtlasTexture(self.device, &self.font_grid.atlas_greyscale, &frame.greyscale); } texture: { const modified = self.font_grid.atlas_color.modified.load(.monotonic); - if (modified <= self.texture_color_modified) break :texture; + if (modified <= frame.color_modified) break :texture; self.font_grid.lock.lockShared(); defer self.font_grid.lock.unlockShared(); - self.texture_color_modified = self.font_grid.atlas_color.modified.load(.monotonic); - try syncAtlasTexture(self.device, &self.font_grid.atlas_color, &self.texture_color); + frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic); + try syncAtlasTexture(self.device, &self.font_grid.atlas_color, &frame.color); } // Command buffer (MTLCommandBuffer) @@ -1238,7 +1248,7 @@ fn drawCells( void, objc.sel("setFragmentTexture:atIndex:"), .{ - self.texture_greyscale.value, + frame.greyscale.value, @as(c_ulong, 0), }, ); @@ -1246,7 +1256,7 @@ fn drawCells( void, objc.sel("setFragmentTexture:atIndex:"), .{ - self.texture_color.value, + frame.color.value, @as(c_ulong, 1), }, ); From 20bfbd9b2e26715d3501c9d094a5c88885ad9b02 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Apr 2024 19:26:17 -0700 Subject: [PATCH 03/26] renderer/metal: implement triple-buffering --- src/renderer/Metal.zig | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 50be3ebd8..7645e92b3 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -122,11 +122,6 @@ custom_shader_state: ?CustomShaderState = null, /// this will have to be part of the frame state. health: std.atomic.Value(Health) = .{ .raw = .healthy }, -/// Sempahore blocking our in-flight buffer updates. For now this is just -/// one but in the future if we implement double/triple-buffering this -/// will be incremented. -inflight: std.Thread.Semaphore = .{ .permits = 1 }, - /// Our GPU state gpu_state: GPUState, @@ -167,6 +162,21 @@ pub const GPUState = struct { for (0..BufferCount) |_| self.frame_sema.wait(); for (&self.frames) |*frame| frame.deinit(); } + + /// Get the next frame state to draw to. This will wait on the + /// semaphore to ensure that the frame is available. This must + /// always be paired with a call to releaseFrame. + pub fn nextFrame(self: *GPUState) *FrameState { + self.frame_sema.wait(); + errdefer self.frame_sema.post(); + self.frame_index = (self.frame_index + 1) % BufferCount; + return &self.frames[self.frame_index]; + } + + /// This should be called when the frame has completed drawing. + pub fn releaseFrame(self: *GPUState) void { + self.frame_sema.post(); + } }; /// State we need duplicated for every frame. Any state that could be @@ -566,13 +576,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { } pub fn deinit(self: *Metal) void { - // If we have inflight buffers, wait for completion. This ensures that - // any pending GPU operations are completed before we start deallocating - // everything. This is important because our completion callbacks access - // "self" - self.inflight.wait(); - - // All inflight frames are done, deinit our GPU state. self.gpu_state.deinit(); self.cells.deinit(self.alloc); @@ -812,12 +815,12 @@ pub fn updateFrame( pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { _ = surface; - // Wait for a buffer to be available. - self.inflight.wait(); - errdefer self.inflight.post(); + // Wait for a frame to be available. + const frame = self.gpu_state.nextFrame(); + errdefer self.gpu_state.releaseFrame(); + //log.debug("drawing frame index={}", .{self.gpu_state.frame_index}); // Setup our frame data - const frame = &self.gpu_state.frames[self.gpu_state.frame_index]; try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); try frame.cells_bg.sync(self.gpu_state.device, self.cells_bg.items); try frame.cells.sync(self.gpu_state.device, self.cells.items); @@ -1036,7 +1039,7 @@ fn bufferCompleted( } // Always release our semaphore - self.inflight.post(); + self.gpu_state.releaseFrame(); } fn drawPostShader( From 80930885152bc1979462f75ca8160ea39f2e75bf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Apr 2024 19:32:17 -0700 Subject: [PATCH 04/26] renderer/metal: move more metal objects into GPUState --- src/renderer/Metal.zig | 66 ++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 7645e92b3..bba162167 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -108,11 +108,8 @@ image_text_end: u32 = 0, /// Metal state shaders: Shaders, // Compiled shaders -buf_instance: InstanceBuffer, // MTLBuffer /// Metal objects -device: objc.Object, // MTLDevice -queue: objc.Object, // MTLCommandQueue layer: objc.Object, // CAMetalLayer /// Custom shader state. This is only set if we have custom shaders. @@ -141,10 +138,26 @@ pub const GPUState = struct { frame_sema: std.Thread.Semaphore = .{ .permits = BufferCount }, device: objc.Object, // MTLDevice + queue: objc.Object, // MTLCommandQueue + + /// This buffer is written exactly once so we can use it globally. + instance: InstanceBuffer, // MTLBuffer pub fn init() !GPUState { + const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); + const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); + errdefer queue.msgSend(void, objc.sel("release"), .{}); + + var instance = try InstanceBuffer.initFill(device, &.{ + 0, 1, 3, // Top-left triangle + 1, 2, 3, // Bottom-right triangle + }); + errdefer instance.deinit(); + var result: GPUState = .{ - .device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()), + .device = device, + .queue = queue, + .instance = instance, .frames = undefined, }; @@ -161,6 +174,8 @@ pub const GPUState = struct { // we can cleanly deinit our GPU state. for (0..BufferCount) |_| self.frame_sema.wait(); for (&self.frames) |*frame| frame.deinit(); + self.instance.deinit(); + self.queue.msgSend(void, objc.sel("release"), .{}); } /// Get the next frame state to draw to. This will wait on the @@ -423,8 +438,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }; // Initialize our metal stuff - const device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); - const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); + var gpu_state = try GPUState.init(); + errdefer gpu_state.deinit(); // Get our CAMetalLayer const layer = switch (builtin.os.tag) { @@ -438,7 +453,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { else => @compileError("unsupported target for Metal"), }; - layer.setProperty("device", device.value); + layer.setProperty("device", gpu_state.device.value); layer.setProperty("opaque", options.config.background_opacity >= 1); layer.setProperty("displaySyncEnabled", false); // disable v-sync @@ -467,13 +482,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }); errdefer font_shaper.deinit(); - // Vertex buffers - var buf_instance = try InstanceBuffer.initFill(device, &.{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }); - errdefer buf_instance.deinit(); - // Load our custom shaders const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( arena_alloc, @@ -489,7 +497,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { if (custom_shaders.len == 0) break :state null; // Build our sampler for our texture - var sampler = try mtl_sampler.Sampler.init(device); + var sampler = try mtl_sampler.Sampler.init(gpu_state.device); errdefer sampler.deinit(); break :state .{ @@ -517,7 +525,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { errdefer if (custom_shader_state) |*state| state.deinit(); // Initialize our shaders - var shaders = try Shaders.init(alloc, device, custom_shaders); + var shaders = try Shaders.init(alloc, gpu_state.device, custom_shaders); errdefer shaders.deinit(alloc); // Initialize all the data that requires a critical font section. @@ -532,10 +540,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }; }; - // Build our GPU state - var gpu_state = try GPUState.init(); - errdefer gpu_state.deinit(); - return Metal{ .alloc = alloc, .config = options.config, @@ -564,11 +568,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // Shaders .shaders = shaders, - .buf_instance = buf_instance, // Metal stuff - .device = device, - .queue = queue, .layer = layer, .custom_shader_state = custom_shader_state, .gpu_state = gpu_state, @@ -592,9 +593,6 @@ pub fn deinit(self: *Metal) void { } self.image_placements.deinit(self.alloc); - self.buf_instance.deinit(); - self.queue.msgSend(void, objc.sel("release"), .{}); - if (self.custom_shader_state) |*state| state.deinit(); self.shaders.deinit(self.alloc); @@ -797,7 +795,7 @@ pub fn updateFrame( .replace_grey_alpha, .replace_rgb, .replace_rgba, - => try kv.value_ptr.image.upload(self.alloc, self.device), + => try kv.value_ptr.image.upload(self.alloc, self.gpu_state.device), .unload_pending, .unload_replace, @@ -858,7 +856,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { self.font_grid.lock.lockShared(); defer self.font_grid.lock.unlockShared(); frame.greyscale_modified = self.font_grid.atlas_greyscale.modified.load(.monotonic); - try syncAtlasTexture(self.device, &self.font_grid.atlas_greyscale, &frame.greyscale); + try syncAtlasTexture(self.gpu_state.device, &self.font_grid.atlas_greyscale, &frame.greyscale); } texture: { const modified = self.font_grid.atlas_color.modified.load(.monotonic); @@ -866,11 +864,11 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { self.font_grid.lock.lockShared(); defer self.font_grid.lock.unlockShared(); frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic); - try syncAtlasTexture(self.device, &self.font_grid.atlas_color, &frame.color); + try syncAtlasTexture(self.gpu_state.device, &self.font_grid.atlas_color, &frame.color); } // Command buffer (MTLCommandBuffer) - const buffer = self.queue.msgSend(objc.Object, objc.sel("commandBuffer"), .{}); + const buffer = self.gpu_state.queue.msgSend(objc.Object, objc.sel("commandBuffer"), .{}); { // MTLRenderPassDescriptor @@ -1150,7 +1148,7 @@ fn drawImagePlacement( // Create our vertex buffer, which is always exactly one item. // future(mitchellh): we can group rendering multiple instances of a single image const Buffer = mtl_buffer.Buffer(mtl_shaders.Image); - var buf = try Buffer.initFill(self.device, &.{.{ + var buf = try Buffer.initFill(self.gpu_state.device, &.{.{ .grid_pos = .{ @as(f32, @floatFromInt(p.x)), @as(f32, @floatFromInt(p.y)), @@ -1208,7 +1206,7 @@ fn drawImagePlacement( @intFromEnum(mtl.MTLPrimitiveType.triangle), @as(c_ulong, 6), @intFromEnum(mtl.MTLIndexType.uint16), - self.buf_instance.buffer.value, + self.gpu_state.instance.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1), }, @@ -1271,7 +1269,7 @@ fn drawCells( @intFromEnum(mtl.MTLPrimitiveType.triangle), @as(c_ulong, 6), @intFromEnum(mtl.MTLIndexType.uint16), - self.buf_instance.buffer.value, + self.gpu_state.instance.buffer.value, @as(c_ulong, 0), @as(c_ulong, len), }, @@ -1573,7 +1571,7 @@ pub fn setScreenSize( // If we fail to create the texture, then we just don't have a screen // texture and our custom shaders won't run. - const id = self.device.msgSend( + const id = self.gpu_state.device.msgSend( ?*anyopaque, objc.sel("newTextureWithDescriptor:"), .{desc}, From 7a6a2b07529c716f79c27b45afd430b476c23635 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 21 Apr 2024 19:42:48 -0700 Subject: [PATCH 05/26] renderer/metal: don't draw with zero instances --- src/renderer/Metal.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index bba162167..1371b6ceb 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -816,7 +816,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // Wait for a frame to be available. const frame = self.gpu_state.nextFrame(); errdefer self.gpu_state.releaseFrame(); - //log.debug("drawing frame index={}", .{self.gpu_state.frame_index}); + // log.debug("drawing frame index={}", .{self.gpu_state.frame_index}); // Setup our frame data try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); @@ -1227,6 +1227,10 @@ fn drawCells( buf: CellBuffer, len: usize, ) !void { + // This triggers an assertion in the Metal API if we try to draw + // with an instance count of 0 so just bail. + if (len == 0) return; + // Use our shader pipeline encoder.msgSend( void, From e8b623e82919f620d2e64f8eed00f2988fc073fd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Apr 2024 10:01:59 -0700 Subject: [PATCH 06/26] renderer/metal: dedicated cell bg shader --- src/renderer/Metal.zig | 46 ++++++++- src/renderer/metal/shaders.zig | 176 ++++++++++++++++++++++++++++++++ src/renderer/shaders/cell.metal | 72 +++++++++++++ 3 files changed, 293 insertions(+), 1 deletion(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 1371b6ceb..d7b0024a1 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -920,7 +920,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells - try self.drawCells(encoder, frame, frame.cells_bg, self.cells_bg.items.len); + try self.drawCellBgs(encoder, frame, self.cells_bg.items.len); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); @@ -1215,6 +1215,50 @@ fn drawImagePlacement( // log.debug("drawImagePlacement: {}", .{p}); } +/// Draw the cell backgrounds. +fn drawCellBgs( + self: *Metal, + encoder: objc.Object, + frame: *const FrameState, + len: usize, +) !void { + // This triggers an assertion in the Metal API if we try to draw + // with an instance count of 0 so just bail. + if (len == 0) return; + + // Use our shader pipeline + encoder.msgSend( + void, + objc.sel("setRenderPipelineState:"), + .{self.shaders.cell_bg_pipeline.value}, + ); + + // Set our buffers + encoder.msgSend( + void, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, + ); + encoder.msgSend( + void, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, + ); + + encoder.msgSend( + void, + objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), + .{ + @intFromEnum(mtl.MTLPrimitiveType.triangle), + @as(c_ulong, 6), + @intFromEnum(mtl.MTLIndexType.uint16), + self.gpu_state.instance.buffer.value, + @as(c_ulong, 0), + @as(c_ulong, len), + }, + ); +} + /// Loads some set of cell data into our buffer and issues a draw call. /// This expects all the Metal command encoder state to be setup. /// diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index d5a6baccb..6d2e9addb 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -18,6 +18,10 @@ pub const Shaders = struct { /// foreground. cell_pipeline: objc.Object, + /// The cell background shader is the shader used to render the + /// background of terminal cells. + cell_bg_pipeline: objc.Object, + /// The image shader is the shader used to render images for things /// like the Kitty image protocol. image_pipeline: objc.Object, @@ -43,6 +47,9 @@ pub const Shaders = struct { const cell_pipeline = try initCellPipeline(device, library); errdefer cell_pipeline.msgSend(void, objc.sel("release"), .{}); + const cell_bg_pipeline = try initCellBgPipeline(device, library); + errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{}); + const image_pipeline = try initImagePipeline(device, library); errdefer image_pipeline.msgSend(void, objc.sel("release"), .{}); @@ -66,6 +73,7 @@ pub const Shaders = struct { return .{ .library = library, .cell_pipeline = cell_pipeline, + .cell_bg_pipeline = cell_bg_pipeline, .image_pipeline = image_pipeline, .post_pipelines = post_pipelines, }; @@ -74,6 +82,7 @@ pub const Shaders = struct { pub fn deinit(self: *Shaders, alloc: Allocator) void { // Release our primary shaders self.cell_pipeline.msgSend(void, objc.sel("release"), .{}); + self.cell_bg_pipeline.msgSend(void, objc.sel("release"), .{}); self.image_pipeline.msgSend(void, objc.sel("release"), .{}); self.library.msgSend(void, objc.sel("release"), .{}); @@ -493,6 +502,173 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { return pipeline_state; } +/// This is a single parameter for the cell bg shader. +pub const CellBg = extern struct { + mode: Mode, + grid_pos: [2]f32, + cell_width: u8, + color: [4]u8, + + pub const Mode = enum(u8) { + rgb = 1, + }; +}; + +/// Initialize the cell background render pipeline for our shader library. +fn initCellBgPipeline(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( + "cell_bg_vertex", + .utf8, + false, + ); + defer str.release(); + + const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + break :func_vert objc.Object.fromId(ptr.?); + }; + defer func_vert.msgSend(void, objc.sel("release"), .{}); + const func_frag = func_frag: { + const str = try macos.foundation.String.createWithBytes( + "cell_bg_fragment", + .utf8, + false, + ); + defer str.release(); + + const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + break :func_frag objc.Object.fromId(ptr.?); + }; + defer func_frag.msgSend(void, objc.sel("release"), .{}); + + // Create the vertex descriptor. The vertex descriptor describes the + // data layout of the vertex inputs. We use indexed (or "instanced") + // rendering, so this makes it so that each instance gets a single + // Cell as input. + const vertex_desc = vertex_desc: { + const desc = init: { + const Class = objc.getClass("MTLVertexDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + + // 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", @intFromEnum(mtl.MTLVertexFormat.uchar)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "mode"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 1)}, + ); + + attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "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", @intFromEnum(mtl.MTLVertexFormat.uchar)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "cell_width"))); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } + { + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 3)}, + ); + + attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4)); + attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "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")); + { + const layout = layouts.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + // Access each Cell per instance, not per vertex. + layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance)); + layout.setProperty("stride", @as(c_ulong, @sizeOf(Cell))); + } + + break :vertex_desc desc; + }; + defer vertex_desc.msgSend(void, objc.sel("release"), .{}); + + // Create our descriptor + const desc = init: { + const Class = objc.getClass("MTLRenderPipelineDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + defer desc.msgSend(void, objc.sel("release"), .{}); + + // Set our properties + desc.setProperty("vertexFunction", func_vert); + desc.setProperty("fragmentFunction", func_frag); + desc.setProperty("vertexDescriptor", vertex_desc); + + // Set our color attachment + const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); + { + const attachment = attachments.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + // Value is MTLPixelFormatBGRA8Unorm + attachment.setProperty("pixelFormat", @as(c_ulong, 80)); + + // Blending. This is required so that our text we render on top + // of our drawable properly blends into the bg. + attachment.setProperty("blendingEnabled", true); + attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); + attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); + attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); + attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); + attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); + attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); + } + + // Make our state + var err: ?*anyopaque = null; + const pipeline_state = device.msgSend( + objc.Object, + objc.sel("newRenderPipelineStateWithDescriptor:error:"), + .{ desc, &err }, + ); + try checkError(err); + errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); + + return pipeline_state; +} + /// Initialize the image render pipeline for our shader library. fn initImagePipeline(device: objc.Object, library: objc.Object) !objc.Object { // Get our vertex and fragment functions diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 10e5804cc..a1c4389b1 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -231,6 +231,78 @@ fragment float4 uber_fragment(VertexOut in [[stage_in]], } } +//------------------------------------------------------------------- +// Cell Background Shader +//------------------------------------------------------------------- +#pragma mark - Cell BG Shader + +// The possible modes that a cell bg entry can take. +enum CellbgMode : uint8_t { + MODE_RGB = 1u, +}; + +struct CellBgVertexIn { + // The mode for this cell. + uint8_t mode [[attribute(0)]]; + + // The grid coordinates (x, y) where x < columns and y < rows + float2 grid_pos [[attribute(1)]]; + + // The width of the cell in cells (i.e. 2 for double-wide). + uint8_t cell_width [[attribute(2)]]; + + // 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(3)]]; +}; + +struct CellBgVertexOut { + float4 position [[position]]; + float4 color; +}; + +vertex CellBgVertexOut cell_bg_vertex(unsigned int vid [[vertex_id]], + CellBgVertexIn input [[stage_in]], + constant Uniforms& uniforms + [[buffer(1)]]) { + // Convert the grid x,y into world space x, y by accounting for cell size + float2 cell_pos = uniforms.cell_size * input.grid_pos; + + // Scaled cell size for the cell width + float2 cell_size_scaled = uniforms.cell_size; + cell_size_scaled.x = cell_size_scaled.x * input.cell_width; + + // Turn the cell position into a vertex point depending on the + // vertex ID. Since we use instanced drawing, we have 4 vertices + // for each corner of the cell. We can use vertex ID to determine + // which one we're looking at. Using this, we can use 1 or 0 to keep + // or discard the value for the vertex. + // + // 0 = top-right + // 1 = bot-right + // 2 = bot-left + // 3 = top-left + float2 position; + position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; + position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; + + // Calculate the final position of our cell in world space. + // We have to add our cell size since our vertices are offset + // one cell up and to the left. (Do the math to verify yourself) + cell_pos = cell_pos + cell_size_scaled * position; + + CellBgVertexOut out; + out.color = float4(input.color) / 255.0f; + out.position = + uniforms.projection_matrix * float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); + + return out; +} + +fragment float4 cell_bg_fragment(CellBgVertexOut in [[stage_in]]) { + return in.color; +} + //------------------------------------------------------------------- // Image Shader //------------------------------------------------------------------- From d12e3db599a95f1bb90187f0c840c5e4fc9b7d3e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Apr 2024 10:58:23 -0700 Subject: [PATCH 07/26] renderer/metal: dedicated cell fg shader --- src/renderer/Metal.zig | 37 ++-- src/renderer/metal/shaders.zig | 99 +++++----- src/renderer/shaders/cell.metal | 336 +++++++++++++++----------------- 3 files changed, 225 insertions(+), 247 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index d7b0024a1..f6b0d1ee1 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -36,7 +36,6 @@ const Image = mtl_image.Image; const ImageMap = mtl_image.ImageMap; const Shaders = mtl_shaders.Shaders; -const CellBuffer = mtl_buffer.Buffer(mtl_shaders.Cell); const ImageBuffer = mtl_buffer.Buffer(mtl_shaders.Image); const InstanceBuffer = mtl_buffer.Buffer(u16); @@ -90,8 +89,8 @@ current_background_color: terminal.color.RGB, /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. -cells_bg: std.ArrayListUnmanaged(mtl_shaders.Cell), -cells: std.ArrayListUnmanaged(mtl_shaders.Cell), +cells_bg: std.ArrayListUnmanaged(mtl_shaders.CellBg), +cells: std.ArrayListUnmanaged(mtl_shaders.CellText), /// The current GPU uniform values. uniforms: mtl_shaders.Uniforms, @@ -205,8 +204,8 @@ pub const GPUState = struct { /// This is used to implement double/triple buffering. pub const FrameState = struct { uniforms: UniformBuffer, - cells: CellBuffer, - cells_bg: CellBuffer, + cells: CellTextBuffer, + cells_bg: CellBgBuffer, greyscale: objc.Object, // MTLTexture greyscale_modified: usize = 0, @@ -215,6 +214,8 @@ pub const FrameState = struct { /// A buffer containing the uniform data. const UniformBuffer = mtl_buffer.Buffer(mtl_shaders.Uniforms); + const CellBgBuffer = mtl_buffer.Buffer(mtl_shaders.CellBg); + const CellTextBuffer = mtl_buffer.Buffer(mtl_shaders.CellText); pub fn init(device: objc.Object) !FrameState { // Uniform buffer contains exactly 1 uniform struct. The @@ -225,9 +226,9 @@ pub const FrameState = struct { // Create the buffers for our vertex data. The preallocation size // is likely too small but our first frame update will resize it. - var cells = try CellBuffer.init(device, 10 * 10); + var cells = try CellTextBuffer.init(device, 10 * 10); errdefer cells.deinit(); - var cells_bg = try CellBuffer.init(device, 10 * 10); + var cells_bg = try CellBgBuffer.init(device, 10 * 10); errdefer cells_bg.deinit(); // Initialize our textures for our font atlas. @@ -1268,7 +1269,7 @@ fn drawCells( self: *Metal, encoder: objc.Object, frame: *const FrameState, - buf: CellBuffer, + buf: FrameState.CellTextBuffer, len: usize, ) !void { // This triggers an assertion in the Metal API if we try to draw @@ -1279,7 +1280,7 @@ fn drawCells( encoder.msgSend( void, objc.sel("setRenderPipelineState:"), - .{self.shaders.cell_pipeline.value}, + .{self.shaders.cell_text_pipeline.value}, ); // Set our buffers @@ -1694,7 +1695,7 @@ fn rebuildCells( // This is the cell that has [mode == .fg] and is underneath our cursor. // We keep track of it so that we can invert the colors so the character // remains visible. - var cursor_cell: ?mtl_shaders.Cell = null; + var cursor_cell: ?mtl_shaders.CellText = null; // Build each cell var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); @@ -1847,12 +1848,6 @@ fn rebuildCells( self.cells.appendAssumeCapacity(cell.*); } } - - // Some debug mode safety checks - if (std.debug.runtime_safety) { - for (self.cells_bg.items) |cell| assert(cell.mode == .bg); - for (self.cells.items) |cell| assert(cell.mode != .bg); - } } fn updateCell( @@ -1964,11 +1959,10 @@ fn updateCell( }; self.cells_bg.appendAssumeCapacity(.{ - .mode = .bg, + .mode = .rgb, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, .cell_width = cell.gridWidth(), .color = .{ rgb.r, rgb.g, rgb.b, bg_alpha }, - .bg_color = .{ 0, 0, 0, 0 }, }); break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha }; @@ -1992,7 +1986,7 @@ fn updateCell( }, ); - const mode: mtl_shaders.Cell.Mode = switch (try fgMode( + const mode: mtl_shaders.CellText.Mode = switch (try fgMode( render.presentation, cell_pin, )) { @@ -2080,7 +2074,7 @@ fn addCursor( self: *Metal, screen: *terminal.Screen, cursor_style: renderer.CursorStyle, -) ?*const mtl_shaders.Cell { +) ?*const mtl_shaders.CellText { // Add the cursor. We render the cursor over the wide character if // we're on the wide characer tail. const wide, const x = cell: { @@ -2166,11 +2160,10 @@ fn addPreeditCell( // Add our opaque background cell self.cells_bg.appendAssumeCapacity(.{ - .mode = .bg, + .mode = .rgb, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, .cell_width = if (cp.wide) 2 else 1, .color = .{ bg.r, bg.g, bg.b, 255 }, - .bg_color = .{ bg.r, bg.g, bg.b, 255 }, }); // Add our text diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 6d2e9addb..40e048984 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -16,7 +16,7 @@ pub const Shaders = struct { /// The cell shader is the shader used to render the terminal cells. /// It is a single shader that is used for both the background and /// foreground. - cell_pipeline: objc.Object, + cell_text_pipeline: objc.Object, /// The cell background shader is the shader used to render the /// background of terminal cells. @@ -44,8 +44,8 @@ pub const Shaders = struct { const library = try initLibrary(device); errdefer library.msgSend(void, objc.sel("release"), .{}); - const cell_pipeline = try initCellPipeline(device, library); - errdefer cell_pipeline.msgSend(void, objc.sel("release"), .{}); + const cell_text_pipeline = try initCellTextPipeline(device, library); + errdefer cell_text_pipeline.msgSend(void, objc.sel("release"), .{}); const cell_bg_pipeline = try initCellBgPipeline(device, library); errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{}); @@ -72,7 +72,7 @@ pub const Shaders = struct { return .{ .library = library, - .cell_pipeline = cell_pipeline, + .cell_text_pipeline = cell_text_pipeline, .cell_bg_pipeline = cell_bg_pipeline, .image_pipeline = image_pipeline, .post_pipelines = post_pipelines, @@ -81,7 +81,7 @@ pub const Shaders = struct { pub fn deinit(self: *Shaders, alloc: Allocator) void { // Release our primary shaders - self.cell_pipeline.msgSend(void, objc.sel("release"), .{}); + self.cell_text_pipeline.msgSend(void, objc.sel("release"), .{}); self.cell_bg_pipeline.msgSend(void, objc.sel("release"), .{}); self.image_pipeline.msgSend(void, objc.sel("release"), .{}); self.library.msgSend(void, objc.sel("release"), .{}); @@ -96,25 +96,6 @@ pub const Shaders = struct { } }; -/// This is a single parameter for the terminal cell shader. -pub const Cell = extern struct { - mode: Mode, - grid_pos: [2]f32, - glyph_pos: [2]u32 = .{ 0, 0 }, - glyph_size: [2]u32 = .{ 0, 0 }, - glyph_offset: [2]i32 = .{ 0, 0 }, - color: [4]u8, - bg_color: [4]u8, - cell_width: u8, - - pub const Mode = enum(u8) { - bg = 1, - fg = 2, - fg_constrained = 3, - fg_color = 7, - }; -}; - /// Single parameter for the image shader. See shader for field details. pub const Image = extern struct { grid_pos: [2]f32, @@ -303,12 +284,31 @@ fn initPostPipeline( return pipeline_state; } +/// This is a single parameter for the terminal cell shader. +pub const CellText = extern struct { + mode: Mode, + grid_pos: [2]f32, + glyph_pos: [2]u32 = .{ 0, 0 }, + glyph_size: [2]u32 = .{ 0, 0 }, + glyph_offset: [2]i32 = .{ 0, 0 }, + color: [4]u8, + bg_color: [4]u8, + cell_width: u8, + + pub const Mode = enum(u8) { + bg = 1, + fg = 2, + fg_constrained = 3, + fg_color = 7, + }; +}; + /// Initialize the cell render pipeline for our shader library. -fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { +fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object { // Get our vertex and fragment functions const func_vert = func_vert: { const str = try macos.foundation.String.createWithBytes( - "uber_vertex", + "cell_text_vertex", .utf8, false, ); @@ -319,7 +319,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { }; const func_frag = func_frag: { const str = try macos.foundation.String.createWithBytes( - "uber_fragment", + "cell_text_fragment", .utf8, false, ); @@ -353,7 +353,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "mode"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "mode"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -364,7 +364,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "grid_pos"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "grid_pos"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -375,7 +375,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_pos"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_pos"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -386,7 +386,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uint2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_size"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_size"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -397,7 +397,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.int2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "glyph_offset"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "glyph_offset"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -408,7 +408,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "color"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "color"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -419,7 +419,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "bg_color"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "bg_color"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -430,7 +430,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "cell_width"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "cell_width"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } @@ -445,7 +445,7 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { // Access each Cell per instance, not per vertex. layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance)); - layout.setProperty("stride", @as(c_ulong, @sizeOf(Cell))); + layout.setProperty("stride", @as(c_ulong, @sizeOf(CellText))); } break :vertex_desc desc; @@ -506,8 +506,8 @@ fn initCellPipeline(device: objc.Object, library: objc.Object) !objc.Object { pub const CellBg = extern struct { mode: Mode, grid_pos: [2]f32, - cell_width: u8, color: [4]u8, + cell_width: u8, pub const Mode = enum(u8) { rgb = 1, @@ -575,7 +575,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "grid_pos"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "grid_pos"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -586,7 +586,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "cell_width"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "cell_width"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } { @@ -597,7 +597,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { ); attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.uchar4)); - attr.setProperty("offset", @as(c_ulong, @offsetOf(Cell, "color"))); + attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "color"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } @@ -612,7 +612,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { // Access each Cell per instance, not per vertex. layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance)); - layout.setProperty("stride", @as(c_ulong, @sizeOf(Cell))); + layout.setProperty("stride", @as(c_ulong, @sizeOf(CellBg))); } break :vertex_desc desc; @@ -840,11 +840,20 @@ fn checkError(err_: ?*anyopaque) !void { // on macOS 12 or Apple Silicon macOS 13. // // To be safe, we put this test in here. -test "Cell offsets" { +test "CellText offsets" { const testing = std.testing; - const alignment = @alignOf(Cell); - inline for (@typeInfo(Cell).Struct.fields) |field| { - const offset = @offsetOf(Cell, field.name); + const alignment = @alignOf(CellText); + inline for (@typeInfo(CellText).Struct.fields) |field| { + const offset = @offsetOf(CellText, field.name); + try testing.expectEqual(0, @mod(offset, alignment)); + } +} + +test "CellBg offsets" { + const testing = std.testing; + const alignment = @alignOf(CellBg); + inline for (@typeInfo(CellBg).Struct.fields) |field| { + const offset = @offsetOf(CellBg, field.name); try testing.expectEqual(0, @mod(offset, alignment)); } } diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index a1c4389b1..dd9ace86a 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -1,58 +1,11 @@ using namespace metal; -// The possible modes that a shader can take. -enum Mode : uint8_t { - MODE_BG = 1u, - MODE_FG = 2u, - MODE_FG_CONSTRAINED = 3u, - MODE_FG_COLOR = 7u, -}; - struct Uniforms { float4x4 projection_matrix; float2 cell_size; float min_contrast; }; -struct VertexIn { - // The mode for this cell. - uint8_t mode [[attribute(0)]]; - - // The grid coordinates (x, y) where x < columns and y < rows - float2 grid_pos [[attribute(1)]]; - - // The width of the cell in cells (i.e. 2 for double-wide). - uint8_t cell_width [[attribute(6)]]; - - // The color. For BG modes, this is the bg color, for FG modes this is - // the text color. For styles, this is the color of the style. - uchar4 color [[attribute(5)]]; - - // The fields below are present only when rendering text (fg mode) - - // The background color of the cell. This is used to determine if - // we need to render the text with a different color to ensure - // contrast. - uchar4 bg_color [[attribute(7)]]; - - // The position of the glyph in the texture (x,y) - uint2 glyph_pos [[attribute(2)]]; - - // The size of the glyph in the texture (w,h) - uint2 glyph_size [[attribute(3)]]; - - // The left and top bearings for the glyph (x,y) - int2 glyph_offset [[attribute(4)]]; -}; - -struct VertexOut { - float4 position [[position]]; - float2 cell_size; - uint8_t mode; - float4 color; - float2 tex_coord; -}; - //------------------------------------------------------------------- // Color Functions //------------------------------------------------------------------- @@ -102,142 +55,13 @@ float4 contrasted_color(float min, float4 fg, float4 bg) { return fg; } -//------------------------------------------------------------------- -// Terminal Grid Cell Shader -//------------------------------------------------------------------- -#pragma mark - Terminal Grid Cell Shader - -vertex VertexOut uber_vertex(unsigned int vid [[vertex_id]], - VertexIn input [[stage_in]], - constant Uniforms& uniforms [[buffer(1)]]) { - // Convert the grid x,y into world space x, y by accounting for cell size - float2 cell_pos = uniforms.cell_size * input.grid_pos; - - // Scaled cell size for the cell width - float2 cell_size_scaled = uniforms.cell_size; - cell_size_scaled.x = cell_size_scaled.x * input.cell_width; - - // Turn the cell position into a vertex point depending on the - // vertex ID. Since we use instanced drawing, we have 4 vertices - // for each corner of the cell. We can use vertex ID to determine - // which one we're looking at. Using this, we can use 1 or 0 to keep - // or discard the value for the vertex. - // - // 0 = top-right - // 1 = bot-right - // 2 = bot-left - // 3 = top-left - float2 position; - position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; - position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; - - VertexOut out; - out.mode = input.mode; - out.cell_size = uniforms.cell_size; - out.color = float4(input.color) / 255.0f; - switch (input.mode) { - case MODE_BG: - // Calculate the final position of our cell in world space. - // We have to add our cell size since our vertices are offset - // one cell up and to the left. (Do the math to verify yourself) - cell_pos = cell_pos + cell_size_scaled * position; - - out.position = uniforms.projection_matrix * - float4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); - break; - - case MODE_FG: - case MODE_FG_CONSTRAINED: - case MODE_FG_COLOR: { - float2 glyph_size = float2(input.glyph_size); - float2 glyph_offset = float2(input.glyph_offset); - - // The glyph_offset.y is the y bearing, a y value that when added - // to the baseline is the offset (+y is up). Our grid goes down. - // So we flip it with `cell_size.y - glyph_offset.y`. - glyph_offset.y = cell_size_scaled.y - glyph_offset.y; - - // If we're constrained then we need to scale the glyph. - // We also always constrain colored glyphs since we should have - // their scaled cell size exactly correct. - if (input.mode == MODE_FG_CONSTRAINED || input.mode == MODE_FG_COLOR) { - if (glyph_size.x > cell_size_scaled.x) { - float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x); - glyph_offset.y += (glyph_size.y - new_y) / 2; - glyph_size.y = new_y; - glyph_size.x = cell_size_scaled.x; - } - } - - // 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. - out.tex_coord = - float2(input.glyph_pos) + float2(input.glyph_size) * position; - - // If we have a minimum contrast, we need to check if we need to - // change the color of the text to ensure it has enough contrast - // with the background. - if (uniforms.min_contrast > 1.0f && input.mode == MODE_FG) { - float4 bg_color = float4(input.bg_color) / 255.0f; - out.color = - contrasted_color(uniforms.min_contrast, out.color, bg_color); - } - - break; - } - } - - return out; -} - -fragment float4 uber_fragment(VertexOut in [[stage_in]], - texture2d textureGreyscale [[texture(0)]], - texture2d textureColor [[texture(1)]]) { - constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); - - switch (in.mode) { - case MODE_BG: - return in.color; - - case MODE_FG_CONSTRAINED: - 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; - - // We premult the alpha to our whole color since our blend function - // uses One/OneMinusSourceAlpha to avoid blurry edges. - // We first premult our given color. - float4 premult = float4(in.color.rgb * in.color.a, in.color.a); - // Then premult the texture color - float a = textureGreyscale.sample(textureSampler, coord).r; - premult = premult * a; - return premult; - } - - 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); - } - } -} - //------------------------------------------------------------------- // Cell Background Shader //------------------------------------------------------------------- #pragma mark - Cell BG Shader // The possible modes that a cell bg entry can take. -enum CellbgMode : uint8_t { +enum CellBgMode : uint8_t { MODE_RGB = 1u, }; @@ -248,12 +72,12 @@ struct CellBgVertexIn { // 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(2)]]; - // 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(3)]]; + + // The width of the cell in cells (i.e. 2 for double-wide). + uint8_t cell_width [[attribute(2)]]; }; struct CellBgVertexOut { @@ -303,6 +127,158 @@ fragment float4 cell_bg_fragment(CellBgVertexOut in [[stage_in]]) { return in.color; } +//------------------------------------------------------------------- +// Cell Text Shader +//------------------------------------------------------------------- +#pragma mark - Cell Text Shader + +// The possible modes that a cell fg entry can take. +enum CellTextMode : uint8_t { + MODE_TEXT = 2u, + MODE_TEXT_CONSTRAINED = 3u, + MODE_TEXT_COLOR = 7u, +}; + +struct CellTextVertexIn { + // The mode for this cell. + uint8_t mode [[attribute(0)]]; + + // The grid coordinates (x, y) where x < columns and y < rows + float2 grid_pos [[attribute(1)]]; + + // The width of the cell in cells (i.e. 2 for double-wide). + uint8_t cell_width [[attribute(6)]]; + + // The color of the rendered text glyph. + uchar4 color [[attribute(5)]]; + + // The background color of the cell. This is used to determine if + // we need to render the text with a different color to ensure + // contrast. + uchar4 bg_color [[attribute(7)]]; + + // The position of the glyph in the texture (x,y) + uint2 glyph_pos [[attribute(2)]]; + + // The size of the glyph in the texture (w,h) + uint2 glyph_size [[attribute(3)]]; + + // The left and top bearings for the glyph (x,y) + int2 glyph_offset [[attribute(4)]]; +}; + +struct CellTextVertexOut { + float4 position [[position]]; + float2 cell_size; + uint8_t mode; + float4 color; + float2 tex_coord; +}; + +vertex CellTextVertexOut cell_text_vertex(unsigned int vid [[vertex_id]], + CellTextVertexIn input [[stage_in]], + constant Uniforms& uniforms + [[buffer(1)]]) { + // Convert the grid x,y into world space x, y by accounting for cell size + float2 cell_pos = uniforms.cell_size * input.grid_pos; + + // Scaled cell size for the cell width + float2 cell_size_scaled = uniforms.cell_size; + cell_size_scaled.x = cell_size_scaled.x * input.cell_width; + + // Turn the cell position into a vertex point depending on the + // vertex ID. Since we use instanced drawing, we have 4 vertices + // for each corner of the cell. We can use vertex ID to determine + // which one we're looking at. Using this, we can use 1 or 0 to keep + // or discard the value for the vertex. + // + // 0 = top-right + // 1 = bot-right + // 2 = bot-left + // 3 = top-left + float2 position; + position.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; + position.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; + + CellTextVertexOut out; + out.mode = input.mode; + out.cell_size = uniforms.cell_size; + out.color = float4(input.color) / 255.0f; + + float2 glyph_size = float2(input.glyph_size); + float2 glyph_offset = float2(input.glyph_offset); + + // The glyph_offset.y is the y bearing, a y value that when added + // to the baseline is the offset (+y is up). Our grid goes down. + // So we flip it with `cell_size.y - glyph_offset.y`. + glyph_offset.y = cell_size_scaled.y - glyph_offset.y; + + // If we're constrained then we need to scale the glyph. + // We also always constrain colored glyphs since we should have + // their scaled cell size exactly correct. + if (input.mode == MODE_TEXT_CONSTRAINED || input.mode == MODE_TEXT_COLOR) { + if (glyph_size.x > cell_size_scaled.x) { + float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x); + glyph_offset.y += (glyph_size.y - new_y) / 2; + glyph_size.y = new_y; + glyph_size.x = cell_size_scaled.x; + } + } + + // 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. + out.tex_coord = float2(input.glyph_pos) + float2(input.glyph_size) * position; + + // If we have a minimum contrast, we need to check if we need to + // change the color of the text to ensure it has enough contrast + // with the background. + if (uniforms.min_contrast > 1.0f && input.mode == MODE_TEXT) { + float4 bg_color = float4(input.bg_color) / 255.0f; + out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color); + } + + return out; +} + +fragment float4 cell_text_fragment(CellTextVertexOut in [[stage_in]], + texture2d textureGreyscale + [[texture(0)]], + texture2d textureColor + [[texture(1)]]) { + constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); + + switch (in.mode) { + case MODE_TEXT_CONSTRAINED: + case MODE_TEXT: { + // Normalize the texture coordinates to [0,1] + float2 size = + float2(textureGreyscale.get_width(), textureGreyscale.get_height()); + float2 coord = in.tex_coord / size; + + // We premult the alpha to our whole color since our blend function + // uses One/OneMinusSourceAlpha to avoid blurry edges. + // We first premult our given color. + float4 premult = float4(in.color.rgb * in.color.a, in.color.a); + // Then premult the texture color + float a = textureGreyscale.sample(textureSampler, coord).r; + premult = premult * a; + return premult; + } + + case MODE_TEXT_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); + } + } +} //------------------------------------------------------------------- // Image Shader //------------------------------------------------------------------- From e07ae90d4918c8e3efce7c486fee5f53f484aa75 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Apr 2024 10:59:36 -0700 Subject: [PATCH 08/26] renderer/metal: rename drawcells --- src/renderer/Metal.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index f6b0d1ee1..7c54ab4af 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -927,7 +927,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells - try self.drawCells(encoder, frame, frame.cells, self.cells.items.len); + try self.drawCellFgs(encoder, frame, self.cells.items.len); // Then draw remaining images try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); @@ -1265,11 +1265,10 @@ fn drawCellBgs( /// /// Future: when we move to multiple shaders, this will go away and /// we'll have a draw call per-shader. -fn drawCells( +fn drawCellFgs( self: *Metal, encoder: objc.Object, frame: *const FrameState, - buf: FrameState.CellTextBuffer, len: usize, ) !void { // This triggers an assertion in the Metal API if we try to draw @@ -1287,7 +1286,7 @@ fn drawCells( encoder.msgSend( void, objc.sel("setVertexBuffer:offset:atIndex:"), - .{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, + .{ frame.cells.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, ); encoder.msgSend( void, From 556f52015b26fca68217a9ea913050f6d7d012f5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 22 Apr 2024 11:06:07 -0700 Subject: [PATCH 09/26] renderer/metal: update comment --- src/renderer/Metal.zig | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 7c54ab4af..7c3aa75b6 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1260,11 +1260,7 @@ fn drawCellBgs( ); } -/// Loads some set of cell data into our buffer and issues a draw call. -/// This expects all the Metal command encoder state to be setup. -/// -/// Future: when we move to multiple shaders, this will go away and -/// we'll have a draw call per-shader. +/// Draw the cell foregrounds using the text shader. fn drawCellFgs( self: *Metal, encoder: objc.Object, From da55da2c9633951d9f18476c10ce616674c82023 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 23 Apr 2024 21:51:44 -0700 Subject: [PATCH 10/26] renderer/metal: prepare cell contents mapping data (not implemented yet) --- src/renderer/Metal.zig | 106 +++++++++++++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 15 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 7c3aa75b6..3636d0eb3 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -90,7 +90,8 @@ current_background_color: terminal.color.RGB, /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. cells_bg: std.ArrayListUnmanaged(mtl_shaders.CellBg), -cells: std.ArrayListUnmanaged(mtl_shaders.CellText), +cells_text: std.ArrayListUnmanaged(mtl_shaders.CellText), +cells: CellContents, /// The current GPU uniform values. uniforms: mtl_shaders.Uniforms, @@ -121,6 +122,76 @@ health: std.atomic.Value(Health) = .{ .raw = .healthy }, /// Our GPU state gpu_state: GPUState, +/// The contents of all the cells in the terminal. +const CellContents = struct { + /// The possible cell content keys that exist. + const Key = enum { bg, text, underline, strikethrough }; + + /// The map contains the mapping of cell content for every cell in the + /// terminal to the index in the cells array that the content is at. + /// This is ALWAYS sized to exactly (rows * cols) so we want to keep + /// this as small as possible. + map: []const Map = &.{}, + + /// The actual GPU data (on the CPU) for all the cells in the terminal. + /// This only contains the cells that have content set. To determine + /// if a cell has content set, we check the map. + /// + /// This data is synced to a buffer on every frame. + bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg) = .{}, + text: std.ArrayListUnmanaged(mtl_shaders.CellText) = .{}, + + pub fn deinit(self: *CellContents, alloc: Allocator) void { + alloc.free(self.map); + self.bgs.deinit(alloc); + self.text.deinit(alloc); + } + + /// Resize the cell contents for the given grid size. This will + /// always invalidate the entire cell contents. + pub fn resize( + self: *CellContents, + alloc: Allocator, + size: renderer.GridSize, + ) !void { + const map = try alloc.alloc(Map, size.rows * size.columns); + errdefer alloc.free(map); + @memset(map, .{}); + + alloc.free(self.map); + self.map = map; + self.bgs.clearAndFree(alloc); + self.text.clearAndFree(alloc); + } + + /// Structures related to the contents of the cell. + const Map = struct { + /// The set of cell content mappings for a given cell for every + /// possible key. This is used to determine if a cell has a given + /// type of content (i.e. an underlyine styling) and if so what index + /// in the cells array that content is at. + const Array = std.EnumArray(Key, Mapping); + + /// The mapping for a given key consists of a bit indicating if the + /// content is set and the index in the cells array that the content + /// is at. We pack this into a 32-bit integer so we only use 4 bytes + /// per possible cell content type. + const Mapping = packed struct(u32) { + set: bool = false, + index: u31 = 0, + }; + + /// The backing array of mappings. + array: Array = Array.initFill(.{}), + }; +}; + +test "CellContents.Map size" { + // We want to be mindful of when this increases because it affects + // renderer memory significantly. + try std.testing.expectEqual(@as(usize, 16), @sizeOf(CellContents.Map)); +} + /// State we need for the GPU that is shared between all frames. pub const GPUState = struct { // The count of buffers we use for double/triple buffering. If @@ -556,6 +627,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // Render state .cells_bg = .{}, + .cells_text = .{}, .cells = .{}, .uniforms = .{ .projection_matrix = undefined, @@ -582,6 +654,7 @@ pub fn deinit(self: *Metal) void { self.cells.deinit(self.alloc); self.cells_bg.deinit(self.alloc); + self.cells_text.deinit(self.alloc); self.font_shaper.deinit(); @@ -822,7 +895,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // Setup our frame data try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); try frame.cells_bg.sync(self.gpu_state.device, self.cells_bg.items); - try frame.cells.sync(self.gpu_state.device, self.cells.items); + try frame.cells.sync(self.gpu_state.device, self.cells_text.items); // If we have custom shaders, update the animation time. if (self.custom_shader_state) |*state| { @@ -927,7 +1000,7 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells - try self.drawCellFgs(encoder, frame, self.cells.items.len); + try self.drawCellFgs(encoder, frame, self.cells_text.items.len); // Then draw remaining images try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); @@ -1577,9 +1650,12 @@ pub fn setScreenSize( // Reset our buffer sizes so that we free memory when the screen shrinks. // This could be made more clever by only doing this when the screen // shrinks but the performance cost really isn't that much. - self.cells.clearAndFree(self.alloc); + self.cells_text.clearAndFree(self.alloc); self.cells_bg.clearAndFree(self.alloc); + // Reset our cell contents. + try self.cells.resize(self.alloc, grid_size); + // If we have custom shaders then we update the state if (self.custom_shader_state) |*state| { // Only free our previous texture if this isn't our first @@ -1650,8 +1726,8 @@ fn rebuildCells( ); // Over-allocate just to ensure we don't allocate again during loops. - self.cells.clearRetainingCapacity(); - try self.cells.ensureTotalCapacity( + self.cells_text.clearRetainingCapacity(); + try self.cells_text.ensureTotalCapacity( self.alloc, // * 3 for glyph + underline + strikethrough for each cell @@ -1726,13 +1802,13 @@ fn rebuildCells( // 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; + const start_i: usize = self.cells_text.items.len; defer if (cursor_row) { // If we're on a wide spacer tail, then we want to look for // the previous cell. const screen_cell = row.cells(.all)[screen.cursor.x]; const x = screen.cursor.x - @intFromBool(screen_cell.wide == .spacer_tail); - for (self.cells.items[start_i..]) |cell| { + for (self.cells_text.items[start_i..]) |cell| { if (cell.grid_pos[0] == @as(f32, @floatFromInt(x)) and (cell.mode == .fg or cell.mode == .fg_color)) { @@ -1840,7 +1916,7 @@ fn rebuildCells( .{ self.background_color.r, self.background_color.g, self.background_color.b, 255 }; } - self.cells.appendAssumeCapacity(cell.*); + self.cells_text.appendAssumeCapacity(cell.*); } } } @@ -1990,7 +2066,7 @@ fn updateCell( .constrained => .fg_constrained, }; - self.cells.appendAssumeCapacity(.{ + self.cells_text.appendAssumeCapacity(.{ .mode = mode, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, .cell_width = cell.gridWidth(), @@ -2027,7 +2103,7 @@ fn updateCell( const color = style.underlineColor(palette) orelse colors.fg; - self.cells.appendAssumeCapacity(.{ + self.cells_text.appendAssumeCapacity(.{ .mode = .fg, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, .cell_width = cell.gridWidth(), @@ -2050,7 +2126,7 @@ fn updateCell( }, ); - self.cells.appendAssumeCapacity(.{ + self.cells_text.appendAssumeCapacity(.{ .mode = .fg, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, .cell_width = cell.gridWidth(), @@ -2110,7 +2186,7 @@ fn addCursor( return null; }; - self.cells.appendAssumeCapacity(.{ + self.cells_text.appendAssumeCapacity(.{ .mode = .fg, .grid_pos = .{ @as(f32, @floatFromInt(x)), @@ -2124,7 +2200,7 @@ fn addCursor( .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, }); - return &self.cells.items[self.cells.items.len - 1]; + return &self.cells_text.items[self.cells_text.items.len - 1]; } fn addPreeditCell( @@ -2162,7 +2238,7 @@ fn addPreeditCell( }); // Add our text - self.cells.appendAssumeCapacity(.{ + self.cells_text.appendAssumeCapacity(.{ .mode = .fg, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, .cell_width = if (cp.wide) 2 else 1, From 15b7a37cf9861cb0da48e6f0705292b44a8c9bad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Apr 2024 19:07:10 -0700 Subject: [PATCH 11/26] terminal: export Coordinate --- src/terminal/main.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/terminal/main.zig b/src/terminal/main.zig index be60aa477..857dd79f3 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -25,6 +25,7 @@ pub const Charset = charsets.Charset; pub const CharsetSlot = charsets.Slots; pub const CharsetActiveSlot = charsets.ActiveSlot; pub const Cell = page.Cell; +pub const Coordinate = point.Coordinate; pub const CSI = Parser.Action.CSI; pub const DCS = Parser.Action.DCS; pub const MouseShape = @import("mouse_shape.zig").MouseShape; From ced8776120d2dbd7ebeab6d538a9958e44fb44ff Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 25 Apr 2024 19:16:40 -0700 Subject: [PATCH 12/26] renderer/metal: grid pos for bg/text should be ushort2 This saves 50% memory per vertex. --- src/renderer/Metal.zig | 33 +++++++++++++++------------------ src/renderer/State.zig | 12 ++++++++---- src/renderer/metal/api.zig | 1 + src/renderer/metal/shaders.zig | 8 ++++---- src/renderer/shaders/cell.metal | 8 ++++---- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 3636d0eb3..19d73f3d4 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1751,8 +1751,8 @@ fn rebuildCells( // Determine our x/y range for preedit. We don't want to render anything // here because we will render the preedit separately. const preedit_range: ?struct { - y: usize, - x: [2]usize, + y: terminal.size.CellCountInt, + x: [2]terminal.size.CellCountInt, cp_offset: usize, } = if (preedit) |preedit_v| preedit: { const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); @@ -1770,7 +1770,7 @@ fn rebuildCells( // Build each cell var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); - var y: usize = 0; + var y: terminal.size.CellCountInt = 0; while (row_it.next()) |row| { defer y += 1; @@ -1809,7 +1809,7 @@ fn rebuildCells( const screen_cell = row.cells(.all)[screen.cursor.x]; const x = screen.cursor.x - @intFromBool(screen_cell.wide == .spacer_tail); for (self.cells_text.items[start_i..]) |cell| { - if (cell.grid_pos[0] == @as(f32, @floatFromInt(x)) and + if (cell.grid_pos[0] == x and (cell.mode == .fg or cell.mode == .fg_color)) { cursor_cell = cell; @@ -1929,8 +1929,8 @@ fn updateCell( palette: *const terminal.color.Palette, shaper_cell: font.shape.Cell, shaper_run: font.shape.TextRun, - x: usize, - y: usize, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, ) !bool { const BgFg = struct { /// Background is optional because in un-inverted mode @@ -2031,7 +2031,7 @@ fn updateCell( self.cells_bg.appendAssumeCapacity(.{ .mode = .rgb, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + .grid_pos = .{ x, y }, .cell_width = cell.gridWidth(), .color = .{ rgb.r, rgb.g, rgb.b, bg_alpha }, }); @@ -2068,7 +2068,7 @@ fn updateCell( self.cells_text.appendAssumeCapacity(.{ .mode = mode, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + .grid_pos = .{ x, y }, .cell_width = cell.gridWidth(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, .bg_color = bg, @@ -2105,7 +2105,7 @@ fn updateCell( self.cells_text.appendAssumeCapacity(.{ .mode = .fg, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + .grid_pos = .{ x, y }, .cell_width = cell.gridWidth(), .color = .{ color.r, color.g, color.b, alpha }, .bg_color = bg, @@ -2128,7 +2128,7 @@ fn updateCell( self.cells_text.appendAssumeCapacity(.{ .mode = .fg, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + .grid_pos = .{ x, y }, .cell_width = cell.gridWidth(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, .bg_color = bg, @@ -2188,10 +2188,7 @@ fn addCursor( self.cells_text.appendAssumeCapacity(.{ .mode = .fg, - .grid_pos = .{ - @as(f32, @floatFromInt(x)), - @as(f32, @floatFromInt(screen.cursor.y)), - }, + .grid_pos = .{ x, screen.cursor.y }, .cell_width = if (wide) 2 else 1, .color = .{ color.r, color.g, color.b, alpha }, .bg_color = .{ 0, 0, 0, 0 }, @@ -2206,8 +2203,8 @@ fn addCursor( fn addPreeditCell( self: *Metal, cp: renderer.State.Preedit.Codepoint, - x: usize, - y: usize, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, ) !void { // Preedit is rendered inverted const bg = self.foreground_color; @@ -2232,7 +2229,7 @@ fn addPreeditCell( // Add our opaque background cell self.cells_bg.appendAssumeCapacity(.{ .mode = .rgb, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + .grid_pos = .{ x, y }, .cell_width = if (cp.wide) 2 else 1, .color = .{ bg.r, bg.g, bg.b, 255 }, }); @@ -2240,7 +2237,7 @@ fn addPreeditCell( // Add our text self.cells_text.appendAssumeCapacity(.{ .mode = .fg, - .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, + .grid_pos = .{ x, y }, .cell_width = if (cp.wide) 2 else 1, .color = .{ fg.r, fg.g, fg.b, 255 }, .bg_color = .{ bg.r, bg.g, bg.b, 255 }, diff --git a/src/renderer/State.zig b/src/renderer/State.zig index 6bff5b219..8a11a7403 100644 --- a/src/renderer/State.zig +++ b/src/renderer/State.zig @@ -78,9 +78,13 @@ pub const Preedit = struct { /// Range returns the start and end x position of the preedit text /// along with any codepoint offset necessary to fit the preedit /// into the available space. - pub fn range(self: *const Preedit, start: usize, max: usize) struct { - start: usize, - end: usize, + pub fn range( + self: *const Preedit, + start: terminal.size.CellCountInt, + max: terminal.size.CellCountInt, + ) struct { + start: terminal.size.CellCountInt, + end: terminal.size.CellCountInt, cp_offset: usize, } { // If our width is greater than the number of cells we have @@ -92,7 +96,7 @@ pub const Preedit = struct { // Rebuild our width in reverse order. This is because we want // to offset by the end cells, not the start cells (if we have to). - var w: usize = 0; + var w: terminal.size.CellCountInt = 0; for (0..self.codepoints.len) |i| { const reverse_i = self.codepoints.len - i - 1; const cp = self.codepoints[reverse_i]; diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 0421a34a2..3ec04e367 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -50,6 +50,7 @@ pub const MTLIndexType = enum(c_ulong) { /// https://developer.apple.com/documentation/metal/mtlvertexformat?language=objc pub const MTLVertexFormat = enum(c_ulong) { uchar4 = 3, + ushort2 = 13, float2 = 29, float4 = 31, int2 = 33, diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 40e048984..f764e4909 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -287,7 +287,7 @@ fn initPostPipeline( /// This is a single parameter for the terminal cell shader. pub const CellText = extern struct { mode: Mode, - grid_pos: [2]f32, + grid_pos: [2]u16, glyph_pos: [2]u32 = .{ 0, 0 }, glyph_size: [2]u32 = .{ 0, 0 }, glyph_offset: [2]i32 = .{ 0, 0 }, @@ -363,7 +363,7 @@ fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object .{@as(c_ulong, 1)}, ); - attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2)); + attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.ushort2)); attr.setProperty("offset", @as(c_ulong, @offsetOf(CellText, "grid_pos"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } @@ -505,7 +505,7 @@ fn initCellTextPipeline(device: objc.Object, library: objc.Object) !objc.Object /// This is a single parameter for the cell bg shader. pub const CellBg = extern struct { mode: Mode, - grid_pos: [2]f32, + grid_pos: [2]u16, color: [4]u8, cell_width: u8, @@ -574,7 +574,7 @@ fn initCellBgPipeline(device: objc.Object, library: objc.Object) !objc.Object { .{@as(c_ulong, 1)}, ); - attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.float2)); + attr.setProperty("format", @intFromEnum(mtl.MTLVertexFormat.ushort2)); attr.setProperty("offset", @as(c_ulong, @offsetOf(CellBg, "grid_pos"))); attr.setProperty("bufferIndex", @as(c_ulong, 0)); } diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index dd9ace86a..3aae45ef5 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -70,7 +70,7 @@ struct CellBgVertexIn { uint8_t mode [[attribute(0)]]; // The grid coordinates (x, y) where x < columns and y < rows - float2 grid_pos [[attribute(1)]]; + ushort2 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. @@ -90,7 +90,7 @@ vertex CellBgVertexOut cell_bg_vertex(unsigned int vid [[vertex_id]], constant Uniforms& uniforms [[buffer(1)]]) { // Convert the grid x,y into world space x, y by accounting for cell size - float2 cell_pos = uniforms.cell_size * input.grid_pos; + float2 cell_pos = uniforms.cell_size * float2(input.grid_pos); // Scaled cell size for the cell width float2 cell_size_scaled = uniforms.cell_size; @@ -144,7 +144,7 @@ struct CellTextVertexIn { uint8_t mode [[attribute(0)]]; // The grid coordinates (x, y) where x < columns and y < rows - float2 grid_pos [[attribute(1)]]; + ushort2 grid_pos [[attribute(1)]]; // The width of the cell in cells (i.e. 2 for double-wide). uint8_t cell_width [[attribute(6)]]; @@ -180,7 +180,7 @@ vertex CellTextVertexOut cell_text_vertex(unsigned int vid [[vertex_id]], constant Uniforms& uniforms [[buffer(1)]]) { // Convert the grid x,y into world space x, y by accounting for cell size - float2 cell_pos = uniforms.cell_size * input.grid_pos; + float2 cell_pos = uniforms.cell_size * float2(input.grid_pos); // Scaled cell size for the cell width float2 cell_size_scaled = uniforms.cell_size; From 2b67eaa18d3d267deeab5f601fdb89241fecd28a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Apr 2024 20:35:25 -0700 Subject: [PATCH 13/26] renderer/metal: working on cell contents map --- src/renderer/Metal.zig | 470 ++++++++++++++++++++++++++++++------ src/renderer/metal/cell.zig | 294 ++++++++++++++++++++++ 2 files changed, 692 insertions(+), 72 deletions(-) create mode 100644 src/renderer/metal/cell.zig diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 19d73f3d4..56071ce22 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -29,6 +29,7 @@ const Health = renderer.Health; const mtl = @import("metal/api.zig"); const mtl_buffer = @import("metal/buffer.zig"); +const mtl_cell = @import("metal/cell.zig"); const mtl_image = @import("metal/image.zig"); const mtl_sampler = @import("metal/sampler.zig"); const mtl_shaders = @import("metal/shaders.zig"); @@ -91,7 +92,7 @@ current_background_color: terminal.color.RGB, /// cells goes into a separate shader. cells_bg: std.ArrayListUnmanaged(mtl_shaders.CellBg), cells_text: std.ArrayListUnmanaged(mtl_shaders.CellText), -cells: CellContents, +cells: mtl_cell.Contents, /// The current GPU uniform values. uniforms: mtl_shaders.Uniforms, @@ -122,76 +123,6 @@ health: std.atomic.Value(Health) = .{ .raw = .healthy }, /// Our GPU state gpu_state: GPUState, -/// The contents of all the cells in the terminal. -const CellContents = struct { - /// The possible cell content keys that exist. - const Key = enum { bg, text, underline, strikethrough }; - - /// The map contains the mapping of cell content for every cell in the - /// terminal to the index in the cells array that the content is at. - /// This is ALWAYS sized to exactly (rows * cols) so we want to keep - /// this as small as possible. - map: []const Map = &.{}, - - /// The actual GPU data (on the CPU) for all the cells in the terminal. - /// This only contains the cells that have content set. To determine - /// if a cell has content set, we check the map. - /// - /// This data is synced to a buffer on every frame. - bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg) = .{}, - text: std.ArrayListUnmanaged(mtl_shaders.CellText) = .{}, - - pub fn deinit(self: *CellContents, alloc: Allocator) void { - alloc.free(self.map); - self.bgs.deinit(alloc); - self.text.deinit(alloc); - } - - /// Resize the cell contents for the given grid size. This will - /// always invalidate the entire cell contents. - pub fn resize( - self: *CellContents, - alloc: Allocator, - size: renderer.GridSize, - ) !void { - const map = try alloc.alloc(Map, size.rows * size.columns); - errdefer alloc.free(map); - @memset(map, .{}); - - alloc.free(self.map); - self.map = map; - self.bgs.clearAndFree(alloc); - self.text.clearAndFree(alloc); - } - - /// Structures related to the contents of the cell. - const Map = struct { - /// The set of cell content mappings for a given cell for every - /// possible key. This is used to determine if a cell has a given - /// type of content (i.e. an underlyine styling) and if so what index - /// in the cells array that content is at. - const Array = std.EnumArray(Key, Mapping); - - /// The mapping for a given key consists of a bit indicating if the - /// content is set and the index in the cells array that the content - /// is at. We pack this into a 32-bit integer so we only use 4 bytes - /// per possible cell content type. - const Mapping = packed struct(u32) { - set: bool = false, - index: u31 = 0, - }; - - /// The backing array of mappings. - array: Array = Array.initFill(.{}), - }; -}; - -test "CellContents.Map size" { - // We want to be mindful of when this increases because it affects - // renderer memory significantly. - try std.testing.expectEqual(@as(usize, 16), @sizeOf(CellContents.Map)); -} - /// State we need for the GPU that is shared between all frames. pub const GPUState = struct { // The count of buffers we use for double/triple buffering. If @@ -844,7 +775,7 @@ pub fn updateFrame( if (critical.preedit) |p| p.deinit(self.alloc); } - // Build our GPU cells + // Build our GPU cells (OLD) try self.rebuildCells( &critical.screen, critical.mouse, @@ -853,6 +784,15 @@ pub fn updateFrame( &critical.color_palette, ); + // Build our GPU cells + try self.rebuildCells2( + &critical.screen, + critical.mouse, + critical.preedit, + critical.cursor_style, + &critical.color_palette, + ); + // Update our background color self.current_background_color = critical.bg; @@ -1921,6 +1861,388 @@ fn rebuildCells( } } +/// Convert the terminal state to GPU cells stored in CPU memory. These +/// are then synced to the GPU in the next frame. This only updates CPU +/// memory and doesn't touch the GPU. +fn rebuildCells2( + self: *Metal, + screen: *terminal.Screen, + mouse: renderer.State.Mouse, + preedit: ?renderer.State.Preedit, + cursor_style_: ?renderer.CursorStyle, + color_palette: *const terminal.color.Palette, +) !void { + // TODO: cursor_cell + // TODO: cursor_Row + _ = cursor_style_; + + // Create an arena for all our temporary allocations while rebuilding + var arena = ArenaAllocator.init(self.alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + // Create our match set for the links. + var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet( + arena_alloc, + screen, + mouse_pt, + mouse.mods, + ) else .{}; + + // Determine our x/y range for preedit. We don't want to render anything + // here because we will render the preedit separately. + const preedit_range: ?struct { + y: usize, + x: [2]usize, + cp_offset: usize, + } = if (preedit) |preedit_v| preedit: { + const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); + break :preedit .{ + .y = screen.cursor.y, + .x = .{ range.start, range.end }, + .cp_offset = range.cp_offset, + }; + } else null; + + // Go row-by-row to build the cells. We go row by row because we do + // font shaping by row. In the future, we will also do dirty tracking + // by row. + var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); + var y: usize = 0; + while (row_it.next()) |row| { + defer y += 1; + + // True if we want to do font shaping around the cursor. We want to + // do font shaping as long as the cursor is enabled. + const shape_cursor = screen.viewportIsBottom() and + y == screen.cursor.y; + + // We need to get this row's selection if there is one for proper + // run splitting. + const row_selection = sel: { + const sel = screen.selection orelse break :sel null; + const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse + break :sel null; + break :sel sel.containedRow(screen, pin) orelse null; + }; + + // Split our row into runs and shape each one. + var iter = self.font_shaper.runIterator( + self.font_grid, + screen, + row, + row_selection, + if (shape_cursor) screen.cursor.x else null, + ); + while (try iter.next(self.alloc)) |run| { + for (try self.font_shaper.shape(run)) |shaper_cell| { + const coord: terminal.Coordinate = .{ + .x = shaper_cell.x, + .y = y, + }; + + // If this cell falls within our preedit range then we skip it. + // We do this so we don't have conflicting data on the same + // cell. + if (preedit_range) |range| { + if (range.y == coord.y and + coord.x >= range.x[0] and + coord.x <= range.x[1]) + { + continue; + } + } + + // It this cell is within our hint range then we need to + // underline it. + const cell: terminal.Pin = cell: { + var copy = row; + copy.x = coord.x; + break :cell copy; + }; + + if (self.updateCell2( + screen, + cell, + if (link_match_set.orderedContains(screen, cell)) + .single + else + null, + color_palette, + shaper_cell, + run, + coord, + )) |update| { + assert(update); + } else |err| { + log.warn("error building cell, will be invalid x={} y={}, err={}", .{ + coord.x, + coord.y, + err, + }); + } + } + } + } + + // 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. + // if (cursor_style_) |cursor_style| cursor_style: { + // // If we have a preedit, we try to render the preedit text on top + // // of the cursor. + // if (preedit) |preedit_v| { + // const range = preedit_range.?; + // var x = range.x[0]; + // for (preedit_v.codepoints[range.cp_offset..]) |cp| { + // self.addPreeditCell(cp, x, range.y) catch |err| { + // log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ + // x, + // range.y, + // err, + // }); + // }; + // + // x += if (cp.wide) 2 else 1; + // } + // + // // Preedit hides the cursor + // break :cursor_style; + // } + // + // _ = self.addCursor(screen, cursor_style); + // // if (cursor_cell) |*cell| { + // // if (cell.mode == .fg) { + // // cell.color = if (self.config.cursor_text) |txt| + // // .{ txt.r, txt.g, txt.b, 255 } + // // else + // // .{ self.background_color.r, self.background_color.g, self.background_color.b, 255 }; + // // } + // // + // // self.cells_text.appendAssumeCapacity(cell.*); + // // } + // } +} + +fn updateCell2( + self: *Metal, + screen: *const terminal.Screen, + cell_pin: terminal.Pin, + cell_underline: ?terminal.Attribute.Underline, + palette: *const terminal.color.Palette, + shaper_cell: font.shape.Cell, + shaper_run: font.shape.TextRun, + coord: terminal.Coordinate, +) !bool { + const BgFg = struct { + /// Background is optional because in un-inverted mode + /// it may just be equivalent to the default background in + /// which case we do nothing to save on GPU render time. + bg: ?terminal.color.RGB, + + /// Fg is always set to some color, though we may not render + /// any fg if the cell is empty or has no attributes like + /// underline. + fg: terminal.color.RGB, + }; + + // True if this cell is selected + const selected: bool = if (screen.selection) |sel| + sel.contains(screen, cell_pin) + else + false; + + const rac = cell_pin.rowAndCell(); + const cell = rac.cell; + const style = cell_pin.style(cell); + const underline = cell_underline orelse style.flags.underline; + + // The colors for the cell. + const colors: BgFg = colors: { + // The normal cell result + const cell_res: BgFg = if (!style.flags.inverse) .{ + // In normal mode, background and fg match the cell. We + // un-optionalize the fg by defaulting to our fg color. + .bg = style.bg(cell, palette), + .fg = style.fg(palette) orelse self.foreground_color, + } 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 = style.fg(palette) orelse self.foreground_color, + .fg = style.bg(cell, palette) orelse self.background_color, + }; + + // If we are selected, we our colors are just inverted fg/bg + const selection_res: ?BgFg = if (selected) .{ + .bg = if (self.config.invert_selection_fg_bg) + cell_res.fg + else + self.config.selection_background orelse self.foreground_color, + .fg = if (self.config.invert_selection_fg_bg) + cell_res.bg orelse self.background_color + else + self.config.selection_foreground orelse self.background_color, + } else null; + + // If the cell is "invisible" then we just make fg = bg so that + // the cell is transparent but still copy-able. + const res: BgFg = selection_res orelse cell_res; + if (style.flags.invisible) { + break :colors BgFg{ + .bg = res.bg, + .fg = res.bg orelse self.background_color, + }; + } + + break :colors res; + }; + + // Alpha multiplier + const alpha: u8 = if (style.flags.faint) 175 else 255; + + // If the cell has a background, we always draw it. + const bg: [4]u8 = if (colors.bg) |rgb| bg: { + // Determine our background alpha. If we have transparency configured + // then this is dynamic depending on some situations. This is all + // in an attempt to make transparency look the best for various + // situations. See inline comments. + const bg_alpha: u8 = bg_alpha: { + const default: u8 = 255; + + if (self.config.background_opacity >= 1) break :bg_alpha default; + + // If we're selected, we do not apply background opacity + if (selected) break :bg_alpha default; + + // If we're reversed, do not apply background opacity + if (style.flags.inverse) break :bg_alpha default; + + // If we have a background and its not the default background + // then we apply background opacity + if (style.bg(cell, palette) != null and !rgb.eql(self.background_color)) { + break :bg_alpha default; + } + + // We apply background opacity. + var bg_alpha: f64 = @floatFromInt(default); + bg_alpha *= self.config.background_opacity; + bg_alpha = @ceil(bg_alpha); + break :bg_alpha @intFromFloat(bg_alpha); + }; + + try self.cells.set(self.alloc, .bg, .{ + .mode = .rgb, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, + .cell_width = cell.gridWidth(), + .color = .{ rgb.r, rgb.g, rgb.b, bg_alpha }, + }); + + break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha }; + } else .{ + self.current_background_color.r, + self.current_background_color.g, + self.current_background_color.b, + @intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))), + }; + + // If the cell has a character, draw it + if (cell.hasText()) fg: { + // Render + const render = try self.font_grid.renderGlyph( + self.alloc, + shaper_run.font_index, + shaper_cell.glyph_index orelse break :fg, + .{ + .grid_metrics = self.grid_metrics, + .thicken = self.config.font_thicken, + }, + ); + + const mode: mtl_shaders.CellText.Mode = switch (try fgMode( + render.presentation, + cell_pin, + )) { + .normal => .fg, + .color => .fg_color, + .constrained => .fg_constrained, + }; + + try self.cells.set(self.alloc, .text, .{ + .mode = mode, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, + .cell_width = cell.gridWidth(), + .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, + .bg_color = bg, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .glyph_offset = .{ + render.glyph.offset_x + shaper_cell.x_offset, + render.glyph.offset_y + shaper_cell.y_offset, + }, + }); + } + + if (underline != .none) { + const sprite: font.Sprite = switch (underline) { + .none => unreachable, + .single => .underline, + .double => .underline_double, + .dotted => .underline_dotted, + .dashed => .underline_dashed, + .curly => .underline_curly, + }; + + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(sprite), + .{ + .cell_width = if (cell.wide == .wide) 2 else 1, + .grid_metrics = self.grid_metrics, + }, + ); + + const color = style.underlineColor(palette) orelse colors.fg; + + try self.cells.set(self.alloc, .underline, .{ + .mode = .fg, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, + .cell_width = cell.gridWidth(), + .color = .{ color.r, color.g, color.b, alpha }, + .bg_color = bg, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, + }); + } + + if (style.flags.strikethrough) { + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(font.Sprite.strikethrough), + .{ + .cell_width = if (cell.wide == .wide) 2 else 1, + .grid_metrics = self.grid_metrics, + }, + ); + + try self.cells.set(self.alloc, .strikethrough, .{ + .mode = .fg, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, + .cell_width = cell.gridWidth(), + .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, + .bg_color = bg, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, + }); + } + + return true; +} + fn updateCell( self: *Metal, screen: *const terminal.Screen, @@ -2326,3 +2648,7 @@ fn initAtlasTexture(device: objc.Object, atlas: *const font.Atlas) !objc.Object fn deinitMTLResource(obj: objc.Object) void { obj.msgSend(void, objc.sel("release"), .{}); } + +test { + _ = mtl_cell; +} diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig new file mode 100644 index 000000000..2073be6e0 --- /dev/null +++ b/src/renderer/metal/cell.zig @@ -0,0 +1,294 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const renderer = @import("../../renderer.zig"); +const terminal = @import("../../terminal/main.zig"); +const mtl_shaders = @import("shaders.zig"); + +/// The possible cell content keys that exist. +pub const Key = enum { + bg, + text, + underline, + strikethrough, + + /// Returns the GPU vertex type for this key. + fn CellType(self: Key) type { + return switch (self) { + .bg => mtl_shaders.CellBg, + + .text, + .underline, + .strikethrough, + => mtl_shaders.CellText, + }; + } +}; + +/// The contents of all the cells in the terminal. +pub const Contents = struct { + /// The map contains the mapping of cell content for every cell in the + /// terminal to the index in the cells array that the content is at. + /// This is ALWAYS sized to exactly (rows * cols) so we want to keep + /// this as small as possible. + /// + /// Before any operation, this must be initialized by calling resize + /// on the contents. + map: []Map = undefined, + + /// The grid size of the terminal. This is used to determine the + /// map array index from a coordinate. + cols: usize = 0, + + /// The actual GPU data (on the CPU) for all the cells in the terminal. + /// This only contains the cells that have content set. To determine + /// if a cell has content set, we check the map. + /// + /// This data is synced to a buffer on every frame. + bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg) = .{}, + text: std.ArrayListUnmanaged(mtl_shaders.CellText) = .{}, + + pub fn deinit(self: *Contents, alloc: Allocator) void { + alloc.free(self.map); + self.bgs.deinit(alloc); + self.text.deinit(alloc); + } + + /// Resize the cell contents for the given grid size. This will + /// always invalidate the entire cell contents. + pub fn resize( + self: *Contents, + alloc: Allocator, + size: renderer.GridSize, + ) !void { + const map = try alloc.alloc(Map, size.rows * size.columns); + errdefer alloc.free(map); + @memset(map, .{}); + + alloc.free(self.map); + self.map = map; + self.cols = size.columns; + self.bgs.clearAndFree(alloc); + self.text.clearAndFree(alloc); + } + + /// Get the cell contents for the given type and coordinate. + pub fn get( + self: *const Contents, + comptime key: Key, + coord: terminal.Coordinate, + ) ?key.CellType() { + const idx = coord.y * self.cols + coord.x; + const mapping = self.map[idx].array.get(key); + if (!mapping.set) return null; + return switch (key) { + .bg => self.bgs.items[mapping.index], + + .text, + .underline, + .strikethrough, + => self.text.items[mapping.index], + }; + } + + /// Set the cell contents for a given type of content at a given + /// coordinate (provided by the celll contents). + pub fn set( + self: *Contents, + alloc: Allocator, + comptime key: Key, + cell: key.CellType(), + ) !void { + const mapping = self.map[ + self.index(.{ + .x = cell.grid_pos[0], + .y = cell.grid_pos[1], + }) + ].array.getPtr(key); + + // Get our list of cells based on the key (comptime). + const list = &@field(self, switch (key) { + .bg => "bgs", + .text, .underline, .strikethrough => "text", + }); + + // If this content type is already set on this cell, we can + // simply update the pre-existing index in the list to the new + // contents. + if (mapping.set) { + list.items[mapping.index] = cell; + return; + } + + // Otherwise we need to append the new cell to the list. + const idx: u31 = @intCast(list.items.len); + try list.append(alloc, cell); + mapping.* = .{ .set = true, .index = idx }; + } + + /// Clear all of the cell contents for a given row. + pub fn clear( + self: *Contents, + y: usize, + ) void { + const start_idx = y * self.cols; + const end_idx = start_idx + self.cols; + const maps = self.map[start_idx..end_idx]; + for (maps) |*map| { + var it = map.array.iterator(); + while (it.next()) |entry| { + if (!entry.value.set) continue; + + // This value is no longer set + entry.value.set = false; + + // Remove the value at index. This does a "swap remove" + // which swaps the last element in to this place. This is + // important because after this we need to update the mapping + // for the swapped element. + const original_index = entry.value.index; + const coord_: ?terminal.Coordinate = switch (entry.key) { + .bg => bg: { + _ = self.bgs.swapRemove(original_index); + if (self.bgs.items.len == 0) break :bg null; + const new = self.bgs.items[original_index]; + break :bg .{ .x = new.grid_pos[0], .y = new.grid_pos[1] }; + }, + + .text, + .underline, + .strikethrough, + => text: { + _ = self.text.swapRemove(original_index); + if (self.text.items.len == 0) break :text null; + const new = self.text.items[original_index]; + break :text .{ .x = new.grid_pos[0], .y = new.grid_pos[1] }; + }, + }; + + // If we have the coordinate of the swapped element, then + // we need to update it to point at its new index, which is + // the index of the element we just removed. + // + // The reason we wouldn't have a coordinate is if we are + // removing the last element in the array, then nothing + // is swapped in and nothing needs to be updated. + if (coord_) |coord| { + const mapping = self.map[self.index(coord)].array.getPtr(entry.key); + assert(mapping.set); + mapping.index = original_index; + } + } + } + } + + fn index(self: *const Contents, coord: terminal.Coordinate) usize { + return coord.y * self.cols + coord.x; + } + + /// Structures related to the contents of the cell. + const Map = struct { + /// The set of cell content mappings for a given cell for every + /// possible key. This is used to determine if a cell has a given + /// type of content (i.e. an underlyine styling) and if so what index + /// in the cells array that content is at. + const Array = std.EnumArray(Key, Mapping); + + /// The mapping for a given key consists of a bit indicating if the + /// content is set and the index in the cells array that the content + /// is at. We pack this into a 32-bit integer so we only use 4 bytes + /// per possible cell content type. + const Mapping = packed struct(u32) { + set: bool = false, + index: u31 = 0, + }; + + /// The backing array of mappings. + array: Array = Array.initFill(.{}), + + pub fn empty(self: *Map) bool { + var it = self.array.iterator(); + while (it.next()) |entry| { + if (entry.value.set) return false; + } + + return true; + } + }; +}; + +test Contents { + const testing = std.testing; + const alloc = testing.allocator; + + const rows = 10; + const cols = 10; + + var c: Contents = .{}; + try c.resize(alloc, .{ .rows = rows, .columns = cols }); + defer c.deinit(alloc); + + // Assert that get returns null for everything. + for (0..rows) |y| { + for (0..cols) |x| { + try testing.expect(c.get(.bg, .{ .x = x, .y = y }) == null); + } + } + + // Set some contents + const cell: mtl_shaders.CellBg = .{ + .mode = .rgb, + .grid_pos = .{ 4, 1 }, + .cell_width = 1, + .color = .{ 0, 0, 0, 1 }, + }; + try c.set(alloc, .bg, cell); + try testing.expectEqual(cell, c.get(.bg, .{ .x = 4, .y = 1 }).?); + + // Can clear it + c.clear(1); + for (0..rows) |y| { + for (0..cols) |x| { + try testing.expect(c.get(.bg, .{ .x = x, .y = y }) == null); + } + } +} + +test "Contents clear retains other content" { + const testing = std.testing; + const alloc = testing.allocator; + + const rows = 10; + const cols = 10; + + var c: Contents = .{}; + try c.resize(alloc, .{ .rows = rows, .columns = cols }); + defer c.deinit(alloc); + + // Set some contents + const cell1: mtl_shaders.CellBg = .{ + .mode = .rgb, + .grid_pos = .{ 4, 1 }, + .cell_width = 1, + .color = .{ 0, 0, 0, 1 }, + }; + const cell2: mtl_shaders.CellBg = .{ + .mode = .rgb, + .grid_pos = .{ 4, 2 }, + .cell_width = 1, + .color = .{ 0, 0, 0, 1 }, + }; + try c.set(alloc, .bg, cell1); + try c.set(alloc, .bg, cell2); + c.clear(1); + + // Row 2 should still be valid. + try testing.expectEqual(cell2, c.get(.bg, .{ .x = 4, .y = 2 }).?); +} + +test "Contents.Map size" { + // We want to be mindful of when this increases because it affects + // renderer memory significantly. + try std.testing.expectEqual(@as(usize, 16), @sizeOf(Contents.Map)); +} From 3f16234f72df9eb9a94ec81ad033400af6688039 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Apr 2024 20:52:08 -0700 Subject: [PATCH 14/26] terminal: Coordinate uses CellCountInt --- src/renderer/Metal.zig | 2 +- src/renderer/metal/cell.zig | 10 ++++- src/renderer/metal/shaders.zig | 2 +- src/terminal/PageList.zig | 43 ++++++++++--------- src/terminal/Screen.zig | 30 ++++++++++---- src/terminal/Selection.zig | 2 +- src/terminal/Terminal.zig | 55 ++++++++++++++++++++----- src/terminal/kitty/graphics_storage.zig | 15 +++---- src/terminal/point.zig | 5 ++- 9 files changed, 111 insertions(+), 53 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 56071ce22..030ed8d64 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1908,7 +1908,7 @@ fn rebuildCells2( // font shaping by row. In the future, we will also do dirty tracking // by row. var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); - var y: usize = 0; + var y: terminal.size.CellCountInt = 0; while (row_it.next()) |row| { defer y += 1; diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig index 2073be6e0..f55e99adb 100644 --- a/src/renderer/metal/cell.zig +++ b/src/renderer/metal/cell.zig @@ -232,7 +232,10 @@ test Contents { // Assert that get returns null for everything. for (0..rows) |y| { for (0..cols) |x| { - try testing.expect(c.get(.bg, .{ .x = x, .y = y }) == null); + try testing.expect(c.get(.bg, .{ + .x = @intCast(x), + .y = @intCast(y), + }) == null); } } @@ -250,7 +253,10 @@ test Contents { c.clear(1); for (0..rows) |y| { for (0..cols) |x| { - try testing.expect(c.get(.bg, .{ .x = x, .y = y }) == null); + try testing.expect(c.get(.bg, .{ + .x = @intCast(x), + .y = @intCast(y), + }) == null); } } } diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index f764e4909..2d7fb9700 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -287,12 +287,12 @@ fn initPostPipeline( /// This is a single parameter for the terminal cell shader. pub const CellText = extern struct { mode: Mode, - grid_pos: [2]u16, glyph_pos: [2]u32 = .{ 0, 0 }, glyph_size: [2]u32 = .{ 0, 0 }, glyph_offset: [2]i32 = .{ 0, 0 }, color: [4]u8, bg_color: [4]u8, + grid_pos: [2]u16, cell_width: u8, pub const Mode = enum(u8) { diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 1946a84aa..95df3e911 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1421,7 +1421,7 @@ fn resizeWithoutReflowGrowCols( // Keeps track of all our copied rows. Assertions at the end is that // we copied exactly our page size. - var copied: usize = 0; + var copied: size.CellCountInt = 0; // This function has an unfortunate side effect in that it causes memory // fragmentation on rows if the columns are increasing in a way that @@ -2545,7 +2545,7 @@ pub fn cellIterator( pub const RowIterator = struct { page_it: PageIterator, chunk: ?PageIterator.Chunk = null, - offset: usize = 0, + offset: size.CellCountInt = 0, pub fn next(self: *RowIterator) ?Pin { const chunk = self.chunk orelse return null; @@ -2767,8 +2767,8 @@ pub const PageIterator = struct { pub const Chunk = struct { page: *List.Node, - start: usize, - end: usize, + start: size.CellCountInt, + end: size.CellCountInt, pub fn rows(self: Chunk) []Row { const rows_ptr = self.page.data.rows.ptr(self.page.data.memory); @@ -2944,8 +2944,8 @@ fn growRows(self: *PageList, n: usize) !void { /// should limit the number of active pins as much as possible. pub const Pin = struct { page: *List.Node, - y: usize = 0, - x: usize = 0, + y: size.CellCountInt = 0, + x: size.CellCountInt = 0, pub fn rowAndCell(self: Pin) struct { row: *pagepkg.Row, @@ -3104,7 +3104,7 @@ pub const Pin = struct { pub fn left(self: Pin, n: usize) Pin { assert(n <= self.x); var result = self; - result.x -= n; + result.x -= std.math.cast(size.CellCountInt, n) orelse result.x; return result; } @@ -3112,7 +3112,8 @@ pub const Pin = struct { pub fn right(self: Pin, n: usize) Pin { assert(self.x + n < self.page.data.size.cols); var result = self; - result.x += n; + result.x +|= std.math.cast(size.CellCountInt, n) orelse + std.math.maxInt(size.CellCountInt); return result; } @@ -3147,7 +3148,8 @@ pub const Pin = struct { const rows = self.page.data.size.rows - (self.y + 1); if (n <= rows) return .{ .offset = .{ .page = self.page, - .y = n + self.y, + .y = std.math.cast(size.CellCountInt, self.y + n) orelse + std.math.maxInt(size.CellCountInt), .x = self.x, } }; @@ -3165,7 +3167,8 @@ pub const Pin = struct { } }; if (n_left <= page.data.size.rows) return .{ .offset = .{ .page = page, - .y = n_left - 1, + .y = std.math.cast(size.CellCountInt, n_left - 1) orelse + std.math.maxInt(size.CellCountInt), .x = self.x, } }; n_left -= page.data.size.rows; @@ -3184,7 +3187,8 @@ pub const Pin = struct { // Index fits within this page if (n <= self.y) return .{ .offset = .{ .page = self.page, - .y = self.y - n, + .y = std.math.cast(size.CellCountInt, self.y - n) orelse + std.math.maxInt(size.CellCountInt), .x = self.x, } }; @@ -3198,7 +3202,8 @@ pub const Pin = struct { } }; if (n_left <= page.data.size.rows) return .{ .offset = .{ .page = page, - .y = page.data.size.rows - n_left, + .y = std.math.cast(size.CellCountInt, page.data.size.rows - n_left) orelse + std.math.maxInt(size.CellCountInt), .x = self.x, } }; n_left -= page.data.size.rows; @@ -3210,8 +3215,8 @@ const Cell = struct { page: *List.Node, row: *pagepkg.Row, cell: *pagepkg.Cell, - row_idx: usize, - col_idx: usize, + row_idx: size.CellCountInt, + col_idx: size.CellCountInt, /// Get the cell style. /// @@ -3231,7 +3236,7 @@ const Cell = struct { /// this file then consider a different approach and ask yourself very /// carefully if you really need this. pub fn screenPoint(self: Cell) point.Point { - var y: usize = self.row_idx; + var y: size.CellCountInt = self.row_idx; var page = self.page; while (page.prev) |prev| { y += prev.data.size.rows; @@ -3402,7 +3407,7 @@ test "PageList pointFromPin traverse pages" { try testing.expectEqual(point.Point{ .screen = .{ - .y = expected_y, + .y = @intCast(expected_y), .x = 2, }, }, s.pointFromPin(.screen, .{ @@ -5629,7 +5634,7 @@ test "PageList resize (no reflow) more rows adds blank rows if cursor at bottom" // Go through our active, we should get only 3,4,5 for (0..3) |y| { - const get = s.getCell(.{ .active = .{ .y = y } }).?; + const get = s.getCell(.{ .active = .{ .y = @intCast(y) } }).?; const expected: u21 = @intCast(y + 2); try testing.expectEqual(expected, get.cell.content.codepoint); } @@ -6557,7 +6562,7 @@ test "PageList resize reflow less cols no wrapped rows" { while (it.next()) |offset| { for (0..4) |x| { var offset_copy = offset; - offset_copy.x = x; + offset_copy.x = @intCast(x); const rac = offset_copy.rowAndCell(); const cells = offset.page.data.getCells(rac.row); try testing.expectEqual(@as(usize, 5), cells.len); @@ -7247,7 +7252,7 @@ test "PageList resize reflow less cols copy style" { while (it.next()) |offset| { for (0..s.cols - 1) |x| { var offset_copy = offset; - offset_copy.x = x; + offset_copy.x = @intCast(x); const rac = offset_copy.rowAndCell(); const style_id = rac.cell.style_id; try testing.expect(style_id != 0); diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 98c3daa21..58e4e681b 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1412,8 +1412,8 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! if (mapbuilder) |*b| { for (0..encode_len) |_| try b.append(.{ .page = chunk.page, - .y = y, - .x = x, + .y = @intCast(y), + .x = @intCast(x), }); } } @@ -1425,8 +1425,8 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! if (mapbuilder) |*b| { for (0..encode_len) |_| try b.append(.{ .page = chunk.page, - .y = y, - .x = x, + .y = @intCast(y), + .x = @intCast(x), }); } } @@ -1441,7 +1441,7 @@ pub fn selectionString(self: *Screen, alloc: Allocator, opts: SelectionString) ! try strbuilder.append('\n'); if (mapbuilder) |*b| try b.append(.{ .page = chunk.page, - .y = y, + .y = @intCast(y), .x = chunk.page.data.size.cols - 1, }); } @@ -3959,7 +3959,10 @@ test "Screen: resize (no reflow) less rows trims blank lines" { // Write only a background color into the remaining rows for (1..s.pages.rows) |y| { - const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = y } }).?; + const list_cell = s.pages.getCell(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } }).?; list_cell.cell.* = .{ .content_tag = .bg_color_rgb, .content = .{ .color_rgb = .{ .r = 0xFF, .g = 0, .b = 0 } }, @@ -3991,7 +3994,10 @@ test "Screen: resize (no reflow) more rows trims blank lines" { // Write only a background color into the remaining rows for (1..s.pages.rows) |y| { - const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = y } }).?; + const list_cell = s.pages.getCell(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } }).?; list_cell.cell.* = .{ .content_tag = .bg_color_rgb, .content = .{ .color_rgb = .{ .r = 0xFF, .g = 0, .b = 0 } }, @@ -4118,7 +4124,10 @@ test "Screen: resize (no reflow) more rows with soft wrapping" { // Every second row should be wrapped for (0..6) |y| { - const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = y } }).?; + const list_cell = s.pages.getCell(.{ .screen = .{ + .x = 0, + .y = @intCast(y), + } }).?; const row = list_cell.row; const wrapped = (y % 2 == 0); try testing.expectEqual(wrapped, row.wrap); @@ -4135,7 +4144,10 @@ test "Screen: resize (no reflow) more rows with soft wrapping" { // Every second row should be wrapped for (0..6) |y| { - const list_cell = s.pages.getCell(.{ .screen = .{ .x = 0, .y = y } }).?; + const list_cell = s.pages.getCell(.{ .screen = .{ + .x = 0, + .y = @intCast(y), + } }).?; const row = list_cell.row; const wrapped = (y % 2 == 0); try testing.expectEqual(wrapped, row.wrap); diff --git a/src/terminal/Selection.zig b/src/terminal/Selection.zig index 9da0f134e..d1bd4accb 100644 --- a/src/terminal/Selection.zig +++ b/src/terminal/Selection.zig @@ -435,7 +435,7 @@ pub fn adjust( const cells = next.page.data.getCells(rac.row); if (page.Cell.hasTextAny(cells)) { end_pin.* = next; - end_pin.x = cells.len - 1; + end_pin.x = @intCast(cells.len - 1); break; } } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 94d32e1a8..9ddec218e 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -4169,7 +4169,10 @@ test "Terminal: insertLines colors with bg color" { } for (0..t.cols) |x| { - const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 1 } }).?; + const list_cell = t.screen.pages.getCell(.{ .active = .{ + .x = @intCast(x), + .y = 1, + } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, @@ -5297,7 +5300,10 @@ test "Terminal: index bottom of primary screen background sgr" { defer testing.allocator.free(str); try testing.expectEqualStrings("\n\n\nA", str); for (0..5) |x| { - const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 4 } }).?; + const list_cell = t.screen.pages.getCell(.{ .active = .{ + .x = @intCast(x), + .y = 4, + } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, @@ -5349,7 +5355,10 @@ test "Terminal: index bottom of scroll region with background SGR" { } for (0..t.cols) |x| { - const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 2 } }).?; + const list_cell = t.screen.pages.getCell(.{ .active = .{ + .x = @intCast(x), + .y = 2, + } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, @@ -5961,7 +5970,10 @@ test "Terminal: deleteLines colors with bg color" { } for (0..t.cols) |x| { - const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 4 } }).?; + const list_cell = t.screen.pages.getCell(.{ .active = .{ + .x = @intCast(x), + .y = 4, + } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, @@ -6148,7 +6160,10 @@ test "Terminal: deleteLines resets wrap" { } for (0..t.rows) |y| { - const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = 0, .y = y } }).?; + const list_cell = t.screen.pages.getCell(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } }).?; const row = list_cell.row; try testing.expect(!row.wrap); } @@ -7183,7 +7198,10 @@ test "Terminal: deleteChars preserves background sgr" { try testing.expectEqualStrings("AB23", str); } for (t.cols - 2..t.cols) |x| { - const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?; + const list_cell = t.screen.pages.getCell(.{ .active = .{ + .x = @intCast(x), + .y = 0, + } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, @@ -7573,7 +7591,10 @@ test "Terminal: eraseLine right preserves background sgr" { defer testing.allocator.free(str); try testing.expectEqualStrings("A", str); for (1..5) |x| { - const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?; + const list_cell = t.screen.pages.getCell(.{ .active = .{ + .x = @intCast(x), + .y = 0, + } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, @@ -7727,7 +7748,10 @@ test "Terminal: eraseLine left preserves background sgr" { defer testing.allocator.free(str); try testing.expectEqualStrings(" CDE", str); for (0..2) |x| { - const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?; + const list_cell = t.screen.pages.getCell(.{ .active = .{ + .x = @intCast(x), + .y = 0, + } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, @@ -7847,7 +7871,10 @@ test "Terminal: eraseLine complete preserves background sgr" { defer testing.allocator.free(str); try testing.expectEqualStrings("", str); for (0..5) |x| { - const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 0 } }).?; + const list_cell = t.screen.pages.getCell(.{ .active = .{ + .x = @intCast(x), + .y = 0, + } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, @@ -8096,7 +8123,10 @@ test "Terminal: eraseDisplay erase below preserves SGR bg" { defer testing.allocator.free(str); try testing.expectEqualStrings("ABC\nD", str); for (1..5) |x| { - const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 1 } }).?; + const list_cell = t.screen.pages.getCell(.{ .active = .{ + .x = @intCast(x), + .y = 1, + } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, @@ -8271,7 +8301,10 @@ test "Terminal: eraseDisplay erase above preserves SGR bg" { defer testing.allocator.free(str); try testing.expectEqualStrings("\n F\nGHI", str); for (0..2) |x| { - const list_cell = t.screen.pages.getCell(.{ .active = .{ .x = x, .y = 1 } }).?; + const list_cell = t.screen.pages.getCell(.{ .active = .{ + .x = @intCast(x), + .y = 1, + } }).?; try testing.expect(list_cell.cell.content_tag == .bg_color_rgb); try testing.expectEqual(Cell.RGB{ .r = 0xFF, diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index 1071f065a..547f78b97 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -5,6 +5,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const terminal = @import("../main.zig"); const point = @import("../point.zig"); +const size = @import("../size.zig"); const command = @import("graphics_command.zig"); const PageList = @import("../PageList.zig"); const Screen = @import("../Screen.zig"); @@ -265,13 +266,13 @@ pub const ImageStorage = struct { ); }, - .intersect_cell => |v| { + .intersect_cell => |v| intersect_cell: { self.deleteIntersecting( alloc, t, .{ .active = .{ - .x = v.x, - .y = v.y, + .x = std.math.cast(size.CellCountInt, v.x) orelse break :intersect_cell, + .y = std.math.cast(size.CellCountInt, v.y) orelse break :intersect_cell, } }, v.delete, {}, @@ -279,13 +280,13 @@ pub const ImageStorage = struct { ); }, - .intersect_cell_z => |v| { + .intersect_cell_z => |v| intersect_cell_z: { self.deleteIntersecting( alloc, t, .{ .active = .{ - .x = v.x, - .y = v.y, + .x = std.math.cast(size.CellCountInt, v.x) orelse break :intersect_cell_z, + .y = std.math.cast(size.CellCountInt, v.y) orelse break :intersect_cell_z, } }, v.delete, v.z, @@ -317,7 +318,7 @@ pub const ImageStorage = struct { // v.y is in active coords so we want to convert it to a pin // so we can compare by page offsets. const target_pin = t.screen.pages.pin(.{ .active = .{ - .y = v.y, + .y = std.math.cast(size.CellCountInt, v.y) orelse break :row, } }) orelse break :row; var it = self.placements.iterator(); diff --git a/src/terminal/point.zig b/src/terminal/point.zig index 41b7a3558..45aa28dea 100644 --- a/src/terminal/point.zig +++ b/src/terminal/point.zig @@ -1,6 +1,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; +const size = @import("size.zig"); /// The possible reference locations for a point. When someone says "(42, 80)" in the context of a terminal, that could mean multiple /// things: it is in the current visible viewport? the current active @@ -65,8 +66,8 @@ pub const Point = union(Tag) { }; pub const Coordinate = struct { - x: usize = 0, - y: usize = 0, + x: size.CellCountInt = 0, + y: size.CellCountInt = 0, pub fn eql(self: Coordinate, other: Coordinate) bool { return self.x == other.x and self.y == other.y; From e397abcadd9ee6759230c50bbdd96793ba65ea38 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Apr 2024 21:24:14 -0700 Subject: [PATCH 15/26] renderer/metal: swap to new cell contents map --- src/renderer/Metal.zig | 43 +++++++++++++----------------- src/renderer/metal/cell.zig | 52 +++++++++++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 030ed8d64..df32b0180 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -90,8 +90,8 @@ current_background_color: terminal.color.RGB, /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. -cells_bg: std.ArrayListUnmanaged(mtl_shaders.CellBg), -cells_text: std.ArrayListUnmanaged(mtl_shaders.CellText), +// cells_bg: std.ArrayListUnmanaged(mtl_shaders.CellBg), +// cells_text: std.ArrayListUnmanaged(mtl_shaders.CellText), cells: mtl_cell.Contents, /// The current GPU uniform values. @@ -557,8 +557,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .current_background_color = options.config.background, // Render state - .cells_bg = .{}, - .cells_text = .{}, .cells = .{}, .uniforms = .{ .projection_matrix = undefined, @@ -584,8 +582,6 @@ pub fn deinit(self: *Metal) void { self.gpu_state.deinit(); self.cells.deinit(self.alloc); - self.cells_bg.deinit(self.alloc); - self.cells_text.deinit(self.alloc); self.font_shaper.deinit(); @@ -776,13 +772,13 @@ pub fn updateFrame( } // Build our GPU cells (OLD) - try self.rebuildCells( - &critical.screen, - critical.mouse, - critical.preedit, - critical.cursor_style, - &critical.color_palette, - ); + // try self.rebuildCells( + // &critical.screen, + // critical.mouse, + // critical.preedit, + // critical.cursor_style, + // &critical.color_palette, + // ); // Build our GPU cells try self.rebuildCells2( @@ -834,8 +830,8 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // Setup our frame data try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); - try frame.cells_bg.sync(self.gpu_state.device, self.cells_bg.items); - try frame.cells.sync(self.gpu_state.device, self.cells_text.items); + try frame.cells_bg.sync(self.gpu_state.device, self.cells.bgs.items); + try frame.cells.sync(self.gpu_state.device, self.cells.text.items); // If we have custom shaders, update the animation time. if (self.custom_shader_state) |*state| { @@ -934,13 +930,13 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells - try self.drawCellBgs(encoder, frame, self.cells_bg.items.len); + try self.drawCellBgs(encoder, frame, self.cells.bgs.items.len); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells - try self.drawCellFgs(encoder, frame, self.cells_text.items.len); + try self.drawCellFgs(encoder, frame, self.cells.text.items.len); // Then draw remaining images try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); @@ -1587,12 +1583,6 @@ pub fn setScreenSize( .min_contrast = old.min_contrast, }; - // Reset our buffer sizes so that we free memory when the screen shrinks. - // This could be made more clever by only doing this when the screen - // shrinks but the performance cost really isn't that much. - self.cells_text.clearAndFree(self.alloc); - self.cells_bg.clearAndFree(self.alloc); - // Reset our cell contents. try self.cells.resize(self.alloc, grid_size); @@ -1892,8 +1882,8 @@ fn rebuildCells2( // Determine our x/y range for preedit. We don't want to render anything // here because we will render the preedit separately. const preedit_range: ?struct { - y: usize, - x: [2]usize, + y: terminal.size.CellCountInt, + x: [2]terminal.size.CellCountInt, cp_offset: usize, } = if (preedit) |preedit_v| preedit: { const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); @@ -1912,6 +1902,9 @@ fn rebuildCells2( while (row_it.next()) |row| { defer y += 1; + // If we're rebuilding a row, then we always clear the cells + self.cells.clear(y); + // True if we want to do font shaping around the cursor. We want to // do font shaping as long as the cursor is enabled. const shape_cursor = screen.viewportIsBottom() and diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig index f55e99adb..fb0b5ddfa 100644 --- a/src/renderer/metal/cell.zig +++ b/src/renderer/metal/cell.zig @@ -151,7 +151,7 @@ pub const Contents = struct { const coord_: ?terminal.Coordinate = switch (entry.key) { .bg => bg: { _ = self.bgs.swapRemove(original_index); - if (self.bgs.items.len == 0) break :bg null; + if (self.bgs.items.len == original_index) break :bg null; const new = self.bgs.items[original_index]; break :bg .{ .x = new.grid_pos[0], .y = new.grid_pos[1] }; }, @@ -161,7 +161,7 @@ pub const Contents = struct { .strikethrough, => text: { _ = self.text.swapRemove(original_index); - if (self.text.items.len == 0) break :text null; + if (self.text.items.len == original_index) break :text null; const new = self.text.items[original_index]; break :text .{ .x = new.grid_pos[0], .y = new.grid_pos[1] }; }, @@ -175,9 +175,19 @@ pub const Contents = struct { // removing the last element in the array, then nothing // is swapped in and nothing needs to be updated. if (coord_) |coord| { - const mapping = self.map[self.index(coord)].array.getPtr(entry.key); - assert(mapping.set); - mapping.index = original_index; + const old_index = switch (entry.key) { + .bg => self.bgs.items.len, + .text, .underline, .strikethrough => self.text.items.len, + }; + var old_it = self.map[self.index(coord)].array.iterator(); + while (old_it.next()) |old_entry| { + if (old_entry.value.set and + old_entry.value.index == old_index) + { + old_entry.value.index = original_index; + break; + } + } } } } @@ -293,6 +303,38 @@ test "Contents clear retains other content" { try testing.expectEqual(cell2, c.get(.bg, .{ .x = 4, .y = 2 }).?); } +test "Contents clear last added content" { + const testing = std.testing; + const alloc = testing.allocator; + + const rows = 10; + const cols = 10; + + var c: Contents = .{}; + try c.resize(alloc, .{ .rows = rows, .columns = cols }); + defer c.deinit(alloc); + + // Set some contents + const cell1: mtl_shaders.CellBg = .{ + .mode = .rgb, + .grid_pos = .{ 4, 1 }, + .cell_width = 1, + .color = .{ 0, 0, 0, 1 }, + }; + const cell2: mtl_shaders.CellBg = .{ + .mode = .rgb, + .grid_pos = .{ 4, 2 }, + .cell_width = 1, + .color = .{ 0, 0, 0, 1 }, + }; + try c.set(alloc, .bg, cell1); + try c.set(alloc, .bg, cell2); + c.clear(2); + + // Row 2 should still be valid. + try testing.expectEqual(cell1, c.get(.bg, .{ .x = 4, .y = 1 }).?); +} + test "Contents.Map size" { // We want to be mindful of when this increases because it affects // renderer memory significantly. From ef326ad11c7013f75159fbc17724870758576766 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 26 Apr 2024 21:53:29 -0700 Subject: [PATCH 16/26] renderer/metal: clear cell contents bottom-up --- src/renderer/Metal.zig | 6 +++--- src/renderer/metal/cell.zig | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index df32b0180..58e75afa0 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1897,10 +1897,10 @@ fn rebuildCells2( // Go row-by-row to build the cells. We go row by row because we do // font shaping by row. In the future, we will also do dirty tracking // by row. - var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); - var y: terminal.size.CellCountInt = 0; + var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); + var y: terminal.size.CellCountInt = screen.pages.rows; while (row_it.next()) |row| { - defer y += 1; + y = y - 1; // If we're rebuilding a row, then we always clear the cells self.cells.clear(y); diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig index fb0b5ddfa..e6974ae34 100644 --- a/src/renderer/metal/cell.zig +++ b/src/renderer/metal/cell.zig @@ -128,6 +128,14 @@ pub const Contents = struct { } /// Clear all of the cell contents for a given row. + /// + /// Due to the way this works internally, it is best to clear rows + /// from the bottom up. This is because when we clear a row, we + /// swap remove the last element in the list and then update the + /// mapping for the swapped element. If we clear from the top down, + /// then we would have to update the mapping for every element in + /// the list. If we clear from the bottom up, then we only have to + /// update the mapping for the last element in the list. pub fn clear( self: *Contents, y: usize, @@ -135,7 +143,12 @@ pub const Contents = struct { const start_idx = y * self.cols; const end_idx = start_idx + self.cols; const maps = self.map[start_idx..end_idx]; - for (maps) |*map| { + for (0..self.cols) |x| { + // It is better to clear from the right left due to the same + // reasons noted for bottom-up clearing in the doc comment. + const rev_x = self.cols - x - 1; + const map = &maps[rev_x]; + var it = map.array.iterator(); while (it.next()) |entry| { if (!entry.value.set) continue; From fe4fc509e9e7a7e8f91dfa9c0b44a809ecf4f213 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Apr 2024 21:42:09 -0700 Subject: [PATCH 17/26] renderer/metal: use index() for all cell settings --- src/renderer/metal/cell.zig | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig index e6974ae34..29f101534 100644 --- a/src/renderer/metal/cell.zig +++ b/src/renderer/metal/cell.zig @@ -79,8 +79,7 @@ pub const Contents = struct { comptime key: Key, coord: terminal.Coordinate, ) ?key.CellType() { - const idx = coord.y * self.cols + coord.x; - const mapping = self.map[idx].array.get(key); + const mapping = self.map[self.index(coord)].array.get(key); if (!mapping.set) return null; return switch (key) { .bg => self.bgs.items[mapping.index], @@ -136,11 +135,8 @@ pub const Contents = struct { /// then we would have to update the mapping for every element in /// the list. If we clear from the bottom up, then we only have to /// update the mapping for the last element in the list. - pub fn clear( - self: *Contents, - y: usize, - ) void { - const start_idx = y * self.cols; + pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void { + const start_idx = self.index(.{ .x = 0, .y = y }); const end_idx = start_idx + self.cols; const maps = self.map[start_idx..end_idx]; for (0..self.cols) |x| { From c15f4d72585c4c7efeb03a1de424fca4adaed777 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Apr 2024 22:01:03 -0700 Subject: [PATCH 18/26] renderer/metal: render the cursor --- src/renderer/Metal.zig | 145 ++++++++++++++++++++++++++---------- src/renderer/metal/cell.zig | 67 +++++++++++++++-- 2 files changed, 165 insertions(+), 47 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 58e75afa0..30dfc6d52 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -543,6 +543,9 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }; }; + const cells = try mtl_cell.Contents.init(alloc); + errdefer cells.deinit(alloc); + return Metal{ .alloc = alloc, .config = options.config, @@ -557,7 +560,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .current_background_color = options.config.background, // Render state - .cells = .{}, + .cells = cells, .uniforms = .{ .projection_matrix = undefined, .cell_size = undefined, @@ -829,9 +832,11 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { // log.debug("drawing frame index={}", .{self.gpu_state.frame_index}); // Setup our frame data + const cells_bg = self.cells.bgCells(); + const cells_fg = self.cells.fgCells(); try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); - try frame.cells_bg.sync(self.gpu_state.device, self.cells.bgs.items); - try frame.cells.sync(self.gpu_state.device, self.cells.text.items); + try frame.cells_bg.sync(self.gpu_state.device, cells_bg); + try frame.cells.sync(self.gpu_state.device, cells_fg); // If we have custom shaders, update the animation time. if (self.custom_shader_state) |*state| { @@ -930,13 +935,13 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]); // Then draw background cells - try self.drawCellBgs(encoder, frame, self.cells.bgs.items.len); + try self.drawCellBgs(encoder, frame, cells_bg.len); // Then draw images under text try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]); // Then draw fg cells - try self.drawCellFgs(encoder, frame, self.cells.text.items.len); + try self.drawCellFgs(encoder, frame, cells_fg.len); // Then draw remaining images try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]); @@ -1864,7 +1869,6 @@ fn rebuildCells2( ) !void { // TODO: cursor_cell // TODO: cursor_Row - _ = cursor_style_; // Create an arena for all our temporary allocations while rebuilding var arena = ArenaAllocator.init(self.alloc); @@ -1978,42 +1982,48 @@ fn rebuildCells2( } } - // 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. - // if (cursor_style_) |cursor_style| cursor_style: { - // // If we have a preedit, we try to render the preedit text on top - // // of the cursor. - // if (preedit) |preedit_v| { - // const range = preedit_range.?; - // var x = range.x[0]; - // for (preedit_v.codepoints[range.cp_offset..]) |cp| { - // self.addPreeditCell(cp, x, range.y) catch |err| { - // log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ - // x, - // range.y, - // err, - // }); - // }; + // Setup our cursor rendering information. + cursor: { + // If we have no cursor style then we don't render the cursor. + const style = cursor_style_ orelse { + self.cells.setCursor(null); + break :cursor; + }; + + // Prepare the cursor cell contents. + self.addCursor2(screen, style); + } + + // If we have a preedit, we try to render the preedit text on top + // of the cursor. + // if (preedit) |preedit_v| { + // const range = preedit_range.?; + // var x = range.x[0]; + // for (preedit_v.codepoints[range.cp_offset..]) |cp| { + // self.addPreeditCell(cp, x, range.y) catch |err| { + // log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ + // x, + // range.y, + // err, + // }); + // }; // - // x += if (cp.wide) 2 else 1; - // } - // - // // Preedit hides the cursor - // break :cursor_style; + // x += if (cp.wide) 2 else 1; // } // - // _ = self.addCursor(screen, cursor_style); - // // if (cursor_cell) |*cell| { - // // if (cell.mode == .fg) { - // // cell.color = if (self.config.cursor_text) |txt| - // // .{ txt.r, txt.g, txt.b, 255 } - // // else - // // .{ self.background_color.r, self.background_color.g, self.background_color.b, 255 }; - // // } - // // - // // self.cells_text.appendAssumeCapacity(cell.*); - // // } + // // Preedit hides the cursor + // break :cursor_style; + // } + + // if (cursor_cell) |*cell| { + // if (cell.mode == .fg) { + // cell.color = if (self.config.cursor_text) |txt| + // .{ txt.r, txt.g, txt.b, 255 } + // else + // .{ self.background_color.r, self.background_color.g, self.background_color.b, 255 }; + // } + // + // self.cells_text.appendAssumeCapacity(cell.*); // } } @@ -2456,6 +2466,63 @@ fn updateCell( return true; } +fn addCursor2( + self: *Metal, + screen: *terminal.Screen, + cursor_style: renderer.CursorStyle, +) void { + // Add the cursor. We render the cursor over the wide character if + // we're on the wide characer tail. + const wide, const x = cell: { + // The cursor goes over the screen cursor position. + const cell = screen.cursor.page_cell; + if (cell.wide != .spacer_tail or screen.cursor.x == 0) + break :cell .{ cell.wide == .wide, screen.cursor.x }; + + // If we're part of a wide character, we move the cursor back to + // the actual character. + const prev_cell = screen.cursorCellLeft(1); + break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 }; + }; + + const color = self.cursor_color orelse self.foreground_color; + const alpha: u8 = if (!self.focused) 255 else alpha: { + const alpha = 255 * self.config.cursor_opacity; + break :alpha @intFromFloat(@ceil(alpha)); + }; + + const sprite: font.Sprite = switch (cursor_style) { + .block => .cursor_rect, + .block_hollow => .cursor_hollow_rect, + .bar => .cursor_bar, + .underline => .underline, + }; + + const render = self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(sprite), + .{ + .cell_width = if (wide) 2 else 1, + .grid_metrics = self.grid_metrics, + }, + ) catch |err| { + log.warn("error rendering cursor glyph err={}", .{err}); + return; + }; + + self.cells.setCursor(.{ + .mode = .fg, + .grid_pos = .{ x, screen.cursor.y }, + .cell_width = if (wide) 2 else 1, + .color = .{ color.r, color.g, color.b, alpha }, + .bg_color = .{ 0, 0, 0, 0 }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, + }); +} + fn addCursor( self: *Metal, screen: *terminal.Screen, diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig index 29f101534..66ffd7497 100644 --- a/src/renderer/metal/cell.zig +++ b/src/renderer/metal/cell.zig @@ -35,19 +35,47 @@ pub const Contents = struct { /// /// Before any operation, this must be initialized by calling resize /// on the contents. - map: []Map = undefined, + map: []Map, /// The grid size of the terminal. This is used to determine the /// map array index from a coordinate. - cols: usize = 0, + cols: usize, /// The actual GPU data (on the CPU) for all the cells in the terminal. /// This only contains the cells that have content set. To determine /// if a cell has content set, we check the map. /// /// This data is synced to a buffer on every frame. - bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg) = .{}, - text: std.ArrayListUnmanaged(mtl_shaders.CellText) = .{}, + bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg), + text: std.ArrayListUnmanaged(mtl_shaders.CellText), + + /// True when the cursor should be rendered. + cursor: bool, + + /// The amount of text elements we reserve at the beginning for + /// special elements like the cursor. + const text_reserved_len = 1; + + pub fn init(alloc: Allocator) !Contents { + const map = try alloc.alloc(Map, 0); + errdefer alloc.free(map); + + var result: Contents = .{ + .map = map, + .cols = 0, + .bgs = .{}, + .text = .{}, + .cursor = false, + }; + + // We preallocate some amount of space for cell contents + // we always have as a prefix. For now the current prefix + // is length 1: the cursor. + try result.text.ensureTotalCapacity(alloc, text_reserved_len); + result.text.items.len = text_reserved_len; + + return result; + } pub fn deinit(self: *Contents, alloc: Allocator) void { alloc.free(self.map); @@ -70,7 +98,30 @@ pub const Contents = struct { self.map = map; self.cols = size.columns; self.bgs.clearAndFree(alloc); - self.text.clearAndFree(alloc); + self.text.shrinkAndFree(alloc, text_reserved_len); + } + + /// Returns the slice of fg cell contents to sync with the GPU. + pub fn fgCells(self: *const Contents) []const mtl_shaders.CellText { + const start: usize = if (self.cursor) 0 else 1; + return self.text.items[start..]; + } + + /// Returns the slice of bg cell contents to sync with the GPU. + pub fn bgCells(self: *const Contents) []const mtl_shaders.CellBg { + return self.bgs.items; + } + + /// Set the cursor value. If the value is null then the cursor + /// is hidden. + pub fn setCursor(self: *Contents, v: ?mtl_shaders.CellText) void { + const cell = v orelse { + self.cursor = false; + return; + }; + + self.cursor = true; + self.text.items[0] = cell; } /// Get the cell contents for the given type and coordinate. @@ -244,7 +295,7 @@ test Contents { const rows = 10; const cols = 10; - var c: Contents = .{}; + var c = try Contents.init(alloc); try c.resize(alloc, .{ .rows = rows, .columns = cols }); defer c.deinit(alloc); @@ -287,7 +338,7 @@ test "Contents clear retains other content" { const rows = 10; const cols = 10; - var c: Contents = .{}; + var c = try Contents.init(alloc); try c.resize(alloc, .{ .rows = rows, .columns = cols }); defer c.deinit(alloc); @@ -319,7 +370,7 @@ test "Contents clear last added content" { const rows = 10; const cols = 10; - var c: Contents = .{}; + var c = try Contents.init(alloc); try c.resize(alloc, .{ .rows = rows, .columns = cols }); defer c.deinit(alloc); From 3a7dc355a028c0350a4771ed00d45c98756dcb24 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Apr 2024 22:13:17 -0700 Subject: [PATCH 19/26] renderer/metal: invert text under cursor again --- src/renderer/Metal.zig | 27 ++++++++++++++++++++++++++- src/renderer/metal/shaders.zig | 12 ++++++++---- src/renderer/shaders/cell.metal | 17 ++++++++++++++--- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 30dfc6d52..a5f48c179 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -565,6 +565,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { .projection_matrix = undefined, .cell_size = undefined, .min_contrast = options.config.min_contrast, + .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) }, + .cursor_color = undefined, }, // Fonts @@ -683,6 +685,8 @@ pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) void { @floatFromInt(metrics.cell_height), }, .min_contrast = self.uniforms.min_contrast, + .cursor_pos = self.uniforms.cursor_pos, + .cursor_color = self.uniforms.cursor_color, }; } @@ -1586,6 +1590,8 @@ pub fn setScreenSize( @floatFromInt(self.grid_metrics.cell_height), }, .min_contrast = old.min_contrast, + .cursor_pos = old.cursor_pos, + .cursor_color = old.cursor_color, }; // Reset our cell contents. @@ -1987,11 +1993,30 @@ fn rebuildCells2( // If we have no cursor style then we don't render the cursor. const style = cursor_style_ orelse { self.cells.setCursor(null); + self.uniforms.cursor_pos = .{ + std.math.maxInt(u16), + std.math.maxInt(u16), + }; break :cursor; }; // Prepare the cursor cell contents. self.addCursor2(screen, style); + + // Setup our uniforms for the cursor so that any data + // under the cursor can render differently. + self.uniforms.cursor_pos = .{ screen.cursor.x, screen.cursor.y }; + self.uniforms.cursor_color = if (self.config.cursor_text) |txt| .{ + txt.r, + txt.g, + txt.b, + 255, + } else .{ + self.background_color.r, + self.background_color.g, + self.background_color.b, + 255, + }; } // If we have a preedit, we try to render the preedit text on top @@ -2512,7 +2537,7 @@ fn addCursor2( }; self.cells.setCursor(.{ - .mode = .fg, + .mode = .cursor, .grid_pos = .{ x, screen.cursor.y }, .cell_width = if (wide) 2 else 1, .color = .{ color.r, color.g, color.b, alpha }, diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 2d7fb9700..a6303c78d 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -116,6 +116,10 @@ pub const Uniforms = extern struct { /// The minimum contrast ratio for text. The contrast ratio is calculated /// according to the WCAG 2.0 spec. min_contrast: f32, + + /// The cursor position and color. + cursor_pos: [2]u16, + cursor_color: [4]u8, }; /// The uniforms used for custom postprocess shaders. @@ -296,10 +300,10 @@ pub const CellText = extern struct { cell_width: u8, pub const Mode = enum(u8) { - bg = 1, - fg = 2, - fg_constrained = 3, - fg_color = 7, + fg = 1, + fg_constrained = 2, + fg_color = 3, + cursor = 4, }; }; diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 3aae45ef5..f486d4ecf 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -4,6 +4,8 @@ struct Uniforms { float4x4 projection_matrix; float2 cell_size; float min_contrast; + ushort2 cursor_pos; + uchar4 cursor_color; }; //------------------------------------------------------------------- @@ -134,9 +136,10 @@ fragment float4 cell_bg_fragment(CellBgVertexOut in [[stage_in]]) { // The possible modes that a cell fg entry can take. enum CellTextMode : uint8_t { - MODE_TEXT = 2u, - MODE_TEXT_CONSTRAINED = 3u, - MODE_TEXT_COLOR = 7u, + MODE_TEXT = 1u, + MODE_TEXT_CONSTRAINED = 2u, + MODE_TEXT_COLOR = 3u, + MODE_TEXT_CURSOR = 4u, }; struct CellTextVertexIn { @@ -243,6 +246,13 @@ vertex CellTextVertexOut cell_text_vertex(unsigned int vid [[vertex_id]], out.color = contrasted_color(uniforms.min_contrast, out.color, bg_color); } + // If this cell is the cursor cell, then we need to change the color. + if (input.mode != MODE_TEXT_CURSOR && + input.grid_pos.x == uniforms.cursor_pos.x && + input.grid_pos.y == uniforms.cursor_pos.y) { + out.color = float4(uniforms.cursor_color) / 255.0f; + } + return out; } @@ -254,6 +264,7 @@ fragment float4 cell_text_fragment(CellTextVertexOut in [[stage_in]], constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); switch (in.mode) { + case MODE_TEXT_CURSOR: case MODE_TEXT_CONSTRAINED: case MODE_TEXT: { // Normalize the texture coordinates to [0,1] From 06f21a0daa6d2d383338241573bc7dafadb2d3f3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 27 Apr 2024 22:26:40 -0700 Subject: [PATCH 20/26] renderer/metal: only invert if block --- src/renderer/Metal.zig | 49 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index a5f48c179..4ac6ef807 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1873,9 +1873,6 @@ fn rebuildCells2( cursor_style_: ?renderer.CursorStyle, color_palette: *const terminal.color.Palette, ) !void { - // TODO: cursor_cell - // TODO: cursor_Row - // Create an arena for all our temporary allocations while rebuilding var arena = ArenaAllocator.init(self.alloc); defer arena.deinit(); @@ -1990,33 +1987,35 @@ fn rebuildCells2( // Setup our cursor rendering information. cursor: { - // If we have no cursor style then we don't render the cursor. - const style = cursor_style_ orelse { - self.cells.setCursor(null); - self.uniforms.cursor_pos = .{ - std.math.maxInt(u16), - std.math.maxInt(u16), - }; - break :cursor; + // By default, we don't handle cursor inversion on the shader. + self.cells.setCursor(null); + self.uniforms.cursor_pos = .{ + std.math.maxInt(u16), + std.math.maxInt(u16), }; // Prepare the cursor cell contents. + const style = cursor_style_ orelse break :cursor; self.addCursor2(screen, style); - // Setup our uniforms for the cursor so that any data - // under the cursor can render differently. - self.uniforms.cursor_pos = .{ screen.cursor.x, screen.cursor.y }; - self.uniforms.cursor_color = if (self.config.cursor_text) |txt| .{ - txt.r, - txt.g, - txt.b, - 255, - } else .{ - self.background_color.r, - self.background_color.g, - self.background_color.b, - 255, - }; + // If the cursor is visible then we set our uniforms. + if (style == .block and screen.viewportIsBottom()) { + self.uniforms.cursor_pos = .{ + screen.cursor.x, + screen.cursor.y, + }; + self.uniforms.cursor_color = if (self.config.cursor_text) |txt| .{ + txt.r, + txt.g, + txt.b, + 255, + } else .{ + self.background_color.r, + self.background_color.g, + self.background_color.b, + 255, + }; + } } // If we have a preedit, we try to render the preedit text on top From 313eb1176d809ac9bda88acc1e3ed1abeb5d8b56 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 09:37:06 -0700 Subject: [PATCH 21/26] renderer/metal: preedit works again --- src/renderer/Metal.zig | 99 ++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 33 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 4ac6ef807..22afe2d95 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1941,9 +1941,8 @@ fn rebuildCells2( .y = y, }; - // If this cell falls within our preedit range then we skip it. - // We do this so we don't have conflicting data on the same - // cell. + // If this cell falls within our preedit range then we + // skip this because preedits are setup separately. if (preedit_range) |range| { if (range.y == coord.y and coord.x >= range.x[0] and @@ -1994,6 +1993,9 @@ fn rebuildCells2( std.math.maxInt(u16), }; + // If we have preedit text, we don't setup a cursor + if (preedit != null) break :cursor; + // Prepare the cursor cell contents. const style = cursor_style_ orelse break :cursor; self.addCursor2(screen, style); @@ -2018,37 +2020,22 @@ fn rebuildCells2( } } - // If we have a preedit, we try to render the preedit text on top - // of the cursor. - // if (preedit) |preedit_v| { - // const range = preedit_range.?; - // var x = range.x[0]; - // for (preedit_v.codepoints[range.cp_offset..]) |cp| { - // self.addPreeditCell(cp, x, range.y) catch |err| { - // log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ - // x, - // range.y, - // err, - // }); - // }; - // - // x += if (cp.wide) 2 else 1; - // } - // - // // Preedit hides the cursor - // break :cursor_style; - // } + // Setup our preedit text. + if (preedit) |preedit_v| { + const range = preedit_range.?; + var x = range.x[0]; + for (preedit_v.codepoints[range.cp_offset..]) |cp| { + self.addPreeditCell2(cp, .{ .x = x, .y = range.y }) catch |err| { + log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ + x, + range.y, + err, + }); + }; - // if (cursor_cell) |*cell| { - // if (cell.mode == .fg) { - // cell.color = if (self.config.cursor_text) |txt| - // .{ txt.r, txt.g, txt.b, 255 } - // else - // .{ self.background_color.r, self.background_color.g, self.background_color.b, 255 }; - // } - // - // self.cells_text.appendAssumeCapacity(cell.*); - // } + x += if (cp.wide) 2 else 1; + } + } } fn updateCell2( @@ -2606,6 +2593,52 @@ fn addCursor( return &self.cells_text.items[self.cells_text.items.len - 1]; } +fn addPreeditCell2( + self: *Metal, + cp: renderer.State.Preedit.Codepoint, + coord: terminal.Coordinate, +) !void { + // Preedit is rendered inverted + const bg = self.foreground_color; + const fg = self.background_color; + + // Render the glyph for our preedit text + const render_ = self.font_grid.renderCodepoint( + self.alloc, + @intCast(cp.codepoint), + .regular, + .text, + .{ .grid_metrics = self.grid_metrics }, + ) catch |err| { + log.warn("error rendering preedit glyph err={}", .{err}); + return; + }; + const render = render_ orelse { + log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); + return; + }; + + // Add our opaque background cell + try self.cells.set(self.alloc, .bg, .{ + .mode = .rgb, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, + .cell_width = if (cp.wide) 2 else 1, + .color = .{ bg.r, bg.g, bg.b, 255 }, + }); + + // Add our text + try self.cells.set(self.alloc, .text, .{ + .mode = .fg, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, + .cell_width = if (cp.wide) 2 else 1, + .color = .{ fg.r, fg.g, fg.b, 255 }, + .bg_color = .{ bg.r, bg.g, bg.b, 255 }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, + }); +} + fn addPreeditCell( self: *Metal, cp: renderer.State.Preedit.Codepoint, From f7714a113dd08ba8cc729a07008f6e792491b891 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 09:40:03 -0700 Subject: [PATCH 22/26] renderer/opengl: fix compilation --- src/renderer/OpenGL.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 7e7ab7dbe..041947f32 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -939,8 +939,8 @@ pub fn rebuildCells( // Determine our x/y range for preedit. We don't want to render anything // here because we will render the preedit separately. const preedit_range: ?struct { - y: usize, - x: [2]usize, + y: terminal.size.CellCountInt, + x: [2]terminal.size.CellCountInt, cp_offset: usize, } = if (preedit) |preedit_v| preedit: { const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); @@ -958,7 +958,7 @@ pub fn rebuildCells( // Build each cell var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); - var y: usize = 0; + var y: terminal.size.CellCountInt = 0; while (row_it.next()) |row| { defer y += 1; From a299338099a00884a1078d99add03534c70e40a0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 09:41:45 -0700 Subject: [PATCH 23/26] renderer/metal: remove old functions --- src/renderer/Metal.zig | 568 +---------------------------------------- 1 file changed, 8 insertions(+), 560 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 22afe2d95..ed901d75c 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -778,17 +778,8 @@ pub fn updateFrame( if (critical.preedit) |p| p.deinit(self.alloc); } - // Build our GPU cells (OLD) - // try self.rebuildCells( - // &critical.screen, - // critical.mouse, - // critical.preedit, - // critical.cursor_style, - // &critical.color_palette, - // ); - // Build our GPU cells - try self.rebuildCells2( + try self.rebuildCells( &critical.screen, critical.mouse, critical.preedit, @@ -1645,227 +1636,10 @@ pub fn setScreenSize( log.debug("screen size screen={} grid={}, cell_width={} cell_height={}", .{ dim, grid_size, self.grid_metrics.cell_width, self.grid_metrics.cell_height }); } -/// 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, - screen: *terminal.Screen, - mouse: renderer.State.Mouse, - preedit: ?renderer.State.Preedit, - cursor_style_: ?renderer.CursorStyle, - color_palette: *const terminal.color.Palette, -) !void { - const rows_usize: usize = @intCast(screen.pages.rows); - const cols_usize: usize = @intCast(screen.pages.cols); - - // Bg cells at most will need space for the visible screen size - self.cells_bg.clearRetainingCapacity(); - try self.cells_bg.ensureTotalCapacity( - self.alloc, - rows_usize * cols_usize, - ); - - // Over-allocate just to ensure we don't allocate again during loops. - self.cells_text.clearRetainingCapacity(); - try self.cells_text.ensureTotalCapacity( - self.alloc, - - // * 3 for glyph + underline + strikethrough for each cell - // + 1 for cursor - (rows_usize * cols_usize * 3) + 1, - ); - - // Create an arena for all our temporary allocations while rebuilding - var arena = ArenaAllocator.init(self.alloc); - defer arena.deinit(); - const arena_alloc = arena.allocator(); - - // Create our match set for the links. - var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet( - arena_alloc, - screen, - mouse_pt, - mouse.mods, - ) else .{}; - - // Determine our x/y range for preedit. We don't want to render anything - // here because we will render the preedit separately. - const preedit_range: ?struct { - y: terminal.size.CellCountInt, - x: [2]terminal.size.CellCountInt, - cp_offset: usize, - } = if (preedit) |preedit_v| preedit: { - const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); - break :preedit .{ - .y = screen.cursor.y, - .x = .{ range.start, range.end }, - .cp_offset = range.cp_offset, - }; - } else null; - - // 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: ?mtl_shaders.CellText = null; - - // Build each cell - var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); - var y: terminal.size.CellCountInt = 0; - while (row_it.next()) |row| { - defer y += 1; - - // True if this is the row with our cursor. There are a lot of conditions - // here because the reasons we need to know this are primarily to invert. - // - // - If we aren't drawing the cursor then we don't need to change our rendering. - // - If the cursor is not visible, then we don't need to change rendering. - // - If the cursor style is not a box, then we don't need to change - // rendering because it'll never fully overlap a glyph. - // - If the viewport is not at the bottom, then we don't need to - // change rendering because the cursor is not visible. - // (NOTE: this may not be fully correct, we may be scrolled - // slightly up and the cursor may be visible) - // - If this y doesn't match our cursor y then we don't need to - // change rendering. - // - const cursor_row = if (cursor_style_) |cursor_style| - cursor_style == .block and - screen.viewportIsBottom() and - y == screen.cursor.y - else - false; - - // True if we want to do font shaping around the cursor. We want to - // do font shaping as long as the cursor is enabled. - const shape_cursor = screen.viewportIsBottom() and - y == screen.cursor.y; - - // 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_text.items.len; - defer if (cursor_row) { - // If we're on a wide spacer tail, then we want to look for - // the previous cell. - const screen_cell = row.cells(.all)[screen.cursor.x]; - const x = screen.cursor.x - @intFromBool(screen_cell.wide == .spacer_tail); - for (self.cells_text.items[start_i..]) |cell| { - if (cell.grid_pos[0] == x and - (cell.mode == .fg or cell.mode == .fg_color)) - { - cursor_cell = cell; - break; - } - } - }; - - // We need to get this row's selection if there is one for proper - // run splitting. - const row_selection = sel: { - const sel = screen.selection orelse break :sel null; - const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse - break :sel null; - break :sel sel.containedRow(screen, pin) orelse null; - }; - - // Split our row into runs and shape each one. - var iter = self.font_shaper.runIterator( - self.font_grid, - screen, - row, - row_selection, - if (shape_cursor) screen.cursor.x else null, - ); - while (try iter.next(self.alloc)) |run| { - for (try self.font_shaper.shape(run)) |shaper_cell| { - // If this cell falls within our preedit range then we skip it. - // We do this so we don't have conflicting data on the same - // cell. - if (preedit_range) |range| { - if (range.y == y and - shaper_cell.x >= range.x[0] and - shaper_cell.x <= range.x[1]) - { - continue; - } - } - - // It this cell is within our hint range then we need to - // underline it. - const cell: terminal.Pin = cell: { - var copy = row; - copy.x = shaper_cell.x; - break :cell copy; - }; - - if (self.updateCell( - screen, - cell, - if (link_match_set.orderedContains(screen, cell)) - .single - else - null, - color_palette, - shaper_cell, - run, - shaper_cell.x, - y, - )) |update| { - assert(update); - } else |err| { - log.warn("error building cell, will be invalid x={} y={}, err={}", .{ - shaper_cell.x, - y, - err, - }); - } - } - } - } - - // 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. - if (cursor_style_) |cursor_style| cursor_style: { - // If we have a preedit, we try to render the preedit text on top - // of the cursor. - if (preedit) |preedit_v| { - const range = preedit_range.?; - var x = range.x[0]; - for (preedit_v.codepoints[range.cp_offset..]) |cp| { - self.addPreeditCell(cp, x, range.y) catch |err| { - log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ - x, - range.y, - err, - }); - }; - - x += if (cp.wide) 2 else 1; - } - - // Preedit hides the cursor - break :cursor_style; - } - - _ = self.addCursor(screen, cursor_style); - if (cursor_cell) |*cell| { - if (cell.mode == .fg) { - cell.color = if (self.config.cursor_text) |txt| - .{ txt.r, txt.g, txt.b, 255 } - else - .{ self.background_color.r, self.background_color.g, self.background_color.b, 255 }; - } - - self.cells_text.appendAssumeCapacity(cell.*); - } - } -} - /// Convert the terminal state to GPU cells stored in CPU memory. These /// are then synced to the GPU in the next frame. This only updates CPU /// memory and doesn't touch the GPU. -fn rebuildCells2( +fn rebuildCells( self: *Metal, screen: *terminal.Screen, mouse: renderer.State.Mouse, @@ -1960,7 +1734,7 @@ fn rebuildCells2( break :cell copy; }; - if (self.updateCell2( + if (self.updateCell( screen, cell, if (link_match_set.orderedContains(screen, cell)) @@ -1998,7 +1772,7 @@ fn rebuildCells2( // Prepare the cursor cell contents. const style = cursor_style_ orelse break :cursor; - self.addCursor2(screen, style); + self.addCursor(screen, style); // If the cursor is visible then we set our uniforms. if (style == .block and screen.viewportIsBottom()) { @@ -2025,7 +1799,7 @@ fn rebuildCells2( const range = preedit_range.?; var x = range.x[0]; for (preedit_v.codepoints[range.cp_offset..]) |cp| { - self.addPreeditCell2(cp, .{ .x = x, .y = range.y }) catch |err| { + self.addPreeditCell(cp, .{ .x = x, .y = range.y }) catch |err| { log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ x, range.y, @@ -2038,7 +1812,7 @@ fn rebuildCells2( } } -fn updateCell2( +fn updateCell( self: *Metal, screen: *const terminal.Screen, cell_pin: terminal.Pin, @@ -2257,227 +2031,7 @@ fn updateCell2( return true; } -fn updateCell( - self: *Metal, - screen: *const terminal.Screen, - cell_pin: terminal.Pin, - cell_underline: ?terminal.Attribute.Underline, - palette: *const terminal.color.Palette, - shaper_cell: font.shape.Cell, - shaper_run: font.shape.TextRun, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, -) !bool { - const BgFg = struct { - /// Background is optional because in un-inverted mode - /// it may just be equivalent to the default background in - /// which case we do nothing to save on GPU render time. - bg: ?terminal.color.RGB, - - /// Fg is always set to some color, though we may not render - /// any fg if the cell is empty or has no attributes like - /// underline. - fg: terminal.color.RGB, - }; - - // True if this cell is selected - const selected: bool = if (screen.selection) |sel| - sel.contains(screen, cell_pin) - else - false; - - const rac = cell_pin.rowAndCell(); - const cell = rac.cell; - const style = cell_pin.style(cell); - const underline = cell_underline orelse style.flags.underline; - - // The colors for the cell. - const colors: BgFg = colors: { - // The normal cell result - const cell_res: BgFg = if (!style.flags.inverse) .{ - // In normal mode, background and fg match the cell. We - // un-optionalize the fg by defaulting to our fg color. - .bg = style.bg(cell, palette), - .fg = style.fg(palette) orelse self.foreground_color, - } 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 = style.fg(palette) orelse self.foreground_color, - .fg = style.bg(cell, palette) orelse self.background_color, - }; - - // If we are selected, we our colors are just inverted fg/bg - const selection_res: ?BgFg = if (selected) .{ - .bg = if (self.config.invert_selection_fg_bg) - cell_res.fg - else - self.config.selection_background orelse self.foreground_color, - .fg = if (self.config.invert_selection_fg_bg) - cell_res.bg orelse self.background_color - else - self.config.selection_foreground orelse self.background_color, - } else null; - - // If the cell is "invisible" then we just make fg = bg so that - // the cell is transparent but still copy-able. - const res: BgFg = selection_res orelse cell_res; - if (style.flags.invisible) { - break :colors BgFg{ - .bg = res.bg, - .fg = res.bg orelse self.background_color, - }; - } - - break :colors res; - }; - - // Alpha multiplier - const alpha: u8 = if (style.flags.faint) 175 else 255; - - // If the cell has a background, we always draw it. - const bg: [4]u8 = if (colors.bg) |rgb| bg: { - // Determine our background alpha. If we have transparency configured - // then this is dynamic depending on some situations. This is all - // in an attempt to make transparency look the best for various - // situations. See inline comments. - const bg_alpha: u8 = bg_alpha: { - const default: u8 = 255; - - if (self.config.background_opacity >= 1) break :bg_alpha default; - - // If we're selected, we do not apply background opacity - if (selected) break :bg_alpha default; - - // If we're reversed, do not apply background opacity - if (style.flags.inverse) break :bg_alpha default; - - // If we have a background and its not the default background - // then we apply background opacity - if (style.bg(cell, palette) != null and !rgb.eql(self.background_color)) { - break :bg_alpha default; - } - - // We apply background opacity. - var bg_alpha: f64 = @floatFromInt(default); - bg_alpha *= self.config.background_opacity; - bg_alpha = @ceil(bg_alpha); - break :bg_alpha @intFromFloat(bg_alpha); - }; - - self.cells_bg.appendAssumeCapacity(.{ - .mode = .rgb, - .grid_pos = .{ x, y }, - .cell_width = cell.gridWidth(), - .color = .{ rgb.r, rgb.g, rgb.b, bg_alpha }, - }); - - break :bg .{ rgb.r, rgb.g, rgb.b, bg_alpha }; - } else .{ - self.current_background_color.r, - self.current_background_color.g, - self.current_background_color.b, - @intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))), - }; - - // If the cell has a character, draw it - if (cell.hasText()) fg: { - // Render - const render = try self.font_grid.renderGlyph( - self.alloc, - shaper_run.font_index, - shaper_cell.glyph_index orelse break :fg, - .{ - .grid_metrics = self.grid_metrics, - .thicken = self.config.font_thicken, - }, - ); - - const mode: mtl_shaders.CellText.Mode = switch (try fgMode( - render.presentation, - cell_pin, - )) { - .normal => .fg, - .color => .fg_color, - .constrained => .fg_constrained, - }; - - self.cells_text.appendAssumeCapacity(.{ - .mode = mode, - .grid_pos = .{ x, y }, - .cell_width = cell.gridWidth(), - .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, - .bg_color = bg, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .glyph_offset = .{ - render.glyph.offset_x + shaper_cell.x_offset, - render.glyph.offset_y + shaper_cell.y_offset, - }, - }); - } - - if (underline != .none) { - const sprite: font.Sprite = switch (underline) { - .none => unreachable, - .single => .underline, - .double => .underline_double, - .dotted => .underline_dotted, - .dashed => .underline_dashed, - .curly => .underline_curly, - }; - - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = if (cell.wide == .wide) 2 else 1, - .grid_metrics = self.grid_metrics, - }, - ); - - const color = style.underlineColor(palette) orelse colors.fg; - - self.cells_text.appendAssumeCapacity(.{ - .mode = .fg, - .grid_pos = .{ x, y }, - .cell_width = cell.gridWidth(), - .color = .{ color.r, color.g, color.b, alpha }, - .bg_color = bg, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, - }); - } - - if (style.flags.strikethrough) { - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(font.Sprite.strikethrough), - .{ - .cell_width = if (cell.wide == .wide) 2 else 1, - .grid_metrics = self.grid_metrics, - }, - ); - - self.cells_text.appendAssumeCapacity(.{ - .mode = .fg, - .grid_pos = .{ x, y }, - .cell_width = cell.gridWidth(), - .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, - .bg_color = bg, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, - }); - } - - return true; -} - -fn addCursor2( +fn addCursor( self: *Metal, screen: *terminal.Screen, cursor_style: renderer.CursorStyle, @@ -2534,66 +2088,7 @@ fn addCursor2( }); } -fn addCursor( - self: *Metal, - screen: *terminal.Screen, - cursor_style: renderer.CursorStyle, -) ?*const mtl_shaders.CellText { - // Add the cursor. We render the cursor over the wide character if - // we're on the wide characer tail. - const wide, const x = cell: { - // The cursor goes over the screen cursor position. - const cell = screen.cursor.page_cell; - if (cell.wide != .spacer_tail or screen.cursor.x == 0) - break :cell .{ cell.wide == .wide, screen.cursor.x }; - - // If we're part of a wide character, we move the cursor back to - // the actual character. - const prev_cell = screen.cursorCellLeft(1); - break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 }; - }; - - const color = self.cursor_color orelse self.foreground_color; - const alpha: u8 = if (!self.focused) 255 else alpha: { - const alpha = 255 * self.config.cursor_opacity; - break :alpha @intFromFloat(@ceil(alpha)); - }; - - const sprite: font.Sprite = switch (cursor_style) { - .block => .cursor_rect, - .block_hollow => .cursor_hollow_rect, - .bar => .cursor_bar, - .underline => .underline, - }; - - const render = self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = if (wide) 2 else 1, - .grid_metrics = self.grid_metrics, - }, - ) catch |err| { - log.warn("error rendering cursor glyph err={}", .{err}); - return null; - }; - - self.cells_text.appendAssumeCapacity(.{ - .mode = .fg, - .grid_pos = .{ x, screen.cursor.y }, - .cell_width = if (wide) 2 else 1, - .color = .{ color.r, color.g, color.b, alpha }, - .bg_color = .{ 0, 0, 0, 0 }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, - }); - - return &self.cells_text.items[self.cells_text.items.len - 1]; -} - -fn addPreeditCell2( +fn addPreeditCell( self: *Metal, cp: renderer.State.Preedit.Codepoint, coord: terminal.Coordinate, @@ -2639,53 +2134,6 @@ fn addPreeditCell2( }); } -fn addPreeditCell( - self: *Metal, - cp: renderer.State.Preedit.Codepoint, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, -) !void { - // Preedit is rendered inverted - const bg = self.foreground_color; - const fg = self.background_color; - - // Render the glyph for our preedit text - const render_ = self.font_grid.renderCodepoint( - self.alloc, - @intCast(cp.codepoint), - .regular, - .text, - .{ .grid_metrics = self.grid_metrics }, - ) catch |err| { - log.warn("error rendering preedit glyph err={}", .{err}); - return; - }; - const render = render_ orelse { - log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); - return; - }; - - // Add our opaque background cell - self.cells_bg.appendAssumeCapacity(.{ - .mode = .rgb, - .grid_pos = .{ x, y }, - .cell_width = if (cp.wide) 2 else 1, - .color = .{ bg.r, bg.g, bg.b, 255 }, - }); - - // Add our text - self.cells_text.appendAssumeCapacity(.{ - .mode = .fg, - .grid_pos = .{ x, y }, - .cell_width = if (cp.wide) 2 else 1, - .color = .{ fg.r, fg.g, fg.b, 255 }, - .bg_color = .{ bg.r, bg.g, bg.b, 255 }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .glyph_offset = .{ render.glyph.offset_x, render.glyph.offset_y }, - }); -} - /// 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. From a54fbe328c18093f0f880e13e7c516dfe2c1d7c0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 09:52:29 -0700 Subject: [PATCH 24/26] renderer/metal: improved comments --- src/renderer/Metal.zig | 2 -- src/renderer/metal/cell.zig | 19 +++++++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index ed901d75c..3a8d0d78f 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -90,8 +90,6 @@ current_background_color: terminal.color.RGB, /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of /// cells goes into a separate shader. -// cells_bg: std.ArrayListUnmanaged(mtl_shaders.CellBg), -// cells_text: std.ArrayListUnmanaged(mtl_shaders.CellText), cells: mtl_cell.Contents, /// The current GPU uniform values. diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig index 66ffd7497..bafb1556e 100644 --- a/src/renderer/metal/cell.zig +++ b/src/renderer/metal/cell.zig @@ -27,6 +27,19 @@ pub const Key = enum { }; /// The contents of all the cells in the terminal. +/// +/// The goal of this data structure is to make it efficient for two operations: +/// +/// 1. Setting the contents of a cell by coordinate. More specifically, +/// we want to be efficient setting cell contents by row since we +/// will be doing row dirty tracking. +/// +/// 2. Syncing the contents of the CPU buffers to GPU buffers. This happens +/// every frame and should be as fast as possible. +/// +/// To achieve this, the contents are stored in contiguous arrays by +/// GPU vertex type and we have an array of mappings indexed by coordinate +/// that map to the index in the GPU vertex array that the content is at. pub const Contents = struct { /// The map contains the mapping of cell content for every cell in the /// terminal to the index in the cells array that the content is at. @@ -49,7 +62,8 @@ pub const Contents = struct { bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg), text: std.ArrayListUnmanaged(mtl_shaders.CellText), - /// True when the cursor should be rendered. + /// True when the cursor should be rendered. This is managed by + /// the setCursor method and should not be set directly. cursor: bool, /// The amount of text elements we reserve at the beginning for @@ -257,7 +271,8 @@ pub const Contents = struct { return coord.y * self.cols + coord.x; } - /// Structures related to the contents of the cell. + /// The mapping of a cell at a specific coordinate to the index in the + /// vertex arrays where the cell content is at, if it is set. const Map = struct { /// The set of cell content mappings for a given cell for every /// possible key. This is used to determine if a cell has a given From 105dbe9e05be178459e31f41140dd0f1d052900d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 09:52:51 -0700 Subject: [PATCH 25/26] renderer/metal: go back to single buffering for now --- src/renderer/Metal.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 3a8d0d78f..4f5baaf6d 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -128,7 +128,8 @@ pub const GPUState = struct { // is comptime because there isn't a good reason to change this at // runtime and there is a lot of complexity to support it. For comptime, // this is useful for debugging. - const BufferCount = 3; + // TODO(mitchellh): enable triple-buffering when we improve our frame times + const BufferCount = 1; /// The frame data, the current frame index, and the semaphore protecting /// the frame data. This is used to implement double/triple/etc. buffering. From b1f324e87ab881f743fadc408b966b67345912b6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 10:02:07 -0700 Subject: [PATCH 26/26] renderer/metal: frame index must fit up to buffer count --- src/renderer/Metal.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 4f5baaf6d..73c515239 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -134,7 +134,7 @@ pub const GPUState = struct { /// The frame data, the current frame index, and the semaphore protecting /// the frame data. This is used to implement double/triple/etc. buffering. frames: [BufferCount]FrameState, - frame_index: std.math.IntFittingRange(0, BufferCount - 1) = 0, + frame_index: std.math.IntFittingRange(0, BufferCount) = 0, frame_sema: std.Thread.Semaphore = .{ .permits = BufferCount }, device: objc.Object, // MTLDevice