.NET Development for Rack

I’m working on a somewhat long-term project that is going to allow plugin development for Rack via the languages supported by the .NET framework, such as C# and VB .NET.

I started the project back in July and haven’t made a public repo yet, but so far the C# plugin has been successfully pulled into Rack, and some of its init code is successfully called. I’m stuck at creating the model and setting it to a widget, as there are some issues there, but I’m optimistic.

Here’s some example code that successfully outputs to my log file once initialized by Rack: image

I’m working on making some static functions in the Rack lib to help with passing pointers between Rack and plugins, as it is currently hard to inject functionality without such functions.

Anyway, expanding Rack plugin development to the .NET framework could definitely open up opportunities in the long-run.

Let me know any ideas you all have. The reason I created this thread is to get feedback and ideas. Thanks!

2 Likes

It would be interesting to hear from VCV if this is something they would support for plugins in the library.

1 Like

From everything I’ve heard .NET is not suitable for realtime (ie. audio DSP) purposes, but good luck I guess :slight_smile:

2 Likes

If you’re going full OOP on everything, then you’re going to have hurt performance due to a focus on world-oriented design instead of data-oriented design. That’s an issue with any language, including C and C++. That aside, there are plenty of scenarios for .NET in DSP and high-performance applications.

For example, the Unity Engine rolled out DSPGraph for their Data-Oriented Tech Stack (DOTS). There are even .NET libraries out there for DSP, such as NWaves.

I think the misunderstanding we often have is about how a language is most often used instead of its capabilities. For C#, it is often used in object-oriented, deeply abstracted scenarios, like with Java, but that is not necessary in any way.

Yeah that would be good to know from VCV. I know they don’t accept PRs, so it would have to be something they’d be on board with. Otherwise we’d have to maintain a fork, which I want to avoid.

The problem with Java is not just object orientation, it’s the relative lack of control over memory allocation and destruction. For anyone who has wept as their system does a .1 second gc pause, you know what I mean, I think if you approach this you will want to make very sure that your process method is non allocating and you manage that carefully. Modern Java has ways to do this of course but still tricky. Presume .Net does too

The library build you can test yourself with the docker image. Last I saw it has none of the mono tools I think you would need to cross compile .net from linux image where all the builds. But you could probably make a dll image out of library and get there

If I was doing this project I would write a c++ module which calls into the managed language and use a macro to introduce it to the plugin with a bit of c++ and a mixed compile. But I think the project will be hard going honestly especially given the cross platform strategy rack uses

5 Likes

[EDIT: looks like @baconpaul and I were basically raising the same flag at the same time :)]

Don’t know if this is what @dreamer had in mind, but I’d expect the gotchas have more to do with managed language/garbage collection issues than the level of OOP involved. C# is typically written with the GC in mind and that’s not going to play well with Rack at all. (Here’s a recent Reddit discussion on .NET in a near-real-time scenario; doesn’t sound impossible, necessarily, but then you’re essentially doing backbends to use C# as if it were C++, which begs a question… :slight_smile: )

From what little I know of game audio, I expect that DSPGraph designed to be doing way less DSP than a typical audio-focused Rack module (i.e. good for dynamic mixing but you probably wouldn’t write a synth in it). NWaves looks neat but the examples seem pretty focused on non-real-time analysis applications.

No intention to be discouraging–language interop is a neat problem on its own!–but I guess if your goal is to make (say) C# into a viable alternative to C++ for general Rack development, I’d suggest (FWIW) that you port a few medium-weight audio modules over as early as possible in your process, so you can figure out if the GC issues (in particular) are worth working around.

3 Likes

Thanks for the details and the links! I definitely am aware of the drawbacks of the GC (no pun intended with your name), and once I get more of the module implementation working I will do a basic real-time implementation to see how much of a problem GC would be.

Currently the plugin.dll I’m creating is native AOT compiled. There are general incompatibilities and trimming issues with creating such single-file deployments, but it’s not hard to avoid most of those incompatibilities, especially if you bring code-gen tooling into the picture.

