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>
173 lines
8.1 KiB
Rust
173 lines
8.1 KiB
Rust
//! Kernels made from Splash values (`vm_kernel`): the entry is the VM's fn
|
|
//! object and what it reaches is found through the VM's scopes, not by
|
|
//! reading document text.
|
|
#![cfg(feature = "vm")]
|
|
|
|
use makepad_script_compute_sched::kernel::KernelExt;
|
|
use makepad_script::*;
|
|
use makepad_script_compute::kernel::MathMode;
|
|
use makepad_script_compute::vm_kernel::{self, Decl, Entry, Record, VmKernel};
|
|
use makepad_script_compute::Backend;
|
|
|
|
fn doc(code: &str) -> (ScriptVm<'static>, ScriptObject) {
|
|
let host = Box::leak(Box::new(ScriptVmHost::new(0i32, ())));
|
|
let mut vm = ScriptVm {
|
|
host: host.host_context(),
|
|
bx: Box::new(ScriptVmBase::new()),
|
|
};
|
|
vm.capture_errors();
|
|
let v = vm.eval(ScriptMod::new("", "", "doc.splash", 0, 0, &code, &[]));
|
|
assert!(vm.take_errors().is_empty());
|
|
let obj = v.as_object().expect("the document's kernel object");
|
|
let _ = vm.new_object_ref(obj);
|
|
(vm, obj)
|
|
}
|
|
|
|
fn field(vm: &ScriptVm, obj: ScriptObject, name: LiveId) -> ScriptObject {
|
|
vm.value(obj, name).as_object().expect("a fn field")
|
|
}
|
|
|
|
#[test]
|
|
fn entry_reaches_document_fns_and_constants_through_the_vm() {
|
|
let (vm, k) = doc(
|
|
"use mod.pod.*\nlet R = 3.5\nlet OFF = vec3(1.0, 2.0, 3.0)\nlet scale = fn(x) { return x * R }\n\
|
|
fn bump(y) { return scale(y) + 1.0 }\n\
|
|
let K = {count: 4 element: fn(i) { let f = float(i) pos[i] = vec3(bump(f), f, 0.0) + OFF * amp }}\nK",
|
|
);
|
|
let kernel = VmKernel {
|
|
decls: vec![
|
|
Decl::Output { name: "pos".into(), ty: "vec3".into(), stride: None, offset: None, buffer: None },
|
|
Decl::Param { name: "amp".into(), default: 2.0, range: None },
|
|
],
|
|
entry: Entry::Element,
|
|
entry_fn: field(&vm, k, id!(element)),
|
|
math: MathMode::Portable,
|
|
uses: Vec::new(),
|
|
bind: Vec::new(),
|
|
};
|
|
// `bump`, `scale`, `R` and `OFF` are found through the VM and compiled in.
|
|
let compiled = vm_kernel::compile(&vm, &kernel, &[], Backend::Interp, &[]).unwrap_or_else(|e| panic!("{e:?}"));
|
|
let mut out = vec![0f32; 12];
|
|
let mut call = compiled.call();
|
|
call.output("pos", &mut out).unwrap();
|
|
call.run(4).unwrap();
|
|
drop(call);
|
|
for i in 0..4 {
|
|
let f = i as f32;
|
|
assert_eq!(&out[i * 3..i * 3 + 3], &[f * 3.5 + 1.0 + 2.0, f + 4.0, 6.0], "element {i}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn errors_name_the_document_line() {
|
|
let (vm, k) = doc("let helper = fn(x) {\n return x + undefined_thing\n}\nlet K = {element: fn(i) { out[i] = helper(1.0) }}\nK");
|
|
let kernel = VmKernel {
|
|
decls: vec![Decl::EmitBuffer { name: "unused".into(), record: Record::Words(1), capacity: 1, buffer: None },
|
|
Decl::Output { name: "out".into(), ty: "f32".into(), stride: None, offset: None, buffer: None }],
|
|
entry: Entry::Element,
|
|
entry_fn: field(&vm, k, id!(element)),
|
|
math: MathMode::Fast,
|
|
uses: Vec::new(),
|
|
bind: Vec::new(),
|
|
};
|
|
let errors = vm_kernel::compile(&vm, &kernel, &[], Backend::Interp, &[]).err().expect("an error");
|
|
assert!(errors.iter().any(|e| e.message.contains("doc.splash:2:")), "{errors:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_kernel_object_reads_back_from_the_vm() {
|
|
let host = Box::leak(Box::new(ScriptVmHost::new(0i32, ())));
|
|
let mut vm = ScriptVm {
|
|
host: host.host_context(),
|
|
bx: Box::new(ScriptVmBase::new()),
|
|
};
|
|
vm.capture_errors();
|
|
use makepad_script_compute::boundary::{LayoutFieldView, LayoutView};
|
|
use makepad_script_compute::kernel::FieldTy;
|
|
let layout = LayoutView { name: "Dot", stride: 4, fields: &[LayoutFieldView { name: "pos", ty: FieldTy::Vec3, offset: 0 }, LayoutFieldView { name: "size", ty: FieldTy::F32, offset: 3 }] };
|
|
vm_kernel::script_mod(&mut vm, std::slice::from_ref(&layout));
|
|
let v = vm.eval(ScriptMod::new("", "", "doc.splash", 0, 0, "use mod.pod.*\nuse mod.kernel.*\nlet GAP = 2.0\n{count: 3 dots: output(Dot) h: input(f32) amp: param(0.5, 0.0, 1.0) math: \"portable\" element: fn(i) { dots[i].pos = vec3(float(i) * GAP, h[i], 0.0) dots[i].size = amp }}", &[]));
|
|
assert!(vm.take_errors().is_empty());
|
|
let obj = v.as_object().unwrap();
|
|
let _keep = vm.new_object_ref(obj);
|
|
let k = vm_kernel::from_object(&vm, obj).unwrap();
|
|
assert_eq!(k.entry, Entry::Element);
|
|
assert_eq!(k.math, MathMode::Portable);
|
|
assert_eq!(k.decls.len(), 3);
|
|
let compiled = vm_kernel::compile(&vm, &k, &[layout], Backend::Interp, &[]).unwrap_or_else(|e| panic!("{e:?}"));
|
|
let h = [10.0f32, 20.0, 30.0];
|
|
let mut dots = vec![0f32; 12];
|
|
let mut call = compiled.call();
|
|
call.input("h", &h).unwrap();
|
|
call.output("dots", &mut dots).unwrap();
|
|
call.run(3).unwrap();
|
|
drop(call);
|
|
assert_eq!(dots, vec![0.0, 10.0, 0.0, 0.5, 2.0, 20.0, 0.0, 0.5, 4.0, 30.0, 0.0, 0.5]);
|
|
}
|
|
|
|
#[test]
|
|
fn closures_and_colours_compile_from_the_vm_tokens() {
|
|
let (vm, k) = doc(
|
|
"use mod.pod.*\nlet TINT = #ff8000\nlet NEG = -2.5\nlet sq = |x| x * x\nlet add = |a, b| { return a + b }\nlet one = || 1.0\n\
|
|
let K = {element: fn(i) { let f = float(i) out[i] = vec4(add(sq(f), one()), NEG, 0.0, 0.0) + TINT }}\nK",
|
|
);
|
|
let kernel = VmKernel {
|
|
decls: vec![Decl::Output { name: "out".into(), ty: "vec4".into(), stride: None, offset: None, buffer: None }],
|
|
entry: Entry::Element,
|
|
entry_fn: field(&vm, k, id!(element)),
|
|
math: MathMode::Fast,
|
|
uses: Vec::new(),
|
|
bind: Vec::new(),
|
|
};
|
|
let compiled = vm_kernel::compile(&vm, &kernel, &[], Backend::Interp, &[]).unwrap_or_else(|e| panic!("{e:?}"));
|
|
let mut out = vec![0f32; 12];
|
|
let mut call = compiled.call();
|
|
call.output("out", &mut out).unwrap();
|
|
call.run(3).unwrap();
|
|
drop(call);
|
|
for i in 0..3 {
|
|
let f = i as f32;
|
|
assert_eq!(&out[i * 4..i * 4 + 4], &[f * f + 1.0 + 1.0, -2.5 + 128.0 / 255.0, 0.0, 1.0], "element {i}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_kernel_made_from_values_is_recorded_for_build_tools() {
|
|
let (vm, k) = doc("let K = {element: fn(i) { rec[i] = float(i) * 7.25 }}\nK");
|
|
let kernel = VmKernel {
|
|
decls: vec![Decl::Output { name: "rec".into(), ty: "f32".into(), stride: None, offset: None, buffer: None }],
|
|
entry: Entry::Element,
|
|
entry_fn: field(&vm, k, id!(element)),
|
|
math: MathMode::Fast,
|
|
uses: Vec::new(),
|
|
bind: Vec::new(),
|
|
};
|
|
makepad_script_compute::kernel::record_compiled(true);
|
|
let compiled = vm_kernel::compile(&vm, &kernel, &[], Backend::Interp, &[]).unwrap_or_else(|e| panic!("{e:?}"));
|
|
let recorded = makepad_script_compute::kernel::take_recorded();
|
|
let key = makepad_script_compute::kernel::program_key(compiled.program());
|
|
assert!(recorded.iter().any(|r| makepad_script_compute::kernel::program_key(r.kernel.program()) == key && r.source.is_empty()));
|
|
}
|
|
|
|
#[test]
|
|
fn a_kernel_object_compiled_with_a_drifted_layout_is_an_error() {
|
|
use makepad_script_compute::boundary::{register_layout, LayoutFieldView, LayoutView};
|
|
use makepad_script_compute::kernel::FieldTy;
|
|
const GOOD: &[LayoutFieldView<'static>] = &[LayoutFieldView { name: "pos", ty: FieldTy::Vec3, offset: 0 }];
|
|
const DRIFTED: &[LayoutFieldView<'static>] = &[LayoutFieldView { name: "pos", ty: FieldTy::Vec3, offset: 1 }];
|
|
let good = LayoutView { name: "ObjDriftProbe", stride: 4, fields: GOOD };
|
|
let drifted = LayoutView { name: "ObjDriftProbe", stride: 4, fields: DRIFTED };
|
|
assert!(register_layout(&good));
|
|
let (vm, k) = doc("let K = {element: fn(i) { out[i].pos = vec3(1.0, 2.0, 3.0) }}\nK");
|
|
let kernel = |vm: &ScriptVm| VmKernel {
|
|
decls: vec![Decl::Output { name: "out".into(), ty: "ObjDriftProbe".into(), stride: None, offset: None, buffer: None }],
|
|
entry: Entry::Element,
|
|
entry_fn: field(vm, k, id!(element)),
|
|
math: MathMode::Fast,
|
|
uses: Vec::new(),
|
|
bind: Vec::new(),
|
|
};
|
|
assert!(vm_kernel::compile(&vm, &kernel(&vm), &[good], Backend::Interp, &[]).is_ok());
|
|
let errors = vm_kernel::compile(&vm, &kernel(&vm), &[drifted], Backend::Interp, &[]).err().expect("the drifted table must not compile");
|
|
assert!(errors.iter().any(|e| e.message.contains("`ObjDriftProbe` is not the registered one")), "{errors:?}");
|
|
}
|