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

@@ -18,9 +18,12 @@
//!
//! Limitations (documented, reported as compile errors by naga): a sampler
//! argument must be a plain identifier (no ternaries or struct members),
//! samplers cannot be stored in local variables/arrays/struct fields, and
//! overloads of one function name must agree on which parameters are
//! samplers.
//! samplers cannot be stored in local variables/arrays/struct fields.
//!
//! Overloads are keyed by their full signature (the per-parameter sampler
//! layout), not by name. At a call site the overload is chosen by arity and by
//! which arguments are samplers (`iChannelN` or a sampler parameter of the
//! enclosing function), which is what GLSL overload resolution would see.
use std::collections::HashMap;
@@ -171,10 +174,11 @@ struct Def {
samplers: Vec<Option<String>>,
}
struct FnInfo {
/// Per parameter: the sampler name, if the parameter is a `sampler2D`.
samplers: Vec<Option<String>>,
}
/// Overload signature: per parameter, whether it is a `sampler2D`.
type SamplerLayout = Vec<bool>;
/// The combined samplers the host provides.
const GLOBAL_SAMPLERS: [&str; 4] = ["iChannel0", "iChannel1", "iChannel2", "iChannel3"];
/// Rewrite the user's ShaderToy source. Returns the source unchanged if it
/// needs no adaptation other than `texture2D(` -> `texture(`.
@@ -188,7 +192,7 @@ pub fn rewrite(src: &str) -> String {
.collect();
// Pass 1: find top-level functions with sampler2D parameters.
let mut funcs: HashMap<&str, FnInfo> = HashMap::new();
let mut funcs: HashMap<&str, Vec<SamplerLayout>> = HashMap::new();
let mut defs: Vec<Def> = Vec::new();
let mut k = 0;
let mut brace = 0i32;
@@ -211,7 +215,6 @@ pub fn rewrite(src: &str) -> String {
if next == Some("{") || next == Some(";") {
let params = split_args(&toks, &sig, k + 3, close);
let mut samplers = Vec::new();
let mut any = false;
for &(a, b) in &params {
let has = (a..b).any(|q| toks[sig[q]].text == "sampler2D");
if has {
@@ -220,7 +223,6 @@ pub fn rewrite(src: &str) -> String {
.map(|q| &toks[sig[q]])
.find(|t| t.kind == Kind::Ident && t.text != "sampler2D")
.map(|t| t.text.to_owned());
any |= name.is_some();
samplers.push(name);
} else {
samplers.push(None);
@@ -231,12 +233,12 @@ pub fn rewrite(src: &str) -> String {
} else {
None
};
if any {
funcs
.entry(toks[sig[name_k]].text)
.or_insert_with(|| FnInfo {
samplers: samplers.clone(),
});
// Every overload is recorded (also the ones without
// samplers) so call sites can tell them apart.
let layout: SamplerLayout = samplers.iter().map(Option::is_some).collect();
let list = funcs.entry(toks[sig[name_k]].text).or_default();
if !list.contains(&layout) {
list.push(layout);
}
defs.push(Def {
name_k,
@@ -293,18 +295,32 @@ pub fn rewrite(src: &str) -> String {
if def_names.contains(&k) {
continue;
}
let Some(info) = funcs.get(t.text) else {
let Some(layouts) = funcs.get(t.text) else {
continue;
};
let Some(close) = matching(&toks, &sig, k + 1) else {
continue;
};
let args = split_args(&toks, &sig, k + 2, close);
for (ai, &(a, b)) in args.iter().enumerate() {
if info.samplers.get(ai).is_some_and(Option::is_some)
&& b == a + 1
// Sampler parameters of the function this call is in.
let enclosing: Vec<&str> = defs
.iter()
.filter(|d| d.body.is_some_and(|(o, c)| o < k && k < c))
.flat_map(|d| d.samplers.iter().flatten().map(String::as_str))
.collect();
let is_sampler_arg = |&(a, b): &(usize, usize)| {
b == a + 1
&& toks[sig[a]].kind == Kind::Ident
{
&& (GLOBAL_SAMPLERS.contains(&toks[sig[a]].text)
|| enclosing.contains(&toks[sig[a]].text))
};
let actual: SamplerLayout = args.iter().map(is_sampler_arg).collect();
// Pick the overload with this arity and sampler positions.
if !layouts.contains(&actual) {
continue;
}
for (ai, &(a, _)) in args.iter().enumerate() {
if actual[ai] {
let n = toks[sig[a]].text;
repl.insert(sig[a], format!("{n}_tex, {n}_smp"));
}
@@ -336,6 +352,35 @@ mod tests {
assert!(out.contains("fetch(iChannel0_tex, iChannel0_smp, p)"));
}
#[test]
fn overloads_are_keyed_by_signature() {
let out = rewrite(
"vec4 f(sampler2D s, vec2 uv) { return texture(s, uv); }\n\
vec4 f(vec2 uv, sampler2D s) { return texture(s, uv); }\n\
vec4 f(vec2 uv, float x) { return vec4(uv, x, 1.0); }\n\
void mainImage(out vec4 c, in vec2 p) {\n\
c = f(iChannel0, p) + f(p, iChannel1) + f(p, 1.0);\n\
}",
);
assert!(out.contains("vec4 f(texture2D s_tex, sampler s_smp, vec2 uv)"));
assert!(out.contains("vec4 f(vec2 uv, texture2D s_tex, sampler s_smp)"));
assert!(out.contains("vec4 f(vec2 uv, float x)"));
assert!(out.contains("f(iChannel0_tex, iChannel0_smp, p)"));
assert!(out.contains("f(p, iChannel1_tex, iChannel1_smp)"));
assert!(out.contains("f(p, 1.0)"));
}
#[test]
fn overloads_through_sampler_parameters() {
let out = rewrite(
"float g(sampler2D t) { return texture(t, vec2(0.0)).x; }\n\
float g(float t) { return t; }\n\
float h(sampler2D u, float v) { return g(u) + g(v); }\n",
);
assert!(out.contains("g(u_tex, u_smp)"));
assert!(out.contains("+ g(v)"));
}
#[test]
fn leaves_plain_code_and_rewrites_legacy_texture2d() {
let src = "void mainImage(out vec4 c, in vec2 p) { c = texture2D(iChannel0, p); }";