use core::cell::UnsafeCell;
use core::ffi::c_void;
use core::ptr;
use core::slice;
use core::sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, Ordering};
use bela_sys::BelaContext;
use crate::application::{BelaApplication, ThreadInfo};
use crate::context::{BlockContext, CleanupContext, RenderContext, SetupContext};
const EXCLUSIVE: u64 = 1 << 63;
#[cfg(bela_device)]
pub(crate) fn request_stop() {
unsafe { bela_sys::Bela_requestStop() }
}
#[cfg(not(bela_device))]
const fn request_stop() {}
#[cfg(bela_device)]
pub(crate) fn stop_requested() -> bool {
unsafe { bela_sys::Bela_stopRequested() != 0 }
}
#[cfg(not(bela_device))]
const fn stop_requested() -> bool {
false
}
struct Guard {
state: AtomicU64,
busy: Box<[AtomicBool]>,
faults: AtomicU32,
faults_while_stopping: AtomicU32,
stopping: AtomicBool,
}
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the device-gated system module builds a runtime; still unit-tested on the host"
)
)]
impl Guard {
fn new(threads: usize) -> Self {
Self {
state: AtomicU64::new(0),
busy: (0..threads).map(|_| AtomicBool::new(false)).collect(),
faults: AtomicU32::new(0),
faults_while_stopping: AtomicU32::new(0),
stopping: AtomicBool::new(false),
}
}
fn threads(&self) -> usize {
self.busy.len()
}
fn faults(&self) -> u32 {
self.faults.load(Ordering::Relaxed)
}
fn faults_while_stopping(&self) -> u32 {
self.faults_while_stopping.load(Ordering::Relaxed)
}
fn fault(&self) {
self.record_fault(stop_requested());
}
fn record_fault(&self, while_stopping: bool) {
if while_stopping {
self.faults_while_stopping.fetch_add(1, Ordering::Relaxed);
return;
}
if self.faults.fetch_add(1, Ordering::Relaxed) == 0 {
crate::rt_println!(
"bela: a callback broke the protocol the render states rely on and was refused; \
stopping"
);
}
request_stop();
}
fn enter_exclusive(&self) -> Option<Exclusive<'_>> {
self.enter_exclusive_seeing(stop_requested())
}
fn enter_exclusive_seeing(&self, stopping: bool) -> Option<Exclusive<'_>> {
self.stopping.store(stopping, Ordering::Relaxed);
if self
.state
.compare_exchange(0, EXCLUSIVE, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
self.record_fault(stopping);
return None;
}
Some(Exclusive { guard: self })
}
fn enter_render(&self, thread: usize) -> Option<Active<'_>> {
let Some(busy) = self.busy.get(thread) else {
self.fault();
return None;
};
if self
.state
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |state| {
(state & EXCLUSIVE == 0).then(|| state + 1)
})
.is_err()
{
self.record_fault(self.stopping.load(Ordering::Relaxed));
return None;
}
if busy.swap(true, Ordering::Acquire) {
self.state.fetch_sub(1, Ordering::Release);
self.fault();
return None;
}
Some(Active {
guard: self,
thread,
})
}
}
struct Exclusive<'a> {
guard: &'a Guard,
}
impl Drop for Exclusive<'_> {
fn drop(&mut self) {
self.guard.state.store(0, Ordering::Release);
}
}
struct Active<'a> {
guard: &'a Guard,
thread: usize,
}
impl Drop for Active<'_> {
fn drop(&mut self) {
self.guard.busy[self.thread].store(false, Ordering::Release);
self.guard.state.fetch_sub(1, Ordering::Release);
}
}
pub(crate) struct Runtime<T: BelaApplication> {
app: UnsafeCell<T>,
storage: UnsafeCell<Vec<T::RenderState>>,
states: AtomicPtr<T::RenderState>,
guard: Guard,
}
unsafe impl<T: BelaApplication> Sync for Runtime<T> {}
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the device-gated system module builds a runtime; still unit-tested on the host"
)
)]
impl<T: BelaApplication> Runtime<T> {
pub(crate) fn new(application: T, threads: usize) -> Self {
let threads = threads.max(1);
Self {
app: UnsafeCell::new(application),
storage: UnsafeCell::new(Vec::with_capacity(threads)),
states: AtomicPtr::new(ptr::null_mut()),
guard: Guard::new(threads),
}
}
pub(crate) fn faults(&self) -> u32 {
self.guard.faults()
}
pub(crate) fn faults_while_stopping(&self) -> u32 {
self.guard.faults_while_stopping()
}
fn threads_agree(&self, threads: usize) -> bool {
if threads == self.guard.threads() {
return true;
}
self.guard.fault();
false
}
unsafe fn setup(&self, context: *mut BelaContext) -> bool {
let Some(exclusive) = self.guard.enter_exclusive() else {
return false;
};
let context: &SetupContext = unsafe { SetupContext::from_mut_ptr(context) };
if !self.threads_agree(context.thread_count()) {
return false;
}
if !self.states.load(Ordering::Acquire).is_null() {
self.guard.fault();
return false;
}
let app = unsafe { &mut *self.app.get() };
if !app.setup(context) {
return false;
}
let storage = unsafe { &mut *self.storage.get() };
let threads = self.guard.threads();
for index in 0..threads {
storage.push(app.create_render_state(ThreadInfo::new(index, threads), context));
}
self.states.store(storage.as_mut_ptr(), Ordering::Release);
drop(exclusive);
true
}
unsafe fn render(&self, context: *mut BelaContext) {
let thread = unsafe { (*context).thisThread } as usize;
let Some(active) = self.guard.enter_render(thread) else {
return;
};
let context = unsafe { RenderContext::from_mut_ptr(context) };
if !self.threads_agree(context.thread_count()) {
return;
}
let base = self.states.load(Ordering::Acquire);
if base.is_null() {
self.guard.fault();
return;
}
let state = unsafe { &mut *base.add(active.thread) };
let app = unsafe { &*self.app.get() };
app.render(state, context);
}
unsafe fn render_pre(&self, context: *mut BelaContext) {
let Some(exclusive) = self.guard.enter_exclusive() else {
return;
};
let Some(states) = self.states_mut(&exclusive) else {
return;
};
let context = unsafe { BlockContext::from_mut_ptr(context) };
if !self.threads_agree(context.thread_count()) {
return;
}
let app = unsafe { &mut *self.app.get() };
app.render_pre(states, context);
}
unsafe fn render_post(&self, context: *mut BelaContext) {
let Some(exclusive) = self.guard.enter_exclusive() else {
return;
};
let Some(states) = self.states_mut(&exclusive) else {
return;
};
let context = unsafe { BlockContext::from_mut_ptr(context) };
if !self.threads_agree(context.thread_count()) {
return;
}
let app = unsafe { &mut *self.app.get() };
app.render_post(states, context);
}
unsafe fn cleanup(&self, context: *mut BelaContext) {
let Some(exclusive) = self.guard.enter_exclusive() else {
return;
};
let Some(states) = self.states_mut(&exclusive) else {
return;
};
let context: &CleanupContext = unsafe { CleanupContext::from_mut_ptr(context) };
if !self.threads_agree(context.thread_count()) {
return;
}
let app = unsafe { &mut *self.app.get() };
app.cleanup(states, context);
}
#[allow(
clippy::mut_from_ref,
reason = "the exclusive claim, not the &self, is what makes this the only reference"
)]
fn states_mut(&self, _exclusive: &Exclusive<'_>) -> Option<&mut [T::RenderState]> {
let base = self.states.load(Ordering::Acquire);
if base.is_null() {
self.guard.fault();
return None;
}
Some(unsafe { slice::from_raw_parts_mut(base, self.guard.threads()) })
}
}
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only called by the device-gated system module; still unit-tested on the host"
)
)]
pub(crate) mod trampoline {
use core::ffi::c_void;
use bela_sys::BelaContext;
use super::{BelaApplication, Runtime};
pub(crate) unsafe extern "C" fn setup<T: BelaApplication>(
context: *mut BelaContext,
user_data: *mut c_void,
) -> bool {
let runtime = unsafe { &*user_data.cast::<Runtime<T>>() };
unsafe { runtime.setup(context) }
}
pub(crate) unsafe extern "C" fn render_pre<T: BelaApplication>(
context: *mut BelaContext,
user_data: *mut c_void,
) {
let runtime = unsafe { &*user_data.cast::<Runtime<T>>() };
unsafe { runtime.render_pre(context) };
}
pub(crate) unsafe extern "C" fn render<T: BelaApplication>(
context: *mut BelaContext,
user_data: *mut c_void,
) {
let runtime = unsafe { &*user_data.cast::<Runtime<T>>() };
unsafe { runtime.render(context) };
}
pub(crate) unsafe extern "C" fn render_post<T: BelaApplication>(
context: *mut BelaContext,
user_data: *mut c_void,
) {
let runtime = unsafe { &*user_data.cast::<Runtime<T>>() };
unsafe { runtime.render_post(context) };
}
pub(crate) unsafe extern "C" fn cleanup<T: BelaApplication>(
context: *mut BelaContext,
user_data: *mut c_void,
) {
let runtime = unsafe { &*user_data.cast::<Runtime<T>>() };
unsafe { runtime.cleanup(context) };
}
}
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the device-gated system module hands the pointer over"
)
)]
pub(crate) const fn user_data<T: BelaApplication>(runtime: *mut Runtime<T>) -> *mut c_void {
runtime.cast::<c_void>()
}
#[cfg(test)]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::float_cmp,
reason = "tests use small exact values where these casts and comparisons are lossless"
)]
mod tests {
use std::sync::{Arc, Barrier};
use std::thread;
use super::*;
use crate::context::tests::Fixture;
#[derive(Clone, Copy)]
struct Shared<T>(*mut T);
unsafe impl<T> Send for Shared<T> {}
fn mirrors(fixture: &Fixture, threads: usize) -> Vec<BelaContext> {
(0..threads)
.map(|thread| {
let mut mirror = fixture.context;
mirror.thisThread = thread as u32;
mirror
})
.collect()
}
#[derive(Default)]
struct Recorder {
setup_calls: AtomicU32,
pre_calls: AtomicU32,
render_calls: AtomicU32,
post_calls: AtomicU32,
cleanup_calls: AtomicU32,
setup_ok: bool,
}
#[derive(Debug, PartialEq, Eq)]
struct Slot {
thread: usize,
renders: usize,
}
impl BelaApplication for Recorder {
type RenderState = Slot;
fn setup(&mut self, _context: &SetupContext) -> bool {
self.setup_calls.fetch_add(1, Ordering::Relaxed);
self.setup_ok
}
fn create_render_state(&mut self, thread: ThreadInfo, _context: &SetupContext) -> Slot {
Slot {
thread: thread.index(),
renders: 0,
}
}
fn render_pre(&mut self, _states: &mut [Slot], _context: &mut BlockContext) {
self.pre_calls.fetch_add(1, Ordering::Relaxed);
}
fn render(&self, state: &mut Slot, context: &mut RenderContext) {
self.render_calls.fetch_add(1, Ordering::Relaxed);
state.renders += 1;
assert_eq!(
state.thread,
context.this_thread(),
"each render must get its own thread's state"
);
let marker = state.thread as f32 + 1.0;
for frame in context.audio_frame_range() {
context.audio_write(frame, 0, marker);
}
for frame in context.analog_frame_range() {
context.analog_write_once(frame, 0, marker);
}
for frame in context.digital_frame_range() {
context.digital_write_once(frame, state.thread, true);
assert!(
context.digital_read(frame, state.thread),
"a thread should read back what it just wrote"
);
}
}
fn render_post(&mut self, _states: &mut [Slot], _context: &mut BlockContext) {
self.post_calls.fetch_add(1, Ordering::Relaxed);
}
fn cleanup(&mut self, _states: &mut [Slot], _context: &CleanupContext) {
self.cleanup_calls.fetch_add(1, Ordering::Relaxed);
}
}
fn recorder() -> Recorder {
Recorder {
setup_ok: true,
..Recorder::default()
}
}
fn counts(runtime: &Runtime<Recorder>) -> [u32; 5] {
let app = unsafe { &*runtime.app.get() };
[
app.setup_calls.load(Ordering::Relaxed),
app.pre_calls.load(Ordering::Relaxed),
app.render_calls.load(Ordering::Relaxed),
app.post_calls.load(Ordering::Relaxed),
app.cleanup_calls.load(Ordering::Relaxed),
]
}
#[test]
fn a_block_runs_pre_then_render_then_post() {
let mut fixture = Fixture::new();
let runtime = Runtime::new(recorder(), 1);
let context = &raw mut fixture.context;
unsafe {
assert!(runtime.setup(context));
runtime.render_pre(context);
runtime.render(context);
runtime.render_post(context);
runtime.cleanup(context);
}
assert_eq!(counts(&runtime), [1, 1, 1, 1, 1]);
assert_eq!(runtime.faults(), 0);
}
#[test]
fn a_setup_that_aborts_leaves_no_states() {
let mut fixture = Fixture::new();
let runtime = Runtime::new(Recorder::default(), 1);
let context = &raw mut fixture.context;
assert!(!unsafe { runtime.setup(context) });
assert!(
runtime.states.load(Ordering::Acquire).is_null(),
"no render state should exist for an application that refused to start"
);
assert_eq!(runtime.faults(), 0);
}
#[test]
fn rendering_before_setup_is_refused() {
let mut fixture = Fixture::new();
let runtime = Runtime::new(recorder(), 1);
unsafe { runtime.render(&raw mut fixture.context) };
assert_eq!(counts(&runtime)[2], 0, "render must not have run");
assert_eq!(runtime.faults(), 1);
}
#[test]
fn the_block_phases_and_cleanup_are_refused_before_setup() {
let mut fixture = Fixture::new();
let runtime = Runtime::new(recorder(), 1);
let context = &raw mut fixture.context;
unsafe {
runtime.render_pre(context);
runtime.render_post(context);
runtime.cleanup(context);
}
assert_eq!(counts(&runtime), [0, 0, 0, 0, 0], "no callback should run");
assert_eq!(runtime.faults(), 3);
}
#[test]
fn a_second_setup_is_refused() {
let mut fixture = Fixture::new();
let runtime = Runtime::new(recorder(), 1);
let context = &raw mut fixture.context;
assert!(unsafe { runtime.setup(context) });
assert!(!unsafe { runtime.setup(context) });
assert_eq!(counts(&runtime)[0], 1, "the application saw one setup");
assert_eq!(runtime.faults(), 1);
}
#[test]
fn a_context_with_the_wrong_thread_count_is_refused() {
let mut fixture = Fixture::with_threads(4);
let runtime = Runtime::new(recorder(), 1);
let context = &raw mut fixture.context;
assert!(!unsafe { runtime.setup(context) });
assert_eq!(runtime.faults(), 1);
unsafe { runtime.render(context) };
assert_eq!(runtime.faults(), 2);
assert_eq!(counts(&runtime)[2], 0);
}
#[test]
fn a_thread_number_outside_the_states_is_refused() {
let mut fixture = Fixture::with_threads(2);
let runtime = Runtime::new(recorder(), 2);
assert!(unsafe { runtime.setup(&raw mut fixture.context) });
let mut rogue = fixture.context;
rogue.thisThread = 2;
unsafe { runtime.render(&raw mut rogue) };
assert_eq!(counts(&runtime)[2], 0, "render must not have run");
assert_eq!(runtime.faults(), 1);
}
#[test]
fn a_zero_thread_count_is_one_render_thread() {
let mut fixture = Fixture::with_threads(0);
let runtime = Runtime::new(recorder(), 0);
let context = &raw mut fixture.context;
unsafe {
assert!(runtime.setup(context));
runtime.render_pre(context);
runtime.render(context);
runtime.render_post(context);
}
assert_eq!(counts(&runtime), [1, 1, 1, 1, 0]);
assert_eq!(runtime.faults(), 0);
assert_eq!(
fixture.audio_out[0], 1.0,
"the one thread rendered the whole block"
);
}
#[test]
fn more_threads_than_frames_renders_empty_shares() {
let mut fixture = Fixture::with_threads(8);
let runtime = Runtime::new(recorder(), 8);
assert!(unsafe { runtime.setup(&raw mut fixture.context) });
let mut mirrors = mirrors(&fixture, 8);
for mirror in &mut mirrors {
unsafe { runtime.render(&raw mut *mirror) };
}
assert_eq!(counts(&runtime)[2], 8);
assert_eq!(runtime.faults(), 0);
let channels = fixture.context.audioOutChannels as usize;
let written: Vec<f32> = (0..4).map(|f| fixture.audio_out[f * channels]).collect();
assert_eq!(written, vec![2.0, 4.0, 6.0, 8.0]);
}
#[test]
fn every_thread_renders_its_own_share_of_the_shared_buffers() {
const THREADS: usize = 4;
let mut fixture = Fixture::with_threads(THREADS as u32);
let runtime = Runtime::new(recorder(), THREADS);
assert!(unsafe { runtime.setup(&raw mut fixture.context) });
let mut mirrors = mirrors(&fixture, THREADS);
let runtime = Arc::new(runtime);
let start = Arc::new(Barrier::new(THREADS));
thread::scope(|scope| {
for mirror in &mut mirrors {
let context = Shared(&raw mut *mirror);
let runtime = Arc::clone(&runtime);
let start = Arc::clone(&start);
scope.spawn(move || {
let context = context;
start.wait();
unsafe { runtime.render(context.0) };
});
}
});
assert_eq!(runtime.faults(), 0);
let channels = fixture.context.audioOutChannels as usize;
let written: Vec<f32> = (0..4).map(|f| fixture.audio_out[f * channels]).collect();
assert_eq!(written, vec![1.0, 2.0, 3.0, 4.0]);
let analog_channels = fixture.context.analogOutChannels as usize;
let analog: Vec<f32> = (0..4)
.map(|f| fixture.analog_out[f * analog_channels])
.collect();
assert_eq!(analog, vec![1.0, 2.0, 3.0, 4.0]);
let digital: Vec<u32> = (0..4).map(|f| fixture.digital[f]).collect();
assert_eq!(digital, (0..4).map(|t| 1 << (t + 16)).collect::<Vec<u32>>());
}
#[test]
fn a_block_phase_arriving_while_a_render_is_in_flight_is_refused() {
let mut fixture = Fixture::with_threads(2);
let runtime = Arc::new(Runtime::new(Blocker::new(true), 2));
assert!(unsafe { runtime.setup(&raw mut fixture.context) });
let mut mirrors = mirrors(&fixture, 2);
let inside = Arc::clone(&unsafe { &*runtime.app.get() }.inside);
let release = Arc::clone(&unsafe { &*runtime.app.get() }.release);
let context = Shared(&raw mut mirrors[1]);
thread::scope(|scope| {
let rendering = {
let runtime = Arc::clone(&runtime);
scope.spawn(move || {
let context = context;
unsafe { runtime.render(context.0) };
})
};
inside.wait();
unsafe { runtime.render_post(&raw mut fixture.context) };
release.wait();
rendering
.join()
.expect("the render thread should not panic");
});
assert_eq!(
runtime.faults(),
1,
"the block phase must have been refused"
);
assert_eq!(
unsafe { &*runtime.app.get() }
.post_calls
.load(Ordering::Relaxed),
0,
"render_post must not have run"
);
}
struct Blocker {
block_render: bool,
inside: Arc<Barrier>,
release: Arc<Barrier>,
render_calls: AtomicU32,
post_calls: AtomicU32,
}
impl Blocker {
fn new(block_render: bool) -> Self {
Self {
block_render,
inside: Arc::new(Barrier::new(2)),
release: Arc::new(Barrier::new(2)),
render_calls: AtomicU32::new(0),
post_calls: AtomicU32::new(0),
}
}
fn hold(&self) {
self.inside.wait();
self.release.wait();
}
}
impl BelaApplication for Blocker {
type RenderState = ();
fn create_render_state(&mut self, _thread: ThreadInfo, _context: &SetupContext) {}
fn render_pre(&mut self, _states: &mut [()], _context: &mut BlockContext) {
if !self.block_render {
self.hold();
}
}
fn render(&self, _state: &mut (), _context: &mut RenderContext) {
self.render_calls.fetch_add(1, Ordering::Relaxed);
if self.block_render {
self.hold();
}
}
fn render_post(&mut self, _states: &mut [()], _context: &mut BlockContext) {
self.post_calls.fetch_add(1, Ordering::Relaxed);
}
}
#[test]
fn a_render_arriving_while_a_block_phase_holds_the_runtime_is_refused() {
let mut fixture = Fixture::with_threads(2);
let runtime = Arc::new(Runtime::new(Blocker::new(false), 2));
assert!(unsafe { runtime.setup(&raw mut fixture.context) });
let mut mirrors = mirrors(&fixture, 2);
let app = || unsafe { &*runtime.app.get() };
let inside = Arc::clone(&app().inside);
let release = Arc::clone(&app().release);
let rendering = Shared(&raw mut mirrors[1]);
let block = Shared(&raw mut fixture.context);
thread::scope(|scope| {
let preparing = {
let runtime = Arc::clone(&runtime);
scope.spawn(move || {
let block = block;
unsafe { runtime.render_pre(block.0) };
})
};
inside.wait();
unsafe { runtime.render(rendering.0) };
release.wait();
preparing.join().expect("the block phase should not panic");
});
assert_eq!(runtime.faults(), 1, "the render must have been refused");
assert_eq!(
app().render_calls.load(Ordering::Relaxed),
0,
"render must not have run"
);
}
#[test]
fn a_refusal_while_stopping_is_counted_apart() {
let guard = Guard::new(1);
guard.record_fault(false);
guard.record_fault(true);
guard.record_fault(true);
assert_eq!(guard.faults(), 1, "only the live refusal is a fault");
assert_eq!(
guard.faults_while_stopping(),
2,
"the refusals during the shutdown are counted, and kept apart"
);
}
#[test]
fn a_render_turned_away_by_a_stopping_phase_is_counted_as_stopping() {
let guard = Guard::new(2);
let stopping = guard
.enter_exclusive_seeing(true)
.expect("nothing is in flight");
assert!(guard.enter_render(1).is_none(), "the claim is held");
assert_eq!(guard.faults(), 0, "a shutdown is not a live fault");
assert_eq!(guard.faults_while_stopping(), 1);
drop(stopping);
}
#[test]
fn a_render_turned_away_by_a_live_phase_is_still_a_fault() {
let guard = Guard::new(2);
let live = guard
.enter_exclusive_seeing(false)
.expect("nothing is in flight");
assert!(guard.enter_render(1).is_none(), "the claim is held");
assert_eq!(guard.faults(), 1);
assert_eq!(guard.faults_while_stopping(), 0);
drop(live);
}
#[test]
fn the_same_thread_number_cannot_render_twice() {
let guard = Guard::new(2);
let first = guard.enter_render(1).expect("the slot is free");
assert!(
guard.enter_render(1).is_none(),
"a second claim on the same slot must be refused"
);
assert_eq!(guard.faults(), 1);
drop(first);
assert_eq!(guard.state.load(Ordering::Acquire), 0);
}
#[test]
fn different_thread_numbers_render_together() {
let guard = Guard::new(4);
let claims: Vec<Active<'_>> = (0..4)
.map(|thread| guard.enter_render(thread).expect("each slot is free"))
.collect();
assert_eq!(guard.faults(), 0);
drop(claims);
assert_eq!(guard.state.load(Ordering::Acquire), 0);
}
#[test]
fn an_exclusive_phase_and_a_render_never_overlap() {
let guard = Guard::new(2);
let rendering = guard.enter_render(0).expect("the slot is free");
assert!(
guard.enter_exclusive().is_none(),
"an exclusive phase must wait for the render threads to be out"
);
drop(rendering);
let exclusive = guard.enter_exclusive().expect("nothing is in flight");
assert!(
guard.enter_render(0).is_none(),
"a render must not join a block an exclusive phase is in"
);
assert!(
guard.enter_exclusive().is_none(),
"two exclusive phases must not overlap"
);
drop(exclusive);
assert_eq!(guard.faults(), 3);
assert!(
guard.enter_exclusive().is_some(),
"and then it is free again"
);
}
}