From 66d7c04108a4f2de3dfe03e63c21acdc1be95a77 Mon Sep 17 00:00:00 2001 From: Horuse <39675195+Horuse@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:39:07 +0300 Subject: [PATCH 1/7] fix(plugins): stabilize VST3 editor lifecycle, compositing, and thread affinity --- src-tauri/src/audio/plugins/editor.rs | 43 +++---- src-tauri/src/audio/plugins/main_thread.rs | 36 +++++- src-tauri/src/audio/plugins/registry.rs | 13 ++- src-tauri/src/audio/plugins/vst3_backend.rs | 13 ++- src-tauri/src/audio/plugins/vst3_editor.rs | 100 +++++++++------- src-tauri/src/audio/plugins/vst3_host.rs | 48 ++++++-- src-tauri/src/audio/plugins/vst3_registry.rs | 114 +++++++++++++++---- src-tauri/src/commands.rs | 34 +++--- src-tauri/src/lib.rs | 1 + 9 files changed, 287 insertions(+), 115 deletions(-) diff --git a/src-tauri/src/audio/plugins/editor.rs b/src-tauri/src/audio/plugins/editor.rs index 99cbec07..c24fd4c8 100644 --- a/src-tauri/src/audio/plugins/editor.rs +++ b/src-tauri/src/audio/plugins/editor.rs @@ -36,8 +36,11 @@ pub fn window_for(node_id: &str) -> Option { /// Closes a node's editor window if one is open. Shared with the format hosts, /// which have to take the window down alongside the instance it belongs to. pub fn close_window(node_id: &str) { + if let Some(host) = super::registry::for_node(node_id) { + host.destroy_editor(node_id); + } if let Some(w) = windows().lock().unwrap().remove(node_id) { - let _ = w.close(); + let _ = w.destroy(); } } @@ -85,6 +88,10 @@ pub fn open(node_id: &str, title: &str) -> Result<(), String> { tracing::debug!(node_id, title, "opening plugin editor"); let app = crate::app_handle().ok_or("app handle not ready")?; if let Some(w) = windows().lock().unwrap().get(node_id) { + if let Some(host) = super::registry::for_node(node_id) { + let _ = host.embed_editor(node_id, w); + } + let _ = w.show(); let _ = w.set_focus(); return Ok(()); } @@ -100,14 +107,14 @@ pub fn open(node_id: &str, title: &str) -> Result<(), String> { let nid = node_id.to_string(); window.on_window_event(move |ev| { - // The plugin's view is a child of this window: tear the GUI down before - // the window goes away, and tell the FE node its editor button is stale. - if matches!(ev, tauri::WindowEvent::CloseRequested { .. }) { - // Already the main thread, which is where `destroy_editor` belongs. - if let Some(host) = super::registry::for_node(&nid) { - host.destroy_editor(&nid); + // The user closed the window: keep the native view parented to avoid + // COM/lifecycle destruction churn in plugins, just hide the window and + // notify the FE node that its editor is closed. + if let tauri::WindowEvent::CloseRequested { api, .. } = ev { + api.prevent_close(); + if let Some(w) = windows().lock().unwrap().get(&nid) { + let _ = w.hide(); } - windows().lock().unwrap().remove(&nid); if let Some(app) = crate::app_handle() { let _ = app.emit(super::host_api::EDITOR_CLOSED_EVENT, &nid); } @@ -139,21 +146,19 @@ pub fn open(node_id: &str, title: &str) -> Result<(), String> { tracing::debug!(node_id, width, height, "plugin editor embedded"); set_content_size(&window, width as f64, height as f64); + let _ = window.show(); + let _ = window.set_focus(); Ok(()) } -/// Tears down the plugin editor and closes its native window. +/// Hides the plugin editor window (or tears it down if requested). pub fn close(node_id: &str) -> Result<(), String> { - // The plugin's view is a child of this window, so it goes first -- and it - // goes on the main thread, which is the one place AppKit and every format - // agree on. - let nid = node_id.to_string(); - let _ = super::main_thread::run(move || { - if let Some(host) = super::registry::for_node(&nid) { - host.destroy_editor(&nid); - } - }); - close_window(node_id); + if let Some(w) = windows().lock().unwrap().get(node_id) { + let _ = w.hide(); + } + if let Some(app) = crate::app_handle() { + let _ = app.emit(super::host_api::EDITOR_CLOSED_EVENT, node_id); + } Ok(()) } diff --git a/src-tauri/src/audio/plugins/main_thread.rs b/src-tauri/src/audio/plugins/main_thread.rs index 09f98c4a..8f8d0af3 100644 --- a/src-tauri/src/audio/plugins/main_thread.rs +++ b/src-tauri/src/audio/plugins/main_thread.rs @@ -13,9 +13,41 @@ use std::time::Duration; /// timeout keeps the calling thread from hanging with it. const MAIN_THREAD_TIMEOUT: Duration = Duration::from_secs(5); -/// Runs `f` on the Tauri main thread and blocks for its result. Callers must -/// not be the main thread themselves, or this deadlocks. +use std::sync::OnceLock; +use std::thread::ThreadId; + +static MAIN_THREAD_ID: OnceLock = OnceLock::new(); + +/// Registers the current thread as the main thread. +pub fn register_main_thread() { + MAIN_THREAD_ID.get_or_init(|| std::thread::current().id()); +} + +#[cfg(target_os = "macos")] +extern "C" { + fn pthread_main_np() -> i32; +} + +/// Returns whether the caller is currently on the main thread. +pub fn is_main_thread() -> bool { + #[cfg(target_os = "macos")] + unsafe { + pthread_main_np() != 0 + } + #[cfg(not(target_os = "macos"))] + { + MAIN_THREAD_ID + .get() + .is_some_and(|&id| id == std::thread::current().id()) + } +} + +/// Runs `f` on the Tauri main thread and blocks for its result. If already on +/// the main thread, runs `f` directly to prevent deadlock. pub fn run(f: impl FnOnce() -> R + Send + 'static) -> Result { + if is_main_thread() { + return Ok(f()); + } let app = crate::app_handle().ok_or_else(|| "app handle not ready".to_string())?; let (tx, rx) = mpsc::channel(); app.run_on_main_thread(move || { diff --git a/src-tauri/src/audio/plugins/registry.rs b/src-tauri/src/audio/plugins/registry.rs index c8c64aa4..c0159b1c 100644 --- a/src-tauri/src/audio/plugins/registry.rs +++ b/src-tauri/src/audio/plugins/registry.rs @@ -58,8 +58,12 @@ pub fn activate(format: PluginFormat, req: ActivateRequest<'_>) -> Result) -> Result Vec { - let module = match Vst3Module::open(path) { + let p = path.to_path_buf(); + let open_res = if crate::audio::plugins::main_thread::is_main_thread() { + Vst3Module::open(&p) + } else if let Ok(res) = + crate::audio::plugins::main_thread::run(move || Vst3Module::open(&p)) + { + res + } else { + Vst3Module::open(path) + }; + + let module = match open_res { Ok(module) => module, Err(err) => { tracing::warn!("vst3: {err}"); diff --git a/src-tauri/src/audio/plugins/vst3_editor.rs b/src-tauri/src/audio/plugins/vst3_editor.rs index f2b6614b..9fee7686 100644 --- a/src-tauri/src/audio/plugins/vst3_editor.rs +++ b/src-tauri/src/audio/plugins/vst3_editor.rs @@ -8,8 +8,6 @@ use std::ffi::c_void; -use vst3::Steinberg::Vst::ViewType::kEditor; -use vst3::Steinberg::Vst::{IEditController, IEditControllerTrait}; use vst3::Steinberg::{ kResultOk, kResultTrue, tresult, FIDString, IPlugFrame, IPlugFrameTrait, IPlugView, IPlugViewTrait, ViewRect, @@ -117,24 +115,18 @@ pub struct EditorView { impl EditorView { /// Builds the plugin's view into `parent` -- an `NSView` on macOS, an `HWND` /// on Windows, an X11 window id on Linux -- and returns the size the plugin - /// asked for. `None` when the plugin has no editor at all, which is not an - /// error. + /// asked for. /// /// Must run on the main thread. pub fn attach( - controller: &ComPtr, + view: ComPtr, parent: *mut c_void, titlebar: f64, resize: ResizeRequest, - ) -> Result, String> { + ) -> Result<(Self, EditorSize), String> { // SAFETY: main thread, and `parent` is a live window handle owned by a // window that outlives this editor. unsafe { - let raw = controller.createView(kEditor); - let Some(view) = ComPtr::from_raw(raw) else { - return Ok(None); - }; - let platform = platform_type(); if view.isPlatformTypeSupported(platform) != kResultTrue { return Err(format!( @@ -143,15 +135,6 @@ impl EditorView { )); } - let frame = ComWrapper::new(PlugFrame { resize }); - let frame_ptr = frame - .as_com_ref::() - .map(|r| r.as_ptr()) - .ok_or("PlugFrame implements IPlugFrame")?; - // Before `attached`, so a plugin that resizes on open has somewhere - // to send the request. - view.setFrame(frame_ptr); - let mut rect = ViewRect { left: 0, top: 0, @@ -162,27 +145,75 @@ impl EditorView { return Err("editor reported no size".into()); } + #[cfg(target_os = "macos")] + { + use objc2::msg_send; + use objc2::runtime::AnyObject; + let parent_obj = parent as *mut AnyObject; + let _: () = msg_send![parent_obj, setWantsLayer: true]; + } + + let frame = ComWrapper::new(PlugFrame { resize }); + let frame_ptr = frame + .as_com_ref::() + .map(|r| r.as_ptr()) + .ok_or("PlugFrame implements IPlugFrame")?; + // Before `attached`, so a plugin that resizes on open has somewhere + // to send the request. + view.setFrame(frame_ptr); + if view.attached(parent, platform) != kResultOk { view.setFrame(std::ptr::null_mut()); return Err("editor refused to attach to the window".into()); } + // Inform the view of its initial size so it initializes layout and rendering. + let _ = view.onSize(&mut rect); + + // Notify the view that it has focus so timers and render loops start. + let _ = view.onFocus(1); + #[cfg(target_os = "macos")] - inset_below_titlebar(parent, titlebar); - #[cfg(not(target_os = "macos"))] + { + use objc2::msg_send; + use objc2::runtime::AnyObject; + let parent_obj = parent as *mut AnyObject; + let _: () = msg_send![parent_obj, setNeedsDisplay: true]; + if let Some(view) = editor::last_subview(parent) { + let _: () = msg_send![view, setNeedsDisplay: true]; + } + } let _ = titlebar; let size = ( (rect.right - rect.left).max(1) as u32, (rect.bottom - rect.top).max(1) as u32, ); - Ok(Some(( + Ok(( Self { view, _frame: frame, }, size, - ))) + )) + } + } + + pub fn on_focus(&self, state: bool) { + unsafe { + let _ = self.view.onFocus(if state { 1 } else { 0 }); + } + } + + pub fn on_size(&self, width: u32, height: u32) { + unsafe { + let mut rect = ViewRect { + left: 0, + top: 0, + right: width as i32, + bottom: height as i32, + }; + let _ = self.view.onSize(&mut rect); } } } @@ -192,23 +223,8 @@ impl Drop for EditorView { // Order matters: the plugin must let go of the parent view before the // window closes, and of our frame before it is freed. unsafe { - self.view.removed(); - self.view.setFrame(std::ptr::null_mut()); - } - } -} - -/// Lays the view the plugin just parented under the title bar rather than at -/// the window's bottom-left corner, where an unflipped `NSView` origin puts it. -/// Win32 and X11 children are placed from the top-left of the client area, so -/// they need no equivalent. -#[cfg(target_os = "macos")] -fn inset_below_titlebar(parent: *mut c_void, titlebar: f64) { - // SAFETY: `parent` is the window's content view, and the plugin has just - // added its own view as the last subview of it. - unsafe { - if let Some(view) = editor::last_subview(parent) { - editor::inset_below_titlebar(parent, view, titlebar); + let _ = self.view.removed(); + let _ = self.view.setFrame(std::ptr::null_mut()); } } } @@ -219,6 +235,8 @@ mod tests { use crate::audio::plugins::vst3_backend::{Vst3Backend, Vst3Module}; use crate::audio::plugins::vst3_host::Vst3Instance; use crate::audio::plugins::PluginBackend; + use vst3::Steinberg::Vst::IEditControllerTrait; + use vst3::Steinberg::Vst::ViewType::kEditor; fn skipped(what: &str) { println!("SKIPPED: no vst3 plugins installed, cannot check {what}"); diff --git a/src-tauri/src/audio/plugins/vst3_host.rs b/src-tauri/src/audio/plugins/vst3_host.rs index 4f27b56b..cbf2f428 100644 --- a/src-tauri/src/audio/plugins/vst3_host.rs +++ b/src-tauri/src/audio/plugins/vst3_host.rs @@ -29,6 +29,10 @@ pub struct Vst3Instance { separate: bool, /// Kept alive for the plugin, which holds only a borrowed reference to it. handler: Option>>>, + /// Cached editor view so has_editor doesn't create and immediately destroy it, + /// which would corrupt internal static state in plugins (such as JUCE LookAndFeel). + cached_view: Option>, + has_editor_cache: Option, /// The factory that made these lives in the module, so it outlives them. _module: Vst3Module, } @@ -118,6 +122,8 @@ impl Vst3Instance { controller, separate, handler: None, + cached_view: None, + has_editor_cache: None, _module: module, }) } @@ -232,14 +238,38 @@ impl Vst3Instance { /// Whether the plugin has an editor at all. Asked before offering the /// button, so the node can say "no editor" instead of opening a blank - /// window. - pub fn has_editor(&self) -> bool { + /// window. Caches the view so it is not created and immediately destroyed. + pub fn has_editor(&mut self) -> bool { + if let Some(has) = self.has_editor_cache { + return has; + } + use vst3::Steinberg::Vst::ViewType::kEditor; + if self.cached_view.is_some() { + self.has_editor_cache = Some(true); + return true; + } + unsafe { + if let Some(view) = + ComPtr::::from_raw(self.controller.createView(kEditor)) + { + self.cached_view = Some(view); + self.has_editor_cache = Some(true); + true + } else { + self.has_editor_cache = Some(false); + false + } + } + } + + /// Takes the cached editor view or creates a new one if not cached. + pub fn take_view(&mut self) -> Option> { use vst3::Steinberg::Vst::ViewType::kEditor; - // SAFETY: the view is created only to be counted and immediately - // released; it is never attached. + if let Some(view) = self.cached_view.take() { + return Some(view); + } unsafe { ComPtr::::from_raw(self.controller.createView(kEditor)) - .is_some() } } @@ -471,10 +501,12 @@ impl Drop for Vst3Instance { fn drop(&mut self) { use vst3::Steinberg::Vst::{IConnectionPoint, IConnectionPointTrait}; - // Unwind the setup in reverse: stop processing, deactivate, disconnect, - // then terminate each half. Terminating a running or still-connected - // plugin leaves the other side holding a reference to a dead object. unsafe { + self.cached_view = None; + if self.handler.is_some() { + self.controller.setComponentHandler(std::ptr::null_mut()); + self.handler = None; + } use vst3::Steinberg::Vst::{IAudioProcessor, IAudioProcessorTrait}; if let Some(processor) = self.component.cast::() { processor.setProcessing(0); diff --git a/src-tauri/src/audio/plugins/vst3_registry.rs b/src-tauri/src/audio/plugins/vst3_registry.rs index 94bd4323..cec0b33c 100644 --- a/src-tauri/src/audio/plugins/vst3_registry.rs +++ b/src-tauri/src/audio/plugins/vst3_registry.rs @@ -10,6 +10,8 @@ use std::collections::HashMap; use std::ffi::c_void; use std::sync::Arc; +use tauri::Emitter; + use super::host_api::{ alive_flag, tag_state, untag_state, ActivateRequest, AliveFlag, EditorSize, Graveyard, HostedNode, PluginHost, PluginParamInfo, PluginStatus, Unsupported, @@ -103,18 +105,44 @@ fn activate_on_main( // and let `tick_and_reclaim` free it once its `alive` flag is clear. let old = SLOTS.with(|s| { s.borrow_mut().insert( - node_id, + node_id.clone(), Slot { instance, alive, - path, - plugin_id, + path: path.clone(), + plugin_id: plugin_id.clone(), editor: None, }, ) }); if let Some(old) = old { + let same_plugin = old.path == path && old.plugin_id == plugin_id; GRAVEYARD.with(|g| g.borrow_mut().bury(old.instance, old.alive)); + + if same_plugin { + // Same plugin, pipeline rebuilt: re-attach to existing window! + if let Some(window) = editor::window_for(&node_id) { + SLOTS.with(|s| { + if let Some(slot) = s.borrow_mut().get_mut(&node_id) { + if let Ok(size) = attach_slot_editor(slot, &node_id, &window) { + let (width, height) = editor::valid_gui_size(size.0, size.1) + .unwrap_or(editor::FALLBACK_EDITOR_SIZE); + editor::set_content_size(&window, width as f64, height as f64); + if let Some(ref ed) = slot.editor { + ed.on_size(width, height); + } + } + } + }); + } + } else { + // Different plugin chosen on this node: close the previous editor + // window so the new one opens cleanly with its own UI and geometry. + editor::close_window(&node_id); + if let Some(app) = crate::app_handle() { + let _ = app.emit(super::host_api::EDITOR_CLOSED_EVENT, &node_id); + } + } } } else { // A metering duplicate has no editor and no parameters to answer for; @@ -124,6 +152,46 @@ fn activate_on_main( Ok(node) } +fn attach_slot_editor( + slot: &mut Slot, + node_id: &str, + window: &tauri::Window, +) -> Result { + let view_addr = parent_handle(window).map_err(|e| format!("vst3 {node_id}: {e}"))?; + let (_, titlebar) = editor::decoration_overhead(window); + let resize_target = window.clone(); + + // Cleanly drop any previous editor view before attaching the new one. + slot.editor = None; + + #[cfg(target_os = "macos")] + unsafe { + use objc2::msg_send; + use objc2::runtime::AnyObject; + let parent_obj = view_addr as *mut AnyObject; + let subviews: *mut AnyObject = msg_send![parent_obj, subviews]; + let count: usize = msg_send![subviews, count]; + for i in (0..count).rev() { + let sv: *mut AnyObject = msg_send![subviews, objectAtIndex: i]; + let _: () = msg_send![sv, removeFromSuperview]; + } + } + + let view = slot + .instance + .take_view() + .ok_or_else(|| format!("vst3 {node_id}: plugin has no editor"))?; + + let resize = Box::new(move |w: u32, h: u32| { + let _ = resize_target.set_size(tauri::LogicalSize::new(w as f64, h as f64)); + }); + let (view, size) = EditorView::attach(view, view_addr as *mut c_void, titlebar, resize) + .map_err(|e| format!("vst3 {node_id}: {e}"))?; + + slot.editor = Some(view); + Ok(size) +} + /// The window a plugin view is parented to, as the address VST3 expects for /// this platform's `attached` type. fn parent_handle(window: &tauri::Window) -> Result { @@ -234,12 +302,8 @@ impl PluginHost for Vst3Host { } fn embed_editor(&self, node_id: &str, window: &tauri::Window) -> Result { - // A raw pointer is not `Send`; the address is, and the window outlives - // the editor it hosts. - let view_addr = parent_handle(window).map_err(|e| format!("vst3 {node_id}: {e}"))?; - let (_, titlebar) = editor::decoration_overhead(window); let id = node_id.to_string(); - let resize_target = window.clone(); + let win = window.clone(); main_thread::run(move || { SLOTS.with(|slots| { @@ -248,22 +312,24 @@ impl PluginHost for Vst3Host { .get_mut(&id) .ok_or_else(|| format!("vst3 {id}: no plugin loaded"))?; - let resize = Box::new(move |w: u32, h: u32| { - let _ = resize_target.set_size(tauri::LogicalSize::new(w as f64, h as f64)); - }); - let attached = EditorView::attach( - &slot.instance.controller, - view_addr as *mut c_void, - titlebar, - resize, - ) - .map_err(|e| format!("vst3 {id}: {e}"))?; - - let Some((view, size)) = attached else { - return Err(format!("vst3 {id}: plugin has no editor")); - }; - slot.editor = Some(view); - Ok(size) + if let Some(ref editor) = slot.editor { + editor.on_focus(true); + #[cfg(target_os = "macos")] + unsafe { + use objc2::msg_send; + use objc2::runtime::AnyObject; + if let Ok(addr) = parent_handle(&win) { + let parent_obj = addr as *mut AnyObject; + let _: () = msg_send![parent_obj, setNeedsDisplay: true]; + if let Some(v) = editor::last_subview(addr as *mut std::ffi::c_void) { + let _: () = msg_send![v, setNeedsDisplay: true]; + } + } + } + return Ok(editor::FALLBACK_EDITOR_SIZE); + } + + attach_slot_editor(slot, &id, &win) }) })? } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index aec66f16..509b7756 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -57,11 +57,10 @@ pub async fn scan_plugins() -> AppResult AppResult<()> { let id = node_id.clone(); - let r = tauri::async_runtime::spawn_blocking(move || { + let r = crate::audio::plugins::main_thread::run(move || { crate::audio::plugins::editor::open(&id, &title) }) - .await - .map_err(|_| AppError::Plugin(format!("editor task for {node_id} failed")))? + .map_err(|e| AppError::Plugin(format!("editor task for {node_id} failed: {e}")))? .map_err(AppError::Plugin); if let Err(e) = &r { // The node id has to be re-supplied here: it was moved into the task. @@ -75,7 +74,7 @@ pub async fn open_plugin_editor(node_id: String, title: String) -> AppResult<()> #[tauri::command] pub async fn get_plugin_state(node_id: String) -> AppResult> { let id = node_id.clone(); - Ok(tauri::async_runtime::spawn_blocking(move || { + let res = crate::audio::plugins::main_thread::run(move || { // A format without state persistence reports it; the node then has // nothing to store, which is not the same as an empty state. crate::audio::plugins::registry::for_node(&id).and_then(|h| match h.save_state(&id) { @@ -89,27 +88,28 @@ pub async fn get_plugin_state(node_id: String) -> AppResult> { None } }) - }) - .await - .unwrap_or_else(|_| { - error!(node_id, "get_plugin_state task failed"); - None - })) + }); + match res { + Ok(state) => Ok(state), + Err(e) => { + error!(node_id, error = %e, "get_plugin_state task failed"); + Ok(None) + } + } } /// Automatable parameters of a running plugin for the node UI. Empty when the -/// plugin isn't running or exposes no parameters. +/// plugin is not running, does not advertise parameters, or on error. #[tauri::command] pub async fn get_plugin_params( node_id: String, ) -> AppResult> { let id = node_id.clone(); - Ok(tauri::async_runtime::spawn_blocking(move || { + Ok(crate::audio::plugins::main_thread::run(move || { crate::audio::plugins::registry::for_node(&id) .map(|h| h.params(&id)) .unwrap_or_default() }) - .await .unwrap_or_else(|_| { error!(node_id, "get_plugin_params task failed"); Vec::new() @@ -122,12 +122,11 @@ pub async fn get_plugin_params( #[tauri::command] pub async fn plugin_status(node_id: String) -> AppResult { let id = node_id.clone(); - Ok(tauri::async_runtime::spawn_blocking(move || { + Ok(crate::audio::plugins::main_thread::run(move || { crate::audio::plugins::registry::for_node(&id) .map(|h| h.status(&id)) .unwrap_or_default() }) - .await .unwrap_or_else(|_| { error!(node_id, "plugin_status task failed"); Default::default() @@ -162,9 +161,8 @@ pub fn debug_panic(app: AppHandle) { #[tauri::command] pub async fn close_plugin_editor(node_id: String) -> AppResult<()> { let id = node_id.clone(); - tauri::async_runtime::spawn_blocking(move || crate::audio::plugins::editor::close(&id)) - .await - .map_err(|_| AppError::Plugin(format!("editor task for {node_id} failed")))? + crate::audio::plugins::main_thread::run(move || crate::audio::plugins::editor::close(&id)) + .map_err(|e| AppError::Plugin(format!("editor close task for {node_id} failed: {e}")))? .map_err(AppError::Plugin) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dd90775e..46f6189c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -165,6 +165,7 @@ pub fn reinstall_panic_hook() { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { install_panic_hook(); + audio::plugins::main_thread::register_main_thread(); use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; From 5b2c97c8e3254a19a9f47519b3816b6756275729 Mon Sep 17 00:00:00 2001 From: Horuse <39675195+Horuse@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:08:14 +0300 Subject: [PATCH 2/7] fix(plugins): stabilize editor lifecycle and thread affinity across formats --- src-tauri/src/audio/plugins/au_host.rs | 28 +++++ src-tauri/src/audio/plugins/clap_host.rs | 22 ++++ src-tauri/src/audio/plugins/clap_registry.rs | 26 +++++ src-tauri/src/audio/plugins/editor.rs | 8 +- src-tauri/src/audio/plugins/host_api.rs | 6 + src-tauri/src/audio/plugins/vst3_backend.rs | 38 ++++--- src-tauri/src/audio/plugins/vst3_host.rs | 6 +- src-tauri/src/audio/plugins/vst3_registry.rs | 101 +++++++++++++---- src-tauri/src/commands.rs | 109 ++++++++++++------- 9 files changed, 270 insertions(+), 74 deletions(-) diff --git a/src-tauri/src/audio/plugins/au_host.rs b/src-tauri/src/audio/plugins/au_host.rs index 1b3b41f2..ea203548 100644 --- a/src-tauri/src/audio/plugins/au_host.rs +++ b/src-tauri/src/audio/plugins/au_host.rs @@ -317,6 +317,14 @@ fn activate( None }; if let Some(old) = retired { + if let Some(view) = old.view { + unsafe { drop_view(view) }; + editor::close_window(node_id); + if let Some(app) = crate::app_handle() { + use tauri::Emitter; + let _ = app.emit(super::host_api::EDITOR_CLOSED_EVENT, node_id); + } + } graveyard().lock().unwrap().bury(old.instance, old.alive); } Ok(node) @@ -1283,6 +1291,26 @@ impl PluginHost for AuHost { Ok((width as u32, height as u32)) } + fn show_editor(&self, node_id: &str) -> Result<(), String> { + let id = node_id.to_string(); + main_thread::run(move || { + use objc2::msg_send; + use objc2::runtime::AnyObject; + if let Some(slot) = instances().lock().unwrap().get(&id) { + if let Some(view) = slot.view { + unsafe { + let _: () = msg_send![view as *mut AnyObject, setNeedsDisplay: true]; + } + } + } + Ok(()) + })? + } + + fn hide_editor(&self, _node_id: &str) -> Result<(), String> { + Ok(()) + } + fn destroy_editor(&self, node_id: &str) { let Some(view) = instances() .lock() diff --git a/src-tauri/src/audio/plugins/clap_host.rs b/src-tauri/src/audio/plugins/clap_host.rs index ce31110e..8a7f82a4 100644 --- a/src-tauri/src/audio/plugins/clap_host.rs +++ b/src-tauri/src/audio/plugins/clap_host.rs @@ -517,6 +517,28 @@ impl ClapInstance { } } + /// Shows an already-created editor when its window is unhidden. + pub fn show_editor(&mut self) -> Result<(), String> { + if !self.gui_open { + return Ok(()); + } + if let Some(gui) = self.instance.plugin_handle().get_extension::() { + let _ = gui.show(&mut self.instance.plugin_handle()); + } + Ok(()) + } + + /// Hides the editor when its window is hidden. + pub fn hide_editor(&mut self) -> Result<(), String> { + if !self.gui_open { + return Ok(()); + } + if let Some(gui) = self.instance.plugin_handle().get_extension::() { + let _ = gui.hide(&mut self.instance.plugin_handle()); + } + Ok(()) + } + /// Tears the editor down. Destroying a GUI that was never created is /// undefined per the CLAP spec, hence the flag. pub fn destroy_editor(&mut self) { diff --git a/src-tauri/src/audio/plugins/clap_registry.rs b/src-tauri/src/audio/plugins/clap_registry.rs index 9a64499a..2115b9b8 100644 --- a/src-tauri/src/audio/plugins/clap_registry.rs +++ b/src-tauri/src/audio/plugins/clap_registry.rs @@ -182,6 +182,32 @@ impl PluginHost for ClapHost { })? } + fn show_editor(&self, node_id: &str) -> Result<(), String> { + let id = node_id.to_string(); + main_thread::run(move || { + SLOTS.with(|slots| { + if let Some(slot) = slots.borrow_mut().get_mut(&id) { + slot.instance.show_editor() + } else { + Ok(()) + } + }) + })? + } + + fn hide_editor(&self, node_id: &str) -> Result<(), String> { + let id = node_id.to_string(); + main_thread::run(move || { + SLOTS.with(|slots| { + if let Some(slot) = slots.borrow_mut().get_mut(&id) { + slot.instance.hide_editor() + } else { + Ok(()) + } + }) + })? + } + /// Already on the main thread by contract, so the view is dropped here /// rather than marshalled: the window's own close handler calls this, and /// marshalling would deadlock. diff --git a/src-tauri/src/audio/plugins/editor.rs b/src-tauri/src/audio/plugins/editor.rs index c24fd4c8..651ed2c2 100644 --- a/src-tauri/src/audio/plugins/editor.rs +++ b/src-tauri/src/audio/plugins/editor.rs @@ -89,7 +89,7 @@ pub fn open(node_id: &str, title: &str) -> Result<(), String> { let app = crate::app_handle().ok_or("app handle not ready")?; if let Some(w) = windows().lock().unwrap().get(node_id) { if let Some(host) = super::registry::for_node(node_id) { - let _ = host.embed_editor(node_id, w); + let _ = host.show_editor(node_id); } let _ = w.show(); let _ = w.set_focus(); @@ -112,6 +112,9 @@ pub fn open(node_id: &str, title: &str) -> Result<(), String> { // notify the FE node that its editor is closed. if let tauri::WindowEvent::CloseRequested { api, .. } = ev { api.prevent_close(); + if let Some(host) = super::registry::for_node(&nid) { + let _ = host.hide_editor(&nid); + } if let Some(w) = windows().lock().unwrap().get(&nid) { let _ = w.hide(); } @@ -153,6 +156,9 @@ pub fn open(node_id: &str, title: &str) -> Result<(), String> { /// Hides the plugin editor window (or tears it down if requested). pub fn close(node_id: &str) -> Result<(), String> { + if let Some(host) = super::registry::for_node(node_id) { + let _ = host.hide_editor(node_id); + } if let Some(w) = windows().lock().unwrap().get(node_id) { let _ = w.hide(); } diff --git a/src-tauri/src/audio/plugins/host_api.rs b/src-tauri/src/audio/plugins/host_api.rs index 57904d23..b7cf159d 100644 --- a/src-tauri/src/audio/plugins/host_api.rs +++ b/src-tauri/src/audio/plugins/host_api.rs @@ -336,6 +336,12 @@ pub trait PluginHost: Sync { /// the caller can fit the window to it. fn embed_editor(&self, node_id: &str, window: &tauri::Window) -> Result; + /// Notifies the plugin that its editor window was shown (unhidden) or focused. + fn show_editor(&self, node_id: &str) -> Result<(), String>; + + /// Notifies the plugin that its editor window was hidden. + fn hide_editor(&self, node_id: &str) -> Result<(), String>; + /// Tears the view down. Must run before the host window closes, since the /// plugin's view is a child of it. /// diff --git a/src-tauri/src/audio/plugins/vst3_backend.rs b/src-tauri/src/audio/plugins/vst3_backend.rs index 9ddcad54..0ba4b988 100644 --- a/src-tauri/src/audio/plugins/vst3_backend.rs +++ b/src-tauri/src/audio/plugins/vst3_backend.rs @@ -66,24 +66,32 @@ impl PluginBackend for Vst3Backend { fn scan_bundle(&self, path: &Path) -> Vec { let p = path.to_path_buf(); - let open_res = if crate::audio::plugins::main_thread::is_main_thread() { - Vst3Module::open(&p) - } else if let Ok(res) = - crate::audio::plugins::main_thread::run(move || Vst3Module::open(&p)) - { - res - } else { - Vst3Module::open(path) + let scan_op = move || { + let module = match Vst3Module::open(&p) { + Ok(module) => module, + Err(err) => { + tracing::warn!("vst3: {err}"); + return Vec::new(); + } + }; + let descriptors = module.descriptors(); + // Drop module here on the main thread so ExitDll/bundleExit and dlclose/FreeLibrary + // execute on Thread 0. + drop(module); + descriptors }; - let module = match open_res { - Ok(module) => module, - Err(err) => { - tracing::warn!("vst3: {err}"); - return Vec::new(); + if crate::audio::plugins::main_thread::is_main_thread() || crate::app_handle().is_none() { + scan_op() + } else { + match crate::audio::plugins::main_thread::run(scan_op) { + Ok(descriptors) => descriptors, + Err(err) => { + tracing::warn!("vst3 {}: main thread scan failed: {err}", path.display()); + Vec::new() + } } - }; - module.descriptors() + } } } diff --git a/src-tauri/src/audio/plugins/vst3_host.rs b/src-tauri/src/audio/plugins/vst3_host.rs index cbf2f428..a21a9333 100644 --- a/src-tauri/src/audio/plugins/vst3_host.rs +++ b/src-tauri/src/audio/plugins/vst3_host.rs @@ -266,10 +266,14 @@ impl Vst3Instance { pub fn take_view(&mut self) -> Option> { use vst3::Steinberg::Vst::ViewType::kEditor; if let Some(view) = self.cached_view.take() { + self.has_editor_cache = Some(true); return Some(view); } unsafe { - ComPtr::::from_raw(self.controller.createView(kEditor)) + let view = + ComPtr::::from_raw(self.controller.createView(kEditor)); + self.has_editor_cache = Some(view.is_some()); + view } } diff --git a/src-tauri/src/audio/plugins/vst3_registry.rs b/src-tauri/src/audio/plugins/vst3_registry.rs index cec0b33c..da16b39f 100644 --- a/src-tauri/src/audio/plugins/vst3_registry.rs +++ b/src-tauri/src/audio/plugins/vst3_registry.rs @@ -115,29 +115,51 @@ fn activate_on_main( }, ) }); - if let Some(old) = old { + if let Some(mut old) = old { let same_plugin = old.path == path && old.plugin_id == plugin_id; GRAVEYARD.with(|g| g.borrow_mut().bury(old.instance, old.alive)); if same_plugin { // Same plugin, pipeline rebuilt: re-attach to existing window! if let Some(window) = editor::window_for(&node_id) { - SLOTS.with(|s| { + // IMPORTANT: Drop the old editor view first so its removed() and + // peer teardown happen BEFORE the new view calls attached()! + old.editor = None; + + let attached = SLOTS.with(|s| { if let Some(slot) = s.borrow_mut().get_mut(&node_id) { - if let Ok(size) = attach_slot_editor(slot, &node_id, &window) { - let (width, height) = editor::valid_gui_size(size.0, size.1) - .unwrap_or(editor::FALLBACK_EDITOR_SIZE); - editor::set_content_size(&window, width as f64, height as f64); - if let Some(ref ed) = slot.editor { - ed.on_size(width, height); + attach_slot_editor(slot, &node_id, &window) + } else { + Err("slot missing".into()) + } + }); + + match attached { + Ok(size) => { + let (width, height) = editor::valid_gui_size(size.0, size.1) + .unwrap_or(editor::FALLBACK_EDITOR_SIZE); + editor::set_content_size(&window, width as f64, height as f64); + SLOTS.with(|s| { + if let Some(slot) = s.borrow().get(&node_id) { + if let Some(ref ed) = slot.editor { + ed.on_size(width, height); + } } + }); + } + Err(e) => { + tracing::warn!(node_id, error = %e, "failed to re-attach VST3 editor on rebuild; closing window"); + editor::close_window(&node_id); + if let Some(app) = crate::app_handle() { + let _ = app.emit(super::host_api::EDITOR_CLOSED_EVENT, &node_id); } } - }); + } } } else { // Different plugin chosen on this node: close the previous editor // window so the new one opens cleanly with its own UI and geometry. + old.editor = None; editor::close_window(&node_id); if let Some(app) = crate::app_handle() { let _ = app.emit(super::host_api::EDITOR_CLOSED_EVENT, &node_id); @@ -312,24 +334,63 @@ impl PluginHost for Vst3Host { .get_mut(&id) .ok_or_else(|| format!("vst3 {id}: no plugin loaded"))?; + attach_slot_editor(slot, &id, &win) + }) + })? + } + + fn show_editor(&self, node_id: &str) -> Result<(), String> { + let id = node_id.to_string(); + main_thread::run(move || { + SLOTS.with(|slots| { + let slots = slots.borrow(); + let Some(slot) = slots.get(&id) else { + return Ok(()); + }; if let Some(ref editor) = slot.editor { editor.on_focus(true); - #[cfg(target_os = "macos")] - unsafe { - use objc2::msg_send; - use objc2::runtime::AnyObject; - if let Ok(addr) = parent_handle(&win) { - let parent_obj = addr as *mut AnyObject; - let _: () = msg_send![parent_obj, setNeedsDisplay: true]; - if let Some(v) = editor::last_subview(addr as *mut std::ffi::c_void) { - let _: () = msg_send![v, setNeedsDisplay: true]; + if let Some(window) = editor::window_for(&id) { + #[cfg(target_os = "macos")] + unsafe { + use objc2::msg_send; + use objc2::runtime::AnyObject; + if let Ok(addr) = parent_handle(&window) { + let parent_obj = addr as *mut AnyObject; + let _: () = msg_send![parent_obj, setNeedsDisplay: true]; + if let Some(v) = editor::last_subview(addr as *mut std::ffi::c_void) + { + let _: () = msg_send![v, setNeedsDisplay: true]; + } + } + } + #[cfg(target_os = "windows")] + unsafe { + use windows::Win32::Foundation::HWND; + use windows::Win32::Graphics::Gdi::{InvalidateRect, UpdateWindow}; + if let Ok(addr) = parent_handle(&window) { + let hwnd = HWND(addr as _); + let _ = InvalidateRect(hwnd, None, true); + let _ = UpdateWindow(hwnd); } } } - return Ok(editor::FALLBACK_EDITOR_SIZE); } + Ok(()) + }) + })? + } - attach_slot_editor(slot, &id, &win) + fn hide_editor(&self, node_id: &str) -> Result<(), String> { + let id = node_id.to_string(); + main_thread::run(move || { + SLOTS.with(|slots| { + let slots = slots.borrow(); + if let Some(slot) = slots.get(&id) { + if let Some(ref editor) = slot.editor { + editor.on_focus(false); + } + } + Ok(()) }) })? } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 509b7756..10c4cd41 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -57,9 +57,13 @@ pub async fn scan_plugins() -> AppResult AppResult<()> { let id = node_id.clone(); - let r = crate::audio::plugins::main_thread::run(move || { - crate::audio::plugins::editor::open(&id, &title) + let r = tauri::async_runtime::spawn_blocking(move || { + crate::audio::plugins::main_thread::run(move || { + crate::audio::plugins::editor::open(&id, &title) + }) }) + .await + .map_err(|_| AppError::Plugin(format!("editor task for {node_id} failed")))? .map_err(|e| AppError::Plugin(format!("editor task for {node_id} failed: {e}")))? .map_err(AppError::Plugin); if let Err(e) = &r { @@ -74,27 +78,34 @@ pub async fn open_plugin_editor(node_id: String, title: String) -> AppResult<()> #[tauri::command] pub async fn get_plugin_state(node_id: String) -> AppResult> { let id = node_id.clone(); - let res = crate::audio::plugins::main_thread::run(move || { - // A format without state persistence reports it; the node then has - // nothing to store, which is not the same as an empty state. - crate::audio::plugins::registry::for_node(&id).and_then(|h| match h.save_state(&id) { - Ok(state) => state, - Err(unsupported) => { - tracing::debug!( - ?unsupported.format, - capability = unsupported.capability, - "plugin state not persisted" - ); - None - } + let res = tauri::async_runtime::spawn_blocking(move || { + crate::audio::plugins::main_thread::run(move || { + // A format without state persistence reports it; the node then has + // nothing to store, which is not the same as an empty state. + crate::audio::plugins::registry::for_node(&id).and_then(|h| match h.save_state(&id) { + Ok(state) => state, + Err(unsupported) => { + tracing::debug!( + ?unsupported.format, + capability = unsupported.capability, + "plugin state not persisted" + ); + None + } + }) }) - }); + }) + .await; match res { - Ok(state) => Ok(state), - Err(e) => { + Ok(Ok(state)) => Ok(state), + Ok(Err(e)) => { error!(node_id, error = %e, "get_plugin_state task failed"); Ok(None) } + Err(_) => { + error!(node_id, "get_plugin_state worker failed"); + Ok(None) + } } } @@ -105,15 +116,25 @@ pub async fn get_plugin_params( node_id: String, ) -> AppResult> { let id = node_id.clone(); - Ok(crate::audio::plugins::main_thread::run(move || { - crate::audio::plugins::registry::for_node(&id) - .map(|h| h.params(&id)) - .unwrap_or_default() + let res = tauri::async_runtime::spawn_blocking(move || { + crate::audio::plugins::main_thread::run(move || { + crate::audio::plugins::registry::for_node(&id) + .map(|h| h.params(&id)) + .unwrap_or_default() + }) }) - .unwrap_or_else(|_| { - error!(node_id, "get_plugin_params task failed"); - Vec::new() - })) + .await; + match res { + Ok(Ok(params)) => Ok(params), + Ok(Err(e)) => { + error!(node_id, error = %e, "get_plugin_params task failed"); + Ok(Vec::new()) + } + Err(_) => { + error!(node_id, "get_plugin_params worker failed"); + Ok(Vec::new()) + } + } } /// Which plugin a node is actually running and whether it can show an editor. @@ -122,15 +143,25 @@ pub async fn get_plugin_params( #[tauri::command] pub async fn plugin_status(node_id: String) -> AppResult { let id = node_id.clone(); - Ok(crate::audio::plugins::main_thread::run(move || { - crate::audio::plugins::registry::for_node(&id) - .map(|h| h.status(&id)) - .unwrap_or_default() + let res = tauri::async_runtime::spawn_blocking(move || { + crate::audio::plugins::main_thread::run(move || { + crate::audio::plugins::registry::for_node(&id) + .map(|h| h.status(&id)) + .unwrap_or_default() + }) }) - .unwrap_or_else(|_| { - error!(node_id, "plugin_status task failed"); - Default::default() - })) + .await; + match res { + Ok(Ok(status)) => Ok(status), + Ok(Err(e)) => { + error!(node_id, error = %e, "plugin_status task failed"); + Ok(Default::default()) + } + Err(_) => { + error!(node_id, "plugin_status worker failed"); + Ok(Default::default()) + } + } } /// Persisted crash reports from previous runs, cleared as they are read. @@ -161,9 +192,13 @@ pub fn debug_panic(app: AppHandle) { #[tauri::command] pub async fn close_plugin_editor(node_id: String) -> AppResult<()> { let id = node_id.clone(); - crate::audio::plugins::main_thread::run(move || crate::audio::plugins::editor::close(&id)) - .map_err(|e| AppError::Plugin(format!("editor close task for {node_id} failed: {e}")))? - .map_err(AppError::Plugin) + tauri::async_runtime::spawn_blocking(move || { + crate::audio::plugins::main_thread::run(move || crate::audio::plugins::editor::close(&id)) + }) + .await + .map_err(|_| AppError::Plugin(format!("editor close task for {node_id} failed")))? + .map_err(|e| AppError::Plugin(format!("editor close task for {node_id} failed: {e}")))? + .map_err(AppError::Plugin) } #[tauri::command] From b0554919d2086254e805463ad115f1d25b8c2493 Mon Sep 17 00:00:00 2001 From: Horuse <39675195+Horuse@users.noreply.github.com> Date: Fri, 4 Sep 2026 22:34:29 +0300 Subject: [PATCH 3/7] fix(plugins): prevent deadlock on editor reopen and defer close --- src-tauri/src/audio/plugins/editor.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/audio/plugins/editor.rs b/src-tauri/src/audio/plugins/editor.rs index 651ed2c2..24e622a3 100644 --- a/src-tauri/src/audio/plugins/editor.rs +++ b/src-tauri/src/audio/plugins/editor.rs @@ -87,7 +87,8 @@ pub fn set_content_size(window: &tauri::Window, w: f64, h: f64) { pub fn open(node_id: &str, title: &str) -> Result<(), String> { tracing::debug!(node_id, title, "opening plugin editor"); let app = crate::app_handle().ok_or("app handle not ready")?; - if let Some(w) = windows().lock().unwrap().get(node_id) { + let existing = windows().lock().unwrap().get(node_id).cloned(); + if let Some(w) = existing { if let Some(host) = super::registry::for_node(node_id) { let _ = host.show_editor(node_id); } @@ -112,15 +113,12 @@ pub fn open(node_id: &str, title: &str) -> Result<(), String> { // notify the FE node that its editor is closed. if let tauri::WindowEvent::CloseRequested { api, .. } = ev { api.prevent_close(); - if let Some(host) = super::registry::for_node(&nid) { - let _ = host.hide_editor(&nid); - } - if let Some(w) = windows().lock().unwrap().get(&nid) { - let _ = w.hide(); - } - if let Some(app) = crate::app_handle() { - let _ = app.emit(super::host_api::EDITOR_CLOSED_EVENT, &nid); - } + let nid = nid.clone(); + tauri::async_runtime::spawn(async move { + let _ = crate::audio::plugins::main_thread::run(move || { + let _ = close(&nid); + }); + }); } }); windows() @@ -159,7 +157,8 @@ pub fn close(node_id: &str) -> Result<(), String> { if let Some(host) = super::registry::for_node(node_id) { let _ = host.hide_editor(node_id); } - if let Some(w) = windows().lock().unwrap().get(node_id) { + let existing = windows().lock().unwrap().get(node_id).cloned(); + if let Some(w) = existing { let _ = w.hide(); } if let Some(app) = crate::app_handle() { From 9836a7fb1099f15a1a65b23b04ce3d6c3f0f5ed9 Mon Sep 17 00:00:00 2001 From: Horuse <39675195+Horuse@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:35:49 +0300 Subject: [PATCH 4/7] fix(plugins): wrap hwnd in Some for windows InvalidateRect --- src-tauri/src/audio/plugins/vst3_registry.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/audio/plugins/vst3_registry.rs b/src-tauri/src/audio/plugins/vst3_registry.rs index da16b39f..6f55b00b 100644 --- a/src-tauri/src/audio/plugins/vst3_registry.rs +++ b/src-tauri/src/audio/plugins/vst3_registry.rs @@ -369,7 +369,7 @@ impl PluginHost for Vst3Host { use windows::Win32::Graphics::Gdi::{InvalidateRect, UpdateWindow}; if let Ok(addr) = parent_handle(&window) { let hwnd = HWND(addr as _); - let _ = InvalidateRect(hwnd, None, true); + let _ = InvalidateRect(Some(hwnd), None, true); let _ = UpdateWindow(hwnd); } } From 31e5801fa776a848a7df7bde02c022281841f6a2 Mon Sep 17 00:00:00 2001 From: Horuse <39675195+Horuse@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:08:58 +0300 Subject: [PATCH 5/7] fix(plugins): marshal AU activate to main thread and support flexible VST3 editor sizing --- src-tauri/src/audio/plugins/au_host.rs | 62 ++++++++++++++++------ src-tauri/src/audio/plugins/vst3_editor.rs | 60 +++++++++++++++++---- 2 files changed, 96 insertions(+), 26 deletions(-) diff --git a/src-tauri/src/audio/plugins/au_host.rs b/src-tauri/src/audio/plugins/au_host.rs index ea203548..3c905ea1 100644 --- a/src-tauri/src/audio/plugins/au_host.rs +++ b/src-tauri/src/audio/plugins/au_host.rs @@ -1225,21 +1225,45 @@ pub struct AuHost; impl PluginHost for AuHost { fn activate(&self, req: ActivateRequest<'_>) -> Result { - activate( - req.node_id, - req.path, + main_thread::ensure_ticker(); + let (node_id, path) = (req.node_id.to_string(), req.path.to_string()); + let state = req.state.map(str::to_string); + let (sample_rate, max_frames, channels, primary, params) = ( req.sample_rate, req.max_frames, req.channels, - req.state, req.primary, req.params, - ) - .map(HostedNode::Au) + ); + + let act = move || { + activate( + &node_id, + &path, + sample_rate, + max_frames, + channels, + state.as_deref(), + primary, + params, + ) + .map(HostedNode::Au) + }; + + if main_thread::is_main_thread() { + act() + } else { + main_thread::run(act).map_err(|e| format!("main thread error: {e}"))? + } } fn forget(&self, node_id: &str) { - forget(node_id); + let id = node_id.to_string(); + if main_thread::is_main_thread() { + forget(&id); + } else { + let _ = main_thread::run(move || forget(&id)); + } } fn status(&self, node_id: &str) -> PluginStatus { @@ -1312,15 +1336,23 @@ impl PluginHost for AuHost { } fn destroy_editor(&self, node_id: &str) { - let Some(view) = instances() - .lock() - .unwrap() - .get_mut(node_id) - .and_then(|s| s.view.take()) - else { - return; + let id = node_id.to_string(); + let destroy = move || { + let Some(view) = instances() + .lock() + .unwrap() + .get_mut(&id) + .and_then(|s| s.view.take()) + else { + return; + }; + unsafe { drop_view(view) }; }; - unsafe { drop_view(view) }; + if main_thread::is_main_thread() { + destroy(); + } else { + let _ = main_thread::run(destroy); + } } /// Frees units whose RT node has left the graph. The host holds a reference diff --git a/src-tauri/src/audio/plugins/vst3_editor.rs b/src-tauri/src/audio/plugins/vst3_editor.rs index 9fee7686..3db05ae9 100644 --- a/src-tauri/src/audio/plugins/vst3_editor.rs +++ b/src-tauri/src/audio/plugins/vst3_editor.rs @@ -135,16 +135,6 @@ impl EditorView { )); } - let mut rect = ViewRect { - left: 0, - top: 0, - right: 0, - bottom: 0, - }; - if view.getSize(&mut rect) != kResultOk { - return Err("editor reported no size".into()); - } - #[cfg(target_os = "macos")] { use objc2::msg_send; @@ -159,14 +149,62 @@ impl EditorView { .map(|r| r.as_ptr()) .ok_or("PlugFrame implements IPlugFrame")?; // Before `attached`, so a plugin that resizes on open has somewhere - // to send the request. + // to send the request, and plugins that inspect frame metrics during + // size calculations have an initialized frame pointer. view.setFrame(frame_ptr); + let mut rect = ViewRect { + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + + // Query initial size before attached (succeeds for most plugins) + let mut got_size = view.getSize(&mut rect) == kResultOk + && (rect.right > rect.left && rect.bottom > rect.top); + if view.attached(parent, platform) != kResultOk { view.setFrame(std::ptr::null_mut()); return Err("editor refused to attach to the window".into()); } + // Many plugins (e.g. Native Instruments) only report valid size + // after attached() has parented the view hierarchy. + if !got_size { + if view.getSize(&mut rect) == kResultOk + && (rect.right > rect.left && rect.bottom > rect.top) + { + got_size = true; + } + } + + // If the plugin still hasn't reported a valid size via getSize(), + // measure the native subview that the plugin attached to the parent. + #[cfg(target_os = "macos")] + if !got_size { + if let Some(subview) = editor::last_subview(parent) { + use objc2::msg_send; + use objc2_foundation::NSRect; + let f: NSRect = msg_send![subview, frame]; + if f.size.width > 0.0 && f.size.height > 0.0 { + rect.left = 0; + rect.top = 0; + rect.right = f.size.width.round() as i32; + rect.bottom = f.size.height.round() as i32; + got_size = true; + } + } + } + + // Fallback default size (800x600) so the editor window still opens and renders. + if !got_size || rect.right <= rect.left || rect.bottom <= rect.top { + rect.left = 0; + rect.top = 0; + rect.right = 800; + rect.bottom = 600; + } + // Inform the view of its initial size so it initializes layout and rendering. let _ = view.onSize(&mut rect); From 661ec40b1a11e9b7958fbda7aa25c59f038d1300 Mon Sep 17 00:00:00 2001 From: Horuse <39675195+Horuse@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:25:23 +0300 Subject: [PATCH 6/7] fix(plugins): handle AU internal resizing and auto-fill for Apple system views --- src-tauri/src/audio/plugins/au_host.rs | 148 ++++++++++++++++--- src-tauri/src/audio/plugins/clap_host.rs | 2 +- src-tauri/src/audio/plugins/editor.rs | 144 +++++++++--------- src-tauri/src/audio/plugins/vst3_editor.rs | 28 ++-- src-tauri/src/audio/plugins/vst3_registry.rs | 3 +- 5 files changed, 220 insertions(+), 105 deletions(-) diff --git a/src-tauri/src/audio/plugins/au_host.rs b/src-tauri/src/audio/plugins/au_host.rs index 3c905ea1..2074f2a4 100644 --- a/src-tauri/src/audio/plugins/au_host.rs +++ b/src-tauri/src/audio/plugins/au_host.rs @@ -102,6 +102,54 @@ impl Drop for AuInstance { } } +use objc2::{AnyThread, DefinedClass}; + +objc2::define_class!( + #[unsafe(super(objc2::runtime::NSObject))] + #[name = "SplitwaveAuFrameObserver"] + #[ivars = String] + struct AuFrameObserver; + + impl AuFrameObserver { + #[unsafe(method(onFrameChanged:))] + unsafe fn on_frame_changed(&self, notification: *mut objc2::runtime::AnyObject) { + let node_id = self.ivars(); + if let Some(win) = editor::window_for(node_id) { + let view: *mut objc2::runtime::AnyObject = objc2::msg_send![notification, object]; + if !view.is_null() { + let frame: objc2_foundation::NSRect = objc2::msg_send![view, frame]; + let w = frame.size.width.round() as u32; + let h = frame.size.height.round() as u32; + if let Some((valid_w, valid_h)) = editor::valid_gui_size(w, h) { + editor::request_resize(&win, node_id, valid_w, valid_h); + if frame.origin.x != 0.0 || frame.origin.y != 0.0 { + let _: () = objc2::msg_send![ + view, + setFrameOrigin: objc2_foundation::NSPoint::new(0.0, 0.0) + ]; + } + } + } + } + } + } +); + +unsafe impl Send for AuFrameObserver {} +unsafe impl Sync for AuFrameObserver {} + +impl Drop for AuFrameObserver { + fn drop(&mut self) { + unsafe { + if let Some(center_class) = objc2::runtime::AnyClass::get(c"NSNotificationCenter") { + let center: *mut objc2::runtime::AnyObject = + objc2::msg_send![center_class, defaultCenter]; + let _: () = objc2::msg_send![center, removeObserver: &*self]; + } + } + } +} + /// The editor/parameter target for a node, held by the host rather than by the /// RT node alone. Holding it here is what guarantees the registry drops the /// last reference, and therefore that the unit is disposed on the main thread. @@ -112,6 +160,8 @@ struct AuSlot { alive: AliveFlag, /// The plugin's Cocoa view while its editor is open. Main thread only. view: Option, + /// Frame observer for internal plugin resizes (e.g. TDR Nova, BattleFX). + observer: Option>, } fn instances() -> &'static Mutex> { @@ -305,6 +355,7 @@ fn activate( instance: node.instance.clone(), alive, view: None, + observer: None, }, ) } else { @@ -726,11 +777,11 @@ fn param_name(info: &AudioUnitParameterInfo) -> String { pub(super) fn embed_editor( node_id: &str, parent_view: *mut c_void, - titlebar: f64, + _titlebar: f64, ) -> Result<(f64, f64), String> { use objc2::msg_send; - use objc2::runtime::AnyObject; - use objc2_foundation::{NSRect, NSSize}; + use objc2::runtime::{AnyClass, AnyObject}; + use objc2_foundation::{NSPoint, NSRect, NSSize, NSString}; let view = create_view(node_id)?; @@ -741,17 +792,81 @@ pub(super) fn embed_editor( // Apple's generic views build their content lazily, so the frame is // still degenerate right after `addSubview`. let _: () = msg_send![view, layoutSubtreeIfNeeded]; - let measured = (msg_send![view, frame], msg_send![view, fittingSize]); - editor::inset_below_titlebar(parent_view, view, titlebar); - measured + (msg_send![view, frame], msg_send![view, fittingSize]) + }; + + let is_apple = instances() + .lock() + .unwrap() + .get(node_id) + .map(|s| s.instance.url.ends_with("/appl")) + .unwrap_or(false); + + if is_apple { + let bounds: NSRect = unsafe { msg_send![parent_view as *mut AnyObject, bounds] }; + let frame = NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(bounds.size.width, bounds.size.height), + ); + unsafe { + let _: () = msg_send![view, setFrame: frame]; + let _: () = msg_send![ + view, + setAutoresizingMask: (1 << 1) | (1 << 4) // NS_VIEW_WIDTH_SIZABLE | NS_VIEW_HEIGHT_SIZABLE + ]; + let _: () = msg_send![view, setNeedsDisplay: true]; + } + if let Some(slot) = instances().lock().unwrap().get_mut(node_id) { + slot.view = Some(view as usize); + slot.observer = None; + } + return Ok((bounds.size.width, bounds.size.height)); + } + + let measured_w = frame.size.width.max(fitting.width); + let measured_h = frame.size.height.max(fitting.height); + + let (width, height) = if let Some((w, h)) = + editor::valid_gui_size(measured_w.round() as u32, measured_h.round() as u32) + { + (w as f64, h as f64) + } else { + ( + editor::FALLBACK_EDITOR_SIZE.0 as f64, + editor::FALLBACK_EDITOR_SIZE.1 as f64, + ) }; + let observer = AuFrameObserver::alloc().set_ivars(node_id.to_string()); + let observer: objc2::rc::Retained = + unsafe { msg_send![super(observer), init] }; + + unsafe { + let initial_frame = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(width, height)); + let _: () = msg_send![view, setFrame: initial_frame]; + let _: () = msg_send![view, setPostsFrameChangedNotifications: true]; + + if let Some(center_class) = AnyClass::get(c"NSNotificationCenter") { + let center: *mut AnyObject = msg_send![center_class, defaultCenter]; + let notif_name = NSString::from_str("NSViewFrameDidChangeNotification"); + let _: () = msg_send![ + center, + addObserver: &*observer, + selector: objc2::sel!(onFrameChanged:), + name: &*notif_name, + object: view + ]; + } + } + tracing::debug!( node_id, frame_w = frame.size.width, frame_h = frame.size.height, fitting_w = fitting.width, fitting_h = fitting.height, + width, + height, "au editor view measured" ); // A view whose content is laid out by constraints reports its real content @@ -760,11 +875,9 @@ pub(super) fn embed_editor( // is the one rule that sizes each kind correctly. if let Some(slot) = instances().lock().unwrap().get_mut(node_id) { slot.view = Some(view as usize); + slot.observer = Some(observer); } - let size = ( - frame.size.width.max(fitting.width), - frame.size.height.max(fitting.height), - ); + let size = (width, height); Ok(size) } @@ -1338,12 +1451,12 @@ impl PluginHost for AuHost { fn destroy_editor(&self, node_id: &str) { let id = node_id.to_string(); let destroy = move || { - let Some(view) = instances() - .lock() - .unwrap() - .get_mut(&id) - .and_then(|s| s.view.take()) - else { + let mut guard = instances().lock().unwrap(); + let Some(slot) = guard.get_mut(&id) else { + return; + }; + slot.observer = None; + let Some(view) = slot.view.take() else { return; }; unsafe { drop_view(view) }; @@ -1364,7 +1477,8 @@ impl PluginHost for AuHost { let mut freed = graveyard().lock().unwrap().reclaim(); let dead = super::host_api::take_dead(&mut instances().lock().unwrap(), |s| &s.alive); - for (node_id, slot) in dead { + for (node_id, mut slot) in dead { + slot.observer = None; // The view is a child of the editor window and points at the unit; // both go before the unit itself does. if let Some(view) = slot.view { diff --git a/src-tauri/src/audio/plugins/clap_host.rs b/src-tauri/src/audio/plugins/clap_host.rs index 8a7f82a4..2016aa3b 100644 --- a/src-tauri/src/audio/plugins/clap_host.rs +++ b/src-tauri/src/audio/plugins/clap_host.rs @@ -83,7 +83,7 @@ impl HostGuiImpl for SplitwaveShared { return Ok(()); }; if let Some(win) = editor::window_for(&self.node_id) { - editor::set_content_size(&win, w as f64, h as f64); + editor::request_resize(&win, &self.node_id, w, h); } Ok(()) } diff --git a/src-tauri/src/audio/plugins/editor.rs b/src-tauri/src/audio/plugins/editor.rs index 24e622a3..2b279f3e 100644 --- a/src-tauri/src/audio/plugins/editor.rs +++ b/src-tauri/src/audio/plugins/editor.rs @@ -13,13 +13,6 @@ use super::host_api::EditorSize; /// Fallback editor size for plugins that report a nonsensical one. pub const FALLBACK_EDITOR_SIZE: EditorSize = (800, 600); -/// Standard title-bar height (logical px) used when the window reports no -/// decoration overhead, which tao does on macOS (`outer_size == inner_size`). -#[cfg(target_os = "macos")] -const TITLEBAR_LOGICAL: f64 = 28.0; -#[cfg(not(target_os = "macos"))] -const TITLEBAR_LOGICAL: f64 = 32.0; - /// Native host windows that plugin editors are embedded into, keyed by node id. /// `tauri::Window` is `Send + Sync`, so this lives outside any main-thread state /// and can be created/closed from the command thread. @@ -50,35 +43,34 @@ pub fn valid_gui_size(w: u32, h: u32) -> Option { (w >= 100 && h >= 100 && w <= 8000 && h <= 8000).then_some((w, h)) } -/// Logical px the window decoration takes beyond its content, as (width, -/// height). The content view runs the full height of the window, under the -/// title bar, so this is also how far down a child view must start to clear it. -pub fn decoration_overhead(window: &tauri::Window) -> (f64, f64) { - let scale = window.scale_factor().unwrap_or(1.0); - let (dw, measured_dh) = match (window.inner_size(), window.outer_size()) { - (Ok(inner), Ok(outer)) => ( - outer.width.saturating_sub(inner.width) as f64 / scale, - outer.height.saturating_sub(inner.height) as f64 / scale, - ), - _ => (0.0, 0.0), - }; - // tao returns outer == inner on macOS, so the measurement is 0; fall back to - // the platform title-bar height so the plugin renders below the bar. - let dh = if measured_dh > 0.5 { - measured_dh - } else { - TITLEBAR_LOGICAL - }; - (dw, dh) +/// Returns window decoration overhead. Because `window.set_size` in Tauri 2 +/// sets the inner (client) size directly across all platforms, content area +/// matches the requested dimensions 1:1 without adding synthetic padding. +pub fn decoration_overhead(_window: &tauri::Window) -> (f64, f64) { + (0.0, 0.0) } -/// Sizes the window so its content area (below the title bar) is `w` x `h` -/// logical px. The plugin view fills the content area, so the title bar's -/// height is added -- otherwise the bar overlaps the top of the plugin and the -/// bottom gets clipped. +/// Sizes the window so its content area is `w` x `h` logical px. pub fn set_content_size(window: &tauri::Window, w: f64, h: f64) { - let (dw, dh) = decoration_overhead(window); - let _ = window.set_size(tauri::LogicalSize::new(w + dw, h + dh)); + let _ = window.set_size(tauri::LogicalSize::new(w, h)); +} + +/// Requests a window resize from a plugin. If the window's content area already +/// matches the requested dimensions, this is a no-op, preventing redundant OS +/// resizing calls and feedback loops. When dimensions differ, the window is +/// resized synchronously on the main thread so plugin internal layout engines +/// (JUCE, VST3, AU) immediately see the updated parent bounds without frame +/// tearing or asynchronous jitter. +pub fn request_resize(window: &tauri::Window, _node_id: &str, w: u32, h: u32) { + if let Ok(inner) = window.inner_size() { + let scale = window.scale_factor().unwrap_or(1.0); + let cur_w = (inner.width as f64 / scale).round() as u32; + let cur_h = (inner.height as f64 / scale).round() as u32; + if cur_w == w && cur_h == h { + return; + } + } + let _ = window.set_size(tauri::LogicalSize::new(w as f64, h as f64)); } /// Opens the plugin editor embedded in a native host window. The tested plugins @@ -96,16 +88,40 @@ pub fn open(node_id: &str, title: &str) -> Result<(), String> { let _ = w.set_focus(); return Ok(()); } - let window = tauri::WindowBuilder::new(app, format!("plugin-editor-{node_id}")) + let mut builder = tauri::WindowBuilder::new(app, format!("plugin-editor-{node_id}")) .title(if title.is_empty() { "Plugin" } else { title }) .inner_size(FALLBACK_EDITOR_SIZE.0 as f64, FALLBACK_EDITOR_SIZE.1 as f64) + .visible(false) // Always resizable with a small floor: even when a plugin reports a bad // size or does not reflow, the user can enlarge the window to reveal it. .resizable(true) - .min_inner_size(200.0, 150.0) + .min_inner_size(200.0, 150.0); + + #[cfg(target_os = "macos")] + { + builder = builder.title_bar_style(tauri::TitleBarStyle::Visible); + } + + let window = builder .build() .map_err(|e| format!("editor window for {node_id}: {e}"))?; + #[cfg(target_os = "macos")] + unsafe { + use objc2::msg_send; + use objc2::runtime::AnyObject; + if let Ok(ns_window) = window.ns_window() { + let nsw = ns_window as *mut AnyObject; + let mut mask: usize = msg_send![nsw, styleMask]; + // Clear NSWindowStyleMaskFullSizeContentView (1 << 15 = 32768) so + // the content view stays strictly below the titlebar and plugin + // headers never overlap window controls. + mask &= !(1 << 15); + let _: () = msg_send![nsw, setStyleMask: mask]; + let _: () = msg_send![nsw, setTitlebarAppearsTransparent: false]; + } + } + let nid = node_id.to_string(); window.on_window_event(move |ev| { // The user closed the window: keep the native view parented to avoid @@ -147,6 +163,23 @@ pub fn open(node_id: &str, title: &str) -> Result<(), String> { tracing::debug!(node_id, width, height, "plugin editor embedded"); set_content_size(&window, width as f64, height as f64); + + #[cfg(target_os = "macos")] + unsafe { + use objc2::msg_send; + use objc2_foundation::{NSPoint, NSRect, NSSize}; + if let Ok(parent) = window.ns_view() { + if let Some(view) = last_subview(parent) { + let frame = NSRect::new( + NSPoint::new(0.0, 0.0), + NSSize::new(width as f64, height as f64), + ); + let _: () = msg_send![view, setFrame: frame]; + let _: () = msg_send![view, setNeedsDisplay: true]; + } + } + } + let _ = window.show(); let _ = window.set_focus(); Ok(()) @@ -167,47 +200,6 @@ pub fn close(node_id: &str) -> Result<(), String> { Ok(()) } -/// `NSViewWidthSizable` / `NSViewHeightSizable`: the plugin view follows the -/// editor window's content area instead of staying pinned at its initial size. -#[cfg(target_os = "macos")] -const NS_VIEW_WIDTH_SIZABLE: usize = 1 << 1; -#[cfg(target_os = "macos")] -const NS_VIEW_HEIGHT_SIZABLE: usize = 1 << 4; - -/// Frames `view` into the content view it was just added to, leaving `titlebar` -/// px clear at the top. -/// -/// The content view runs the full window height, under the title bar, so -/// filling its bounds outright would put the top of the editor behind the bar. -/// Starting at the bottom-left origin of an unflipped NSView and stopping short -/// of the top is the arrangement a plugin ends up in when it parents its own -/// view. Margins stay fixed by default, so the mask preserves that gap through -/// every later resize. -/// -/// SAFETY: `parent` is a live window content view and `view` one of its -/// subviews; main thread only. -#[cfg(target_os = "macos")] -pub unsafe fn inset_below_titlebar( - parent: *mut std::ffi::c_void, - view: *mut objc2::runtime::AnyObject, - titlebar: f64, -) { - use objc2::msg_send; - use objc2::runtime::AnyObject; - use objc2_foundation::{NSPoint, NSRect, NSSize}; - - unsafe { - let bounds: NSRect = msg_send![parent as *mut AnyObject, bounds]; - let frame = NSRect::new( - NSPoint::new(0.0, 0.0), - NSSize::new(bounds.size.width, (bounds.size.height - titlebar).max(1.0)), - ); - let _: () = msg_send![view, setFrame: frame]; - let _: () = - msg_send![view, setAutoresizingMask: NS_VIEW_WIDTH_SIZABLE | NS_VIEW_HEIGHT_SIZABLE]; - } -} - /// The last subview of `parent`, which is the one a plugin just added. /// /// SAFETY: `parent` is a live NSView; main thread only. diff --git a/src-tauri/src/audio/plugins/vst3_editor.rs b/src-tauri/src/audio/plugins/vst3_editor.rs index 3db05ae9..b8a06928 100644 --- a/src-tauri/src/audio/plugins/vst3_editor.rs +++ b/src-tauri/src/audio/plugins/vst3_editor.rs @@ -143,16 +143,6 @@ impl EditorView { let _: () = msg_send![parent_obj, setWantsLayer: true]; } - let frame = ComWrapper::new(PlugFrame { resize }); - let frame_ptr = frame - .as_com_ref::() - .map(|r| r.as_ptr()) - .ok_or("PlugFrame implements IPlugFrame")?; - // Before `attached`, so a plugin that resizes on open has somewhere - // to send the request, and plugins that inspect frame metrics during - // size calculations have an initialized frame pointer. - view.setFrame(frame_ptr); - let mut rect = ViewRect { left: 0, top: 0, @@ -164,6 +154,24 @@ impl EditorView { let mut got_size = view.getSize(&mut rect) == kResultOk && (rect.right > rect.left && rect.bottom > rect.top); + if got_size { + let init_w = (rect.right - rect.left).max(1) as u32; + let init_h = (rect.bottom - rect.top).max(1) as u32; + if let Some((valid_w, valid_h)) = editor::valid_gui_size(init_w, init_h) { + resize(valid_w, valid_h); + } + } + + let frame = ComWrapper::new(PlugFrame { resize }); + let frame_ptr = frame + .as_com_ref::() + .map(|r| r.as_ptr()) + .ok_or("PlugFrame implements IPlugFrame")?; + // Before `attached`, so a plugin that resizes on open has somewhere + // to send the request, and plugins that inspect frame metrics during + // size calculations have an initialized frame pointer. + view.setFrame(frame_ptr); + if view.attached(parent, platform) != kResultOk { view.setFrame(std::ptr::null_mut()); return Err("editor refused to attach to the window".into()); diff --git a/src-tauri/src/audio/plugins/vst3_registry.rs b/src-tauri/src/audio/plugins/vst3_registry.rs index 6f55b00b..d4a9fbbd 100644 --- a/src-tauri/src/audio/plugins/vst3_registry.rs +++ b/src-tauri/src/audio/plugins/vst3_registry.rs @@ -204,8 +204,9 @@ fn attach_slot_editor( .take_view() .ok_or_else(|| format!("vst3 {node_id}: plugin has no editor"))?; + let nid = node_id.to_string(); let resize = Box::new(move |w: u32, h: u32| { - let _ = resize_target.set_size(tauri::LogicalSize::new(w as f64, h as f64)); + editor::request_resize(&resize_target, &nid, w, h); }); let (view, size) = EditorView::attach(view, view_addr as *mut c_void, titlebar, resize) .map_err(|e| format!("vst3 {node_id}: {e}"))?; From 43bd842619026855c93d242f436dd412643cf500 Mon Sep 17 00:00:00 2001 From: Horuse <39675195+Horuse@users.noreply.github.com> Date: Sat, 5 Sep 2026 22:41:22 +0300 Subject: [PATCH 7/7] fix(plugins): fix cross-platform compilation and compiler warnings --- src-tauri/src/audio/plugins/editor.rs | 6 ++---- src-tauri/src/audio/plugins/vst3_editor.rs | 1 - src-tauri/src/audio/plugins/vst3_registry.rs | 6 +++--- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/audio/plugins/editor.rs b/src-tauri/src/audio/plugins/editor.rs index 2b279f3e..4151b184 100644 --- a/src-tauri/src/audio/plugins/editor.rs +++ b/src-tauri/src/audio/plugins/editor.rs @@ -88,7 +88,7 @@ pub fn open(node_id: &str, title: &str) -> Result<(), String> { let _ = w.set_focus(); return Ok(()); } - let mut builder = tauri::WindowBuilder::new(app, format!("plugin-editor-{node_id}")) + let builder = tauri::WindowBuilder::new(app, format!("plugin-editor-{node_id}")) .title(if title.is_empty() { "Plugin" } else { title }) .inner_size(FALLBACK_EDITOR_SIZE.0 as f64, FALLBACK_EDITOR_SIZE.1 as f64) .visible(false) @@ -98,9 +98,7 @@ pub fn open(node_id: &str, title: &str) -> Result<(), String> { .min_inner_size(200.0, 150.0); #[cfg(target_os = "macos")] - { - builder = builder.title_bar_style(tauri::TitleBarStyle::Visible); - } + let builder = builder.title_bar_style(tauri::TitleBarStyle::Visible); let window = builder .build() diff --git a/src-tauri/src/audio/plugins/vst3_editor.rs b/src-tauri/src/audio/plugins/vst3_editor.rs index b8a06928..404e9377 100644 --- a/src-tauri/src/audio/plugins/vst3_editor.rs +++ b/src-tauri/src/audio/plugins/vst3_editor.rs @@ -14,7 +14,6 @@ use vst3::Steinberg::{ }; use vst3::{Class, ComPtr, ComWrapper}; -#[cfg(target_os = "macos")] use crate::audio::plugins::editor; use crate::audio::plugins::host_api::EditorSize; #[cfg(target_os = "linux")] diff --git a/src-tauri/src/audio/plugins/vst3_registry.rs b/src-tauri/src/audio/plugins/vst3_registry.rs index d4a9fbbd..393743f0 100644 --- a/src-tauri/src/audio/plugins/vst3_registry.rs +++ b/src-tauri/src/audio/plugins/vst3_registry.rs @@ -350,12 +350,12 @@ impl PluginHost for Vst3Host { }; if let Some(ref editor) = slot.editor { editor.on_focus(true); - if let Some(window) = editor::window_for(&id) { + if let Some(_window) = editor::window_for(&id) { #[cfg(target_os = "macos")] unsafe { use objc2::msg_send; use objc2::runtime::AnyObject; - if let Ok(addr) = parent_handle(&window) { + if let Ok(addr) = parent_handle(&_window) { let parent_obj = addr as *mut AnyObject; let _: () = msg_send![parent_obj, setNeedsDisplay: true]; if let Some(v) = editor::last_subview(addr as *mut std::ffi::c_void) @@ -368,7 +368,7 @@ impl PluginHost for Vst3Host { unsafe { use windows::Win32::Foundation::HWND; use windows::Win32::Graphics::Gdi::{InvalidateRect, UpdateWindow}; - if let Ok(addr) = parent_handle(&window) { + if let Ok(addr) = parent_handle(&_window) { let hwnd = HWND(addr as _); let _ = InvalidateRect(Some(hwnd), None, true); let _ = UpdateWindow(hwnd);