renderer: switch to using render state

pull/9662/head
Mitchell Hashimoto 2025-11-19 05:07:07 -10:00
parent 9162e71bcc
commit ebc8bff8f1
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
3 changed files with 334 additions and 385 deletions

View File

@ -255,8 +255,12 @@ pub fn isSymbol(cp: u21) bool {
/// Returns the appropriate `constraint_width` for /// Returns the appropriate `constraint_width` for
/// the provided cell when rendering its glyph(s). /// the provided cell when rendering its glyph(s).
pub fn constraintWidth(cell_pin: terminal.Pin) u2 { pub fn constraintWidth(
const cell = cell_pin.rowAndCell().cell; raw_slice: []const terminal.page.Cell,
x: usize,
cols: usize,
) u2 {
const cell = raw_slice[x];
const cp = cell.codepoint(); const cp = cell.codepoint();
const grid_width = cell.gridWidth(); const grid_width = cell.gridWidth();
@ -271,20 +275,14 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
if (!isSymbol(cp)) return grid_width; if (!isSymbol(cp)) return grid_width;
// If we are at the end of the screen it must be constrained to one cell. // If we are at the end of the screen it must be constrained to one cell.
if (cell_pin.x == cell_pin.node.data.size.cols - 1) return 1; if (x == cols - 1) return 1;
// If we have a previous cell and it was a symbol then we need // If we have a previous cell and it was a symbol then we need
// to also constrain. This is so that multiple PUA glyphs align. // to also constrain. This is so that multiple PUA glyphs align.
// This does not apply if the previous symbol is a graphics // This does not apply if the previous symbol is a graphics
// element such as a block element or Powerline glyph. // element such as a block element or Powerline glyph.
if (cell_pin.x > 0) { if (x > 0) {
const prev_cp = prev_cp: { const prev_cp = raw_slice[x - 1].codepoint();
var copy = cell_pin;
copy.x -= 1;
const prev_cell = copy.rowAndCell().cell;
break :prev_cp prev_cell.codepoint();
};
if (isSymbol(prev_cp) and !isGraphicsElement(prev_cp)) { if (isSymbol(prev_cp) and !isGraphicsElement(prev_cp)) {
return 1; return 1;
} }
@ -292,15 +290,8 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
// If the next cell is whitespace, then we // If the next cell is whitespace, then we
// allow the glyph to be up to two cells wide. // allow the glyph to be up to two cells wide.
const next_cp = next_cp: { const next_cp = raw_slice[x + 1].codepoint();
var copy = cell_pin; if (next_cp == 0 or isSpace(next_cp)) return 2;
copy.x += 1;
const next_cell = copy.rowAndCell().cell;
break :next_cp next_cell.codepoint();
};
if (next_cp == 0 or isSpace(next_cp)) {
return 2;
}
// Otherwise, this has to be 1 cell wide. // Otherwise, this has to be 1 cell wide.
return 1; return 1;
@ -524,108 +515,171 @@ test "Cell constraint widths" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;
var s = try terminal.Screen.init(alloc, .{ .cols = 4, .rows = 1, .max_scrollback = 0 }); var t: terminal.Terminal = try .init(alloc, .{
.cols = 4,
.rows = 1,
});
defer t.deinit(alloc);
var s = t.vtStream();
defer s.deinit(); defer s.deinit();
var state: terminal.RenderState = .empty;
defer state.deinit(alloc);
// for each case, the numbers in the comment denote expected // for each case, the numbers in the comment denote expected
// constraint widths for the symbol-containing cells // constraint widths for the symbol-containing cells
// symbol->nothing: 2 // symbol->nothing: 2
{ {
try s.testWriteString(""); t.fullReset();
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; try s.nextSlice("");
try testing.expectEqual(2, constraintWidth(p0)); try state.update(alloc, &t);
s.reset(); try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
} }
// symbol->character: 1 // symbol->character: 1
{ {
try s.testWriteString("z"); t.fullReset();
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; try s.nextSlice("z");
try testing.expectEqual(1, constraintWidth(p0)); try state.update(alloc, &t);
s.reset(); try testing.expectEqual(1, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
} }
// symbol->space: 2 // symbol->space: 2
{ {
try s.testWriteString(" z"); t.fullReset();
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; try s.nextSlice(" z");
try testing.expectEqual(2, constraintWidth(p0)); try state.update(alloc, &t);
s.reset(); try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
} }
// symbol->no-break space: 1 // symbol->no-break space: 1
{ {
try s.testWriteString("\u{00a0}z"); t.fullReset();
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; try s.nextSlice("\u{00a0}z");
try testing.expectEqual(1, constraintWidth(p0)); try state.update(alloc, &t);
s.reset(); try testing.expectEqual(1, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
} }
// symbol->end of row: 1 // symbol->end of row: 1
{ {
try s.testWriteString(""); t.fullReset();
const p3 = s.pages.pin(.{ .screen = .{ .x = 3, .y = 0 } }).?; try s.nextSlice("");
try testing.expectEqual(1, constraintWidth(p3)); try state.update(alloc, &t);
s.reset(); try testing.expectEqual(1, constraintWidth(
state.row_data.get(0).cells.items(.raw),
3,
state.cols,
));
} }
// character->symbol: 2 // character->symbol: 2
{ {
try s.testWriteString("z"); t.fullReset();
const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; try s.nextSlice("z");
try testing.expectEqual(2, constraintWidth(p1)); try state.update(alloc, &t);
s.reset(); try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
1,
state.cols,
));
} }
// symbol->symbol: 1,1 // symbol->symbol: 1,1
{ {
try s.testWriteString(""); t.fullReset();
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; try s.nextSlice("");
const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; try state.update(alloc, &t);
try testing.expectEqual(1, constraintWidth(p0)); try testing.expectEqual(1, constraintWidth(
try testing.expectEqual(1, constraintWidth(p1)); state.row_data.get(0).cells.items(.raw),
s.reset(); 0,
state.cols,
));
try testing.expectEqual(1, constraintWidth(
state.row_data.get(0).cells.items(.raw),
1,
state.cols,
));
} }
// symbol->space->symbol: 2,2 // symbol->space->symbol: 2,2
{ {
try s.testWriteString(" "); t.fullReset();
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; try s.nextSlice(" ");
const p2 = s.pages.pin(.{ .screen = .{ .x = 2, .y = 0 } }).?; try state.update(alloc, &t);
try testing.expectEqual(2, constraintWidth(p0)); try testing.expectEqual(2, constraintWidth(
try testing.expectEqual(2, constraintWidth(p2)); state.row_data.get(0).cells.items(.raw),
s.reset(); 0,
state.cols,
));
try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
2,
state.cols,
));
} }
// symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg) // symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg)
{ {
try s.testWriteString(""); t.fullReset();
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; try s.nextSlice("");
try testing.expectEqual(1, constraintWidth(p0)); try state.update(alloc, &t);
s.reset(); try testing.expectEqual(1, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
} }
// powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg) // powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg)
{ {
try s.testWriteString(""); t.fullReset();
const p1 = s.pages.pin(.{ .screen = .{ .x = 1, .y = 0 } }).?; try s.nextSlice("");
try testing.expectEqual(2, constraintWidth(p1)); try state.update(alloc, &t);
s.reset(); try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
1,
state.cols,
));
} }
// powerline->nothing: 2 (dedicated test because powerline is special-cased in cellpkg) // powerline->nothing: 2 (dedicated test because powerline is special-cased in cellpkg)
{ {
try s.testWriteString(""); t.fullReset();
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; try s.nextSlice("");
try testing.expectEqual(2, constraintWidth(p0)); try state.update(alloc, &t);
s.reset(); try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
} }
// powerline->space: 2 (dedicated test because powerline is special-cased in cellpkg) // powerline->space: 2 (dedicated test because powerline is special-cased in cellpkg)
{ {
try s.testWriteString(" z"); t.fullReset();
const p0 = s.pages.pin(.{ .screen = .{ .x = 0, .y = 0 } }).?; try s.nextSlice(" z");
try testing.expectEqual(2, constraintWidth(p0)); try state.update(alloc, &t);
s.reset(); try testing.expectEqual(2, constraintWidth(
state.row_data.get(0).cells.items(.raw),
0,
state.cols,
));
} }
} }

