From 58bc6864cb84ac6504370854836ec80855edfed5 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Fri, 19 Jan 2024 13:13:20 -0600 Subject: [PATCH] render: implement double buffered screen for rendering This lets us efficiently render by only updating cells that have changed since last render Signed-off-by: Tim Culverhouse --- examples/main.zig | 1 + src/Screen.zig | 17 +++++++---------- src/Tty.zig | 5 +++++ src/Window.zig | 10 ++++++++++ src/vaxis.zig | 39 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 59 insertions(+), 13 deletions(-) diff --git a/examples/main.zig b/examples/main.zig index 1f3da0b..3a21ee5 100644 --- a/examples/main.zig +++ b/examples/main.zig @@ -39,6 +39,7 @@ pub fn main() !void { } const win = vx.window(); + win.clear(); const child = win.initChild(win.width / 2 - msg.len / 2, win.height / 2, .expand, .expand); for (msg, 0..) |_, i| { const cell: Cell = .{ .char = .{ .grapheme = msg[i .. i + 1] } }; diff --git a/src/Screen.zig b/src/Screen.zig index 1151024..6241814 100644 --- a/src/Screen.zig +++ b/src/Screen.zig @@ -7,16 +7,16 @@ const log = std.log.scoped(.screen); const Screen = @This(); -width: usize, -height: usize, +width: usize = 0, +height: usize = 0, buf: []Cell = undefined, -pub fn init() Screen { - return Screen{ - .width = 0, - .height = 0, - }; +/// sets each cell to the default cell +pub fn init(self: *Screen) void { + for (self.buf, 0..) |_, i| { + self.buf[i] = .{}; + } } pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void { @@ -27,9 +27,6 @@ pub fn resize(self: *Screen, alloc: std.mem.Allocator, w: usize, h: usize) !void log.debug("resizing screen: width={d} height={d}", .{ w, h }); alloc.free(self.buf); self.buf = try alloc.alloc(Cell, w * h); - for (self.buf, 0..) |_, i| { - self.buf[i] = .{}; - } self.width = w; self.height = h; } diff --git a/src/Tty.zig b/src/Tty.zig index e8996c1..dc0bbe5 100644 --- a/src/Tty.zig +++ b/src/Tty.zig @@ -173,6 +173,11 @@ pub fn run( } } +const Writer = std.io.Writer(os.fd_t, os.WriteError, os.write); + +pub fn writer(self: *Tty) Writer { + return .{ .context = self.fd }; +} /// write to the tty // // TODO: buffer the writes diff --git a/src/Window.zig b/src/Window.zig index da1fc1f..3321bb3 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -67,6 +67,16 @@ pub fn writeCell(self: Window, col: usize, row: usize, cell: Cell) void { self.screen.writeCell(col + self.x_off, row + self.y_off, cell); } +pub fn clear(self: Window) void { + var row: usize = self.y_off; + while (row < (self.height + self.y_off)) : (row += 1) { + var col: usize = self.x_off; + while (col < (self.width + self.x_off)) : (col += 1) { + self.screen.writeCell(col, row, .{}); + } + } +} + test "Window size set" { var parent = Window{ .x_off = 0, diff --git a/src/vaxis.zig b/src/vaxis.zig index a9f8413..7296170 100644 --- a/src/vaxis.zig +++ b/src/vaxis.zig @@ -34,6 +34,9 @@ pub fn Vaxis(comptime T: type) type { tty: ?Tty, screen: Screen, + // The last screen we drew. We keep this so we can efficiently update on + // the next render + screen_last: Screen, alt_screen: bool, @@ -42,7 +45,8 @@ pub fn Vaxis(comptime T: type) type { return Self{ .queue = .{}, .tty = null, - .screen = Screen.init(), + .screen = .{}, + .screen_last = .{}, .alt_screen = false, }; } @@ -59,7 +63,10 @@ pub fn Vaxis(comptime T: type) type { } tty.deinit(); } - if (alloc) |a| self.screen.deinit(a); + if (alloc) |a| { + self.screen.deinit(a); + self.screen_last.deinit(a); + } } /// spawns the input thread to start listening to the tty for input @@ -94,6 +101,10 @@ pub fn Vaxis(comptime T: type) type { /// freed when resizing pub fn resize(self: *Self, alloc: std.mem.Allocator, winsize: Winsize) !void { try self.screen.resize(alloc, winsize.cols, winsize.rows); + // we only init our current screen. This has the effect of redrawing + // every cell + self.screen.init(); + try self.screen_last.resize(alloc, winsize.cols, winsize.rows); } /// returns a Window comprising of the entire terminal screen @@ -127,8 +138,30 @@ pub fn Vaxis(comptime T: type) type { pub fn render(self: *Self) !void { var tty = self.tty orelse return; + // TODO: optimize writes + + // Send the cursor to 0,0 _ = try tty.write(ctlseqs.home); - for (self.screen.buf) |cell| { + var reposition: bool = false; + var row: usize = 0; + var col: usize = 0; + for (self.screen.buf, 0..) |cell, i| { + col += 1; + if (col == self.screen.width) { + row += 1; + col = 0; + } + // If cell is the same as our last frame, we don't need to do + // anything + if (std.meta.eql(cell, self.screen_last.buf[i])) { + reposition = true; + continue; + } + // Set this cell in the last frame + self.screen_last.buf[i] = cell; + if (reposition) { + try std.fmt.format(tty.writer(), ctlseqs.cup, .{ row + 1, col + 1 }); + } _ = try tty.write(cell.char.grapheme); } }