makepad/platform/script/tests/math_aot.rs
Admin 3b4d6a4ff7 platform+widgets: sploded 3D inspect view, the tweaker design-feedback suite, script math-AOT and docs channel, pointer-pin and pixel-probe, modal layout, map warp
Squashed from work; the fine-grained history is under tag archive/work-2026-08-29:
- mp* wave: mpwm window manager + the mp app family, WM API, theme bridge, PDF engine fix
- mpwm polish wave: terminal key focus, focus-history close order, pop-back-to-origin, occupied-workspace cycling, demo
- mpwm: warm-instance pool, flat-luminance opens, flicker-free CEF resize
- work: land the sources the last commits reference
- kenney: catalogue all 50 free 3D kits; Modal dismissed() never fired
- platform: windows check green again — SetWindowTextW binding
- map: exact warp-aware inverse projections — pointer ops work folded
- mpwm: quick-look gap fixes; image cache eviction on preview unload
- tweaker: material thumbnails + vibecode popup + ctrl-space notes, undo/redo over the edit ledger, capture-semantics pi
- tweaker: vibe popup card chrome + dispatch order, ctrl-space notes verified, sploded design v2 chapter
- tweaker: tabbed side panel (Props/Shader/Tree) - shader tab with checkerboard material well + prompt, complete widget-
- sploded v2: nesting-depth z, hairline scope frames, body pass
- sploded: pin the depth convention with a test, kill the draw_depth residue
- sploded: real body-pass split (scene-pass capture, panel flat) + y-convention source of truth with anti-flip gate test
- sploded: hollow outlines, flat-band input, ray-pick unprojection
- tweaker: shader tab defaults to the selection's first draw layer, stale hint trimmed
- sploded: outlines become clipped, antialiased strips; tighter deck
- sploded: merge the lane's v2 (nesting-depth z, clipped AA strip outlines, flat-band input, unproject, SplodedStack bod
- sploded: the exploded view is a LIVE view — pointer events route through the inverse explode transform (ray -> plane -
- tweaker: tabs are real widgets (uid, tree node under the dock, own plane in 3D) and pickable; navigation-class clicks
- sploded: pinned/hover outlines render on the widget's own plane in 3D — per-widget nesting depth lives on the platform
- tweaker: the material well renders the pinned widget's actual shader — the swatch byte-copies the widget's live draw c
- tweaker: the Shader tab shows the shader as written — the layer's pixel/vertex fn source (nearest definition up the co
- sploded: I = true isometric preset (yaw 45°, pitch atan(1/√2))
- tweaker: eyedropper — the colour popover's pick button arms a pixel probe; the next press in the app samples that devi
- tweaker: the shader loop closes — /tweak/apply resolves the pinned widget by uid (anonymous path segments never round-
- vj: responsive DJ mixer + Windows drag-and-drop, cherry-picked from PR #1199 (vjroger)
- tweaker: the material well is a magnifier — the mirrored instance draws at the widget's native size in the well's own
- tweaker: per-layer material thumbnails — the Widget derive emits WidgetNode::layer_areas() (every #[live] Draw… field
- tweaker: the shader source view is the real CodeView (syntax highlighting, selection, editing) when the app registers
- tweaker: Ctrl+Enter sends on every platform (TextInput treated only Cmd as primary on macOS, so Ctrl+Enter inserted a
- Modal claims no layout slot: the DJ page fills its window again
- tweaker: every fn apply recompiles (eval_chunk ran every chunk under ONE synthetic callsite, so the script body — and
- tweaker: an apply whose draw shader fails to compile is rejected — the layer goes back (last live fns / the fn as writ
- tweaker: the Shader tab's source view owns its scrolling (the ScrollYView around the CodeView double-scrolled the care
- widgets: set_visible belongs to every widget, not just View (#1194)
- script: a dead heap's resource handles must not outlive it (#1195)
- Resources: search the executable's directory, not only the working directory (#1196)
- Windows: fit a restored window to the displays that are actually attached (#1197)
- d3d11: a failing GPU call reports the loss instead of killing the process (#1198)

Co-authored-by: Kevin Boos <1139460+kevinaboos@users.noreply.github.com>
2026-08-29 09:26:24 +02:00

598 lines
20 KiB
Rust

//! Math-AOT test suite.
//!
//! Layer map (each test names its layer):
//! - translate: one accepted splash form -> correct compiled result,
//! bit-identical to the interpreter; one rejected form -> clean `None`.
//! - slots: splash `let` locals / params mapped to wasm locals.
//! - batch: the eval_batch entry and its edges.
//! - fuzz: differential fuzzing interpreter-vs-AOT over random expressions.
use makepad_script::math_aot::{MathAot, MathAotParam, MathAotValue};
use makepad_script::makepad_math::{Vec2f, Vec3f, Vec4f};
use makepad_script::*;
fn test_vm() -> ScriptVm<'static> {
let host = Box::leak(Box::new(0i32));
let std = Box::leak(Box::new(0i32));
ScriptVm {
host,
std,
bx: Box::new(ScriptVmBase::new()),
}
}
/// Evaluates `code` (which must end with an expression yielding a fn) and
/// returns the fn value.
fn eval_fn(vm: &mut ScriptVm, code: &str) -> ScriptValue {
vm.bx.captured_errors = Some(Vec::new());
let value = vm.eval(ScriptMod {
cargo_manifest_path: String::new(),
module_path: String::new(),
file: "math_aot_test".to_string(),
line: 0,
column: 0,
code: code.to_string(),
values: vec![],
});
let errors = vm.take_errors();
assert!(errors.is_empty(), "script errors: {errors:?}");
assert!(value.as_object().is_some(), "script did not yield a fn: {code}");
value
}
fn to_script_arg(vm: &mut ScriptVm, arg: &MathAotValue) -> ScriptValue {
match arg {
MathAotValue::Scalar(v) => (*v).into(),
MathAotValue::Vec2(v) => Vec2f { x: v[0], y: v[1] }.script_to_value(vm),
MathAotValue::Vec3(v) => Vec3f {
x: v[0],
y: v[1],
z: v[2],
}
.script_to_value(vm),
MathAotValue::Vec4(v) => Vec4f {
x: v[0],
y: v[1],
z: v[2],
w: v[3],
}
.script_to_value(vm),
}
}
/// Compiles `code`'s fn and checks AOT-vs-interpreter bit identity for
/// every argument tuple.
fn check_bit_identical(
code: &str,
params: &[MathAotParam],
arg_sets: &[Vec<MathAotValue>],
) {
let mut vm = test_vm();
let fn_value = eval_fn(&mut vm, code);
let aot = MathAot::new(&mut vm);
let mut compiled = aot
.compile(&vm, fn_value, params, &[])
.unwrap_or_else(|| panic!("expression rejected by the AOT: {code}"));
for args in arg_sets {
let script_args: Vec<ScriptValue> =
args.iter().map(|a| to_script_arg(&mut vm, a)).collect();
let expected = vm.call(fn_value, &script_args);
let expected = expected
.as_number()
.unwrap_or_else(|| panic!("interpreter returned non-number for {code}: {expected:?}"));
let actual = compiled.call(args).expect("aot call failed");
// Bit-identical, except at the NaN boundary: the interpreter boxes
// NaN RESULTS as traced NaNs (payload = source location), so any
// NaN output compares as NaN-vs-NaN.
let same = if actual.is_nan() && expected.is_nan() {
true
} else {
actual.to_bits() == expected.to_bits()
};
assert!(
same,
"MISMATCH for {code}\n args {args:?}\n interp {expected:?} ({:#x}) aot {actual:?} ({:#x})",
expected.to_bits(),
actual.to_bits()
);
}
}
/// Asserts the AOT cleanly rejects `code` (returns None, no panic).
fn check_rejected(code: &str, params: &[MathAotParam]) {
let mut vm = test_vm();
let fn_value = eval_fn(&mut vm, code);
let aot = MathAot::new(&mut vm);
assert!(
aot.compile(&vm, fn_value, params, &[]).is_none(),
"expected rejection: {code}"
);
}
fn scalar_args(sets: &[&[f64]]) -> Vec<Vec<MathAotValue>> {
sets.iter()
.map(|set| set.iter().map(|v| MathAotValue::Scalar(*v)).collect())
.collect()
}
const XS: &[f64] = &[
0.0, 1.0, -1.0, 0.5, -0.75, 2.5, 3.14159, -7.25, 100.5, 1.0e10, -0.0,
];
fn one_scalar_sets() -> Vec<Vec<MathAotValue>> {
XS.iter().map(|x| vec![MathAotValue::Scalar(*x)]).collect()
}
fn two_scalar_sets() -> Vec<Vec<MathAotValue>> {
let mut out = Vec::new();
for a in XS {
for b in XS {
out.push(vec![MathAotValue::Scalar(*a), MathAotValue::Scalar(*b)]);
}
}
out
}
// -- layer: translate (accepted forms) ------------------------------------
#[test]
fn translate_scalar_arithmetic() {
let s2 = two_scalar_sets();
check_bit_identical("let f = |a, b| a + b\n(f)", &[MathAotParam::Scalar; 2], &s2);
check_bit_identical("let f = |a, b| a - b\n(f)", &[MathAotParam::Scalar; 2], &s2);
check_bit_identical("let f = |a, b| a * b\n(f)", &[MathAotParam::Scalar; 2], &s2);
check_bit_identical("let f = |a, b| a / b\n(f)", &[MathAotParam::Scalar; 2], &s2);
check_bit_identical("let f = |a, b| a % b\n(f)", &[MathAotParam::Scalar; 2], &s2);
check_bit_identical("let f = |a| -a\n(f)", &[MathAotParam::Scalar], &one_scalar_sets());
// Inline-constant fast path (the parser fuses small integer RHS).
check_bit_identical("let f = |a| a * 3\n(f)", &[MathAotParam::Scalar], &one_scalar_sets());
check_bit_identical("let f = |a| a + 7\n(f)", &[MathAotParam::Scalar], &one_scalar_sets());
}
#[test]
fn translate_scalar_intrinsics() {
let s1 = one_scalar_sets();
for name in [
"sin", "cos", "tan", "asin", "acos", "atan", "exp", "log", "sqrt", "abs", "floor",
"ceil", "fract",
] {
let code = format!("use mod.math.*\nlet f = |a| {name}(a)\n(f)");
check_bit_identical(&code, &[MathAotParam::Scalar], &s1);
}
let s2 = two_scalar_sets();
for name in ["atan2", "pow", "min", "max"] {
let code = format!("use mod.math.*\nlet f = |a, b| {name}(a, b)\n(f)");
check_bit_identical(&code, &[MathAotParam::Scalar; 2], &s2);
}
}
#[test]
fn translate_scalar_composite() {
check_bit_identical(
"use mod.math.*\nlet f = |x, y, z| sin(x) * cos(z) - y + 0.3 * sin(5 * x) * sin(5 * y) * sin(5 * z)\n(f)",
&[MathAotParam::Scalar; 3],
&scalar_args(&[
&[0.1, 0.2, 0.3],
&[1.5, -2.5, 3.5],
&[0.0, 0.0, 0.0],
&[-10.25, 5.125, 0.75],
]),
);
}
#[test]
fn translate_comparisons_and_if() {
let s2 = two_scalar_sets();
check_bit_identical(
"let f = |a, b| if a < b { a } else { b }\n(f)",
&[MathAotParam::Scalar; 2],
&s2,
);
check_bit_identical(
"let f = |a, b| if a >= b { a * 2 } else { b - 1 }\n(f)",
&[MathAotParam::Scalar; 2],
&s2,
);
}
#[test]
fn translate_early_return() {
check_bit_identical(
"let f = |a| { if a < 0 { return 0 - a }\na * 2 }\n(f)",
&[MathAotParam::Scalar],
&one_scalar_sets(),
);
}
#[test]
fn translate_logic_ops() {
let s2 = two_scalar_sets();
check_bit_identical(
"let f = |a, b| a && b\n(f)",
&[MathAotParam::Scalar; 2],
&s2,
);
check_bit_identical(
"let f = |a, b| a || b\n(f)",
&[MathAotParam::Scalar; 2],
&s2,
);
}
#[test]
fn translate_scope_constant() {
check_bit_identical(
"let r = 1.25\nlet f = |a| a - r\n(f)",
&[MathAotParam::Scalar],
&one_scalar_sets(),
);
// Module constant through the scope chain.
check_bit_identical(
"use mod.math.*\nlet f = |a| a * PI\n(f)",
&[MathAotParam::Scalar],
&one_scalar_sets(),
);
}
// -- layer: slots ---------------------------------------------------------
#[test]
fn slots_let_locals() {
check_bit_identical(
"use mod.math.*\nlet f = |x, y| {\nlet a = x * 2\nlet b = sin(a) + y\nb * a\n}\n(f)",
&[MathAotParam::Scalar; 2],
&two_scalar_sets(),
);
}
#[test]
fn slots_compound_assign() {
check_bit_identical(
"let f = |x| {\nlet a = x\na += 2\na *= 3\na -= x\na /= 2\na\n}\n(f)",
&[MathAotParam::Scalar],
&one_scalar_sets(),
);
}
// -- layer: translate (vectors) -------------------------------------------
fn vec3_sets() -> Vec<Vec<MathAotValue>> {
[
[0.0f32, 0.0, 0.0],
[1.0, 2.0, 3.0],
[-1.5, 0.25, -8.0],
[0.1, -0.2, 0.3],
]
.iter()
.map(|v| vec![MathAotValue::Vec3(*v)])
.collect()
}
#[test]
fn translate_vec3_length_sphere() {
check_bit_identical(
"use mod.math.*\nlet f = |p| length(p) - 1.0\n(f)",
&[MathAotParam::Vec3],
&vec3_sets(),
);
}
#[test]
fn translate_vec_arithmetic_and_swizzle() {
check_bit_identical(
"let f = |p| (p * 2.0 + p).x\n(f)",
&[MathAotParam::Vec3],
&vec3_sets(),
);
check_bit_identical(
"let f = |p| p.z * p.y + p.x\n(f)",
&[MathAotParam::Vec3],
&vec3_sets(),
);
check_bit_identical(
"use mod.math.*\nlet f = |p| length(p.zyx - p.xxz)\n(f)",
&[MathAotParam::Vec3],
&vec3_sets(),
);
// Vector division has the interpreter's zero-divisor guard.
check_bit_identical(
"let f = |p| (p / p.yzx).x\n(f)",
&[MathAotParam::Vec3],
&vec3_sets(),
);
}
#[test]
fn translate_vec_constructor() {
check_bit_identical(
"use mod.math.*\nuse mod.pod.*\nlet f = |x, y, z| length(vec3(x, y, z))\n(f)",
&[MathAotParam::Scalar; 3],
&scalar_args(&[&[1.0, 2.0, 3.0], &[0.0, 0.0, 0.0], &[-4.5, 0.5, 9.0]]),
);
}
#[test]
fn translate_vec_intrinsics() {
let sets: Vec<Vec<MathAotValue>> = [
([1.0f32, 2.0, 3.0], [4.0f32, -5.0, 6.0]),
([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]),
([-0.5, 0.25, -0.125], [8.0, -16.0, 32.0]),
]
.iter()
.map(|(a, b)| vec![MathAotValue::Vec3(*a), MathAotValue::Vec3(*b)])
.collect();
check_bit_identical(
"use mod.math.*\nlet f = |a, b| dot(a, b)\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
check_bit_identical(
"use mod.math.*\nlet f = |a, b| distance(a, b)\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
check_bit_identical(
"use mod.math.*\nlet f = |a, b| length(cross(a, b))\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
check_bit_identical(
"use mod.math.*\nlet f = |a, b| length(normalize(a) + normalize(b))\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
check_bit_identical(
"use mod.math.*\nlet f = |a, b| length(min(a, b) - max(a, b))\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
check_bit_identical(
"use mod.math.*\nlet f = |a, b| length(mix(a, b, 0.25))\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
check_bit_identical(
"use mod.math.*\nlet f = |a, b| length(abs(a) - floor(b))\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
check_bit_identical(
"use mod.math.*\nlet f = |a, b| length(sin(a) + cos(b))\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
check_bit_identical(
"use mod.math.*\nlet f = |a, b| length(clamp(a, 0.0 - 1.0, 1.0))\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
// Pod methods.
check_bit_identical(
"let f = |a, b| a.dot(b)\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
check_bit_identical(
"let f = |a, b| a.cross(b).length()\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
check_bit_identical(
"let f = |a, b| a.normalized().dot(b)\n(f)",
&[MathAotParam::Vec3; 2],
&sets,
);
}
// -- layer: translate (rejected forms) ------------------------------------
#[test]
fn rejects_outside_subset() {
// Object literal.
check_rejected("let f = |a| {x: a}\n(f)", &[MathAotParam::Scalar]);
// Array literal.
check_rejected("let f = |a| [a]\n(f)", &[MathAotParam::Scalar]);
// String.
check_rejected("let f = |a| \"s\"\n(f)", &[MathAotParam::Scalar]);
// Loop.
check_rejected(
"let f = |a| { let t = 0\nfor i in 0..3 { t += a }\nt }\n(f)",
&[MathAotParam::Scalar],
);
// Closure creation inside.
check_rejected("let f = |a| { let g = |b| b\ng(a) }\n(f)", &[MathAotParam::Scalar]);
// Unknown free identifier.
check_rejected("let f = |a| a + undefined_thing\n(f)", &[MathAotParam::Scalar]);
// Calling a non-math native.
check_rejected(
"let f = |a| { log(a)\na }\n(f)",
&[MathAotParam::Scalar],
);
// `if` without else in value position (nil on the untaken path).
check_rejected("let f = |a| if a > 0 { a }\n(f)", &[MathAotParam::Scalar]);
// Param count mismatch.
check_rejected("let f = |a, b| a + b\n(f)", &[MathAotParam::Scalar]);
// Equality: splash deep_eq compares raw NaN-box bits (traced NaNs),
// which the compiled form cannot mirror for data-dependent NaNs.
check_rejected(
"let f = |a, b| if a == b { 1.0 } else { 0.0 }\n(f)",
&[MathAotParam::Scalar; 2],
);
}
// -- layer: batch ----------------------------------------------------------
#[test]
fn batch_matches_single_calls() {
let mut vm = test_vm();
let fn_value = eval_fn(
&mut vm,
"use mod.math.*\nlet f = |p| length(p) - 1.0\n(f)",
);
let aot = MathAot::new(&mut vm);
let mut compiled = aot.compile(&vm, fn_value, &[MathAotParam::Vec3], &[]).unwrap();
// 10_001 points: not a multiple of the chunk size, crosses a chunk
// boundary, exercises reuse of the same instance across chunks.
let n = 10_001;
let mut input = Vec::with_capacity(n * 3);
for i in 0..n {
input.push((i as f32) * 0.01 - 37.0);
input.push((i as f32) * -0.003 + 1.0);
input.push((i as f32) * 0.02 - 100.0);
}
let mut out = vec![0f32; n];
compiled.eval_batch(&input, &[], &mut out);
for i in (0..n).step_by(997) {
let expected = compiled
.call(&[MathAotValue::Vec3([
input[i * 3],
input[i * 3 + 1],
input[i * 3 + 2],
])])
.unwrap() as f32;
assert_eq!(out[i].to_bits(), expected.to_bits(), "point {i}");
}
}
#[test]
fn batch_edges() {
let mut vm = test_vm();
let fn_value = eval_fn(&mut vm, "use mod.math.*\nlet f = |p| length(p)\n(f)");
let aot = MathAot::new(&mut vm);
let mut compiled = aot.compile(&vm, fn_value, &[MathAotParam::Vec3], &[]).unwrap();
// N = 0
compiled.eval_batch(&[], &[], &mut []);
// N = 1
let mut out = [0f32];
compiled.eval_batch(&[3.0, 4.0, 12.0], &[], &mut out);
assert_eq!(out[0], 13.0);
// Exactly one chunk, then chunk+1.
for n in [4096usize, 4097] {
let input: Vec<f32> = (0..n * 3).map(|i| (i % 17) as f32 - 8.0).collect();
let mut out = vec![0f32; n];
compiled.eval_batch(&input, &[], &mut out);
let expected = compiled
.call(&[MathAotValue::Vec3([
input[(n - 1) * 3],
input[(n - 1) * 3 + 1],
input[(n - 1) * 3 + 2],
])])
.unwrap() as f32;
assert_eq!(out[n - 1].to_bits(), expected.to_bits(), "n={n}");
}
// Two compiled expressions interleaved on one MathAot.
let fn2 = eval_fn(&mut vm, "use mod.math.*\nlet g = |p| p.x + p.y + p.z\n(g)");
let mut compiled2 = aot.compile(&vm, fn2, &[MathAotParam::Vec3], &[]).unwrap();
let mut out1 = [0f32];
let mut out2 = [0f32];
compiled.eval_batch(&[1.0, 2.0, 2.0], &[], &mut out1);
compiled2.eval_batch(&[1.0, 2.0, 2.0], &[], &mut out2);
compiled.eval_batch(&[3.0, 4.0, 12.0], &[], &mut out1);
assert_eq!(out1[0], 13.0);
assert_eq!(out2[0], 5.0);
}
#[test]
fn batch_scalar_params() {
let mut vm = test_vm();
let fn_value = eval_fn(
&mut vm,
"use mod.math.*\nlet f = |x, y, z| sin(x) * cos(z) - y\n(f)",
);
let aot = MathAot::new(&mut vm);
let mut compiled = aot
.compile(&vm, fn_value, &[MathAotParam::Scalar; 3], &[])
.unwrap();
let n = 100;
let input: Vec<f32> = (0..n * 3).map(|i| (i as f32) * 0.05 - 3.0).collect();
let mut out = vec![0f32; n];
compiled.eval_batch(&input, &[], &mut out);
for i in 0..n {
let expected = compiled
.call(&[
MathAotValue::Scalar(input[i * 3] as f64),
MathAotValue::Scalar(input[i * 3 + 1] as f64),
MathAotValue::Scalar(input[i * 3 + 2] as f64),
])
.unwrap() as f32;
assert_eq!(out[i].to_bits(), expected.to_bits(), "point {i}");
}
}
// -- layer: uniforms (parametric models) -----------------------------------
/// A parametric sphere: `|p, r| length(p) - r` with `r` a uniform.
/// Changing `r` between calls on ONE compiled function must match the
/// interpreter with the same values — no recompile.
#[test]
fn uniforms_parametric_sphere() {
let mut vm = test_vm();
let fn_value = eval_fn(&mut vm, "use mod.math.*\nlet f = |p, r| length(p) - r\n(f)");
let aot = MathAot::new(&mut vm);
let mut compiled = aot
.compile(&vm, fn_value, &[MathAotParam::Vec3], &[MathAotParam::Scalar])
.expect("parametric sphere in subset");
for r in [1.0f32, 2.5, 0.25] {
let p = [3.0f32, 0.0, 4.0];
let arg_p = Vec3f { x: p[0], y: p[1], z: p[2] }.script_to_value(&mut vm);
let expected = vm.call(fn_value, &[arg_p, (r as f64).into()]);
let expected = expected.as_number().unwrap();
let actual = compiled
.call(&[MathAotValue::Vec3(p), MathAotValue::Scalar(r as f64)])
.unwrap();
assert_eq!(actual.to_bits(), expected.to_bits(), "r={r}");
// Batch entry with the uniform block.
let mut out = [0f32; 2];
compiled.eval_batch(&[3.0, 0.0, 4.0, 0.0, 0.0, 0.0], &[r], &mut out);
assert_eq!(out[0], 5.0 - r);
assert_eq!(out[1], -r);
}
}
/// The round-shapes idiom: polynomial smooth-min of two spheres with the
/// blend radius `k` and the sphere offset `c` as uniforms (vec uniform +
/// scalar uniform); resampled with several k values on one compiled fn.
#[test]
fn uniforms_smooth_min_blend() {
let mut vm = test_vm();
let code = "use mod.math.*\nuse mod.pod.*\nlet f = |p, c, k| {\n\
let a = length(p - c) - 0.6\n\
let b = length(p + c) - 0.6\n\
let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0)\n\
mix(b, a, h) - k * h * (1.0 - h)\n}\n(f)";
let fn_value = eval_fn(&mut vm, code);
let aot = MathAot::new(&mut vm);
let mut compiled = aot
.compile(
&vm,
fn_value,
&[MathAotParam::Vec3],
&[MathAotParam::Vec3, MathAotParam::Scalar],
)
.expect("smooth-min in subset");
let c = [0.4f32, 0.0, 0.0];
for k in [0.1f32, 0.3, 0.7] {
for p in [[0.0f32, 0.3, 0.1], [0.5, -0.2, 0.4], [-0.8, 0.0, 0.0]] {
let arg_p = Vec3f { x: p[0], y: p[1], z: p[2] }.script_to_value(&mut vm);
let arg_c = Vec3f { x: c[0], y: c[1], z: c[2] }.script_to_value(&mut vm);
let expected = vm.call(fn_value, &[arg_p, arg_c, (k as f64).into()]);
let expected = expected.as_number().unwrap();
let actual = compiled
.call(&[
MathAotValue::Vec3(p),
MathAotValue::Vec3(c),
MathAotValue::Scalar(k as f64),
])
.unwrap();
assert!(
(actual.is_nan() && expected.is_nan()) || actual.to_bits() == expected.to_bits(),
"k={k} p={p:?}: interp {expected:?} aot {actual:?}"
);
// Batch with the uniform block [cx, cy, cz, k].
let mut out = [0f32];
compiled.eval_batch(&p, &[c[0], c[1], c[2], k], &mut out);
assert_eq!(out[0].to_bits(), (actual as f32).to_bits());
}
}
}