View File

@ -125,12 +125,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
/// cells goes into a separate shader. /// cells goes into a separate shader.
cells: cellpkg.Contents, cells: cellpkg.Contents,
/// The last viewport that we based our rebuild off of. If this changes,
/// then we do a full rebuild of the cells. The pointer values in this pin
/// are NOT SAFE to read because they may be modified, freed, etc from the
/// termio thread. We treat the pointers as integers for comparison only.
cells_viewport: ?terminal.Pin = null,
/// Set to true after rebuildCells is called. This can be used /// Set to true after rebuildCells is called. This can be used
/// to determine if any possible changes have been made to the /// to determine if any possible changes have been made to the
/// cells for the draw call. /// cells for the draw call.
@ -940,8 +934,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
} }
/// Mark the full screen as dirty so that we redraw everything. /// Mark the full screen as dirty so that we redraw everything.
pub fn markDirty(self: *Self) void { pub inline fn markDirty(self: *Self) void {
self.cells_viewport = null; self.terminal_state.redraw = true;
} }
/// Called when we get an updated display ID for our display link. /// Called when we get an updated display ID for our display link.
@ -1047,7 +1041,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Force a full rebuild, because cached rows may still reference // Force a full rebuild, because cached rows may still reference
// an outdated atlas from the old grid and this can cause garbage // an outdated atlas from the old grid and this can cause garbage
// to be rendered. // to be rendered.
self.cells_viewport = null; self.markDirty();
} }
/// Update uniforms that are based on the font grid. /// Update uniforms that are based on the font grid.
@ -1070,17 +1064,12 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const Critical = struct { const Critical = struct {
bg: terminal.color.RGB, bg: terminal.color.RGB,
fg: terminal.color.RGB, fg: terminal.color.RGB,
screen: terminal.Screen,
screen_type: terminal.ScreenSet.Key,
mouse: renderer.State.Mouse, mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit, preedit: ?renderer.State.Preedit,
cursor_color: ?terminal.color.RGB, cursor_color: ?terminal.color.RGB,
cursor_style: ?renderer.CursorStyle, cursor_style: ?renderer.CursorStyle,
color_palette: terminal.color.Palette, color_palette: terminal.color.Palette,
scrollbar: terminal.Scrollbar, scrollbar: terminal.Scrollbar,
/// If true, rebuild the full screen.
full_rebuild: bool,
}; };
// Update all our data as tightly as possible within the mutex. // Update all our data as tightly as possible within the mutex.
@ -1122,19 +1111,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.{ bg, fg }; .{ bg, fg };
}; };
// Get the viewport pin so that we can compare it to the current.
const viewport_pin = state.terminal.screens.active.pages.pin(.{ .viewport = .{} }).?;
// We used to share terminal state, but we've since learned through
// analysis that it is faster to copy the terminal state than to
// hold the lock while rebuilding GPU cells.
var screen_copy = try state.terminal.screens.active.clone(
self.alloc,
.{ .viewport = .{} },
null,
);
errdefer screen_copy.deinit();
// Whether to draw our cursor or not. // Whether to draw our cursor or not.
const cursor_style = if (state.terminal.flags.password_input) const cursor_style = if (state.terminal.flags.password_input)
.lock .lock
@ -1166,77 +1142,23 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
try self.prepKittyGraphics(state.terminal); try self.prepKittyGraphics(state.terminal);
} }
// If we have any terminal dirty flags set then we need to rebuild
// the entire screen. This can be optimized in the future.
const full_rebuild: bool = rebuild: {
{
const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?;
const v: Int = @bitCast(state.terminal.flags.dirty);
if (v > 0) break :rebuild true;
}
{
const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?;
const v: Int = @bitCast(state.terminal.screens.active.dirty);
if (v > 0) break :rebuild true;
}
// If our viewport changed then we need to rebuild the entire
// screen because it means we scrolled. If we have no previous
// viewport then we must rebuild.
const prev_viewport = self.cells_viewport orelse break :rebuild true;
if (!prev_viewport.eql(viewport_pin)) break :rebuild true;
break :rebuild false;
};
// Reset the dirty flags in the terminal and screen. We assume
// that our rebuild will be successful since so we optimize for
// success and reset while we hold the lock. This is much easier
// than coordinating row by row or as changes are persisted.
state.terminal.flags.dirty = .{};
state.terminal.screens.active.dirty = .{};
{
var it = state.terminal.screens.active.pages.pageIterator(
.right_down,
.{ .viewport = .{} },
null,
);
while (it.next()) |chunk| {
chunk.node.data.dirty = false;
for (chunk.rows()) |*row| {
row.dirty = false;
}
}
}
// Update our viewport pin
self.cells_viewport = viewport_pin;
break :critical .{ break :critical .{
.bg = bg, .bg = bg,
.fg = fg, .fg = fg,
.screen = screen_copy,
.screen_type = state.terminal.screens.active_key,
.mouse = state.mouse, .mouse = state.mouse,
.preedit = preedit, .preedit = preedit,
.cursor_color = state.terminal.colors.cursor.get(), .cursor_color = state.terminal.colors.cursor.get(),
.cursor_style = cursor_style, .cursor_style = cursor_style,
.color_palette = state.terminal.colors.palette.current, .color_palette = state.terminal.colors.palette.current,
.scrollbar = scrollbar, .scrollbar = scrollbar,
.full_rebuild = full_rebuild,
}; };
}; };
defer { defer {
critical.screen.deinit();
if (critical.preedit) |p| p.deinit(self.alloc); if (critical.preedit) |p| p.deinit(self.alloc);
} }
// Build our GPU cells // Build our GPU cells
try self.rebuildCells( try self.rebuildCells(
critical.full_rebuild,
&critical.screen,
critical.screen_type,
critical.mouse,
critical.preedit, critical.preedit,
critical.cursor_style, critical.cursor_style,
&critical.color_palette, &critical.color_palette,
@ -2098,7 +2020,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
if (bg_image_config_changed) self.updateBgImageBuffer(); if (bg_image_config_changed) self.updateBgImageBuffer();
// Reset our viewport to force a rebuild, in case of a font change. // Reset our viewport to force a rebuild, in case of a font change.
self.cells_viewport = null; self.markDirty();
const blending_changed = old_blending != config.blending; const blending_changed = old_blending != config.blending;
@ -2319,95 +2241,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
} }
} }
fn rebuildCells2(
self: *Self,
) !void {
const state: *terminal.RenderState = &self.terminal_state;
self.draw_mutex.lock();
defer self.draw_mutex.unlock();
// Handle the case that our grid size doesn't match the terminal
// state grid size. It's possible our backing views for renderers
// have a mismatch temporarily since view resize is handled async
// to terminal state resize and is mostly dependent on GUI
// frameworks.
const grid_size_diff =
self.cells.size.rows != state.rows or
self.cells.size.columns != state.cols;
if (grid_size_diff) {
var new_size = self.cells.size;
new_size.rows = state.rows;
new_size.columns = state.cols;
try self.cells.resize(self.alloc, new_size);
// Update our uniforms accordingly, otherwise
// our background cells will be out of place.
self.uniforms.grid_size = .{ new_size.columns, new_size.rows };
}
// Redraw means we are redrawing the full grid, regardless of
// individual row dirtiness.
const redraw = state.redraw or grid_size_diff;
if (redraw) {
// If we are doing a full rebuild, then we clear the entire
// cell buffer.
self.cells.reset();
// We also reset our padding extension depending on the
// screen type
switch (self.config.padding_color) {
.background => {},
// For extension, assume we are extending in all directions.
// For "extend" this may be disabled due to heuristics below.
.extend, .@"extend-always" => {
self.uniforms.padding_extend = .{
.up = true,
.down = true,
.left = true,
.right = true,
};
},
}
}
// Go through all the rows and rebuild as necessary. If we have
// a size mismatch on the state and our grid we just fill what
// we can from the BOTTOM of the viewport.
const start_idx = state.rows - @min(
state.rows,
self.cells.size.rows,
);
const row_data = state.row_data.slice();
for (
0..,
row_data.items(.cells)[start_idx..],
row_data.items(.dirty)[start_idx..],
) |y, *cell, dirty| {
if (!redraw) {
// Only rebuild if we are doing a full rebuild or
// this row is dirty.
if (!dirty) continue;
// Clear the cells if the row is dirty
self.cells.clear(y);
}
_ = cell;
}
}
/// Convert the terminal state to GPU cells stored in CPU memory. These /// Convert the terminal state to GPU cells stored in CPU memory. These
/// are then synced to the GPU in the next frame. This only updates CPU /// are then synced to the GPU in the next frame. This only updates CPU
/// memory and doesn't touch the GPU. /// memory and doesn't touch the GPU.
fn rebuildCells( fn rebuildCells(
self: *Self, self: *Self,
wants_rebuild: bool,
screen: *terminal.Screen,
screen_type: terminal.ScreenSet.Key,
mouse: renderer.State.Mouse,
preedit: ?renderer.State.Preedit, preedit: ?renderer.State.Preedit,
cursor_style_: ?renderer.CursorStyle, cursor_style_: ?renderer.CursorStyle,
color_palette: *const terminal.color.Palette, color_palette: *const terminal.color.Palette,
@ -2415,6 +2253,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
foreground: terminal.color.RGB, foreground: terminal.color.RGB,
terminal_cursor_color: ?terminal.color.RGB, terminal_cursor_color: ?terminal.color.RGB,
) !void { ) !void {
const state: *terminal.RenderState = &self.terminal_state;
defer state.redraw = false;
self.draw_mutex.lock(); self.draw_mutex.lock();
defer self.draw_mutex.unlock(); defer self.draw_mutex.unlock();
@ -2426,20 +2267,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us}); // std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us});
// } // }
_ = screen_type; // we might use this again later so not deleting it yet // TODO: renderstate
// Create an arena for all our temporary allocations while rebuilding
var arena = ArenaAllocator.init(self.alloc);
defer arena.deinit();
const arena_alloc = arena.allocator();
// Create our match set for the links. // Create our match set for the links.
var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet( // var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet(
arena_alloc, // arena_alloc,
screen, // screen,
mouse_pt, // mouse_pt,
mouse.mods, // mouse.mods,
) else .{}; // ) else .{};
// Determine our x/y range for preedit. We don't want to render anything // Determine our x/y range for preedit. We don't want to render anything
// here because we will render the preedit separately. // here because we will render the preedit separately.
@ -2448,22 +2283,31 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
x: [2]terminal.size.CellCountInt, x: [2]terminal.size.CellCountInt,
cp_offset: usize, cp_offset: usize,
} = if (preedit) |preedit_v| preedit: { } = if (preedit) |preedit_v| preedit: {
const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); // We base the preedit on the position of the cursor in the
// viewport. If the cursor isn't visible in the viewport we
// don't show it.
const cursor_vp = state.cursor.viewport orelse
break :preedit null;
const range = preedit_v.range(
cursor_vp.x,
state.cols - 1,
);
break :preedit .{ break :preedit .{
.y = screen.cursor.y, .y = @intCast(cursor_vp.y),
.x = .{ range.start, range.end }, .x = .{ range.start, range.end },
.cp_offset = range.cp_offset, .cp_offset = range.cp_offset,
}; };
} else null; } else null;
const grid_size_diff = const grid_size_diff =
self.cells.size.rows != screen.pages.rows or self.cells.size.rows != state.rows or
self.cells.size.columns != screen.pages.cols; self.cells.size.columns != state.cols;
if (grid_size_diff) { if (grid_size_diff) {
var new_size = self.cells.size; var new_size = self.cells.size;
new_size.rows = screen.pages.rows; new_size.rows = state.rows;
new_size.columns = screen.pages.cols; new_size.columns = state.cols;
try self.cells.resize(self.alloc, new_size); try self.cells.resize(self.alloc, new_size);
// Update our uniforms accordingly, otherwise // Update our uniforms accordingly, otherwise
@ -2471,8 +2315,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.uniforms.grid_size = .{ new_size.columns, new_size.rows }; self.uniforms.grid_size = .{ new_size.columns, new_size.rows };
} }
const rebuild = wants_rebuild or grid_size_diff; const rebuild = state.redraw or grid_size_diff;
if (rebuild) { if (rebuild) {
// If we are doing a full rebuild, then we clear the entire cell buffer. // If we are doing a full rebuild, then we clear the entire cell buffer.
self.cells.reset(); self.cells.reset();
@ -2494,76 +2337,82 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
} }
} }
// We rebuild the cells row-by-row because we // Get our row data from our state
// do font shaping and dirty tracking by row. const row_data = state.row_data.slice();
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); const row_cells = row_data.items(.cells);
const row_dirty = row_data.items(.dirty);
const row_selection = row_data.items(.selection);
// If our cell contents buffer is shorter than the screen viewport, // If our cell contents buffer is shorter than the screen viewport,
// we render the rows that fit, starting from the bottom. If instead // we render the rows that fit, starting from the bottom. If instead
// the viewport is shorter than the cell contents buffer, we align // the viewport is shorter than the cell contents buffer, we align
// the top of the viewport with the top of the contents buffer. // the top of the viewport with the top of the contents buffer.
var y: terminal.size.CellCountInt = @min( const row_len: usize = @min(
screen.pages.rows, state.rows,
self.cells.size.rows, self.cells.size.rows,
); );
while (row_it.next()) |row| { for (
// The viewport may have more rows than our cell contents, 0..,
// so we need to break from the loop early if we hit y = 0. row_cells[0..row_len],
if (y == 0) break; row_dirty[0..row_len],
row_selection[0..row_len],
y -= 1; ) |y_usize, *cells, *dirty, selection| {
const y: terminal.size.CellCountInt = @intCast(y_usize);
if (!rebuild) { if (!rebuild) {
// Only rebuild if we are doing a full rebuild or this row is dirty. // Only rebuild if we are doing a full rebuild or this row is dirty.
if (!row.isDirty()) continue; if (!dirty.*) continue;
// Clear the cells if the row is dirty // Clear the cells if the row is dirty
self.cells.clear(y); self.cells.clear(y);
} }
// True if we want to do font shaping around the cursor. // Unmark the dirty state in our render state.
// We want to do font shaping as long as the cursor is enabled. dirty.* = false;
const shape_cursor = screen.viewportIsBottom() and
y == screen.cursor.y;
// We need to get this row's selection, if
// there is one, for proper run splitting.
const row_selection = sel: {
const sel = screen.selection orelse break :sel null;
const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse
break :sel null;
break :sel sel.containedRow(screen, pin) orelse null;
};
// TODO: renderstate
// On primary screen, we still apply vertical padding // On primary screen, we still apply vertical padding
// extension under certain conditions we feel are safe. // extension under certain conditions we feel are safe.
// //
// This helps make some scenarios look better while // This helps make some scenarios look better while
// avoiding scenarios we know do NOT look good. // avoiding scenarios we know do NOT look good.
switch (self.config.padding_color) { // switch (self.config.padding_color) {
// These already have the correct values set above. // // These already have the correct values set above.
.background, .@"extend-always" => {}, // .background, .@"extend-always" => {},
//
// Apply heuristics for padding extension. // // Apply heuristics for padding extension.
.extend => if (y == 0) { // .extend => if (y == 0) {
self.uniforms.padding_extend.up = !row.neverExtendBg( // self.uniforms.padding_extend.up = !row.neverExtendBg(
color_palette, // color_palette,
background, // background,
); // );
} else if (y == self.cells.size.rows - 1) { // } else if (y == self.cells.size.rows - 1) {
self.uniforms.padding_extend.down = !row.neverExtendBg( // self.uniforms.padding_extend.down = !row.neverExtendBg(
color_palette, // color_palette,
background, // background,
); // );
}, // },
} // }
// Iterator of runs for shaping. // Iterator of runs for shaping.
const cells_slice = cells.slice();
var run_iter_opts: font.shape.RunOptions = .{ var run_iter_opts: font.shape.RunOptions = .{
.grid = self.font_grid, .grid = self.font_grid,
.screen = screen, .cells = cells_slice,
.row = row, .selection2 = if (selection) |s| s else null,
.selection = row_selection,
.cursor_x = if (shape_cursor) screen.cursor.x else null, // We want to do font shaping as long as the cursor is
// visible on this viewport.
.cursor_x = cursor_x: {
const vp = state.cursor.viewport orelse break :cursor_x null;
if (vp.y != y) break :cursor_x null;
break :cursor_x vp.x;
},
// Old stuff
.screen = undefined,
.row = undefined,
.selection = null,
}; };
run_iter_opts.applyBreakConfig(self.config.font_shaping_break); run_iter_opts.applyBreakConfig(self.config.font_shaping_break);
var run_iter = self.font_shaper.runIterator(run_iter_opts); var run_iter = self.font_shaper.runIterator(run_iter_opts);
@ -2571,13 +2420,16 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
var shaper_cells: ?[]const font.shape.Cell = null; var shaper_cells: ?[]const font.shape.Cell = null;
var shaper_cells_i: usize = 0; var shaper_cells_i: usize = 0;
const row_cells_all = row.cells(.all);
// If our viewport is wider than our cell contents buffer, // If our viewport is wider than our cell contents buffer,
// we still only process cells up to the width of the buffer. // we still only process cells up to the width of the buffer.
const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)]; const cells_len = @min(cells_slice.len, self.cells.size.columns);
const cells_raw = cells_slice.items(.raw);
for (row_cells, 0..) |*cell, x| { const cells_style = cells_slice.items(.style);
for (
0..,
cells_raw[0..cells_len],
cells_style[0..cells_len],
) |x, *cell, *managed_style| {
// If this cell falls within our preedit range then we // If this cell falls within our preedit range then we
// skip this because preedits are setup separately. // skip this because preedits are setup separately.
if (preedit_range) |range| preedit: { if (preedit_range) |range| preedit: {
@ -2610,7 +2462,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.font_shaper_cache.get(run) orelse self.font_shaper_cache.get(run) orelse
cache: { cache: {
// Otherwise we have to shape them. // Otherwise we have to shape them.
const cells = try self.font_shaper.shape(run); const new_cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we // Try to cache them. If caching fails for any reason we
// continue because it is just a performance optimization, // continue because it is just a performance optimization,
@ -2618,7 +2470,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.font_shaper_cache.put( self.font_shaper_cache.put(
self.alloc, self.alloc,
run, run,
cells, new_cells,
) catch |err| { ) catch |err| {
log.warn( log.warn(
"error caching font shaping results err={}", "error caching font shaping results err={}",
@ -2629,49 +2481,42 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// The cells we get from direct shaping are always owned // The cells we get from direct shaping are always owned
// by the shaper and valid until the next shaping call so // by the shaper and valid until the next shaping call so
// we can safely use them. // we can safely use them.
break :cache cells; break :cache new_cells;
}; };
const cells = shaper_cells.?;
// Advance our index until we reach or pass // Advance our index until we reach or pass
// our current x position in the shaper cells. // our current x position in the shaper cells.
while (run.offset + cells[shaper_cells_i].x < x) { const shaper_cells_unwrapped = shaper_cells.?;
while (run.offset + shaper_cells_unwrapped[shaper_cells_i].x < x) {
shaper_cells_i += 1; shaper_cells_i += 1;
} }
} }
const wide = cell.wide; const wide = cell.wide;
const style: terminal.Style = if (cell.hasStyling())
const style = row.style(cell); managed_style.*
else
const cell_pin: terminal.Pin = cell: { .{};
var copy = row;
copy.x = @intCast(x);
break :cell copy;
};
// True if this cell is selected // True if this cell is selected
const selected: bool = if (screen.selection) |sel| const selected: bool = selected: {
sel.contains(screen, .{ const sel = selection orelse break :selected false;
.node = row.node, const x_compare = if (wide == .spacer_tail)
.y = row.y, x -| 1
.x = @intCast( else
// Spacer tails should show the selection x;
// state of the wide cell they belong to.
if (wide == .spacer_tail) break :selected x_compare >= sel[0] and
x -| 1 x_compare <= sel[1];
else };
x,
),
})
else
false;
// The `_style` suffixed values are the colors based on // The `_style` suffixed values are the colors based on
// the cell style (SGR), before applying any additional // the cell style (SGR), before applying any additional
// configuration, inversions, selections, etc. // configuration, inversions, selections, etc.
const bg_style = style.bg(cell, color_palette); const bg_style = style.bg(
cell,
color_palette,
);
const fg_style = style.fg(.{ const fg_style = style.fg(.{
.default = foreground, .default = foreground,
.palette = color_palette, .palette = color_palette,
@ -2793,16 +2638,18 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
continue; continue;
} }
// TODO: renderstate
// Give links a single underline, unless they already have // Give links a single underline, unless they already have
// an underline, in which case use a double underline to // an underline, in which case use a double underline to
// distinguish them. // distinguish them.
const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin)) // const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin))
if (style.flags.underline == .single) // if (style.flags.underline == .single)
.double // .double
else // else
.single // .single
else // else
style.flags.underline; // style.flags.underline;
const underline = style.flags.underline;
// We draw underlines first so that they layer underneath text. // We draw underlines first so that they layer underneath text.
// This improves readability when a colored underline is used // This improves readability when a colored underline is used
@ -2842,7 +2689,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.font_shaper_cache.get(run) orelse self.font_shaper_cache.get(run) orelse
cache: { cache: {
// Otherwise we have to shape them. // Otherwise we have to shape them.
const cells = try self.font_shaper.shape(run); const new_cells = try self.font_shaper.shape(run);
// Try to cache them. If caching fails for any reason we // Try to cache them. If caching fails for any reason we
// continue because it is just a performance optimization, // continue because it is just a performance optimization,
@ -2850,7 +2697,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.font_shaper_cache.put( self.font_shaper_cache.put(
self.alloc, self.alloc,
run, run,
cells, new_cells,
) catch |err| { ) catch |err| {
log.warn( log.warn(
"error caching font shaping results err={}", "error caching font shaping results err={}",
@ -2861,32 +2708,34 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// The cells we get from direct shaping are always owned // The cells we get from direct shaping are always owned
// by the shaper and valid until the next shaping call so // by the shaper and valid until the next shaping call so
// we can safely use them. // we can safely use them.
break :cache cells; break :cache new_cells;
}; };
const cells = shaper_cells orelse break :glyphs; const shaped_cells = shaper_cells orelse break :glyphs;
// If there are no shaper cells for this run, ignore it. // If there are no shaper cells for this run, ignore it.
// This can occur for runs of empty cells, and is fine. // This can occur for runs of empty cells, and is fine.
if (cells.len == 0) break :glyphs; if (shaped_cells.len == 0) break :glyphs;
// If we encounter a shaper cell to the left of the current // If we encounter a shaper cell to the left of the current
// cell then we have some problems. This logic relies on x // cell then we have some problems. This logic relies on x
// position monotonically increasing. // position monotonically increasing.
assert(run.offset + cells[shaper_cells_i].x >= x); assert(run.offset + shaped_cells[shaper_cells_i].x >= x);
// NOTE: An assumption is made here that a single cell will never // NOTE: An assumption is made here that a single cell will never
// be present in more than one shaper run. If that assumption is // be present in more than one shaper run. If that assumption is
// violated, this logic breaks. // violated, this logic breaks.
while (shaper_cells_i < cells.len and run.offset + cells[shaper_cells_i].x == x) : ({ while (shaper_cells_i < shaped_cells.len and
run.offset + shaped_cells[shaper_cells_i].x == x) : ({
shaper_cells_i += 1; shaper_cells_i += 1;
}) { }) {
self.addGlyph( self.addGlyph(
@intCast(x), @intCast(x),
@intCast(y), @intCast(y),
cell_pin, state.cols,
cells[shaper_cells_i], cells_raw,
shaped_cells[shaper_cells_i],
shaper_run.?, shaper_run.?,
fg, fg,
alpha, alpha,
@ -2938,14 +2787,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
inline .@"cell-foreground", inline .@"cell-foreground",
.@"cell-background", .@"cell-background",
=> |_, tag| { => |_, tag| {
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); const sty: terminal.Style = state.cursor.style;
const fg_style = sty.fg(.{ const fg_style = sty.fg(.{
.default = foreground, .default = foreground,
.palette = color_palette, .palette = color_palette,
.bold = self.config.bold_color, .bold = self.config.bold_color,
}); });
const bg_style = sty.bg( const bg_style = sty.bg(
screen.cursor.page_cell, &state.cursor.cell,
color_palette, color_palette,
) orelse background; ) orelse background;
@ -2960,21 +2809,27 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
break :cursor_color foreground; break :cursor_color foreground;
}; };
self.addCursor(screen, style, cursor_color); self.addCursor(
&state.cursor,
style,
cursor_color,
);
// If the cursor is visible then we set our uniforms. // If the cursor is visible then we set our uniforms.
if (style == .block and screen.viewportIsBottom()) { if (style == .block) cursor_uniforms: {
const wide = screen.cursor.page_cell.wide; const cursor_vp = state.cursor.viewport orelse
break :cursor_uniforms;
const wide = state.cursor.cell.wide;
self.uniforms.cursor_pos = .{ self.uniforms.cursor_pos = .{
// If we are a spacer tail of a wide cell, our cursor needs // If we are a spacer tail of a wide cell, our cursor needs
// to move back one cell. The saturate is to ensure we don't // to move back one cell. The saturate is to ensure we don't
// overflow but this shouldn't happen with well-formed input. // overflow but this shouldn't happen with well-formed input.
switch (wide) { switch (wide) {
.narrow, .spacer_head, .wide => screen.cursor.x, .narrow, .spacer_head, .wide => cursor_vp.x,
.spacer_tail => screen.cursor.x -| 1, .spacer_tail => cursor_vp.x -| 1,
}, },
screen.cursor.y, @intCast(cursor_vp.y),
}; };
self.uniforms.bools.cursor_wide = switch (wide) { self.uniforms.bools.cursor_wide = switch (wide) {
@ -2990,14 +2845,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
break :blk txt.color.toTerminalRGB(); break :blk txt.color.toTerminalRGB();
} }
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); const sty = state.cursor.style;
const fg_style = sty.fg(.{ const fg_style = sty.fg(.{
.default = foreground, .default = foreground,
.palette = color_palette, .palette = color_palette,
.bold = self.config.bold_color, .bold = self.config.bold_color,
}); });
const bg_style = sty.bg( const bg_style = sty.bg(
screen.cursor.page_cell, &state.cursor.cell,
color_palette, color_palette,
) orelse background; ) orelse background;
@ -3157,15 +3012,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self: *Self, self: *Self,
x: terminal.size.CellCountInt, x: terminal.size.CellCountInt,
y: terminal.size.CellCountInt, y: terminal.size.CellCountInt,
cell_pin: terminal.Pin, cols: usize,
cell_raws: []const terminal.page.Cell,
shaper_cell: font.shape.Cell, shaper_cell: font.shape.Cell,
shaper_run: font.shape.TextRun, shaper_run: font.shape.TextRun,
color: terminal.color.RGB, color: terminal.color.RGB,
alpha: u8, alpha: u8,
) !void { ) !void {
const rac = cell_pin.rowAndCell(); const cell = cell_raws[x];
const cell = rac.cell;
const cp = cell.codepoint(); const cp = cell.codepoint();
// Render // Render
@ -3185,7 +3039,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
if (cellpkg.isSymbol(cp)) .{ if (cellpkg.isSymbol(cp)) .{
.size = .fit, .size = .fit,
} else .none, } else .none,
.constraint_width = constraintWidth(cell_pin), .constraint_width = constraintWidth(
cell_raws,
x,
cols,
),
}, },
); );
@ -3214,22 +3072,24 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
fn addCursor( fn addCursor(
self: *Self, self: *Self,
screen: *terminal.Screen, cursor_state: *const terminal.RenderState.Cursor,
cursor_style: renderer.CursorStyle, cursor_style: renderer.CursorStyle,
cursor_color: terminal.color.RGB, cursor_color: terminal.color.RGB,
) void { ) void {
const cursor_vp = cursor_state.viewport orelse return;
// Add the cursor. We render the cursor over the wide character if // Add the cursor. We render the cursor over the wide character if
// we're on the wide character tail. // we're on the wide character tail.
const wide, const x = cell: { const wide, const x = cell: {
// The cursor goes over the screen cursor position. // The cursor goes over the screen cursor position.
const cell = screen.cursor.page_cell; if (!cursor_vp.wide_tail) break :cell .{
if (cell.wide != .spacer_tail or screen.cursor.x == 0) cursor_state.cell.wide == .wide,
break :cell .{ cell.wide == .wide, screen.cursor.x }; cursor_vp.x,
};
// If we're part of a wide character, we move the cursor back to // If we're part of a wide character, we move the cursor back
// the actual character. // to the actual character.
const prev_cell = screen.cursorCellLeft(1); break :cell .{ true, cursor_vp.x - 1 };
break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 };
}; };
const alpha: u8 = if (!self.focused) 255 else alpha: { const alpha: u8 = if (!self.focused) 255 else alpha: {
@ -3288,7 +3148,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
self.cells.setCursor(.{ self.cells.setCursor(.{
.atlas = .grayscale, .atlas = .grayscale,
.bools = .{ .is_cursor_glyph = true }, .bools = .{ .is_cursor_glyph = true },
.grid_pos = .{ x, screen.cursor.y }, .grid_pos = .{ x, cursor_vp.y },
.color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha }, .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha },
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
.glyph_size = .{ render.glyph.width, render.glyph.height }, .glyph_size = .{ render.glyph.width, render.glyph.height },

View File

@ -6,6 +6,7 @@ const fastmem = @import("../fastmem.zig");
const point = @import("point.zig"); const point = @import("point.zig");
const size = @import("size.zig"); const size = @import("size.zig");
const page = @import("page.zig"); const page = @import("page.zig");
const Pin = @import("PageList.zig").Pin;
const Screen = @import("Screen.zig"); const Screen = @import("Screen.zig");
const ScreenSet = @import("ScreenSet.zig"); const ScreenSet = @import("ScreenSet.zig");
const Style = @import("style.zig").Style; const Style = @import("style.zig").Style;
@ -68,6 +69,11 @@ pub const RenderState = struct {
/// to detect changes. /// to detect changes.
screen: ScreenSet.Key, screen: ScreenSet.Key,
/// The last viewport pin used to generate this state. This is NOT
/// a tracked pin and is generally NOT safe to read other than the direct
/// values for comparison.
viewport_pin: ?Pin = null,
/// Initial state. /// Initial state.
pub const empty: RenderState = .{ pub const empty: RenderState = .{
.rows = 0, .rows = 0,
@ -90,7 +96,7 @@ pub const RenderState = struct {
/// The x/y position of the cursor within the viewport. This /// The x/y position of the cursor within the viewport. This
/// may be null if the cursor is not visible within the viewport. /// may be null if the cursor is not visible within the viewport.
viewport: ?point.Coordinate, viewport: ?Viewport,
/// The cell data for the cursor position. Managed memory is not /// The cell data for the cursor position. Managed memory is not
/// safe to access from this. /// safe to access from this.
@ -98,6 +104,17 @@ pub const RenderState = struct {
/// The style, always valid even if the cell is default style. /// The style, always valid even if the cell is default style.
style: Style, style: Style,
pub const Viewport = struct {
/// The x/y position of the cursor within the viewport.
x: size.CellCountInt,
y: size.CellCountInt,
/// Whether the cursor is part of a wide character and
/// on the tail of it. If so, some renderers may use this
/// to move the cursor back one.
wide_tail: bool,
};
}; };
/// A row within the viewport. /// A row within the viewport.
@ -118,7 +135,7 @@ pub const RenderState = struct {
dirty: bool, dirty: bool,
/// The x range of the selection within this row. /// The x range of the selection within this row.
selection: [2]size.CellCountInt, selection: ?[2]size.CellCountInt,
}; };
pub const Cell = struct { pub const Cell = struct {
@ -159,6 +176,7 @@ pub const RenderState = struct {
t: *Terminal, t: *Terminal,
) Allocator.Error!void { ) Allocator.Error!void {
const s: *Screen = t.screens.active; const s: *Screen = t.screens.active;
const viewport_pin = s.pages.getTopLeft(.viewport);
const redraw = redraw: { const redraw = redraw: {
// If our screen key changed, we need to do a full rebuild // If our screen key changed, we need to do a full rebuild
// because our render state is viewport-specific. // because our render state is viewport-specific.
@ -187,6 +205,11 @@ pub const RenderState = struct {
break :redraw true; break :redraw true;
} }
// If our viewport pin changed, we do a full rebuild.
if (self.viewport_pin) |old| {
if (!old.eql(viewport_pin)) break :redraw true;
}
break :redraw false; break :redraw false;
}; };
@ -203,6 +226,7 @@ pub const RenderState = struct {
self.rows = s.pages.rows; self.rows = s.pages.rows;
self.cols = s.pages.cols; self.cols = s.pages.cols;
self.viewport_is_bottom = s.viewportIsBottom(); self.viewport_is_bottom = s.viewportIsBottom();
self.viewport_pin = viewport_pin;
self.cursor.active = .{ .x = s.cursor.x, .y = s.cursor.y }; self.cursor.active = .{ .x = s.cursor.x, .y = s.cursor.y };
self.cursor.cell = s.cursor.page_cell.*; self.cursor.cell = s.cursor.page_cell.*;
self.cursor.style = s.cursor.page_pin.style(s.cursor.page_cell); self.cursor.style = s.cursor.page_pin.style(s.cursor.page_cell);
@ -232,7 +256,7 @@ pub const RenderState = struct {
.arena = .{}, .arena = .{},
.cells = .empty, .cells = .empty,
.dirty = true, .dirty = true,
.selection = .{ 0, 0 }, .selection = null,
}); });
} }
} else { } else {
@ -272,11 +296,22 @@ pub const RenderState = struct {
self.cursor.viewport = .{ self.cursor.viewport = .{
.y = y, .y = y,
.x = s.cursor.x, .x = s.cursor.x,
// Future: we should use our own state here to look this
// up rather than calling this.
.wide_tail = if (s.cursor.x > 0)
s.cursorCellLeft(1).wide == .wide
else
false,
}; };
} }
// If the row isn't dirty then we assume it is unchanged. // If the row isn't dirty then we assume it is unchanged.
if (!redraw and !row_pin.isDirty()) continue; var dirty_set = row_pin.node.data.dirtyBitSet();
if (!redraw and !dirty_set.isSet(row_pin.y)) continue;
// Clear the dirty flag on the row
dirty_set.unset(row_pin.y);
// Promote our arena. State is copied by value so we need to // Promote our arena. State is copied by value so we need to
// restore it on all exit paths so we don't leak memory. // restore it on all exit paths so we don't leak memory.