Files
ghostty-rs/crates/ghostty-terminal/src/tabstops.rs
T
ddidderr 8af6ab68d3 fix: C allocator contract, shadertoy overloads/MSL stride, minidumps
Closes the remaining library rows of the final completeness audit
(docs/rewrite/notes/final-Z3.md):
- libghostty-vt: OSC parser storage goes through the caller-supplied C
  allocator via a ByteStorage abstraction; ghostty_osc_new reports
  GHOSTTY_OUT_OF_MEMORY and failed grows invalidate the OSC like Zig.
  Tested with a counting/failing/limiting C allocator.
- shadertoy: sampler parameter rewriting is keyed by full signature so
  overloads work; the Metal uniform block keeps std140 strides for
  iChannelTime[4] (MSL offsets checked against the upload layout).
- tripwire.zig ported (named trip points, hit counts, verification) and
  used to port the previously skipped allocation-failure tests that have
  a fallible Rust path; the rest are documented.
- crash: fatal signals capture siginfo + registers and a forked helper
  writes a minidump (minidump-writer) as an envelope attachment, like
  sentry-native/Breakpad; tests parse it with the minidump crate.
- kitty LoadingImage::debug_dump (debug builds) and the Darwin
  Instruments signpost around benchmark runs.

Test Plan:
- cargo test -p ghostty-vt-capi --test c_allocator
- cargo test -p ghostty-crash --test minidump
2026-10-12 08:14:02 +02:00

268 lines
7.8 KiB
Rust

