Feat: tweak and fix from last night workshop
Some checks failed
Deploy Website / deploy (push) Failing after 4m46s

This commit is contained in:
2026-02-04 09:37:29 +01:00
parent 5579708f69
commit 2097997372
9 changed files with 151 additions and 65 deletions

View File

@@ -1,45 +1,52 @@
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(target_os = "linux")]
mod memory {
use std::sync::atomic::{AtomicBool, Ordering};
static MLOCKALL_CALLED: AtomicBool = AtomicBool::new(false);
static MLOCKALL_SUCCESS: AtomicBool = AtomicBool::new(false);
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);
/// Locks all current and future memory pages to prevent page faults during RT execution.
/// Must be called BEFORE spawning any threads for maximum effectiveness.
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 {
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
}
}
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
#[allow(dead_code)]
pub fn is_memory_locked() -> bool {
MLOCKALL_SUCCESS.load(Ordering::Relaxed)
}
}
#[cfg(not(unix))]
#[cfg(target_os = "linux")]
pub use memory::{is_memory_locked, lock_memory};
#[cfg(not(target_os = "linux"))]
pub fn lock_memory() -> bool {
// Windows: VirtualLock exists but isn't typically needed for audio
true
}
/// Check if memory locking is active.
#[cfg(not(target_os = "linux"))]
#[allow(dead_code)]
pub fn is_memory_locked() -> bool {
MLOCKALL_SUCCESS.load(Ordering::Relaxed)
false
}
/// Attempts to set realtime scheduling priority for the current thread.

View File

@@ -531,6 +531,7 @@ impl KeyCache {
pub(crate) struct SequencerState {
audio_state: AudioState,
pattern_cache: PatternCache,
pending_updates: HashMap<(usize, usize), PatternSnapshot>,
runs_counter: RunsCounter,
step_traces: Arc<StepTracesMap>,
event_count: usize,
@@ -559,6 +560,7 @@ impl SequencerState {
Self {
audio_state: AudioState::new(),
pattern_cache: PatternCache::new(),
pending_updates: HashMap::new(),
runs_counter: RunsCounter::new(),
step_traces: Arc::new(HashMap::new()),
event_count: 0,
@@ -596,7 +598,14 @@ impl SequencerState {
pattern,
data,
} => {
self.pattern_cache.set(bank, pattern, data);
let id = PatternId { bank, pattern };
let is_active = self.audio_state.active_patterns.contains_key(&id);
let has_cache = self.pattern_cache.get(bank, pattern).is_some();
if is_active && has_cache {
self.pending_updates.insert((bank, pattern), data);
} else {
self.pattern_cache.set(bank, pattern, data);
}
}
SeqCommand::PatternStart {
bank,
@@ -652,6 +661,10 @@ impl SequencerState {
}
}
SeqCommand::StopAll => {
// Flush pending updates so cache stays current for future launches
for ((bank, pattern), snapshot) in self.pending_updates.drain() {
self.pattern_cache.set(bank, pattern, snapshot);
}
self.audio_state.active_patterns.clear();
self.audio_state.pending_starts.clear();
self.audio_state.pending_stops.clear();
@@ -715,6 +728,10 @@ impl SequencerState {
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
bank != pending.id.bank || pattern != pending.id.pattern
});
let key = (pending.id.bank, pending.id.pattern);
if let Some(snapshot) = self.pending_updates.remove(&key) {
self.pattern_cache.set(key.0, key.1, snapshot);
}
}
self.audio_state.pending_starts.clear();
self.audio_state.prev_beat = -1.0;
@@ -773,6 +790,11 @@ impl SequencerState {
Arc::make_mut(&mut self.step_traces).retain(|&(bank, pattern, _), _| {
bank != pending.id.bank || pattern != pending.id.pattern
});
// Flush pending update so cache stays current for future launches
let key = (pending.id.bank, pending.id.pattern);
if let Some(snapshot) = self.pending_updates.remove(&key) {
self.pattern_cache.set(key.0, key.1, snapshot);
}
self.buf_stopped.push(pending.id);
}
}
@@ -921,6 +943,14 @@ impl SequencerState {
}
}
// Apply deferred updates for patterns that just completed an iteration
for completed_id in &self.buf_completed_iterations {
let key = (completed_id.bank, completed_id.pattern);
if let Some(snapshot) = self.pending_updates.remove(&key) {
self.pattern_cache.set(key.0, key.1, snapshot);
}
}
result
}
@@ -1872,8 +1902,9 @@ mod tests {
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, fires 1 step. step_index=0%2=0 fires, advances to 1
// Update pattern to length 2 while running — deferred until iteration boundary
// beat=1.25: update is deferred (pattern active), still length 4
// step_index=0 fires, advances to 1
state.tick(tick_with(
vec![SeqCommand::PatternUpdate {
bank: 0,
@@ -1883,10 +1914,21 @@ mod tests {
1.25,
));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
assert_eq!(ap.step_index, 1); // still length 4
// Advance through remaining steps of original length-4 pattern
state.tick(tick_at(1.5, true)); // step 1→2
state.tick(tick_at(1.75, true)); // step 2→3
state.tick(tick_at(2.0, true)); // step 3→wraps to 0, iteration completes, update applies
// Now length=2 is applied. Next tick uses new length.
// beat=2.25: step 0 fires, advances to 1
state.tick(tick_at(2.25, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
assert_eq!(ap.step_index, 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));
// beat=2.5: step 1 fires, wraps to 0 (length 2)
state.tick(tick_at(2.5, true));
let ap = state.audio_state.active_patterns.get(&pid(0, 0)).unwrap();
assert_eq!(ap.step_index, 0);
}