makepad/libs/milkdrop/examples/render_check.rs
Admin 012e6f7df5 tools, libs and the rest
cargo-makepad, makepad-builder (with an Android step on every OS), CI tools
and tiles, and the remaining libraries.

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

255 lines
9.5 KiB
Rust

//! Verification rig for the MilkDrop renderer: plays preset files from the
//! command line with synthetic music, stepping on a key or a timer, and logs
//! each preset's load report and the frame's CPU cost.
//!
//! ```text
//! cargo run -p makepad-milkdrop --release --example milkdrop_render_check -- \
//! [--seconds=4] [--pixels=1280x720] <preset.milk | dir>...
//! ```
//!
//! N / P step to the next / previous preset. Run it with `--remote` to grab
//! frames; `MAKEPAD_TRACE=present,gpu.pass` adds GPU time per pass.
pub use makepad_widgets;
use makepad_milkdrop::MilkdropViewWidgetRefExt;
use makepad_widgets::*;
use std::path::PathBuf;
app_main!(App);
script_mod! {
use mod.prelude.widgets.*
use mod.widgets.*
startup() do #(App::script_component(vm)){
ui: Root{
main_window := Window{
window.title: "MilkDrop render check"
window.inner_size: vec2(1280, 720)
body +: {
flow: Overlay
milk := MilkdropView{}
status := Label{
margin: 8
text: ""
draw_text.text_style.font_size: 10
}
}
}
}
}
}
#[derive(Script, ScriptHook)]
pub struct App {
#[live]
ui: WidgetRef,
#[rust]
presets: Vec<PathBuf>,
#[rust]
index: usize,
#[rust]
seconds_per_preset: f64,
#[rust]
since_switch: f64,
#[rust]
next_frame: NextFrame,
#[rust]
last_time: Option<f64>,
#[rust]
synth: Synth,
#[rust]
frames: u64,
#[rust]
cpu_sum: f64,
#[rust]
cpu_max: f64,
#[rust]
reported: bool,
/// Wanted render size; the view's resolution scale is set to reach it.
#[rust]
pixels: Option<(f64, f64)>,
#[rust(1.0)]
scale: f64,
/// Sums of the engine's stage times since the preset loaded.
#[rust]
stages: [f64; 5],
}
/// A deterministic groove: kick, hats, bass line and a pad chord, so the
/// band levels and waveform move like music.
#[derive(Default)]
struct Synth {
t: f64,
noise: u32,
}
impl Synth {
fn render(&mut self, rate: f64, n: usize, out: &mut Vec<f32>) {
out.clear();
for _ in 0..n {
let t = self.t;
let beat = t * 2.0; // 120 bpm
let phase = beat.fract();
let kick = (-phase * 18.0).exp() * (std::f64::consts::TAU * (50.0 + 90.0 * (-phase * 30.0).exp()) * t).sin();
self.noise ^= self.noise << 13;
self.noise ^= self.noise >> 17;
self.noise ^= self.noise << 5;
let white = (self.noise as f64 / u32::MAX as f64) * 2.0 - 1.0;
let hat_phase = (beat * 2.0 + 0.5).fract();
let hat = white * (-hat_phase * 40.0).exp() * 0.25;
let bar = (beat / 4.0).floor() as i64;
let root = [55.0, 55.0 * 1.335, 55.0 * 1.498, 55.0 * 1.26][(bar.rem_euclid(4)) as usize];
let bass = (std::f64::consts::TAU * root * t).sin().signum() * 0.18 * (1.0 - phase * 0.5);
let pad = [1.0, 1.26, 1.498]
.iter()
.map(|k| (std::f64::consts::TAU * root * 4.0 * k * t).sin())
.sum::<f64>()
* 0.05;
out.push((kick * 0.7 + hat + bass + pad) as f32);
self.t += 1.0 / rate;
}
}
}
impl App {
fn load_current(&mut self, cx: &mut Cx) {
let Some(path) = self.presets.get(self.index).cloned() else { return };
self.ui.milkdrop_view(cx, ids!(milk)).load_preset(cx, &path);
self.since_switch = 0.0;
self.reported = false;
self.frames = 0;
self.cpu_sum = 0.0;
self.cpu_max = 0.0;
self.stages = [0.0; 5];
log!("milkdrop: [{}/{}] {}", self.index + 1, self.presets.len(), path.display());
}
}
impl MatchEvent for App {
fn handle_startup(&mut self, cx: &mut Cx) {
self.seconds_per_preset = 0.0;
for arg in std::env::args().skip(1) {
if let Some(s) = arg.strip_prefix("--seconds=") {
self.seconds_per_preset = s.parse().unwrap_or(0.0);
} else if let Some((w, h)) = arg.strip_prefix("--pixels=").and_then(|s| s.split_once('x')) {
self.pixels = w.parse().ok().zip(h.parse().ok());
} else if arg.starts_with("--") {
continue;
} else {
let p = PathBuf::from(&arg);
if p.is_dir() {
let mut found = Vec::new();
let mut stack = vec![p];
while let Some(d) = stack.pop() {
for e in std::fs::read_dir(&d).into_iter().flatten().flatten() {
let q = e.path();
if q.is_dir() {
stack.push(q);
} else if q.extension().is_some_and(|x| x == "milk") {
found.push(q);
}
}
}
found.sort();
self.presets.extend(found);
} else {
self.presets.push(p);
}
}
}
self.synth.noise = 0x1234_5678;
self.ui.milkdrop_view(cx, ids!(milk)).set_playing(cx, true);
self.load_current(cx);
self.next_frame = cx.new_next_frame();
}
fn handle_key_down(&mut self, cx: &mut Cx, e: &KeyEvent) {
match e.key_code {
KeyCode::KeyN => {
self.index = (self.index + 1) % self.presets.len().max(1);
self.load_current(cx);
}
KeyCode::KeyP => {
self.index = (self.index + self.presets.len().max(1) - 1) % self.presets.len().max(1);
self.load_current(cx);
}
_ => {}
}
}
}
impl AppMain for App {
fn script_mod(vm: &mut ScriptVm) -> ScriptValue {
crate::makepad_widgets::script_mod(vm);
makepad_milkdrop::script_mod(vm);
self::script_mod(vm)
}
fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
if let Some(ne) = self.next_frame.is_event(event) {
let dt = self.last_time.replace(ne.time).map(|t| (ne.time - t).clamp(0.0, 0.1)).unwrap_or(0.0);
let milk = self.ui.milkdrop_view(cx, ids!(milk));
let (rw, rh) = milk.render_size();
if let (Some((w, _)), true) = (self.pixels, rw > 0) {
// Aim the render width at the wanted pixels; the height
// follows the window's shape.
if (rw as f64 - w).abs() > 1.0 {
self.scale *= w / rw as f64;
milk.set_resolution_scale(cx, self.scale);
}
}
let rate = 44100.0;
let mut buf = Vec::new();
self.synth.render(rate, (dt * rate) as usize, &mut buf);
milk.feed_audio(&buf, rate as f32);
// Frame cost of the previous frame.
let cpu = milk.cpu_ms();
if cpu > 0.0 {
self.frames += 1;
self.cpu_sum += cpu;
self.cpu_max = self.cpu_max.max(cpu);
let t = milk.stage_times();
for (sum, v) in self.stages.iter_mut().zip([t.per_frame, t.waves, t.shapes, t.per_vertex, t.rest]) {
*sum += v;
}
}
self.since_switch += dt;
let report_after = if self.seconds_per_preset > 0.0 { (self.seconds_per_preset * 0.8).min(6.0) } else { 6.0 };
if !self.reported && self.since_switch > report_after {
self.reported = true;
if let Some(status) = milk.status() {
log!(
"milkdrop: {} | {}x{} | warp shader {} comp shader {} | cpu per-vertex {:?} | cpu waves {:?} | errors {:?} | shader errors {:?} | load {:.1} ms | cpu {:.3} ms/frame avg, {:.3} max | per-frame {:.3} waves {:.3} shapes {:.3} per-vertex {:.3} rest {:.3} ms avg",
status.name,
rw,
rh,
status.warp_shader,
status.comp_shader,
status.equations.vertex_cpu,
status.equations.waves_cpu,
status.equations.errors,
status.shader_errors,
status.load_ms,
self.cpu_sum / self.frames.max(1) as f64,
self.cpu_max,
self.stages[0] / self.frames.max(1) as f64,
self.stages[1] / self.frames.max(1) as f64,
self.stages[2] / self.frames.max(1) as f64,
self.stages[3] / self.frames.max(1) as f64,
self.stages[4] / self.frames.max(1) as f64,
);
let label = format!("{} · {:.2} ms cpu", status.name, self.cpu_sum / self.frames.max(1) as f64);
self.ui.label(cx, ids!(status)).set_text(cx, &label);
}
}
if self.seconds_per_preset > 0.0 && self.since_switch > self.seconds_per_preset && !self.presets.is_empty() {
self.index = (self.index + 1) % self.presets.len();
self.load_current(cx);
}
self.next_frame = cx.new_next_frame();
}
self.match_event(cx, event);
self.ui.handle_event(cx, event, &mut Scope::empty());
}
}