Skip to content

Commit

Permalink
#1376 Attempt
Browse files Browse the repository at this point in the history
Improve vst-rs by placing catch_unwind around all
calls from C, even shutdown. It shouldn't cause
a hard crash at least.
  • Loading branch information
helgoboss committed Dec 27, 2024
1 parent 26cfea6 commit 652e469
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 212 deletions.
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ webbrowser = "0.8.12"
runas = "1.1.0"
qrcode = "0.14.1"
uuid = "1.6.1"
vst = "0.3.0"
vst = "0.4.0"
c_str_macro = "1.0.2"
arboard = "3.3.0"
smallvec = "1.7.0"
Expand Down Expand Up @@ -247,9 +247,9 @@ debug = 1
# time! Turns out the same is actually true for axum, so we use select! there as well.
#hyper = { git = "https://github.com/helgoboss/hyper.git", branch = "feature/realearn" }

# TODO-low-wait Wait until https://github.com/RustAudio/vst-rs/issues/184 merged.
vst = { git = "https://github.com/helgoboss/vst-rs.git", branch = "feature/param-props" }
#vst = { path = "../vst-rs" }
# We need to use our on "vst" crate that contains a bunch of improvements
#vst = { git = "https://github.com/helgoboss/vst-rs.git", branch = "feature/param-props" }
vst = { path = "../vst-rs" }

# This is for temporary development with local reaper-rs.
#[patch.'https://github.com/helgoboss/reaper-rs.git']
Expand Down
243 changes: 109 additions & 134 deletions main/src/infrastructure/plugin/helgobox_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ use reaper_medium::{Hz, ReaperStr};

use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_void};
use std::panic::{catch_unwind, AssertUnwindSafe};

use std::sync::{Arc, OnceLock};

Expand Down Expand Up @@ -97,26 +96,23 @@ unsafe impl Send for HelgoboxPlugin {}

