Skip to content

Commit b414a5e

Browse files
committed
renderer: simplify cell row storage
Cell contents used our ArrayListCollection container to manage per-row foreground lists. This was the only place ArrayListCollection was used. We now own the row list slice directly, initialize cursor capacity to exactly one cell, and reallocate the contiguous background buffer in place when possible. Foreground rows still use exact sizes so resizes (which are infrequent) do not retain the high-water mark.
1 parent afa9e4f commit b414a5e

3 files changed

Lines changed: 91 additions & 109 deletions

File tree

src/datastruct/array_list_collection.zig

Lines changed: 0 additions & 44 deletions
This file was deleted.

src/renderer/cell.zig

Lines changed: 90 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ const font = @import("../font/main.zig");
55
const terminal = @import("../terminal/main.zig");
66
const renderer = @import("../renderer.zig");
77
const shaderpkg = renderer.Renderer.API.shaders;
8-
const ArrayListCollection = @import("../datastruct/array_list_collection.zig").ArrayListCollection;
98
const symbols = @import("../unicode/symbols_table.zig").table;
109

10+
const CellTextRow = std.ArrayListUnmanaged(shaderpkg.CellText);
11+
1112
/// The possible cell content keys that exist.
1213
pub const Key = enum {
1314
bg,
@@ -47,11 +48,11 @@ pub const Contents = struct {
4748
///
4849
/// Prefer accessing with `Contents.bgCell(row, col).*` instead
4950
/// of directly indexing in order to avoid integer size bugs.
50-
bg_cells: []shaderpkg.CellBg = undefined,
51+
bg_cells: []shaderpkg.CellBg = &.{},
5152

52-
/// The ArrayListCollection which holds all of the foreground cells. When
53-
/// sized with Contents.resize the individual ArrayLists are given enough
54-
/// room that they can hold a single row with #cols glyphs, underlines, and
53+
/// The lists which hold all of the foreground cells. When sized with
54+
/// Contents.resize the individual ArrayLists are given enough room that
55+
/// they can hold a single row with #cols glyphs, underlines, and
5556
/// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
5657
/// it is possible to exceed this with combining glyphs that add a glyph
5758
/// but take up no column since they combine with the previous one, as
@@ -70,11 +71,12 @@ pub const Contents = struct {
7071
///
7172
/// Must be initialized by calling resize on the Contents struct before
7273
/// calling any operations.
73-
fg_rows: ArrayListCollection(shaderpkg.CellText) = .{ .lists = &.{} },
74+
fg_rows: []CellTextRow = &.{},
7475

7576
pub fn deinit(self: *Contents, alloc: Allocator) void {
7677
alloc.free(self.bg_cells);
77-
self.fg_rows.deinit(alloc);
78+
for (self.fg_rows) |*row| row.deinit(alloc);
79+
alloc.free(self.fg_rows);
7880
}
7981

8082
/// Resize the cell contents for the given grid size. This will
@@ -84,55 +86,50 @@ pub const Contents = struct {
8486
alloc: Allocator,
8587
size: renderer.GridSize,
8688
) Allocator.Error!void {
87-
self.size = size;
89+
const row_count: usize = size.rows;
90+
91+
// The two extra lists hold cursor cells: index 0 is drawn before the
92+
// row contents, and index row_count + 1 is drawn after them.
93+
const fg_rows = try alloc.alloc(CellTextRow, row_count + 2);
94+
@memset(fg_rows, .empty);
95+
errdefer {
96+
for (fg_rows) |*row| row.deinit(alloc);
97+
alloc.free(fg_rows);
98+
}
8899

89-
const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
90-
91-
const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count);
92-
errdefer alloc.free(bg_cells);
93-
@memset(bg_cells, .{ 0, 0, 0, 0 });
94-
95-
// The foreground lists can hold 3 types of items:
96-
// - Glyphs
97-
// - Underlines
98-
// - Strikethroughs
99-
// So we give them an initial capacity of size.columns * 3, which will
100-
// avoid any further allocations in the vast majority of cases. Sadly
101-
// we can not assume capacity though, since with combining glyphs that
102-
// form a single grapheme, and multi-substitutions in fonts, the number
103-
// of glyphs in a row is theoretically unlimited.
104-
//
105-
// We have size.rows + 2 lists because indexes 0 and size.rows - 1 are
106-
// used for special lists containing the cursor cell which need to
107-
// be first and last in the buffer, respectively.
108-
var fg_rows: ArrayListCollection(shaderpkg.CellText) = try .init(
109-
alloc,
110-
size.rows + 2,
111-
size.columns * 3,
112-
);
113-
errdefer fg_rows.deinit(alloc);
100+
// Foreground rows hold glyphs plus underlines, overlines, and
101+
// strikethroughs. Three entries per column cover the common cases
102+
// without reserving space for every decoration combination. We can't
103+
// assume this capacity because combining glyphs and font substitutions
104+
// can produce arbitrarily many glyphs in one column.
105+
const fg_row_capacity = @as(usize, size.columns) * 3;
106+
107+
// The cursor lists need just one cell. The rest get the full capacity.
108+
fg_rows[0] = try .initCapacity(alloc, 1);
109+
fg_rows[row_count + 1] = try .initCapacity(alloc, 1);
110+
for (fg_rows[1 .. row_count + 1]) |*row| {
111+
row.* = try .initCapacity(alloc, fg_row_capacity);
112+
}
114113

115-
// We don't need 3*cols worth of cells for the cursor lists, so we can
116-
// replace them with smaller lists. This is technically a tiny bit of
117-
// extra work but resize is not a hot function so it's worth it to not
118-
// waste the memory.
119-
fg_rows.lists[0].deinit(alloc);
120-
fg_rows.lists[0] = try .initCapacity(alloc, 1);
121-
fg_rows.lists[size.rows + 1].deinit(alloc);
122-
fg_rows.lists[size.rows + 1] = try .initCapacity(alloc, 1);
114+
const bg_cells = try alloc.realloc(
115+
self.bg_cells,
116+
row_count * @as(usize, size.columns),
117+
);
123118

124119
// Perform the swap, no going back from here.
125120
errdefer comptime unreachable;
126-
alloc.free(self.bg_cells);
127-
self.fg_rows.deinit(alloc);
121+
for (self.fg_rows) |*row| row.deinit(alloc);
122+
alloc.free(self.fg_rows);
123+
self.size = size;
128124
self.bg_cells = bg_cells;
129125
self.fg_rows = fg_rows;
126+
self.reset();
130127
}
131128

132129
/// Reset the cell contents to an empty state without resizing.
133130
pub fn reset(self: *Contents) void {
134131
@memset(self.bg_cells, .{ 0, 0, 0, 0 });
135-
self.fg_rows.reset();
132+
for (self.fg_rows) |*row| row.clearRetainingCapacity();
136133
}
137134

138135
/// Set the cursor value. If the value is null then the cursor is hidden.
@@ -142,28 +139,28 @@ pub const Contents = struct {
142139
cursor_style: ?renderer.CursorStyle,
143140
) void {
144141
if (self.size.rows == 0) return;
145-
self.fg_rows.lists[0].clearRetainingCapacity();
146-
self.fg_rows.lists[self.size.rows + 1].clearRetainingCapacity();
142+
self.fg_rows[0].clearRetainingCapacity();
143+
self.fg_rows[self.size.rows + 1].clearRetainingCapacity();
147144

148145
const cell = v orelse return;
149146
const style = cursor_style orelse return;
150147

151148
switch (style) {
152149
// Block cursors should be drawn first
153-
.block => self.fg_rows.lists[0].appendAssumeCapacity(cell),
150+
.block => self.fg_rows[0].appendAssumeCapacity(cell),
154151
// Other cursor styles should be drawn last
155-
.block_hollow, .bar, .underline, .lock => self.fg_rows.lists[self.size.rows + 1].appendAssumeCapacity(cell),
152+
.block_hollow, .bar, .underline, .lock => self.fg_rows[self.size.rows + 1].appendAssumeCapacity(cell),
156153
}
157154
}
158155

159156
/// Returns the current cursor glyph if present, checking both cursor lists.
160157
pub fn getCursorGlyph(self: *Contents) ?shaderpkg.CellText {
161158
if (self.size.rows == 0) return null;
162-
if (self.fg_rows.lists[0].items.len > 0) {
163-
return self.fg_rows.lists[0].items[0];
159+
if (self.fg_rows[0].items.len > 0) {
160+
return self.fg_rows[0].items[0];
164161
}
165-
if (self.fg_rows.lists[self.size.rows + 1].items.len > 0) {
166-
return self.fg_rows.lists[self.size.rows + 1].items[0];
162+
if (self.fg_rows[self.size.rows + 1].items.len > 0) {
163+
return self.fg_rows[self.size.rows + 1].items[0];
167164
}
168165
return null;
169166
}
@@ -201,7 +198,7 @@ pub const Contents = struct {
201198
// We have a special list containing the cursor cell at the start
202199
// of our fg row collection, so we need to add 1 to the y to get
203200
// the correct index.
204-
=> try self.fg_rows.lists[y + 1].append(alloc, cell),
201+
=> try self.fg_rows[y + 1].append(alloc, cell),
205202
}
206203
}
207204

@@ -214,7 +211,7 @@ pub const Contents = struct {
214211
// We have a special list containing the cursor cell at the start
215212
// of our fg row collection, so we need to add 1 to the y to get
216213
// the correct index.
217-
self.fg_rows.lists[y + 1].clearRetainingCapacity();
214+
self.fg_rows[y + 1].clearRetainingCapacity();
218215
}
219216
};
220217

@@ -362,14 +359,14 @@ test Contents {
362359

363360
// We should start off empty after resizing.
364361
for (0..rows) |y| {
365-
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
362+
try testing.expect(c.fg_rows[y + 1].items.len == 0);
366363
for (0..cols) |x| {
367364
try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
368365
}
369366
}
370367
// And the cursor row should have a capacity of 1 and also be empty.
371-
try testing.expect(c.fg_rows.lists[0].capacity == 1);
372-
try testing.expect(c.fg_rows.lists[0].items.len == 0);
368+
try testing.expect(c.fg_rows[0].capacity == 1);
369+
try testing.expect(c.fg_rows[0].items.len == 0);
373370

374371
// Add some contents.
375372
const bg_cell: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
@@ -382,12 +379,12 @@ test Contents {
382379
try c.add(alloc, .text, fg_cell);
383380
try testing.expectEqual(bg_cell, c.bgCell(1, 4).*);
384381
// The fg row index is offset by 1 because of the cursor list.
385-
try testing.expectEqual(fg_cell, c.fg_rows.lists[2].items[0]);
382+
try testing.expectEqual(fg_cell, c.fg_rows[2].items[0]);
386383

387384
// And we should be able to clear it.
388385
c.clear(1);
389386
for (0..rows) |y| {
390-
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
387+
try testing.expect(c.fg_rows[y + 1].items.len == 0);
391388
for (0..cols) |x| {
392389
try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
393390
}
@@ -401,20 +398,49 @@ test Contents {
401398
.color = .{ 0, 0, 0, 1 },
402399
};
403400
c.setCursor(cursor_cell, .block);
404-
try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]);
401+
try testing.expectEqual(cursor_cell, c.fg_rows[0].items[0]);
405402
try testing.expectEqual(cursor_cell, c.getCursorGlyph().?);
406403

407404
// And remove it.
408405
c.setCursor(null, null);
409-
try testing.expectEqual(0, c.fg_rows.lists[0].items.len);
406+
try testing.expectEqual(0, c.fg_rows[0].items.len);
410407
try testing.expect(c.getCursorGlyph() == null);
411408

412409
// Add a hollow cursor.
413410
c.setCursor(cursor_cell, .block_hollow);
414-
try testing.expectEqual(cursor_cell, c.fg_rows.lists[rows + 1].items[0]);
411+
try testing.expectEqual(cursor_cell, c.fg_rows[rows + 1].items[0]);
415412
try testing.expectEqual(cursor_cell, c.getCursorGlyph().?);
416413
}
417414

415+
test "Contents resize grows and shrinks" {
416+
const testing = std.testing;
417+
const alloc = testing.allocator;
418+
419+
var c: Contents = .{};
420+
defer c.deinit(alloc);
421+
422+
try c.resize(alloc, .{ .rows = 2, .columns = 2 });
423+
var cell: shaderpkg.CellText = .{
424+
.atlas = .grayscale,
425+
.grid_pos = .{ 1, 1 },
426+
.color = .{ 0, 0, 0, 1 },
427+
};
428+
try c.add(alloc, .text, cell);
429+
c.bgCell(1, 1).* = .{ 0, 0, 0, 1 };
430+
c.setCursor(cell, .bar);
431+
432+
try c.resize(alloc, .{ .rows = 3, .columns = 4 });
433+
try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(1, 1).*);
434+
try testing.expectEqual(@as(usize, 0), c.fg_rows[2].items.len);
435+
try testing.expectEqual(@as(?shaderpkg.CellText, null), c.getCursorGlyph());
436+
cell.grid_pos = .{ 3, 2 };
437+
try c.add(alloc, .text, cell);
438+
439+
try c.resize(alloc, .{ .rows = 1, .columns = 1 });
440+
cell.grid_pos = .{ 0, 0 };
441+
try c.add(alloc, .text, cell);
442+
}
443+
418444
test "Contents clear retains other content" {
419445
const testing = std.testing;
420446
const alloc = testing.allocator;
@@ -452,7 +478,7 @@ test "Contents clear retains other content" {
452478
// Row 2 should still contain its cells.
453479
try testing.expectEqual(bg_cell_2, c.bgCell(2, 4).*);
454480
// Fg row index is +1 because of cursor list at start
455-
try testing.expectEqual(fg_cell_2, c.fg_rows.lists[3].items[0]);
481+
try testing.expectEqual(fg_cell_2, c.fg_rows[3].items[0]);
456482
}
457483

458484
test "Contents clear last added content" {
@@ -492,7 +518,7 @@ test "Contents clear last added content" {
492518
// Row 1 should still contain its cells.
493519
try testing.expectEqual(bg_cell_1, c.bgCell(1, 4).*);
494520
// Fg row index is +1 because of cursor list at start
495-
try testing.expectEqual(fg_cell_1, c.fg_rows.lists[2].items[0]);
521+
try testing.expectEqual(fg_cell_1, c.fg_rows[2].items[0]);
496522
}
497523

498524
test "Contents with zero-sized screen" {

src/renderer/generic.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1575,7 +1575,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
15751575
// Setup our frame data
15761576
try frame.uniforms.sync(&.{self.uniforms});
15771577
try frame.cells_bg.sync(self.cells.bg_cells);
1578-
const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists);
1578+
const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows);
15791579

15801580
// If our background image buffer has changed, sync it.
15811581
if (frame.bg_image_buffer_modified != self.bg_image_buffer_modified) {

0 commit comments

Comments
 (0)