From c61de49082cfcd260f9ee7b47cf15c9767bdea10 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 8 Mar 2024 21:33:10 -0800 Subject: [PATCH] renderer/metal: port --- src/renderer/Metal.zig | 217 ++++++++++++++++++---------------------- src/renderer/cell.zig | 44 ++++---- src/terminal/Screen.zig | 1 + src/terminal/page.zig | 8 ++ src/terminal/style.zig | 50 +++++++++ 5 files changed, 181 insertions(+), 139 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 02ba2f93f..f9393fc46 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -623,7 +623,6 @@ pub fn updateFrame( // Data we extract out of the critical area. const Critical = struct { bg: terminal.color.RGB, - selection: ?terminal.Selection, screen: terminal.Screen, mouse: renderer.State.Mouse, preedit: ?renderer.State.Preedit, @@ -657,25 +656,13 @@ pub fn updateFrame( // We used to share terminal state, but we've since learned through // analysis that it is faster to copy the terminal state than to // hold the lock while rebuilding GPU cells. - const viewport_bottom = state.terminal.screen.viewportIsBottom(); - var screen_copy = if (viewport_bottom) try state.terminal.screen.clone( + var screen_copy = try state.terminal.screen.clone( self.alloc, - .{ .active = 0 }, - .{ .active = state.terminal.rows - 1 }, - ) else try state.terminal.screen.clone( - self.alloc, - .{ .viewport = 0 }, - .{ .viewport = state.terminal.rows - 1 }, + .{ .viewport = .{} }, + null, ); errdefer screen_copy.deinit(); - // Convert our selection to viewport points because we copy only - // the viewport above. - const selection: ?terminal.Selection = if (state.terminal.screen.selection) |sel| - sel.toViewport(&state.terminal.screen) - else - null; - // Whether to draw our cursor or not. const cursor_style = renderer.cursorStyle( state, @@ -694,13 +681,15 @@ pub fn updateFrame( // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. // We only do this if the Kitty image state is dirty meaning only if // it changes. - if (state.terminal.screen.kitty_images.dirty) { - try self.prepKittyGraphics(state.terminal); + // TODO(paged-terminal) + if (false) { + if (state.terminal.screen.kitty_images.dirty) { + try self.prepKittyGraphics(state.terminal); + } } break :critical .{ .bg = self.background_color, - .selection = selection, .screen = screen_copy, .mouse = state.mouse, .preedit = preedit, @@ -715,7 +704,6 @@ pub fn updateFrame( // Build our GPU cells try self.rebuildCells( - critical.selection, &critical.screen, critical.mouse, critical.preedit, @@ -1536,16 +1524,21 @@ pub fn setScreenSize( /// down to the GPU yet. fn rebuildCells( self: *Metal, - term_selection: ?terminal.Selection, 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, screen.rows * screen.cols); + 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.clearRetainingCapacity(); @@ -1554,21 +1547,24 @@ fn rebuildCells( // * 3 for glyph + underline + strikethrough for each cell // + 1 for cursor - (screen.rows * screen.cols * 3) + 1, + (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(); + _ = arena_alloc; + _ = mouse; // 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 .{}; + // TODO(paged-terminal) + // 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. @@ -1577,7 +1573,7 @@ fn rebuildCells( x: [2]usize, cp_offset: usize, } = if (preedit) |preedit_v| preedit: { - const range = preedit_v.range(screen.cursor.x, screen.cols - 1); + const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); break :preedit .{ .y = screen.cursor.y, .x = .{ range.start, range.end }, @@ -1591,9 +1587,9 @@ fn rebuildCells( var cursor_cell: ?mtl_shaders.Cell = null; // Build each cell - var rowIter = screen.rowIterator(.viewport); + var row_it = screen.pages.rowIterator(.right_down, .{ .viewport = .{} }, null); var y: usize = 0; - while (rowIter.next()) |row| { + while (row_it.next()) |row| { defer y += 1; // True if this is the row with our cursor. There are a lot of conditions @@ -1628,8 +1624,8 @@ fn rebuildCells( 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.getCell(screen.cursor.x); - const x = screen.cursor.x - @intFromBool(screen_cell.attrs.wide_spacer_tail); + 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| { if (cell.grid_pos[0] == @as(f32, @floatFromInt(x)) and (cell.mode == .fg or cell.mode == .fg_color)) @@ -1643,15 +1639,16 @@ fn rebuildCells( // We need to get this row's selection if there is one for proper // run splitting. const row_selection = sel: { - if (term_selection) |sel| { - const screen_point = (terminal.point.Viewport{ - .x = 0, - .y = y, - }).toScreen(screen); - if (sel.containedRow(screen, screen_point)) |row_sel| { - break :sel row_sel; - } - } + // TODO(paged-terminal) + // if (screen.selection) |sel| { + // const screen_point = (terminal.point.Viewport{ + // .x = 0, + // .y = y, + // }).toScreen(screen); + // if (sel.containedRow(screen, screen_point)) |row_sel| { + // break :sel row_sel; + // } + // } break :sel null; }; @@ -1659,6 +1656,7 @@ fn rebuildCells( // Split our row into runs and shape each one. var iter = self.font_shaper.runIterator( self.font_group, + screen, row, row_selection, if (shape_cursor) screen.cursor.x else null, @@ -1679,24 +1677,23 @@ fn rebuildCells( // It this cell is within our hint range then we need to // underline it. - const cell: terminal.Screen.Cell = cell: { - var cell = row.getCell(shaper_cell.x); + const cell: terminal.Pin = cell: { + var copy = row; + copy.x = shaper_cell.x; + break :cell copy; - // If our links contain this cell then we want to - // underline it. - if (link_match_set.orderedContains(.{ - .x = shaper_cell.x, - .y = y, - })) { - cell.attrs.underline = .single; - } - - break :cell cell; + // TODO(paged-terminal) + // // If our links contain this cell then we want to + // // underline it. + // if (link_match_set.orderedContains(.{ + // .x = shaper_cell.x, + // .y = y, + // })) { + // cell.attrs.underline = .single; + // } }; if (self.updateCell( - term_selection, - screen, cell, color_palette, shaper_cell, @@ -1714,9 +1711,6 @@ fn rebuildCells( } } } - - // Set row is not dirty anymore - row.setDirty(false); } // Add the cursor at the end so that it overlays everything. If we have @@ -1744,7 +1738,8 @@ fn rebuildCells( break :cursor_style; } - _ = self.addCursor(screen, cursor_style); + _ = cursor_style; + //_ = self.addCursor(screen, cursor_style); if (cursor_cell) |*cell| { if (cell.mode == .fg) { cell.color = if (self.config.cursor_text) |txt| @@ -1766,9 +1761,7 @@ fn rebuildCells( fn updateCell( self: *Metal, - selection: ?terminal.Selection, - screen: *terminal.Screen, - cell: terminal.Screen.Cell, + cell_pin: terminal.Pin, palette: *const terminal.color.Palette, shaper_cell: font.shape.Cell, shaper_run: font.shape.TextRun, @@ -1790,45 +1783,35 @@ fn updateCell( // True if this cell is selected // TODO(perf): we can check in advance if selection is in // our viewport at all and not run this on every point. - const selected: bool = if (selection) |sel| selected: { - const screen_point = (terminal.point.Viewport{ - .x = x, - .y = y, - }).toScreen(screen); + const selected = false; + // TODO(paged-terminal) + // const selected: bool = if (screen.selection) |sel| selected: { + // const screen_point = (terminal.point.Viewport{ + // .x = x, + // .y = y, + // }).toScreen(screen); + // + // break :selected sel.contains(screen_point); + // } else false; - break :selected sel.contains(screen_point); - } else false; + const rac = cell_pin.rowAndCell(); + const cell = rac.cell; + const style = cell_pin.style(cell); // The colors for the cell. const colors: BgFg = colors: { // The normal cell result - const cell_res: BgFg = if (!cell.attrs.inverse) .{ + 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 = switch (cell.bg) { - .none => null, - .indexed => |i| palette[i], - .rgb => |rgb| rgb, - }, - .fg = switch (cell.fg) { - .none => self.foreground_color, - .indexed => |i| palette[i], - .rgb => |rgb| rgb, - }, + .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 = switch (cell.fg) { - .none => self.foreground_color, - .indexed => |i| palette[i], - .rgb => |rgb| rgb, - }, - .fg = switch (cell.bg) { - .none => self.background_color, - .indexed => |i| palette[i], - .rgb => |rgb| rgb, - }, + .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 @@ -1846,7 +1829,7 @@ fn updateCell( // 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 (cell.attrs.invisible) { + if (style.flags.invisible) { break :colors BgFg{ .bg = res.bg, .fg = res.bg orelse self.background_color, @@ -1857,7 +1840,7 @@ fn updateCell( }; // Alpha multiplier - const alpha: u8 = if (cell.attrs.faint) 175 else 255; + 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: { @@ -1874,11 +1857,11 @@ fn updateCell( if (selected) break :bg_alpha default; // If we're reversed, do not apply background opacity - if (cell.attrs.inverse) break :bg_alpha default; + 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 (cell.bg != .none and !rgb.eql(self.background_color)) { + if (style.bg(cell, palette) != null and !rgb.eql(self.background_color)) { break :bg_alpha default; } @@ -1892,7 +1875,7 @@ fn updateCell( self.cells_bg.appendAssumeCapacity(.{ .mode = .bg, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, - .cell_width = cell.widthLegacy(), + .cell_width = cell.gridWidth(), .color = .{ rgb.r, rgb.g, rgb.b, bg_alpha }, .bg_color = .{ 0, 0, 0, 0 }, }); @@ -1906,7 +1889,7 @@ fn updateCell( }; // If the cell has a character, draw it - if (cell.char > 0) fg: { + if (cell.hasText()) fg: { // Render const glyph = try self.font_group.renderGlyph( self.alloc, @@ -1920,11 +1903,8 @@ fn updateCell( const mode: mtl_shaders.Cell.Mode = switch (try fgMode( &self.font_group.group, - screen, - cell, + cell_pin, shaper_run, - x, - y, )) { .normal => .fg, .color => .fg_color, @@ -1934,7 +1914,7 @@ fn updateCell( self.cells.appendAssumeCapacity(.{ .mode = mode, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, - .cell_width = cell.widthLegacy(), + .cell_width = cell.gridWidth(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, .bg_color = bg, .glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }, @@ -1946,8 +1926,8 @@ fn updateCell( }); } - if (cell.attrs.underline != .none) { - const sprite: font.Sprite = switch (cell.attrs.underline) { + if (style.flags.underline != .none) { + const sprite: font.Sprite = switch (style.flags.underline) { .none => unreachable, .single => .underline, .double => .underline_double, @@ -1961,17 +1941,17 @@ fn updateCell( font.sprite_index, @intFromEnum(sprite), .{ - .cell_width = if (cell.attrs.wide) 2 else 1, + .cell_width = if (cell.wide == .wide) 2 else 1, .grid_metrics = self.grid_metrics, }, ); - const color = if (cell.attrs.underline_color) cell.underline_fg else colors.fg; + const color = style.underlineColor(palette) orelse colors.fg; self.cells.appendAssumeCapacity(.{ .mode = .fg, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, - .cell_width = cell.widthLegacy(), + .cell_width = cell.gridWidth(), .color = .{ color.r, color.g, color.b, alpha }, .bg_color = bg, .glyph_pos = .{ glyph.atlas_x, glyph.atlas_y }, @@ -1980,11 +1960,11 @@ fn updateCell( }); } - if (cell.attrs.strikethrough) { + if (style.flags.strikethrough) { self.cells.appendAssumeCapacity(.{ .mode = .strikethrough, .grid_pos = .{ @as(f32, @floatFromInt(x)), @as(f32, @floatFromInt(y)) }, - .cell_width = cell.widthLegacy(), + .cell_width = cell.gridWidth(), .color = .{ colors.fg.r, colors.fg.g, colors.fg.b, alpha }, .bg_color = bg, }); @@ -2002,21 +1982,14 @@ fn addCursor( // we're on the wide characer tail. const wide, const x = cell: { // The cursor goes over the screen cursor position. - const cell = screen.getCell( - .active, - screen.cursor.y, - screen.cursor.x, - ); - if (!cell.attrs.wide_spacer_tail or screen.cursor.x == 0) - break :cell .{ cell.attrs.wide, screen.cursor.x }; + 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. - break :cell .{ screen.getCell( - .active, - screen.cursor.y, - screen.cursor.x - 1, - ).attrs.wide, screen.cursor.x - 1 }; + 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; diff --git a/src/renderer/cell.zig b/src/renderer/cell.zig index 1a5ac51d9..44087da44 100644 --- a/src/renderer/cell.zig +++ b/src/renderer/cell.zig @@ -21,11 +21,8 @@ pub const FgMode = enum { /// renderer. pub fn fgMode( group: *font.Group, - screen: *terminal.Screen, - cell: terminal.Screen.Cell, + cell_pin: terminal.Pin, shaper_run: font.shape.TextRun, - x: usize, - y: usize, ) !FgMode { const presentation = try group.presentationFromIndex(shaper_run.font_index); return switch (presentation) { @@ -41,42 +38,55 @@ pub fn fgMode( // the subsequent character is empty, then we allow it to use // the full glyph size. See #1071. .text => text: { - if (!ziglyph.general_category.isPrivateUse(@intCast(cell.char)) and - !ziglyph.blocks.isDingbats(@intCast(cell.char))) + const cell = cell_pin.rowAndCell().cell; + const cp = cell.codepoint(); + + if (!ziglyph.general_category.isPrivateUse(cp) and + !ziglyph.blocks.isDingbats(cp)) { break :text .normal; } // We exempt the Powerline range from this since they exhibit // box-drawing behavior and should not be constrained. - if (isPowerline(cell.char)) { + if (isPowerline(cp)) { break :text .normal; } // If we are at the end of the screen its definitely constrained - if (x == screen.cols - 1) break :text .constrained; + if (cell_pin.x == cell_pin.page.data.size.cols - 1) break :text .constrained; // If we have a previous cell and it was PUA then we need to // also constrain. This is so that multiple PUA glyphs align. // As an exception, we ignore powerline glyphs since they are // used for box drawing and we consider them whitespace. - if (x > 0) prev: { - const prev_cell = screen.getCell(.active, y, x - 1); + if (cell_pin.x > 0) prev: { + const prev_cp = prev_cp: { + var copy = cell_pin; + copy.x -= 1; + const prev_cell = copy.rowAndCell().cell; + break :prev_cp prev_cell.codepoint(); + }; // Powerline is whitespace - if (isPowerline(prev_cell.char)) break :prev; + if (isPowerline(prev_cp)) break :prev; - if (ziglyph.general_category.isPrivateUse(@intCast(prev_cell.char))) { + if (ziglyph.general_category.isPrivateUse(prev_cp)) { break :text .constrained; } } // If the next cell is empty, then we allow it to use the // full glyph size. - const next_cell = screen.getCell(.active, y, x + 1); - if (next_cell.char == 0 or - next_cell.char == ' ' or - isPowerline(next_cell.char)) + const next_cp = next_cp: { + var copy = cell_pin; + copy.x += 1; + const next_cell = copy.rowAndCell().cell; + break :next_cp next_cell.codepoint(); + }; + if (next_cp == 0 or + next_cp == ' ' or + isPowerline(next_cp)) { break :text .normal; } @@ -88,7 +98,7 @@ pub fn fgMode( } // Returns true if the codepoint is a part of the Powerline range. -fn isPowerline(char: u32) bool { +fn isPowerline(char: u21) bool { return switch (char) { 0xE0B0...0xE0C8, 0xE0CA, 0xE0CC...0xE0D2, 0xE0D4 => true, else => false, diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index ee78ad812..5438afb2c 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -227,6 +227,7 @@ pub fn clonePool( .pages = pages, .no_scrollback = self.no_scrollback, + // TODO: selection // TODO: let's make this reasonble .cursor = undefined, }; diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 69d286709..0e53696a7 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -779,6 +779,14 @@ pub const Cell = packed struct(u64) { }; } + /// The width in grid cells that this cell takes up. + pub fn gridWidth(self: Cell) u2 { + return switch (self.wide) { + .narrow, .spacer_head, .spacer_tail => 1, + .wide => 2, + }; + } + pub fn hasStyling(self: Cell) bool { return self.style_id != style.default_id; } diff --git a/src/terminal/style.zig b/src/terminal/style.zig index e48630711..0d7380185 100644 --- a/src/terminal/style.zig +++ b/src/terminal/style.zig @@ -51,6 +51,56 @@ pub const Style = struct { return std.mem.eql(u8, std.mem.asBytes(&self), def); } + /// Returns the bg color for a cell with this style given the cell + /// that has this style and the palette to use. + /// + /// Note that generally if a cell is a color-only cell, it SHOULD + /// only have the default style, but this is meant to work with the + /// default style as well. + pub fn bg( + self: Style, + cell: *const page.Cell, + palette: *const color.Palette, + ) ?color.RGB { + return switch (cell.content_tag) { + .bg_color_palette => palette[cell.content.color_palette], + .bg_color_rgb => rgb: { + const rgb = cell.content.color_rgb; + break :rgb .{ .r = rgb.r, .g = rgb.g, .b = rgb.b }; + }, + + else => switch (self.bg_color) { + .none => null, + .palette => |idx| palette[idx], + .rgb => |rgb| rgb, + }, + }; + } + + /// Returns the fg color for a cell with this style given the palette. + pub fn fg( + self: Style, + palette: *const color.Palette, + ) ?color.RGB { + return switch (self.fg_color) { + .none => null, + .palette => |idx| palette[idx], + .rgb => |rgb| rgb, + }; + } + + /// Returns the underline color for this style. + pub fn underlineColor( + self: Style, + palette: *const color.Palette, + ) ?color.RGB { + return switch (self.underline_color) { + .none => null, + .palette => |idx| palette[idx], + .rgb => |rgb| rgb, + }; + } + /// Returns a bg-color only cell from this style, if it exists. pub fn bgCell(self: Style) ?page.Cell { return switch (self.bg_color) {