Skip to main content

truce_loader/
static_shell.rs

1//! `StaticShell` - embeds the plugin directly into the binary.
2//!
3//! No dlopen, no file watcher, no Mutex. Same types as `HotShell`
4//! but zero runtime overhead. Use via `export_static!`.
5
6use std::sync::Arc;
7use std::sync::atomic::{AtomicU32, Ordering};
8
9use truce_core::buffer::AudioBuffer;
10use truce_core::bus::BusLayout;
11use truce_core::editor::Editor;
12use truce_core::events::{EventBody, EventList};
13use truce_core::info::PluginInfo;
14use truce_core::plugin::Plugin;
15use truce_core::process::{ProcessContext, ProcessStatus};
16use truce_gui::PluginLogicCore;
17use truce_params::Params;
18use truce_params::sample::Sample;
19
20// ---------------------------------------------------------------------------
21// StaticShell
22// ---------------------------------------------------------------------------
23
24/// A static plugin shell that embeds the user's `PluginLogic` impl
25/// directly into the format-wrapper binary.
26///
27/// Same bridging as `HotShell` but without `NativeLoader`, `Mutex`,
28/// file watching, or any dynamic loading overhead. Use via `export_static!`.
29pub struct StaticShell<P: Params, L: PluginLogicCore<S>, S: Sample = f32> {
30    pub params: Arc<P>,
31    logic: L,
32    meters: Arc<[AtomicU32; 256]>,
33    sample_rate: f64,
34    _sample: std::marker::PhantomData<fn() -> S>,
35}
36
37// SAFETY: `StaticShell` owns `Arc<P>` (params, `Sync` by the
38// `Params` trait contract), `L` (the user's logic - `Send + 'static`
39// per the `PluginLogicCore` bound), an `AtomicU32`-backed meters
40// array, and a `PhantomData<fn() -> S>`. No raw pointers, no
41// `!Send` fields, no interior mutability that escapes the shell's
42// own `&mut` borrows. The host contract that format wrappers
43// invoke methods on a single thread at a time per instance is what
44// keeps the embedded `L` safe to access without an inner mutex -
45// same model `HotShell` uses through `parking_lot::Mutex`.
46unsafe impl<P: Params, L: PluginLogicCore<S>, S: Sample> Send for StaticShell<P, L, S> {}
47
48impl<P: Params + Default + 'static, L: PluginLogicCore<S> + 'static, S: Sample>
49    StaticShell<P, L, S>
50{
51    /// Create from pre-constructed parts. The plugin logic should
52    /// hold an `Arc::clone` of the same params.
53    pub fn from_parts(params: Arc<P>, logic: L) -> Self {
54        Self {
55            params,
56            logic,
57            meters: Arc::new(std::array::from_fn(|_| AtomicU32::new(0))),
58            sample_rate: 44100.0,
59            _sample: std::marker::PhantomData,
60        }
61    }
62
63    /// Access the plugin logic (for testing).
64    pub fn logic_ref(&self) -> &L {
65        &self.logic
66    }
67
68    /// Mutable access to the plugin logic (for testing).
69    pub fn logic_ref_mut(&mut self) -> &mut L {
70        &mut self.logic
71    }
72
73    /// Try to get a custom editor from the plugin logic.
74    pub fn try_custom_editor(&self) -> Option<Box<dyn Editor>> {
75        self.logic.custom_editor()
76    }
77
78    /// Try to create a `BuiltinEditor` from the plugin's layout.
79    /// Returns `None` if the layout has zero size.
80    pub fn try_builtin_editor(&self) -> Option<truce_gui::editor::BuiltinEditor<P>> {
81        let layout = self.logic.layout();
82        if layout.width == 0 || layout.height == 0 {
83            return None;
84        }
85        Some(truce_gui::editor::BuiltinEditor::new_grid(
86            Arc::clone(&self.params),
87            layout,
88        ))
89    }
90}
91
92impl<P: Params + Default + 'static, L: PluginLogicCore<S> + 'static, S: Sample> Plugin
93    for StaticShell<P, L, S>
94{
95    type Sample = S;
96
97    fn info() -> PluginInfo
98    where
99        Self: Sized,
100    {
101        unreachable!("StaticShell::info() should not be called statically")
102    }
103
104    fn bus_layouts() -> Vec<BusLayout>
105    where
106        Self: Sized,
107    {
108        unreachable!("StaticShell::bus_layouts() should not be called statically")
109    }
110
111    fn init(&mut self) {}
112
113    fn reset(&mut self, sample_rate: f64, max_block_size: usize) {
114        self.sample_rate = sample_rate;
115        self.params.set_sample_rate(sample_rate);
116        self.logic.reset(sample_rate, max_block_size);
117    }
118
119    fn process(
120        &mut self,
121        buffer: &mut AudioBuffer<S>,
122        events: &EventList,
123        context: &mut ProcessContext,
124    ) -> ProcessStatus {
125        // Apply parameter change events to the shell's params.
126        // ParamChange values from format wrappers are PLAIN (already
127        // denormalized). `set_normalized` here would double-denormalize.
128        for e in events.iter() {
129            if let EventBody::ParamChange { id, value } = &e.body {
130                self.params.set_plain(*id, *value);
131            }
132        }
133
134        // No sync needed - plugin reads from the same Arc<Params>.
135
136        // Build a ProcessContext with param/meter callbacks for the logic.
137        let params = &self.params;
138        let meters = &self.meters;
139        let param_fn = |id: u32| -> f64 { params.get_plain(id).unwrap_or(0.0) };
140        let meter_fn = |id: u32, v: f32| {
141            // Meter IDs are offset by `truce_params::METER_ID_BASE`;
142            // mirror the offset in `get_meter` exactly.
143            let idx = id.wrapping_sub(truce_params::METER_ID_BASE) as usize;
144            if let Some(slot) = meters.get(idx) {
145                slot.store(v.to_bits(), Ordering::Relaxed);
146            }
147        };
148        let mut ctx = ProcessContext::new(
149            context.transport,
150            context.sample_rate,
151            buffer.num_samples(),
152            &mut *context.output_events,
153        )
154        .with_params(&param_fn)
155        .with_meters(&meter_fn);
156
157        self.logic.process(buffer, events, &mut ctx)
158    }
159
160    fn save_state(&self) -> Vec<u8> {
161        self.logic.save_state()
162    }
163
164    fn load_state(&mut self, data: &[u8]) -> Result<(), truce_core::state::StateLoadError> {
165        let result = self.logic.load_state(data);
166        // Plugin-side cache invalidation runs in the same `&mut`
167        // borrow window so the next `process()` block sees the
168        // refreshed caches - fire it whether or not load_state
169        // succeeded so partial state still triggers a refresh.
170        PluginLogicCore::state_changed(&mut self.logic);
171        result
172    }
173
174    fn editor(&mut self) -> Option<Box<dyn Editor>> {
175        if let Some(editor) = self.logic.custom_editor() {
176            return Some(editor);
177        }
178        // iOS's `BuiltinEditor` already owns its UIView and the
179        // CAMetalLayer-backed wgpu surface, so no `GpuEditor` wrapper
180        // is needed on that platform.
181        #[cfg(target_os = "ios")]
182        {
183            self.try_builtin_editor()
184                .map(|e| Box::new(e) as Box<dyn Editor>)
185        }
186        #[cfg(not(target_os = "ios"))]
187        {
188            self.try_builtin_editor()
189                .map(|e| Box::new(truce_gpu::GpuEditor::new(e)) as Box<dyn Editor>)
190        }
191    }
192
193    fn latency(&self) -> u32 {
194        self.logic.latency()
195    }
196    fn tail(&self) -> u32 {
197        self.logic.tail()
198    }
199
200    fn get_meter(&self, meter_id: u32) -> f32 {
201        // Meter IDs live in a dedicated high range starting at
202        // `truce_params::METER_ID_BASE`; storage is offset into
203        // `self.meters`. `wrapping_sub` keeps out-of-range ids from
204        // panicking - they fall through to the `get` -> None path.
205        let idx = meter_id.wrapping_sub(truce_params::METER_ID_BASE) as usize;
206        if let Some(slot) = self.meters.get(idx) {
207            f32::from_bits(slot.load(Ordering::Relaxed))
208        } else {
209            0.0
210        }
211    }
212}
213
214// ---------------------------------------------------------------------------
215// export_static! macro
216// ---------------------------------------------------------------------------
217
218/// Compile-time static embedding of a `PluginLogic` impl into the binary.
219///
220/// Produces a `__HotShellWrapper` struct that implements `Plugin + PluginExport`,
221/// so format export macros (`export_clap!`, `export_vst3!`, etc.) work unchanged.
222/// No dlopen, no file watcher, zero runtime overhead. Bus layouts come from
223/// `<$logic as PluginLogic>::bus_layouts()` - override the trait method to
224/// pick something other than the stereo default.
225///
226/// ```ignore
227/// export_static! {
228///     params: GainParams,
229///     info: plugin_info!(...),
230///     logic: Gain,
231/// }
232///
233/// #[cfg(feature = "clap")]
234/// truce_clap::export_clap!(__HotShellWrapper);
235/// ```
236#[macro_export]
237macro_rules! export_static {
238    (
239        params: $params:ty,
240        info: $info:expr,
241        logic: $logic:ty,
242    ) => {
243        pub struct __HotShellWrapper {
244            // `Sample` here resolves to the type alias the user
245            // imported from a prelude (`prelude` / `prelude32` →
246            // `f32`; `prelude64` → `f64`; `prelude64m` → `f32`). The
247            // `PluginLogic<Sample>` bound on the user's impl must
248            // match this, so the prelude is what picks the audio
249            // buffer precision end-to-end.
250            inner: $crate::static_shell::StaticShell<$params, $logic, Sample>,
251        }
252
253        impl $crate::__macro_deps::truce_core::plugin::Plugin for __HotShellWrapper {
254            type Sample = Sample;
255
256            fn supports_in_place() -> bool
257            where
258                Self: Sized,
259            {
260                // `PluginLogicCore<Sample>` is the wrapper-facing
261                // trait; the user impl'd one of the leaf traits
262                // (`PluginLogic` / `PluginLogic64`), and the blanket
263                // bridge defined alongside those traits in
264                // `truce-plugin` makes them also satisfy
265                // `PluginLogicCore<Sample>` automatically. Sample
266                // resolves through the prelude alias in scope at the
267                // macro call site.
268                <$logic as $crate::__macro_deps::truce_gui::PluginLogicCore<Sample>>::supports_in_place()
269            }
270
271            fn info() -> $crate::__macro_deps::truce_core::info::PluginInfo
272            where
273                Self: Sized,
274            {
275                $info
276            }
277
278            fn bus_layouts() -> Vec<$crate::__macro_deps::truce_core::bus::BusLayout>
279            where
280                Self: Sized,
281            {
282                <$logic as $crate::__macro_deps::truce_gui::PluginLogicCore<Sample>>::bus_layouts()
283            }
284
285            fn init(&mut self) {
286                self.inner.init();
287            }
288
289            fn reset(&mut self, sample_rate: f64, max_block_size: usize) {
290                self.inner.reset(sample_rate, max_block_size);
291            }
292
293            fn process(
294                &mut self,
295                buffer: &mut $crate::__macro_deps::truce_core::buffer::AudioBuffer<Sample>,
296                events: &$crate::__macro_deps::truce_core::events::EventList,
297                context: &mut $crate::__macro_deps::truce_core::process::ProcessContext,
298            ) -> $crate::__macro_deps::truce_core::process::ProcessStatus {
299                self.inner.process(buffer, events, context)
300            }
301
302            fn save_state(&self) -> Vec<u8> {
303                self.inner.save_state()
304            }
305
306            fn load_state(
307                &mut self,
308                data: &[u8],
309            ) -> Result<(), $crate::__macro_deps::truce_core::state::StateLoadError> {
310                self.inner.load_state(data)
311            }
312
313            fn editor(
314                &mut self,
315            ) -> Option<Box<dyn $crate::__macro_deps::truce_core::editor::Editor>> {
316                self.inner.editor()
317            }
318
319            fn latency(&self) -> u32 {
320                self.inner.latency()
321            }
322            fn tail(&self) -> u32 {
323                self.inner.tail()
324            }
325            fn get_meter(&self, meter_id: u32) -> f32 {
326                self.inner.get_meter(meter_id)
327            }
328        }
329
330        impl $crate::__macro_deps::truce_core::export::PluginExport for __HotShellWrapper {
331            type Params = $params;
332
333            fn create() -> Self {
334                let params = std::sync::Arc::new(<$params>::new());
335                let logic = <$logic>::new(std::sync::Arc::clone(&params));
336                Self {
337                    inner: $crate::static_shell::StaticShell::from_parts(params, logic),
338                }
339            }
340
341            fn params(&self) -> &$params {
342                &self.inner.params
343            }
344
345            fn params_arc(&self) -> std::sync::Arc<$params> {
346                std::sync::Arc::clone(&self.inner.params)
347            }
348        }
349    };
350}