Skip to main content

cpu/
cpu.rs

1//! Reports how much of the block deadline the audio thread uses, and
2//! how much of that goes into one measured section of `render`.
3//!
4//! `render` synthesises a bank of sine oscillators, which is there to
5//! use a visible and adjustable amount of CPU. `Settings::cpu_monitoring`
6//! turns on Bela's own bracketing of the whole audio thread, read back
7//! with `BlockContext::cpu_usage`, and a `CpuTimer` covers just the
8//! oscillator bank, so the two numbers can be compared: the difference
9//! is what the rest of the audio thread costs.
10//!
11//! The `CpuTimer` lives in the render state rather than in the
12//! application, because it is the section of *this thread's* `render`
13//! that it measures, and its counters are its own. With more than one
14//! render thread there is one per thread, and the report reads the
15//! first of them.
16//!
17//! The printing happens on an auxiliary task, since it is not work for
18//! the audio thread. Both percentages are read in `render_post` — the
19//! audio thread's counters may only be read from a callback that runs
20//! on that thread — and handed to the task through atomics, which is
21//! the pattern to copy.
22//!
23//! Cross-compile and run on the board (see docs/cross-compile.md):
24//!
25//! ```sh
26//! cargo build -p bela --release --target aarch64-unknown-linux-gnu --example cpu
27//! ```
28
29#![cfg_attr(
30    not(bela_device),
31    allow(
32        dead_code,
33        reason = "only the fallback main is reachable off-device; the application code should still compile and lint"
34    )
35)]
36
37use core::f32::consts::TAU;
38use core::num::NonZeroU32;
39use core::sync::atomic::{AtomicU32, Ordering};
40#[cfg(not(bela_device))]
41use std::process::ExitCode;
42use std::sync::Arc;
43
44use bela::{
45    AuxiliaryTask, BelaApplication, BlockContext, CleanupContext, CpuTimer, Priority,
46    RenderContext, SetupContext, ThreadInfo, rt_println,
47};
48
49/// Enough oscillators to be measurable, few enough to leave headroom.
50const OSCILLATORS: usize = 64;
51const BASE_FREQUENCY: f32 = 110.0;
52const AMPLITUDE: f32 = 0.2;
53
54/// Blocks per acquisition cycle. At 44.1 kHz and 16 frames per block
55/// that is a fresh reading roughly every 0.7 s, so a one-second report
56/// always has one.
57const MEASUREMENTS_PER_CYCLE: u32 = 2000;
58
59/// The report runs below the audio thread, so it can never delay it.
60const TASK_PRIORITY: Priority = Priority::new(75).expect("75 is within Bela's priority range");
61
62/// The two percentages the audio thread publishes for the task to
63/// print, each as `f32::to_bits`.
64#[derive(Debug, Default)]
65struct Published {
66    thread: AtomicU32,
67    section: AtomicU32,
68}
69
70struct Load {
71    published: Arc<Published>,
72    task: Option<AuxiliaryTask>,
73    /// The phases the *block* starts at, advanced once per block in
74    /// `render_post`, so the tone does not depend on how the block was
75    /// split. See `examples/sine.rs` for the pattern on its own.
76    phases: [f32; OSCILLATORS],
77    phase_increments: [f32; OSCILLATORS],
78    blocks: u64,
79    blocks_per_report: u64,
80}
81
82/// One render thread's oscillator bank, and its measurement of it.
83struct Bank {
84    /// The first frame this thread writes, so that `render_pre` can
85    /// seed the phases its share of the block starts at.
86    first_frame: usize,
87    phases: [f32; OSCILLATORS],
88    /// This application's measurement of the oscillator bank alone.
89    timer: CpuTimer,
90}
91
92impl Load {
93    fn new() -> Self {
94        Self {
95            published: Arc::new(Published::default()),
96            task: None,
97            phases: [0.0; OSCILLATORS],
98            phase_increments: [0.0; OSCILLATORS],
99            blocks: 0,
100            // Replaced in setup, once the block size is known.
101            blocks_per_report: 1,
102        }
103    }
104}
105
106const fn cycle() -> NonZeroU32 {
107    NonZeroU32::new(MEASUREMENTS_PER_CYCLE).expect("the cycle length is a non-zero constant")
108}
109
110impl BelaApplication for Load {
111    type RenderState = Bank;
112
113    fn setup(&mut self, context: &SetupContext) -> bool {
114        let sample_rate = context.audio_sample_rate();
115        for (index, increment) in self.phase_increments.iter_mut().enumerate() {
116            #[allow(
117                clippy::cast_precision_loss,
118                reason = "the oscillator index is far below f32's exact integer range"
119            )]
120            let harmonic = index as f32 + 1.0;
121            *increment = TAU * BASE_FREQUENCY * harmonic / sample_rate;
122        }
123
124        #[allow(
125            clippy::cast_possible_truncation,
126            clippy::cast_sign_loss,
127            reason = "the sample rate is a small positive number"
128        )]
129        let sample_rate_hz = sample_rate as u64;
130        self.blocks_per_report = (sample_rate_hz / context.audio_frames().max(1) as u64).max(1);
131
132        if context.cpu_usage().is_none() {
133            rt_println!("setup: CPU monitoring is off; run with Settings::cpu_monitoring");
134            return false;
135        }
136
137        // The callback owns everything it touches: the percentages
138        // arrive through the atomics, because the audio thread's
139        // counters cannot be read from this thread.
140        let published = Arc::clone(&self.published);
141        let task = AuxiliaryTask::new("bela-rs-cpu", TASK_PRIORITY, move || {
142            let thread = f32::from_bits(published.thread.load(Ordering::Relaxed));
143            let section = f32::from_bits(published.section.load(Ordering::Relaxed));
144            rt_println!("cpu: audio thread {thread:.1}%; oscillators {section:.1}%");
145        });
146
147        match task {
148            Ok(task) => {
149                self.task = Some(task);
150                rt_println!(
151                    "setup: {OSCILLATORS} oscillators over {} render thread(s), \
152                     reporting every {} blocks",
153                    context.thread_count(),
154                    self.blocks_per_report
155                );
156                true
157            }
158            Err(error) => {
159                rt_println!("setup: could not create the task: {error}");
160                false
161            }
162        }
163    }
164
165    fn create_render_state(&mut self, thread: ThreadInfo, context: &SetupContext) -> Bank {
166        Bank {
167            first_frame: thread.frame_range(context.audio_frames()).start,
168            phases: [0.0; OSCILLATORS],
169            timer: CpuTimer::new(cycle()),
170        }
171    }
172
173    // Real-time safe: arithmetic on values the states already hold.
174    fn render_pre(&mut self, states: &mut [Bank], _context: &mut BlockContext) {
175        for state in states {
176            #[allow(
177                clippy::cast_precision_loss,
178                reason = "a frame index within a block is far below f32's exact integer range"
179            )]
180            let offset = state.first_frame as f32;
181            for (phase, (block_phase, increment)) in state
182                .phases
183                .iter_mut()
184                .zip(self.phases.iter().zip(&self.phase_increments))
185            {
186                *phase = block_phase + offset * increment;
187            }
188        }
189    }
190
191    // Real-time safe: arithmetic, writes to this thread's frames, and
192    // a clock read through the CPU timer.
193    fn render(&self, state: &mut Bank, context: &mut RenderContext) {
194        // Measures until the guard is dropped at the end of this
195        // scope. Entered on every block, so the period each
196        // measurement is a fraction of is the block period.
197        let _oscillators = state.timer.measure();
198
199        let channels = context.audio_out_channels();
200        for frame in context.audio_frame_range() {
201            let mut sample = 0.0;
202            for (phase, increment) in state.phases.iter_mut().zip(&self.phase_increments) {
203                sample += phase.sin();
204                *phase += increment;
205                if *phase >= TAU {
206                    *phase -= TAU;
207                }
208            }
209            #[allow(
210                clippy::cast_precision_loss,
211                reason = "the oscillator count is far below f32's exact integer range"
212            )]
213            let sample = AMPLITUDE * sample / OSCILLATORS as f32;
214            for channel in 0..channels {
215                context.audio_write(frame, channel, sample);
216            }
217        }
218    }
219
220    // Real-time safe: arithmetic, atomic stores and a schedule.
221    fn render_post(&mut self, states: &mut [Bank], context: &mut BlockContext) {
222        #[allow(
223            clippy::cast_precision_loss,
224            reason = "a block's frame count is far below f32's exact integer range"
225        )]
226        let frames = context.audio_frames() as f32;
227        for (phase, increment) in self.phases.iter_mut().zip(&self.phase_increments) {
228            *phase = (*phase + frames * increment) % TAU;
229        }
230
231        self.blocks += 1;
232        if self.blocks % self.blocks_per_report != 0 {
233            return;
234        }
235        // Read here, on the main audio thread, and handed to the task
236        // as plain numbers.
237        let thread = context.cpu_usage().map_or(0.0, |usage| usage.percentage());
238        let section = states
239            .first()
240            .map_or(0.0, |bank| bank.timer.usage().percentage());
241        self.published
242            .thread
243            .store(thread.to_bits(), Ordering::Relaxed);
244        self.published
245            .section
246            .store(section.to_bits(), Ordering::Relaxed);
247        if let Some(task) = &self.task {
248            task.schedule(context);
249        }
250    }
251
252    fn cleanup(&mut self, states: &mut [Bank], context: &CleanupContext) {
253        // Sound from `cleanup` too: libbela has joined the audio thread
254        // by the time this runs, so nothing is writing the counters.
255        if let Some(usage) = context.cpu_usage() {
256            rt_println!("cleanup: audio thread {usage}");
257        }
258        if let Some(bank) = states.first() {
259            rt_println!(
260                "cleanup: oscillators {}; {} blocks rendered",
261                bank.timer.usage(),
262                self.blocks
263            );
264        }
265    }
266}
267
268/// Checks that monitoring is refused at a period size where libbela
269/// would run `render` on its FIFO thread, away from the thread that
270/// updates the counters. Only the board can tell: the split happens
271/// inside libbela, and nothing in the context reveals it.
272#[cfg(bela_device)]
273fn report_fifo_guard() {
274    let settings = bela::Settings::new()
275        .cpu_monitoring(cycle())
276        .period_size(bela::MAX_MONITORED_PERIOD_SIZE * 2);
277    let outcome = match bela::Bela::new(Load::new(), &settings) {
278        Err(bela::Error::CpuMonitoringPeriodSize(frames)) => format!("refused at {frames} frames"),
279        Err(error) => format!("other-error {error}"),
280        // Dropped immediately, which tears the audio system down again.
281        Ok(_) => "accepted".to_owned(),
282    };
283    println!("fifo-guard: {outcome}");
284}
285
286#[cfg(bela_device)]
287fn main() -> Result<(), bela::Error> {
288    report_fifo_guard();
289    bela::Bela::run(Load::new(), &bela::Settings::new().cpu_monitoring(cycle()))
290}
291
292#[cfg(not(bela_device))]
293fn main() -> ExitCode {
294    eprintln!("This example must be cross-compiled for Bela Gem (aarch64-unknown-linux-gnu).");
295    ExitCode::FAILURE
296}