Still searching...

This commit is contained in:
2026-02-03 02:53:34 +01:00
parent b305df3d79
commit 5c805c60d7
5 changed files with 146 additions and 126 deletions

View File

@@ -3,11 +3,12 @@ use crossbeam_channel::{Receiver, RecvTimeoutError, Sender};
use std::cmp::Ordering;
use std::collections::BinaryHeap;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use super::link::LinkState;
use super::sequencer::{AudioCommand, MidiCommand};
use super::timing::{SyncTime, ACTIVE_WAIT_THRESHOLD_US};
use super::sequencer::{set_realtime_priority, AudioCommand, MidiCommand};
use super::timing::SyncTime;
/// A command scheduled for dispatch at a specific time.
#[derive(Clone)]
@@ -47,6 +48,9 @@ impl PartialEq for TimedCommand {
impl Eq for TimedCommand {}
/// Spin-wait threshold in microseconds for dispatcher.
const SPIN_THRESHOLD_US: SyncTime = 100;
/// Main dispatcher loop - receives timed commands and dispatches them at the right moment.
pub fn dispatcher_loop(
cmd_rx: Receiver<TimedCommand>,
@@ -54,6 +58,13 @@ pub fn dispatcher_loop(
midi_tx: Arc<ArcSwap<Sender<MidiCommand>>>,
link: Arc<LinkState>,
) {
let has_rt = set_realtime_priority();
#[cfg(target_os = "linux")]
if !has_rt {
eprintln!("[cagire] Warning: Could not set realtime priority for dispatcher thread.");
}
let mut queue: BinaryHeap<TimedCommand> = BinaryHeap::with_capacity(256);
loop {
@@ -81,9 +92,9 @@ pub fn dispatcher_loop(
// Dispatch ready commands
let current_us = link.clock_micros() as SyncTime;
while let Some(cmd) = queue.peek() {
if cmd.target_time_us <= current_us + ACTIVE_WAIT_THRESHOLD_US {
if cmd.target_time_us <= current_us + SPIN_THRESHOLD_US {
let cmd = queue.pop().unwrap();
wait_until_dispatch(cmd.target_time_us, &link);
wait_until_dispatch(cmd.target_time_us, &link, has_rt);
dispatch_command(cmd.command, &audio_tx, &midi_tx);
} else {
break;
@@ -92,10 +103,41 @@ pub fn dispatcher_loop(
}
}
/// Active-wait until the target time for precise dispatch.
fn wait_until_dispatch(target_us: SyncTime, link: &LinkState) {
while (link.clock_micros() as SyncTime) < target_us {
std::hint::spin_loop();
/// High-precision sleep using clock_nanosleep on Linux
#[cfg(target_os = "linux")]
fn precise_sleep(micros: u64) {
let duration_ns = micros * 1000;
let ts = libc::timespec {
tv_sec: (duration_ns / 1_000_000_000) as i64,
tv_nsec: (duration_ns % 1_000_000_000) as i64,
};
unsafe {
libc::clock_nanosleep(libc::CLOCK_MONOTONIC, 0, &ts, std::ptr::null_mut());
}
}
#[cfg(not(target_os = "linux"))]
fn precise_sleep(micros: u64) {
thread::sleep(Duration::from_micros(micros));
}
/// Wait until the target time for dispatch.
/// With RT priority: spin-wait for precision
/// Without RT priority: sleep (spinning wastes CPU without benefit)
fn wait_until_dispatch(target_us: SyncTime, link: &LinkState, has_rt: bool) {
let current = link.clock_micros() as SyncTime;
let remaining = target_us.saturating_sub(current);
if has_rt {
// With RT priority: spin-wait for precise timing
while (link.clock_micros() as SyncTime) < target_us {
std::hint::spin_loop();
}
} else {
// Without RT priority: sleep (spin-waiting is counterproductive)
if remaining > 0 {
precise_sleep(remaining);
}
}
}

View File

@@ -4,10 +4,7 @@ mod link;
pub mod sequencer;
mod timing;
pub use timing::{
beats_to_micros, micros_to_beats, micros_until_next_substep, substeps_crossed, StepTiming,
SyncTime, ACTIVE_WAIT_THRESHOLD_US, NEVER,
};
pub use timing::{micros_until_next_substep, substeps_crossed, StepTiming, SyncTime};
// AnalysisHandle and SequencerHandle are used by src/bin/desktop.rs
#[allow(unused_imports)]

View File

@@ -9,13 +9,11 @@ use std::thread::{self, JoinHandle};
use std::time::Duration;
#[cfg(not(unix))]
use thread_priority::set_current_thread_priority;
#[allow(unused_imports)]
use thread_priority::ThreadPriority;
use super::dispatcher::{dispatcher_loop, DispatchCommand, TimedCommand};
use super::{
micros_until_next_substep, substeps_crossed, LinkState, StepTiming, SyncTime,
ACTIVE_WAIT_THRESHOLD_US,
};
use super::{micros_until_next_substep, substeps_crossed, LinkState, StepTiming, SyncTime};
use crate::model::{
CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables,
};
@@ -1039,39 +1037,17 @@ fn sequencer_loop(
) {
use std::sync::atomic::Ordering;
#[cfg(unix)]
{
use thread_priority::unix::{
set_thread_priority_and_policy, thread_native_id, NormalThreadSchedulePolicy,
RealtimeThreadSchedulePolicy, ThreadSchedulePolicy,
};
let has_rt = set_realtime_priority();
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);
}
}
}
}
#[cfg(not(unix))]
{
let _ = set_current_thread_priority(ThreadPriority::Max);
#[cfg(target_os = "linux")]
if !has_rt {
eprintln!("[cagire] Warning: Could not set realtime priority for sequencer thread.");
eprintln!("[cagire] For best performance on Linux, configure rtprio limits:");
eprintln!("[cagire] Add your user to 'audio' group: sudo usermod -aG audio $USER");
eprintln!("[cagire] Edit /etc/security/limits.conf and add:");
eprintln!("[cagire] @audio - rtprio 95");
eprintln!("[cagire] @audio - memlock unlimited");
eprintln!("[cagire] Then log out and back in.");
}
let mut seq_state = SequencerState::new(variables, dict, rng, cc_access);
@@ -1203,47 +1179,26 @@ fn sequencer_loop(
};
let target_time_us = current_time_us + next_event_us;
wait_until(target_time_us, &link);
wait_until(target_time_us, &link, has_rt);
}
}
/// High-precision sleep using timerfd on Linux, falling back to thread::sleep elsewhere
/// Spin-wait threshold in microseconds. With RT priority, we sleep most of the
/// wait time and spin-wait for the final portion for precision. Without RT priority,
/// spinning is counterproductive and we sleep the entire duration.
const SPIN_THRESHOLD_US: SyncTime = 100;
/// High-precision sleep using clock_nanosleep on Linux
#[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) };
let duration_ns = micros * 1000;
let ts = libc::timespec {
tv_sec: (duration_ns / 1_000_000_000) as i64,
tv_nsec: (duration_ns % 1_000_000_000) as i64,
};
unsafe {
libc::clock_nanosleep(libc::CLOCK_MONOTONIC, 0, &ts, std::ptr::null_mut());
}
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"))]
@@ -1251,18 +1206,77 @@ fn precise_sleep(micros: u64) {
thread::sleep(Duration::from_micros(micros));
}
/// Two-phase wait: bulk sleep followed by active spin-wait for final precision.
fn wait_until(target_us: SyncTime, link: &LinkState) {
/// Attempts to set realtime scheduling priority for the current thread.
/// Returns true if RT priority was successfully set, false otherwise.
///
/// On Linux, this requires either:
/// - CAP_SYS_NICE capability, or
/// - Configured rtprio limits in /etc/security/limits.conf:
/// @audio - rtprio 95
/// @audio - memlock unlimited
#[cfg(unix)]
pub fn set_realtime_priority() -> bool {
use thread_priority::unix::{
set_thread_priority_and_policy, thread_native_id, NormalThreadSchedulePolicy,
RealtimeThreadSchedulePolicy, ThreadSchedulePolicy,
};
use thread_priority::ThreadPriority;
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_ok() {
return true;
}
// 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_ok() {
return true;
}
// 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 on Linux
#[cfg(target_os = "linux")]
unsafe {
libc::setpriority(libc::PRIO_PROCESS, 0, -20);
}
false
}
#[cfg(not(unix))]
pub fn set_realtime_priority() -> bool {
set_current_thread_priority(ThreadPriority::Max).is_ok()
}
/// Two-phase wait: sleep most of the time, optionally spin-wait for final precision.
/// With RT priority: sleep + spin for precision
/// Without RT priority: sleep only (spinning wastes CPU without benefit)
fn wait_until(target_us: SyncTime, link: &LinkState, has_rt_priority: bool) {
let current = link.clock_micros() as SyncTime;
let remaining = target_us.saturating_sub(current);
if remaining > ACTIVE_WAIT_THRESHOLD_US {
precise_sleep(remaining - ACTIVE_WAIT_THRESHOLD_US);
}
// Active wait for final precision
while (link.clock_micros() as SyncTime) < target_us {
std::hint::spin_loop();
if has_rt_priority {
// With RT priority: sleep most, spin for final precision
if remaining > SPIN_THRESHOLD_US {
precise_sleep(remaining - SPIN_THRESHOLD_US);
}
while (link.clock_micros() as SyncTime) < target_us {
std::hint::spin_loop();
}
} else {
// Without RT priority: sleep the entire time (spin-waiting is counterproductive)
if remaining > 0 {
precise_sleep(remaining);
}
}
}

View File

@@ -1,25 +1,14 @@
/// Microsecond-precision timestamp for audio synchronization.
pub type SyncTime = u64;
/// Sentinel value representing "never" or "no scheduled time".
pub const NEVER: SyncTime = SyncTime::MAX;
/// Convert beat duration to microseconds at given tempo.
pub fn beats_to_micros(beats: f64, tempo: f64) -> SyncTime {
fn beats_to_micros(beats: f64, tempo: f64) -> SyncTime {
if tempo <= 0.0 {
return 0;
}
((beats / tempo) * 60_000_000.0).round() as SyncTime
}
/// Convert microseconds to beats at given tempo.
pub fn micros_to_beats(micros: SyncTime, tempo: f64) -> f64 {
if tempo <= 0.0 {
return 0.0;
}
(tempo * (micros as f64)) / 60_000_000.0
}
/// Timing boundary types for step and pattern scheduling.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StepTiming {
@@ -67,9 +56,6 @@ pub fn substeps_crossed(prev_beat: f64, curr_beat: f64, speed: f64) -> usize {
(curr_substep - prev_substep).clamp(0, 16) as usize
}
/// Threshold for switching from sleep to active wait (100μs).
pub const ACTIVE_WAIT_THRESHOLD_US: SyncTime = 100;
/// Calculate microseconds until the next substep boundary.
pub fn micros_until_next_substep(current_beat: f64, speed: f64, tempo: f64) -> SyncTime {
if tempo <= 0.0 || speed <= 0.0 {
@@ -94,26 +80,9 @@ mod tests {
assert_eq!(beats_to_micros(0.5, 120.0), 250_000);
}
#[test]
fn test_micros_to_beats_at_120_bpm() {
// At 120 BPM, 500,000 microseconds = 1 beat
assert!((micros_to_beats(500_000, 120.0) - 1.0).abs() < 1e-10);
assert!((micros_to_beats(1_000_000, 120.0) - 2.0).abs() < 1e-10);
}
#[test]
fn test_zero_tempo() {
assert_eq!(beats_to_micros(1.0, 0.0), 0);
assert_eq!(micros_to_beats(1_000_000, 0.0), 0.0);
}
#[test]
fn test_roundtrip() {
let tempo = 135.0;
let beats = 3.75;
let micros = beats_to_micros(beats, tempo);
let back = micros_to_beats(micros, tempo);
assert!((back - beats).abs() < 1e-6);
}
#[test]