From 306ab947e7539fac216f4891890f3af14382966c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 8 Nov 2022 16:54:39 -0800 Subject: [PATCH] implement region scrolling directly in screen to use memcpy This doubles scroll region scrolling speed. --- src/terminal/Screen.zig | 182 ++++++++++++++++++++++++++++++++++++-- src/terminal/Terminal.zig | 24 ++--- 2 files changed, 183 insertions(+), 23 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 64b9b72cf..9eec662c0 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -827,12 +827,17 @@ pub fn getRow(self: *Screen, index: RowIndex) Row { // Store the header row.storage[0].header.id = id; - // Mark that we're dirty since we're a new row - row.storage[0].header.flags.dirty = true; + // We only set dirty and fill if its not dirty. If its dirty + // we assume this row has been written but just hasn't had + // an ID assigned yet. + if (!row.storage[0].header.flags.dirty) { + // Mark that we're dirty since we're a new row + row.storage[0].header.flags.dirty = true; - // We only need to fill with runtime safety because unions are - // tag-checked. Otherwise, the default value of zero will be valid. - if (std.debug.runtime_safety) row.fill(.{}); + // We only need to fill with runtime safety because unions are + // tag-checked. Otherwise, the default value of zero will be valid. + if (std.debug.runtime_safety) row.fill(.{}); + } } return row; } @@ -846,6 +851,86 @@ pub fn copyRow(self: *Screen, dst: RowIndex, src: RowIndex) !void { try dst_row.copyRow(src_row); } +/// Scroll rows in a region up. Rows that go beyond the region +/// top or bottom are deleted, and new rows inserted are blank according +/// to the current pen. +/// +/// This does NOT create any new scrollback. This modifies an existing +/// region within the screen (including possibly the scrollback if +/// the top/bottom are within it). +/// +/// This can be used to implement terminal scroll regions efficiently. +pub fn scrollRegionUp(self: *Screen, top: RowIndex, bottom: RowIndex, count: usize) void { + // Avoid a lot of work if we're doing nothing. + if (count == 0) return; + + // Convert our top/bottom to screen y values. This is the y offset + // in the entire screen buffer. + const top_y = top.toScreen(self).screen; + const bot_y = bottom.toScreen(self).screen; + assert(bot_y > top_y); + assert(count <= (bot_y - top_y)); + + // Get the storage pointer for the full scroll region. We're going to + // be modifying the whole thing so we get it right away. + const height = bot_y - top_y; + const slices = self.storage.getPtrSlice( + top_y * (self.cols + 1), + (height * (self.cols + 1)) + self.cols + 1, + ); + + // Fast-path is that we have a contigous buffer in our circular buffer. + // In this case we can do some memmoves. + if (slices[1].len == 0) { + const buf = slices[0]; + + { + // Our copy starts "count" rows below and is the length of + // the remainder of the data. Our destination is the top since + // we're scrolling up. + // + // Note we do NOT need to set any row headers to dirty because + // the row contents are not changing for the row ID. + const dst = buf; + const src_offset = count * (self.cols + 1); + const src = buf[src_offset..]; + assert(@ptrToInt(dst.ptr) < @ptrToInt(src.ptr)); + std.mem.copy(StorageCell, dst, src); + } + + { + // Copy in our empties. The destination is the bottom + // count rows. We first fill with the pen values since there + // is a lot more of that. + const dst_offset = (height - (count - 1)) * (self.cols + 1); + const dst = buf[dst_offset..]; + std.mem.set(StorageCell, dst, .{ .cell = self.cursor.pen }); + + // Then we make sure our row headers are zeroed out. We set + // the value to a dirty row header so that the renderer re-draws. + // + // NOTE: we do NOT set a valid row ID here. The next time getRow + // is called it will be initialized. This should work fine as + // far as I can tell. It is important to set dirty so that the + // renderer knows to redraw this. + var i: usize = dst_offset; + while (i < buf.len) : (i += self.cols + 1) { + buf[i] = .{ .header = .{ + .flags = .{ .dirty = true }, + } }; + } + } + + return; + } + + // If we're split across two buffers this is a "slow" path. + // This isn't possible with the /active/ area of the screen so mark + // it as unreachable for now but this is something we need to fix. + + unreachable; +} + /// Returns the offset into the storage buffer that the given row can /// be found. This assumes valid input and will crash if the input is /// invalid. @@ -2163,6 +2248,93 @@ test "Screen: row copy" { try testing.expectEqualStrings("2EFGH\n3IJKL\n2EFGH", contents); } +test "Screen: scrollRegionUp single" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 4, 5, 0); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); + + s.scrollRegionUp(.{ .active = 1 }, .{ .active = 2 }, 1); + { + // Test our contents rotated + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n3IJKL\n\n4ABCD", contents); + } +} + +test "Screen: scrollRegionUp single with pen" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 4, 5, 0); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); + + s.cursor.pen = .{ .char = 'X' }; + s.scrollRegionUp(.{ .active = 1 }, .{ .active = 2 }, 1); + { + // Test our contents rotated + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n3IJKL\nXXXXX\n4ABCD", contents); + } +} + +test "Screen: scrollRegionUp multiple" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 4, 5, 0); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); + + s.scrollRegionUp(.{ .active = 1 }, .{ .active = 3 }, 1); + { + // Test our contents rotated + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n3IJKL\n4ABCD", contents); + } +} + +test "Screen: scrollRegionUp multiple count" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 4, 5, 0); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); + + s.scrollRegionUp(.{ .active = 1 }, .{ .active = 3 }, 2); + { + // Test our contents rotated + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings("1ABCD\n4ABCD", contents); + } +} + +test "Screen: scrollRegionUp foo" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 4, 5, 0); + defer s.deinit(); + try s.testWriteString("A\nB\nC\nD"); + + s.cursor.pen = .{ .char = 'X' }; + s.scrollRegionUp(.{ .active = 0 }, .{ .active = 2 }, 1); + { + // Test our contents rotated + var contents = try s.testString(alloc, .screen); + defer alloc.free(contents); + try testing.expectEqualStrings("B\nC\nXXXXX\nD", contents); + } +} + test "Screen: clear history with no history" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 7de7f7e56..b2f0b7303 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1216,27 +1216,15 @@ pub fn deleteLines(self: *Terminal, count: usize) !void { const tracy = trace(@src()); defer tracy.end(); - // TODO: scroll region bounds - // Move the cursor to the left margin self.screen.cursor.x = 0; - // Remaining number of lines in the scrolling region - const rem = self.scrolling_region.bottom - self.screen.cursor.y + 1; - - // If the count is more than our remaining lines, we adjust down. - const adjusted_count = @min(count, rem); - - // Scroll up the count amount. - var y: usize = self.screen.cursor.y; - while (y <= self.scrolling_region.bottom - adjusted_count) : (y += 1) { - try self.screen.copyRow(.{ .active = y }, .{ .active = y + adjusted_count }); - } - - while (y <= self.scrolling_region.bottom) : (y += 1) { - const row = self.screen.getRow(.{ .active = y }); - row.fill(self.screen.cursor.pen); - } + // Perform the scroll + self.screen.scrollRegionUp( + .{ .active = self.screen.cursor.y }, + .{ .active = self.scrolling_region.bottom }, + count, + ); } /// Scroll the text down by one row.