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 <tim@timculverhouse.com>
This commit is contained in:
Tim Culverhouse 2024-01-19 13:13:20 -06:00
parent 1e7c82fe44
commit 58bc6864cb
5 changed files with 59 additions and 13 deletions

View file

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

View file

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

View file

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

View file

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

View file

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