From c980576102e9ae3de3fec5d88ad01cd236c03e1f Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sat, 22 Oct 2022 15:05:39 +0200 Subject: [PATCH] Add an AsyncExecutor for editor GUIs This is decoupled form `GuiContext` as that would require invasive changes all over the place. --- BREAKING_CHANGES.md | 2 + plugins/crisp/src/lib.rs | 2 +- plugins/diopser/src/lib.rs | 2 +- plugins/examples/gain_gui_egui/src/lib.rs | 2 +- plugins/examples/gain_gui_iced/src/lib.rs | 2 +- plugins/examples/gain_gui_vizia/src/lib.rs | 2 +- plugins/spectral_compressor/src/lib.rs | 2 +- src/plugin.rs | 3 +- src/wrapper/clap/wrapper.rs | 43 +++++++++++++++++----- src/wrapper/standalone/context.rs | 3 +- src/wrapper/standalone/wrapper.rs | 29 ++++++++++++--- src/wrapper/vst3/inner.rs | 27 +++++++++++--- src/wrapper/vst3/wrapper.rs | 2 +- 13 files changed, 91 insertions(+), 30 deletions(-) diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 4f4af44f..dd479dfd 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -29,6 +29,8 @@ code then it will not be listed here. changed to `&mut impl InitContext`. - The `&mut impl ProcessContext` argument to `Plugin::process()` needs to be changed to `&mut impl ProcessContext`. +- The `Plugin::editor()` method now also takes a + `_async_executor: AsyncExecutor` parameter. ## [2022-10-20] diff --git a/plugins/crisp/src/lib.rs b/plugins/crisp/src/lib.rs index ad29eb02..9e12dd1b 100644 --- a/plugins/crisp/src/lib.rs +++ b/plugins/crisp/src/lib.rs @@ -313,7 +313,7 @@ impl Plugin for Crisp { self.params.clone() } - fn editor(&self) -> Option> { + fn editor(&self, _async_executor: AsyncExecutor) -> Option> { editor::create(self.params.clone(), self.params.editor_state.clone()) } diff --git a/plugins/diopser/src/lib.rs b/plugins/diopser/src/lib.rs index 9a5ddd35..90515d39 100644 --- a/plugins/diopser/src/lib.rs +++ b/plugins/diopser/src/lib.rs @@ -258,7 +258,7 @@ impl Plugin for Diopser { self.params.clone() } - fn editor(&self) -> Option> { + fn editor(&self, _async_executor: AsyncExecutor) -> Option> { editor::create(self.params.clone(), self.params.editor_state.clone()) } diff --git a/plugins/examples/gain_gui_egui/src/lib.rs b/plugins/examples/gain_gui_egui/src/lib.rs index 6d6437b8..0ee74d7c 100644 --- a/plugins/examples/gain_gui_egui/src/lib.rs +++ b/plugins/examples/gain_gui_egui/src/lib.rs @@ -89,7 +89,7 @@ impl Plugin for Gain { self.params.clone() } - fn editor(&self) -> Option> { + fn editor(&self, _async_executor: AsyncExecutor) -> Option> { let params = self.params.clone(); let peak_meter = self.peak_meter.clone(); create_egui_editor( diff --git a/plugins/examples/gain_gui_iced/src/lib.rs b/plugins/examples/gain_gui_iced/src/lib.rs index fc7250bf..9dba4bd1 100644 --- a/plugins/examples/gain_gui_iced/src/lib.rs +++ b/plugins/examples/gain_gui_iced/src/lib.rs @@ -86,7 +86,7 @@ impl Plugin for Gain { self.params.clone() } - fn editor(&self) -> Option> { + fn editor(&self, _async_executor: AsyncExecutor) -> Option> { editor::create( self.params.clone(), self.peak_meter.clone(), diff --git a/plugins/examples/gain_gui_vizia/src/lib.rs b/plugins/examples/gain_gui_vizia/src/lib.rs index 5fecf0b3..2e4a4ba1 100644 --- a/plugins/examples/gain_gui_vizia/src/lib.rs +++ b/plugins/examples/gain_gui_vizia/src/lib.rs @@ -85,7 +85,7 @@ impl Plugin for Gain { self.params.clone() } - fn editor(&self) -> Option> { + fn editor(&self, _async_executor: AsyncExecutor) -> Option> { editor::create( self.params.clone(), self.peak_meter.clone(), diff --git a/plugins/spectral_compressor/src/lib.rs b/plugins/spectral_compressor/src/lib.rs index bbac67ca..fd95d280 100644 --- a/plugins/spectral_compressor/src/lib.rs +++ b/plugins/spectral_compressor/src/lib.rs @@ -278,7 +278,7 @@ impl Plugin for SpectralCompressor { self.params.clone() } - fn editor(&self) -> Option> { + fn editor(&self, _async_executor: AsyncExecutor) -> Option> { editor::create(self.params.clone(), self.editor_state.clone()) } diff --git a/src/plugin.rs b/src/plugin.rs index 0160a32b..9639d41a 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -8,6 +8,7 @@ use crate::context::process::ProcessContext; use crate::editor::Editor; use crate::midi::MidiConfig; use crate::params::Params; +use crate::prelude::AsyncExecutor; use crate::wrapper::clap::features::ClapFeature; use crate::wrapper::state::PluginState; @@ -122,7 +123,7 @@ pub trait Plugin: Default + Send + 'static { /// access into the editor. You can later modify the parameters through the /// [`GuiContext`][crate::prelude::GuiContext] and [`ParamSetter`][crate::prelude::ParamSetter] after the editor /// GUI has been created. - fn editor(&self) -> Option> { + fn editor(&self, async_executor: AsyncExecutor) -> Option> { None } diff --git a/src/wrapper/clap/wrapper.rs b/src/wrapper/clap/wrapper.rs index 661d1d9d..5291f5ef 100644 --- a/src/wrapper/clap/wrapper.rs +++ b/src/wrapper/clap/wrapper.rs @@ -77,6 +77,7 @@ use super::context::{WrapperGuiContext, WrapperInitContext, WrapperProcessContex use super::descriptor::PluginDescriptor; use super::util::ClapPtr; use crate::buffer::Buffer; +use crate::context::gui::AsyncExecutor; use crate::context::process::Transport; use crate::editor::{Editor, ParentWindowHandle}; use crate::event_loop::{EventLoop, MainThreadExecutor, TASK_QUEUE_CAPACITY}; @@ -114,8 +115,8 @@ pub struct Wrapper { params: Arc, /// The plugin's editor, if it has one. This object does not do anything on its own, but we need /// to instantiate this in advance so we don't need to lock the entire [`Plugin`] object when - /// creating an editor. - editor: Option>>, + /// creating an editor. Wrapped in an `AtomicRefCell` because it needs to be initialized late. + editor: AtomicRefCell>>>, /// A handle for the currently active editor instance. The plugin should implement `Drop` on /// this handle for its closing behavior. editor_handle: Mutex>>, @@ -388,7 +389,6 @@ impl Wrapper

