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:
1 parent
96314c1a8b
commit
f6002d7519
22 files changed
+2079
-57
No files matched your search
@@ -371,7 +371,118 @@ pub fn msl_from_compiled(compiled: &Compiled) -> Result<String, Error> {
|
||||
|
||||
let (msl, _info) = msl_out::write_string(&module, &compiled.info, &options, &pipeline)
|
||||
.map_err(|e| fail(&e))?;
|
||||
Ok(msl)
|
||||
Ok(fix_uniform_array_strides(&module, msl))
|
||||
}
|
||||
|
||||
/// A uniform-block member that is an array of scalars. std140 gives it a
|
||||
/// 16-byte element stride (what we upload and what spirv-cross emits padded
|
||||
/// types for), but naga's MSL writer ignores the array `stride` and emits a
|
||||
/// tightly packed `float[N]`.
|
||||
struct StridedArray {
|
||||
member: String,
|
||||
/// naga's MSL name of the array wrapper struct (`type_<handle index>`).
|
||||
type_name: String,
|
||||
/// MSL scalar element type.
|
||||
scalar: &'static str,
|
||||
len: u32,
|
||||
/// Lanes of the padded element (stride / scalar size).
|
||||
lanes: u32,
|
||||
}
|
||||
|
||||
fn strided_uniform_arrays(module: &naga::Module) -> Vec<StridedArray> {
|
||||
let mut out = Vec::new();
|
||||
for (_, gv) in module.global_variables.iter() {
|
||||
if gv.space != naga::AddressSpace::Uniform {
|
||||
continue;
|
||||
}
|
||||
let naga::TypeInner::Struct { members, .. } = &module.types[gv.ty].inner else {
|
||||
continue;
|
||||
};
|
||||
for m in members {
|
||||
let naga::TypeInner::Array {
|
||||
base,
|
||||
size: naga::ArraySize::Constant(n),
|
||||
stride,
|
||||
} = &module.types[m.ty].inner
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let naga::TypeInner::Scalar(sc) = &module.types[*base].inner else {
|
||||
continue;
|
||||
};
|
||||
let scalar = match (sc.kind, sc.width) {
|
||||
(naga::ScalarKind::Float, 4) => "float",
|
||||
(naga::ScalarKind::Sint, 4) => "int",
|
||||
(naga::ScalarKind::Uint, 4) => "uint",
|
||||
_ => continue,
|
||||
};
|
||||
let size = u32::from(sc.width);
|
||||
if *stride <= size {
|
||||
continue;
|
||||
}
|
||||
let (Some(name), true) = (m.name.clone(), stride % size == 0) else {
|
||||
continue;
|
||||
};
|
||||
out.push(StridedArray {
|
||||
member: name,
|
||||
type_name: format!("type_{}", m.ty.index()),
|
||||
scalar,
|
||||
len: n.get(),
|
||||
lanes: stride / size,
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Make the MSL uniform block match the std140 layout we upload: scalar
|
||||
/// arrays become arrays of `vecN` (stride 16 for std140) and element reads
|
||||
/// take `.x`. The writer ignores array strides, so this is done on the text.
|
||||
fn fix_uniform_array_strides(module: &naga::Module, mut msl: String) -> String {
|
||||
for a in strided_uniform_arrays(module) {
|
||||
let wrapper = format!("{}_std140", a.type_name);
|
||||
// 1. The block member uses the padded wrapper.
|
||||
let decl = format!(" {} {};", a.type_name, a.member);
|
||||
if !msl.contains(&decl) {
|
||||
continue;
|
||||
}
|
||||
msl = msl.replace(&decl, &format!(" {wrapper} {};", a.member));
|
||||
// 2. Define the wrapper before the struct that uses it.
|
||||
let def = format!(
|
||||
"struct {wrapper} {{\n metal::vec<{}, {}> inner[{}];\n}};\n",
|
||||
a.scalar, a.lanes, a.len
|
||||
);
|
||||
if let Some(at) = msl.find(&format!("\n {wrapper} {};", a.member))
|
||||
&& let Some(start) = msl[..at].rfind("\nstruct ")
|
||||
{
|
||||
msl.insert_str(start + 1, &def);
|
||||
}
|
||||
// 3. Element reads: `<expr>.member.inner[idx]` -> `... [idx].x`.
|
||||
let needle = format!(".{}.inner[", a.member);
|
||||
let mut from = 0;
|
||||
while let Some(rel) = msl[from..].find(&needle) {
|
||||
let open = from + rel + needle.len() - 1;
|
||||
let mut depth = 0usize;
|
||||
let mut close = None;
|
||||
for (k, ch) in msl[open..].char_indices() {
|
||||
match ch {
|
||||
'[' => depth += 1,
|
||||
']' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
close = Some(open + k);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let Some(close) = close else { break };
|
||||
msl.insert_str(close + 1, ".x");
|
||||
from = close + 3;
|
||||
}
|
||||
}
|
||||
msl
|
||||
}
|
||||
|
||||
/// Convert a compiled shader to GLSL 430 core (`glslFromSpv`).
|
||||
@@ -628,4 +739,124 @@ mod tests {
|
||||
assert_eq!(offset_of!(Uniforms, selection_foreground_color), 4480);
|
||||
assert_eq!(size_of::<Uniforms>(), 4496);
|
||||
}
|
||||
|
||||
/// (size, align) of an MSL type as emitted by naga for the shadertoy
|
||||
/// uniform block, resolving the wrapper structs defined in `msl`.
|
||||
fn msl_size_align(msl: &str, ty: &str) -> (usize, usize) {
|
||||
match ty {
|
||||
"float" | "int" | "uint" => (4, 4),
|
||||
"metal::packed_float3" => (12, 4),
|
||||
"metal::float2" => (8, 8),
|
||||
"metal::float3" | "metal::float4" => (16, 16),
|
||||
_ => {
|
||||
if let Some(rest) = ty.strip_prefix("metal::vec<") {
|
||||
let (scalar, n) = rest.trim_end_matches('>').split_once(", ").unwrap();
|
||||
let n: usize = n.parse().unwrap();
|
||||
assert!(matches!(scalar, "float" | "int" | "uint"));
|
||||
let n = if n == 3 { 4 } else { n };
|
||||
return (4 * n, 4 * n);
|
||||
}
|
||||
// A wrapper struct `struct <ty> { <elem> inner[N]; };`.
|
||||
let head = format!("struct {ty} {{\n ");
|
||||
let at = msl.find(&head).unwrap_or_else(|| panic!("no struct {ty}"));
|
||||
let body = &msl[at + head.len()..];
|
||||
let line = &body[..body.find(';').unwrap()];
|
||||
let (elem, count) = line.rsplit_once(" inner[").unwrap();
|
||||
let count: usize = count.trim_end_matches(']').parse().unwrap();
|
||||
let (es, ea) = msl_size_align(msl, elem);
|
||||
(es * count, ea)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `(offset, name)` of each member of `struct Globals` in `msl`,
|
||||
/// skipping `char _padN[k]` fillers, plus the struct size.
|
||||
fn msl_globals_layout(msl: &str) -> (Vec<(usize, String)>, usize) {
|
||||
let at = msl.find("struct Globals {\n").unwrap();
|
||||
let body = &msl[at + "struct Globals {\n".len()..];
|
||||
let body = &body[..body.find("\n};").unwrap()];
|
||||
let mut off = 0usize;
|
||||
let mut max_align = 1;
|
||||
let mut members = Vec::new();
|
||||
for line in body.lines() {
|
||||
let line = line.trim().trim_end_matches(';');
|
||||
let (ty, name) = line.rsplit_once(' ').unwrap();
|
||||
if ty == "char" {
|
||||
let n: usize = name
|
||||
.split('[')
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.trim_end_matches(']')
|
||||
.parse()
|
||||
.unwrap();
|
||||
off += n;
|
||||
continue;
|
||||
}
|
||||
let (size, align) = msl_size_align(msl, ty);
|
||||
off = off.next_multiple_of(align);
|
||||
max_align = max_align.max(align);
|
||||
members.push((off, name.to_owned()));
|
||||
off += size;
|
||||
}
|
||||
(members, off.next_multiple_of(max_align))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn msl_uniform_block_matches_std140_upload_layout() {
|
||||
use std::mem::offset_of as o;
|
||||
// A shader that reads the scalar array so the accessor path is used.
|
||||
let user = "void mainImage(out vec4 c, in vec2 p) { \
|
||||
int i = int(p.x) & 3; c = vec4(iChannelTime[i], iChannelTime[2], iTime, iSampleRate); }";
|
||||
let compiled = compile_shadertoy(user, None).unwrap();
|
||||
let msl = msl_from_compiled(&compiled).unwrap();
|
||||
let (members, size) = msl_globals_layout(&msl);
|
||||
|
||||
let expect = [
|
||||
("iResolution", o!(Uniforms, resolution)),
|
||||
("iTime", o!(Uniforms, time)),
|
||||
("iTimeDelta", o!(Uniforms, time_delta)),
|
||||
("iFrameRate", o!(Uniforms, frame_rate)),
|
||||
("iFrame", o!(Uniforms, frame)),
|
||||
("iChannelTime", o!(Uniforms, channel_time)),
|
||||
("iChannelResolution", o!(Uniforms, channel_resolution)),
|
||||
("iMouse", o!(Uniforms, mouse)),
|
||||
("iDate", o!(Uniforms, date)),
|
||||
("iSampleRate", o!(Uniforms, sample_rate)),
|
||||
("iCurrentCursor", o!(Uniforms, current_cursor)),
|
||||
("iPreviousCursor", o!(Uniforms, previous_cursor)),
|
||||
("iCurrentCursorColor", o!(Uniforms, current_cursor_color)),
|
||||
("iPreviousCursorColor", o!(Uniforms, previous_cursor_color)),
|
||||
("iCurrentCursorStyle", o!(Uniforms, current_cursor_style)),
|
||||
("iPreviousCursorStyle", o!(Uniforms, previous_cursor_style)),
|
||||
("iCursorVisible", o!(Uniforms, cursor_visible)),
|
||||
("iTimeCursorChange", o!(Uniforms, cursor_change_time)),
|
||||
("iTimeFocus", o!(Uniforms, time_focus)),
|
||||
("iFocus", o!(Uniforms, focus)),
|
||||
("iPalette", o!(Uniforms, palette)),
|
||||
("iBackgroundColor", o!(Uniforms, background_color)),
|
||||
("iForegroundColor", o!(Uniforms, foreground_color)),
|
||||
("iCursorColor", o!(Uniforms, cursor_color)),
|
||||
("iCursorText", o!(Uniforms, cursor_text)),
|
||||
// Upstream quirk: the Zig struct (and so ours) has the selection
|
||||
// colors swapped relative to the GLSL block; both are vec3 so the
|
||||
// offsets agree.
|
||||
(
|
||||
"iSelectionForegroundColor",
|
||||
o!(Uniforms, selection_background_color),
|
||||
),
|
||||
(
|
||||
"iSelectionBackgroundColor",
|
||||
o!(Uniforms, selection_foreground_color),
|
||||
),
|
||||
];
|
||||
assert_eq!(members.len(), expect.len(), "{msl}");
|
||||
for ((off, name), (ename, eoff)) in members.iter().zip(expect) {
|
||||
assert_eq!(name, ename);
|
||||
assert_eq!(*off, eoff, "MSL offset of {name}\n{msl}");
|
||||
}
|
||||
assert_eq!(size, std::mem::size_of::<Uniforms>());
|
||||
// Reads of the padded array take the first lane.
|
||||
assert!(msl.contains(".iChannelTime.inner["), "{msl}");
|
||||
assert!(msl.contains("].x"), "{msl}");
|
||||
}
|
||||
}
|
||||
@@ -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 ¶ms {
|
||||
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); }";
|
||||
|
||||
Reference in new issue
Block a user