makepad/platform/script/tests/shader_while_call_cond.rs
Admin 62d4f31b1e script: Splash VM heaps, isolates and lazy system modules
Every heap ref carries its heap tag, generation checks always on, rooted
async values. Splash isolates get a domain table and curated minimal
preludes; system modules under `mod` expand lazily on first touch. No
wall-clock script caps: isolation and force-stop instead.

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

109 lines
5.6 KiB
Rust

//! A `while` whose condition calls a function on a var the body reassigns
//! (render's CSM cascade search: `while ci < 3.5 && self.csm_inside(q, 0.99)
//! < 0.5 { ci = ci + 1.0 q = self.csm_proj(min(ci, 3.0), wp) }`) re-tests
//! the whole condition, call included, at the top of every pass, on every
//! backend. The GPU result is checked in the platform's metal_gpu_tests.
use makepad_script::*;
fn compile(backend: &str, fragment_body: &str, extra: &str) -> String {
let host = Box::leak(Box::new(ScriptVmHost::new(0i32, ())));
let mut vm = ScriptVm {
host: host.host_context(),
bx: Box::new(ScriptVmBase::new()),
};
vm.bx.captured_errors = Some(Vec::new());
let code = format!("use mod.pod.*\nuse mod.math.*\nuse mod.shader\nlet sh = {{\nvertex_pos: shader.vertex_position(vec4f)\npixel: shader.fragment_output(0, vec4f)\nu_n: shader.uniform(0.0)\n{extra}\nvertex: fn() {{\nself.vertex_pos = vec4(0.0, 0.0, 0.0, 1.0)\n}}\nfragment: fn() {{\n{fragment_body}\n}}\n}}\nshader.test_compile_draw_source(sh, \"{backend}\", false)");
let value = vm.with_instruction_limit(5_000_000, |vm| vm.eval(ScriptMod::new("", "", "x", 0, 0, &code, &[])));
let src = vm.bx.heap.string_with(value, |_, s| s.to_string()).unwrap_or_else(|| format!("ERRORS: not a string {value:?}"));
wgsl_plain(&src)
}
/// The IR printers' locals (`l3_f`) and WGSL's typed literals (`2.0f`, `0i`)
/// as the other emitters spell them (`l_f`, `2.0`, `0`), so one needle checks
/// every backend.
fn wgsl_plain(src: &str) -> String {
let b = src.as_bytes();
let mut out = String::with_capacity(src.len());
let mut i = 0;
while i < b.len() {
let c = b[i];
let prev_ident = i > 0 && (b[i - 1].is_ascii_alphanumeric() || b[i - 1] == b'_');
// `l<digits>_` at the start of an identifier.
if c == b'l' && !prev_ident {
let mut j = i + 1;
while j < b.len() && b[j].is_ascii_digit() {
j += 1;
}
if j > i + 1 && j < b.len() && b[j] == b'_' {
out.push_str("l_");
i = j + 1;
continue;
}
}
// A number's `f` / `i` suffix.
if (c == b'f' || c == b'i') && i > 0 && b[i - 1].is_ascii_digit() && !(i + 1 < b.len() && (b[i + 1].is_ascii_alphanumeric() || b[i + 1] == b'_')) {
let mut j = i;
while j > 0 && (b[j - 1].is_ascii_digit() || b[j - 1] == b'.' || b[j - 1] == b'e' || b[j - 1] == b'-') {
j -= 1;
}
let starts_number = j == 0 || !(b[j - 1].is_ascii_alphanumeric() || b[j - 1] == b'_');
if starts_number {
i += 1;
continue;
}
}
out.push(c as char);
i += 1;
}
out
}
/// The emitted body of the loop that follows q's declaration: from its
/// `while(true){` / `while (true) {` / `loop{` to the matching close.
fn loop_body(src: &str) -> &str {
let start = match (src.find("l_q = "), src.find("l_q: ")) {
(Some(a), Some(b)) => a.min(b),
(a, b) => a.or(b).expect("q declared"),
};
let rest = &src[start..];
let open = rest.find("while(true){").or_else(|| rest.find("while (true) {")).or_else(|| rest.find("loop{")).or_else(|| rest.find("loop {")).expect("loop");
let body = &rest[open..];
let mut depth = 0;
for (i, c) in body.char_indices() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return &body[..=i];
}
}
_ => {}
}
}
panic!("unclosed loop in\n{src}")
}
#[test]
fn a_while_with_a_called_condition_retests_it_every_pass() {
let extra = "inside: fn(q: vec3, m: float) -> float {\nif q.x > m { return 0.0 }\nreturn 1.0\n}\nproj: fn(ci: float) -> vec3 {\nreturn vec3(2.0 - ci + self.u_n, 0.0, 0.0)\n}";
let body = "var ci = 0.0\nvar q = self.proj(0.0)\nwhile ci < 3.5 && self.inside(q, 0.99) < 0.5 {\nci = ci + 1.0\nq = self.proj(min(ci, 3.0))\n}\nself.pixel = vec4(ci, q.x, 0.0, 1.0)";
for backend in ["metal", "hlsl", "glsl", "wgsl"] {
let src = compile(backend, body, extra);
assert!(!src.starts_with("ERRORS"), "{backend}: {src}");
let lp = loop_body(&src);
let lines: Vec<&str> = lp.lines().map(str::trim).collect();
// The guard, then the whole condition (the call included) as the
// pass's exit test, then the body's two reassignments, in order.
let cond = lines.iter().position(|l| (l.starts_with("if(!(") || l.starts_with("if ((!(")) && l.contains("< 3.5") && l.contains("inside(") && (l.ends_with("{break;}") || l.ends_with("{ break; }"))).unwrap_or_else(|| panic!("{backend}: no in-loop condition test\n{lp}"));
let ci = lines.iter().position(|l| l.starts_with("l_ci = (l_ci + 1")).unwrap_or_else(|| panic!("{backend}: no ci step\n{lp}"));
let q = lines.iter().position(|l| l.starts_with("l_q = ") && l.contains("proj(") && l.contains("min(l_ci, 3.0)")).unwrap_or_else(|| panic!("{backend}: no q reassignment\n{lp}"));
assert!(cond < ci && ci < q, "{backend}: condition, step, reassignment out of order\n{lp}");
// The reassigned var is the one the condition reads: no copy made
// before the loop stands in for it.
assert!(lines[cond].contains("inside(") && lines[cond].contains("l_q, 0.99"), "{backend}: {}", lines[cond]);
// Declared once, before the loop (a declaration inside would shadow).
assert!(!lp.contains("float3 l_q") && !lp.contains("vec3 l_q") && !lp.contains("var l_q"), "{backend}: q redeclared in the loop\n{lp}");
}
}