makepad/libs/render_graph/tests/kits_compile.rs
Admin 53e0e85b92 draw and widgets: Slug text everywhere, no blank glyphs, slimmer widgets
All outline text draws with Slug curves on every platform (SDF/MSDF stays
selectable per size); a frame never presents a blank glyph and retained draw
lists keep theirs. One coverage model for vector feathering, widget families
split out of the core crate, fewer steady-state allocations, video keeps its
aspect (Contain/Cover), render and SSAO pass unification.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:26:21 +02:00

216 lines
10 KiB
Rust

//! Every standard kit evaluates in Splash, reads as passes, and each pass
//! compiles on every backend the shader compiler writes; the accumulator's
//! and the standard block's own shaders compile too. (Shader errors
//! otherwise only show at runtime, as a pass that draws nothing.)
use makepad_draw::*;
use makepad_render_graph::{kits, pass, script, UniformDecl};
const BACKENDS: [&str; 4] = ["metal", "hlsl", "glsl", "wgsl"];
fn compile_value(vm: &mut ScriptVm, file: &str, code: String) -> Result<String, String> {
vm.capture_errors();
let v = vm.eval(ScriptMod::new("", "", &file, 0, 0, &code, &[]));
let errors = vm.take_errors();
if !errors.is_empty() || v.is_err() {
return Err(errors.join("; "));
}
let text = vm.heap().string_with(v, |_, s| s.to_string()).ok_or("the compiler gave no source")?;
if text.starts_with("ERRORS") {
return Err(text);
}
Ok(text)
}
/// The pass's shader (an object built from the decl, its fns moved by their
/// tokens) compiled for `backend`.
fn compile_for(vm: &mut ScriptVm, decl: &pass::PassDecl, backend: &str) -> Result<String, String> {
let obj = makepad_render_graph::program::pass_object(vm, decl)?;
let shader = vm.value(vm.modules(), LiveId::from_str("shader")).as_object().unwrap();
let f = vm.value(shader, LiveId::from_str("test_compile_draw_source"));
let b = vm.new_string_with(|_, s| s.push_str(backend));
vm.capture_errors();
let v = vm.call(f, &[obj.into(), b, false.into()]);
let errors = vm.take_errors();
if !errors.is_empty() || v.is_err() {
return Err(errors.join("; "));
}
let text = vm.heap().string_with(v, |_, s| s.to_string()).ok_or("the compiler gave no source")?;
if text.starts_with("ERRORS") {
return Err(text);
}
Ok(text)
}
fn compile_pass(vm: &mut ScriptVm, decl: &pass::PassDecl) {
for backend in BACKENDS {
if let Err(e) = compile_for(vm, decl, backend) {
panic!("{} ({backend}) did not compile: {e}", decl.label);
}
}
}
/// A Splash fn of this VM as a pass's code.
fn code(vm: &mut ScriptVm, src: &str) -> pass::PassCode {
let v = vm.eval(ScriptMod::new("", "", "kits_compile_code", 0, 0, src, &[]));
pass::PassCode::Fn(pass::PassFn::new(vm.fn_image(v.as_object().unwrap()).unwrap()))
}
#[test]
fn every_kit_builds_passes_that_compile_everywhere() {
let mut cx = Cx::new(Box::new(|_, _| {}));
cx.with_vm(|vm| {
makepad_draw::script_mod(vm);
// The passes' shared stdlib (`use mod.shared.*` in program and accum).
makepad_render_graph::pass_stdlib(vm);
makepad_render_graph::program::script_mod(vm);
makepad_render_graph::accum::script_mod(vm);
// A host module with the kits' types (the host's own markers).
let m = vm.new_module(LiveId::from_str("gtest"));
for k in kits::KITS {
let proto = vm.new_object();
vm.set_value_def(proto, LiveId::from_str("__kind"), LiveId::from_str(k.kind));
vm.set_value_def(m, LiveId::from_str(k.name), proto);
}
let errors = script::install_kits(vm, "mod.gtest", None);
assert!(errors.is_empty(), "kits did not evaluate: {errors:?}");
let mut total = 0;
let mut chain: Vec<pass::PassDecl> = Vec::new();
// Every kit with its defaults, and the variants a parameter selects.
let variants: Vec<(&kits::KitDef, Option<(&str, f64)>)> = kits::KITS.iter().map(|k| (k, None)).chain([("vignette", "mode", 1.0), ("frame_post", "mode", 1.0), ("outline", "mode", 1.0)].into_iter().map(|(kind, f, v)| (kits::kit(kind).unwrap(), Some((f, v))))).collect();
for (k, over) in variants {
let module = vm.value(vm.modules(), LiveId::from_str("gtest")).as_object().unwrap();
let template = vm.value(module, LiveId::from_str(&format!("kit_{}", k.kind)));
let proto = vm.value(module, LiveId::from_str(k.name));
// The author's object: the kit with its defaults.
let author = vm.new_with_proto(proto);
if let Some((field, value)) = over {
vm.set_value_def(author, LiveId::from_str(field), value);
}
vm.capture_errors();
let built = vm.call(template, &[author.into()]);
let errors = vm.take_errors();
assert!(errors.is_empty() && !built.is_err(), "{} did not build: {errors:?}", k.name);
let items: Vec<ScriptValue> = match built.as_array() {
Some(a) => (0..vm.heap().array_len(a)).map(|i| vm.heap().array_index(a, i, NoTrap)).collect(),
None => panic!("{} returned no list", k.name),
};
assert!(!items.is_empty());
let mut decls = Vec::new();
for (i, it) in items.into_iter().enumerate() {
let read = script::read_pass(vm, it, &format!("{}[{i}]", k.name)).unwrap_or_else(|e| panic!("{e}"));
let mut decl = read.decl;
for (name, v) in read.uniforms {
// Every kit parameter is a number or a colour here.
let width = if v.as_number().is_some() { 1 } else { 4 };
decl.uniforms.push(UniformDecl { name, width });
}
decl.validate().unwrap_or_else(|e| panic!("{}: {e}", decl.label));
decls.push(decl);
}
pass::namespace(&mut decls, &format!("k{}_", chain.len()));
for d in &decls {
compile_pass(vm, d);
total += 1;
}
chain.extend(decls);
}
assert!(total >= 20, "{total} passes");
// Every kit one after another: the map passes that follow one
// another fuse, and each fused pass compiles everywhere.
let (fused, members) = pass::fuse(&chain);
let merged: Vec<&pass::PassDecl> = fused.iter().zip(&members).filter(|(_, m)| m.len() > 1).map(|(d, _)| d).collect();
assert!(!merged.is_empty(), "no kit passes fused");
for d in merged {
d.validate().unwrap_or_else(|e| panic!("{}: {e}", d.label));
compile_pass(vm, d);
}
// A raymarch-style pass with further outputs (MRT): a G-buffer and
// a view distance, written as `self.<name>`.
let mrt = pass::PassDecl {
name: Some("march".into()),
stage: makepad_render_graph::Stage::Hdr,
reads: vec!["color".into()],
slots: Vec::new(),
scale: 0.5,
preview_scale: 1.0, scale_height: None,
size: None,
format: None,
uniforms: Vec::new(),
pixel: code(vm, "fn() -> vec4 { self.gbuf = vec4(self.uv(), 0.0, 1.0) self.march_depth = vec4(3.0, 0.0, 0.0, 0.0) return vec4(1.0, 0.5, 0.2, 1.0) }"),
helpers: Vec::new(),
history: false,
outputs: vec![
pass::OutputDecl { name: "gbuf".into(), slot: "gbuf".into(), format: makepad_render_graph::Format::Rgba16f },
pass::OutputDecl { name: "march_depth".into(), slot: "march_depth".into(), format: makepad_render_graph::Format::R32f },
],
label: "mrt".into(),
map: false,
live_literals: false,
};
mrt.validate().unwrap();
compile_pass(vm, &mrt);
// Analytic-derivative sampling (PDOOM-PARITY R9): a warped lookup
// with the derivatives of the unwarped coordinate.
let grad = pass::PassDecl {
name: None,
outputs: Vec::new(),
pixel: code(vm, "fn() -> vec4 { let q = fract(self.uv() * 3.0) return self.color.sample_grad(q, dFdx(self.uv() * 3.0), dFdy(self.uv() * 3.0)) }"),
label: "grad".into(),
map: false,
live_literals: false,
..mrt.clone()
};
compile_pass(vm, &grad);
// And the check has teeth: a pass reading what it does not declare
// is refused.
let bad = pass::PassDecl {
name: None,
stage: makepad_render_graph::Stage::Hdr,
reads: vec!["color".into()],
slots: Vec::new(),
scale: 1.0,
preview_scale: 1.0, scale_height: None,
size: None,
format: None,
uniforms: Vec::new(),
pixel: code(vm, "fn() -> vec4 { return self.nope.sample(self.uv()) }"),
helpers: Vec::new(),
history: false,
outputs: Vec::new(),
label: "bad".into(),
map: false,
live_literals: false,
};
assert!(compile_for(vm, &bad, "metal").is_err());
// Short slot and uniform names (a pass named `g`, a uniform `r`)
// compile: they do not collide with the shader's own members.
let short = pass::PassDecl {
name: None,
stage: makepad_render_graph::Stage::Hdr,
reads: vec!["color".into(), "g".into()],
slots: Vec::new(),
scale: 1.0,
preview_scale: 1.0, scale_height: None,
size: None,
format: None,
uniforms: vec![UniformDecl { name: "r".into(), width: 1 }, UniformDecl { name: "b".into(), width: 4 }],
pixel: code(vm, "fn() -> vec4 { return self.color.sample(self.uv()) + self.g.sample(self.uv()) * self.r + self.b }"),
helpers: Vec::new(),
history: false,
outputs: Vec::new(),
label: "short".into(),
map: false,
live_literals: false,
};
short.validate().unwrap();
compile_pass(vm, &short);
// The accumulator's own shaders.
for name in ["DrawGraphAccum", "DrawGraphMerge", "DrawGraphAverage", "DrawGraphError", "DrawGraphErrorMax", "DrawGraphPass"] {
for backend in BACKENDS {
let code = format!("use mod.draw\nmod.shader.test_compile_draw_source(mod.draw.{name}, \"{backend}\", false)\n");
compile_value(vm, name, code).unwrap_or_else(|e| panic!("{name} ({backend}): {e}"));
}
}
});
}