When it comes to GC, the first thing that comes to my head is the LOH (large-object heap), which is allocated to when an object passes 84KB I think. With such computationally-intensive processes as DSP, I’d imagine most of what we operate with are lighter data structures, like primitive data types and small structs, which are easy to keep off the LOH.

If you’re worried about too many hurdles on the DSP threads, I’d like to add that it’s really easy to program multithreaded code in C# if you know what you’re doing, which can be a huge bonus in performance (maybe have a separate thread that manages larger objects, if you really need them).

Also, I personally haven’t used these much, but System.Numerics and System.Runtime.Intrinsics are supported in native AOT as well (.NET 8 or later), which means you can bring beefed-up hardware computation to C# with relative ease. Again, this isn’t anything C++ or C can’t do, but it’s a great plus for C# to have it as well.

A lot of this awesome compatibility and ahead of time compilation is relatively new in .NET though, as .NET 8 introduced many new features for AOT native compilation. .NET 9 will come out in November (only two months away, but I’m using release candidates regularly), and I’ll be on the look out for new improvements.

Thanks for this awesome feedback! I’ll keep you all updated on progress I make towards a minimal plugin reproduction.

1 Like

This depends very much on the algo

Surge has an explicit pre allocate in place new strategy and a memory pool for voices and most things are indeed small. But an item like reverb2 has about 15mb of state

Threading is also super difficult in an audio context because you can easily end up with priority inversions (which of course is really the ur problem with allocation and gc). Most of us with performant software and multi threading have lock free data structures into and off of the audio thread

But the short version is: you have 16/48000 of a second to make 16 floats. If you are late the user will hear it instantly. So things like pausing to wait for an allocation or a mutex are audible errors. Also remember you are deploying into a cpu intensive environment with other modules.

I’m 5 or 6 versions back since I wrote .net and I’m sure there’s a lot there but I would suggest you proceed with caution regarding this class of issue from the very first instance. It’s remarkably easy to write software which appears to work until it’s used in a real performance environment.

(Also I’m not sure any of this will work in Mac and Linux and even if it does it would be built outside the library toolchain)

But look I’ve never discouraged someone trying something super hard that gets them excited! Go for it! I’m just trying to make sure you understand where the land mines are before you walk across the field

3 Likes

Oh one other thing. Most of the performant modules use simd parallelism. Again I’m sure net can do that but you may want it early in your research stack while scouting around how to proceed

2 Likes

Thank you! I’ll be looking out for those pitfalls.

Threading is also super difficult in an audio context because you can easily end up with priority inversions

I definitely feel that. For sure, I’ll be looking into more performant threading and preferably lock-free threading.

You have 16/48000 of a second to make 16 floats. If you are late the user will hear it instantly

Absolutely. This reminds me of performance issues in game development, where common tasks only have a 16ms interval to complete for 60fps or else there will be drops in performance. In this case, we’re talking about 1/3 ms, which is brutal, but at the same time, that’s 333 microseconds, which is a very reasonable amount of time for calculations when you look at it in μs instead of ms. You just have to benchmark everything if you’re unsure of its performance implications. One thing I do need to note is the round time for marshaling; I haven’t tested such performance yet.

Also I’m not sure any of this will work in Mac and Linux and even if it does it would be built outside the library toolchain

I have my code configured to import .dll, .dylib and .so libs for interop, and the code that is written in it should be cross-platform. A nice thing about the .NET build tools is that you can get analyzer warnings if some of your code is specific to a platform instead of supported by all platforms (unless you explicitly target that specific platform, of course). In order to ensure code really works on all platforms I would have to do cross-compilation and testing, but I don’t have Mac or Linux machines.

Most of the performant modules use simd parallelism

Yep, that’s what I referenced with the intrinsics libraries I mentioned. AVX2 support is needed for parts of that.

I look forward to learning more as I progress through this challenge, and I appreciate you noting these pitfalls!

1 Like