max / shop
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, |