makepad/libs/piano_model/tests/rt_safety.rs
Admin 145d0b1fe2 score: the notation suite — engraving, layout, playback, midi and musicxml import, the physical piano model, and the score app
Squashed from work:
- score: a headless music engraving, playback and notation engine
- score: the notation app — pianist mode, editing, playback
- piano_model: it was a plucked string by construction, and 20 voicings
- score: one document you can pan, zoom and navigate
- piano_model: the body tap was a click, and the objective was rewarding noise
- score: add the sound panel and library modules
- piano_model: a second engine, and the attack that finally sounded right
- score: two instruments, reverb and brightness — and the rest of the panel gone
- score model: a note remembers how it was struck, and the score remembers the pedal
- score import: keep the velocities and the pedal the file was carrying
- score playback: play the performance, not a flattened copy of it
- score ui: the music list moves to the sidebar, and the view stops fighting itself
- score: the application ships its font and eight performances
- piano_model: a limiter that rides the music, so the knee stops shaping chords
- piano_model: the forte bell was the treble's dynamic slope, and the bass was dying at its own prompt rate
- piano_model: the bridge decides each partial's decay, and a fixed multiplier cannot say that
- piano_model: each partial gets its own two coupled modes, from the eigen algebra
- piano_model: a median that fell between the peaks made every bass partial a drain
- score-ai: LocalBroker — the seam's in-process implementation over the session engine (aicore P8)
- client + chat dispatcher: the dead wire comes out (aicore P7/P8)
- score_pdf: the score model grew a pedal map — the pdf importer initialises it
- libs: the zero-warning sweep — stitch casts say what they mean, xatlas keeps upstream's surface quietly
- score app: the shipped-piece test speaks the PERFORMANCES table
- zero-warning sweep, round three — the model lanes and the deep examples
2026-09-01 16:46:32 +02:00

87 lines
3.4 KiB
Rust

// Proof that Piano::process never allocates: a counting global allocator
// wraps the system allocator for this whole test binary; after construction
// and warm-up, two seconds of heavy rendering (notes, re-strikes, pedal
// churn) must leave the allocation counter untouched.
//
// (Locks/IO: the render path calls no std sync or IO APIs at all — verified
// by review; the multicore path is offline-only and documented as such.
// Panics: every slice access in the render path is bounds-checked against
// preallocated fixed sizes, and the adversarial test in verify.rs exercises
// the edge cases.)
use makepad_piano_model::{Piano, PianoEvent, TimedEvent};
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
static DEALLOCS: AtomicUsize = AtomicUsize::new(0);
struct CountingAlloc;
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
DEALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static A: CountingAlloc = CountingAlloc;
#[test]
fn render_path_never_allocates() {
let mut piano = Piano::new(48000.0);
let mut out_l = vec![0.0f32; 512];
let mut out_r = vec![0.0f32; 512];
let mut events: Vec<TimedEvent> = Vec::with_capacity(64);
// Warm-up: touch every code path (strikes on every key, damper noise,
// sympathetic banks, voice sleep/wake) before counting.
for key in 21..=108u8 {
events.clear();
events.push(TimedEvent { offset: 0, event: PianoEvent::NoteOn { key, velocity: 100 } });
events.push(TimedEvent { offset: 256, event: PianoEvent::NoteOff { key } });
piano.process(&events, &mut out_l, &mut out_r);
}
for _ in 0..200 {
piano.process(&[], &mut out_l, &mut out_r);
}
let a0 = ALLOCS.load(Ordering::Relaxed);
let d0 = DEALLOCS.load(Ordering::Relaxed);
// Two seconds of dense playing through the counted section.
let mut key = 21u8;
for block in 0..188u32 {
events.clear();
if block % 2 == 0 {
events.push(TimedEvent { offset: (block % 512) & 511, event: PianoEvent::NoteOn { key, velocity: 127 } });
key = if key >= 108 { 21 } else { key + 1 };
}
if block % 3 == 0 {
events.push(TimedEvent {
offset: 511,
event: PianoEvent::Sustain { value: if block % 6 == 0 { 1.0 } else { 0.0 } },
});
}
if block % 7 == 0 {
events.push(TimedEvent { offset: 100, event: PianoEvent::NoteOff { key: 21 + (block as u8 % 88) } });
}
events.sort_by_key(|e| e.offset);
piano.process(&events, &mut out_l, &mut out_r);
}
let allocs = ALLOCS.load(Ordering::Relaxed) - a0;
let deallocs = DEALLOCS.load(Ordering::Relaxed) - d0;
assert_eq!(allocs, 0, "Piano::process allocated {allocs} times");
assert_eq!(deallocs, 0, "Piano::process deallocated {deallocs} times");
println!("render path: 0 allocations across 188 blocks of dense playing");
}