implement region scrolling directly in screen to use memcpy

This doubles scroll region scrolling speed.
This commit is contained in:
Mitchell Hashimoto
2022-11-08 16:54:39 -08:00
parent 4b2f2a81db
commit 306ab947e7
2 changed files with 183 additions and 23 deletions

View File

@ -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;

View File

@ -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.