diff --git a/shaders/cell.v.glsl b/shaders/cell.v.glsl index 3417152a2..549f39eba 100644 --- a/shaders/cell.v.glsl +++ b/shaders/cell.v.glsl @@ -37,6 +37,9 @@ layout (location = 5) in vec4 bg_color_in; // the entire terminal grid in a single GPU pass. layout (location = 6) in uint mode_in; +// The width in cells of this item. +layout (location = 7) in uint grid_width; + // The background or foreground color for the fragment, depending on // whether this is a background or foreground pass. flat out vec4 color; @@ -133,7 +136,7 @@ void main() { // The "+ 3" here is to give some wiggle room for fonts that are // BARELY over it. vec2 glyph_size_downsampled = glyph_size; - if (glyph_size.x > (cell_size.x + 3)) { + if (glyph_size.y > cell_size.y + 2) { glyph_size_downsampled.x = cell_size_scaled.x; glyph_size_downsampled.y = glyph_size.y * (glyph_size_downsampled.x / glyph_size.x); glyph_offset_calc.y = glyph_offset.y * (glyph_size_downsampled.x / glyph_size.x); diff --git a/src/Grid.zig b/src/Grid.zig index 36fc1345e..22b9f8222 100644 --- a/src/Grid.zig +++ b/src/Grid.zig @@ -109,6 +109,9 @@ const GPUCell = struct { /// uint mode mode: GPUCellMode, + + /// The width in grid cells that a rendering takes. + grid_width: u16 = 1, }; const GPUCellMode = enum(u8) { @@ -224,6 +227,8 @@ pub fn init( try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(GPUCell), offset); offset += 4 * @sizeOf(u8); try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(GPUCell), offset); + offset += 1 * @sizeOf(u8); + try vbobind.attributeAdvanced(7, 1, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(GPUCell), offset); try vbobind.enableAttribArray(0); try vbobind.enableAttribArray(1); try vbobind.enableAttribArray(2); @@ -231,6 +236,7 @@ pub fn init( try vbobind.enableAttribArray(4); try vbobind.enableAttribArray(5); try vbobind.enableAttribArray(6); + try vbobind.enableAttribArray(7); try vbobind.attributeDivisor(0, 1); try vbobind.attributeDivisor(1, 1); try vbobind.attributeDivisor(2, 1); @@ -238,6 +244,7 @@ pub fn init( try vbobind.attributeDivisor(4, 1); try vbobind.attributeDivisor(5, 1); try vbobind.attributeDivisor(6, 1); + try vbobind.attributeDivisor(7, 1); // Build our texture const tex = try gl.Texture.create(); @@ -341,17 +348,31 @@ pub fn rebuildCells(self: *Grid, term: *Terminal) !void { // We've written no data to the GPU, refresh it all self.gl_cells_written = 0; + // Create a text shaper we'll use for the screen + var shape_buf = try self.alloc.alloc(font.Shaper.Cell, term.screen.cols * 2); + defer self.alloc.free(shape_buf); + var shaper = try font.Shaper.init(&self.font_group, shape_buf); + defer shaper.deinit(); + // Build each cell var rowIter = term.screen.rowIterator(.viewport); var y: usize = 0; while (rowIter.next()) |row| { defer y += 1; - var cellIter = row.cellIterator(); - var x: usize = 0; - while (cellIter.next()) |cell| { - defer x += 1; - assert(try self.updateCell(term, cell, x, y)); + // Split our row into runs and shape each one. + var iter = shaper.runIterator(row); + while (try iter.next(self.alloc)) |run| { + for (try shaper.shape(run)) |shaper_cell| { + assert(try self.updateCell( + term, + row[shaper_cell.x], + shaper_cell, + run, + shaper_cell.x, + y, + )); + } } } @@ -398,6 +419,7 @@ fn addCursor(self: *Grid, term: *Terminal) void { .mode = mode, .grid_col = @intCast(u16, term.screen.cursor.x), .grid_row = @intCast(u16, term.screen.cursor.y), + .grid_width = if (cell.attrs.wide) 2 else 1, .fg_r = 0, .fg_g = 0, .fg_b = 0, @@ -417,6 +439,8 @@ pub fn updateCell( self: *Grid, term: *Terminal, cell: terminal.Screen.Cell, + shaper_cell: font.Shaper.Cell, + shaper_run: font.Shaper.TextRun, x: usize, y: usize, ) !bool { @@ -471,9 +495,6 @@ pub fn updateCell( break :colors res; }; - // If we are a trailing spacer, we never render anything. - if (cell.attrs.wide_spacer_tail) return true; - // Calculate the amount of space we need in the cells list. const needed = needed: { var i: usize = 0; @@ -496,6 +517,7 @@ pub fn updateCell( .mode = mode, .grid_col = @intCast(u16, x), .grid_row = @intCast(u16, y), + .grid_width = shaper_cell.width, .glyph_x = 0, .glyph_y = 0, .glyph_width = 0, @@ -515,37 +537,16 @@ pub fn updateCell( // If the cell is empty then we draw nothing in the box. if (!cell.empty()) { - // Determine our glyph styling - const style: font.Style = if (cell.attrs.bold) - .bold - else - .regular; - - var mode: GPUCellMode = .fg; - - // Get the glyph that we're going to use. We first try what the cell - // wants, then the Unicode replacement char, then finally a space. - const FontInfo = struct { index: font.Group.FontIndex, ch: u32 }; - const font_info: FontInfo = font_info: { - var chars = [_]u32{ @intCast(u32, cell.char), 0xFFFD, ' ' }; - for (chars) |char| { - if (try self.font_group.indexForCodepoint(self.alloc, style, char)) |idx| { - break :font_info FontInfo{ - .index = idx, - .ch = char, - }; - } - } - - @panic("all fonts require at least space"); - }; - // Render - const face = self.font_group.group.faceFromIndex(font_info.index); - const glyph_index = face.glyphIndex(font_info.ch).?; - const glyph = try self.font_group.renderGlyph(self.alloc, font_info.index, glyph_index); + const face = self.font_group.group.faceFromIndex(shaper_run.font_index); + const glyph = try self.font_group.renderGlyph( + self.alloc, + shaper_run.font_index, + shaper_cell.glyph_index, + ); // If we're rendering a color font, we use the color atlas + var mode: GPUCellMode = .fg; if (face.hasColor()) mode = .fg_color; // If the cell is wide, we need to note that in the mode @@ -555,6 +556,7 @@ pub fn updateCell( .mode = mode, .grid_col = @intCast(u16, x), .grid_row = @intCast(u16, y), + .grid_width = shaper_cell.width, .glyph_x = glyph.atlas_x, .glyph_y = glyph.atlas_y, .glyph_width = glyph.width, @@ -580,6 +582,7 @@ pub fn updateCell( .mode = mode, .grid_col = @intCast(u16, x), .grid_row = @intCast(u16, y), + .grid_width = shaper_cell.width, .glyph_x = 0, .glyph_y = 0, .glyph_width = 0, diff --git a/src/font/Shaper.zig b/src/font/Shaper.zig index 91e511f0b..4e6788d01 100644 --- a/src/font/Shaper.zig +++ b/src/font/Shaper.zig @@ -22,10 +22,16 @@ group: *GroupCache, /// calls to prevent allocations. hb_buf: harfbuzz.Buffer, -pub fn init(group: *GroupCache) !Shaper { +/// The shared memory used for shaping results. +cell_buf: []Cell, + +/// The cell_buf argument is the buffer to use for storing shaped results. +/// This should be at least the number of columns in the terminal. +pub fn init(group: *GroupCache, cell_buf: []Cell) !Shaper { return Shaper{ .group = group, .hb_buf = try harfbuzz.Buffer.create(), + .cell_buf = cell_buf, }; } @@ -44,27 +50,85 @@ pub fn runIterator(self: *Shaper, row: terminal.Screen.Row) RunIterator { /// text run that was iterated since the text run does share state with the /// Shaper struct. /// -/// NOTE: there is no return value here yet because its still WIP -pub fn shape(self: Shaper, run: TextRun) void { +/// The return value is only valid until the next shape call is called. +/// +/// If there is not enough space in the cell buffer, an error is returned. +pub fn shape(self: *Shaper, run: TextRun) ![]Cell { const face = self.group.group.faceFromIndex(run.font_index); harfbuzz.shape(face.hb_font, self.hb_buf, null); + // If our buffer is empty, we short-circuit the rest of the work + // return nothing. + if (self.hb_buf.getLength() == 0) return self.cell_buf[0..0]; const info = self.hb_buf.getGlyphInfos(); - const pos = self.hb_buf.getGlyphPositions() orelse return; + const pos = self.hb_buf.getGlyphPositions() orelse return error.HarfbuzzFailed; // This is perhaps not true somewhere, but we currently assume it is true. // If it isn't true, I'd like to catch it and learn more. assert(info.len == pos.len); - // log.warn("info={} pos={}", .{ info.len, pos.len }); - // for (info) |v, i| { - // log.warn("info {} = {}", .{ i, v }); - // } + // Convert all our info/pos to cells and set it. + if (info.len > self.cell_buf.len) return error.OutOfMemory; + // log.debug("info={} pos={}", .{ info.len, pos.len }); + + // x is the column that we currently occupy. We start at the offset. + var x: u16 = run.offset; + + for (info) |v, i| { + // The number of codepoints is used as the cell "width". If + // we're the last cell, this is remaining otherwise we use cluster numbers + // to detect since we set the cluster number to the column it + // originated. + const cp_width = if (i == info.len - 1) + run.max_cluster - v.cluster + else width: { + const next_cluster = info[i + 1].cluster; + break :width next_cluster - v.cluster; + }; + + self.cell_buf[i] = .{ + .x = x, + .glyph_index = v.codepoint, + .width = if (cp_width > 2) 2 else @intCast(u8, cp_width), + }; + + // Increase x by the amount of codepoints we replaced so that + // we retain the grid. + x += @intCast(u16, cp_width); + + // log.debug("i={} info={} pos={} cell={}", .{ i, v, pos[i], self.cell_buf[i] }); + } + + return self.cell_buf[0..info.len]; } +pub const Cell = struct { + /// The column that this cell occupies. Since a set of shaper cells is + /// always on the same line, only the X is stored. It is expected the + /// caller has access to the original screen cell. + x: u16, + + /// The glyph index for this cell. The font index to use alongside + /// this cell is available in the text run. + glyph_index: u32, + + /// The width that this cell consumes. + width: u8, +}; + /// A single text run. A text run is only valid for one Shaper and /// until the next run is created. pub const TextRun = struct { + /// The offset in the row where this run started + offset: u16, + + /// The total number of cells produced by this run. + cells: u16, + + /// The maximum cluster value used + max_cluster: u16, + + /// The font index to use for the glyphs of this run. font_index: Group.FontIndex, }; @@ -85,6 +149,7 @@ pub const RunIterator = struct { // Go through cell by cell and accumulate while we build our run. var j: usize = self.i; + var max_cluster: usize = j; while (j < self.row.lenCells()) : (j += 1) { const cell = self.row.getCell(j); @@ -96,8 +161,19 @@ pub const RunIterator = struct { else .regular; - // Determine the font for this cell - const font_idx_opt = try self.shaper.group.indexForCodepoint(alloc, style, cell.char); + // Determine the font for this cell. We'll use fallbacks + // manually here to try replacement chars and then a space + // for unknown glyphs. + const font_idx_opt = (try self.shaper.group.indexForCodepoint( + alloc, + style, + cell.char, + )) orelse (try self.shaper.group.indexForCodepoint( + alloc, + style, + 0xFFFD, + )) orelse + try self.shaper.group.indexForCodepoint(alloc, style, ' '); const font_idx = font_idx_opt.?; //log.warn("char={x} idx={}", .{ cell.char, font_idx }); if (j == self.i) current_font = font_idx; @@ -116,15 +192,22 @@ pub const RunIterator = struct { self.shaper.hb_buf.add(cp, @intCast(u32, j)); } } + + max_cluster = j; } // Finalize our buffer self.shaper.hb_buf.guessSegmentProperties(); - // Move our cursor - self.i = j; + // Move our cursor. Must defer since we use self.i below. + defer self.i = j; - return TextRun{ .font_index = current_font }; + return TextRun{ + .offset = @intCast(u16, self.i), + .cells = @intCast(u16, j - self.i), + .max_cluster = @intCast(u16, max_cluster), + .font_index = current_font, + }; } }; @@ -194,7 +277,7 @@ test "shape" { while (try it.next(alloc)) |run| { count += 1; try testing.expectEqual(@as(u32, 3), shaper.hb_buf.getLength()); - shaper.shape(run); + _ = try shaper.shape(run); } try testing.expectEqual(@as(usize, 1), count); } @@ -204,11 +287,13 @@ const TestShaper = struct { shaper: Shaper, cache: *GroupCache, lib: Library, + cell_buf: []Cell, pub fn deinit(self: *TestShaper) void { self.shaper.deinit(); self.cache.deinit(self.alloc); self.alloc.destroy(self.cache); + self.alloc.free(self.cell_buf); self.lib.deinit(); } }; @@ -230,7 +315,10 @@ fn testShaper(alloc: Allocator) !TestShaper { try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 })); try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testEmoji, .{ .points = 12 })); - var shaper = try init(cache_ptr); + var cell_buf = try alloc.alloc(Cell, 80); + errdefer alloc.free(cell_buf); + + var shaper = try init(cache_ptr, cell_buf); errdefer shaper.deinit(); return TestShaper{ @@ -238,5 +326,6 @@ fn testShaper(alloc: Allocator) !TestShaper { .shaper = shaper, .cache = cache_ptr, .lib = lib, + .cell_buf = cell_buf, }; }