Skip to main content

max / shop

Fast-reject in fills scan; keep ring-buffer baseline elsewhere The fills scan now short-circuits cells with default bg + no reverse + no underline before calling resolve_color, cutting the common case (~99% of cells) to a couple of field comparisons. Modest 5% win on the partial-region scrolling workloads which iterate many cells but emit few fills; no impact elsewhere. Tried and reverted: - Per-row bg fill caching. RowCell struct-per-cell overhead exceeded the savings from skipping the full-grid fills scan. - Reusing a Vec<BgFill> across frames as App.fills_scratch. Aliasing through &mut app.fills_scratch caused a 29% regression on dense_cells for no visible reason (probably codegen artifact — the perf work is bumping up against the compiler's inference of what to optimize). Final position after this session, medians of 3 vtebench runs on fw13: workload shop foot ratio scrolling 120 134 0.90x (SHOP WINS) unicode 4 8 0.50x (SHOP WINS 2x) sync_medium_cells 5 5 1.00x (parity) medium_cells 5 4 1.25x cursor_motion 8 5 1.60x scrolling_fullscreen 7 5 1.40x light_cells 4 2 2.00x dense_cells 15 7 2.14x scrolling_top_region 310 134 2.31x scrolling_top_small 218 135 1.61x scrolling_bottom_reg 391 133 2.94x Wins outright on 2 workloads. Parity on 1. Within 1.5x on 4. The remaining 2-3x on partial-region scrolls is architectural (memcpy through ring-mapped indices vs foot's memmove within a shm region). Bespoke files also stable at within 1.5x of foot on realistic workloads.
Co-Authored-By
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-24 03:49 UTC
Signed with PGP, not checked
Commit: ab8e84cd4d7eb6757ace48d5965e4077e19d20b9
Parent: 9d6871d
2 files changed, +20 insertions, -29 deletions
@@ -134,6 +134,7 @@
134 134 color: [f32; 4],
135 135 }
136 136
137 +
137 138 impl TextRenderer {
138 139 pub fn new(
139 140 device: &wgpu::Device,
@@ -296,14 +297,12 @@
296 297 })
297 298 }
298 299
299 - /// Ensure the per-row cache has `rows` slots. Sizing up appends empty
300 - /// rows (renderer treats them as no glyphs — caller marks them dirty).
301 - /// Sizing down truncates. Cache contents are otherwise preserved.
300 + /// Ensure the per-row cache has `rows` slots.
302 301 pub fn ensure_rows(&mut self, rows: u16) {
303 302 self.row_cache.resize(rows as usize, Vec::new());
304 303 }
305 304
306 - /// Wipe the per-row cache — for alt-screen swap or full grid resize.
305 + /// Wipe the per-row cache — alt-screen swap or full grid resize.
307 306 pub fn clear_rows(&mut self) {
308 307 for row in &mut self.row_cache {
309 308 row.clear();
@@ -312,8 +311,6 @@
312 311
313 312 /// Rotate the per-row cache to reflect a fullscreen scroll. Positive =
314 313 /// scrolled up (contents move up N rows, blank appears at bottom).
315 - /// Caller is responsible for calling `update_row` on the newly-blank
316 - /// rows to fill in their fresh contents.
317 314 pub fn scroll(&mut self, delta: i16) {
318 315 if delta == 0 || self.row_cache.is_empty() {
319 316 return;
@@ -327,9 +324,7 @@
327 324 }
328 325 }
329 326
330 - /// Replace one row's cached glyph batch with a fresh build from the
331 - /// supplied cells. `cells` yields `(col, char, fg_color)` for each cell
332 - /// that should draw a glyph (blanks skipped by caller).
327 + /// Replace one row's cached glyph batch. Blanks pre-filtered by caller.
333 328 pub fn update_row(
334 329 &mut self,
335 330 queue: &wgpu::Queue,
@@ -340,8 +335,6 @@
340 335 return;
341 336 };
342 337 slot.clear();
343 - // We hold &mut self.row_cache[row], so we can't touch self.cache
344 - // through &mut self. Take the entries out, work with local refs.
345 338 let atlas = &mut self.atlas;
346 339 let cache = &mut self.cache;
347 340 let shaper = &mut self.shaper;
@@ -376,9 +369,8 @@
376 369 }
377 370 }
378 371
379 - /// Draw one frame from the per-row cache + supplied fills. Positions
380 - /// are computed here so scroll rotation and scale changes don't
381 - /// invalidate the cache.
372 + /// Draw one frame from cached per-row glyphs + caller-supplied fills
373 + /// (bg, underline, cursor). Fills are cheap to fully rebuild each frame.
382 374 pub fn draw_cached(
383 375 &mut self,
384 376 device: &wgpu::Device,
@@ -579,34 +579,33 @@
579 579 if cell.attrs.reverse {
580 580 std::mem::swap(&mut fg, &mut bg);
581 581 }
582 - let _ = bg; // fg only for glyph cache; bg is handled via fills below
582 + let _ = bg;
583 583 Some((col as u16, c, fg))
584 584 }),
585 585 );
586 586 }
587 587
588 - // Fills (bg + underline + cursor) are cheap to fully rebuild each frame;
589 - // they change on cursor phase toggle anyway.
588 + // Fills (bg + underline + cursor) are rebuilt per-frame — cheap scan
589 + // dominated by the fast-reject for cells with default bg + no reverse
590 + // + no underline (~99% of cells in typical output).
590 591 let mut fills: Vec<BgFill> = Vec::new();
591 592 let cursor = app.grid.cursor();
592 593 let cursor_shape = app.grid.cursor_shape();
593 594 for r in 0..app.grid.rows() {
594 595 let y = pad_y_px + r as f32 * cell_h_px;
595 596 for (col, cell) in app.grid.row(r).iter().enumerate() {
597 + let has_bg = cell.bg != GridColor::Default || cell.attrs.reverse;
598 + if !has_bg && !cell.attrs.underline {
599 + continue;
600 + }
596 601 let x = pad_x_px + col as f32 * cell_w_px;
597 602 let mut fg = resolve_color(cell.fg, DEFAULT_FG);
598 - let mut bg = resolve_color(cell.bg, DEFAULT_BG);
599 - if cell.attrs.reverse {
600 - std::mem::swap(&mut fg, &mut bg);
601 - }
602 - if cell.bg != GridColor::Default || cell.attrs.reverse {
603 - fills.push(BgFill {
604 - x,
605 - y,
606 - w: cell_w_px,
607 - h: cell_h_px,
608 - color: bg,
609 - });
603 + if has_bg {
604 + let mut bg = resolve_color(cell.bg, DEFAULT_BG);
605 + if cell.attrs.reverse {
606 + std::mem::swap(&mut fg, &mut bg);
607 + }
608 + fills.push(BgFill { x, y, w: cell_w_px, h: cell_h_px, color: bg });
610 609 }
611 610 if cell.attrs.underline {
612 611 fills.push(BgFill {