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
405 lines
14 KiB
Rust
405 lines
14 KiB
Rust
//! Source-level adaptation of combined image samplers for naga.
|
|
//!
|
|
//! glslang (Vulkan GLSL) accepts `sampler2D` as a function parameter type and
|
|
//! lets shaders pass the combined `iChannel0` around. naga's GLSL frontend
|
|
//! has no combined sampler *type*, only separate `texture2D` + `sampler`
|
|
//! objects (and a `sampler2D(texture, sampler)` constructor expression that
|
|
//! is valid only directly inside texture calls).
|
|
//!
|
|
//! This module rewrites user code so such helpers work:
|
|
//!
|
|
//! * `T f(.., [qual] sampler2D s, ..)` -> `T f(.., texture2D s_tex, sampler s_smp, ..)`
|
|
//! * uses of `s` in the function body -> `sampler2D(s_tex, s_smp)`
|
|
//! * `f(.., s, ..)` / `f(.., iChannel0, ..)` call arguments in sampler
|
|
//! positions -> `s_tex, s_smp` / `iChannel0_tex, iChannel0_smp`
|
|
//! * legacy `texture2D(` calls -> `texture(` (glslang gets this from the
|
|
//! prefix macro `#define texture2D texture`, which would otherwise clash
|
|
//! with the `texture2D` type).
|
|
//!
|
|
//! 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.
|
|
//!
|
|
//! 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;
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
enum Kind {
|
|
/// Whitespace, comments and preprocessor lines (not significant).
|
|
Trivia,
|
|
Ident,
|
|
Punct,
|
|
Other,
|
|
}
|
|
|
|
struct Tok<'a> {
|
|
kind: Kind,
|
|
text: &'a str,
|
|
}
|
|
|
|
fn lex(src: &str) -> Vec<Tok<'_>> {
|
|
let b = src.as_bytes();
|
|
let mut toks = Vec::new();
|
|
let mut i = 0;
|
|
let mut line_start = true;
|
|
while i < b.len() {
|
|
let start = i;
|
|
let c = b[i];
|
|
let kind;
|
|
if c.is_ascii_whitespace() {
|
|
while i < b.len() && b[i].is_ascii_whitespace() {
|
|
if b[i] == b'\n' {
|
|
line_start = true;
|
|
}
|
|
i += 1;
|
|
}
|
|
// line_start must reflect "only whitespace since newline".
|
|
toks.push(Tok {
|
|
kind: Kind::Trivia,
|
|
text: &src[start..i],
|
|
});
|
|
continue;
|
|
} else if c == b'/' && b.get(i + 1) == Some(&b'/') {
|
|
while i < b.len() && b[i] != b'\n' {
|
|
i += 1;
|
|
}
|
|
kind = Kind::Trivia;
|
|
} else if c == b'/' && b.get(i + 1) == Some(&b'*') {
|
|
i += 2;
|
|
while i + 1 < b.len() && !(b[i] == b'*' && b[i + 1] == b'/') {
|
|
i += 1;
|
|
}
|
|
i = (i + 2).min(b.len());
|
|
kind = Kind::Trivia;
|
|
} else if c == b'#' && line_start {
|
|
// Preprocessor line (with backslash continuations).
|
|
while i < b.len() && b[i] != b'\n' {
|
|
if b[i] == b'\\' && b.get(i + 1) == Some(&b'\n') {
|
|
i += 1;
|
|
}
|
|
i += 1;
|
|
}
|
|
kind = Kind::Trivia;
|
|
} else if c.is_ascii_alphabetic() || c == b'_' {
|
|
while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_') {
|
|
i += 1;
|
|
}
|
|
kind = Kind::Ident;
|
|
} else if c.is_ascii_digit() || (c == b'.' && b.get(i + 1).is_some_and(u8::is_ascii_digit))
|
|
{
|
|
while i < b.len() && (b[i].is_ascii_alphanumeric() || b[i] == b'_' || b[i] == b'.') {
|
|
i += 1;
|
|
}
|
|
kind = Kind::Other;
|
|
} else {
|
|
// Any single (possibly multi-byte) character.
|
|
i += 1;
|
|
while i < b.len() && (b[i] & 0xC0) == 0x80 {
|
|
i += 1;
|
|
}
|
|
kind = Kind::Punct;
|
|
}
|
|
line_start = false;
|
|
toks.push(Tok {
|
|
kind,
|
|
text: &src[start..i],
|
|
});
|
|
}
|
|
toks
|
|
}
|
|
|
|
fn is_sig(t: &Tok<'_>) -> bool {
|
|
t.kind != Kind::Trivia
|
|
}
|
|
|
|
/// Index of the token matching the opener at `sig[open]` (indices into the
|
|
/// significant-token list), or None.
|
|
fn matching(toks: &[Tok<'_>], sig: &[usize], open: usize) -> Option<usize> {
|
|
let o = toks[sig[open]].text;
|
|
let c = match o {
|
|
"(" => ")",
|
|
"{" => "}",
|
|
"[" => "]",
|
|
_ => return None,
|
|
};
|
|
let mut depth = 0usize;
|
|
for (k, &ti) in sig.iter().enumerate().skip(open) {
|
|
let t = toks[ti].text;
|
|
if t == o {
|
|
depth += 1;
|
|
} else if t == c {
|
|
depth -= 1;
|
|
if depth == 0 {
|
|
return Some(k);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Split the significant range `(from..to)` at top-level commas, returning
|
|
/// `(start, end)` ranges (exclusive end) in significant-token indices.
|
|
fn split_args(toks: &[Tok<'_>], sig: &[usize], from: usize, to: usize) -> Vec<(usize, usize)> {
|
|
let mut out = Vec::new();
|
|
if from >= to {
|
|
return out;
|
|
}
|
|
let mut depth = 0i32;
|
|
let mut start = from;
|
|
for k in from..to {
|
|
match toks[sig[k]].text {
|
|
"(" | "[" | "{" => depth += 1,
|
|
")" | "]" | "}" => depth -= 1,
|
|
"," if depth == 0 => {
|
|
out.push((start, k));
|
|
start = k + 1;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
out.push((start, to));
|
|
out
|
|
}
|
|
|
|
/// A top-level function definition/prototype (indices are into the
|
|
/// significant-token list).
|
|
struct Def {
|
|
name_k: usize,
|
|
params: Vec<(usize, usize)>,
|
|
body: Option<(usize, usize)>,
|
|
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(`.
|
|
pub fn rewrite(src: &str) -> String {
|
|
let toks = lex(src);
|
|
let sig: Vec<usize> = toks
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, t)| is_sig(t))
|
|
.map(|(i, _)| i)
|
|
.collect();
|
|
|
|
// Pass 1: find top-level functions with sampler2D parameters.
|
|
let mut funcs: HashMap<&str, Vec<SamplerLayout>> = HashMap::new();
|
|
let mut defs: Vec<Def> = Vec::new();
|
|
let mut k = 0;
|
|
let mut brace = 0i32;
|
|
while k < sig.len() {
|
|
let t = &toks[sig[k]];
|
|
match t.text {
|
|
"{" => brace += 1,
|
|
"}" => brace -= 1,
|
|
_ => {}
|
|
}
|
|
if brace == 0
|
|
&& t.kind == Kind::Ident
|
|
&& k + 2 < sig.len()
|
|
&& toks[sig[k + 1]].kind == Kind::Ident
|
|
&& toks[sig[k + 2]].text == "("
|
|
{
|
|
let name_k = k + 1;
|
|
if let Some(close) = matching(&toks, &sig, k + 2) {
|
|
let next = sig.get(close + 1).map(|&i| toks[i].text);
|
|
if next == Some("{") || next == Some(";") {
|
|
let params = split_args(&toks, &sig, k + 3, close);
|
|
let mut samplers = Vec::new();
|
|
for &(a, b) in ¶ms {
|
|
let has = (a..b).any(|q| toks[sig[q]].text == "sampler2D");
|
|
if has {
|
|
let name = (a..b)
|
|
.rev()
|
|
.map(|q| &toks[sig[q]])
|
|
.find(|t| t.kind == Kind::Ident && t.text != "sampler2D")
|
|
.map(|t| t.text.to_owned());
|
|
samplers.push(name);
|
|
} else {
|
|
samplers.push(None);
|
|
}
|
|
}
|
|
let body = if next == Some("{") {
|
|
matching(&toks, &sig, close + 1).map(|e| (close + 1, e))
|
|
} else {
|
|
None
|
|
};
|
|
// 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,
|
|
params,
|
|
body,
|
|
samplers,
|
|
});
|
|
// Skip past the signature (bodies are scanned for
|
|
// braces by the normal loop).
|
|
k = close + 1;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
k += 1;
|
|
}
|
|
|
|
// Pass 2: collect replacements keyed by token index.
|
|
let mut repl: HashMap<usize, String> = HashMap::new();
|
|
let mut def_names: Vec<usize> = Vec::new();
|
|
for d in &defs {
|
|
def_names.push(d.name_k);
|
|
for (pi, &(a, b)) in d.params.iter().enumerate() {
|
|
if let Some(name) = d.samplers.get(pi).and_then(Option::as_ref) {
|
|
repl.insert(sig[a], format!("texture2D {name}_tex, sampler {name}_smp"));
|
|
// Blank the rest of the parameter, including interior trivia.
|
|
for ti in (sig[a] + 1)..=sig[b - 1] {
|
|
repl.insert(ti, String::new());
|
|
}
|
|
}
|
|
}
|
|
// Uses of the sampler parameters in the body.
|
|
if let Some((open, close)) = d.body {
|
|
let names: Vec<&String> = d.samplers.iter().flatten().collect();
|
|
for q in open + 1..close {
|
|
let t = &toks[sig[q]];
|
|
if t.kind == Kind::Ident && names.iter().any(|n| n.as_str() == t.text) {
|
|
repl.insert(sig[q], format!("sampler2D({0}_tex, {0}_smp)", t.text));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call sites (and legacy texture2D).
|
|
for k in 0..sig.len() {
|
|
let t = &toks[sig[k]];
|
|
if t.kind != Kind::Ident || sig.get(k + 1).map(|&i| toks[i].text) != Some("(") {
|
|
continue;
|
|
}
|
|
if t.text == "texture2D" {
|
|
repl.insert(sig[k], "texture".to_owned());
|
|
continue;
|
|
}
|
|
if def_names.contains(&k) {
|
|
continue;
|
|
}
|
|
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);
|
|
// 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"));
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut out = String::with_capacity(src.len() + 64);
|
|
for (i, t) in toks.iter().enumerate() {
|
|
match repl.get(&i) {
|
|
Some(r) => out.push_str(r),
|
|
None => out.push_str(t.text),
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn rewrites_param_use_and_call() {
|
|
let out = rewrite(
|
|
"vec4 fetch(sampler2D tex, vec2 uv) { return texture(tex, uv); }\n\
|
|
void mainImage(out vec4 c, in vec2 p) { c = fetch(iChannel0, p); }",
|
|
);
|
|
assert!(out.contains("vec4 fetch(texture2D tex_tex, sampler tex_smp, vec2 uv)"));
|
|
assert!(out.contains("texture(sampler2D(tex_tex, tex_smp), uv)"));
|
|
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); }";
|
|
assert_eq!(
|
|
rewrite(src),
|
|
"void mainImage(out vec4 c, in vec2 p) { c = texture(iChannel0, p); }"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ignores_comments_and_forwards_samplers() {
|
|
let out = rewrite(
|
|
"// sampler2D in comment\n\
|
|
vec4 a(in sampler2D s) { return texture(s, vec2(0.0)); }\n\
|
|
vec4 b(sampler2D t, float x) { return a(t) * x; }\n",
|
|
);
|
|
assert!(out.contains("// sampler2D in comment"));
|
|
assert!(out.contains("vec4 a(texture2D s_tex, sampler s_smp)"));
|
|
assert!(out.contains("return a(t_tex, t_smp) * x;"));
|
|
}
|
|
}
|