Skip to main content

maolan_engine/workers/
hw_worker.rs

1use crate::{
2    hw::traits::{HwMidiHub, HwWorkerDriver},
3    message::{HwMidiEvent, Message},
4};
5#[cfg(unix)]
6use nix::libc;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::time::Duration;
10use tokio::sync::mpsc::{Receiver, Sender};
11use tracing::error;
12
13pub trait Backend: Send + Sync + 'static {
14    type Driver: HwWorkerDriver + Send + 'static;
15    type MidiHub: HwMidiHub + Send + 'static;
16
17    const LABEL: &'static str;
18    const WORKER_THREAD_NAME: &'static str;
19    const ASSIST_THREAD_NAME: &'static str;
20    const ASSIST_AUTONOMOUS_ENV: &'static str;
21    const ASSIST_AUTONOMOUS_DEFAULT: bool = false;
22    const CYCLE_ON_WORKER_WHEN_ASSIST_AUTONOMOUS: bool = false;
23    const ASSIST_STEP_REQUIRES_REQUEST_CYCLE: bool = false;
24}
25
26#[derive(Debug)]
27pub struct HwWorker<B: Backend> {
28    /// Owned by the worker between cycles; shuttled to the spawn_blocking
29    /// thread for the duration of each audio cycle (`None` only while a
30    /// cycle is in flight, during which the message channel is not polled).
31    driver: Option<B::Driver>,
32    midi_hub: B::MidiHub,
33    rx: Receiver<Message>,
34    tx: Sender<Message>,
35    cycle_frames: u32,
36    pending_midi_out_events: Vec<HwMidiEvent>,
37    pending_midi_out_sorted: bool,
38    midi_stop: Arc<AtomicBool>,
39    /// Mirrors the last `HWSetPlaying`; while stopped, MIDI-out events are
40    /// flushed on receipt (panic All-Sound-Off must not wait for a cycle)
41    /// and MIDI input is drained by a periodic timer instead of per cycle.
42    playing: bool,
43}
44
45/// How often hardware MIDI input is polled when no audio cycles are running
46/// (transport stopped). While playing, input is drained every cycle and this
47/// timer is only a harmless extra non-blocking read.
48const MIDI_INPUT_POLL_INTERVAL: Duration = Duration::from_millis(10);
49
50impl<B: Backend> Drop for HwWorker<B> {
51    fn drop(&mut self) {
52        if let Some(driver) = self.driver.as_mut() {
53            driver.request_stop();
54        }
55        self.midi_stop.store(true, Ordering::Release);
56        self.midi_hub.wake_input_waiter();
57        self.midi_hub.close_all();
58        if let Some(driver) = self.driver.as_mut() {
59            driver.close_fds();
60        }
61    }
62}
63
64#[cfg(unix)]
65const RT_POLICY: i32 = libc::SCHED_FIFO;
66const RT_PRIORITY_WORKER: i32 = 18;
67
68impl<B: Backend> HwWorker<B> {
69    fn configure_rt_thread(name: &str, priority: i32) -> Result<(), String> {
70        #[cfg(unix)]
71        {
72            let thread = unsafe { libc::pthread_self() };
73            #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))]
74            let c_name = std::ffi::CString::new(name).map_err(|e| e.to_string())?;
75            #[cfg(target_os = "linux")]
76            unsafe {
77                let _ = libc::pthread_setname_np(thread, c_name.as_ptr());
78            }
79            #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
80            unsafe {
81                libc::pthread_set_name_np(thread, c_name.as_ptr());
82            }
83
84            let param = unsafe {
85                let mut p = std::mem::zeroed::<libc::sched_param>();
86                p.sched_priority = priority;
87                p
88            };
89            let rc = unsafe { libc::pthread_setschedparam(thread, RT_POLICY, &param) };
90            if rc != 0 {
91                return Err(format!(
92                    "pthread_setschedparam({}, prio {}) failed with errno {}",
93                    name, priority, rc
94                ));
95            }
96
97            let mut actual_policy = 0_i32;
98            let mut actual_param = unsafe { std::mem::zeroed::<libc::sched_param>() };
99            let rc = unsafe {
100                libc::pthread_getschedparam(thread, &mut actual_policy, &mut actual_param)
101            };
102            if rc != 0 {
103                return Err(format!(
104                    "pthread_getschedparam({}) failed with errno {}",
105                    name, rc
106                ));
107            }
108            if actual_policy != RT_POLICY || actual_param.sched_priority != priority {
109                return Err(format!(
110                    "realtime verification failed for {}: policy {}, prio {}",
111                    name, actual_policy, actual_param.sched_priority
112                ));
113            }
114            Ok(())
115        }
116        #[cfg(not(unix))]
117        {
118            let _ = name;
119            let _ = priority;
120            Err("Realtime thread priority is not supported on this platform".to_string())
121        }
122    }
123
124    fn lock_memory_pages() -> Result<(), String> {
125        #[cfg(unix)]
126        {
127            let rc = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) };
128            if rc == 0 {
129                Ok(())
130            } else {
131                Err(format!(
132                    "mlockall(MCL_CURRENT|MCL_FUTURE) failed: {}",
133                    std::io::Error::last_os_error()
134                ))
135            }
136        }
137        #[cfg(not(unix))]
138        {
139            Err("mlockall is not supported on this platform".to_string())
140        }
141    }
142
143    pub fn new(
144        driver: B::Driver,
145        midi_hub: B::MidiHub,
146        rx: Receiver<Message>,
147        tx: Sender<Message>,
148    ) -> Self {
149        let cycle_frames = driver.cycle_samples() as u32;
150        Self {
151            driver: Some(driver),
152            midi_hub,
153            rx,
154            tx,
155            cycle_frames,
156            pending_midi_out_events: vec![],
157            pending_midi_out_sorted: true,
158            midi_stop: Arc::new(AtomicBool::new(false)),
159            playing: false,
160        }
161    }
162
163    fn driver_mut(&mut self) -> &mut B::Driver {
164        self.driver
165            .as_mut()
166            .expect("driver is only absent while a cycle runs on the blocking thread")
167    }
168
169    /// Run one audio cycle on a tokio blocking thread. The blocking pool
170    /// thread does not inherit the async worker thread's realtime priority,
171    /// so configure it for every cycle — the pool may hand each cycle to a
172    /// different thread.
173    fn run_cycle_blocking(mut driver: B::Driver) -> (B::Driver, Result<(), String>) {
174        if let Err(e) = Self::configure_rt_thread(B::WORKER_THREAD_NAME, RT_PRIORITY_WORKER) {
175            static WARNED: std::sync::atomic::AtomicBool =
176                std::sync::atomic::AtomicBool::new(false);
177            if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
178                tracing::warn!(
179                    "{} cycle thread realtime priority not enabled: {}",
180                    B::LABEL,
181                    e
182                );
183            }
184        }
185        let result = driver.run_cycle_for_worker();
186        (driver, result)
187    }
188
189    pub async fn work(mut self) {
190        crate::enable_flush_denormals_to_zero();
191        if let Err(e) = Self::lock_memory_pages() {
192            error!("{} worker memory lock not enabled: {}", B::LABEL, e);
193        }
194        if let Err(e) = Self::configure_rt_thread(B::WORKER_THREAD_NAME, RT_PRIORITY_WORKER) {
195            error!("{} worker realtime priority not enabled: {}", B::LABEL, e);
196        }
197        #[cfg(target_os = "macos")]
198        unsafe {
199            libc::pthread_set_qos_class_self_np(libc::qos_class_t::QOS_CLASS_USER_INTERACTIVE, 0);
200        }
201
202        #[cfg(unix)]
203        {
204            let has_fds = self
205                .driver
206                .as_ref()
207                .is_some_and(|d| d.capture_fd().is_some() && d.playback_fd().is_some());
208            if has_fds {
209                self.work_async().await;
210                return;
211            }
212        }
213
214        self.work_legacy().await;
215    }
216
217    #[cfg(unix)]
218    async fn work_async(&mut self) {
219        let mut cycle_running = false;
220        let (cycle_tx, mut cycle_rx) =
221            tokio::sync::mpsc::channel::<(B::Driver, Result<(), String>)>(1);
222        let mut midi_input_poll = tokio::time::interval(MIDI_INPUT_POLL_INTERVAL);
223        loop {
224            tokio::select! {
225                // While a cycle is in flight the driver lives on the blocking
226                // thread, so the message channel is not polled; queued
227                // messages (including Quit) are handled once the cycle
228                // returns — bounded by one audio period.
229                msg = self.rx.recv(), if !cycle_running => {
230                    let msg = match msg {
231                        Some(m) => m,
232                        None => {
233                            self.driver_mut().request_stop();
234                            self.shutdown_channel_closed();
235                            return;
236                        }
237                    };
238                    match msg {
239                        Message::Request(crate::message::Action::Quit) => {
240                            self.driver_mut().request_stop();
241                            self.shutdown_quit();
242                            return;
243                        }
244                        Message::TracksFinished => {
245                            self.flush_pending_midi_out();
246                            self.drain_midi_input().await;
247                            if !cycle_running {
248                                cycle_running = true;
249                                let tx = cycle_tx.clone();
250                                let driver = self.driver.take().expect(
251                                    "driver is only absent while a cycle is running",
252                                );
253                                tokio::task::spawn_blocking(move || {
254                                    let _ = tx.blocking_send(Self::run_cycle_blocking(driver));
255                                });
256                            }
257                        }
258                        Message::HWMidiOutEvents(mut events) => {
259                            self.pending_midi_out_events.append(&mut events);
260                            self.pending_midi_out_sorted = false;
261                            // Stopped transport means no cycles and no
262                            // TracksFinished to flush on; write immediately so
263                            // e.g. panic All-Sound-Off reaches the device.
264                            if !self.playing {
265                                self.flush_pending_midi_out();
266                            }
267                        }
268                        Message::ClearHWMidiOutEvents => {
269                            self.pending_midi_out_events.clear();
270                            self.pending_midi_out_sorted = true;
271                        }
272                        Message::HWSetPlaying(playing) => {
273                            self.playing = playing;
274                            self.driver_mut().set_playing(playing);
275                        }
276                        Message::HWSetOutputGainBalance { gain, balance } => {
277                            self.driver_mut().set_output_gain_balance(gain, balance);
278                        }
279                        Message::HWOpenMidiInputDevice(device) => {
280                            let result = self.midi_hub.open_input(&device);
281                            let action = crate::message::Action::OpenMidiInputDevice(device);
282                            let _ = self.tx.send(Message::Response(result.map(|_| action))).await;
283                        }
284                        Message::HWOpenMidiOutputDevice(device) => {
285                            let result = self.midi_hub.open_output(&device);
286                            let action = crate::message::Action::OpenMidiOutputDevice(device);
287                            let _ = self.tx.send(Message::Response(result.map(|_| action))).await;
288                        }
289                        Message::HWCloseMidiDevices => {
290                            self.midi_hub.close_all();
291                        }
292                        _ => {}
293                    }
294                }
295                result = cycle_rx.recv(), if cycle_running => {
296                    cycle_running = false;
297                    if let Some((driver, result)) = result {
298                        self.driver = Some(driver);
299                        if let Err(e) = result {
300                            error!("{} cycle error: {}", B::LABEL, e);
301                            let _ = self.tx.send(Message::Response(Err(format!(
302                                "{} cycle error: {}", B::LABEL, e
303                            )))).await;
304                        }
305                    }
306                    if let Err(e) = self.tx.send(Message::HWFinished).await {
307                        error!("{} worker failed to send HWFinished: {}", B::LABEL, e);
308                    }
309                }
310                // Hardware MIDI input must flow even while the transport is
311                // stopped (MIDI learn, monitoring, external controllers);
312                // while playing, input is drained every cycle on
313                // TracksFinished and this is an extra non-blocking read.
314                _ = midi_input_poll.tick(), if !cycle_running => {
315                    self.drain_midi_input().await;
316                }
317            }
318        }
319    }
320
321    async fn work_legacy(&mut self) {
322        let mut midi_input_poll = tokio::time::interval(MIDI_INPUT_POLL_INTERVAL);
323        loop {
324            let msg = tokio::select! {
325                msg = self.rx.recv() => match msg {
326                    Some(msg) => msg,
327                    None => {
328                        self.driver_mut().request_stop();
329                        self.shutdown_midi();
330                        self.driver_mut().close_fds();
331                        return;
332                    }
333                },
334                // Keep hardware MIDI input flowing while the transport is
335                // stopped; see work_async.
336                _ = midi_input_poll.tick() => {
337                    self.drain_midi_input().await;
338                    continue;
339                }
340            };
341            match msg {
342                Message::Request(crate::message::Action::Quit) => {
343                    self.driver_mut().request_stop();
344                    self.flush_pending_midi_out();
345                    self.shutdown_midi();
346                    self.driver_mut().close_fds();
347                    self.driver_mut().request_stop();
348                    return;
349                }
350                Message::TracksFinished => {
351                    self.flush_pending_midi_out();
352                    self.drain_midi_input().await;
353                    // The cycle blocks for a full audio period; run it on a
354                    // blocking thread with per-cycle RT priority instead of
355                    // stalling the async worker task (see work_async).
356                    let driver = self
357                        .driver
358                        .take()
359                        .expect("driver is only absent while a cycle is running");
360                    let cycle =
361                        tokio::task::spawn_blocking(move || Self::run_cycle_blocking(driver));
362                    match cycle.await {
363                        Ok((driver, result)) => {
364                            self.driver = Some(driver);
365                            if let Err(e) = result {
366                                error!("{} assist cycle error: {}", B::LABEL, e);
367                                let _ = self
368                                    .tx
369                                    .send(Message::Response(Err(format!(
370                                        "{} assist cycle error: {}",
371                                        B::LABEL,
372                                        e
373                                    ))))
374                                    .await;
375                            }
376                        }
377                        Err(e) => {
378                            error!("{} cycle task failed: {}", B::LABEL, e);
379                            return;
380                        }
381                    }
382                    if let Err(e) = self.tx.send(Message::HWFinished).await {
383                        error!(
384                            "{} worker failed to send HWFinished to engine: {}",
385                            B::LABEL,
386                            e
387                        );
388                    }
389                }
390                Message::HWMidiOutEvents(mut events) => {
391                    self.pending_midi_out_events.append(&mut events);
392                    self.pending_midi_out_sorted = false;
393                    // Stopped transport means no cycles and no TracksFinished
394                    // to flush on; write immediately (panic All-Sound-Off).
395                    if !self.playing {
396                        self.flush_pending_midi_out();
397                    }
398                }
399                Message::ClearHWMidiOutEvents => {
400                    self.pending_midi_out_events.clear();
401                    self.pending_midi_out_sorted = true;
402                }
403                Message::HWSetPlaying(playing) => {
404                    self.playing = playing;
405                    self.driver_mut().set_playing(playing);
406                }
407                Message::HWSetOutputGainBalance { gain, balance } => {
408                    self.driver_mut().set_output_gain_balance(gain, balance);
409                }
410                Message::HWOpenMidiInputDevice(device) => {
411                    let result = self.midi_hub.open_input(&device);
412                    let action = crate::message::Action::OpenMidiInputDevice(device);
413                    let _ = self
414                        .tx
415                        .send(Message::Response(result.map(|_| action)))
416                        .await;
417                }
418                Message::HWOpenMidiOutputDevice(device) => {
419                    let result = self.midi_hub.open_output(&device);
420                    let action = crate::message::Action::OpenMidiOutputDevice(device);
421                    let _ = self
422                        .tx
423                        .send(Message::Response(result.map(|_| action)))
424                        .await;
425                }
426                Message::HWCloseMidiDevices => {
427                    self.midi_hub.close_all();
428                }
429                _ => {}
430            }
431        }
432    }
433
434    fn flush_pending_midi_out(&mut self) {
435        if self.pending_midi_out_events.is_empty() {
436            return;
437        }
438        if !self.pending_midi_out_sorted {
439            self.pending_midi_out_events.sort_by(|a, b| {
440                a.event
441                    .frame
442                    .cmp(&b.event.frame)
443                    .then_with(|| a.device.cmp(&b.device))
444            });
445            self.pending_midi_out_sorted = true;
446        }
447        self.midi_hub.write_events(&self.pending_midi_out_events);
448        self.pending_midi_out_events.clear();
449    }
450
451    async fn drain_midi_input(&mut self) {
452        let mut midi_in_events = Vec::with_capacity(64);
453        self.midi_hub.read_events_into(&mut midi_in_events);
454        if midi_in_events.is_empty() {
455            return;
456        }
457        spread_hw_event_frames(&mut midi_in_events, self.cycle_frames);
458        let _ = self.tx.send(Message::HWMidiEvents(midi_in_events)).await;
459    }
460
461    fn shutdown_midi(&mut self) {
462        self.midi_stop.store(true, Ordering::Release);
463        self.midi_hub.wake_input_waiter();
464        self.midi_hub.close_all();
465    }
466
467    #[cfg(unix)]
468    fn shutdown_quit(&mut self) {
469        self.driver_mut().request_stop();
470        self.flush_pending_midi_out();
471        self.shutdown_midi();
472        self.driver_mut().close_fds();
473        self.driver_mut().request_stop();
474    }
475
476    #[cfg(unix)]
477    fn shutdown_channel_closed(&mut self) {
478        self.driver_mut().request_stop();
479        self.shutdown_midi();
480        self.driver_mut().close_fds();
481        self.driver_mut().request_stop();
482    }
483}
484
485fn spread_hw_event_frames(events: &mut [HwMidiEvent], frames: u32) {
486    if events.len() <= 1 || frames <= 1 {
487        return;
488    }
489    let n = events.len() as u32;
490    for (idx, event) in events.iter_mut().enumerate() {
491        let pos = idx as u32;
492        event.event.frame = ((pos as u64 * (frames - 1) as u64) / n as u64) as u32;
493    }
494}