mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 07:46:12 +03:00
fix(renderer): clip terminal contents to expected grid size (#4523)
This significantly improves the robustness of the renderers since it prevents synchronization issues from causing memory corruption due to out of bounds read/writes while building the cells. TODO: when viewport is narrower than renderer grid size, fill blank margin with bg color- currently appears as black, this only affects DECCOLM right now, and possibly could create single-frame artefacts during poorly managed resizes, but it's not ideal regardless.
This commit is contained in:
@ -1046,19 +1046,6 @@ pub fn updateFrame(
|
||||
}
|
||||
}
|
||||
|
||||
// If our terminal screen size doesn't match our expected renderer
|
||||
// size then we skip a frame. This can happen if the terminal state
|
||||
// is resized between when the renderer mailbox is drained and when
|
||||
// the state mutex is acquired inside this function.
|
||||
//
|
||||
// For some reason this doesn't seem to cause any significant issues
|
||||
// with flickering while resizing. '\_('-')_/'
|
||||
if (self.cells.size.rows != state.terminal.rows or
|
||||
self.cells.size.columns != state.terminal.cols)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the viewport pin so that we can compare it to the current.
|
||||
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
|
||||
|
||||
@ -2437,12 +2424,22 @@ fn rebuildCells(
|
||||
}
|
||||
}
|
||||
|
||||
// Go row-by-row to build the cells. We go row by row because we do
|
||||
// font shaping by row. In the future, we will also do dirty tracking
|
||||
// by row.
|
||||
// We rebuild the cells row-by-row because we
|
||||
// do font shaping and dirty tracking by row.
|
||||
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
|
||||
var y: terminal.size.CellCountInt = screen.pages.rows;
|
||||
// If our cell contents buffer is shorter than the screen viewport,
|
||||
// we render the rows that fit, starting from the bottom. If instead
|
||||
// the viewport is shorter than the cell contents buffer, we align
|
||||
// the top of the viewport with the top of the contents buffer.
|
||||
var y: terminal.size.CellCountInt = @min(
|
||||
screen.pages.rows,
|
||||
self.cells.size.rows,
|
||||
);
|
||||
while (row_it.next()) |row| {
|
||||
// The viewport may have more rows than our cell contents,
|
||||
// so we need to break from the loop early if we hit y = 0.
|
||||
if (y == 0) break;
|
||||
|
||||
y -= 1;
|
||||
|
||||
if (!rebuild) {
|
||||
@ -2501,7 +2498,11 @@ fn rebuildCells(
|
||||
var shaper_cells: ?[]const font.shape.Cell = null;
|
||||
var shaper_cells_i: usize = 0;
|
||||
|
||||
const row_cells = row.cells(.all);
|
||||
const row_cells_all = row.cells(.all);
|
||||
|
||||
// If our viewport is wider than our cell contents 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)];
|
||||
|
||||
for (row_cells, 0..) |*cell, x| {
|
||||
// If this cell falls within our preedit range then we
|
||||
|
@ -706,8 +706,6 @@ pub fn updateFrame(
|
||||
|
||||
// Update all our data as tightly as possible within the mutex.
|
||||
var critical: Critical = critical: {
|
||||
const grid_size = self.size.grid();
|
||||
|
||||
state.mutex.lock();
|
||||
defer state.mutex.unlock();
|
||||
|
||||
@ -748,19 +746,6 @@ pub fn updateFrame(
|
||||
}
|
||||
}
|
||||
|
||||
// If our terminal screen size doesn't match our expected renderer
|
||||
// size then we skip a frame. This can happen if the terminal state
|
||||
// is resized between when the renderer mailbox is drained and when
|
||||
// the state mutex is acquired inside this function.
|
||||
//
|
||||
// For some reason this doesn't seem to cause any significant issues
|
||||
// with flickering while resizing. '\_('-')_/'
|
||||
if (grid_size.rows != state.terminal.rows or
|
||||
grid_size.columns != state.terminal.cols)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the viewport pin so that we can compare it to the current.
|
||||
const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?;
|
||||
|
||||
@ -1276,10 +1261,23 @@ pub fn rebuildCells(
|
||||
}
|
||||
}
|
||||
|
||||
// Build each cell
|
||||
const grid_size = self.size.grid();
|
||||
|
||||
// We rebuild the cells row-by-row because we do font shaping by row.
|
||||
var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null);
|
||||
var y: terminal.size.CellCountInt = screen.pages.rows;
|
||||
// If our cell contents buffer is shorter than the screen viewport,
|
||||
// we render the rows that fit, starting from the bottom. If instead
|
||||
// the viewport is shorter than the cell contents buffer, we align
|
||||
// the top of the viewport with the top of the contents buffer.
|
||||
var y: terminal.size.CellCountInt = @min(
|
||||
screen.pages.rows,
|
||||
grid_size.rows,
|
||||
);
|
||||
while (row_it.next()) |row| {
|
||||
// The viewport may have more rows than our cell contents,
|
||||
// so we need to break from the loop early if we hit y = 0.
|
||||
if (y == 0) break;
|
||||
|
||||
y -= 1;
|
||||
|
||||
// True if we want to do font shaping around the cursor. We want to
|
||||
@ -1356,7 +1354,11 @@ pub fn rebuildCells(
|
||||
var shaper_cells: ?[]const font.shape.Cell = null;
|
||||
var shaper_cells_i: usize = 0;
|
||||
|
||||
const row_cells = row.cells(.all);
|
||||
const row_cells_all = row.cells(.all);
|
||||
|
||||
// If our viewport is wider than our cell contents 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, grid_size.columns)];
|
||||
|
||||
for (row_cells, 0..) |*cell, x| {
|
||||
// If this cell falls within our preedit range then we
|
||||
|
Reference in New Issue
Block a user