diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..95b65b662 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/baseview"] + path = vendor/baseview + url = git@github.com:Wild-Surmise/baseview.git diff --git a/Cargo.toml b/Cargo.toml index 0e6e2fdf6..113ed1d54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,9 @@ members = [ "examples/stft", "examples/sysex", ] +# baseview is a vendored submodule with its own [workspace] root; keep it out +# of ours so each side stays self-contained for upstream-diff sanity. +exclude = ["vendor/baseview"] default-members = ["crates/nih_plug"] [workspace.package] @@ -42,7 +45,13 @@ serde = { version = "1.0", features = ["derive"] } atomic_float = "1.1" atomic_refcell = "0.1" bitflags = "2.11" -baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "1ec490c99fa2bef0c3c81c4f42d0b7552412c50d", default-features = false } +# Vendored at vendor/baseview (submodule of Wild-Surmise/baseview, branch +# gealach-occlusion-patch). Carries a macOS occlusion-state patch so the +# 15ms CFRunLoop draw timer skips frames when the window is occluded — +# without it, App Nap-throttled draws backlog and freeze the host UI on +# resume. Upstream rev the patch sits on top of: +# 5187acfc578f4fd10c247de6a39c19604e72fab6. +baseview = { path = "vendor/baseview", default-features = false } raw-window-handle = "0.5" realfft = "3.0" approx = "0.5.1" diff --git a/baseview-adapters/egui-baseview/src/window.rs b/baseview-adapters/egui-baseview/src/window.rs index 5ae412577..e478dc2b8 100644 --- a/baseview-adapters/egui-baseview/src/window.rs +++ b/baseview-adapters/egui-baseview/src/window.rs @@ -331,10 +331,12 @@ where }; self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64()); - self.egui_input.screen_rect = Some(calculate_screen_rect( - self.physical_size, - self.points_per_pixel, - )); + let effective_points_per_pixel = self.egui_ctx.pixels_per_point().recip(); + let screen_rect = calculate_screen_rect(self.physical_size, effective_points_per_pixel); + self.egui_input.screen_rect = Some(screen_rect); + if let Some(viewport_info) = self.egui_input.viewports.get_mut(&self.viewport_id) { + viewport_info.inner_rect = Some(screen_rect); + } //let mut repaint_requested = false; let mut queue = Queue::new( @@ -386,7 +388,7 @@ where window, self.bg_color, self.physical_size, - self.pixels_per_point, + full_output.pixels_per_point, &mut self.egui_ctx, &mut full_output, ); @@ -465,7 +467,11 @@ where } => { self.update_modifiers(modifiers); - let pos = pos2(position.x as f32, position.y as f32); + let point_scale = self.pixels_per_point / self.egui_ctx.pixels_per_point(); + let pos = pos2( + position.x as f32 * point_scale, + position.y as f32 * point_scale, + ); self.pointer_pos_in_points = Some(pos); self.egui_input.events.push(egui::Event::PointerMoved(pos)); } @@ -511,7 +517,7 @@ where baseview::ScrollDelta::Pixels { x, y } => ( egui::MouseWheelUnit::Point, - egui::vec2(*x, *y) * self.points_per_pixel, + egui::vec2(*x, *y) * self.egui_ctx.pixels_per_point().recip(), ), }; diff --git a/crates/nih_plug/src/wrapper/clap/wrapper.rs b/crates/nih_plug/src/wrapper/clap/wrapper.rs index 02d5f6da8..d29cb6691 100644 --- a/crates/nih_plug/src/wrapper/clap/wrapper.rs +++ b/crates/nih_plug/src/wrapper/clap/wrapper.rs @@ -2863,12 +2863,20 @@ impl Wrapper

