Insane linux fixes

This commit is contained in:
2026-02-03 01:15:07 +01:00
parent 2cee1ba686
commit 33ee1822a5
2 changed files with 110 additions and 22 deletions

View File

@@ -69,6 +69,10 @@ ringbuf = "0.4"
arc-swap = "1" arc-swap = "1"
midir = "0.10" midir = "0.10"
parking_lot = "0.12" 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) # Desktop-only dependencies (behind feature flag)
egui = { version = "0.33", optional = true } egui = { version = "0.33", optional = true }

View File

@@ -843,7 +843,14 @@ impl SequencerState {
let beat_int = (beat * 4.0 * speed_mult).floor() as i64; let beat_int = (beat * 4.0 * speed_mult).floor() as i64;
let prev_beat_int = (prev_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; result.any_step_fired = true;
let step_idx = active.step_index % pattern.length; let step_idx = active.step_index % pattern.length;
@@ -1054,15 +1061,30 @@ fn sequencer_loop(
#[cfg(unix)] #[cfg(unix)]
{ {
use thread_priority::unix::{ use thread_priority::unix::{
set_thread_priority_and_policy, thread_native_id, RealtimeThreadSchedulePolicy, set_thread_priority_and_policy, thread_native_id, NormalThreadSchedulePolicy,
ThreadSchedulePolicy, RealtimeThreadSchedulePolicy, ThreadSchedulePolicy,
}; };
let policy = ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Fifo); let tid = thread_native_id();
if let Err(e) =
set_thread_priority_and_policy(thread_native_id(), ThreadPriority::Max, policy) // Try SCHED_FIFO first (requires CAP_SYS_NICE on Linux)
{ let fifo = ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Fifo);
eprintln!("Warning: Could not set SCHED_FIFO: {e:?}"); 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; let substep_us = (secs_per_substep * 1_000_000.0) as u64;
// Sleep for most of the substep duration, clamped to reasonable bounds // Sleep for most of the substep duration, clamped to reasonable bounds
let sleep_us = substep_us.saturating_sub(500).clamp(50, 2000); 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<Option<TimerFd>> = 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<f64>)> { fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
if !cmd.starts_with("/midi/") { if !cmd.starts_with("/midi/") {
return None; return None;
@@ -1632,19 +1698,23 @@ mod tests {
0.5, 0.5,
)); ));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); // beat_int at 0.5 is 2, prev_beat_int at 0.0 is 0
assert_eq!(ap.step_index, 1); // steps_to_fire = 2-0 = 2, firing steps 0 and 1, wrapping to 0
assert_eq!(ap.iter, 0);
state.tick(tick_at(0.75, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
assert_eq!(ap.step_index, 0); assert_eq!(ap.step_index, 0);
assert_eq!(ap.iter, 1); 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(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
assert_eq!(ap.step_index, 1); assert_eq!(ap.step_index, 1);
assert_eq!(ap.iter, 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] #[test]
@@ -1673,9 +1743,15 @@ mod tests {
0.5, 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)); state.tick(tick_at(0.625, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); 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] #[test]
@@ -1840,14 +1916,22 @@ mod tests {
0.5, 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(0.75, true));
state.tick(tick_at(1.0, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
assert_eq!(ap.step_index, 3); 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 // 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( state.tick(tick_with(
vec![SeqCommand::PatternUpdate { vec![SeqCommand::PatternUpdate {
bank: 0, bank: 0,
@@ -1857,12 +1941,12 @@ mod tests {
1.25, 1.25,
)); ));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); 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)); state.tick(tick_at(1.5, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap(); 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] #[test]