From 7b750b7ed92a24afb331657dad981e9f4a15ef5a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Apr 2024 12:18:47 -0700 Subject: [PATCH 01/24] terminal: add dirty bits to the page structure --- src/terminal/PageList.zig | 1 + src/terminal/page.zig | 87 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 95df3e911..8d3f893c1 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1917,6 +1917,7 @@ fn createPage( self: *PageList, cap: Capacity, ) !*List.Node { + log.debug("create page cap={}", .{cap}); return try createPageExt(&self.pool, cap, &self.page_size); } diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 02bd45776..38618e1d3 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -91,6 +91,44 @@ pub const Page = struct { /// The available set of styles in use on this page. styles: style.Set, + /// The offset to the first mask of dirty bits in the page. + /// + /// The dirty bits is a contiguous array of usize where each bit represents + /// a row in the page, in order. If the bit is set, then the row is dirty + /// and requires a redraw. Dirty status is only ever meant to convey that + /// a cell has changed visually. A cell which changes in a way that doesn't + /// affect the visual representation may not be marked as dirty. + /// + /// Dirty tracking may have false positives but should never have false + /// negatives. A false negative would result in a visual artifact on the + /// screen. + /// + /// Dirty bits are only ever unset by consumers of a page. The page + /// structure itself does not unset dirty bits since the page does not + /// know when a cell has been redrawn. + /// + /// As implementation background: it may seem that dirty bits should be + /// stored elsewhere and not on the page itself, because the only data + /// that could possibly change is in the active area of a terminal + /// historically and that area is small compared to the typical scrollback. + /// My original thinking was to put the dirty bits on Screen instead and + /// have them only track the active area. However, I decided to put them + /// into the page directly for a few reasons: + /// + /// 1. It's simpler. The page is a self-contained unit and it's nice + /// to have all the data for a page in one place. + /// + /// 2. It's cheap. Even a very large page might have 1000 rows and + /// that's only ~128 bytes of 64-bit integers to track all the dirty + /// bits. Compared to the hundreds of kilobytes a typical page + /// consumes, this is nothing. + /// + /// 3. It's more flexible. If we ever want to implement new terminal + /// features that allow non-active area to be dirty, we can do that + /// with minimal dirty-tracking work. + /// + dirty: Offset(usize), + /// The current dimensions of the page. The capacity may be larger /// than this. This allows us to allocate a larger page than necessary /// and also to resize a page smaller witout reallocating. @@ -155,6 +193,7 @@ pub const Page = struct { .memory = @alignCast(buf.start()[0..l.total_size]), .rows = rows, .cells = cells, + .dirty = buf.member(usize, l.dirty_start), .styles = style.Set.init( buf.add(l.styles_start), l.styles_layout, @@ -866,12 +905,26 @@ pub const Page = struct { return self.grapheme_map.map(self.memory).count(); } + /// Returns the bitset for the dirty bits on this page. + /// + /// The returned value is a DynamicBitSetUnmanaged but it is NOT + /// actually dynamic; do NOT call resize on this. It is safe to + /// read and write but do not resize it. + pub fn dirtyBitSet(self: *const Page) std.DynamicBitSetUnmanaged { + return .{ + .bit_length = self.capacity.rows, + .masks = self.dirty.ptr(self.memory), + }; + } + pub const Layout = struct { total_size: usize, rows_start: usize, rows_size: usize, cells_start: usize, cells_size: usize, + dirty_start: usize, + dirty_size: usize, styles_start: usize, styles_layout: style.Set.Layout, grapheme_alloc_start: usize, @@ -892,8 +945,19 @@ pub const Page = struct { const cells_start = alignForward(usize, rows_end, @alignOf(Cell)); const cells_end = cells_start + (cells_count * @sizeOf(Cell)); + // The division below cannot fail because our row count cannot + // exceed the maximum value of usize. + const dirty_bit_length: usize = rows_count; + const dirty_usize_length: usize = std.math.divCeil( + usize, + dirty_bit_length, + @bitSizeOf(usize), + ) catch unreachable; + const dirty_start = alignForward(usize, cells_end, @alignOf(usize)); + const dirty_end: usize = dirty_start + (dirty_usize_length * @sizeOf(usize)); + const styles_layout = style.Set.layout(cap.styles); - const styles_start = alignForward(usize, cells_end, style.Set.base_align); + const styles_start = alignForward(usize, dirty_end, style.Set.base_align); const styles_end = styles_start + styles_layout.total_size; const grapheme_alloc_layout = GraphemeAlloc.layout(cap.grapheme_bytes); @@ -913,6 +977,8 @@ pub const Page = struct { .rows_size = rows_end - rows_start, .cells_start = cells_start, .cells_size = cells_end - cells_start, + .dirty_start = dirty_start, + .dirty_size = dirty_end - dirty_start, .styles_start = styles_start, .styles_layout = styles_layout, .grapheme_alloc_start = grapheme_alloc_start, @@ -981,9 +1047,18 @@ pub const Capacity = struct { const grapheme_alloc_start = alignBackward(usize, grapheme_map_start - layout.grapheme_alloc_layout.total_size, GraphemeAlloc.base_align); const styles_start = alignBackward(usize, grapheme_alloc_start - layout.styles_layout.total_size, style.Set.base_align); - const available_size = styles_start; - const size_per_row = @sizeOf(Row) + (@sizeOf(Cell) * @as(usize, @intCast(cols))); - const new_rows = @divFloor(available_size, size_per_row); + // The size per row is: + // - The row metadata itself + // - The cells per row (n=cols) + // - 1 bit for dirty tracking + const bits_per_row: usize = size: { + var bits: usize = @bitSizeOf(Row); // Row metadata + bits += @bitSizeOf(Cell) * @as(usize, @intCast(cols)); // Cells (n=cols) + bits += 1; // The dirty bit + break :size bits; + }; + const available_bits: usize = styles_start * 8; + const new_rows: usize = @divFloor(available_bits, bits_per_row); // If our rows go to zero then we can't fit any row metadata // for the desired number of columns. @@ -1315,6 +1390,10 @@ test "Page init" { .styles = 32, }); defer page.deinit(); + + // Dirty set should be empty + const dirty = page.dirtyBitSet(); + try std.testing.expectEqual(@as(usize, 0), dirty.count()); } test "Page read and write cells" { From 11c195e493ecc1704b77c81eef79f536eeb8f777 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 10:27:14 -0700 Subject: [PATCH 02/24] terminal: dirty tracking on `print` with tests --- src/terminal/PageList.zig | 26 +++++ src/terminal/Screen.zig | 6 ++ src/terminal/Terminal.zig | 215 +++++++++++++++++++++++++++++++++++++- src/terminal/page.zig | 14 +++ 4 files changed, 258 insertions(+), 3 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 8d3f893c1..f6f3853bb 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2928,6 +2928,17 @@ fn growRows(self: *PageList, n: usize) !void { } } +/// Clear all dirty bits on all pages. This is not efficient since it +/// traverses the entire list of pages. This is used for testing/debugging. +pub fn clearDirty(self: *PageList) void { + var page = self.pages.first; + while (page) |p| { + var set = p.data.dirtyBitSet(); + set.unsetAll(); + page = p.next; + } +} + /// Represents an exact x/y coordinate within the screen. This is called /// a "pin" because it is a fixed point within the pagelist direct to /// a specific page pointer and memory offset. The benefit is that this @@ -2986,6 +2997,13 @@ pub const Pin = struct { ).?.*; } + /// Mark this pin location as dirty. + /// TODO: test + pub fn markDirty(self: *Pin) void { + var set = self.page.data.dirtyBitSet(); + set.set(self.y); + } + /// Iterators. These are the same as PageList iterator funcs but operate /// on pins rather than points. This is MUCH more efficient than calling /// pointFromPin and building up the iterator from points. @@ -3219,6 +3237,14 @@ const Cell = struct { row_idx: size.CellCountInt, col_idx: size.CellCountInt, + /// Returns true if this cell is marked as dirty. + /// + /// This is not very performant this is primarily used for assertions + /// and testing. + pub fn isDirty(self: Cell) bool { + return self.page.data.isRowDirty(self.row_idx); + } + /// Get the cell style. /// /// Not meant for non-test usage since this is inefficient. diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 58e4e681b..a0a48d40c 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -719,6 +719,12 @@ fn cursorChangePin(self: *Screen, new: Pin) void { }; } +/// Mark the cursor position as dirty. +/// TODO: test +pub fn cursorMarkDirty(self: *Screen) void { + self.cursor.page_pin.markDirty(); +} + /// Options for scrolling the viewport of the terminal grid. The reason /// we have this in addition to PageList.Scroll is because we have additional /// scroll behaviors that are not part of the PageList.Scroll enum. diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 9ddec218e..cdeab624f 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -15,6 +15,7 @@ const modes = @import("modes.zig"); const charsets = @import("charsets.zig"); const csi = @import("csi.zig"); const kitty = @import("kitty.zig"); +const point = @import("point.zig"); const sgr = @import("sgr.zig"); const Tabstops = @import("Tabstops.zig"); const color = @import("color.zig"); @@ -354,6 +355,7 @@ pub fn print(self: *Terminal, c: u21) !void { } log.debug("c={x} grapheme attach to left={}", .{ c, prev.left }); + self.screen.cursorMarkDirty(); try self.screen.appendGrapheme(prev.cell, c); return; } @@ -429,7 +431,10 @@ pub fn print(self: *Terminal, c: u21) !void { switch (width) { // Single cell is very easy: just write in the cell - 1 => @call(.always_inline, printCell, .{ self, c, .narrow }), + 1 => { + self.screen.cursorMarkDirty(); + @call(.always_inline, printCell, .{ self, c, .narrow }); + }, // Wide character requires a spacer. We print this by // using two cells: the first is flagged "wide" and has the @@ -452,12 +457,14 @@ pub fn print(self: *Terminal, c: u21) !void { try self.printWrap(); } + self.screen.cursorMarkDirty(); self.printCell(c, .wide); self.screen.cursorRight(1); self.printCell(' ', .spacer_tail); } else { // This is pretty broken, terminals should never be only 1-wide. // We sould prevent this downstream. + self.screen.cursorMarkDirty(); self.printCell(' ', .narrow); }, @@ -2494,6 +2501,16 @@ pub fn fullReset(self: *Terminal) void { self.status_display = .main; } +/// Returns true if the point is dirty, used for testing. +fn isDirty(t: *const Terminal, pt: point.Point) bool { + return t.screen.pages.getCell(pt).?.isDirty(); +} + +/// Clear all dirty bits. Testing only. +fn clearDirty(t: *Terminal) void { + t.screen.pages.clearDirty(); +} + test "Terminal: input with no control characters" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 40, .rows = 40 }); @@ -2508,6 +2525,10 @@ test "Terminal: input with no control characters" { defer alloc.free(str); try testing.expectEqualStrings("hello", str); } + + // The first row should be dirty + try testing.expect(t.isDirty(.{ .screen = .{ .x = 5, .y = 0 } })); + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 5, .y = 1 } })); } test "Terminal: input with basic wraparound" { @@ -2527,6 +2548,20 @@ test "Terminal: input with basic wraparound" { } } +test "Terminal: input with basic wraparound dirty" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .cols = 5, .rows = 40 }); + defer t.deinit(alloc); + + for ("hello") |c| try t.print(c); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 4, .y = 0 } })); + t.clearDirty(); + try t.print('w'); + + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 4, .y = 0 } })); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } })); +} + test "Terminal: input that forces scroll" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 1, .rows = 5 }); @@ -2582,6 +2617,9 @@ test "Terminal: zero-width character at start" { try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); + + // Should not be dirty since we changed nothing. + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } // https://github.com/mitchellh/ghostty/issues/1400 @@ -2613,6 +2651,8 @@ test "Terminal: print wide char" { const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: print wide char at edge creates spacer head" { @@ -2640,6 +2680,12 @@ test "Terminal: print wide char at edge creates spacer head" { const cell = list_cell.cell; try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); } + + // Our first row just had a spacer head added which does not affect + // rendering so only the place where the wide char was printed + // should be marked. + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } })); } test "Terminal: print wide char with 1-column width" { @@ -2648,6 +2694,9 @@ test "Terminal: print wide char with 1-column width" { defer t.deinit(alloc); try t.print('😀'); // 0x1F600 + + // This prints a space so we should be dirty. + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: print wide char in single-width terminal" { @@ -2665,6 +2714,8 @@ test "Terminal: print wide char in single-width terminal" { try testing.expectEqual(@as(u21, ' '), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: print over wide char at 0,0" { @@ -2673,7 +2724,7 @@ test "Terminal: print over wide char at 0,0" { try t.print(0x1F600); // Smiley face t.setCursorPos(0, 0); - try t.print('A'); // Smiley face + try t.print('A'); try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); @@ -2690,6 +2741,9 @@ test "Terminal: print over wide char at 0,0" { try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } })); } test "Terminal: print over wide spacer tail" { @@ -2718,6 +2772,8 @@ test "Terminal: print over wide spacer tail" { defer testing.allocator.free(str); try testing.expectEqualStrings(" X", str); } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: print over wide char with bold" { @@ -2742,6 +2798,8 @@ test "Terminal: print over wide char with bold" { const page = t.screen.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: print over wide char with bg color" { @@ -2770,6 +2828,8 @@ test "Terminal: print over wide char with bg color" { const page = t.screen.cursor.page_pin.page.data; try testing.expectEqual(@as(usize, 0), page.styles.count(page.memory)); } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: print multicodepoint grapheme, disabled mode 2027" { @@ -2840,6 +2900,8 @@ test "Terminal: print multicodepoint grapheme, disabled mode 2027" { try testing.expectEqual(Cell.Wide.spacer_tail, cell.wide); try testing.expect(list_cell.page.data.lookupGrapheme(cell) == null); } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: VS16 doesn't make character with 2027 disabled" { @@ -2869,6 +2931,21 @@ test "Terminal: VS16 doesn't make character with 2027 disabled" { } } +test "Terminal: ignored VS16 doesn't mark dirty" { + var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); + defer t.deinit(testing.allocator); + + // Disable grapheme clustering + t.modes.set(.grapheme_cluster, false); + + try t.print(0x2764); // Heart + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + + t.clearDirty(); + try t.print(0xFE0F); // VS16 to make wide + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); +} + test "Terminal: print invalid VS16 non-grapheme" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator); @@ -2897,6 +2974,21 @@ test "Terminal: print invalid VS16 non-grapheme" { } } +test "Terminal: invalid VS16 doesn't mark dirty" { + var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); + defer t.deinit(testing.allocator); + + // Disable grapheme clustering + t.modes.set(.grapheme_cluster, false); + + try t.print('x'); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + + t.clearDirty(); + try t.print(0xFE0F); // VS16 to make wide + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); +} + test "Terminal: print multicodepoint grapheme, mode 2027" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator); @@ -2916,6 +3008,9 @@ test "Terminal: print multicodepoint grapheme, mode 2027" { try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); + // Row should be dirty + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + // Assert various properties about our screen to verify // we have all expected cells. { @@ -2936,6 +3031,35 @@ test "Terminal: print multicodepoint grapheme, mode 2027" { } } +test "Terminal: multicodepoint grapheme marks dirty on every codepoint" { + var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); + defer t.deinit(testing.allocator); + + // Enable grapheme clustering + t.modes.set(.grapheme_cluster, true); + + // https://github.com/mitchellh/ghostty/issues/289 + // This is: 👨‍👩‍👧 (which may or may not render correctly) + try t.print(0x1F468); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); + try t.print(0x200D); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); + try t.print(0x1F469); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); + try t.print(0x200D); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); + try t.print(0x1F467); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + + // We should have 2 cells taken up. It is one character but "wide". + try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); + try testing.expectEqual(@as(usize, 2), t.screen.cursor.x); +} + test "Terminal: VS15 to make narrow character" { var t = try init(testing.allocator, .{ .rows = 5, .cols = 5 }); defer t.deinit(testing.allocator); @@ -2944,7 +3068,11 @@ test "Terminal: VS15 to make narrow character" { t.modes.set(.grapheme_cluster, true); try t.print(0x26C8); // Thunder cloud and rain + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); try t.print(0xFE0E); // VS15 to make narrow + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); { const str = try t.plainString(testing.allocator); @@ -2971,7 +3099,11 @@ test "Terminal: VS16 to make wide character with mode 2027" { t.modes.set(.grapheme_cluster, true); try t.print(0x2764); // Heart + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); try t.print(0xFE0F); // VS16 to make wide + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); { const str = try t.plainString(testing.allocator); @@ -3002,6 +3134,8 @@ test "Terminal: VS16 repeated with mode 2027" { try t.print(0x2764); // Heart try t.print(0xFE0F); // VS16 to make wide + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -3104,8 +3238,12 @@ test "Terminal: overwrite grapheme should clear grapheme data" { try t.print(0x26C8); // Thunder cloud and rain try t.print(0xFE0E); // VS15 to make narrow + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + t.clearDirty(); + t.setCursorPos(1, 1); try t.print('A'); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -3147,7 +3285,9 @@ test "Terminal: overwrite multicodepoint grapheme clears grapheme data" { // Move back and overwrite wide t.setCursorPos(1, 1); + t.clearDirty(); try t.print('X'); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 1), t.screen.cursor.x); @@ -3233,6 +3373,11 @@ test "Terminal: print writes to bottom if scrolled" { defer testing.allocator.free(str); try testing.expectEqualStrings("\nA", str); } + + try testing.expect(t.isDirty(.{ .active = .{ + .x = t.screen.cursor.x, + .y = t.screen.cursor.y, + } })); } test "Terminal: print charset" { @@ -3244,6 +3389,9 @@ test "Terminal: print charset" { t.configureCharset(.G2, .dec_special); t.configureCharset(.G3, .dec_special); + // No dirty to configure charset + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + // Basic grid writing try t.print('`'); t.configureCharset(.G0, .utf8); @@ -3257,6 +3405,8 @@ test "Terminal: print charset" { defer testing.allocator.free(str); try testing.expectEqualStrings("```◆", str); } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: print charset outside of ASCII" { @@ -3268,6 +3418,9 @@ test "Terminal: print charset outside of ASCII" { t.configureCharset(.G2, .dec_special); t.configureCharset(.G3, .dec_special); + // No dirty to configure charset + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + // Basic grid writing t.configureCharset(.G0, .dec_special); try t.print('`'); @@ -3277,6 +3430,8 @@ test "Terminal: print charset outside of ASCII" { defer testing.allocator.free(str); try testing.expectEqualStrings("◆ ", str); } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: print invoke charset" { @@ -3285,10 +3440,14 @@ test "Terminal: print invoke charset" { t.configureCharset(.G1, .dec_special); - // Basic grid writing try t.print('`'); + + // Invokecharset but should not mark dirty on its own + t.clearDirty(); t.invokeCharset(.GL, .G1, false); + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); try t.print('`'); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); try t.print('`'); t.invokeCharset(.GL, .G0, false); try t.print('`'); @@ -3336,7 +3495,10 @@ test "Terminal: soft wrap with semantic prompt" { var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 }); defer t.deinit(testing.allocator); + // Mark our prompt. Should not make anything dirty on its own. t.markSemanticPrompt(.prompt); + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + for ("hello") |c| try t.print(c); { @@ -3358,6 +3520,7 @@ test "Terminal: disabled wraparound with wide char and one space" { // This puts our cursor at the end and there is NO SPACE for a // wide character. try t.printString("AAAA"); + t.clearDirty(); try t.print(0x1F6A8); // Police car light try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 4), t.screen.cursor.x); @@ -3375,6 +3538,9 @@ test "Terminal: disabled wraparound with wide char and one space" { try testing.expectEqual(@as(u21, 0), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } + + // Should not be dirty since we didn't modify anything + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: disabled wraparound with wide char and no space" { @@ -3386,6 +3552,7 @@ test "Terminal: disabled wraparound with wide char and no space" { // This puts our cursor at the end and there is NO SPACE for a // wide character. try t.printString("AAAAA"); + t.clearDirty(); try t.print(0x1F6A8); // Police car light try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 4), t.screen.cursor.x); @@ -3402,6 +3569,9 @@ test "Terminal: disabled wraparound with wide char and no space" { try testing.expectEqual(@as(u21, 'A'), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } + + // Should not be dirty since we didn't modify anything + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: disabled wraparound with wide grapheme and half space" { @@ -3415,6 +3585,7 @@ test "Terminal: disabled wraparound with wide grapheme and half space" { // wide character. try t.printString("AAAA"); try t.print(0x2764); // Heart + t.clearDirty(); try t.print(0xFE0F); // VS16 to make wide try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 4), t.screen.cursor.x); @@ -3431,6 +3602,9 @@ test "Terminal: disabled wraparound with wide grapheme and half space" { try testing.expectEqual(@as(u21, '❤'), cell.content.codepoint); try testing.expectEqual(Cell.Wide.narrow, cell.wide); } + + // Should not be dirty since we didn't modify anything + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); } test "Terminal: print right margin wrap" { @@ -3456,6 +3630,34 @@ test "Terminal: print right margin wrap" { } } +test "Terminal: print right margin wrap dirty tracking" { + var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 }); + defer t.deinit(testing.allocator); + + try t.printString("123456789"); + t.modes.set(.enable_left_and_right_margin, true); + t.setLeftAndRightMargin(3, 5); + t.setCursorPos(1, 5); + + // Writing our X on the first line should mark only that line dirty. + t.clearDirty(); + try t.print('X'); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 4, .y = 0 } })); + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 2, .y = 1 } })); + + // Writing our Y should wrap and only mark the second line dirty. + t.clearDirty(); + try t.print('Y'); + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 4, .y = 0 } })); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 2, .y = 1 } })); + + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("1234X6789\n Y", str); + } +} + test "Terminal: print right margin outside" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 5 }); defer t.deinit(testing.allocator); @@ -3464,6 +3666,7 @@ test "Terminal: print right margin outside" { t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(3, 5); t.setCursorPos(1, 6); + t.clearDirty(); try t.printString("XY"); { @@ -3471,6 +3674,8 @@ test "Terminal: print right margin outside" { defer testing.allocator.free(str); try testing.expectEqualStrings("12345XY89", str); } + + try testing.expect(t.isDirty(.{ .screen = .{ .x = 5, .y = 0 } })); } test "Terminal: print right margin outside wrap" { @@ -3501,6 +3706,10 @@ test "Terminal: print wide char at right margin does not create spacer head" { try testing.expectEqual(@as(usize, 1), t.screen.cursor.y); try testing.expectEqual(@as(usize, 4), t.screen.cursor.x); + // Only wrapped row should be dirty + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 4, .y = 0 } })); + try testing.expect(t.isDirty(.{ .screen = .{ .x = 4, .y = 1 } })); + { const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 4, .y = 0 } }).?; const cell = list_cell.cell; diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 38618e1d3..1b134a4eb 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -917,6 +917,20 @@ pub const Page = struct { }; } + /// Returns true if the given row is dirty. This is NOT very + /// efficient if you're checking many rows and you should use + /// dirtyBitSet directly instead. + pub fn isRowDirty(self: *const Page, y: usize) bool { + return self.dirtyBitSet().isSet(y); + } + + /// Returns true if this page is dirty at all. If you plan on + /// checking any additional rows, you should use dirtyBitSet and + /// check this on your own so you have the set available. + pub fn isDirty(self: *const Page) bool { + return self.dirtyBitSet().findFirstSet() != null; + } + pub const Layout = struct { total_size: usize, rows_start: usize, From 981f03195102dada87a5ada43726ca75c868d472 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 10:28:09 -0700 Subject: [PATCH 03/24] terminal: remove unused debug log --- src/terminal/PageList.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index f6f3853bb..fdba5dbf2 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1917,7 +1917,7 @@ fn createPage( self: *PageList, cap: Capacity, ) !*List.Node { - log.debug("create page cap={}", .{cap}); + // log.debug("create page cap={}", .{cap}); return try createPageExt(&self.pool, cap, &self.page_size); } From f7a57bd2c871048fb42cf30e782a9e1e64a8ea59 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 10:48:39 -0700 Subject: [PATCH 04/24] terminal: dirty tests on t/b/l/r margins --- src/terminal/PageList.zig | 2 +- src/terminal/Terminal.zig | 86 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index fdba5dbf2..0f1d7ea8a 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2999,7 +2999,7 @@ pub const Pin = struct { /// Mark this pin location as dirty. /// TODO: test - pub fn markDirty(self: *Pin) void { + pub fn markDirty(self: Pin) void { var set = self.page.data.dirtyBitSet(); set.set(self.y); } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index cdeab624f..cf3a0d2ea 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1445,6 +1445,10 @@ pub fn insertLines(self: *Terminal, count: usize) void { self.rowWillBeShifted(&p.page.data, src); self.rowWillBeShifted(&dst_p.page.data, dst); + // Mark both our src/dst as dirty + p.markDirty(); + dst_p.markDirty(); + // If our scrolling region is full width, then we unset wrap. if (!left_right) { dst.wrap = false; @@ -1509,6 +1513,9 @@ pub fn insertLines(self: *Terminal, count: usize) void { while (it.next()) |p| { const row: *Row = p.rowAndCell().row; + // This row is now dirty + p.markDirty(); + // Clear the src row. const page = &p.page.data; const cells = page.getCells(row); @@ -1866,6 +1873,9 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void { break :end end; }; + // Mark our cursor row as dirty + self.screen.cursorMarkDirty(); + // Clear the cells const cells: [*]Cell = @ptrCast(self.screen.cursor.page_cell); @@ -3736,10 +3746,20 @@ test "Terminal: linefeed and carriage return" { var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 }); defer t.deinit(testing.allocator); - // Basic grid writing + // Print and CR. for ("hello") |c| try t.print(c); + t.clearDirty(); t.carriageReturn(); + + // CR should not mark row dirty because it doesn't change rendering. + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + try t.linefeed(); + + // LF should not mark row dirty + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } })); + for ("world") |c| try t.print(c); try testing.expectEqual(@as(usize, 1), t.screen.cursor.y); try testing.expectEqual(@as(usize, 5), t.screen.cursor.x); @@ -3757,7 +3777,10 @@ test "Terminal: linefeed unsets pending wrap" { // Basic grid writing for ("hello") |c| try t.print(c); try testing.expect(t.screen.cursor.pending_wrap == true); + t.clearDirty(); try t.linefeed(); + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } })); + try testing.expect(!t.isDirty(.{ .screen = .{ .x = 0, .y = 1 } })); try testing.expect(t.screen.cursor.pending_wrap == false); } @@ -4132,8 +4155,16 @@ test "Terminal: setTopAndBottomMargin simple" { try t.linefeed(); try t.printString("GHI"); t.setTopAndBottomMargin(0, 0); + + t.clearDirty(); t.scrollDown(1); + // Mark the rows we moved as dirty. + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4154,8 +4185,15 @@ test "Terminal: setTopAndBottomMargin top only" { try t.linefeed(); try t.printString("GHI"); t.setTopAndBottomMargin(2, 0); + + t.clearDirty(); t.scrollDown(1); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4176,8 +4214,14 @@ test "Terminal: setTopAndBottomMargin top and bottom" { try t.linefeed(); try t.printString("GHI"); t.setTopAndBottomMargin(1, 2); + + t.clearDirty(); t.scrollDown(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4198,8 +4242,15 @@ test "Terminal: setTopAndBottomMargin top equal to bottom" { try t.linefeed(); try t.printString("GHI"); t.setTopAndBottomMargin(2, 2); + + t.clearDirty(); t.scrollDown(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4221,8 +4272,13 @@ test "Terminal: setLeftAndRightMargin simple" { try t.printString("GHI"); t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(0, 0); + + t.clearDirty(); t.eraseChars(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4247,8 +4303,15 @@ test "Terminal: setLeftAndRightMargin left only" { try testing.expectEqual(@as(usize, 1), t.scrolling_region.left); try testing.expectEqual(@as(usize, t.cols - 1), t.scrolling_region.right); t.setCursorPos(1, 2); + + t.clearDirty(); t.insertLines(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4271,8 +4334,15 @@ test "Terminal: setLeftAndRightMargin left and right" { t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(1, 2); t.setCursorPos(1, 2); + + t.clearDirty(); t.insertLines(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4295,8 +4365,15 @@ test "Terminal: setLeftAndRightMargin left equal right" { t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(2, 2); t.setCursorPos(1, 2); + + t.clearDirty(); t.insertLines(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4319,8 +4396,15 @@ test "Terminal: setLeftAndRightMargin mode 69 unset" { t.modes.set(.enable_left_and_right_margin, false); t.setLeftAndRightMargin(1, 2); t.setCursorPos(1, 2); + + t.clearDirty(); t.insertLines(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); From 58aa4cc10bc05238bcd12c64682d47f17d39093c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 10:59:38 -0700 Subject: [PATCH 05/24] terminal: dirty tests for insertLines --- src/terminal/Terminal.zig | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index cf3a0d2ea..647a3638a 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -4425,8 +4425,15 @@ test "Terminal: insertLines simple" { try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2); + + t.clearDirty(); t.insertLines(1); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4523,8 +4530,14 @@ test "Terminal: insertLines outside of scroll region" { try t.printString("GHI"); t.setTopAndBottomMargin(3, 4); t.setCursorPos(2, 2); + + t.clearDirty(); t.insertLines(1); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4549,8 +4562,15 @@ test "Terminal: insertLines top/bottom scroll region" { try t.printString("123"); t.setTopAndBottomMargin(1, 3); t.setCursorPos(2, 2); + + t.clearDirty(); t.insertLines(1); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4623,8 +4643,14 @@ test "Terminal: insertLines with scroll region" { t.setTopAndBottomMargin(1, 2); t.setCursorPos(1, 1); + + t.clearDirty(); t.insertLines(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try t.print('X'); { @@ -4658,8 +4684,13 @@ test "Terminal: insertLines more than remaining" { t.setCursorPos(2, 1); // Insert a bunch of lines + t.clearDirty(); t.insertLines(20); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4758,8 +4789,15 @@ test "Terminal: insertLines left/right scroll region" { t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2); + + t.clearDirty(); t.insertLines(1); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); From bd1a7d3db14a30a0ccdb312212efbb328d2c352b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 11:03:39 -0700 Subject: [PATCH 06/24] terminal: scrollDown dirty tests --- src/terminal/Terminal.zig | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 647a3638a..569ed502f 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1594,6 +1594,10 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { const src: *Row = src_rac.row; const dst: *Row = dst_rac.row; + // Mark both our src/dst as dirty + p.markDirty(); + src_p.markDirty(); + self.rowWillBeShifted(&src_p.page.data, src); self.rowWillBeShifted(&p.page.data, dst); @@ -1659,6 +1663,9 @@ pub fn deleteLines(self: *Terminal, count_req: usize) void { while (it.next()) |p| { const row: *Row = p.rowAndCell().row; + // This row is now dirty + p.markDirty(); + // Clear the src row. const page = &p.page.data; const cells = page.getCells(row); @@ -4818,11 +4825,17 @@ test "Terminal: scrollUp simple" { try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2); + const cursor = t.screen.cursor; + t.clearDirty(); t.scrollUp(1); try testing.expectEqual(cursor.x, t.screen.cursor.x); try testing.expectEqual(cursor.y, t.screen.cursor.y); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4844,8 +4857,14 @@ test "Terminal: scrollUp top/bottom scroll region" { try t.printString("GHI"); t.setTopAndBottomMargin(2, 3); t.setCursorPos(1, 1); + + t.clearDirty(); t.scrollUp(1); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4868,11 +4887,17 @@ test "Terminal: scrollUp left/right scroll region" { t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2); + const cursor = t.screen.cursor; + t.clearDirty(); t.scrollUp(1); try testing.expectEqual(cursor.x, t.screen.cursor.x); try testing.expectEqual(cursor.y, t.screen.cursor.y); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4910,8 +4935,13 @@ test "Terminal: scrollUp full top/bottom region" { t.setCursorPos(5, 1); try t.printString("ABCDE"); t.setTopAndBottomMargin(2, 5); + + t.clearDirty(); t.scrollUp(4); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -4930,8 +4960,13 @@ test "Terminal: scrollUp full top/bottomleft/right scroll region" { t.modes.set(.enable_left_and_right_margin, true); t.setTopAndBottomMargin(2, 5); t.setLeftAndRightMargin(2, 4); + + t.clearDirty(); t.scrollUp(4); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + for (1..5) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); From b46e028069777f0b294978ad74cbb3d2954c1b3c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 11:10:08 -0700 Subject: [PATCH 07/24] terminal: scrollDown dirty tests --- src/terminal/Terminal.zig | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 569ed502f..9e8f64415 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -4987,11 +4987,15 @@ test "Terminal: scrollDown simple" { try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2); + const cursor = t.screen.cursor; + t.clearDirty(); t.scrollDown(1); try testing.expectEqual(cursor.x, t.screen.cursor.x); try testing.expectEqual(cursor.y, t.screen.cursor.y); + for (0..5) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -5013,11 +5017,18 @@ test "Terminal: scrollDown outside of scroll region" { try t.printString("GHI"); t.setTopAndBottomMargin(3, 4); t.setCursorPos(2, 2); + const cursor = t.screen.cursor; + t.clearDirty(); t.scrollDown(1); try testing.expectEqual(cursor.x, t.screen.cursor.x); try testing.expectEqual(cursor.y, t.screen.cursor.y); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -5040,11 +5051,15 @@ test "Terminal: scrollDown left/right scroll region" { t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2); + const cursor = t.screen.cursor; + t.clearDirty(); t.scrollDown(1); try testing.expectEqual(cursor.x, t.screen.cursor.x); try testing.expectEqual(cursor.y, t.screen.cursor.y); + for (0..4) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -5067,11 +5082,15 @@ test "Terminal: scrollDown outside of left/right scroll region" { t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(1, 1); + const cursor = t.screen.cursor; + t.clearDirty(); t.scrollDown(1); try testing.expectEqual(cursor.x, t.screen.cursor.x); try testing.expectEqual(cursor.y, t.screen.cursor.y); + for (0..4) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); From 0749b678328cfd9f88d74504137dbe0459bf4fa7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 11:22:27 -0700 Subject: [PATCH 08/24] terminal: index dirty tests one todo --- src/terminal/Terminal.zig | 53 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 9e8f64415..19cd5621c 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -5126,9 +5126,13 @@ test "Terminal: eraseChars simple operation" { for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); + t.clearDirty(); t.eraseChars(2); try t.print('X'); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -5143,8 +5147,10 @@ test "Terminal: eraseChars minimum one" { for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); + t.clearDirty(); t.eraseChars(0); try t.print('X'); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -5578,6 +5584,11 @@ test "Terminal: index" { try t.index(); try t.print('A'); + // Only the row we write to is dirty. Moving the cursor itself + // does not make a row dirty. + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -5593,10 +5604,16 @@ test "Terminal: index from the bottom" { t.setCursorPos(5, 1); try t.print('A'); t.cursorLeft(1); // undo moving right from 'A' - try t.index(); + t.clearDirty(); + try t.index(); try t.print('B'); + // Only the row we write to is dirty. Moving the cursor itself + // does not make a row dirty. + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -5623,8 +5640,10 @@ test "Terminal: index from the bottom outside of scroll region" { t.setTopAndBottomMargin(1, 2); t.setCursorPos(5, 1); try t.print('A'); + t.clearDirty(); try t.index(); try t.print('B'); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); { const str = try t.plainString(testing.allocator); @@ -5639,9 +5658,13 @@ test "Terminal: index no scroll region, top of screen" { defer t.deinit(alloc); try t.print('A'); + t.clearDirty(); try t.index(); try t.print('X'); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -5656,9 +5679,13 @@ test "Terminal: index bottom of primary screen" { t.setCursorPos(5, 1); try t.print('A'); + t.clearDirty(); try t.index(); try t.print('X'); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -5706,9 +5733,13 @@ test "Terminal: index inside scroll region" { t.setTopAndBottomMargin(1, 3); try t.print('A'); + t.clearDirty(); try t.index(); try t.print('X'); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -5762,11 +5793,15 @@ test "Terminal: index bottom of primary screen with scroll region" { t.setCursorPos(3, 1); try t.print('A'); t.setCursorPos(5, 1); + t.clearDirty(); try t.index(); try t.index(); try t.index(); try t.print('X'); + for (0..4) |y| try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -5785,9 +5820,12 @@ test "Terminal: index outside left/right margin" { t.setCursorPos(3, 3); try t.print('A'); t.setCursorPos(3, 1); + t.clearDirty(); try t.index(); try t.print('X'); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -5811,8 +5849,14 @@ test "Terminal: index inside left/right margin" { t.setTopAndBottomMargin(1, 3); t.setLeftAndRightMargin(1, 3); t.setCursorPos(3, 1); + + t.clearDirty(); try t.index(); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expectEqual(@as(usize, 2), t.screen.cursor.y); try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); @@ -5833,9 +5877,16 @@ test "Terminal: index bottom of scroll region" { try t.print('B'); t.setCursorPos(3, 1); try t.print('A'); + t.clearDirty(); try t.index(); try t.print('X'); + // TODO(dirty) + // try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + // try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + // try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + // try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); From 19ddbbc7d6c0dca7425ab413aca7728ba5ff65c2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 14:20:18 -0700 Subject: [PATCH 09/24] terminal: eraseRowBounded dirty tracking --- src/terminal/PageList.zig | 44 +++++++++++++++++++++++++++++++++++++++ src/terminal/Terminal.zig | 9 ++++---- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 0f1d7ea8a..5a7dbf271 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2102,6 +2102,10 @@ pub fn eraseRowBounded( page.data.clearCells(&rows[pn.y], 0, page.data.size.cols); fastmem.rotateOnce(Row, rows[pn.y..][0 .. limit + 1]); + // Set all the rows as dirty + var dirty = page.data.dirtyBitSet(); + dirty.setRangeValue(.{ .start = pn.y, .end = pn.y + limit }, true); + // Update pins in the shifted region. var pin_it = self.tracked_pins.keyIterator(); while (pin_it.next()) |p_ptr| { @@ -2123,6 +2127,12 @@ pub fn eraseRowBounded( fastmem.rotateOnce(Row, rows[pn.y..page.data.size.rows]); + // All the rows in the page are dirty below the erased row. + { + var dirty = page.data.dirtyBitSet(); + dirty.setRangeValue(.{ .start = pn.y, .end = page.data.size.rows }, true); + } + // We need to keep track of how many rows we've shifted so that we can // determine at what point we need to do a partial shift on subsequent // pages. @@ -2165,6 +2175,10 @@ pub fn eraseRowBounded( page.data.clearCells(&rows[0], 0, page.data.size.cols); fastmem.rotateOnce(Row, rows[0 .. shifted_limit + 1]); + // Set all the rows as dirty + var dirty = page.data.dirtyBitSet(); + dirty.setRangeValue(.{ .start = 0, .end = shifted_limit }, true); + // Update pins in the shifted region. var pin_it = self.tracked_pins.keyIterator(); while (pin_it.next()) |p_ptr| { @@ -2183,6 +2197,10 @@ pub fn eraseRowBounded( fastmem.rotateOnce(Row, rows[0..page.data.size.rows]); + // Set all the rows as dirty + var dirty = page.data.dirtyBitSet(); + dirty.setRangeValue(.{ .start = 0, .end = page.data.size.rows }, true); + // Account for the rows shifted in this page. shifted += page.data.size.rows; @@ -2939,6 +2957,11 @@ pub fn clearDirty(self: *PageList) void { } } +/// Returns true if the point is dirty, used for testing. +pub fn isDirty(self: *const PageList, pt: point.Point) bool { + return self.getCell(pt).?.isDirty(); +} + /// Represents an exact x/y coordinate within the screen. This is called /// a "pin" because it is a fixed point within the pagelist direct to /// a specific page pointer and memory offset. The benefit is that this @@ -4513,6 +4536,13 @@ test "PageList eraseRowBounded less than full row" { try s.eraseRowBounded(.{ .active = .{ .y = 5 } }, 3); try testing.expectEqual(s.rows, s.totalRows()); + // The erased rows should be dirty + try testing.expect(!s.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); + try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = 5 } })); + try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = 6 } })); + try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = 7 } })); + try testing.expect(!s.isDirty(.{ .active = .{ .x = 0, .y = 8 } })); + try testing.expectEqual(s.pages.first.?, p_top.page); try testing.expectEqual(@as(usize, 4), p_top.y); try testing.expectEqual(@as(usize, 0), p_top.x); @@ -4541,6 +4571,12 @@ test "PageList eraseRowBounded with pin at top" { try s.eraseRowBounded(.{ .active = .{ .y = 0 } }, 3); try testing.expectEqual(s.rows, s.totalRows()); + // The erased rows should be dirty + try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(!s.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try testing.expectEqual(s.pages.first.?, p_top.page); try testing.expectEqual(@as(usize, 0), p_top.y); try testing.expectEqual(@as(usize, 0), p_top.x); @@ -4563,6 +4599,10 @@ test "PageList eraseRowBounded full rows single page" { try s.eraseRowBounded(.{ .active = .{ .y = 5 } }, 10); try testing.expectEqual(s.rows, s.totalRows()); + // The erased rows should be dirty + try testing.expect(!s.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); + for (5..10) |y| try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = y } })); + // Our pin should move to the first page try testing.expectEqual(s.pages.first.?, p_in.page); try testing.expectEqual(@as(usize, 6), p_in.y); @@ -4620,6 +4660,10 @@ test "PageList eraseRowBounded full rows two pages" { // Erase only a few rows in our active try s.eraseRowBounded(.{ .active = .{ .y = 4 } }, 4); + // The erased rows should be dirty + try testing.expect(!s.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + for (4..8) |y| try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = y } })); + // In page in first page is shifted try testing.expectEqual(s.pages.last.?.prev.?, p_first.page); try testing.expectEqual(@as(usize, p_first.page.data.size.rows - 2), p_first.y); diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 19cd5621c..c2acf8238 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -5881,11 +5881,10 @@ test "Terminal: index bottom of scroll region" { try t.index(); try t.print('X'); - // TODO(dirty) - // try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); - // try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); - // try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); - // try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); { const str = try t.plainString(testing.allocator); From a53dbaaa3177b831d532402753ab8c229af6c47b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 14:38:08 -0700 Subject: [PATCH 10/24] terminal: more dirty tests --- src/terminal/Terminal.zig | 77 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index c2acf8238..93453ac9f 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1763,6 +1763,9 @@ pub fn insertBlanks(self: *Terminal, count: usize) void { // Insert blanks. The blanks preserve the background color. self.screen.clearCells(page, self.screen.cursor.page_row, left[0..adjusted_count]); + + // Our row is always dirty + self.screen.cursorMarkDirty(); } /// Removes amount characters from the current cursor position to the right. @@ -2137,7 +2140,7 @@ pub fn decaln(self: *Terminal) !void { // Move our cursor to the top-left self.setCursorPos(1, 1); - // Erase the display which will deallocate graphames, styles, etc. + // Erase the display which will deallocate graphemes, styles, etc. self.eraseDisplay(.complete, false); // Fill with Es by moving the cursor but reset it after. @@ -2159,6 +2162,7 @@ pub fn decaln(self: *Terminal) !void { row.styled = true; } + self.screen.cursorMarkDirty(); if (self.screen.cursor.y == self.rows - 1) break; self.screen.cursorDown(1); } @@ -6368,8 +6372,15 @@ test "Terminal: deleteLines simple" { try t.linefeed(); try t.printString("GHI"); t.setCursorPos(2, 2); + + t.clearDirty(); t.deleteLines(1); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -6472,8 +6483,15 @@ test "Terminal: deleteLines with scroll region" { t.setTopAndBottomMargin(1, 3); t.setCursorPos(1, 1); + + t.clearDirty(); t.deleteLines(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try t.print('E'); t.carriageReturn(); try t.linefeed(); @@ -6489,7 +6507,6 @@ test "Terminal: deleteLines with scroll region" { } } -// X test "Terminal: deleteLines with scroll region, large count" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 80, .rows = 80 }); @@ -6509,8 +6526,15 @@ test "Terminal: deleteLines with scroll region, large count" { t.setTopAndBottomMargin(1, 3); t.setCursorPos(1, 1); + + t.clearDirty(); t.deleteLines(5); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); + try t.print('E'); t.carriageReturn(); try t.linefeed(); @@ -6526,7 +6550,6 @@ test "Terminal: deleteLines with scroll region, large count" { } } -// X test "Terminal: deleteLines with scroll region, cursor outside of region" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 80, .rows = 80 }); @@ -6546,8 +6569,12 @@ test "Terminal: deleteLines with scroll region, cursor outside of region" { t.setTopAndBottomMargin(1, 3); t.setCursorPos(4, 1); + + t.clearDirty(); t.deleteLines(1); + for (0..4) |y| try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -6619,8 +6646,13 @@ test "Terminal: deleteLines left/right scroll region" { t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2); + + t.clearDirty(); t.deleteLines(1); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + for (1..3) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -6643,8 +6675,12 @@ test "Terminal: deleteLines left/right scroll region from top" { t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(1, 2); + + t.clearDirty(); t.deleteLines(1); + for (0..3) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -6667,8 +6703,13 @@ test "Terminal: deleteLines left/right scroll region high count" { t.scrolling_region.left = 1; t.scrolling_region.right = 3; t.setCursorPos(2, 2); + + t.clearDirty(); t.deleteLines(100); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + for (1..3) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -7031,6 +7072,8 @@ test "Terminal: DECALN" { try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); + for (0..t.rows) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -7096,7 +7139,11 @@ test "Terminal: insertBlanks" { try t.print('B'); try t.print('C'); t.setCursorPos(1, 1); + + t.clearDirty(); t.insertBlanks(2); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); { const str = try t.plainString(testing.allocator); @@ -7116,7 +7163,10 @@ test "Terminal: insertBlanks pushes off end" { try t.print('B'); try t.print('C'); t.setCursorPos(1, 1); + + t.clearDirty(); t.insertBlanks(2); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -7136,7 +7186,10 @@ test "Terminal: insertBlanks more than size" { try t.print('B'); try t.print('C'); t.setCursorPos(1, 1); + + t.clearDirty(); t.insertBlanks(5); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -7152,7 +7205,10 @@ test "Terminal: insertBlanks no scroll region, fits" { for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); + + t.clearDirty(); t.insertBlanks(2); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -7198,7 +7254,9 @@ test "Terminal: insertBlanks shift off screen" { for (" ABC") |c| try t.print(c); t.setCursorPos(1, 3); + t.clearDirty(); t.insertBlanks(2); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try t.print('X'); { @@ -7216,7 +7274,9 @@ test "Terminal: insertBlanks split multi-cell character" { for ("123") |c| try t.print(c); try t.print('橋'); t.setCursorPos(1, 1); + t.clearDirty(); t.insertBlanks(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -7235,7 +7295,10 @@ test "Terminal: insertBlanks inside left/right scroll region" { t.setCursorPos(1, 3); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 3); + + t.clearDirty(); t.insertBlanks(2); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try t.print('X'); { @@ -7255,7 +7318,9 @@ test "Terminal: insertBlanks outside left/right scroll region" { t.scrolling_region.left = 2; t.scrolling_region.right = 4; try testing.expect(t.screen.cursor.pending_wrap); + t.clearDirty(); t.insertBlanks(2); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(!t.screen.cursor.pending_wrap); try t.print('X'); @@ -7275,7 +7340,9 @@ test "Terminal: insertBlanks left/right scroll region large count" { t.modes.set(.enable_left_and_right_margin, true); t.setLeftAndRightMargin(3, 5); t.setCursorPos(1, 1); + t.clearDirty(); t.insertBlanks(140); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try t.print('X'); { @@ -7307,7 +7374,9 @@ test "Terminal: insertBlanks deleting graphemes" { try testing.expectEqual(@as(usize, 1), page.graphemeCount()); t.setCursorPos(1, 1); + t.clearDirty(); t.insertBlanks(4); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -7341,7 +7410,9 @@ test "Terminal: insertBlanks shift graphemes" { try testing.expectEqual(@as(usize, 1), page.graphemeCount()); t.setCursorPos(1, 1); + t.clearDirty(); t.insertBlanks(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); From 1c05939f179663a560c60116420f836098c3f5db Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 14:40:32 -0700 Subject: [PATCH 11/24] terminal: deleteChars dirty --- src/terminal/Terminal.zig | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 93453ac9f..a2bfd0868 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1856,6 +1856,9 @@ pub fn deleteChars(self: *Terminal, count_req: usize) void { // Insert blanks. The blanks preserve the background color. self.screen.clearCells(page, self.screen.cursor.page_row, x[0 .. rem - scroll_amount]); + + // Our row is always dirty + self.screen.cursorMarkDirty(); } pub fn eraseChars(self: *Terminal, count_req: usize) void { @@ -7549,7 +7552,10 @@ test "Terminal: deleteChars" { for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2); + t.clearDirty(); t.deleteChars(2); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -7565,7 +7571,10 @@ test "Terminal: deleteChars zero count" { for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2); + t.clearDirty(); t.deleteChars(0); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -7581,7 +7590,10 @@ test "Terminal: deleteChars more than half" { for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2); + t.clearDirty(); t.deleteChars(3); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -7597,7 +7609,10 @@ test "Terminal: deleteChars more than line width" { for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2); + t.clearDirty(); t.deleteChars(10); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -7613,7 +7628,10 @@ test "Terminal: deleteChars should shift left" { for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 2); + t.clearDirty(); t.deleteChars(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -7675,7 +7693,10 @@ test "Terminal: deleteChars simple operation" { try t.printString("ABC123"); t.setCursorPos(1, 3); + + t.clearDirty(); t.deleteChars(2); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -7726,7 +7747,9 @@ test "Terminal: deleteChars outside scroll region" { t.scrolling_region.left = 2; t.scrolling_region.right = 4; try testing.expect(t.screen.cursor.pending_wrap); + t.clearDirty(); t.deleteChars(2); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(t.screen.cursor.pending_wrap); { @@ -7745,7 +7768,10 @@ test "Terminal: deleteChars inside scroll region" { t.scrolling_region.left = 2; t.scrolling_region.right = 4; t.setCursorPos(1, 4); + + t.clearDirty(); t.deleteChars(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); From cfcd16354a19e0b2f4f1b881e2e35317ab4b7a40 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 14:48:14 -0700 Subject: [PATCH 12/24] terminal: many more dirty checks --- src/terminal/Terminal.zig | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index a2bfd0868..c44b56adb 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1959,6 +1959,9 @@ pub fn eraseLine( // a valid mode at this point. self.screen.cursor.pending_wrap = false; + // We always mark our row as dirty + self.screen.cursorMarkDirty(); + // Start of our cells const cells: [*]Cell = cells: { const cells: [*]Cell = @ptrCast(self.screen.cursor.page_cell); @@ -8050,7 +8053,9 @@ test "Terminal: eraseLine simple erase right" { for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 3); + t.clearDirty(); t.eraseLine(.right, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8146,7 +8151,9 @@ test "Terminal: eraseLine right wide character" { try t.print('橋'); for ("DE") |c| try t.print(c); t.setCursorPos(1, 4); + t.clearDirty(); t.eraseLine(.right, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8163,7 +8170,9 @@ test "Terminal: eraseLine right protected attributes respected with iso" { t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); + t.clearDirty(); t.eraseLine(.right, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8182,7 +8191,9 @@ test "Terminal: eraseLine right protected attributes ignored with dec most recen t.setProtectedMode(.dec); t.setProtectedMode(.off); t.setCursorPos(1, 2); + t.clearDirty(); t.eraseLine(.right, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8199,7 +8210,9 @@ test "Terminal: eraseLine right protected attributes ignored with dec set" { t.setProtectedMode(.dec); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 2); + t.clearDirty(); t.eraseLine(.right, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8218,7 +8231,9 @@ test "Terminal: eraseLine right protected requested" { t.setProtectedMode(.dec); try t.print('X'); t.setCursorPos(t.screen.cursor.y + 1, 4); + t.clearDirty(); t.eraseLine(.right, true); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8234,7 +8249,9 @@ test "Terminal: eraseLine simple erase left" { for ("ABCDE") |c| try t.print(c); t.setCursorPos(1, 3); + t.clearDirty(); t.eraseLine(.left, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8250,7 +8267,9 @@ test "Terminal: eraseLine left resets wrap" { for ("ABCDE") |c| try t.print(c); try testing.expect(t.screen.cursor.pending_wrap); + t.clearDirty(); t.eraseLine(.left, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); try testing.expect(!t.screen.cursor.pending_wrap); try t.print('B'); @@ -8303,7 +8322,9 @@ test "Terminal: eraseLine left wide character" { try t.print('橋'); for ("DE") |c| try t.print(c); t.setCursorPos(1, 3); + t.clearDirty(); t.eraseLine(.left, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8320,7 +8341,9 @@ test "Terminal: eraseLine left protected attributes respected with iso" { t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); + t.clearDirty(); t.eraseLine(.left, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8339,7 +8362,9 @@ test "Terminal: eraseLine left protected attributes ignored with dec most recent t.setProtectedMode(.dec); t.setProtectedMode(.off); t.setCursorPos(1, 2); + t.clearDirty(); t.eraseLine(.left, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8356,7 +8381,9 @@ test "Terminal: eraseLine left protected attributes ignored with dec set" { t.setProtectedMode(.dec); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 2); + t.clearDirty(); t.eraseLine(.left, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8375,7 +8402,9 @@ test "Terminal: eraseLine left protected requested" { t.setProtectedMode(.dec); try t.print('X'); t.setCursorPos(t.screen.cursor.y + 1, 8); + t.clearDirty(); t.eraseLine(.left, true); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8425,7 +8454,9 @@ test "Terminal: eraseLine complete protected attributes respected with iso" { t.setProtectedMode(.iso); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 1); + t.clearDirty(); t.eraseLine(.complete, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8444,7 +8475,9 @@ test "Terminal: eraseLine complete protected attributes ignored with dec most re t.setProtectedMode(.dec); t.setProtectedMode(.off); t.setCursorPos(1, 2); + t.clearDirty(); t.eraseLine(.complete, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8461,7 +8494,9 @@ test "Terminal: eraseLine complete protected attributes ignored with dec set" { t.setProtectedMode(.dec); for ("ABC") |c| try t.print(c); t.setCursorPos(1, 2); + t.clearDirty(); t.eraseLine(.complete, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8480,7 +8515,9 @@ test "Terminal: eraseLine complete protected requested" { t.setProtectedMode(.dec); try t.print('X'); t.setCursorPos(t.screen.cursor.y + 1, 8); + t.clearDirty(); t.eraseLine(.complete, true); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8496,6 +8533,7 @@ test "Terminal: tabClear single" { try t.horizontalTab(); t.tabClear(.current); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); t.setCursorPos(1, 1); try t.horizontalTab(); try testing.expectEqual(@as(usize, 16), t.screen.cursor.x); @@ -8507,6 +8545,7 @@ test "Terminal: tabClear all" { defer t.deinit(alloc); t.tabClear(.all); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); t.setCursorPos(1, 1); try t.horizontalTab(); try testing.expectEqual(@as(usize, 29), t.screen.cursor.x); @@ -8519,6 +8558,7 @@ test "Terminal: printRepeat simple" { try t.printString("A"); try t.printRepeat(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8534,6 +8574,7 @@ test "Terminal: printRepeat wrap" { try t.printString(" A"); try t.printRepeat(1); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); @@ -8548,6 +8589,7 @@ test "Terminal: printRepeat no previous character" { defer t.deinit(alloc); try t.printRepeat(1); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); { const str = try t.plainString(testing.allocator); From fb25f5cea139be6776ec975cdd785ab70c591357 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Apr 2024 14:57:44 -0700 Subject: [PATCH 13/24] terminal: more dirty tests --- src/terminal/Screen.zig | 7 +++++++ src/terminal/Terminal.zig | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index a0a48d40c..0d2d74eb6 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -809,6 +809,10 @@ pub fn clearRows( var it = self.pages.pageIterator(.right_down, tl, bl); while (it.next()) |chunk| { + // Mark everything in this chunk as dirty + var dirty = chunk.page.data.dirtyBitSet(); + dirty.setRangeValue(.{ .start = chunk.start, .end = chunk.end }, true); + for (chunk.rows()) |*row| { const cells_offset = row.cells; const cells_multi: [*]Cell = row.cells.ptr(chunk.page.data.memory); @@ -2508,6 +2512,7 @@ test "Screen clearRows active one line" { try s.testWriteString("hello, world"); s.clearRows(.{ .active = .{} }, null, false); + try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("", str); @@ -2522,6 +2527,8 @@ test "Screen clearRows active multi line" { try s.testWriteString("hello\nworld"); s.clearRows(.{ .active = .{} }, null, false); + try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); const str = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(str); try testing.expectEqualStrings("", str); diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index c44b56adb..b242e959c 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -8662,8 +8662,14 @@ test "Terminal: eraseDisplay simple erase below" { try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2); + + t.clearDirty(); t.eraseDisplay(.below, false); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); + { const str = try t.plainString(testing.allocator); defer testing.allocator.free(str); @@ -8840,7 +8846,12 @@ test "Terminal: eraseDisplay simple erase above" { try t.linefeed(); for ("GHI") |c| try t.print(c); t.setCursorPos(2, 2); + + t.clearDirty(); t.eraseDisplay(.above, false); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); { const str = try t.plainString(testing.allocator); @@ -9018,7 +9029,10 @@ test "Terminal: eraseDisplay protected complete" { t.setProtectedMode(.dec); try t.print('X'); t.setCursorPos(t.screen.cursor.y + 1, 4); + + t.clearDirty(); t.eraseDisplay(.complete, true); + for (0..t.rows) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); { const str = try t.plainString(testing.allocator); From 4f2ee95ecdf7042a5148b2505d29fe31702ddf5a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 17 Apr 2024 20:34:10 -0700 Subject: [PATCH 14/24] renderer/metal: docs --- src/renderer/metal/buffer.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/renderer/metal/buffer.zig b/src/renderer/metal/buffer.zig index b11861817..20590afc2 100644 --- a/src/renderer/metal/buffer.zig +++ b/src/renderer/metal/buffer.zig @@ -64,7 +64,12 @@ pub fn Buffer(comptime T: type) type { return ptr[0..len]; } - /// Sync new contents to the buffer. + /// Sync new contents to the buffer. The data is expected to be the + /// complete contents of the buffer. If the amont of data is larger + /// than the buffer length, the buffer will be reallocated. + /// + /// If the amount of data is smaller than the buffer length, the + /// remaining data in the buffer is left untouched. 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. const req_bytes = data.len * @sizeOf(T); From f867fabf8e12ca6fd5dfe8142762aa08eb690784 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 10:16:10 -0700 Subject: [PATCH 15/24] terminal: new coordinate type --- src/terminal/PageList.zig | 10 +++++-- src/terminal/Terminal.zig | 55 +++++++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 5a7dbf271..53533f8c1 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -4601,7 +4601,10 @@ test "PageList eraseRowBounded full rows single page" { // The erased rows should be dirty try testing.expect(!s.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); - for (5..10) |y| try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (5..10) |y| try testing.expect(s.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); // Our pin should move to the first page try testing.expectEqual(s.pages.first.?, p_in.page); @@ -4662,7 +4665,10 @@ test "PageList eraseRowBounded full rows two pages" { // The erased rows should be dirty try testing.expect(!s.isDirty(.{ .active = .{ .x = 0, .y = 3 } })); - for (4..8) |y| try testing.expect(s.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (4..8) |y| try testing.expect(s.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); // In page in first page is shifted try testing.expectEqual(s.pages.last.?.prev.?, p_first.page); diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index b242e959c..b83b9ff99 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -4975,7 +4975,10 @@ test "Terminal: scrollUp full top/bottomleft/right scroll region" { t.scrollUp(4); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); - for (1..5) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (1..5) |y| try testing.expect(t.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); { const str = try t.plainString(testing.allocator); @@ -5004,7 +5007,10 @@ test "Terminal: scrollDown simple" { try testing.expectEqual(cursor.x, t.screen.cursor.x); try testing.expectEqual(cursor.y, t.screen.cursor.y); - for (0..5) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (0..5) |y| try testing.expect(t.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); { const str = try t.plainString(testing.allocator); @@ -5068,7 +5074,10 @@ test "Terminal: scrollDown left/right scroll region" { try testing.expectEqual(cursor.x, t.screen.cursor.x); try testing.expectEqual(cursor.y, t.screen.cursor.y); - for (0..4) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (0..4) |y| try testing.expect(t.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); { const str = try t.plainString(testing.allocator); @@ -5099,7 +5108,10 @@ test "Terminal: scrollDown outside of left/right scroll region" { try testing.expectEqual(cursor.x, t.screen.cursor.x); try testing.expectEqual(cursor.y, t.screen.cursor.y); - for (0..4) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (0..4) |y| try testing.expect(t.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); { const str = try t.plainString(testing.allocator); @@ -5809,7 +5821,10 @@ test "Terminal: index bottom of primary screen with scroll region" { try t.index(); try t.print('X'); - for (0..4) |y| try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (0..4) |y| try testing.expect(!t.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = 4 } })); { @@ -6579,7 +6594,10 @@ test "Terminal: deleteLines with scroll region, cursor outside of region" { t.clearDirty(); t.deleteLines(1); - for (0..4) |y| try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (0..4) |y| try testing.expect(!t.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); { const str = try t.plainString(testing.allocator); @@ -6657,7 +6675,10 @@ test "Terminal: deleteLines left/right scroll region" { t.deleteLines(1); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); - for (1..3) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (1..3) |y| try testing.expect(t.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); { const str = try t.plainString(testing.allocator); @@ -6685,7 +6706,10 @@ test "Terminal: deleteLines left/right scroll region from top" { t.clearDirty(); t.deleteLines(1); - for (0..3) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (0..3) |y| try testing.expect(t.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); { const str = try t.plainString(testing.allocator); @@ -6714,7 +6738,10 @@ test "Terminal: deleteLines left/right scroll region high count" { t.deleteLines(100); try testing.expect(!t.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); - for (1..3) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (1..3) |y| try testing.expect(t.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); { const str = try t.plainString(testing.allocator); @@ -7078,7 +7105,10 @@ test "Terminal: DECALN" { try testing.expectEqual(@as(usize, 0), t.screen.cursor.y); try testing.expectEqual(@as(usize, 0), t.screen.cursor.x); - for (0..t.rows) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (0..t.rows) |y| try testing.expect(t.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); { const str = try t.plainString(testing.allocator); @@ -9032,7 +9062,10 @@ test "Terminal: eraseDisplay protected complete" { t.clearDirty(); t.eraseDisplay(.complete, true); - for (0..t.rows) |y| try testing.expect(t.isDirty(.{ .active = .{ .x = 0, .y = y } })); + for (0..t.rows) |y| try testing.expect(t.isDirty(.{ .active = .{ + .x = 0, + .y = @intCast(y), + } })); { const str = try t.plainString(testing.allocator); From 3f9e3c39a40077ebd38d69336c46d37f67cb847f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 10:26:41 -0700 Subject: [PATCH 16/24] terminal: track dirty state of palette and reverse colors --- src/terminal/Terminal.zig | 17 +++++++++++++++++ src/termio/Exec.zig | 9 ++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index b83b9ff99..031a399fa 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -114,8 +114,25 @@ flags: packed struct { /// then we want to capture the shift key for the mouse protocol /// if the configuration allows it. mouse_shift_capture: enum(u2) { null, false, true } = .null, + + /// Dirty flags for the renderer. + dirty: Dirty = .{}, } = .{}, +/// This is a set of dirty flags the renderer can use to determine +/// what parts of the screen need to be redrawn. It is up to the renderer +/// to clear these flags. +/// +/// This only contains dirty flags for terminal state, not for the screen +/// state. The screen state has its own dirty flags. +pub const Dirty = packed struct { + /// Set when the color palette is modified in any way. + palette: bool = false, + + /// Set when the reverse colors mode is modified. + reverse_colors: bool = false, +}; + /// The event types that can be reported for mouse-related activities. /// These are all mutually exclusive (hence in a single enum). pub const MouseEvents = enum(u3) { diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 1a044c25b..9d106f79d 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -397,6 +397,7 @@ pub fn changeConfig(self: *Exec, config: *DerivedConfig) !void { for (0..config.palette.len) |i| { if (!self.terminal.color_palette.mask.isSet(i)) { self.terminal.color_palette.colors[i] = config.palette[i]; + self.terminal.flags.dirty.palette = true; } } @@ -2166,7 +2167,10 @@ const StreamHandler = struct { .autorepeat => {}, // Schedule a render since we changed colors - .reverse_colors => try self.queueRender(), + .reverse_colors => { + self.terminal.flags.dirty.reverse_colors = true; + try self.queueRender(); + }, // Origin resets cursor pos. This is called whether or not // we're enabling or disabling origin mode and whether or @@ -2792,6 +2796,7 @@ const StreamHandler = struct { switch (kind) { .palette => |i| { + self.terminal.flags.dirty.palette = true; self.terminal.color_palette.colors[i] = color; self.terminal.color_palette.mask.set(i); }, @@ -2829,6 +2834,7 @@ const StreamHandler = struct { // reset those indices to the default palette var it = mask.iterator(.{}); while (it.next()) |i| { + self.terminal.flags.dirty.palette = true; self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; mask.unset(i); } @@ -2838,6 +2844,7 @@ const StreamHandler = struct { // Skip invalid parameters const i = std.fmt.parseUnsigned(u8, param, 10) catch continue; if (mask.isSet(i)) { + self.terminal.flags.dirty.palette = true; self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; mask.unset(i); } From d47f14f86a78aac22595e9c00eecbb3f253bb8a2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 10:46:39 -0700 Subject: [PATCH 17/24] terminal: dirty tracking on screen clone --- src/terminal/PageList.zig | 37 ++++++++++++++++++++++++++++++++++++- src/terminal/page.zig | 15 ++++++++++----- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 53533f8c1..02f9d0272 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -2962,6 +2962,11 @@ pub fn isDirty(self: *const PageList, pt: point.Point) bool { return self.getCell(pt).?.isDirty(); } +/// Mark a point as dirty, used for testing. +fn markDirty(self: *PageList, pt: point.Point) void { + self.pin(pt).?.markDirty(); +} + /// Represents an exact x/y coordinate within the screen. This is called /// a "pin" because it is a fixed point within the pagelist direct to /// a specific page pointer and memory offset. The benefit is that this @@ -3020,8 +3025,12 @@ pub const Pin = struct { ).?.*; } + /// Check if this pin is dirty. + pub fn isDirty(self: Pin) bool { + return self.page.data.isRowDirty(self.y); + } + /// Mark this pin location as dirty. - /// TODO: test pub fn markDirty(self: Pin) void { var set = self.page.data.dirtyBitSet(); set.set(self.y); @@ -4830,6 +4839,32 @@ test "PageList clone remap tracked pin not in cloned area" { try testing.expect(pin_remap.get(p) == null); } +test "PageList clone full dirty" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 80, 24, null); + defer s.deinit(); + try testing.expectEqual(@as(usize, s.rows), s.totalRows()); + + // Mark a row as dirty + s.markDirty(.{ .active = .{ .x = 0, .y = 0 } }); + s.markDirty(.{ .active = .{ .x = 0, .y = 12 } }); + s.markDirty(.{ .active = .{ .x = 0, .y = 23 } }); + + var s2 = try s.clone(.{ + .top = .{ .screen = .{} }, + .memory = .{ .alloc = alloc }, + }); + defer s2.deinit(); + try testing.expectEqual(@as(usize, s.rows), s2.totalRows()); + + // Should still be dirty + try testing.expect(s2.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(s2.isDirty(.{ .active = .{ .x = 0, .y = 12 } })); + try testing.expect(s2.isDirty(.{ .active = .{ .x = 0, .y = 23 } })); +} + test "PageList resize (no reflow) more rows" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 1b134a4eb..4d3c0b1fd 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -500,11 +500,16 @@ pub const Page = struct { const other_rows = other.rows.ptr(other.memory)[y_start..y_end]; const rows = self.rows.ptr(self.memory)[0 .. y_end - y_start]; - for (rows, other_rows) |*dst_row, *src_row| try self.cloneRowFrom( - other, - dst_row, - src_row, - ); + for (rows, other_rows) |*dst_row, *src_row| { + try self.cloneRowFrom(other, dst_row, src_row); + } + + // Set our dirty range for all the rows we copied + var dirty_set = self.dirtyBitSet(); + dirty_set.setRangeValue(.{ + .start = 0, + .end = y_end - y_start, + }, true); // We should remain consistent self.assertIntegrity(); From 7e52f942782ae2f7ff9ae7c1c77150f08a78251a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 14:14:55 -0700 Subject: [PATCH 18/24] terminal: on clone, only mark rows dirty that were previously dirty --- src/terminal/PageList.zig | 2 ++ src/terminal/page.zig | 14 +++++--------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 02f9d0272..e1da55ad4 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -4861,7 +4861,9 @@ test "PageList clone full dirty" { // Should still be dirty try testing.expect(s2.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + try testing.expect(!s2.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); try testing.expect(s2.isDirty(.{ .active = .{ .x = 0, .y = 12 } })); + try testing.expect(!s2.isDirty(.{ .active = .{ .x = 0, .y = 14 } })); try testing.expect(s2.isDirty(.{ .active = .{ .x = 0, .y = 23 } })); } diff --git a/src/terminal/page.zig b/src/terminal/page.zig index 4d3c0b1fd..7137431cf 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -500,16 +500,12 @@ pub const Page = struct { const other_rows = other.rows.ptr(other.memory)[y_start..y_end]; const rows = self.rows.ptr(self.memory)[0 .. y_end - y_start]; - for (rows, other_rows) |*dst_row, *src_row| { - try self.cloneRowFrom(other, dst_row, src_row); - } - - // Set our dirty range for all the rows we copied + const other_dirty_set = other.dirtyBitSet(); var dirty_set = self.dirtyBitSet(); - dirty_set.setRangeValue(.{ - .start = 0, - .end = y_end - y_start, - }, true); + for (rows, 0.., other_rows, y_start..) |*dst_row, dst_y, *src_row, src_y| { + try self.cloneRowFrom(other, dst_row, src_row); + if (other_dirty_set.isSet(src_y)) dirty_set.set(dst_y); + } // We should remain consistent self.assertIntegrity(); From b166ca7e301bbc3a8a1d9f3ca1d2119be6cde91a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 14:16:20 -0700 Subject: [PATCH 19/24] renderer/Metal: only rebuild rows that are dirty --- src/renderer/Metal.zig | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index e4528eeca..4ac1f4a9f 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -92,6 +92,9 @@ current_background_color: terminal.color.RGB, /// cells goes into a separate shader. cells: mtl_cell.Contents, +/// If this is true, we do a full cell rebuild on the next frame. +cells_rebuild: bool = true, + /// The current GPU uniform values. uniforms: mtl_shaders.Uniforms, @@ -786,6 +789,31 @@ pub fn updateFrame( try self.prepKittyGraphics(state.terminal); } + // If we have any terminal dirty flags set then we need to rebuild + // the entire screen. This can be optimized in the future. + { + const Int = @typeInfo(terminal.Terminal.Dirty).Struct.backing_integer.?; + const v: Int = @bitCast(state.terminal.flags.dirty); + if (v > 0) self.cells_rebuild = true; + } + + // Reset the dirty flags in the terminal and screen. We assume + // that our rebuild will be successful since so we optimize for + // success and reset while we hold the lock. This is much easier + // than coordinating row by row or as changes are persisted. + state.terminal.flags.dirty = .{}; + { + var it = state.terminal.screen.pages.pageIterator( + .right_down, + .{ .screen = .{} }, + null, + ); + while (it.next()) |chunk| { + var dirty_set = chunk.page.data.dirtyBitSet(); + dirty_set.unsetAll(); + } + } + break :critical .{ .bg = self.background_color, .screen = screen_copy, @@ -1711,6 +1739,10 @@ fn rebuildCells( while (row_it.next()) |row| { y = y - 1; + // Only rebuild if we are doing a full rebuild or this row is dirty. + // if (row.isDirty()) std.log.warn("dirty y={}", .{y}); + if (!self.cells_rebuild and !row.isDirty()) continue; + // If we're rebuilding a row, then we always clear the cells self.cells.clear(y); @@ -1856,6 +1888,9 @@ fn rebuildCells( } } + // We always mark our rebuild flag as false since we're done. + self.cells_rebuild = false; + // Log some things // log.debug("rebuildCells complete cached_runs={}", .{ // self.font_shaper_cache.count(), From 037f8d3a5e6eb5c2ed698a9eee79e86a8f69b301 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 14:48:29 -0700 Subject: [PATCH 20/24] terminal: set dirty bit for screen swap --- src/terminal/Terminal.zig | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 031a399fa..176ccc0fa 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -131,6 +131,10 @@ pub const Dirty = packed struct { /// Set when the reverse colors mode is modified. reverse_colors: bool = false, + + /// Screen clear of some kind. This can be due to a screen change, + /// erase display, etc. + clear: bool = false, }; /// The event types that can be reported for mouse-related activities. @@ -2095,6 +2099,9 @@ pub fn eraseDisplay( self, .{ .all = true }, ); + + // Cleared screen dirty bit + self.flags.dirty.clear = true; }, .below => { @@ -2453,6 +2460,9 @@ pub fn alternateScreen( // Mark kitty images as dirty so they redraw self.screen.kitty_images.dirty = true; + // Mark our terminal as dirty + self.flags.dirty.clear = true; + // Bring our pen with us self.screen.cursorCopy(old.cursor) catch |err| { log.warn("cursor copy failed entering alt screen err={}", .{err}); @@ -2488,6 +2498,9 @@ pub fn primaryScreen( // Mark kitty images as dirty so they redraw self.screen.kitty_images.dirty = true; + // Mark our terminal as dirty + self.flags.dirty.clear = true; + // Restore the cursor from the primary screen. This should not // fail because we should not have to allocate memory since swapping // screens does not create new cursors. From bb138becc5208300015d2c1fd7ff3db4b92d5f70 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 14:51:20 -0700 Subject: [PATCH 21/24] terminal: resize causes full screen redraw --- src/terminal/Terminal.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 176ccc0fa..cb13e5edf 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -2383,6 +2383,9 @@ pub fn resize( } } + // Whenever we resize we just mark it as a screen clear + self.flags.dirty.clear = true; + // Set our size self.cols = cols; self.rows = rows; From 22702b69419a8b632331ff96201b5ad731152284 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 28 Apr 2024 14:57:27 -0700 Subject: [PATCH 22/24] renderer/metal: re-enable triple buffer --- src/renderer/Metal.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 4ac1f4a9f..fb956dd00 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -132,8 +132,7 @@ 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. - // TODO(mitchellh): enable triple-buffering when we improve our frame times - const BufferCount = 1; + 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. From 851b1fe2ac0cfd1e183c2caf7bd7605a2da8652e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 May 2024 10:24:41 -0700 Subject: [PATCH 23/24] font: noop shaper --- src/font/DeferredFace.zig | 26 +++++-- src/font/discovery.zig | 6 +- src/font/face.zig | 6 +- src/font/library.zig | 1 + src/font/main.zig | 7 ++ src/font/shape.zig | 6 ++ src/font/shaper/noop.zig | 143 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 188 insertions(+), 7 deletions(-) create mode 100644 src/font/shaper/noop.zig diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 8a5767518..de4293ba1 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -83,9 +83,13 @@ pub const WebCanvas = struct { pub fn deinit(self: *DeferredFace) void { switch (options.backend) { .fontconfig_freetype => if (self.fc) |*fc| fc.deinit(), - .coretext, .coretext_freetype, .coretext_harfbuzz => if (self.ct) |*ct| ct.deinit(), .freetype => {}, .web_canvas => if (self.wc) |*wc| wc.deinit(), + .coretext, + .coretext_freetype, + .coretext_harfbuzz, + .coretext_noshape, + => if (self.ct) |*ct| ct.deinit(), } self.* = undefined; } @@ -98,7 +102,11 @@ pub fn familyName(self: DeferredFace, buf: []u8) ![]const u8 { .fontconfig_freetype => if (self.fc) |fc| return (try fc.pattern.get(.family, 0)).string, - .coretext, .coretext_freetype, .coretext_harfbuzz => if (self.ct) |ct| { + .coretext, + .coretext_freetype, + .coretext_harfbuzz, + .coretext_noshape, + => if (self.ct) |ct| { const family_name = ct.font.copyAttribute(.family_name); return family_name.cstringPtr(.utf8) orelse unsupported: { break :unsupported family_name.cstring(buf, .utf8) orelse @@ -121,7 +129,11 @@ pub fn name(self: DeferredFace, buf: []u8) ![]const u8 { .fontconfig_freetype => if (self.fc) |fc| return (try fc.pattern.get(.fullname, 0)).string, - .coretext, .coretext_freetype, .coretext_harfbuzz => if (self.ct) |ct| { + .coretext, + .coretext_freetype, + .coretext_harfbuzz, + .coretext_noshape, + => if (self.ct) |ct| { const display_name = ct.font.copyDisplayName(); return display_name.cstringPtr(.utf8) orelse unsupported: { // "NULL if the internal storage of theString does not allow @@ -147,7 +159,7 @@ pub fn load( ) !Face { return switch (options.backend) { .fontconfig_freetype => try self.loadFontconfig(lib, opts), - .coretext, .coretext_harfbuzz => try self.loadCoreText(lib, opts), + .coretext, .coretext_harfbuzz, .coretext_noshape => try self.loadCoreText(lib, opts), .coretext_freetype => try self.loadCoreTextFreetype(lib, opts), .web_canvas => try self.loadWebCanvas(opts), @@ -262,7 +274,11 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { } }, - .coretext, .coretext_freetype, .coretext_harfbuzz => { + .coretext, + .coretext_freetype, + .coretext_harfbuzz, + .coretext_noshape, + => { // If we are using coretext, we check the loaded CT font. if (self.ct) |ct| { if (p) |desired_p| { diff --git a/src/font/discovery.zig b/src/font/discovery.zig index b21d374f5..0259200c9 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -14,8 +14,12 @@ const log = std.log.scoped(.discovery); pub const Discover = switch (options.backend) { .freetype => void, // no discovery .fontconfig_freetype => Fontconfig, - .coretext, .coretext_freetype, .coretext_harfbuzz => CoreText, .web_canvas => void, // no discovery + .coretext, + .coretext_freetype, + .coretext_harfbuzz, + .coretext_noshape, + => CoreText, }; /// Descriptor is used to search for fonts. The only required field diff --git a/src/font/face.zig b/src/font/face.zig index 6d5f80169..815629b44 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -13,7 +13,11 @@ pub const Face = switch (options.backend) { .coretext_freetype, => freetype.Face, - .coretext, .coretext_harfbuzz => coretext.Face, + .coretext, + .coretext_harfbuzz, + .coretext_noshape, + => coretext.Face, + .web_canvas => web_canvas.Face, }; diff --git a/src/font/library.zig b/src/font/library.zig index 8f72c4fb8..57e11e64a 100644 --- a/src/font/library.zig +++ b/src/font/library.zig @@ -16,6 +16,7 @@ pub const Library = switch (options.backend) { // Some backends such as CT and Canvas don't have a "library" .coretext, .coretext_harfbuzz, + .coretext_noshape, .web_canvas, => NoopLibrary, }; diff --git a/src/font/main.zig b/src/font/main.zig index 72c3aa9cf..a287d9a06 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -61,6 +61,9 @@ pub const Backend = enum { /// CoreText for font discovery and rendering, HarfBuzz for shaping coretext_harfbuzz, + /// CoreText for font discovery and rendering, no shaping. + coretext_noshape, + /// Use the browser font system and the Canvas API (wasm). This limits /// the available fonts to browser fonts (anything Canvas natively /// supports). @@ -97,6 +100,7 @@ pub const Backend = enum { .coretext, .coretext_harfbuzz, + .coretext_noshape, .web_canvas, => false, }; @@ -107,6 +111,7 @@ pub const Backend = enum { .coretext, .coretext_freetype, .coretext_harfbuzz, + .coretext_noshape, => true, .freetype, @@ -124,6 +129,7 @@ pub const Backend = enum { .coretext, .coretext_freetype, .coretext_harfbuzz, + .coretext_noshape, .web_canvas, => false, }; @@ -138,6 +144,7 @@ pub const Backend = enum { => true, .coretext, + .coretext_noshape, .web_canvas, => false, }; diff --git a/src/font/shape.zig b/src/font/shape.zig index 1e5dd1a9f..e633c15c9 100644 --- a/src/font/shape.zig +++ b/src/font/shape.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); const options = @import("main.zig").options; +pub const noop = @import("shaper/noop.zig"); pub const harfbuzz = @import("shaper/harfbuzz.zig"); pub const coretext = @import("shaper/coretext.zig"); pub const web_canvas = @import("shaper/web_canvas.zig"); @@ -19,6 +20,8 @@ pub const Shaper = switch (options.backend) { // font faces. .coretext => coretext.Shaper, + .coretext_noshape => noop.Shaper, + .web_canvas => web_canvas.Shaper, }; @@ -61,4 +64,7 @@ pub const Options = struct { test { _ = Cache; _ = Shaper; + + // Always test noop + _ = noop; } diff --git a/src/font/shaper/noop.zig b/src/font/shaper/noop.zig new file mode 100644 index 000000000..310b5cf40 --- /dev/null +++ b/src/font/shaper/noop.zig @@ -0,0 +1,143 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const trace = @import("tracy").trace; +const font = @import("../main.zig"); +const Face = font.Face; +const Collection = font.Collection; +const DeferredFace = font.DeferredFace; +const Group = font.Group; +const GroupCache = font.GroupCache; +const Library = font.Library; +const SharedGrid = font.SharedGrid; +const Style = font.Style; +const Presentation = font.Presentation; +const terminal = @import("../../terminal/main.zig"); + +const log = std.log.scoped(.font_shaper); + +/// Shaper that doesn't do any shaping. Each individual codepoint is mapped +/// directly to the detected text run font's glyph index. +pub const Shaper = struct { + /// The allocated used for the feature list and cell buf. + alloc: Allocator, + + /// The string used for shaping the current run. + run_state: RunState, + + /// The shared memory used for shaping results. + cell_buf: CellBuf, + + const CellBuf = std.ArrayListUnmanaged(font.shape.Cell); + const CodepointList = std.ArrayListUnmanaged(Codepoint); + const Codepoint = struct { + codepoint: u32, + cluster: u32, + }; + + const RunState = struct { + codepoints: CodepointList = .{}, + + fn deinit(self: *RunState, alloc: Allocator) void { + self.codepoints.deinit(alloc); + } + + fn reset(self: *RunState) !void { + self.codepoints.clearRetainingCapacity(); + } + }; + + /// 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(alloc: Allocator, opts: font.shape.Options) !Shaper { + _ = opts; + + return Shaper{ + .alloc = alloc, + .cell_buf = .{}, + .run_state = .{}, + }; + } + + pub fn deinit(self: *Shaper) void { + self.cell_buf.deinit(self.alloc); + self.run_state.deinit(self.alloc); + } + + pub fn runIterator( + self: *Shaper, + grid: *SharedGrid, + screen: *const terminal.Screen, + row: terminal.Pin, + selection: ?terminal.Selection, + cursor_x: ?usize, + ) font.shape.RunIterator { + return .{ + .hooks = .{ .shaper = self }, + .grid = grid, + .screen = screen, + .row = row, + .selection = selection, + .cursor_x = cursor_x, + }; + } + + pub fn shape(self: *Shaper, run: font.shape.TextRun) ![]const font.shape.Cell { + const state = &self.run_state; + + // Special fonts aren't shaped and their codepoint == glyph so we + // can just return the codepoints as-is. + if (run.font_index.special() != null) { + self.cell_buf.clearRetainingCapacity(); + try self.cell_buf.ensureTotalCapacity(self.alloc, state.codepoints.items.len); + for (state.codepoints.items) |entry| { + self.cell_buf.appendAssumeCapacity(.{ + .x = @intCast(entry.cluster), + .glyph_index = @intCast(entry.codepoint), + }); + } + + return self.cell_buf.items; + } + + // Go through the run and map each codepoint to a glyph index. + self.cell_buf.clearRetainingCapacity(); + + // Note: this is digging into some internal details, we should maybe + // expose a public API for this. + const face = try run.grid.resolver.collection.getFace(run.font_index); + for (state.codepoints.items) |entry| { + const glyph_index = face.glyphIndex(entry.codepoint); + try self.cell_buf.append(self.alloc, .{ + .x = @intCast(entry.cluster), + .glyph_index = glyph_index, + }); + } + + return self.cell_buf.items; + } + + /// The hooks for RunIterator. + pub const RunIteratorHook = struct { + shaper: *Shaper, + + pub fn prepare(self: *RunIteratorHook) !void { + try self.shaper.run_state.reset(); + } + + pub fn addCodepoint(self: RunIteratorHook, cp: u32, cluster: u32) !void { + try self.shaper.run_state.codepoints.append(self.shaper.alloc, .{ + .codepoint = cp, + .cluster = cluster, + }); + } + + pub fn finalize(self: RunIteratorHook) !void { + _ = self; + } + }; +}; + +test { + @import("std").testing.refAllDecls(@This()); +} From 7c9ce0af73a3d4684911b95451e39b6d460a2629 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 May 2024 20:41:53 -0700 Subject: [PATCH 24/24] terminal: Screen selection marks dirty --- src/renderer/Metal.zig | 5 +++++ src/terminal/Screen.zig | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index fb956dd00..0d05b7983 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -795,6 +795,11 @@ pub fn updateFrame( const v: Int = @bitCast(state.terminal.flags.dirty); if (v > 0) self.cells_rebuild = true; } + { + const Int = @typeInfo(terminal.Screen.Dirty).Struct.backing_integer.?; + const v: Int = @bitCast(state.terminal.screen.dirty); + if (v > 0) self.cells_rebuild = true; + } // Reset the dirty flags in the terminal and screen. We assume // that our rebuild will be successful since so we optimize for diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 0d2d74eb6..304bc7cce 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -62,6 +62,16 @@ kitty_keyboard: kitty.KeyFlagStack = .{}, /// Kitty graphics protocol state. kitty_images: kitty.graphics.ImageStorage = .{}, +/// Dirty flags for the renderer. +dirty: Dirty = .{}, + +/// See Terminal.Dirty. This behaves the same way. +pub const Dirty = packed struct { + /// Set when the selection is set or unset, regardless of if the + /// selection is changed or not. + selection: bool = false, +}; + /// The cursor position. pub const Cursor = struct { // The x/y position within the viewport. @@ -362,6 +372,7 @@ pub fn clonePool( .no_scrollback = self.no_scrollback, .cursor = cursor, .selection = sel, + .dirty = self.dirty, }; result.assertIntegrity(); return result; @@ -1328,12 +1339,14 @@ pub fn select(self: *Screen, sel_: ?Selection) !void { // Untrack prior selection if (self.selection) |*old| old.deinit(self); self.selection = tracked_sel; + self.dirty.selection = true; } /// Same as select(null) but can't fail. pub fn clearSelection(self: *Screen) void { if (self.selection) |*sel| sel.deinit(self); self.selection = null; + self.dirty.selection = true; } pub const SelectionString = struct {