use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::time::Duration;
use parking_lot::Mutex;
use truce_core::buffer::AudioBuffer;
use truce_core::bus::BusLayout;
use truce_core::editor::{Editor, PluginContext, RawWindowHandle};
use truce_core::events::{EventBody, EventList};
use truce_core::info::PluginInfo;
use truce_core::plugin::Plugin;
use truce_core::process::{ProcessContext, ProcessStatus};
use truce_params::Params;
use truce_params::sample::Sample;
use crate::loader::NativeLoader;
macro_rules! hot_debug {
($($arg:tt)*) => {
#[cfg(feature = "hot-debug")]
eprintln!($($arg)*);
};
}
const GUI_LOCK_WAIT: Duration = Duration::from_millis(50);
pub struct HotShell<P: Params, S: Sample = f32> {
pub params: Arc<P>,
loader: Arc<Mutex<NativeLoader<S>>>,
meters: Arc<[AtomicU32; 256]>,
sample_rate: f64,
max_block_size: usize,
last_seen_load_counter: u64,
latency_cache: AtomicU32,
tail_cache: AtomicU32,
}
unsafe impl<P: Params, S: Sample> Send for HotShell<P, S> {}
impl<P: Params + 'static, S: Sample> HotShell<P, S> {
pub fn new(params: P, dylib_path: PathBuf) -> Self {
let params = Arc::new(params);
let params_ptr = Arc::as_ptr(¶ms).cast::<()>();
let loader = NativeLoader::new(dylib_path, params_ptr);
let initial_counter = loader.load_counter();
let loader = Arc::new(Mutex::new(loader));
NativeLoader::spawn_watcher(&loader);
Self {
params,
loader,
meters: Arc::new(std::array::from_fn(|_| AtomicU32::new(0))),
sample_rate: 44100.0,
max_block_size: 1024,
last_seen_load_counter: initial_counter,
latency_cache: AtomicU32::new(0),
tail_cache: AtomicU32::new(0),
}
}
#[must_use]
pub fn try_custom_editor(&self) -> Option<Box<dyn Editor>> {
let loader = self.loader.try_lock_for(GUI_LOCK_WAIT)?;
let plugin = loader.plugin()?;
plugin.custom_editor()
}
#[must_use]
pub fn try_builtin_editor(&self) -> Option<truce_gui::editor::BuiltinEditor<P>> {
let loader = self.loader.try_lock_for(GUI_LOCK_WAIT)?;
let plugin = loader.plugin()?;
let layout = plugin.layout();
if layout.width == 0 || layout.height == 0 {
return None;
}
drop(loader);
Some(truce_gui::editor::BuiltinEditor::new_grid(
Arc::clone(&self.params),
layout,
))
}
}
impl<P: Params + 'static, S: Sample> Plugin for HotShell<P, S> {
type Sample = S;
fn info() -> PluginInfo
where
Self: Sized,
{
unreachable!("HotShell::info() should not be called statically")
}
fn bus_layouts() -> Vec<BusLayout>
where
Self: Sized,
{
unreachable!("HotShell::bus_layouts() should not be called statically")
}
fn init(&mut self) {}
fn reset(&mut self, sample_rate: f64, max_block_size: usize) {
self.sample_rate = sample_rate;
self.max_block_size = max_block_size;
self.params.set_sample_rate(sample_rate);
let Some(mut loader) = self.loader.try_lock() else {
return;
};
if let Some(plugin) = loader.plugin_mut() {
plugin.reset(sample_rate, max_block_size);
self.latency_cache
.store(plugin.latency(), Ordering::Relaxed);
self.tail_cache.store(plugin.tail(), Ordering::Relaxed);
}
}
fn process(
&mut self,
buffer: &mut AudioBuffer<S>,
events: &EventList,
context: &mut ProcessContext,
) -> ProcessStatus {
let Some(mut loader) = self.loader.try_lock() else {
return ProcessStatus::Normal;
};
let counter = loader.load_counter();
if counter != self.last_seen_load_counter {
if let Some(plugin) = loader.plugin_mut() {
plugin.reset(self.sample_rate, self.max_block_size);
}
self.last_seen_load_counter = counter;
}
let Some(plugin) = loader.plugin_mut() else {
return ProcessStatus::Normal;
};
for e in events.iter() {
if let EventBody::ParamChange { id, value } = &e.body {
self.params.set_plain(*id, *value);
}
}
self.params.snap_smoothers();
let params = &self.params;
let meters = &self.meters;
let param_fn = |id: u32| -> f64 { params.get_plain(id).unwrap_or(0.0) };
let meter_fn = |id: u32, v: f32| {
let idx = id.wrapping_sub(truce_params::METER_ID_BASE) as usize;
if let Some(slot) = meters.get(idx) {
slot.store(v.to_bits(), Ordering::Relaxed);
}
};
let mut ctx = ProcessContext::new(
context.transport,
context.sample_rate,
buffer.num_samples(),
&mut *context.output_events,
)
.with_params(¶m_fn)
.with_meters(&meter_fn);
let status = plugin.process(buffer, events, &mut ctx);
self.latency_cache
.store(plugin.latency(), Ordering::Relaxed);
self.tail_cache.store(plugin.tail(), Ordering::Relaxed);
status
}
fn save_state(&self) -> Vec<u8> {
let Some(loader) = self.loader.try_lock_for(GUI_LOCK_WAIT) else {
return Vec::new();
};
loader
.plugin()
.map(truce_gui::PluginLogicCore::save_state)
.unwrap_or_default()
}
fn load_state(&mut self, data: &[u8]) -> Result<(), truce_core::state::StateLoadError> {
let Some(mut loader) = self.loader.try_lock_for(GUI_LOCK_WAIT) else {
return Ok(());
};
let Some(plugin) = loader.plugin_mut() else {
return Ok(());
};
let result = plugin.load_state(data);
plugin.state_changed();
result
}
fn editor(&mut self) -> Option<Box<dyn Editor>> {
hot_debug!("[truce-hot] editor() called");
if let Some(custom) = self.try_custom_editor() {
hot_debug!("[truce-hot] using custom editor");
return Some(Box::new(HotEditor::<P, S>::new_custom(custom)));
}
let builtin = self.try_builtin_editor()?;
hot_debug!("[truce-hot] using builtin editor (GPU path)");
let inner = Arc::new(StdMutex::new(builtin));
let gpu = truce_gpu::GpuEditor::new_shared(Arc::clone(&inner));
Some(Box::new(HotEditor::new_builtin(
gpu,
&inner,
&self.loader,
&self.params,
)))
}
fn latency(&self) -> u32 {
self.latency_cache.load(Ordering::Relaxed)
}
fn tail(&self) -> u32 {
self.tail_cache.load(Ordering::Relaxed)
}
fn get_meter(&self, meter_id: u32) -> f32 {
let idx = meter_id.wrapping_sub(truce_params::METER_ID_BASE) as usize;
self.meters
.get(idx)
.map_or(0.0, |v| f32::from_bits(v.load(Ordering::Relaxed)))
}
}
enum HotEditorInner<P: Params> {
Builtin { gpu: truce_gpu::GpuEditor<P> },
Custom { editor: Box<dyn Editor> },
}
struct HotEditor<P: Params, S: Sample = f32> {
kind: HotEditorInner<P>,
_watcher: Option<std::thread::JoinHandle<()>>,
stop: Arc<AtomicBool>,
_sample: std::marker::PhantomData<fn() -> S>,
}
unsafe impl<P: Params, S: Sample> Send for HotEditor<P, S> {}
impl<P: Params, S: Sample> Drop for HotEditor<P, S> {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
hot_debug!("[truce-gui-reload] stop flag set (editor dropped)");
}
}
impl<P: Params + 'static, S: Sample> HotEditor<P, S> {
fn new_builtin(
gpu: truce_gpu::GpuEditor<P>,
inner: &Arc<StdMutex<truce_gui::editor::BuiltinEditor<P>>>,
loader: &Arc<Mutex<NativeLoader<S>>>,
params: &Arc<P>,
) -> Self {
let stop = Arc::new(AtomicBool::new(false));
let inner_for_thread = Arc::clone(inner);
let params_for_thread = Arc::clone(params);
let loader_for_thread = Arc::clone(loader);
let stop_flag = Arc::clone(&stop);
let watcher = std::thread::Builder::new()
.name("truce-gui-reload".into())
.spawn(move || {
const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(500);
const STOP_CHECK: std::time::Duration = std::time::Duration::from_millis(50);
const LOCK_WAIT: std::time::Duration = std::time::Duration::from_millis(50);
hot_debug!("[truce-gui-reload] watcher thread started");
#[allow(clippy::cast_possible_truncation)]
let chunks = (POLL_INTERVAL.as_millis() / STOP_CHECK.as_millis()) as u32;
let mut last_seen_counter: u64 = 0;
if let Some(guard) = loader_for_thread.try_lock_for(LOCK_WAIT) {
last_seen_counter = guard.load_counter();
}
loop {
for _ in 0..chunks {
std::thread::sleep(STOP_CHECK);
if stop_flag.load(Ordering::Relaxed) {
hot_debug!(
"[truce-gui-reload] watcher thread stopping (editor dropped)"
);
return;
}
}
let Some(guard) = loader_for_thread.try_lock_for(LOCK_WAIT) else {
hot_debug!("[truce-gui-reload] loader busy (audio holds lock); retrying");
continue;
};
let mut new_layout = None;
if guard.load_counter() != last_seen_counter {
hot_debug!(
"[truce-gui-reload] reload detected (counter {} → {}); resyncing GUI",
last_seen_counter,
guard.load_counter()
);
last_seen_counter = guard.load_counter();
if let Some(plugin) = guard.plugin() {
new_layout = Some(plugin.layout());
}
}
drop(guard);
if let Some(layout) = new_layout {
hot_debug!(
"[truce-gui-reload] layout: {}x{}",
layout.width,
layout.height
);
if layout.width == 0 || layout.height == 0 {
hot_debug!("[truce-gui-reload] skipping: layout has zero size");
continue;
}
let new_builtin = truce_gui::editor::BuiltinEditor::new_grid(
Arc::clone(¶ms_for_thread),
layout,
);
if let Ok(mut g) = inner_for_thread.lock() {
let had_ctx = g.take_context();
hot_debug!(
"[truce-gui-reload] old editor had context: {}",
had_ctx.is_some()
);
*g = new_builtin;
if let Some(ctx) = had_ctx {
g.set_context(ctx);
hot_debug!("[truce-gui-reload] context restored on new editor");
} else {
hot_debug!("[truce-gui-reload] WARNING: no context to restore!");
}
} else {
hot_debug!("[truce-gui-reload] ERROR: failed to lock inner mutex");
}
}
}
})
.ok();
Self {
kind: HotEditorInner::Builtin { gpu },
_watcher: watcher,
stop,
_sample: std::marker::PhantomData,
}
}
fn new_custom(editor: Box<dyn Editor>) -> Self {
Self {
kind: HotEditorInner::Custom { editor },
_watcher: None,
stop: Arc::new(AtomicBool::new(false)),
_sample: std::marker::PhantomData,
}
}
}
impl<P: Params + 'static, S: Sample> Editor for HotEditor<P, S> {
fn size(&self) -> (u32, u32) {
match &self.kind {
HotEditorInner::Builtin { gpu, .. } => gpu.size(),
HotEditorInner::Custom { editor } => editor.size(),
}
}
fn open(&mut self, parent: RawWindowHandle, context: PluginContext) {
match &mut self.kind {
HotEditorInner::Builtin { gpu, .. } => gpu.open(parent, context),
HotEditorInner::Custom { editor } => editor.open(parent, context),
}
}
fn close(&mut self) {
match &mut self.kind {
HotEditorInner::Builtin { gpu, .. } => gpu.close(),
HotEditorInner::Custom { editor } => editor.close(),
}
}
fn idle(&mut self) {
match &mut self.kind {
HotEditorInner::Builtin { gpu, .. } => gpu.idle(),
HotEditorInner::Custom { editor } => editor.idle(),
}
}
fn can_resize(&self) -> bool {
match &self.kind {
HotEditorInner::Builtin { gpu, .. } => gpu.can_resize(),
HotEditorInner::Custom { editor } => editor.can_resize(),
}
}
fn state_changed(&mut self) {
match &mut self.kind {
HotEditorInner::Builtin { gpu, .. } => gpu.state_changed(),
HotEditorInner::Custom { editor } => editor.state_changed(),
}
}
fn screenshot(&mut self, params: Arc<dyn truce_params::Params>) -> Option<(Vec<u8>, u32, u32)> {
match &mut self.kind {
HotEditorInner::Builtin { gpu, .. } => gpu.screenshot(params),
HotEditorInner::Custom { editor } => editor.screenshot(params),
}
}
}