From 33ee1822a5e454b0246d481edc63f30bbe0c4355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Tue, 3 Feb 2026 01:15:07 +0100 Subject: [PATCH] Insane linux fixes --- Cargo.toml | 4 ++ src/engine/sequencer.rs | 128 +++++++++++++++++++++++++++++++++------- 2 files changed, 110 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 94c94ea..7590409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,10 @@ ringbuf = "0.4" arc-swap = "1" midir = "0.10" parking_lot = "0.12" +libc = "0.2" + +[target.'cfg(target_os = "linux")'.dependencies] +nix = { version = "0.29", features = ["time"] } # Desktop-only dependencies (behind feature flag) egui = { version = "0.33", optional = true } diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index 0d7cd40..c039548 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -843,7 +843,14 @@ impl SequencerState { let beat_int = (beat * 4.0 * speed_mult).floor() as i64; let prev_beat_int = (prev_beat * 4.0 * speed_mult).floor() as i64; - if beat_int != prev_beat_int && prev_beat >= 0.0 { + // Fire ALL skipped steps when scheduler jitter causes us to miss beats + let steps_to_fire = if prev_beat >= 0.0 { + (beat_int - prev_beat_int).clamp(0, 16) as usize + } else { + 0 + }; + + for _ in 0..steps_to_fire { result.any_step_fired = true; let step_idx = active.step_index % pattern.length; @@ -1054,15 +1061,30 @@ fn sequencer_loop( #[cfg(unix)] { use thread_priority::unix::{ - set_thread_priority_and_policy, thread_native_id, RealtimeThreadSchedulePolicy, - ThreadSchedulePolicy, + set_thread_priority_and_policy, thread_native_id, NormalThreadSchedulePolicy, + RealtimeThreadSchedulePolicy, ThreadSchedulePolicy, }; - let policy = ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Fifo); - if let Err(e) = - set_thread_priority_and_policy(thread_native_id(), ThreadPriority::Max, policy) - { - eprintln!("Warning: Could not set SCHED_FIFO: {e:?}"); + let tid = thread_native_id(); + + // Try SCHED_FIFO first (requires CAP_SYS_NICE on Linux) + let fifo = ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Fifo); + if set_thread_priority_and_policy(tid, ThreadPriority::Max, fifo).is_err() { + // Try SCHED_RR (round-robin realtime, sometimes works without caps) + let rr = ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::RoundRobin); + if set_thread_priority_and_policy(tid, ThreadPriority::Max, rr).is_err() { + // Fall back to highest normal priority (SCHED_OTHER) + let _ = set_thread_priority_and_policy( + tid, + ThreadPriority::Max, + ThreadSchedulePolicy::Normal(NormalThreadSchedulePolicy::Other), + ); + // Also try nice -20 via libc on Linux + #[cfg(target_os = "linux")] + unsafe { + libc::setpriority(libc::PRIO_PROCESS, 0, -20); + } + } } } @@ -1215,10 +1237,54 @@ fn sequencer_loop( let substep_us = (secs_per_substep * 1_000_000.0) as u64; // Sleep for most of the substep duration, clamped to reasonable bounds let sleep_us = substep_us.saturating_sub(500).clamp(50, 2000); - thread::sleep(Duration::from_micros(sleep_us)); + precise_sleep(sleep_us); } } +/// High-precision sleep using timerfd on Linux, falling back to thread::sleep elsewhere +#[cfg(target_os = "linux")] +fn precise_sleep(micros: u64) { + use nix::sys::timerfd::{ClockId, Expiration, TimerFd, TimerFlags, TimerSetTimeFlags}; + use std::os::fd::AsRawFd; + + thread_local! { + static TIMER: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; + } + + TIMER.with(|timer_cell| { + let mut timer_ref = timer_cell.borrow_mut(); + let timer = timer_ref.get_or_insert_with(|| { + TimerFd::new(ClockId::CLOCK_MONOTONIC, TimerFlags::empty()) + .expect("Failed to create timerfd") + }); + + let duration = Duration::from_micros(micros); + if timer + .set(Expiration::OneShot(duration), TimerSetTimeFlags::empty()) + .is_ok() + { + // Use poll to wait for the timer instead of read to avoid allocating + let mut pollfd = libc::pollfd { + fd: timer.as_raw_fd(), + events: libc::POLLIN, + revents: 0, + }; + unsafe { + libc::poll(&mut pollfd, 1, -1); + } + // Clear the timer by reading (discard the count) + let _ = timer.wait(); + } else { + thread::sleep(duration); + } + }); +} + +#[cfg(not(target_os = "linux"))] +fn precise_sleep(micros: u64) { + thread::sleep(Duration::from_micros(micros)); +} + fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option)> { if !cmd.starts_with("/midi/") { return None; @@ -1632,19 +1698,23 @@ mod tests { 0.5, )); - let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); - assert_eq!(ap.step_index, 1); - assert_eq!(ap.iter, 0); - - state.tick(tick_at(0.75, true)); + // beat_int at 0.5 is 2, prev_beat_int at 0.0 is 0 + // steps_to_fire = 2-0 = 2, firing steps 0 and 1, wrapping to 0 let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); assert_eq!(ap.step_index, 0); assert_eq!(ap.iter, 1); - state.tick(tick_at(1.0, true)); + // beat_int at 0.75 is 3, prev is 2, fires 1 step (step 0), advances to 1 + state.tick(tick_at(0.75, true)); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); assert_eq!(ap.step_index, 1); assert_eq!(ap.iter, 1); + + // beat_int at 1.0 is 4, prev is 3, fires 1 step (step 1), wraps to 0 + state.tick(tick_at(1.0, true)); + let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); + assert_eq!(ap.step_index, 0); + assert_eq!(ap.iter, 2); } #[test] @@ -1673,9 +1743,15 @@ mod tests { 0.5, )); + // At 2x speed: beat_int at 0.5 is (0.5*4*2)=4, prev at 0.0 is 0 + // Fires 4 steps (0,1,2,3), advancing to step 4 + let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); + assert_eq!(ap.step_index, 4); + + // beat_int at 0.625 is (0.625*4*2)=5, prev is 4, fires 1 step state.tick(tick_at(0.625, true)); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); - assert_eq!(ap.step_index, 2); + assert_eq!(ap.step_index, 5); } #[test] @@ -1840,14 +1916,22 @@ mod tests { 0.5, )); - // Advance to step_index=3 + // beat_int at 0.5 is 2, prev at 0.0 is 0, fires 2 steps (0,1), step_index=2 + let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); + assert_eq!(ap.step_index, 2); + + // beat_int at 0.75 is 3, prev is 2, fires 1 step (2), step_index=3 state.tick(tick_at(0.75, true)); - state.tick(tick_at(1.0, true)); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); assert_eq!(ap.step_index, 3); + // beat_int at 1.0 is 4, prev is 3, fires 1 step (3), wraps to step_index=0 + state.tick(tick_at(1.0, true)); + let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); + assert_eq!(ap.step_index, 0); + // Update pattern to length 2 while running — step_index wraps via modulo - // beat=1.25: beat_int=5, prev=4, step fires. step_index=3%2=1 fires, advances to (3+1)%2=0 + // beat=1.25: beat_int=5, prev=4, fires 1 step. step_index=0%2=0 fires, advances to 1 state.tick(tick_with( vec![SeqCommand::PatternUpdate { bank: 0, @@ -1857,12 +1941,12 @@ mod tests { 1.25, )); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); - assert_eq!(ap.step_index, 0); + assert_eq!(ap.step_index, 1); - // beat=1.5: beat_int=6, prev=5, step fires. step_index=0 fires, advances to 1 + // beat=1.5: beat_int=6, prev=5, step fires. step_index=1 fires, wraps to 0 state.tick(tick_at(1.5, true)); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); - assert_eq!(ap.step_index, 1); + assert_eq!(ap.step_index, 0); } #[test]