-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement in-place processing (fix Ardour support) #93
Conversation
…can implement it or not (eg. Atom).
I like the idea of using cells and this approach might actually be the best solution for it. At first, I was a bit set off by the idea of ultimately using e.g. However, I think that the additional API and the wrappers are unnecessary since they don't really prevent the user from writing to a read-only port. The user can still use a different and wrong port type and do whatever they like. The additional API does not change this and just adds a separate way to access ports, making it both harder to understand and to use. I would suggest something like this: pub struct InPlaceAudio;
unsafe impl UriBound for InPlaceAudio {
const URI: &'static [u8] = ::lv2_sys::LV2_CORE__AudioPort;
}
impl PortType for InPlaceAudio {
type InputPortType = &'static [Cell<f32>];
type OutputPortType = &'static [Cell<f32>];
#[inline]
unsafe fn input_from_raw(pointer: NonNull<c_void>, sample_count: u32) -> Self::InputPortType {
std::slice::from_raw_parts(pointer.as_ptr() as *const Cell<f32>, sample_count as usize)
}
#[inline]
unsafe fn output_from_raw(pointer: NonNull<c_void>, sample_count: u32) -> Self::OutputPortType {
std::slice::from_raw_parts_mut(pointer.as_ptr() as *mut Cell<f32>, sample_count as usize)
}
} Sure, the user can write to the input port with this implementation, but since they have already said that the same memory is also used by an output port (by saying it's an in-place port), this does not make a difference. We simply don't need to prevent the user from writing to the an in-place input port! 😅 I would also prefer to keep the previous channel types because it would break the API and not all hosts require in-place operations. Ardour support is a strong point, but LV2 isn't only Ardour's plugin standard as VST isn't only Cubase's plugin standard. If you don't need to use celled values, using the value directly is just easier. It's also the rustacean way of implementing channels and I would assume that upcoming LV2 hosts written in Rust will probably not use in-place operations. |
I could do that easily, however I don't think that would be necessary if we ditch the custom
I see your point there, I just personally like it a lot when APIs are as misuse-resistant as possible. 😋
I agree that LV2 isn't Ardour's plugin standard, so I did a quick check I noticed that of Ardour, Zrythm, and Carla, only Carla supports in-place-broken plugins (by assigning a separate input and buffer for each plugin, and Seeing this, I actually disagree on the fact that future Rust LV2 wouldn't use in-place operations in the future, because they are just a much more performant implementation. Using The only advantage of using Now, of course since that would be a breaking change, we could wait until we release another major version to do the swap, if the fix is out there I don't think there would be much of an emergency anymore. 🙂 |
Nice that someone works on this. The Cell solution sounds perfect for me. I've not been familiar enough with Rust to come up with that in the Issue.
LV2 is still 0.x and are there many plugins that depend on rust-lv2 out there? On crates.io I see no dependents on rust-lv2. I would be eager to test this, but my plugins all require UI unfortunately. |
Awesome! Then, we also don't need the test since this is a safe and supported operation!
Thank you! 😊 However, I still don't like that that the
Another advantage of uncelled slices is that you can share them between threads and therefore can work on the input data from different threads without needing to copy them to a shareable buffer first. You may say that you can't do this with hard RT constraints, but almost no one has hard real-time constraints in audio processing, meaning that no one dies if a single frame is dropped while you're mixing your album. You can not achieve hard real-time constraints with a normal CPU and a normal operation system anyway due to caching, branch prediction and whatnot. Audio processing in a DAW running on a Mac or PC has soft real-time constraints and one might achieve their soft real-time constraints more easily with parallelized plugins due to the rise of many-core CPUs like the AMD Ryzen CPUs or Apple's ARM chips. We should leave space for this development and keep the uncelled port types.
While the lv2 crate is still at 0.6.0, the |
Yes, I've been trying to find a more DRY solution, but I think having
That would be only true for the input, not the output ( To give you an example, a small track I've made recently in Ardour had 30+ tracks and busses, each of which had between 5 and 10 plugins on them, and it was no sweat for my 8-core/16-thread mobile CPU.
If I was mixing for a mafia party and I had X-runs/dropped frames, pretty sure I'd get killed. 🙃
Even when a plugin is hard-RT capable, I've also found myself hunting for plugins that take too much time processing (some of them even in the 20ms+ range!), and had to (painfully) sacrifice them for a lighter alternative, on that fact alone.
This is not a new nor "future" developments: DAWs have been taking advantage of many-core CPUs for many years now, they can scale to N cores already. Moreover, if multithread processing of single plugins becomes a thing at some point, then the current uncelled port types will be very much incompatible with the current
Yeah, that kinda sucks, but I agree it's best not to wreck what's on crates.io already. |
I just saw work is progressing to solve the inplacebroken issue. I read all the comment, but honestly, i didn't understood what part of inplacebroken issue the current work try to solve ? i just understood it wouldn't solve the read after write issue. |
This work makes it so plugins with potentially shared input and output buffers (i.e. those without the Reading after writing is fine to not enforce through an API: it's incorrect to do so, but it is not unsafe (see the whole first chapter of the Nomicon for to get a better idea on what's safe and what isn't). What is unsafe is allowing reading while writing (or, more exactly, allowing writing while still having a shared reference to the thing you just wrote to), because you've pretty much swept the rug under the feet of other parts of the code that may have checked for invariants on that data, which are suddenly not true anymore. Except the code that's running on that data has no idea it has changed, and it runs into UB. In practice however, LV2 plugin developers never needed to "hold invariants" of any sorts on audio buffers: they are just a big array of very simple types (here, This specific case is best represented by the |
By the way, @Janonard I'm moving the discussion about "should we keep the non-cell port types or not?" to #94, because while I think this discussion is very important, it does not need to be resolved before this PR is implemented. ^^ |
…documentation to both normal and InPlace core port types.
@Janonard Done! I actually took the time to document the whereabouts of using the I also took the liberty of splitting the port types into their own files (but kept the same mod structure of course), with all those docs and types it was getting a little cramped in there. ^^ |
Hi @prokopyl , it's nice to see you back! :-) Thank you for this pull request, it helps me to re-frame my thinking about this topic. I share my initial thoughts with you below and finish with an idea that is inspired by this PR. First thought: I think an
I don't think hosts are actually required to also use the memory of an input port for an output port. That would be very impractical e.g. for a plugin that has two input ports and only one output port.
Well, nobody says you can't publish version 0.1.0 after version 3.0.0 ;-) LV2 is still very young and we may need some more iterations, so I don't think anybody is going to be mad when you switch to a pre-1.0 version number to reflect that. This PR actually inspired me for another idea, which is to enforce at compile time that the input ports and the output ports cannot be mixed. The idea is a little as follows: struct InputPorts;
struct OutputPorts;
struct Ports {
inputs: InputPorts,
outputs: OutputPorts
}
impl Ports {
fn inputs(&self) -> &InputPorts {
&self.inputs
}
fn outputs(&mut self) -> &mut OutputPorts {
&mut self.outputs
}
} The let mut ports = Ports::new();
let inputs = ports.inputs();
let outputs = ports.outputs();
let a = &inputs;
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The chapter in the book isn't updated to the fact that inPlaceBroken
isn't required anymore. But as I've said before: You don't need to do this in this PR. I will create an issue for that. However, please note that the next version will not be released before this is done.
Thank you for your contribution!
This PR addresses the issue described in #89 that
rust-lv2
currently doesn't support in-place processing, i.e. where the input buffer and output buffer are the same.This PR implements an approach that wasn't discussed in #89 however, which is to leverage the
Cell
type to prevent creating references to the contents of the buffers (input or output).This is actually not done directly using the standard
Cell
type, but withReadCell
andWriteCell
types, which are respectively read-only and write-only wrappers aroundCell
to allow restricting their usage to input ports or output ports.Using
Cell
s has two big advantages:Copy
types (like all audio sample types, such as LV2'sf32
), and are by design safe to keep multiple references to (here, one for input, one for output). Therefore, theinPlaceBroken
requirement on Rust LV2 plugins can be lifted.Cell
being a zero-overhead abstraction, this implementation actually does nothing at runtime).The only downside of this approach is that it does not actually prevent from reading from the buffer after writing to it. However, doing so is not unsafe if there are no references to the contents (reading a valid but arbitrary number cannot trigger UB, and thanks to
Cell
other constraints cannot be put), it only results in incorrect behavior. Moreover, I believe this is rather trivial to avoid for plugin developers, so I don't think it is worth trying to prevent.This PR also adds a new I/O port set:
InPlaceInput
andInPlaceOutput
, allowing to use port types that implementsInPlacePortType
, which return the new Cells instead of just raw references.Duplicating the port trait infrastructure allows to not break backwards compatibility yet, but this raises a couple questions:Should the in-place-compatible port types be the default? A quickripgrep
through my rather big LV2 plugin library shows that none of the plugins I use actually requireinPlaceBroken
. This makes sense to me, since making in-place-compatible processing seems rather trivial.Moreover, the fact that Ardour, a major player in the LV2 ecosystem (if not the reference? I believe some Ardour devs are also LV2 devs), does not supportinPlaceBroken
plugins is a pretty big sign that very few (if any) LV2 plugins actually use this feature, and that really, none should use it.This can be done rather easily by renaming this PR'sInPlace*
types to their current counterparts, and making the current implementations into some sort ofExclusivePort*
alternative.Should in-place-broken processing be supported at all? The more I dive into it, the less plausible it seems to me that plugin developers would actually want to use it, the single fact that Ardour users wouldn't be able to run it being quite a big deterrent in itself. We could remove the non-in-place-compatible implementations altogether from this crate (only leaving the Cell ones), and if users somehow need long references that point inside the buffer we could consider it uncharted territory, and let them useunsafe
if they see fit.(EDIT: This part of the discussion has been moved to #94, since we can implement this without breaking anything yet. 🙂 )
I'm keeping this PR as a draft since there are a few doc items missing still.
Closes #89.