From a07a87a35fbac835570165d5b2b4c56a80b7077e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Tue, 3 Feb 2026 03:08:13 +0100 Subject: [PATCH] Again --- src/bin/desktop.rs | 4 ++ src/engine/dispatcher.rs | 23 ++------ src/engine/mod.rs | 1 + src/engine/realtime.rs | 114 +++++++++++++++++++++++++++++++++++++++ src/engine/sequencer.rs | 78 ++------------------------- src/main.rs | 4 ++ 6 files changed, 129 insertions(+), 95 deletions(-) create mode 100644 src/engine/realtime.rs diff --git a/src/bin/desktop.rs b/src/bin/desktop.rs index 5978097..1e1e332 100644 --- a/src/bin/desktop.rs +++ b/src/bin/desktop.rs @@ -554,6 +554,10 @@ fn load_icon() -> egui::IconData { } fn main() -> eframe::Result<()> { + // Lock memory BEFORE any threads are spawned to prevent page faults in RT context + #[cfg(unix)] + cagire::engine::realtime::lock_memory(); + let args = Args::parse(); let options = NativeOptions { diff --git a/src/engine/dispatcher.rs b/src/engine/dispatcher.rs index cbf9f1c..8ad28ae 100644 --- a/src/engine/dispatcher.rs +++ b/src/engine/dispatcher.rs @@ -3,11 +3,11 @@ 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::{set_realtime_priority, AudioCommand, MidiCommand}; +use super::realtime::{precise_sleep_us, set_realtime_priority}; +use super::sequencer::{AudioCommand, MidiCommand}; use super::timing::SyncTime; /// A command scheduled for dispatch at a specific time. @@ -103,23 +103,6 @@ pub fn dispatcher_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 @@ -136,7 +119,7 @@ fn wait_until_dispatch(target_us: SyncTime, link: &LinkState, has_rt: bool) { } else { // Without RT priority: sleep (spin-waiting is counterproductive) if remaining > 0 { - precise_sleep(remaining); + precise_sleep_us(remaining); } } } diff --git a/src/engine/mod.rs b/src/engine/mod.rs index f3849cf..107eb38 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1,6 +1,7 @@ mod audio; mod dispatcher; mod link; +pub mod realtime; pub mod sequencer; mod timing; diff --git a/src/engine/realtime.rs b/src/engine/realtime.rs new file mode 100644 index 0000000..b454018 --- /dev/null +++ b/src/engine/realtime.rs @@ -0,0 +1,114 @@ +use std::sync::atomic::{AtomicBool, Ordering}; + +static MLOCKALL_CALLED: AtomicBool = AtomicBool::new(false); +static MLOCKALL_SUCCESS: AtomicBool = AtomicBool::new(false); + +/// Locks all current and future memory pages to prevent page faults during RT execution. +/// Must be called BEFORE spawning any threads for maximum effectiveness. +/// Returns true if mlockall succeeded, false otherwise (which is common without rtprio). +#[cfg(unix)] +pub fn lock_memory() -> bool { + if MLOCKALL_CALLED.swap(true, Ordering::SeqCst) { + return MLOCKALL_SUCCESS.load(Ordering::SeqCst); + } + + let result = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) }; + + if result == 0 { + MLOCKALL_SUCCESS.store(true, Ordering::SeqCst); + true + } else { + // Get the actual error for better diagnostics + let errno = std::io::Error::last_os_error(); + eprintln!("[cagire] mlockall failed: {errno}"); + eprintln!("[cagire] Memory locking disabled. For best RT performance on Linux:"); + eprintln!("[cagire] 1. Add user to 'audio' group: sudo usermod -aG audio $USER"); + eprintln!("[cagire] 2. Add to /etc/security/limits.conf:"); + eprintln!("[cagire] @audio - memlock unlimited"); + eprintln!("[cagire] 3. Log out and back in"); + false + } +} + +#[cfg(not(unix))] +pub fn lock_memory() -> bool { + // Windows: VirtualLock exists but isn't typically needed for audio + true +} + +/// Check if memory locking is active. +#[allow(dead_code)] +pub fn is_memory_locked() -> bool { + MLOCKALL_SUCCESS.load(Ordering::Relaxed) +} + +/// 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 { + use thread_priority::{set_current_thread_priority, ThreadPriority}; + set_current_thread_priority(ThreadPriority::Max).is_ok() +} + +/// High-precision sleep using clock_nanosleep on Linux. +/// Uses monotonic clock for jitter-free sleeping. +#[cfg(target_os = "linux")] +pub fn precise_sleep_us(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"))] +pub fn precise_sleep_us(micros: u64) { + std::thread::sleep(std::time::Duration::from_micros(micros)); +} diff --git a/src/engine/sequencer.rs b/src/engine/sequencer.rs index 770742b..6c223f7 100644 --- a/src/engine/sequencer.rs +++ b/src/engine/sequencer.rs @@ -6,13 +6,9 @@ use std::sync::atomic::AtomicU32; use std::sync::atomic::{AtomicI64, AtomicU64}; use std::sync::Arc; 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::realtime::{precise_sleep_us, set_realtime_priority}; use super::{micros_until_next_substep, substeps_crossed, LinkState, StepTiming, SyncTime}; use crate::model::{ CcAccess, Dictionary, ExecutionTrace, Rng, ScriptEngine, StepContext, Value, Variables, @@ -1188,74 +1184,6 @@ fn sequencer_loop( /// 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) { - 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)); -} - -/// 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 @@ -1267,7 +1195,7 @@ fn wait_until(target_us: SyncTime, link: &LinkState, has_rt_priority: bool) { if has_rt_priority { // With RT priority: sleep most, spin for final precision if remaining > SPIN_THRESHOLD_US { - precise_sleep(remaining - SPIN_THRESHOLD_US); + precise_sleep_us(remaining - SPIN_THRESHOLD_US); } while (link.clock_micros() as SyncTime) < target_us { std::hint::spin_loop(); @@ -1275,7 +1203,7 @@ fn wait_until(target_us: SyncTime, link: &LinkState, has_rt_priority: bool) { } else { // Without RT priority: sleep the entire time (spin-waiting is counterproductive) if remaining > 0 { - precise_sleep(remaining); + precise_sleep_us(remaining); } } } diff --git a/src/main.rs b/src/main.rs index 0f6b9bc..5e69a29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -62,6 +62,10 @@ struct Args { } fn main() -> io::Result<()> { + // Lock memory BEFORE any threads are spawned to prevent page faults in RT context + #[cfg(unix)] + engine::realtime::lock_memory(); + let args = Args::parse(); let settings = Settings::load();