impl Plugin for HelgoboxPlugin {
fn new(host: HostCallback) -> Self {
firewall(|| {
let instance_id = InstanceId::next();
Self {
instance_id,
host,
_reaper_guard: None,
param_container: Arc::new(InstanceParameterContainer::new()),
was_playing_in_last_cycle: false,
sample_rate: Default::default(),
block_size: 0,
is_plugin_scan: false,
lazy_data: OnceLock::new(),
instance_panel: Rc::new(InstancePanel::new(instance_id)),
}
})
.unwrap_or_default()
let instance_id = InstanceId::next();
Self {
instance_id,
host,
_reaper_guard: None,
param_container: Arc::new(InstanceParameterContainer::new()),
was_playing_in_last_cycle: false,
sample_rate: Default::default(),
block_size: 0,
is_plugin_scan: false,
lazy_data: OnceLock::new(),
instance_panel: Rc::new(InstancePanel::new(instance_id)),
}
}

fn get_info(&self) -> Info {
firewall(|| Info {
Info {
name: "Helgobox - ReaLearn & Playtime".to_string(),
vendor: "Helgoboss".to_string(),
unique_id: HELGOBOX_UNIQUE_VST_PLUGIN_ID,
Expand All @@ -127,8 +123,7 @@ impl Plugin for HelgoboxPlugin {
inputs: 2,
outputs: 0,
..Default::default()
})
.unwrap_or_default()
}
}

fn get_parameter_info(&self, index: i32) -> Option<PluginParameterInfo> {
Expand All @@ -146,144 +141,128 @@ impl Plugin for HelgoboxPlugin {
}

fn init(&mut self) {
firewall(|| {
// Trick to find out whether we are only being scanned.
self.is_plugin_scan = unsafe { (*self.host.raw_effect()).reserved1 == 0 };
if self.is_plugin_scan {
tracing::debug!("Helgobox is being scanned by REAPER");
return;
}
tracing::debug!("Helgobox is being opened by REAPER");
self._reaper_guard = Some(self.ensure_reaper_setup());
// At this point, REAPER cannot reliably give us the containing FX. As a
// consequence we also don't have a instance shell yet, because creating an incomplete
// instance shell pushes the problem of not knowing the containing FX into the application
// logic, which we for sure don't want. In the next main loop cycle, it should be
// possible to identify the containing FX.
let host = self.host;
Global::task_support()
.do_later_in_main_thread_from_main_thread_asap(move || {
let plugin = unsafe { (*host.raw_effect()).get_plugin() };
plugin.vendor_specific(INIT_INSTANCE_SHELL, 0, null_mut(), 0.0);
})
.unwrap();
});
// Trick to find out whether we are only being scanned.
self.is_plugin_scan = unsafe { (*self.host.raw_effect()).reserved1 == 0 };
if self.is_plugin_scan {
tracing::debug!("Helgobox is being scanned by REAPER");
return;
}
tracing::debug!("Helgobox is being opened by REAPER");
self._reaper_guard = Some(self.ensure_reaper_setup());
// At this point, REAPER cannot reliably give us the containing FX. As a
// consequence we also don't have a instance shell yet, because creating an incomplete
// instance shell pushes the problem of not knowing the containing FX into the application
// logic, which we for sure don't want. In the next main loop cycle, it should be
// possible to identify the containing FX.
let host = self.host;
Global::task_support()
.do_later_in_main_thread_from_main_thread_asap(move || {
let plugin = unsafe { (*host.raw_effect()).get_plugin() };
plugin.vendor_specific(INIT_INSTANCE_SHELL, 0, null_mut(), 0.0);
})
.unwrap();
}

fn get_editor(&mut self) -> Option<Box<dyn Editor>> {
firewall(|| {
// Unfortunately, vst-rs calls `get_editor` before the plug-in is initialized by the
// host, e.g. in order to check if it should the hasEditor flag or not. That means
// we don't know yet if this is a plug-in scan or not. We have to create the editor.
let boxed: Box<dyn Editor> =
Box::new(HelgoboxPluginEditor::new(self.instance_panel.clone()));
Some(boxed)
})
.unwrap_or(None)
// Unfortunately, vst-rs calls `get_editor` before the plug-in is initialized by the
// host, e.g. in order to check if it should the hasEditor flag or not. That means
// we don't know yet if this is a plug-in scan or not. We have to create the editor.
let boxed: Box<dyn Editor> =
Box::new(HelgoboxPluginEditor::new(self.instance_panel.clone()));
Some(boxed)
}

fn can_do(&self, can_do: CanDo) -> Supported {
firewall(|| {
use CanDo::*;
use Supported::*;
#[allow(overflowing_literals)]
match can_do {
SendEvents | SendMidiEvent | ReceiveEvents | ReceiveMidiEvent
| ReceiveSysExEvent => Supported::Yes,
// If we don't do this, REAPER for Linux won't give us a SWELL plug-in window, which
// leads to a horrible crash when doing CreateDialogParam. In our UI we use SWELL
// to put controls into the plug-in window. SWELL assumes that the parent window for
// controls is also a SWELL window.
Other(s) => match s.as_str() {
"hasCockosViewAsConfig" => Custom(0xbeef_0000),
"hasCockosExtensions" => Custom(0xbeef_0000),
// This is necessary for REAPER 6.48 - 6.51 on macOS to not let the background
// turn black. These REAPER versions introduced a change putting third-party
// VSTs into a container window. The following line prevents that. For
// REAPER v6.52+ it's not necessary anymore because it also reacts to
// "hasCockosViewAsConfig".
"hasCockosNoScrollUI" => Custom(0xbeef_0000),
_ => Maybe,
},
_ => Maybe,
use CanDo::*;
use Supported::*;
#[allow(overflowing_literals)]
match can_do {
SendEvents | SendMidiEvent | ReceiveEvents | ReceiveMidiEvent | ReceiveSysExEvent => {
Supported::Yes
}
})
.unwrap_or(Supported::No)
// If we don't do this, REAPER for Linux won't give us a SWELL plug-in window, which
// leads to a horrible crash when doing CreateDialogParam. In our UI we use SWELL
// to put controls into the plug-in window. SWELL assumes that the parent window for
// controls is also a SWELL window.
Other(s) => match s.as_str() {
"hasCockosViewAsConfig" => Custom(0xbeef_0000),
"hasCockosExtensions" => Custom(0xbeef_0000),
// This is necessary for REAPER 6.48 - 6.51 on macOS to not let the background
// turn black. These REAPER versions introduced a change putting third-party
// VSTs into a container window. The following line prevents that. For
// REAPER v6.52+ it's not necessary anymore because it also reacts to
// "hasCockosViewAsConfig".
"hasCockosNoScrollUI" => Custom(0xbeef_0000),
_ => Maybe,
},
_ => Maybe,
}
}

fn get_parameter_object(&mut self) -> Arc<dyn PluginParameters> {
self.param_container.clone()
}

fn vendor_specific(&mut self, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize {
firewall(|| {
// tracing_debug!("VST vendor specific (index = {})", index);
self.handle_vendor_specific(index, value, ptr, opt)
})
.unwrap_or(0)
// tracing_debug!("VST vendor specific (index = {})", index);
self.handle_vendor_specific(index, value, ptr, opt)
}

fn process_events(&mut self, events: &Events) {
firewall(|| {
assert_no_alloc(|| {
let is_transport_start = !self.was_playing_in_last_cycle && self.is_now_playing();
let block_count = GLOBAL_AUDIO_STATE.load_block_count();
let sample_count = block_count * self.block_size as u64;
let device_sample_rate = GLOBAL_AUDIO_STATE.load_sample_rate();
for e in events.events() {
let our_event = match MidiEvent::from_vst(e) {
Err(_) => {
// Just ignore if not a valid MIDI message. Invalid MIDI message was
// observed in the wild: https://github.com/helgoboss/helgobox/issues/82.
continue;
}
Ok(e) => e,
};
let timestamp = ControlEventTimestamp::from_rt(
sample_count,
device_sample_rate,
our_event.offset().to_seconds(self.sample_rate),
);
let our_event = ControlEvent::new(our_event, timestamp);
if let Some(lazy_data) = self.lazy_data.get() {
lazy_data.instance_shell.process_incoming_midi_from_plugin(
our_event,
is_transport_start,
self.host,
);
assert_no_alloc(|| {
let is_transport_start = !self.was_playing_in_last_cycle && self.is_now_playing();
let block_count = GLOBAL_AUDIO_STATE.load_block_count();
let sample_count = block_count * self.block_size as u64;
let device_sample_rate = GLOBAL_AUDIO_STATE.load_sample_rate();
for e in events.events() {
let our_event = match MidiEvent::from_vst(e) {
Err(_) => {
// Just ignore if not a valid MIDI message. Invalid MIDI message was
// observed in the wild: https://github.com/helgoboss/helgobox/issues/82.
continue;
}
Ok(e) => e,
};
let timestamp = ControlEventTimestamp::from_rt(
sample_count,
device_sample_rate,
our_event.offset().to_seconds(self.sample_rate),
);
let our_event = ControlEvent::new(our_event, timestamp);
if let Some(lazy_data) = self.lazy_data.get() {
lazy_data.instance_shell.process_incoming_midi_from_plugin(
our_event,
is_transport_start,
self.host,
);
}
});
}
});
}

fn process_f64(&mut self, buffer: &mut AudioBuffer<f64>) {
firewall(|| {
assert_no_alloc(|| {
// Get current time information so we can detect changes in play state reliably
// (TimeInfoFlags::TRANSPORT_CHANGED doesn't work the way we want it).
self.was_playing_in_last_cycle = self.is_now_playing();
if let Some(lazy_data) = self.lazy_data.get() {
#[cfg(feature = "playtime")]
lazy_data.instance_shell.run_playtime_from_plugin(
buffer,
crate::domain::AudioBlockProps::from_vst(buffer, self.sample_rate),
);
lazy_data.instance_shell.run_from_plugin(self.host);
}
});
assert_no_alloc(|| {
// Get current time information so we can detect changes in play state reliably
// (TimeInfoFlags::TRANSPORT_CHANGED doesn't work the way we want it).
self.was_playing_in_last_cycle = self.is_now_playing();
if let Some(lazy_data) = self.lazy_data.get() {
#[cfg(feature = "playtime")]
lazy_data.instance_shell.run_playtime_from_plugin(
buffer,
crate::domain::AudioBlockProps::from_vst(buffer, self.sample_rate),
);
lazy_data.instance_shell.run_from_plugin(self.host);
}
});
let _ = buffer;
}

fn set_sample_rate(&mut self, rate: f32) {
firewall(|| {
tracing::debug!("VST set sample rate");
self.sample_rate = Hz::new_panic(rate as _);
if let Some(lazy_data) = self.lazy_data.get() {
lazy_data.instance_shell.set_sample_rate(rate);
}
});
tracing::debug!("VST set sample rate");
self.sample_rate = Hz::new_panic(rate as _);
if let Some(lazy_data) = self.lazy_data.get() {
lazy_data.instance_shell.set_sample_rate(rate);
}
}

fn suspend(&mut self) {
Expand Down Expand Up @@ -553,10 +532,6 @@ fn write_to_c_str(dest: *mut c_void, src: String) -> Result<(), &'static str> {
Ok(())
}

fn firewall<F: FnOnce() -> R, R>(f: F) -> Option<R> {
catch_unwind(AssertUnwindSafe(f)).ok()
}

/// This is our own code. We call ourselves in order to safe us an Arc around
/// the instance shell. Why use an Arc (and therefore make each internal access to the instance shell have to
/// dereference a pointer) if we already have a pointer from outside.
Expand Down
18 changes: 7 additions & 11 deletions main/src/infrastructure/plugin/helgobox_plugin_editor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use reaper_low::firewall;
use reaper_low::raw::HWND;

use std::os::raw::c_void;
Expand All @@ -21,28 +20,25 @@ impl HelgoboxPluginEditor {

impl Editor for HelgoboxPluginEditor {
fn size(&self) -> (i32, i32) {
firewall(|| self.instance_panel.dimensions().to_vst()).unwrap_or_default()
self.instance_panel.dimensions().to_vst()
}

fn position(&self) -> (i32, i32) {
(0, 0)
}

fn close(&mut self) {
firewall(|| self.instance_panel.close());
self.instance_panel.close();
}

fn open(&mut self, parent: *mut c_void) -> bool {
firewall(|| {
self.instance_panel
.clone()
.open_with_resize(Window::new(parent as HWND).expect("no parent window"));
true
})
.unwrap_or(false)
self.instance_panel
.clone()
.open_with_resize(Window::new(parent as HWND).expect("no parent window"));
true
}

fn is_open(&mut self) -> bool {
firewall(|| self.instance_panel.is_open()).unwrap_or(false)
self.instance_panel.is_open()
}
}
Loading

0 comments on commit 652e469

Please sign in to comment.