//! Keep track of the location of tabstops.
//!
//! This is implemented as a bit set. There is a preallocation segment that
//! is used for almost all screen sizes. Then there is a dynamically allocated
//! segment if the screen is larger than the preallocation amount.
//!
//! In reality, tabstops don't need to be the most performant in any metric.
//! This implementation tries to balance denser memory usage (by using a bitset)
//! and minimizing unnecessary allocations.
use std::collections::TryReserveError;
/// Unit is the type we use per tabstop unit (see file docs).
type Unit = u8;
const UNIT_BITS: usize = Unit::BITS as usize;
/// The number of columns we preallocate for. This is kind of high which
/// costs us some memory, but this is more columns than my 6k monitor at
/// 12-point font size, so this should prevent allocation in almost all
/// real world scenarios for the price of wasting at most
/// (columns / sizeOf(Unit)) bytes.
const PREALLOC_COLUMNS: usize = 512;
/// The number of entries we need for our preallocation.
const PREALLOC_COUNT: usize = PREALLOC_COLUMNS / UNIT_BITS;
/// We precompute all the possible masks since we never use a huge bit size.
const MASKS: [Unit; UNIT_BITS] = {
let mut res = [0; UNIT_BITS];
let mut i = 0;
while i < UNIT_BITS {
res[i] = 1 << i;
i += 1;
}
res
};
/// Returns the entry in the stops array that would contain this column.
#[inline]
const fn entry(col: usize) -> usize {
col / UNIT_BITS
}
#[inline]
const fn index(col: usize) -> usize {
col % UNIT_BITS
}
/// Tabstop bit set.
#[derive(Debug, Clone)]
pub struct Tabstops {
/// The number of columns this tabstop is set to manage. Use `resize`
/// to change this number.
pub cols: usize,
/// Preallocated tab stops.
prealloc_stops: [Unit; PREALLOC_COUNT],
/// Dynamically expanded stops above prealloc stops.
dynamic_stops: Vec<Unit>,
}
/// Failure points of [`Tabstops::resize`] (Zig: `resize_tw`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ResizePoint {
DynamicAlloc,
}
crate::tripwire_module!(resize_tw, ResizePoint, TryReserveError);
impl Default for Tabstops {
fn default() -> Self {
Self {
cols: 0,
prealloc_stops: [0; PREALLOC_COUNT],
dynamic_stops: Vec::new(),
}
}
}
impl Tabstops {
pub fn new(cols: usize, interval: usize) -> Result<Self, TryReserveError> {
let mut res = Self::default();
res.resize(cols)?;
res.reset(interval);
Ok(res)
}
/// Set the tabstop at a certain column. The columns are 0-indexed.
pub fn set(&mut self, col: usize) {
let i = entry(col);
let idx = index(col);
if i < PREALLOC_COUNT {
self.prealloc_stops[i] |= MASKS[idx];
return;
}
let dynamic_i = i - PREALLOC_COUNT;
assert!(dynamic_i < self.dynamic_stops.len());
self.dynamic_stops[dynamic_i] |= MASKS[idx];
}
/// Unset the tabstop at a certain column. The columns are 0-indexed.
///
/// Note: like the Zig implementation this XORs the bit, so unsetting a
/// tabstop that is not set will set it.
pub fn unset(&mut self, col: usize) {
let i = entry(col);
let idx = index(col);
if i < PREALLOC_COUNT {
self.prealloc_stops[i] ^= MASKS[idx];
return;
}
let dynamic_i = i - PREALLOC_COUNT;
assert!(dynamic_i < self.dynamic_stops.len());
self.dynamic_stops[dynamic_i] ^= MASKS[idx];
}
/// Get the value of a tabstop at a specific column. The columns are 0-indexed.
pub fn get(&self, col: usize) -> bool {
let i = entry(col);
let idx = index(col);
let mask = MASKS[idx];
let unit = if i < PREALLOC_COUNT {
self.prealloc_stops[i]
} else {
let dynamic_i = i - PREALLOC_COUNT;
assert!(dynamic_i < self.dynamic_stops.len());
self.dynamic_stops[dynamic_i]
};
unit & mask == mask
}
/// Resize this to support up to cols columns.
// TODO: needs interval to set new tabstops
pub fn resize(&mut self, cols: usize) -> Result<(), TryReserveError> {
// Do nothing if it fits.
if cols <= PREALLOC_COLUMNS {
self.cols = cols;
return Ok(());
}
// What we need in the dynamic size
let size = cols - PREALLOC_COLUMNS;
if size < self.dynamic_stops.len() {
self.cols = cols;
return Ok(());
}
// Allocate first so that failure leaves our state untouched.
resize_tw::check(ResizePoint::DynamicAlloc)?;
let mut new: Vec<Unit> = Vec::new();
new.try_reserve_exact(size)?;
new.resize(size, 0);
new[..self.dynamic_stops.len()].copy_from_slice(&self.dynamic_stops);
self.dynamic_stops = new;
self.cols = cols;
Ok(())
}
/// Return the maximum number of columns this can support currently.
pub fn capacity(&self) -> usize {
(PREALLOC_COUNT + self.dynamic_stops.len()) * UNIT_BITS
}
/// Unset all tabstops and then reset the initial tabstops to the given
/// interval. An interval of 0 sets no tabstops.
pub fn reset(&mut self, interval: usize) {
self.prealloc_stops.fill(0);
self.dynamic_stops.fill(0);
if interval > 0 {
let mut i = interval;
while i + 1 < self.cols {
self.set(i);
i += interval;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tripwire::ResetMode;
#[test]
fn tabstops_basic() {
let mut t = Tabstops::default();
assert_eq!(0, entry(4));
assert_eq!(1, entry(8));
assert_eq!(0, index(0));
assert_eq!(1, index(1));
assert_eq!(1, index(9));
assert_eq!(0b00001000, MASKS[3]);
assert_eq!(0b00010000, MASKS[4]);
assert!(!t.get(4));
t.set(4);
assert!(t.get(4));
assert!(!t.get(3));
t.reset(0);
assert!(!t.get(4));
t.set(4);
assert!(t.get(4));
t.unset(4);
assert!(!t.get(4));
}
#[test]
fn tabstops_dynamic_allocations() {
let mut t = Tabstops::default();
// Grow the capacity by 2.
let cap = t.capacity();
t.resize(cap * 2).unwrap();
// Set something that was out of range of the first
t.set(cap + 5);
assert!(t.get(cap + 5));
assert!(!t.get(cap + 4));
// Prealloc still works
assert!(!t.get(5));
}
#[test]
fn tabstops_interval() {
let t = Tabstops::new(80, 4).unwrap();
assert!(!t.get(0));
assert!(t.get(4));
assert!(!t.get(5));
assert!(t.get(8));
}
#[test]
fn tabstops_count_on_80() {
// https://superuser.com/questions/710019/why-there-are-11-tabstops-on-a-80-column-console
let t = Tabstops::new(80, 8).unwrap();
// Count the tabstops
let count = (0..80).filter(|&i| t.get(i)).count();
assert_eq!(9, count);
}
#[test]
fn tabstops_resize_alloc_failure_preserves_state() {
// If resize() fails during allocation, the original cols value is
// preserved (not corrupted).
let mut t = Tabstops::new(80, 8).unwrap();
let original_cols = t.cols;
// Trigger allocation failure when resizing beyond prealloc
let oom = Vec::<u8>::new().try_reserve(usize::MAX).unwrap_err();
resize_tw::error_always(ResizePoint::DynamicAlloc, oom.clone());
let result = t.resize(PREALLOC_COLUMNS * 2);
assert_eq!(Err(oom), result);
resize_tw::end(ResetMode::Reset).unwrap();
// cols should be unchanged after failed resize
assert_eq!(original_cols, t.cols);
}
}