{ pub fn new(host_callback: *const clap_host) -> Arc { let plugin = P::default(); let task_executor = Mutex::new(plugin.task_executor()); - let editor = plugin.editor().map(Mutex::new); // This is used to allow the plugin to restore preset data from its editor, see the comment // on `Self::updated_state_sender` @@ -551,7 +551,8 @@ impl Wrapper

{ plugin: Mutex::new(plugin), task_executor, params, - editor, + // Initialized later as it needs a reference to the wrapper for the async executor + editor: AtomicRefCell::new(None), editor_handle: Mutex::new(None), editor_scaling_factor: AtomicF32::new(1.0), @@ -678,6 +679,22 @@ impl Wrapper

{ let wrapper = Arc::new(wrapper); *wrapper.this.borrow_mut() = Arc::downgrade(&wrapper); + // The editor also needs to be initialized later so the Async executor can work. + *wrapper.editor.borrow_mut() = wrapper + .plugin + .lock() + .editor(AsyncExecutor { + inner: Arc::new({ + let wrapper = wrapper.clone(); + + move |task| { + let task_posted = wrapper.do_maybe_async(Task::PluginTask(task)); + nih_debug_assert!(task_posted, "The task queue is full, dropping task..."); + } + }), + }) + .map(Mutex::new); + wrapper } @@ -725,7 +742,7 @@ impl Wrapper

{ /// the editor instance is currently locked then nothing will happen, and the request can safely /// be ignored. pub fn notify_param_values_changed(&self) { - if let Some(editor) = &self.editor { + if let Some(editor) = self.editor.borrow().as_ref() { match editor.try_lock() { Some(editor) => editor.param_values_changed(), None => nih_debug_assert_failure!( @@ -740,7 +757,10 @@ impl Wrapper

{ /// safely be called from any thread. If this returns `false`, then the plugin should reset its /// size back to the previous value. pub fn request_resize(&self) -> bool { - match (&*self.host_gui.borrow(), &self.editor) { + match ( + self.host_gui.borrow().as_ref(), + self.editor.borrow().as_ref(), + ) { (Some(host_gui), Some(editor)) => { let (unscaled_width, unscaled_height) = editor.lock().size(); let scaling_factor = self.editor_scaling_factor.load(Ordering::Relaxed); @@ -2300,7 +2320,7 @@ impl Wrapper

{ &wrapper.clap_plugin_audio_ports_config as *const _ as *const c_void } else if id == CLAP_EXT_AUDIO_PORTS { &wrapper.clap_plugin_audio_ports as *const _ as *const c_void - } else if id == CLAP_EXT_GUI && wrapper.editor.is_some() { + } else if id == CLAP_EXT_GUI && wrapper.editor.borrow().is_some() { // Only report that we support this extension if the plugin has an editor &wrapper.clap_plugin_gui as *const _ as *const c_void } else if id == CLAP_EXT_LATENCY { @@ -2686,6 +2706,7 @@ impl Wrapper

{ if wrapper .editor + .borrow() .as_ref() .unwrap() .lock() @@ -2709,7 +2730,8 @@ impl Wrapper

{ let wrapper = &*(plugin as *const Self); // For macOS the scaling factor is always 1 - let (unscaled_width, unscaled_height) = wrapper.editor.as_ref().unwrap().lock().size(); + let (unscaled_width, unscaled_height) = + wrapper.editor.borrow().as_ref().unwrap().lock().size(); let scaling_factor = wrapper.editor_scaling_factor.load(Ordering::Relaxed); (*width, *height) = ( (unscaled_width as f32 * scaling_factor).round() as u32, @@ -2751,7 +2773,8 @@ impl Wrapper

{ check_null_ptr!(false, plugin); let wrapper = &*(plugin as *const Self); - let (unscaled_width, unscaled_height) = wrapper.editor.as_ref().unwrap().lock().size(); + let (unscaled_width, unscaled_height) = + wrapper.editor.borrow().as_ref().unwrap().lock().size(); let scaling_factor = wrapper.editor_scaling_factor.load(Ordering::Relaxed); let (editor_width, editor_height) = ( (unscaled_width as f32 * scaling_factor).round() as u32, @@ -2793,7 +2816,7 @@ impl Wrapper

{ }; // This extension is only exposed when we have an editor - *editor_handle = Some(wrapper.editor.as_ref().unwrap().lock().spawn( + *editor_handle = Some(wrapper.editor.borrow().as_ref().unwrap().lock().spawn( ParentWindowHandle { handle }, wrapper.clone().make_gui_context(), )); diff --git a/src/wrapper/standalone/context.rs b/src/wrapper/standalone/context.rs index 1317b2df..eb7b2cba 100644 --- a/src/wrapper/standalone/context.rs +++ b/src/wrapper/standalone/context.rs @@ -108,7 +108,8 @@ impl GuiContext for WrapperGuiContext { } fn request_resize(&self) -> bool { - let (unscaled_width, unscaled_height) = self.wrapper.editor.as_ref().unwrap().lock().size(); + let (unscaled_width, unscaled_height) = + self.wrapper.editor.borrow().as_ref().unwrap().lock().size(); // This will cause the editor to be resized at the start of the next frame let push_successful = self diff --git a/src/wrapper/standalone/wrapper.rs b/src/wrapper/standalone/wrapper.rs index 056abdbc..3631dc09 100644 --- a/src/wrapper/standalone/wrapper.rs +++ b/src/wrapper/standalone/wrapper.rs @@ -13,6 +13,7 @@ use std::thread; use super::backend::Backend; use super::config::WrapperConfig; use super::context::{WrapperGuiContext, WrapperInitContext, WrapperProcessContext}; +use crate::context::gui::AsyncExecutor; use crate::context::process::Transport; use crate::editor::{Editor, ParentWindowHandle}; use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop}; @@ -51,8 +52,8 @@ pub struct Wrapper { param_map: HashMap, /// The plugin's editor, if it has one. This object does not do anything on its own, but we need /// to instantiate this in advance so we don't need to lock the entire [`Plugin`] object when - /// creating an editor. - pub editor: Option>>>, + /// creating an editor. Wrapped in an `AtomicRefCell` because it needs to be initialized late. + pub editor: AtomicRefCell>>>>, /// A realtime-safe task queue so the plugin can schedule tasks that need to be run later on the /// GUI thread. See the same field in the VST3 wrapper for more information on why this looks @@ -159,7 +160,6 @@ impl Wrapper { task_executor: Mutex::new(plugin.task_executor()), }); let params = plugin.params(); - let editor = plugin.editor().map(|editor| Arc::new(Mutex::new(editor))); // This is used to allow the plugin to restore preset data from its editor, see the comment // on `Self::updated_state_sender` @@ -211,7 +211,8 @@ impl Wrapper { .into_iter() .map(|(param_id, param_ptr, _)| (param_id, param_ptr)) .collect(), - editor, + // Initialized later as it needs a reference to the wrapper for the async executor + editor: AtomicRefCell::new(None), event_loop: OsEventLoop::new_and_spawn(Arc::downgrade(&task_executor_wrapper)), @@ -236,6 +237,22 @@ impl Wrapper { updated_state_receiver, }); + // The editor needs to be initialized later so the Async executor can work. + *wrapper.editor.borrow_mut() = wrapper + .plugin + .lock() + .editor(AsyncExecutor { + inner: Arc::new({ + let wrapper = wrapper.clone(); + + move |task| { + let task_posted = wrapper.event_loop.do_maybe_async(task); + nih_debug_assert!(task_posted, "The task queue is full, dropping task..."); + } + }), + }) + .map(|editor| Arc::new(Mutex::new(editor))); + // Right now the IO configuration is fixed in the standalone target, so if the plugin cannot // work with this then we cannot initialize the plugin at all. { @@ -283,7 +300,7 @@ impl Wrapper { thread::spawn(move || this.run_audio_thread(terminate_audio_thread, gui_task_sender)) }; - match self.editor.clone() { + match self.editor.borrow().clone() { Some(editor) => { let context = self.clone().make_gui_context(gui_task_sender); @@ -511,7 +528,7 @@ impl Wrapper { /// off-chance that the editor instance is currently locked then nothing will happen, and the /// request can safely be ignored. fn notify_param_values_changed(&self) { - if let Some(editor) = &self.editor { + if let Some(editor) = self.editor.borrow().as_ref() { match editor.try_lock() { Some(editor) => editor.param_values_changed(), None => nih_debug_assert_failure!( diff --git a/src/wrapper/vst3/inner.rs b/src/wrapper/vst3/inner.rs index 893b16a0..f2cf09b0 100644 --- a/src/wrapper/vst3/inner.rs +++ b/src/wrapper/vst3/inner.rs @@ -15,6 +15,7 @@ use super::param_units::ParamUnits; use super::util::{ObjectPtr, VstPtr, VST3_MIDI_PARAMS_END, VST3_MIDI_PARAMS_START}; use super::view::WrapperView; use crate::buffer::Buffer; +use crate::context::gui::AsyncExecutor; use crate::context::process::Transport; use crate::editor::Editor; use crate::event_loop::{EventLoop, MainThreadExecutor, OsEventLoop}; @@ -41,8 +42,8 @@ pub(crate) struct WrapperInner { pub params: Arc, /// The plugin's editor, if it has one. This object does not do anything on its own, but we need /// to instantiate this in advance so we don't need to lock the entire [`Plugin`] object when - /// creating an editor. - pub editor: Option>>>, + /// creating an editor. Wrapped in an `AtomicRefCell` because it needs to be initialized late. + pub editor: AtomicRefCell>>>>, /// The host's [`IComponentHandler`] instance, if passed through /// [`IEditController::set_component_handler`]. @@ -198,7 +199,6 @@ impl WrapperInner

{ pub fn new() -> Arc { let plugin = P::default(); let task_executor = Mutex::new(plugin.task_executor()); - let editor = plugin.editor().map(|editor| Arc::new(Mutex::new(editor))); // This is used to allow the plugin to restore preset data from its editor, see the comment // on `Self::updated_state_sender` @@ -283,7 +283,8 @@ impl WrapperInner

{ plugin: Mutex::new(plugin), task_executor, params, - editor, + // Initialized later as it needs a reference to the wrapper for the async executor + editor: AtomicRefCell::new(None), component_handler: AtomicRefCell::new(None), @@ -331,6 +332,22 @@ impl WrapperInner

{ *wrapper.event_loop.borrow_mut() = Some(OsEventLoop::new_and_spawn(Arc::downgrade(&wrapper))); + // The editor also needs to be initialized later so the Async executor can work. + *wrapper.editor.borrow_mut() = wrapper + .plugin + .lock() + .editor(AsyncExecutor { + inner: Arc::new({ + let wrapper = wrapper.clone(); + + move |task| { + let task_posted = wrapper.do_maybe_async(Task::PluginTask(task)); + nih_debug_assert!(task_posted, "The task queue is full, dropping task..."); + } + }), + }) + .map(|editor| Arc::new(Mutex::new(editor))); + wrapper } @@ -385,7 +402,7 @@ impl WrapperInner

{ /// that the editor instance is currently locked then nothing will happen, and the request can /// safely be ignored. pub fn notify_param_values_changed(&self) { - if let Some(editor) = &self.editor { + if let Some(editor) = self.editor.borrow().as_ref() { match editor.try_lock() { Some(editor) => editor.param_values_changed(), None => nih_debug_assert_failure!( diff --git a/src/wrapper/vst3/wrapper.rs b/src/wrapper/vst3/wrapper.rs index 98caea1c..70f558dd 100644 --- a/src/wrapper/vst3/wrapper.rs +++ b/src/wrapper/vst3/wrapper.rs @@ -771,7 +771,7 @@ impl IEditController for Wrapper

{ unsafe fn create_view(&self, _name: vst3_sys::base::FIDString) -> *mut c_void { // Without specialization this is the least redundant way to check if the plugin has an // editor. The default implementation returns a None here. - match &self.inner.editor { + match self.inner.editor.borrow().as_ref() { Some(editor) => Box::into_raw(WrapperView::new(self.inner.clone(), editor.clone())) as *mut vst3_sys::c_void, None => ptr::null_mut(),