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,