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(unix)]
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(target_os = "macos")]
80            unsafe {
81                // macOS names the current thread and takes no thread handle.
82                let _ = libc::pthread_setname_np(c_name.as_ptr());
83            }
84            #[cfg(any(target_os = "freebsd", target_os = "openbsd"))]
85            unsafe {
86                libc::pthread_set_name_np(thread, c_name.as_ptr());
87            }
88
89            let param = unsafe {
90                let mut p = std::mem::zeroed::<libc::sched_param>();
91                p.sched_priority = priority;
92                p
93            };
94            let rc = unsafe { libc::pthread_setschedparam(thread, RT_POLICY, &param) };
95            if rc != 0 {
96                return Err(format!(
97                    "pthread_setschedparam({}, prio {}) failed with errno {}",
98                    name, priority, rc
99                ));
100            }
101
102            let mut actual_policy = 0_i32;
103            let mut actual_param = unsafe { std::mem::zeroed::<libc::sched_param>() };
104            let rc = unsafe {
105                libc::pthread_getschedparam(thread, &mut actual_policy, &mut actual_param)
106            };
107            if rc != 0 {
108                return Err(format!(
109                    "pthread_getschedparam({}) failed with errno {}",
110                    name, rc
111                ));
112            }
113            if actual_policy != RT_POLICY || actual_param.sched_priority != priority {
114                return Err(format!(
115                    "realtime verification failed for {}: policy {}, prio {}",
116                    name, actual_policy, actual_param.sched_priority
117                ));
118            }
119            Ok(())
120        }
121        #[cfg(target_os = "windows")]
122        {
123            use std::{cell::Cell, ffi::OsStr, os::windows::ffi::OsStrExt};
124
125            #[link(name = "avrt")]
126            unsafe extern "system" {
127                fn AvSetMmThreadCharacteristicsW(
128                    task_name: *const u16,
129                    task_index: *mut u32,
130                ) -> isize;
131            }
132
133            let _ = priority;
134            thread_local! {
135                static MMCSS_TASK_HANDLE: Cell<isize> = const { Cell::new(0) };
136            }
137
138            MMCSS_TASK_HANDLE.with(|handle| {
139                if handle.get() != 0 {
140                    return Ok(());
141                }
142
143                let task_name: Vec<u16> = OsStr::new("Pro Audio")
144                    .encode_wide()
145                    .chain(Some(0))
146                    .collect();
147                let mut task_index = 0_u32;
148                let mmcss_handle =
149                    unsafe { AvSetMmThreadCharacteristicsW(task_name.as_ptr(), &mut task_index) };
150                if mmcss_handle == 0 {
151                    Err(format!(
152                        "AvSetMmThreadCharacteristicsW({name}, Pro Audio) failed: {}",
153                        std::io::Error::last_os_error()
154                    ))
155                } else {
156                    handle.set(mmcss_handle);
157                    Ok(())
158                }
159            })
160        }
161        #[cfg(all(not(unix), not(target_os = "windows")))]
162        {
163            let _ = name;
164            let _ = priority;
165            Err("Realtime thread priority is not supported on this platform".to_string())
166        }
167    }
168
169    #[cfg(unix)]
170    fn lock_memory_pages() -> Result<(), String> {
171        let rc = unsafe { libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE) };
172        if rc == 0 {
173            Ok(())
174        } else {
175            Err(format!(
176                "mlockall(MCL_CURRENT|MCL_FUTURE) failed: {}",
177                std::io::Error::last_os_error()
178            ))
179        }
180    }
181
182    pub fn new(
183        driver: B::Driver,
184        midi_hub: B::MidiHub,
185        rx: Receiver<Message>,
186        tx: Sender<Message>,
187    ) -> Self {
188        let cycle_frames = driver.cycle_samples() as u32;
189        Self {
190            driver: Some(driver),
191            midi_hub,
192            rx,
193            tx,
194            cycle_frames,
195            pending_midi_out_events: vec![],
196            pending_midi_out_sorted: true,
197            midi_stop: Arc::new(AtomicBool::new(false)),
198            playing: false,
199        }
200    }
201
202    fn driver_mut(&mut self) -> &mut B::Driver {
203        self.driver
204            .as_mut()
205            .expect("driver is only absent while a cycle runs on the blocking thread")
206    }
207
208    /// Run one audio cycle on a tokio blocking thread. The blocking pool
209    /// thread does not inherit the async worker thread's realtime priority,
210    /// so configure it for every cycle — the pool may hand each cycle to a
211    /// different thread.
212    fn run_cycle_blocking(mut driver: B::Driver) -> (B::Driver, Result<(), String>) {
213        let rt_start = std::time::Instant::now();
214        if let Err(e) = Self::configure_rt_thread(B::WORKER_THREAD_NAME, RT_PRIORITY_WORKER) {
215            static WARNED: std::sync::atomic::AtomicBool =
216                std::sync::atomic::AtomicBool::new(false);
217            if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
218                tracing::warn!(
219                    "{} cycle thread realtime priority not enabled: {}",
220                    B::LABEL,
221                    e
222                );
223            }
224        }
225        let _rt_us = rt_start.elapsed().as_micros() as u64;
226        let _cycle_start = std::time::Instant::now();
227        let result = driver.run_cycle_for_worker();
228        let _cycle_us = _cycle_start.elapsed().as_micros() as u64;
229        (driver, result)
230    }
231
232    pub async fn work(mut self) {
233        crate::enable_flush_denormals_to_zero();
234        #[cfg(unix)]
235        {
236            if let Err(e) = Self::lock_memory_pages() {
237                error!("{} worker memory lock not enabled: {}", B::LABEL, e);
238            }
239        }
240        if let Err(e) = Self::configure_rt_thread(B::WORKER_THREAD_NAME, RT_PRIORITY_WORKER) {
241            error!("{} worker realtime priority not enabled: {}", B::LABEL, e);
242        }
243        #[cfg(unix)]
244        {
245            let has_fds = self
246                .driver
247                .as_ref()
248                .is_some_and(|d| d.capture_fd().is_some() && d.playback_fd().is_some());
249            if has_fds {
250                self.work_async().await;
251                return;
252            }
253        }
254
255        self.work_legacy().await;
256    }
257
258    #[cfg(unix)]
259    async fn work_async(&mut self) {
260        let mut cycle_running = false;
261        let (cycle_tx, mut cycle_rx) =
262            tokio::sync::mpsc::channel::<(B::Driver, Result<(), String>)>(1);
263        let mut midi_input_poll = tokio::time::interval(MIDI_INPUT_POLL_INTERVAL);
264        loop {
265            tokio::select! {
266                // While a cycle is in flight the driver lives on the blocking
267                // thread, so the message channel is not polled; queued
268                // messages (including Quit) are handled once the cycle
269                // returns — bounded by one audio period.
270                msg = self.rx.recv(), if !cycle_running => {
271                    let msg = match msg {
272                        Some(m) => m,
273                        None => {
274                            self.driver_mut().request_stop();
275                            self.shutdown_channel_closed();
276                            return;
277                        }
278                    };
279                    match msg {
280                        Message::Request(crate::message::Action::Quit) => {
281                            self.driver_mut().request_stop();
282                            self.shutdown_quit();
283                            return;
284                        }
285                        Message::TracksFinished => {
286                            self.flush_pending_midi_out();
287                            self.drain_midi_input().await;
288                            if !cycle_running {
289                                cycle_running = true;
290                                let tx = cycle_tx.clone();
291                                let driver = self.driver.take().expect(
292                                    "driver is only absent while a cycle is running",
293                                );
294                                tokio::task::spawn_blocking(move || {
295                                    let _ = tx.blocking_send(Self::run_cycle_blocking(driver));
296                                });
297                            }
298                        }
299                        Message::HWMidiOutEvents(mut events) => {
300                            self.pending_midi_out_events.append(&mut events);
301                            self.pending_midi_out_sorted = false;
302                            // Stopped transport means no cycles and no
303                            // TracksFinished to flush on; write immediately so
304                            // e.g. panic All-Sound-Off reaches the device.
305                            if !self.playing {
306                                self.flush_pending_midi_out();
307                            }
308                        }
309                        Message::ClearHWMidiOutEvents => {
310                            self.pending_midi_out_events.clear();
311                            self.pending_midi_out_sorted = true;
312                        }
313                        Message::HWSetPlaying(playing) => {
314                            self.playing = playing;
315                            self.driver_mut().set_playing(playing);
316                        }
317                        Message::HWSetOutputGainBalance { gain, balance } => {
318                            self.driver_mut().set_output_gain_balance(gain, balance);
319                        }
320                        Message::HWOpenMidiInputDevice(device) => {
321                            let result = self.midi_hub.open_input(&device);
322                            let action = crate::message::Action::OpenMidiInputDevice(device);
323                            let _ = self.tx.send(Message::Response(result.map(|_| action))).await;
324                        }
325                        Message::HWOpenMidiOutputDevice(device) => {
326                            let result = self.midi_hub.open_output(&device);
327                            let action = crate::message::Action::OpenMidiOutputDevice(device);
328                            let _ = self.tx.send(Message::Response(result.map(|_| action))).await;
329                        }
330                        Message::HWCloseMidiDevices => {
331                            self.midi_hub.close_all();
332                        }
333                        _ => {}
334                    }
335                }
336                result = cycle_rx.recv(), if cycle_running => {
337                    cycle_running = false;
338                    if let Some((driver, result)) = result {
339                        self.driver = Some(driver);
340                        if let Err(e) = result {
341                            error!("{} cycle error: {}", B::LABEL, e);
342                            let _ = self.tx.send(Message::Response(Err(format!(
343                                "{} cycle error: {}", B::LABEL, e
344                            )))).await;
345                        }
346                    }
347                    if let Err(e) = self.tx.send(Message::HWFinished).await {
348                        error!("{} worker failed to send HWFinished: {}", B::LABEL, e);
349                    }
350                }
351                // Hardware MIDI input must flow even while the transport is
352                // stopped (MIDI learn, monitoring, external controllers);
353                // while playing, input is drained every cycle on
354                // TracksFinished and this is an extra non-blocking read.
355                _ = midi_input_poll.tick(), if !cycle_running => {
356                    self.drain_midi_input().await;
357                }
358            }
359        }
360    }
361
362    async fn work_legacy(&mut self) {
363        let mut midi_input_poll = tokio::time::interval(MIDI_INPUT_POLL_INTERVAL);
364        loop {
365            let msg = tokio::select! {
366                msg = self.rx.recv() => match msg {
367                    Some(msg) => msg,
368                    None => {
369                        self.driver_mut().request_stop();
370                        self.shutdown_midi();
371                        self.driver_mut().close_fds();
372                        return;
373                    }
374                },
375                // Keep hardware MIDI input flowing while the transport is
376                // stopped; see work_async.
377                _ = midi_input_poll.tick() => {
378                    self.drain_midi_input().await;
379                    continue;
380                }
381            };
382            match msg {
383                Message::Request(crate::message::Action::Quit) => {
384                    self.driver_mut().request_stop();
385                    self.flush_pending_midi_out();
386                    self.shutdown_midi();
387                    self.driver_mut().close_fds();
388                    self.driver_mut().request_stop();
389                    return;
390                }
391                Message::TracksFinished => {
392                    self.flush_pending_midi_out();
393                    self.drain_midi_input().await;
394                    // The cycle blocks for a full audio period; run it on a
395                    // blocking thread with per-cycle RT priority instead of
396                    // stalling the async worker task (see work_async).
397                    let driver = self
398                        .driver
399                        .take()
400                        .expect("driver is only absent while a cycle is running");
401                    let cycle =
402                        tokio::task::spawn_blocking(move || Self::run_cycle_blocking(driver));
403                    match cycle.await {
404                        Ok((driver, result)) => {
405                            self.driver = Some(driver);
406                            if let Err(e) = result {
407                                error!("{} assist cycle error: {}", B::LABEL, e);
408                                let _ = self
409                                    .tx
410                                    .send(Message::Response(Err(format!(
411                                        "{} assist cycle error: {}",
412                                        B::LABEL,
413                                        e
414                                    ))))
415                                    .await;
416                            }
417                        }
418                        Err(e) => {
419                            error!("{} cycle task failed: {}", B::LABEL, e);
420                            return;
421                        }
422                    }
423                    if let Err(e) = self.tx.send(Message::HWFinished).await {
424                        error!(
425                            "{} worker failed to send HWFinished to engine: {}",
426                            B::LABEL,
427                            e
428                        );
429                    }
430                }
431                Message::HWMidiOutEvents(mut events) => {
432                    self.pending_midi_out_events.append(&mut events);
433                    self.pending_midi_out_sorted = false;
434                    // Stopped transport means no cycles and no TracksFinished
435                    // to flush on; write immediately (panic All-Sound-Off).
436                    if !self.playing {
437                        self.flush_pending_midi_out();
438                    }
439                }
440                Message::ClearHWMidiOutEvents => {
441                    self.pending_midi_out_events.clear();
442                    self.pending_midi_out_sorted = true;
443                }
444                Message::HWSetPlaying(playing) => {
445                    self.playing = playing;
446                    self.driver_mut().set_playing(playing);
447                }
448                Message::HWSetOutputGainBalance { gain, balance } => {
449                    self.driver_mut().set_output_gain_balance(gain, balance);
450                }
451                Message::HWOpenMidiInputDevice(device) => {
452                    let result = self.midi_hub.open_input(&device);
453                    let action = crate::message::Action::OpenMidiInputDevice(device);
454                    let _ = self
455                        .tx
456                        .send(Message::Response(result.map(|_| action)))
457                        .await;
458                }
459                Message::HWOpenMidiOutputDevice(device) => {
460                    let result = self.midi_hub.open_output(&device);
461                    let action = crate::message::Action::OpenMidiOutputDevice(device);
462                    let _ = self
463                        .tx
464                        .send(Message::Response(result.map(|_| action)))
465                        .await;
466                }
467                Message::HWCloseMidiDevices => {
468                    self.midi_hub.close_all();
469                }
470                _ => {}
471            }
472        }
473    }
474
475    fn flush_pending_midi_out(&mut self) {
476        if self.pending_midi_out_events.is_empty() {
477            return;
478        }
479        if !self.pending_midi_out_sorted {
480            self.pending_midi_out_events.sort_by(|a, b| {
481                a.event
482                    .frame
483                    .cmp(&b.event.frame)
484                    .then_with(|| a.device.cmp(&b.device))
485            });
486            self.pending_midi_out_sorted = true;
487        }
488        self.midi_hub.write_events(&self.pending_midi_out_events);
489        self.pending_midi_out_events.clear();
490    }
491
492    async fn drain_midi_input(&mut self) {
493        let mut midi_in_events = Vec::with_capacity(64);
494        self.midi_hub.read_events_into(&mut midi_in_events);
495        if midi_in_events.is_empty() {
496            return;
497        }
498        spread_hw_event_frames(&mut midi_in_events, self.cycle_frames);
499        let _ = self.tx.send(Message::HWMidiEvents(midi_in_events)).await;
500    }
501
502    fn shutdown_midi(&mut self) {
503        self.midi_stop.store(true, Ordering::Release);
504        self.midi_hub.wake_input_waiter();
505        self.midi_hub.close_all();
506    }
507
508    #[cfg(unix)]
509    fn shutdown_quit(&mut self) {
510        self.driver_mut().request_stop();
511        self.flush_pending_midi_out();
512        self.shutdown_midi();
513        self.driver_mut().close_fds();
514        self.driver_mut().request_stop();
515    }
516
517    #[cfg(unix)]
518    fn shutdown_channel_closed(&mut self) {
519        self.driver_mut().request_stop();
520        self.shutdown_midi();
521        self.driver_mut().close_fds();
522        self.driver_mut().request_stop();
523    }
524}
525
526fn spread_hw_event_frames(events: &mut [HwMidiEvent], frames: u32) {
527    if events.len() <= 1 || frames <= 1 {
528        return;
529    }
530    let n = events.len() as u32;
531    for (idx, event) in events.iter_mut().enumerate() {
532        let pos = idx as u32;
533        event.event.frame = ((pos as u64 * (frames - 1) as u64) / n as u64) as u32;
534    }
535}