perf: close the last benchmark gaps to Zig (render state, parser)

- RenderState stores styles sparsely (per-cell u16 style ids plus one
  Style per distinct run) and skips plain cells with a single mask test
  (Cell::is_plain). screen-clone render: 11.5 ms -> 4.1 ms (Zig 4.7).
  row::never_extend_bg now takes a style accessor instead of a slice.
- Parser::next_with(byte, |action| ..) hands actions to a closure
  instead of moving a 64-byte action array out; Parser::next remains a
  wrapper over the same core (equivalence test added).
  terminal-parser SGR: 538 ms -> 231 ms (Zig 354).

All Ghostty benchmarks are now at or below Zig's time; table in
docs/rewrite/notes/perf-P1.md (the parser benchmark uses next_with with
a discarding closure, which like Zig's `_ = actions` lets the optimizer
skip unused work).

Test Plan:
- cargo test --workspace
- ghostty-bench +screen-clone --mode=render / +terminal-parser
This commit is contained in:
ddidderr committed 2026-10-12 05:22:01 +02:00
1 parent 42f05c926d
commit 7d48a4bf29
7 files changed
+177 -56

No files matched your search

+19
View File
@@ -154,6 +154,12 @@ impl Cell {
pub fn has_styling(self) -> bool {
self.style_id() != 0
}
/// True if the cell is a plain single-codepoint cell with the default
/// style (content tag `Codepoint` and style id 0): a single mask test.
#[inline(always)]
pub fn is_plain(self) -> bool {
self.0 & (3 | (0xFFFF << 26)) == 0
}
/// True for a blank narrow textual cell; style does not affect this test.
pub fn is_empty(self) -> bool {
(self.content_tag() as u8) < 2 && !self.has_text() && self.wide() == Wide::Narrow
@@ -1789,6 +1795,19 @@ fn swap_map<T, S: std::hash::BuildHasher>(map: &mut HashMap<usize, T, S>, a: usi
}
#[cfg(test)]
mod tests {
#[test]
fn cell_is_plain() {
let mut c = Cell::init(u32::from('a'));
assert!(c.is_plain());
c.set_style_id(1);
assert!(!c.is_plain());
c.set_style_id(0);
c.set_content_tag(ContentTag::BgColorRgb);
assert!(!c.is_plain());
c.set_content_tag(ContentTag::CodepointGrapheme);
assert!(!c.is_plain());
}
use super::*;
#[test]
fn chunk_alloc_matches_naive_first_fit() {