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>
346 lines
13 KiB
Rust
346 lines
13 KiB
Rust
//! Layer: fuzz (differential).
|
|
//!
|
|
//! Generates random pure-math splash expressions (scalars + vec3s over
|
|
//! the whole taught intrinsic set), then requires, for every random
|
|
//! input tuple:
|
|
//!
|
|
//! splash interpreter == StitchBackend == VirInterpBackend
|
|
//!
|
|
//! bit-for-bit (NaN outputs compare as NaN-class: the interpreter boxes
|
|
//! NaN results with a source-trace payload). The batch entry is also
|
|
//! checked against the single-call entry per expression.
|
|
//!
|
|
//! The committed run is deterministic (seeds 1..=FUZZ_EXPRS). For a
|
|
//! larger sweep set MATH_AOT_FUZZ_EXPRS, e.g.:
|
|
//! MATH_AOT_FUZZ_EXPRS=5000 cargo test --release --test math_aot_fuzz
|
|
//! (a 5000-expression run is recorded in the mathaot report).
|
|
|
|
use makepad_script::math_aot::vir;
|
|
use makepad_script::math_aot::{
|
|
MathAot, MathAotParam, MathAotValue, MathBackend, StitchBackend, VirInterpBackend,
|
|
};
|
|
use makepad_script::makepad_math::Vec3f;
|
|
use makepad_script::*;
|
|
|
|
const FUZZ_EXPRS: u64 = 600;
|
|
const INPUTS_PER_EXPR: usize = 6;
|
|
|
|
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()),
|
|
}
|
|
}
|
|
|
|
/// xorshift64* — deterministic, dependency-free.
|
|
struct Rng(u64);
|
|
|
|
impl Rng {
|
|
fn next(&mut self) -> u64 {
|
|
let mut x = self.0;
|
|
x ^= x >> 12;
|
|
x ^= x << 25;
|
|
x ^= x >> 27;
|
|
self.0 = x;
|
|
x.wrapping_mul(0x2545F4914F6CDD1D)
|
|
}
|
|
|
|
fn below(&mut self, n: u64) -> u64 {
|
|
self.next() % n
|
|
}
|
|
|
|
fn f64(&mut self) -> f64 {
|
|
// Mix of magnitudes and specials.
|
|
match self.below(12) {
|
|
0 => 0.0,
|
|
1 => -0.0,
|
|
2 => 1.0,
|
|
3 => -1.0,
|
|
4 => (self.below(2000) as f64 - 1000.0) / 64.0,
|
|
5 => (self.below(2000) as f64 - 1000.0) * 64.0,
|
|
_ => (self.below(1_000_000) as f64 - 500_000.0) / 32768.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generates a scalar-valued expression over params `x`, `y` (scalars)
|
|
/// and `p`, `q` (vec3s).
|
|
fn gen_scalar(rng: &mut Rng, depth: u32) -> String {
|
|
if depth == 0 {
|
|
return match rng.below(7) {
|
|
0 => "x".into(),
|
|
1 => "y".into(),
|
|
6 => "u".into(),
|
|
2 => format!("{:.4}", (rng.below(4000) as f64 - 2000.0) / 256.0),
|
|
3 => format!("{}", rng.below(9)), // integer: parser inline-fusion path
|
|
4 => "p.x".into(),
|
|
5 => ["p.y", "p.z", "q.x", "q.y", "q.z"][rng.below(5) as usize].into(),
|
|
_ => unreachable!(),
|
|
};
|
|
}
|
|
let d = depth - 1;
|
|
match rng.below(22) {
|
|
0 => format!("({} + {})", gen_scalar(rng, d), gen_scalar(rng, d)),
|
|
1 => format!("({} - {})", gen_scalar(rng, d), gen_scalar(rng, d)),
|
|
2 => format!("({} * {})", gen_scalar(rng, d), gen_scalar(rng, d)),
|
|
3 => format!("({} / {})", gen_scalar(rng, d), gen_scalar(rng, d)),
|
|
4 => format!("({} % {})", gen_scalar(rng, d), gen_scalar(rng, d)),
|
|
5 => format!("(-{})", gen_scalar(rng, d)),
|
|
6 => {
|
|
let f = ["sin", "cos", "tan", "asin", "acos", "atan", "exp", "log", "sqrt",
|
|
"abs", "floor", "ceil", "fract"][rng.below(13) as usize];
|
|
format!("{}({})", f, gen_scalar(rng, d))
|
|
}
|
|
7 => {
|
|
let f = ["min", "max", "pow", "atan2", "step", "modf"][rng.below(6) as usize];
|
|
format!("{}({}, {})", f, gen_scalar(rng, d), gen_scalar(rng, d))
|
|
}
|
|
8 => format!(
|
|
"clamp({}, {}, {})",
|
|
gen_scalar(rng, d),
|
|
gen_scalar(rng, d),
|
|
gen_scalar(rng, d)
|
|
),
|
|
9 => format!(
|
|
"mix({}, {}, {})",
|
|
gen_scalar(rng, d),
|
|
gen_scalar(rng, d),
|
|
gen_scalar(rng, d)
|
|
),
|
|
10 => format!(
|
|
"smoothstep({}, {}, {})",
|
|
gen_scalar(rng, d),
|
|
gen_scalar(rng, d),
|
|
gen_scalar(rng, d)
|
|
),
|
|
11 => format!("dot({}, {})", gen_vec(rng, d), gen_vec(rng, d)),
|
|
12 => format!("length({})", gen_vec(rng, d)),
|
|
13 => format!("distance({}, {})", gen_vec(rng, d), gen_vec(rng, d)),
|
|
14 => {
|
|
let cc = ["<", ">", "<=", ">="][rng.below(4) as usize];
|
|
format!(
|
|
"if {} {} {} {{ {} }} else {{ {} }}",
|
|
gen_scalar(rng, d),
|
|
cc,
|
|
gen_scalar(rng, d),
|
|
gen_scalar(rng, d),
|
|
gen_scalar(rng, d)
|
|
)
|
|
}
|
|
15 => format!("({} && {})", gen_scalar(rng, d), gen_scalar(rng, d)),
|
|
16 => format!("({} || {})", gen_scalar(rng, d), gen_scalar(rng, d)),
|
|
17 => format!("{}.length()", gen_vec(rng, d)),
|
|
18 => format!("{}.dot({})", gen_vec(rng, d), gen_vec(rng, d)),
|
|
19 => {
|
|
let sw = ["x", "y", "z"][rng.below(3) as usize];
|
|
format!("{}.{}", gen_vec(rng, d), sw)
|
|
}
|
|
20 => format!("lerp({}, {}, {})", gen_scalar(rng, d), gen_scalar(rng, d), gen_scalar(rng, d)),
|
|
21 => format!("length(cross({}, {}))", gen_vec(rng, d), gen_vec(rng, d)),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
/// Generates a vec3-valued expression.
|
|
fn gen_vec(rng: &mut Rng, depth: u32) -> String {
|
|
if depth == 0 {
|
|
return if rng.below(2) == 0 { "p".into() } else { "q".into() };
|
|
}
|
|
let d = depth - 1;
|
|
match rng.below(15) {
|
|
0 => format!("({} + {})", gen_vec(rng, d), gen_vec(rng, d)),
|
|
1 => format!("({} - {})", gen_vec(rng, d), gen_vec(rng, d)),
|
|
2 => format!("({} * {})", gen_vec(rng, d), gen_vec(rng, d)),
|
|
3 => format!("({} / {})", gen_vec(rng, d), gen_vec(rng, d)),
|
|
4 => format!("({} * {})", gen_vec(rng, d), gen_scalar(rng, d)),
|
|
5 => format!("({} * {})", gen_scalar(rng, d), gen_vec(rng, d)),
|
|
6 => format!("(-{})", gen_vec(rng, d)),
|
|
7 => {
|
|
let f = ["sin", "cos", "abs", "floor", "ceil", "fract"][rng.below(6) as usize];
|
|
format!("{}({})", f, gen_vec(rng, d))
|
|
}
|
|
8 => format!("normalize({})", gen_vec(rng, d)),
|
|
9 => format!("cross({}, {})", gen_vec(rng, d), gen_vec(rng, d)),
|
|
10 => {
|
|
let f = ["min", "max"][rng.below(2) as usize];
|
|
format!("{}({}, {})", f, gen_vec(rng, d), gen_vec(rng, d))
|
|
}
|
|
11 => format!(
|
|
"mix({}, {}, {})",
|
|
gen_vec(rng, d),
|
|
gen_vec(rng, d),
|
|
gen_scalar(rng, d)
|
|
),
|
|
12 => {
|
|
let sw = ["zyx", "xxy", "yzx", "zzz", "xyz", "yx"][rng.below(6) as usize];
|
|
if sw.len() == 2 {
|
|
// A vec2 swizzle immediately widened again is not valid
|
|
// vec3 math; use a 3-lane swizzle instead.
|
|
format!("{}.zxy", gen_vec(rng, d))
|
|
} else {
|
|
format!("{}.{}", gen_vec(rng, d), sw)
|
|
}
|
|
}
|
|
13 => format!(
|
|
"vec3({}, {}, {})",
|
|
gen_scalar(rng, d),
|
|
gen_scalar(rng, d),
|
|
gen_scalar(rng, d)
|
|
),
|
|
14 => format!(
|
|
"clamp({}, {}, {})",
|
|
gen_vec(rng, d),
|
|
gen_scalar(rng, d),
|
|
gen_scalar(rng, d)
|
|
),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
fn bits_match(a: f64, b: f64) -> bool {
|
|
(a.is_nan() && b.is_nan()) || a.to_bits() == b.to_bits()
|
|
}
|
|
|
|
#[test]
|
|
fn differential_fuzz() {
|
|
let exprs: u64 = std::env::var("MATH_AOT_FUZZ_EXPRS")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(FUZZ_EXPRS);
|
|
let mut vm = test_vm();
|
|
let aot = MathAot::new(&mut vm);
|
|
let stitch = StitchBackend::new();
|
|
let vir_interp = VirInterpBackend;
|
|
let params = [
|
|
MathAotParam::Scalar,
|
|
MathAotParam::Scalar,
|
|
MathAotParam::Vec3,
|
|
MathAotParam::Vec3,
|
|
];
|
|
|
|
let mut accepted = 0u64;
|
|
let mut rejected = 0u64;
|
|
for seed in 1..=exprs {
|
|
let mut rng = Rng(seed.wrapping_mul(0x9E3779B97F4A7C15) | 1);
|
|
let depth = 2 + (seed % 3) as u32;
|
|
let expr = gen_scalar(&mut rng, depth);
|
|
let code = format!("use mod.math.*\nuse mod.pod.*\nlet f = |x, y, p, q, u| {expr}\n(f)");
|
|
|
|
vm.bx.captured_errors = Some(Vec::new());
|
|
let fn_value = vm.eval(ScriptMod {
|
|
cargo_manifest_path: String::new(),
|
|
module_path: String::new(),
|
|
file: format!("fuzz_{seed}"),
|
|
line: 0,
|
|
column: 0,
|
|
code: code.clone(),
|
|
values: vec![],
|
|
});
|
|
let errors = vm.take_errors();
|
|
assert!(errors.is_empty(), "seed {seed} script errors: {errors:?}\n{code}");
|
|
assert!(fn_value.as_object().is_some(), "seed {seed}: no fn\n{code}");
|
|
|
|
let Some(virf) = aot.to_vir(&vm, fn_value, ¶ms, &[MathAotParam::Scalar]) else {
|
|
rejected += 1;
|
|
continue;
|
|
};
|
|
accepted += 1;
|
|
let compiled_stitch = stitch.compile(&virf).expect("stitch backend");
|
|
let compiled_ref = vir_interp.compile(&virf).expect("vir interp backend");
|
|
|
|
for _ in 0..INPUTS_PER_EXPR {
|
|
let x = rng.f64();
|
|
let y = rng.f64();
|
|
let p = [rng.f64() as f32, rng.f64() as f32, rng.f64() as f32];
|
|
let q = [rng.f64() as f32, rng.f64() as f32, rng.f64() as f32];
|
|
let u = rng.f64() as f32;
|
|
let args = [
|
|
MathAotValue::Scalar(x),
|
|
MathAotValue::Scalar(y),
|
|
MathAotValue::Vec3(p),
|
|
MathAotValue::Vec3(q),
|
|
MathAotValue::Scalar(u as f64),
|
|
];
|
|
let script_args: Vec<ScriptValue> = vec![
|
|
x.into(),
|
|
y.into(),
|
|
Vec3f {
|
|
x: p[0],
|
|
y: p[1],
|
|
z: p[2],
|
|
}
|
|
.script_to_value(&mut vm),
|
|
Vec3f {
|
|
x: q[0],
|
|
y: q[1],
|
|
z: q[2],
|
|
}
|
|
.script_to_value(&mut vm),
|
|
(u as f64).into(),
|
|
];
|
|
let interp = vm.call(fn_value, &script_args);
|
|
let interp = interp.as_number().unwrap_or_else(|| {
|
|
panic!("seed {seed}: interpreter returned non-number\n{code}")
|
|
});
|
|
let aot_stitch = compiled_stitch.call(&args).expect("stitch call");
|
|
let aot_ref = compiled_ref.call(&args).expect("ref call");
|
|
assert!(
|
|
bits_match(aot_stitch, interp),
|
|
"seed {seed} STITCH mismatch\n{code}\nargs x={x:?} y={y:?} p={p:?} q={q:?}\ninterp {interp:?} ({:#x}) stitch {aot_stitch:?} ({:#x})",
|
|
interp.to_bits(),
|
|
aot_stitch.to_bits()
|
|
);
|
|
assert!(
|
|
bits_match(aot_ref, interp),
|
|
"seed {seed} VIR-INTERP mismatch\n{code}\nargs x={x:?} y={y:?} p={p:?} q={q:?}\ninterp {interp:?} ({:#x}) vir {aot_ref:?} ({:#x})",
|
|
interp.to_bits(),
|
|
aot_ref.to_bits()
|
|
);
|
|
}
|
|
|
|
// Batch-vs-call parity on a few points (both backends).
|
|
let n = 5;
|
|
let mut input = Vec::new();
|
|
let mut rng2 = Rng(seed.wrapping_mul(0xD1342543DE82EF95) | 1);
|
|
for _ in 0..n {
|
|
for _ in 0..virf.stride() {
|
|
input.push(rng2.f64() as f32);
|
|
}
|
|
}
|
|
let ub = [rng2.f64() as f32];
|
|
let mut out_stitch = vec![0f32; n];
|
|
let mut out_ref = vec![0f32; n];
|
|
compiled_stitch.eval_batch(&input, &ub, &mut out_stitch);
|
|
compiled_ref.eval_batch(&input, &ub, &mut out_ref);
|
|
for i in 0..n {
|
|
let s = i * virf.stride();
|
|
let args = [
|
|
MathAotValue::Scalar(input[s] as f64),
|
|
MathAotValue::Scalar(input[s + 1] as f64),
|
|
MathAotValue::Vec3([input[s + 2], input[s + 3], input[s + 4]]),
|
|
MathAotValue::Vec3([input[s + 5], input[s + 6], input[s + 7]]),
|
|
MathAotValue::Scalar(ub[0] as f64),
|
|
];
|
|
let expected = compiled_stitch.call(&args).unwrap() as f32;
|
|
let sb = out_stitch[i];
|
|
let rb = out_ref[i];
|
|
let ok_s = (sb.is_nan() && expected.is_nan()) || sb.to_bits() == expected.to_bits();
|
|
let ok_r = (rb.is_nan() && expected.is_nan()) || rb.to_bits() == expected.to_bits();
|
|
assert!(ok_s, "seed {seed} batch/stitch point {i}\n{code}");
|
|
assert!(ok_r, "seed {seed} batch/vir point {i}\n{code}");
|
|
}
|
|
}
|
|
|
|
println!("differential fuzz: {accepted} accepted, {rejected} rejected");
|
|
// The generator emits only subset constructs; a high rejection rate
|
|
// would mean the fuzz has stopped covering the compiler.
|
|
assert!(
|
|
accepted * 5 >= (accepted + rejected) * 4,
|
|
"acceptance too low: {accepted} accepted, {rejected} rejected"
|
|
);
|
|
// Silence unused-warning when the vir module is only used via to_vir.
|
|
let _ = vir::VirTy::F64;
|
|
}
|