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
This commit is contained in:
ddidderr committed 2026-10-12 08:14:02 +02:00
1 parent 96314c1a8b
commit f6002d7519
22 files changed
+2079 -57

No files matched your search

+81
View File
@@ -196,6 +196,29 @@ pub struct PageList {
pin_serial: u64,
pause_checks: usize,
}
/// Failure points of [`PageList::init`] (Zig: `init_tw`). The Zig
/// `init_memory_pool` point has no counterpart: Rust has no up-front memory
/// pool, the slabs start empty and grow with the fallible reserves below.
/// `viewport_pin` and `viewport_pin_track` are two allocations in Zig (pool
/// create + set insert) that are one reserve in `track_pin` here; both points
/// are checked before it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InitPoint {
InitPages,
ViewportPin,
ViewportPinTrack,
}
crate::tripwire_module!(init_tw, InitPoint, Error);
/// Failure points of the page creation done by `init` (Zig: `initPages_tw`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InitPagesPoint {
Node,
BufStd,
BufNonStd,
}
crate::tripwire_module!(init_pages_tw, InitPagesPoint, Error);
fn std_size() -> usize {
Page::layout(page::STD_CAPACITY).total_size
}
@@ -242,12 +265,15 @@ impl PageList {
pin_serial: 0,
pause_checks: 0,
};
init_tw::check(InitPoint::InitPages)?;
while s.total_rows < usize::from(rows) {
s.grow()?;
}
let pin = s
.get_top_left(Tag::Screen)
.ok_or(Error::InvalidCoordinate)?;
init_tw::check(InitPoint::ViewportPin)?;
init_tw::check(InitPoint::ViewportPinTrack)?;
s.viewport_pin = Some(s.track_pin(pin)?);
Ok(s)
}
@@ -269,6 +295,7 @@ impl PageList {
if Page::layout(cap).total_size > u32::MAX as usize {
return Err(Error::OutOfSpace);
}
init_pages_tw::check(InitPagesPoint::Node)?;
self.nodes.try_reserve(1).map_err(|_| Error::OutOfMemory)?;
let mut data = if let Some(mut p) = self.clean_pool.pop().filter(|p| p.capacity == cap) {
p.size = page::Size {
@@ -281,6 +308,11 @@ impl PageList {
p.reinit()?;
p
} else {
if Page::layout(cap).total_size <= std_size() {
init_pages_tw::check(InitPagesPoint::BufStd)?;
} else {
init_pages_tw::check(InitPagesPoint::BufNonStd)?;
}
Page::init(cap)?
};
data.size.rows = 0;
@@ -2972,6 +3004,55 @@ mod tests {
s.clear_dirty();
}
#[test]
fn page_list_init_error() {
use crate::tripwire::ResetMode;
// Test every failure point in `init` and ensure we exit early
// without panicking or leaving the tripwire armed.
for tag in [
InitPoint::InitPages,
InitPoint::ViewportPin,
InitPoint::ViewportPinTrack,
] {
init_tw::error_always(tag, Error::OutOfMemory);
assert_eq!(
Err(Error::OutOfMemory),
PageList::init(80, 24, None).map(drop)
);
init_tw::end(ResetMode::Reset).unwrap();
}
// init creates pages transitively, so check each failure there too.
let big = page::STD_CAPACITY.max_cols().unwrap() + 1;
for tag in [
InitPagesPoint::Node,
InitPagesPoint::BufStd,
InitPagesPoint::BufNonStd,
] {
init_pages_tw::error_always(tag, Error::OutOfMemory);
let cols = if tag == InitPagesPoint::BufStd {
80
} else {
big
};
assert_eq!(
Err(Error::OutOfMemory),
PageList::init(cols, 24, None).map(drop)
);
init_pages_tw::end(ResetMode::Reset).unwrap();
}
// Non-standard pages: fail on the second one.
init_pages_tw::error_after(InitPagesPoint::BufNonStd, Error::OutOfMemory, 1);
assert_eq!(
Err(Error::OutOfMemory),
PageList::init(big, page::STD_CAPACITY.rows + 1, None).map(drop)
);
init_pages_tw::end(ResetMode::Reset).unwrap();
// And with nothing armed init still works.
PageList::init(80, 24, None).unwrap();
}
#[test]
fn page_list_init_rows_across_two_pages() {
let mut cols = 50;
while initial_capacity(cols).rows >= 100 {