{ window: *const clap_window, ) -> bool { check_null_ptr!(false, plugin, unsafe { (*plugin).plugin_data }, window); - // For this function we need the underlying Arc so we can pass it to the editor - let wrapper = unsafe { Arc::from_raw((*plugin).plugin_data as *const Self) }; + let wrapper_ref = unsafe { &*((*plugin).plugin_data as *const Self) }; + // For this function we need an `Arc` so we can pass it to the editor. The raw + // `plugin_data` pointer is owned by the CLAP instance and must only be consumed by + // `destroy()`. Reconstructing an `Arc` from it here would temporarily take ownership of + // the plugin wrapper, and any early return before re-leaking it would destroy the live + // plugin while the host may still process audio. + let Some(wrapper) = wrapper_ref.this.borrow().upgrade() else { + nih_debug_assert_failure!("Plugin wrapper was dropped before GUI attachment"); + return false; + }; let window = unsafe { &*window }; - let result = { + { let mut editor_handle = wrapper.editor_handle.lock(); if editor_handle.is_none() { let api = unsafe { CStr::from_ptr(window.api) }; @@ -2904,12 +2912,7 @@ impl Wrapper

{ false } - }; - - // Leak the Arc again since we only needed a clone to pass to the GuiContext - let _ = Arc::into_raw(wrapper); - - result + } } unsafe extern "C" fn ext_gui_set_transient( diff --git a/crates/nih_plug/src/wrapper/util/buffer_management.rs b/crates/nih_plug/src/wrapper/util/buffer_management.rs index 1892da54e..96e7544e5 100644 --- a/crates/nih_plug/src/wrapper/util/buffer_management.rs +++ b/crates/nih_plug/src/wrapper/util/buffer_management.rs @@ -34,6 +34,10 @@ pub struct BufferManager { /// will be shortened when returning a reference to these buffers in `create_buffers` to match /// the function's lifetime. main_buffer: Buffer<'static>, + /// Scratch storage for main output channels when the host reports a channel but provides a + /// null pointer for it. This keeps the `Buffer` invariants intact without allocating during + /// processing. + main_output_storage: Vec>, aux_input_buffers: Vec>, /// Stores the data to back `aux_input_buffers`. We need to copy the host's auxiliary input @@ -90,17 +94,16 @@ impl BufferManager { // The buffers are preallocated so that `create_buffers()` can be called without having to // allocate let mut main_buffer = Buffer::default(); + let main_output_channels = audio_io_layout + .main_output_channels + .map(NonZeroU32::get) + .unwrap_or(0) as usize; unsafe { main_buffer.set_slices(0, |output_slices| { - output_slices.resize_with( - audio_io_layout - .main_output_channels - .map(NonZeroU32::get) - .unwrap_or(0) as usize, - || &mut [], - ); + output_slices.resize_with(main_output_channels, || &mut []); }) }; + let main_output_storage = vec![vec![0.0; max_buffer_size]; main_output_channels]; let mut aux_input_buffers = Vec::with_capacity(audio_io_layout.aux_input_ports.len()); let mut aux_input_storage = Vec::with_capacity(audio_io_layout.aux_input_ports.len()); @@ -138,6 +141,7 @@ impl BufferManager { aux_output_channel_pointers: vec![None; audio_io_layout.aux_output_ports.len()], main_buffer, + main_output_storage, aux_input_buffers, aux_input_storage, @@ -182,38 +186,44 @@ impl BufferManager { aux_output_channel_pointers: &mut self.aux_output_channel_pointers, }); - // The main buffer points directly to the main output pointers + // The main buffer points directly to the main output pointers. If the host reports an + // output channel but gives us a null pointer, fall back to preallocated scratch storage so + // the plugin sees a well-formed buffer without touching invalid memory. + let main_output_storage = &mut self.main_output_storage; unsafe { self.main_buffer.set_slices(num_samples, |output_slices| { match self.main_output_channel_pointers { Some(output_channel_pointers) => { - nih_debug_assert_eq!( - output_slices.len(), - output_channel_pointers.num_channels - ); - for (channel_idx, output_slice) in output_slices - .iter_mut() - .enumerate() - .take(output_channel_pointers.num_channels) - { + for (channel_idx, output_slice) in output_slices.iter_mut().enumerate() { let output_channel_pointer = - output_channel_pointers.ptrs.as_ptr().add(channel_idx); - - *output_slice = std::slice::from_raw_parts_mut( - (*output_channel_pointer).add(sample_offset), - num_samples, - ); + if channel_idx < output_channel_pointers.num_channels { + *output_channel_pointers.ptrs.as_ptr().add(channel_idx) + } else { + std::ptr::null_mut() + }; + + if output_channel_pointer.is_null() { + let storage = &mut main_output_storage[channel_idx]; + nih_debug_assert!(num_samples <= storage.capacity()); + storage.resize(num_samples, 0.0); + storage.fill(0.0); + *output_slice = &mut *(storage.as_mut_slice() as *mut [f32]); + } else { + *output_slice = std::slice::from_raw_parts_mut( + output_channel_pointer.add(sample_offset), + num_samples, + ); + } } - - // If the caller/host should have provided buffer pointers but didn't then we - // must get rid of any dangling slices - output_slices[output_channel_pointers.num_channels..].fill_with(|| &mut []) } None => { - nih_debug_assert_eq!(output_slices.len(), 0); - - // Same as above - output_slices.fill_with(|| &mut []) + for (channel_idx, output_slice) in output_slices.iter_mut().enumerate() { + let storage = &mut main_output_storage[channel_idx]; + nih_debug_assert!(num_samples <= storage.capacity()); + storage.resize(num_samples, 0.0); + storage.fill(0.0); + *output_slice = &mut *(storage.as_mut_slice() as *mut [f32]); + } } } }); @@ -227,20 +237,28 @@ impl BufferManager { ) { unsafe { self.main_buffer.set_slices(num_samples, |output_slices| { - for (channel_idx, output_slice) in output_slices - .iter_mut() - .enumerate() - .take(input_channel_pointers.num_channels) - { + for (channel_idx, output_slice) in output_slices.iter_mut().enumerate().take( + input_channel_pointers + .num_channels + .min(output_channel_pointers.num_channels), + ) { let input_channel_pointer = *input_channel_pointers.ptrs.as_ptr().add(channel_idx); - debug_assert!(channel_idx < output_channel_pointers.num_channels); let output_channel_pointer = *output_channel_pointers.ptrs.as_ptr().add(channel_idx); + if input_channel_pointer.is_null() { + if !output_slice.is_empty() { + output_slice.fill(0.0); + } + continue; + } + // If the host processes the main IO out of place then the inputs need to be // copied to the output buffers. Otherwise the input should already be there. - if input_channel_pointer != output_channel_pointer { + if !output_slice.is_empty() + && input_channel_pointer != output_channel_pointer + { output_slice.copy_from_slice(std::slice::from_raw_parts_mut( input_channel_pointer.add(sample_offset), num_samples, @@ -255,7 +273,9 @@ impl BufferManager { if input_channel_pointers.num_channels < output_channel_pointers.num_channels { unsafe { self.main_buffer.set_slices(num_samples, |output_slices| { - for slice in &mut output_slices[input_channel_pointers.num_channels..] { + let first_excess_channel = + input_channel_pointers.num_channels.min(output_slices.len()); + for slice in &mut output_slices[first_excess_channel..] { slice.fill(0.0); } }); @@ -289,12 +309,17 @@ impl BufferManager { nih_debug_assert!(num_samples <= channel.capacity()); channel.resize(num_samples, 0.0); - channel.copy_from_slice(unsafe { - std::slice::from_raw_parts_mut( - (*input_channel_pointer).add(sample_offset), - num_samples, - ) - }) + let input_channel_pointer = unsafe { *input_channel_pointer }; + if input_channel_pointer.is_null() { + channel.fill(0.0); + } else { + channel.copy_from_slice(unsafe { + std::slice::from_raw_parts_mut( + input_channel_pointer.add(sample_offset), + num_samples, + ) + }) + } } // In case we were provided too few channels we'll fill the rest with zeroes to @@ -343,16 +368,26 @@ impl BufferManager { output_slices.len(), output_channel_pointers.num_channels ); + let provided_output_channels = output_channel_pointers + .num_channels + .min(output_slices.len()); for (channel_idx, output_slice) in output_slices .iter_mut() .enumerate() - .take(output_channel_pointers.num_channels) + .take(provided_output_channels) { let output_channel_pointer = output_channel_pointers.ptrs.as_ptr().add(channel_idx); + let output_channel_pointer = *output_channel_pointer; + + if output_channel_pointer.is_null() { + *output_slice = &mut []; + continue; + } + *output_slice = std::slice::from_raw_parts_mut( - (*output_channel_pointer).add(sample_offset), + output_channel_pointer.add(sample_offset), num_samples, ); @@ -363,8 +398,7 @@ impl BufferManager { // If the caller/host should have provided buffer pointers but didn't then // we must get rid of any dangling slices - output_slices[output_channel_pointers.num_channels..] - .fill_with(|| &mut []) + output_slices[provided_output_channels..].fill_with(|| &mut []) } None => { nih_debug_assert_eq!(output_slices.len(), 0); @@ -410,6 +444,14 @@ mod miri { names: PortNames::const_default(), }; + const MAIN_ONLY_AUDIO_IO_LAYOUT: AudioIOLayout = AudioIOLayout { + main_input_channels: Some(new_nonzero_u32(NUM_MAIN_INPUT_CHANNELS as u32)), + main_output_channels: Some(new_nonzero_u32(NUM_MAIN_OUTPUT_CHANNELS as u32)), + aux_input_ports: &[], + aux_output_ports: &[], + names: PortNames::const_default(), + }; + #[test] fn buffer_io() { // This works very similarly to the standalone CPAL and dummy backends @@ -518,4 +560,69 @@ mod miri { } } } + + #[test] + fn null_main_channel_pointers_are_sanitized() { + let mut input_storage = vec![vec![1.0f32; BUFFER_SIZE]; NUM_MAIN_INPUT_CHANNELS]; + let mut output_storage = vec![vec![2.0f32; BUFFER_SIZE]; NUM_MAIN_OUTPUT_CHANNELS]; + + let mut input_channel_pointers = vec![input_storage[0].as_mut_ptr(), std::ptr::null_mut()]; + let mut output_channel_pointers = vec![ + output_storage[0].as_mut_ptr(), + output_storage[1].as_mut_ptr(), + ]; + + let mut buffer_manager = + BufferManager::for_audio_io_layout(BUFFER_SIZE, MAIN_ONLY_AUDIO_IO_LAYOUT); + let buffers = unsafe { + buffer_manager.create_buffers(0, BUFFER_SIZE, |buffer_sources| { + *buffer_sources.main_input_channel_pointers = Some(ChannelPointers { + ptrs: NonNull::new(input_channel_pointers.as_mut_ptr()).unwrap(), + num_channels: input_channel_pointers.len(), + }); + *buffer_sources.main_output_channel_pointers = Some(ChannelPointers { + ptrs: NonNull::new(output_channel_pointers.as_mut_ptr()).unwrap(), + num_channels: output_channel_pointers.len(), + }); + }) + }; + + let main_buffer = buffers.main_buffer.as_slice_immutable(); + assert_eq!(main_buffer.len(), NUM_MAIN_OUTPUT_CHANNELS); + assert_eq!(main_buffer[0], &input_storage[0][..]); + assert!(main_buffer[1].iter().all(|sample| *sample == 0.0)); + } + + #[test] + fn null_main_output_channel_pointers_use_scratch_storage() { + let mut input_storage = vec![vec![1.0f32; BUFFER_SIZE]; NUM_MAIN_INPUT_CHANNELS]; + let mut output_storage = vec![vec![2.0f32; BUFFER_SIZE]; NUM_MAIN_OUTPUT_CHANNELS]; + + let mut input_channel_pointers: Vec<*mut f32> = input_storage + .iter_mut() + .map(|channel_slice| channel_slice.as_mut_ptr()) + .collect(); + let mut output_channel_pointers = + vec![output_storage[0].as_mut_ptr(), std::ptr::null_mut()]; + + let mut buffer_manager = + BufferManager::for_audio_io_layout(BUFFER_SIZE, MAIN_ONLY_AUDIO_IO_LAYOUT); + let buffers = unsafe { + buffer_manager.create_buffers(0, BUFFER_SIZE, |buffer_sources| { + *buffer_sources.main_input_channel_pointers = Some(ChannelPointers { + ptrs: NonNull::new(input_channel_pointers.as_mut_ptr()).unwrap(), + num_channels: input_channel_pointers.len(), + }); + *buffer_sources.main_output_channel_pointers = Some(ChannelPointers { + ptrs: NonNull::new(output_channel_pointers.as_mut_ptr()).unwrap(), + num_channels: output_channel_pointers.len(), + }); + }) + }; + + let main_buffer = buffers.main_buffer.as_slice_immutable(); + assert_eq!(main_buffer.len(), NUM_MAIN_OUTPUT_CHANNELS); + assert_eq!(main_buffer[0], &input_storage[0][..]); + assert!(main_buffer[1].iter().all(|sample| *sample == 0.0)); + } } diff --git a/crates/nih_plug_egui/src/editor.rs b/crates/nih_plug_egui/src/editor.rs index 9faeb7021..0c5f1a686 100644 --- a/crates/nih_plug_egui/src/editor.rs +++ b/crates/nih_plug_egui/src/editor.rs @@ -104,6 +104,10 @@ where gl_config: Some(gl_config), }; + // Newer baseview revs (≥5187acf) dropped `impl Default for + // WindowOpenOptions` and gate `gl_config` behind the `opengl` feature, + // so on the wgpu path the struct has no remaining fields beyond the + // three listed below — no `..Default::default()` needed. #[cfg(feature = "wgpu")] let window_settings = WindowOpenOptions { title: String::from("egui window"), @@ -114,7 +118,6 @@ where scale: scaling_factor .map(|factor| WindowScalePolicy::ScaleFactor(factor as f64)) .unwrap_or(WindowScalePolicy::SystemScaleFactor), - ..Default::default() }; let window = EguiWindow::open_parented( @@ -127,11 +130,13 @@ where let setter = ParamSetter::new(context.as_ref()); // If the window was requested to resize - if let Some(new_size) = egui_state.requested_size.swap(None) { + if let Some(new_size) = egui_state.requested_size.load() { // Ask the plugin host to resize to self.size() if context.request_resize() { + egui_state.requested_size.store(None); + // Resize the content of egui window - let scale = egui_ctx.pixels_per_point(); + let scale = egui_ctx.native_pixels_per_point().unwrap_or(1.0); queue.resize(PhySize::new( (new_size.0 as f32 * scale).round() as u32, (new_size.1 as f32 * scale).round() as u32, @@ -144,6 +149,8 @@ where // Update the state egui_state.size.store(new_size); + } else { + egui_state.requested_size.store(None); } } diff --git a/crates/nih_plug_egui/src/lib.rs b/crates/nih_plug_egui/src/lib.rs index 99b199e26..58f889949 100644 --- a/crates/nih_plug_egui/src/lib.rs +++ b/crates/nih_plug_egui/src/lib.rs @@ -160,8 +160,11 @@ impl EguiState { self.open.load(Ordering::Acquire) } - /// Set the new size that will be used to resize the window if the host allows. - fn set_requested_size(&self, new_size: (u32, u32)) { + /// Request a new editor window size in logical pixels. + /// + /// The actual resize is applied by the editor on the next GUI frame if the + /// host accepts the request. + pub fn request_resize(&self, new_size: (u32, u32)) { self.requested_size.store(Some(new_size)); } } diff --git a/crates/nih_plug_egui/src/resizable_window.rs b/crates/nih_plug_egui/src/resizable_window.rs index 7333ebbc3..5bad30447 100644 --- a/crates/nih_plug_egui/src/resizable_window.rs +++ b/crates/nih_plug_egui/src/resizable_window.rs @@ -51,7 +51,7 @@ impl ResizableWindow { .max(self.min_size); if corner_response.dragged() { - egui_state.set_requested_size(( + egui_state.request_resize(( desired_size.x.round() as u32, desired_size.y.round() as u32, )); diff --git a/crates/nih_plug_xtask/src/lib.rs b/crates/nih_plug_xtask/src/lib.rs index 71003bc73..388f88ca0 100644 --- a/crates/nih_plug_xtask/src/lib.rs +++ b/crates/nih_plug_xtask/src/lib.rs @@ -34,6 +34,9 @@ type BundlerConfig = HashMap; #[derive(Debug, Clone, Deserialize)] struct PackageConfig { name: Option, + /// Overrides the macOS bundle's `CFBundleIdentifier`. Falls back to + /// `com.nih-plug.{package}` when unset, preserving prior behaviour. + bundle_id: Option, } /// The target we're generating a plugin for. This can be either the native target or a cross @@ -341,10 +344,12 @@ fn bundle_binary( compilation_target: CompilationTarget, ) -> Result<()> { let bundle_home_dir = bundle_home(target_dir); - let bundle_name = match load_bundler_config()?.and_then(|c| c.get(package).cloned()) { - Some(PackageConfig { name: Some(name) }) => name, - _ => package.to_string(), - }; + let package_config = load_bundler_config()?.and_then(|c| c.get(package).cloned()); + let bundle_name = package_config + .as_ref() + .and_then(|c| c.name.clone()) + .unwrap_or_else(|| package.to_string()); + let bundle_id = package_config.as_ref().and_then(|c| c.bundle_id.clone()); // On MacOS the standalone target needs to be in a bundle let standalone_bundle_binary_name = @@ -381,6 +386,7 @@ fn bundle_binary( maybe_create_macos_bundle_metadata( package, &bundle_name, + bundle_id.as_deref(), &standalone_bundle_home, compilation_target, BundleType::Binary, @@ -405,10 +411,12 @@ fn bundle_plugin( compilation_target: CompilationTarget, ) -> Result<()> { let bundle_home_dir = bundle_home(target_dir); - let bundle_name = match load_bundler_config()?.and_then(|c| c.get(package).cloned()) { - Some(PackageConfig { name: Some(name) }) => name, - _ => package.to_string(), - }; + let package_config = load_bundler_config()?.and_then(|c| c.get(package).cloned()); + let bundle_name = package_config + .as_ref() + .and_then(|c| c.name.clone()) + .unwrap_or_else(|| package.to_string()); + let bundle_id = package_config.as_ref().and_then(|c| c.bundle_id.clone()); // We'll detect the plugin formats supported by the plugin binary and create bundled accordingly. // If `lib_path` contains paths to multiple plugins that need to be combined into a macOS @@ -448,6 +456,7 @@ fn bundle_plugin( maybe_create_macos_bundle_metadata( package, &bundle_name, + bundle_id.as_deref(), &clap_bundle_home, compilation_target, BundleType::Plugin, @@ -476,6 +485,7 @@ fn bundle_plugin( maybe_create_macos_bundle_metadata( package, &bundle_name, + bundle_id.as_deref(), &vst2_bundle_home, compilation_target, BundleType::Plugin, @@ -503,6 +513,7 @@ fn bundle_plugin( maybe_create_macos_bundle_metadata( package, &bundle_name, + bundle_id.as_deref(), vst3_bundle_home, compilation_target, BundleType::Plugin, @@ -734,6 +745,7 @@ fn vst3_bundle_library_name(package: &str, target: CompilationTarget) -> String pub fn maybe_create_macos_bundle_metadata( package: &str, display_name: &str, + bundle_id: Option<&str>, bundle_home: &Path, target: CompilationTarget, bundle_type: BundleType, @@ -750,13 +762,15 @@ pub fn maybe_create_macos_bundle_metadata( BundleType::Binary => "APPL", }; - // TODO: May want to add bundler.toml fields for the identifier, version and signature at some - // point. + // TODO: May want to add bundler.toml fields for the version and signature at some point. fs::write( bundle_home.join("Contents").join("PkgInfo"), format!("{package_type}????"), ) .context("Could not create PkgInfo file")?; + let resolved_bundle_id = bundle_id + .map(|s| s.to_string()) + .unwrap_or_else(|| format!("com.nih-plug.{package}")); fs::write( bundle_home.join("Contents").join("Info.plist"), format!(r#" @@ -769,7 +783,7 @@ pub fn maybe_create_macos_bundle_metadata( CFBundleIconFile CFBundleIdentifier - com.nih-plug.{package} + {resolved_bundle_id} CFBundleName {display_name} CFBundleDisplayName diff --git a/vendor/baseview b/vendor/baseview new file mode 160000 index 000000000..ba80695f9 --- /dev/null +++ b/vendor/baseview @@ -0,0 +1 @@ +Subproject commit ba80695f909bcb3b287dbcf0427ee3ebbe7c3f17