Skip to main content

max / shop

Hot print path: cached row start + pre-encoded style words place_char used to redo the ring-mod row_start lookup, encode fg/bg/ attrs into cell words, and bounds-check the store on every glyph. Now: - pending_fg_word / pending_bg_word cache the encoded style, refreshed by apply_sgr at the end of a color/attr run (called once per SGR, not once per print). - cur_row_start caches the byte offset of the cursor row in the active buffer, populated lazily on the first print of a run and invalidated at the top of every non-print Perform entry point (execute, csi, esc), inside newline, and in resize. Sentinel u32::MAX = invalid. - The store itself is get_unchecked_mut behind two debug_asserts: the row-start invariant (multiple of cols, in-bounds) plus row_dirty[row]. Bounds-check elimination is the point — this is the innermost store for every printed glyph, hit at PTY-drain rate on cell-dense workloads. No mode splits (task cabe6d1c's option (a)) because shop has no per- print mode branches yet (no insert mode, no OSC-8 hyperlink attr, no non-ASCII charset). The split is worth revisiting only when we add one; right now the fast path is the only path. Prerequisite lands per task 49e44ae7 (Cell packing) — direct field writes into cell.c_raw / fg_word / bg_word bypass Cell::new so we don't rebuild the 12-byte payload piecewise.
Author: Max Johnson <me@maxj.phd> · 2026-07-24 14:42 UTC
Signed with PGP, not checked
Commit: 6ce86da7dbd612b014a735c1a11858cea30aa8cc
Parent: 6133764
1 file changed, +75 insertions, -7 deletions
@@ -244,6 +244,17 @@
244 244 pending_fg: Color,
245 245 pending_bg: Color,
246 246 pending_attrs: Attrs,
247 + // Pre-encoded pending style, ready to store into a Cell's words. Kept in
248 + // sync with pending_{fg,bg,attrs} by [`Grid::recompute_style_words`],
249 + // called from apply_sgr. Lets the hot `place_char` skip encode_color +
250 + // encode_attrs on every glyph.
251 + pending_fg_word: u32,
252 + pending_bg_word: u32,
253 + // Cached byte offset of the current cursor row's start in the active
254 + // buffer, or `u32::MAX` when invalid. Fast `place_char` populates it on
255 + // first use of a print run; any non-print Perform entry point (execute /
256 + // csi / esc / osc), newline, screen-swap, DECSTBM, or resize invalidates.
257 + cur_row_start: u32,
247 258 pending_title: Option<String>,
248 259 // Damage tracking — accumulated between take_damage() calls.
249 260 row_dirty: Vec<bool>,
@@ -252,6 +263,8 @@
252 263 pending_resize: bool,
253 264 }
254 265
266 + const CUR_ROW_INVALID: u32 = u32::MAX;
267 +
255 268 impl Grid {
256 269 pub fn new(cols: u16, rows: u16) -> Self {
257 270 let cols = cols.max(1);
@@ -278,6 +291,9 @@
278 291 pending_fg: Color::Default,
279 292 pending_bg: Color::Default,
280 293 pending_attrs: Attrs::default(),
294 + pending_fg_word: 0,
295 + pending_bg_word: 0,
296 + cur_row_start: CUR_ROW_INVALID,
281 297 pending_title: None,
282 298 // Initial state: everything dirty so first render populates the
283 299 // per-row cache.
@@ -499,10 +515,19 @@
499 515 self.row_dirty = vec![true; rows as usize];
500 516 self.pending_resize = true;
501 517 self.pending_scroll = 0;
518 + self.invalidate_cur_row();
502 519 }
503 520
504 - fn cell_index(&self, row: u16, col: u16) -> usize {
505 - self.row_start(row) + col as usize
521 + #[inline]
522 + fn invalidate_cur_row(&mut self) {
523 + self.cur_row_start = CUR_ROW_INVALID;
524 + }
525 +
526 + #[inline]
527 + fn recompute_style_words(&mut self) {
528 + self.pending_fg_word =
529 + encode_color(self.pending_fg) | encode_attrs(self.pending_attrs);
530 + self.pending_bg_word = encode_color(self.pending_bg);
506 531 }
507 532
508 533 fn place_char(&mut self, c: char) {
@@ -513,11 +538,40 @@
513 538 self.cursor.col = 0;
514 539 self.cursor.wrap_next = false;
515 540 }
516 - let row = self.cursor.row;
517 - let idx = self.cell_index(row, self.cursor.col);
518 - self.active_cells_mut()[idx] =
519 - Cell::new(c, self.pending_fg, self.pending_bg, self.pending_attrs);
520 - self.mark_row_dirty(row);
541 + // Cache the row start on first print of a run; non-print Perform
542 + // entry points and newline invalidate it back to CUR_ROW_INVALID.
543 + let start = if self.cur_row_start == CUR_ROW_INVALID {
544 + let s = self.row_start(self.cursor.row) as u32;
545 + self.cur_row_start = s;
546 + s as usize
547 + } else {
548 + self.cur_row_start as usize
549 + };
550 + let col = self.cursor.col as usize;
551 + let row_idx = self.cursor.row as usize;
552 + let cells: &mut [Cell] = if self.on_alt {
553 + &mut self.alt
554 + } else {
555 + &mut self.main
556 + };
557 + // Bounds are enforced structurally: start is a valid row start (a
558 + // multiple of cols within cells.len()), col < cols, row < rows.
559 + debug_assert!(start + col < cells.len());
560 + debug_assert!(row_idx < self.row_dirty.len());
561 + // SAFETY: the debug_asserts above encode the invariants — start is
562 + // computed from row_start() (multiple of cols, < cells.len()), col is
563 + // clamped to cols on entry, and row_dirty has one entry per row.
564 + // Bounds-check elimination matters here: this is the innermost store
565 + // for every printed glyph, called at PTY-drain rate on cell-dense
566 + // workloads (millions/sec on `cat` of a wide buffer).
567 + #[allow(unsafe_code)]
568 + unsafe {
569 + let cell = cells.get_unchecked_mut(start + col);
570 + cell.c_raw = c as u32;
571 + cell.fg_word = self.pending_fg_word;
572 + cell.bg_word = self.pending_bg_word;
573 + *self.row_dirty.get_unchecked_mut(row_idx) = true;
574 + }
521 575 if self.cursor.col + 1 >= self.cols {
522 576 self.cursor.wrap_next = true;
523 577 } else {
@@ -531,6 +585,7 @@
531 585 self.pending_fg = Color::Default;
532 586 self.pending_bg = Color::Default;
533 587 self.pending_attrs = Attrs::default();
588 + self.recompute_style_words();
534 589 return;
535 590 }
536 591
@@ -549,6 +604,7 @@
549 604 let p = group.first().copied().unwrap_or(0);
550 605 i += self.apply_single(p, &groups, i);
551 606 }
607 + self.recompute_style_words();
552 608 }
553 609
554 610 /// Semicolon-form: `p` came from a single-subparam group. Returns how many
@@ -676,6 +732,9 @@
676 732 self.scroll_up_in_region(1);
677 733 }
678 734 self.cursor.wrap_next = false;
735 + // Cursor row moved, or the ring rotated under it — either way the
736 + // cached row start is stale. place_char re-derives on next use.
737 + self.invalidate_cur_row();
679 738 }
680 739
681 740 fn scroll_up_in_region(&mut self, n: u16) {
@@ -938,6 +997,10 @@
938 997 }
939 998
940 999 fn execute(&mut self, byte: u8) {
1000 + // Any C0 that isn't NUL/BEL moves the cursor or scrolls; invalidate
1001 + // the fast-path row cache up front so we don't have to sprinkle it
1002 + // across every arm.
1003 + self.invalidate_cur_row();
941 1004 match byte {
942 1005 0x08 => {
943 1006 // BS
@@ -973,6 +1036,10 @@
973 1036 _ignore: bool,
974 1037 action: char,
975 1038 ) {
1039 + // Nearly every CSI mutates cursor, scroll region, or screen; a couple
1040 + // (cursor visibility, SGR) don't but the invalidation is a single
1041 + // store — cheaper than branching on which arm we're taking.
1042 + self.invalidate_cur_row();
976 1043 let private = intermediates.first().copied() == Some(b'?');
977 1044 match (action, private) {
978 1045 ('H', false) | ('f', false) => {
@@ -1071,6 +1138,7 @@
1071 1138 }
1072 1139
1073 1140 fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, byte: u8) {
1141 + self.invalidate_cur_row();
1074 1142 match byte {
1075 1143 b'7' => self.saved_main_cursor = self.cursor,
1076 1144 b'8' => self.cursor = self.saved_main_cursor,