WIP: optimizations for linux

This commit is contained in:
2026-02-03 00:16:31 +01:00
parent 4235862d86
commit c283887ada
11 changed files with 64 additions and 54 deletions

View File

@@ -68,6 +68,7 @@ thread-priority = "1"
ringbuf = "0.4"
arc-swap = "1"
midir = "0.10"
parking_lot = "0.12"
# Desktop-only dependencies (behind feature flag)
egui = { version = "0.33", optional = true }

View File

@@ -13,3 +13,4 @@ desktop = []
[dependencies]
rand = "0.8"
parking_lot = "0.12"

View File

@@ -106,7 +106,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
match &tokens[i] {
Token::Int(n, span) => {
let key = n.to_string();
if let Some(body) = dict.lock().unwrap().get(&key).cloned() {
if let Some(body) = dict.lock().get(&key).cloned() {
ops.extend(body);
} else {
ops.push(Op::PushInt(*n, Some(*span)));
@@ -114,7 +114,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
}
Token::Float(f, span) => {
let key = f.to_string();
if let Some(body) = dict.lock().unwrap().get(&key).cloned() {
if let Some(body) = dict.lock().get(&key).cloned() {
ops.extend(body);
} else {
ops.push(Op::PushFloat(*f, Some(*span)));
@@ -137,7 +137,7 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
} else if word == ":" {
let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?;
i += consumed;
dict.lock().unwrap().insert(name, body);
dict.lock().insert(name, body);
} else if word == ";" {
return Err("unexpected ;".into());
} else if word == "if" {

View File

@@ -1,6 +1,7 @@
use parking_lot::Mutex;
use rand::rngs::StdRng;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use super::ops::Op;

View File

@@ -1,3 +1,4 @@
use parking_lot::Mutex;
use rand::rngs::StdRng;
use rand::{Rng as RngTrait, SeedableRng};
use std::borrow::Cow;
@@ -19,7 +20,7 @@ pub struct Forth {
impl Forth {
pub fn new(vars: Variables, dict: Dictionary, rng: Rng) -> Self {
Self {
stack: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
stack: Arc::new(Mutex::new(Vec::new())),
vars,
dict,
rng,
@@ -28,12 +29,12 @@ impl Forth {
#[allow(dead_code)]
pub fn stack(&self) -> Vec<Value> {
self.stack.lock().unwrap().clone()
self.stack.lock().clone()
}
#[allow(dead_code)]
pub fn clear_stack(&self) {
self.stack.lock().unwrap().clear();
self.stack.lock().clear();
}
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<Vec<String>, String> {
@@ -69,7 +70,7 @@ impl Forth {
ctx: &StepContext,
trace: Option<&mut ExecutionTrace>,
) -> Result<Vec<String>, String> {
let mut stack = self.stack.lock().unwrap();
let mut stack = self.stack.lock();
let mut outputs: Vec<String> = Vec::with_capacity(8);
let mut cmd = CmdRegister::new();
@@ -474,7 +475,7 @@ impl Forth {
Op::Get => {
let name = stack.pop().ok_or("stack underflow")?;
let name = name.as_str()?;
let vars = self.vars.lock().unwrap();
let vars = self.vars.lock();
let val = vars.get(name).cloned().unwrap_or(Value::Int(0, None));
stack.push(val);
}
@@ -482,7 +483,7 @@ impl Forth {
let name = stack.pop().ok_or("stack underflow")?;
let name = name.as_str()?.to_string();
let val = stack.pop().ok_or("stack underflow")?;
self.vars.lock().unwrap().insert(name, val);
self.vars.lock().insert(name, val);
}
Op::GetContext(name) => {
@@ -520,7 +521,7 @@ impl Forth {
} else {
(*b_i, *a_i)
};
let val = self.rng.lock().unwrap().gen_range(lo..=hi);
let val = self.rng.lock().gen_range(lo..=hi);
stack.push(Value::Int(val, None));
}
_ => {
@@ -530,7 +531,7 @@ impl Forth {
let val = if (hi - lo).abs() < f64::EPSILON {
lo
} else {
self.rng.lock().unwrap().gen_range(lo..hi)
self.rng.lock().gen_range(lo..hi)
};
stack.push(Value::Float(val, None));
}
@@ -543,7 +544,7 @@ impl Forth {
return Err("exprand requires positive values".into());
}
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
let u: f64 = self.rng.lock().unwrap().gen();
let u: f64 = self.rng.lock().gen();
let val = lo * (hi / lo).powf(u);
stack.push(Value::Float(val, None));
}
@@ -554,13 +555,13 @@ impl Forth {
return Err("logrand requires positive values".into());
}
let (lo, hi) = if lo <= hi { (lo, hi) } else { (hi, lo) };
let u: f64 = self.rng.lock().unwrap().gen();
let u: f64 = self.rng.lock().gen();
let val = hi * (lo / hi).powf(u);
stack.push(Value::Float(val, None));
}
Op::Seed => {
let s = stack.pop().ok_or("stack underflow")?.as_int()?;
*self.rng.lock().unwrap() = StdRng::seed_from_u64(s as u64);
*self.rng.lock() = StdRng::seed_from_u64(s as u64);
}
Op::Cycle | Op::PCycle => {
@@ -580,14 +581,14 @@ impl Forth {
if count == 0 {
return Err("choose count must be > 0".into());
}
let idx = self.rng.lock().unwrap().gen_range(0..count);
let idx = self.rng.lock().gen_range(0..count);
drain_select_run(count, idx, stack, outputs, cmd)?;
}
Op::ChanceExec | Op::ProbExec => {
let threshold = stack.pop().ok_or("stack underflow")?.as_float()?;
let quot = stack.pop().ok_or("stack underflow")?;
let val: f64 = self.rng.lock().unwrap().gen();
let val: f64 = self.rng.lock().gen();
let limit = match &ops[pc] {
Op::ChanceExec => threshold,
_ => threshold / 100.0,
@@ -598,7 +599,7 @@ impl Forth {
}
Op::Coin => {
let val: f64 = self.rng.lock().unwrap().gen();
let val: f64 = self.rng.lock().gen();
stack.push(Value::Int(if val < 0.5 { 1 } else { 0 }, None));
}
@@ -711,7 +712,6 @@ impl Forth {
let clamped = tempo.clamp(20.0, 300.0);
self.vars
.lock()
.unwrap()
.insert("__tempo__".to_string(), Value::Float(clamped, None));
}
@@ -720,7 +720,6 @@ impl Forth {
let clamped = speed.clamp(0.125, 8.0);
self.vars
.lock()
.unwrap()
.insert(ctx.speed_key.to_string(), Value::Float(clamped, None));
}
@@ -736,7 +735,7 @@ impl Forth {
use std::fmt::Write;
let mut val = String::with_capacity(8);
let _ = write!(&mut val, "{bank}:{pattern}");
self.vars.lock().unwrap().insert(ctx.chain_key.to_string(), Value::Str(Arc::from(val), None));
self.vars.lock().insert(ctx.chain_key.to_string(), Value::Str(Arc::from(val), None));
}
}
@@ -852,7 +851,6 @@ impl Forth {
for i in 0..count {
self.vars
.lock()
.unwrap()
.insert("i".to_string(), Value::Int(i, None));
run_quotation(quot.clone(), stack, outputs, cmd)?;
}
@@ -954,7 +952,7 @@ impl Forth {
}
Op::Forget => {
let name = stack.pop().ok_or("stack underflow")?.as_str()?.to_string();
self.dict.lock().unwrap().remove(&name);
self.dict.lock().remove(&name);
}
}
pc += 1;

View File

@@ -3067,7 +3067,7 @@ pub(super) fn compile_word(
}
// User-defined words from dictionary
if let Some(body) = dict.lock().unwrap().get(name) {
if let Some(body) = dict.lock().get(name) {
ops.extend(body.iter().cloned());
return true;
}

View File

@@ -1,8 +1,9 @@
use parking_lot::Mutex;
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use crossbeam_channel::Sender;
@@ -605,8 +606,8 @@ impl App {
link.set_tempo(tempo);
self.playback.clear_queues();
self.variables.lock().unwrap().clear();
self.dict.lock().unwrap().clear();
self.variables.lock().clear();
self.dict.lock().clear();
for (bank, pattern) in playing {
self.playback.queued_changes.push(StagedChange {

View File

@@ -494,7 +494,6 @@ pub(crate) struct TickOutput {
}
struct StepResult {
completed_iterations: Vec<PatternId>,
any_step_fired: bool,
}
@@ -557,6 +556,7 @@ pub(crate) struct SequencerState {
buf_audio_commands: Vec<TimestampedCommand>,
buf_activated: Vec<PatternId>,
buf_stopped: Vec<PatternId>,
buf_completed_iterations: Vec<PatternId>,
cc_access: Option<Arc<dyn CcAccess>>,
active_notes: HashMap<(u8, u8, u8), ActiveNote>,
muted: std::collections::HashSet<(usize, usize)>,
@@ -580,11 +580,12 @@ impl SequencerState {
dropped_events: 0,
script_engine,
variables,
speed_overrides: HashMap::new(),
speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
key_cache: KeyCache::new(),
buf_audio_commands: Vec::with_capacity(32),
buf_activated: Vec::with_capacity(16),
buf_stopped: Vec::with_capacity(16),
buf_completed_iterations: Vec::with_capacity(16),
cc_access,
active_notes: HashMap::new(),
muted: std::collections::HashSet::new(),
@@ -710,7 +711,7 @@ impl SequencerState {
input.mouse_down,
);
let vars = self.read_variables(&steps.completed_iterations, steps.any_step_fired);
let vars = self.read_variables(&self.buf_completed_iterations, steps.any_step_fired);
self.apply_chain_transitions(vars.chain_transitions);
self.audio_state.prev_beat = beat;
@@ -813,14 +814,14 @@ impl SequencerState {
#[cfg(feature = "desktop")] mouse_down: f64,
) -> StepResult {
self.buf_audio_commands.clear();
self.buf_completed_iterations.clear();
let mut result = StepResult {
completed_iterations: Vec::new(),
any_step_fired: false,
};
self.speed_overrides.clear();
{
let vars = self.variables.lock().unwrap();
let vars = self.variables.lock();
for id in self.audio_state.active_patterns.keys() {
let key = self.key_cache.speed_key(id.bank, id.pattern);
if let Some(v) = vars.get(key).and_then(|v| v.as_float().ok()) {
@@ -921,7 +922,7 @@ impl SequencerState {
let next_step = active.step_index + 1;
if next_step >= pattern.length {
active.iter += 1;
result.completed_iterations.push(PatternId {
self.buf_completed_iterations.push(PatternId {
bank: active.bank,
pattern: active.pattern,
});
@@ -947,7 +948,7 @@ impl SequencerState {
};
}
let mut vars = self.variables.lock().unwrap();
let mut vars = self.variables.lock();
let new_tempo = vars.remove("__tempo__").and_then(|v| v.as_float().ok());
let mut chain_transitions = Vec::new();
@@ -1073,7 +1074,7 @@ fn sequencer_loop(
let mut seq_state = SequencerState::new(variables, dict, rng, cc_access);
loop {
let mut commands = Vec::new();
let mut commands = Vec::with_capacity(8);
while let Ok(cmd) = cmd_rx.try_recv() {
if matches!(cmd, SeqCommand::Shutdown) {
return;
@@ -1205,7 +1206,16 @@ fn sequencer_loop(
shared_state.store(Arc::new(output.shared_state));
thread::sleep(Duration::from_micros(200));
// Adaptive sleep: calculate time until next substep boundary
// At max speed (8x), substeps occur every beat/32
// Sleep for most of that time, leaving 500μs margin for processing
let beats_per_sec = tempo / 60.0;
let max_speed = 8.0; // Maximum speed multiplier from speed.clamp()
let secs_per_substep = 1.0 / (beats_per_sec * 4.0 * max_speed);
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));
}
}
@@ -1317,7 +1327,7 @@ fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>)> {
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
use parking_lot::Mutex;
fn make_state() -> SequencerState {
let variables: Variables = Arc::new(Mutex::new(HashMap::new()));
@@ -1497,7 +1507,7 @@ mod tests {
// Set chain variable
{
let mut vars = state.variables.lock().unwrap();
let mut vars = state.variables.lock();
vars.insert(
"__chain_0_0__".to_string(),
Value::Str(std::sync::Arc::from("0:1"), None),
@@ -1736,7 +1746,7 @@ mod tests {
// Set chain: 0:0 -> 0:1
{
let mut vars = state.variables.lock().unwrap();
let mut vars = state.variables.lock();
vars.insert(
"__chain_0_0__".to_string(),
Value::Str(std::sync::Arc::from("0:1"), None),
@@ -1979,7 +1989,7 @@ mod tests {
// Script fires at beat 1.0 (step 0). Set __tempo__ as if the script did.
{
let mut vars = state.variables.lock().unwrap();
let mut vars = state.variables.lock();
vars.insert("__tempo__".to_string(), Value::Float(140.0, None));
}

View File

@@ -1,4 +1,5 @@
use std::sync::{Arc, Mutex};
use parking_lot::Mutex;
use std::sync::Arc;
use midir::{MidiInput, MidiOutput};
@@ -28,9 +29,8 @@ impl CcMemory {
/// Set a CC value (for testing)
#[allow(dead_code)]
pub fn set_cc(&self, device: usize, channel: usize, cc: usize, value: u8) {
if let Ok(mut mem) = self.0.lock() {
mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)] = value;
}
let mut mem = self.0.lock();
mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)] = value;
}
}
@@ -42,11 +42,8 @@ impl Default for CcMemory {
impl CcAccess for CcMemory {
fn get_cc(&self, device: usize, channel: usize, cc: usize) -> u8 {
self.0
.lock()
.ok()
.map(|mem| mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)])
.unwrap_or(0)
let mem = self.0.lock();
mem[device.min(MAX_MIDI_DEVICES - 1)][channel.min(15)][cc.min(127)]
}
}
@@ -154,9 +151,8 @@ impl MidiState {
let data2 = message[2];
if (status & 0xF0) == 0xB0 && data1 < 128 {
let channel = (status & 0x0F) as usize;
if let Ok(mut mem) = cc_mem.lock() {
mem[*slot][channel][data1] = data2;
}
let mut mem = cc_mem.lock();
mem[*slot][channel][data1] = data2;
}
}
},

View File

@@ -1,7 +1,8 @@
use parking_lot::Mutex;
use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use std::time::Instant;
use rand::rngs::StdRng;

View File

@@ -1,8 +1,9 @@
use cagire::forth::{Dictionary, Forth, Rng, StepContext, Value, Variables};
use parking_lot::Mutex;
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
pub fn default_ctx() -> StepContext<'static> {
StepContext {