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>
253 lines
12 KiB
Rust
253 lines
12 KiB
Rust
//! Host components: the registry, calls from native and interpreted
|
|
//! kernels, clamped slices, failures that never unwind into kernel code,
|
|
//! and the validator's checks.
|
|
|
|
use makepad_script_compute::host::{self, ErrorSink, HostCallbacks, HostCallFrame, HostFn, SliceSig, Tier};
|
|
use makepad_script_compute::ir::{self, Op, Program, Region, Regions, SliceArg, Stmt, Ty, Val};
|
|
use makepad_script_compute::kernel::{compile, compile_with, KernelRef};
|
|
use makepad_script_compute_sched::sched::{InlineExecutor, Job};
|
|
use makepad_script_compute::Backend;
|
|
|
|
|
|
unsafe extern "C-unwind" fn sum3(_context: *mut (), frame: &mut HostCallFrame, _errors: ErrorSink<'_>) -> bool {
|
|
let (args, _s, rets) = frame.parts();
|
|
rets[0] = (f32::from_bits(args[0]) + f32::from_bits(args[1]) + f32::from_bits(args[2])).to_bits();
|
|
true
|
|
}
|
|
|
|
unsafe extern "C-unwind" fn boom(_context: *mut (), frame: &mut HostCallFrame, _errors: ErrorSink<'_>) -> bool {
|
|
let (_args, _s, _rets) = frame.parts();
|
|
panic!("a host component bug");
|
|
}
|
|
|
|
/// Writes `value` to every word of its slice; returns the words written.
|
|
unsafe extern "C-unwind" fn fill(_context: *mut (), frame: &mut HostCallFrame, _errors: ErrorSink<'_>) -> bool {
|
|
let (args, s, rets) = frame.parts();
|
|
for i in 0..s[0].len() {
|
|
s[0].set(i, args[0]);
|
|
}
|
|
rets[0] = s[0].len() as u32;
|
|
true
|
|
}
|
|
|
|
fn register() {
|
|
for f in [
|
|
HostFn { name: "test.sum3", params: &[Ty::F32, Ty::F32, Ty::F32], slices: &[], rets: &[Ty::F32], callbacks: unsafe { HostCallbacks::new(std::ptr::null_mut(), scalar_cost, sum3) }, misses: 0, tier: Tier::X, doc: "" },
|
|
HostFn { name: "test.boom", params: &[Ty::I32], slices: &[], rets: &[Ty::I32], callbacks: unsafe { HostCallbacks::new(std::ptr::null_mut(), scalar_cost, boom) }, misses: 0, tier: Tier::X, doc: "" },
|
|
HostFn {
|
|
name: "test.fill",
|
|
params: &[Ty::I32],
|
|
slices: &[SliceSig { max_words: 8, writable: true }],
|
|
rets: &[Ty::I32],
|
|
callbacks: unsafe { HostCallbacks::new(std::ptr::null_mut(), fill_cost, fill) },
|
|
misses: 1,
|
|
tier: Tier::D,
|
|
doc: "test.fill(buf, off, words, value)",
|
|
},
|
|
] {
|
|
host::register(f).into_result().unwrap();
|
|
}
|
|
}
|
|
|
|
fn run(k: &KernelRef, n: usize, out: &str, words: usize, extra: &[(&str, usize)]) -> (Vec<u32>, Vec<Vec<u32>>, bool) {
|
|
let mut j = Job::new(k.clone(), n);
|
|
j.output_u32(out, vec![0; n * words]).unwrap();
|
|
for (name, len) in extra {
|
|
j.output_u32(name, vec![0; *len]).unwrap();
|
|
}
|
|
let st = j.run(&InlineExecutor, 1).unwrap().clone();
|
|
(j.out_u32(out).unwrap().to_vec(), extra.iter().map(|(name, _)| j.out_u32(name).unwrap().to_vec()).collect(), st.host_error)
|
|
}
|
|
|
|
#[test]
|
|
fn registered_functions_run_natively_and_interpreted() {
|
|
register();
|
|
let src = "let o = output(f32)\nfn element(i) { o[i] = test.sum3(float(i), 0.5, 2.0) }";
|
|
for backend in [Backend::Native, Backend::Interp] {
|
|
let k = compile_with(src, &[], backend).unwrap_or_else(|e| panic!("{:?}", e));
|
|
assert!(!k.simd(), "host calls run on the scalar code");
|
|
let (o, _, err) = run(&k, 5, "o", 1, &[]);
|
|
assert_eq!(o, (0..5).map(|i| (i as f32 + 2.5).to_bits()).collect::<Vec<_>>());
|
|
assert!(!err);
|
|
}
|
|
// Values live across the call stay intact (caller-saved registers).
|
|
let src = "let o = output(vec4)\nfn element(i) { let a = float(i) * 3.0\n let b = sin(a)\n let c = test.sum3(a, b, 1.0)\n o[i] = vec4(a, b, c, a + b + c) }";
|
|
let kn = compile_with(src, &[], Backend::Native).unwrap();
|
|
let ki = compile_with(src, &[], Backend::Interp).unwrap();
|
|
assert_eq!(run(&kn, 33, "o", 4, &[]).0, run(&ki, 33, "o", 4, &[]).0);
|
|
}
|
|
|
|
#[test]
|
|
fn failures_are_reported_and_never_unwind_into_kernels() {
|
|
register();
|
|
let src = "let o = output(i32)\nfn element(i) { o[i] = test.boom(i) + 7 }";
|
|
for backend in [Backend::Native, Backend::Interp] {
|
|
let k = compile_with(src, &[], backend).unwrap();
|
|
let (o, _, err) = run(&k, 3, "o", 1, &[]);
|
|
assert_eq!(o, vec![7, 7, 7], "results are zero after a panic");
|
|
assert!(err, "the error word is set");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn slices_are_clamped_to_their_buffers_and_their_maximum() {
|
|
register();
|
|
// Offsets and lengths from the element: some past the buffer, some
|
|
// longer than the function's maximum (8 words).
|
|
let src = "let o = output(i32)\nlet dst = output(i32, 1, 0, dst)\nfn element(i) { o[i] = test.fill(dst, i * 5 - 3, i * 4, 9) }";
|
|
for backend in [Backend::Native, Backend::Interp] {
|
|
let k = compile_with(src, &[], backend).unwrap_or_else(|e| panic!("{:?}", e));
|
|
assert!(!k.parallel_safe, "a written slice keeps the kernel on one thread");
|
|
let (o, bufs, err) = run(&k, 6, "o", 1, &[("dst", 20)]);
|
|
// i = 0: off -3 wraps to a huge u32 -> clamped to the end, 0 words.
|
|
// i = 1: off 2, 4 words; i = 2: off 7, 8 words; i = 3: 12, 12 -> 8
|
|
// (clamped: error); i = 4: 17, 16 -> 3 (buffer end: error); 5: 22 -> 0.
|
|
assert_eq!(o, vec![0, 4, 8, 8, 3, 0]);
|
|
let mut want = vec![9u32; 20];
|
|
// Words 0, 1 and 6 are in no element's slice.
|
|
for w in [0, 1, 6] {
|
|
want[w] = 0;
|
|
}
|
|
assert_eq!(bufs[0], want);
|
|
assert!(err);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn compile_errors_name_the_contract() {
|
|
register();
|
|
let e = compile("let o = output(f32)\nfn element(i) { o[i] = test.sum3(1.0, 2.0) }").map(|_| ()).unwrap_err();
|
|
assert!(e[0].message.contains("takes 3 arguments"), "{}", e[0].message);
|
|
let e = compile("let src = input(i32)\nlet o = output(i32)\nfn element(i) { o[i] = test.fill(src, 0, 4, 1) }").map(|_| ()).unwrap_err();
|
|
assert!(e[0].message.contains("writes this buffer"), "{}", e[0].message);
|
|
let e = compile("let math = portable\nlet o = output(i32)\nlet d = output(i32)\nfn element(i) { o[i] = test.fill(d, 0, 1, 1) }").map(|_| ()).unwrap_err();
|
|
assert!(e[0].message.contains("portable"), "{}", e[0].message);
|
|
}
|
|
|
|
#[test]
|
|
fn triangulate_a_concave_polygon() {
|
|
// An L shape, clockwise, and a square counter-clockwise.
|
|
let src = "let pts = input(f32)\nlet tris = output(i32, 1, 0, tris)\nlet o = output(i32)\nfn element(i) { o[i] = poly.triangulate(pts, i * 12, if i == 0 { 12 } else { 8 }, tris, i * 12, 12) }";
|
|
let l = [0.0f32, 0.0, 0.0, 2.0, 1.0, 2.0, 1.0, 1.0, 2.0, 1.0, 2.0, 0.0];
|
|
let sq = [0.0f32, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0];
|
|
let mut pts = l.to_vec();
|
|
pts.extend_from_slice(&sq);
|
|
pts.resize(24, 0.0);
|
|
let mut outs = Vec::new();
|
|
for backend in [Backend::Native, Backend::Interp] {
|
|
let k = compile_with(src, &[], backend).unwrap_or_else(|e| panic!("{:?}", e));
|
|
let mut j = Job::new(k, 2);
|
|
j.input("pts", pts.clone().into()).unwrap();
|
|
j.output_u32("tris", vec![u32::MAX; 24]).unwrap();
|
|
j.output_u32("o", vec![0; 2]).unwrap();
|
|
let err = j.run(&InlineExecutor, 1).unwrap().host_error;
|
|
assert!(!err);
|
|
outs.push((j.out_u32("o").unwrap().to_vec(), j.out_u32("tris").unwrap().to_vec()));
|
|
}
|
|
assert_eq!(outs[0], outs[1], "native == interpreter");
|
|
let (counts, tris) = &outs[0];
|
|
assert_eq!(counts, &vec![4, 2]);
|
|
// The triangles cover the polygons' areas exactly.
|
|
let area = |p: &[f32], t: &[u32]| {
|
|
t.chunks(3).map(|c| {
|
|
let (a, b, cc) = (c[0] as usize, c[1] as usize, c[2] as usize);
|
|
((p[2 * b] - p[2 * a]) * (p[2 * cc + 1] - p[2 * a + 1]) - (p[2 * b + 1] - p[2 * a + 1]) * (p[2 * cc] - p[2 * a])).abs() * 0.5
|
|
}).sum::<f32>()
|
|
};
|
|
assert_eq!(area(&l, &tris[..12]), 3.0);
|
|
assert_eq!(area(&sq, &tris[12..18]), 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn the_validator_checks_host_calls() {
|
|
register();
|
|
let f = host::find("test.fill").unwrap();
|
|
let r = Regions { ctx: 32, state: 1, shared: 1, frame: 1, shared_writable: false, bufs: vec![false, false, true], io: false };
|
|
let prog = |slices: Vec<SliceArg>, args: Vec<Val>, rets: Vec<Val>, f: u16| Program {
|
|
vals: vec![Ty::I32, Ty::I32, Ty::F32],
|
|
vars: vec![],
|
|
body: vec![Stmt::Def(Val(0), Op::ConstI(1)), Stmt::Def(Val(2), Op::ConstF(1.0)), Stmt::CallHost { f, args, slices, rets }],
|
|
frame_words: 1,
|
|
..Default::default()
|
|
};
|
|
let s = |buf| vec![SliceArg { buf, off: Val(0), len: Val(0) }];
|
|
assert!(ir::validate(&prog(s(2), vec![Val(0)], vec![Val(1)], f), &r).is_ok());
|
|
assert!(ir::validate(&prog(s(1), vec![Val(0)], vec![Val(1)], f), &r).is_err(), "writes a read-only buffer");
|
|
assert!(ir::validate(&prog(s(9), vec![Val(0)], vec![Val(1)], f), &r).is_err(), "no such buffer");
|
|
assert!(ir::validate(&prog(s(2), vec![Val(2)], vec![Val(1)], f), &r).is_err(), "argument type");
|
|
assert!(ir::validate(&prog(s(2), vec![Val(0)], vec![Val(0)], f), &r).is_err(), "result defined twice");
|
|
assert!(ir::validate(&prog(s(2), vec![Val(0)], vec![], f), &r).is_err(), "arity");
|
|
assert!(ir::validate(&prog(s(2), vec![Val(0)], vec![Val(1)], 60000), &r).is_err(), "no such function");
|
|
let audio = Regions { io: true, ..r.clone() };
|
|
assert!(ir::validate(&prog(s(2), vec![Val(0)], vec![Val(1)], f), &audio).is_err(), "audio programs never call out");
|
|
// A call's result is visible after it, and its cost is the function's.
|
|
let p = prog(s(2), vec![Val(0)], vec![Val(1)], f);
|
|
assert!(p.total_cost() >= 16);
|
|
let _ = Region::Frame;
|
|
}
|
|
|
|
/// A comb: n teeth whose reflex vertices sit near every ear candidate
|
|
/// (ear clipping's hard case), counter-clockwise.
|
|
fn comb(teeth: usize) -> Vec<f32> {
|
|
let mut p = Vec::new();
|
|
for k in 0..teeth {
|
|
let x = k as f32;
|
|
p.extend_from_slice(&[x, 0.0, x + 0.5, 10.0]);
|
|
}
|
|
p.extend_from_slice(&[teeth as f32, 0.0, teeth as f32, -1.0, 0.0, -1.0]);
|
|
// As built the teeth run clockwise along the top: reverse for ccw.
|
|
let n = p.len() / 2;
|
|
let mut r = Vec::with_capacity(p.len());
|
|
for i in (0..n).rev() {
|
|
r.extend_from_slice(&p[2 * i..2 * i + 2]);
|
|
}
|
|
r
|
|
}
|
|
|
|
#[test]
|
|
fn triangulate_cost_is_honest_and_budgets_refuse_large_inputs() {
|
|
let f = host::find("poly.triangulate").unwrap();
|
|
let h = host::get(f).unwrap();
|
|
for teeth in [64usize, 512, 2048] {
|
|
for pts in [comb(teeth), { let mut c = comb(teeth); let n = c.len() / 2; let mut r = Vec::new(); for i in (0..n).rev() { r.extend_from_slice(&c[2 * i..2 * i + 2]); } c.clear(); r }] {
|
|
let mut pts = pts;
|
|
let n = pts.len() / 2;
|
|
let mut tris = vec![0u32; 3 * n];
|
|
let bufs = [
|
|
ir::RawBuf { ptr: pts.as_mut_ptr() as *mut u32, len: pts.len(), writable: false },
|
|
ir::RawBuf { ptr: tris.as_mut_ptr(), len: tris.len(), writable: true },
|
|
];
|
|
let sl = [host::SliceRaw { buf: 0, off: 0, len: pts.len() as u32 }, host::SliceRaw { buf: 1, off: 0, len: tris.len() as u32 }];
|
|
let mut rets = [0u32];
|
|
assert!(host::invoke(f, &[], &sl, &mut rets, &bufs));
|
|
assert_eq!(rets[0] as usize, n - 2, "{} points", n);
|
|
// Honest: the ops it counts on this input stay within what it
|
|
// declares (the same on every machine).
|
|
let declared = h.cost_of(&[pts.len() as u32, tris.len() as u32]);
|
|
let ran = host::triangulate_ops(&pts);
|
|
assert!(ran <= declared, "{} points ran {} ops, declared {}", n, ran, declared);
|
|
// A per-call limit below the input's cost refuses it before it runs.
|
|
let limit = h.cost_of(&[pts.len() as u32, tris.len() as u32]) - 1;
|
|
let mut rets = [7u32];
|
|
assert!(!host::invoke_limited(f, &[], &sl, &mut rets, &bufs, limit));
|
|
assert_eq!(rets, [0]);
|
|
}
|
|
}
|
|
// The same from a kernel: the limit word refuses the call, and it is reported.
|
|
let src = "let pts = input(f32)\nlet tris = output(i32, 1, 0, tris)\nlet o = output(i32)\nfn element(i) { o[i] = poly.triangulate(pts, 0, 1030, tris, 0, 1542) }";
|
|
let k = compile(src).unwrap();
|
|
let mut j = Job::new(k, 1);
|
|
j.input("pts", comb(256).into()).unwrap();
|
|
j.output_u32("tris", vec![0; 1542]).unwrap();
|
|
j.output_u32("o", vec![0; 1]).unwrap();
|
|
j.set_host_call_limit(1000);
|
|
assert!(j.run(&InlineExecutor, 1).unwrap().host_error);
|
|
assert_eq!(j.out_u32("o").unwrap(), &[0]);
|
|
j.set_host_call_limit(0);
|
|
assert!(!j.run(&InlineExecutor, 1).unwrap().host_error);
|
|
assert_eq!(j.out_u32("o").unwrap(), &[513]);
|
|
}
|
|
|
|
// Stateless callbacks retain no borrowed state.
|
|
unsafe extern "C-unwind" fn scalar_cost(_: *mut (), _: *const u32, _: usize) -> u64 { 4 }
|
|
unsafe extern "C-unwind" fn fill_cost(_: *mut (), words: *const u32, len: usize) -> u64 { 16 + std::slice::from_raw_parts(words, len)[0] as u64 }
|