Grid supports sending partial cell updates to GPU

This commit is contained in:
Mitchell Hashimoto
2022-08-19 12:54:07 -07:00
parent 4ca45936f7
commit 73e43b6e64
2 changed files with 227 additions and 153 deletions

View File

@ -32,6 +32,10 @@ cells: std.ArrayListUnmanaged(GPUCell),
/// accordingly. /// accordingly.
gl_cells_size: usize = 0, gl_cells_size: usize = 0,
/// The last length of the cells that was written to the GPU. This is used to
/// determine what data needs to be rewritten on the GPU.
gl_cells_written: usize = 0,
/// Shader program for cell rendering. /// Shader program for cell rendering.
program: gl.Program, program: gl.Program,
vao: gl.VertexArray, vao: gl.VertexArray,
@ -276,9 +280,14 @@ pub fn deinit(self: *Grid) void {
self.* = undefined; self.* = undefined;
} }
/// updateCells updates our GPU cells from the current terminal view. /// rebuildCells rebuilds all the GPU cells from our CPU state. This is a
/// The updated cells will take effect on the next render. /// slow operation but ensures that the GPU state exactly matches the CPU state.
pub fn updateCells(self: *Grid, term: Terminal) !void { /// In steady-state operation, we use some GPU tricks to send down stale data
/// that is ignored. This accumulates more memory; rebuildCells clears it.
///
/// Note this doesn't have to typically be manually called. Internally,
/// the renderer will do this when it needs more memory space.
pub fn rebuildCells(self: *Grid, term: Terminal) !void {
const t = trace(@src()); const t = trace(@src());
defer t.end(); defer t.end();
@ -291,16 +300,82 @@ pub fn updateCells(self: *Grid, term: Terminal) !void {
// * 3 for background modes and cursor and underlines // * 3 for background modes and cursor and underlines
// + 1 for cursor // + 1 for cursor
(term.screen.rows * term.screen.cols * 3) + 1, // * N for cache space for changes
((term.screen.rows * term.screen.cols * 3) + 1) * 10,
); );
// We've written no data to the GPU, refresh it all
self.gl_cells_written = 0;
// Build each cell // Build each cell
var rowIter = term.screen.rowIterator(.viewport); var rowIter = term.screen.rowIterator(.viewport);
var y: usize = 0; var y: usize = 0;
while (rowIter.next()) |line| { while (rowIter.next()) |line| {
defer y += 1; defer y += 1;
for (line) |cell, x| assert(try self.updateCell(term, cell, x, y));
}
// Add the cursor
self.addCursor(term);
}
/// This should be called prior to render to finalize the cells and prepare
/// for render. This performs tasks such as preparing the cursor, refreshing
/// the cells if necessary, etc.
pub fn finalizeCells(self: *Grid, term: Terminal) !void {
// Add the cursor
// TODO: only add cursor if it changed
if (self.cells.items.len < self.cells.capacity)
self.addCursor(term);
// If we're out of space, rebuild
if (self.cells.items.len == self.cells.capacity) {
log.info("cell cache full, rebuilding from scratch", .{});
try self.rebuildCells(term);
}
// If our atlas is dirty, we need to flush it
if (self.atlas_dirty) {
log.info("atlas dirty, flushing changes", .{});
try self.flushAtlas();
}
}
fn addCursor(self: *Grid, term: Terminal) void {
// Add the cursor
if (self.cursor_visible and term.screen.viewportIsBottom()) {
self.cells.appendAssumeCapacity(.{
.mode = @enumToInt(self.cursor_style),
.grid_col = @intCast(u16, term.screen.cursor.x),
.grid_row = @intCast(u16, term.screen.cursor.y),
.fg_r = 0,
.fg_g = 0,
.fg_b = 0,
.fg_a = 0,
.bg_r = 0xFF,
.bg_g = 0xFF,
.bg_b = 0xFF,
.bg_a = 255,
// The cursor is always at the very front
.grid_z = 255,
});
}
}
/// Update a single cell. The bool returns whether the cell was updated
/// or not. If the cell wasn't updated, a full refreshCells call is
/// needed.
pub fn updateCell(
self: *Grid,
term: Terminal,
cell: terminal.Screen.Cell,
x: usize,
y: usize,
) !bool {
const t = trace(@src());
defer t.end();
for (line) |cell, x| {
const BgFg = struct { const BgFg = struct {
/// Background is optional because in un-inverted mode /// Background is optional because in un-inverted mode
/// it may just be equivalent to the default background in /// it may just be equivalent to the default background in
@ -349,6 +424,16 @@ pub fn updateCells(self: *Grid, term: Terminal) !void {
break :colors res; break :colors res;
}; };
// Calculate the amount of space we need in the cells list.
const needed = needed: {
var i: usize = 0;
if (colors.bg != null) i += 1;
if (!cell.empty()) i += 1;
if (cell.attrs.underline == 1) i += 1;
break :needed i;
};
if (self.cells.items.len + needed > self.cells.capacity) return false;
// If the cell has a background, we always draw it. // If the cell has a background, we always draw it.
if (colors.bg) |rgb| { if (colors.bg) |rgb| {
self.cells.appendAssumeCapacity(.{ self.cells.appendAssumeCapacity(.{
@ -434,28 +519,8 @@ pub fn updateCells(self: *Grid, term: Terminal) !void {
.bg_a = 0, .bg_a = 0,
}); });
} }
}
}
// Draw the cursor return true;
if (self.cursor_visible and term.screen.viewportIsBottom()) {
self.cells.appendAssumeCapacity(.{
.mode = @enumToInt(self.cursor_style),
.grid_col = @intCast(u16, term.screen.cursor.x),
.grid_row = @intCast(u16, term.screen.cursor.y),
.fg_r = 0,
.fg_g = 0,
.fg_b = 0,
.fg_a = 0,
.bg_r = 0xFF,
.bg_g = 0xFF,
.bg_b = 0xFF,
.bg_a = 255,
// The cursor is always at the very front
.grid_z = 255,
});
}
} }
/// Set the screen size for rendering. This will update the projection /// Set the screen size for rendering. This will update the projection
@ -483,9 +548,7 @@ pub fn setScreenSize(self: *Grid, dim: ScreenSize) !void {
} }
/// Updates the font texture atlas if it is dirty. /// Updates the font texture atlas if it is dirty.
pub fn flushAtlas(self: *Grid) !void { fn flushAtlas(self: *Grid) !void {
if (!self.atlas_dirty) return;
var texbind = try self.texture.bind(.@"2D"); var texbind = try self.texture.bind(.@"2D");
defer texbind.unbind(); defer texbind.unbind();
try texbind.subImage2D( try texbind.subImage2D(
@ -502,6 +565,8 @@ pub fn flushAtlas(self: *Grid) !void {
self.atlas_dirty = false; self.atlas_dirty = false;
} }
/// Render renders the current cell state. This will not modify any of
/// the cells.
pub fn render(self: *Grid) !void { pub fn render(self: *Grid) !void {
const t = trace(@src()); const t = trace(@src());
defer t.end(); defer t.end();
@ -536,12 +601,21 @@ pub fn render(self: *Grid) !void {
@sizeOf(GPUCell) * self.cells.capacity, @sizeOf(GPUCell) * self.cells.capacity,
.StaticDraw, .StaticDraw,
); );
self.gl_cells_size = self.cells.capacity; self.gl_cells_size = self.cells.capacity;
self.gl_cells_written = 0;
} }
// We always set the data using subdata if possible to avoid reallocation // If we have data to write to the GPU, send it.
// on the GPU. if (self.gl_cells_written < self.cells.items.len) {
try binding.setSubData(0, self.cells.items); const data = self.cells.items[self.gl_cells_written..];
//log.info("sending {} cells to GPU", .{data.len});
try binding.setSubData(self.gl_cells_written * @sizeOf(GPUCell), data);
self.gl_cells_written += data.len;
assert(data.len > 0);
assert(self.gl_cells_written <= self.cells.items.len);
}
// Bind our texture // Bind our texture
try gl.Texture.active(gl.c.GL_TEXTURE0); try gl.Texture.active(gl.c.GL_TEXTURE0);

View File

@ -970,13 +970,13 @@ fn renderTimerCallback(t: *libuv.Timer) void {
gl.clearColor(gl_bg.r, gl_bg.g, gl_bg.b, gl_bg.a); gl.clearColor(gl_bg.r, gl_bg.g, gl_bg.b, gl_bg.a);
gl.clear(gl.c.GL_COLOR_BUFFER_BIT | gl.c.GL_DEPTH_BUFFER_BIT); gl.clear(gl.c.GL_COLOR_BUFFER_BIT | gl.c.GL_DEPTH_BUFFER_BIT);
// Update the cells for drawing // For now, rebuild all cells
win.grid.updateCells(win.terminal) catch |err| win.grid.rebuildCells(win.terminal) catch |err|
log.err("error calling updateCells in render timer err={}", .{err}); log.err("error calling rebuildCells in render timer err={}", .{err});
// Update our texture if we have to // Finalize the cells prior to render
win.grid.flushAtlas() catch |err| win.grid.finalizeCells(win.terminal) catch |err|
log.err("error calling flushAtlas in render timer err={}", .{err}); log.err("error calling updateCells in render timer err={}", .{err});
// Render the grid // Render the grid
win.grid.render() catch |err| { win.grid.render() catch |err| {