Multiple instances and memory space ( VST 2.4 )...

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

Bumping this thread, I have a question. I've been trying to figure out a way to use the PE's data section to store my AudioEffect and AEffect structures. What I am not sure of, is how this will be done WRT multiple plugin instances within one host process that may either thread or sandbox the call to LoadLibrary. Or IOW, I can't figure out if the PE section will need to be large enough for only one instance or all of them.

Will the call to LoadLibrary ever involve duplication of data sections?

I'm attempting this to avoid use of malloc etc. for better code size.

Post

Will the call to LoadLibrary ever involve duplication of data sections?
If the shared library is being loaded to the same process address space, then no. If it's being loaded to a separate process then one starts a separate process first anyway, so that is how data sections are being duplicated.

edit: You can rename your DLL though, and then all its data can be duplicated.
~stratum~

Post

This idea should be threading safe if per-instance data is separated. Right? :?

Post

Basically there is one instance of each DLL for each process and the data section is shared among threads. It's not thread safe for anything other than the constants (the data that you don't modify, not necessarily the data declared to be constant). The variables that you mark to be 'thread local' with _declspec( thread ) are safe, though as the compiler generates special code for these and they aren't really in the DLL 'data section'.

Per-instance data (in the sense of plugin instance not DLL instance) is in the heap, not in the DLL data section.
~stratum~

Post

Why would it not be thread safe, if each instance read and write to non-overlapping addresses?

Post

Someone needs to determine where those non-overlapping addresses are on a dynamic basis, which is the very malloc/heap allocation you're trying to avoid.

Avoiding heap allocation on a dynamic system like a modern OS is simply impossible. Don't even go there.

To reduce code size you might use large chunks: so rather than using heap allocation for all objects limit your allocation to only the instance itself.

This is like:

Code: Select all

struct dynamic_t
{
	enum class constants
	{
		members = 16,
	};
	
	dynamic_t()
	{
		for (int i = 0; i < constants::members; i++) {
			member[i] = new type_t(...);
		}
	}
	
	~dynamic_t()
	{
		for (int i = 0; i < constants::members; i++) {
			delete member[i];
		}
	}

	type_t *member[constants::members];
};
Vs.

Code: Select all

struct static_t
{
	enum class constants
	{
		members = 16,
	};
	
	static_t() {}
	~static_t() {}

	type_t member[constants::members];
};
This is the proper way to write C++ anyway: RAII

You should never be manually calling new/delete, ever. Objects should never contain pointers to other objects allocated by/within the object itself. That doesn't make sense at all and is just fragmenting the object all over the heap with 100s of allocations rather than a single allocation.

If you know ahead of time the maximum length of a string; it means you can use templates and allocate into a static array with an index like so:

Code: Select all

template <typename T, int max_length>
struct static_stack_t
{
	static_stack_t()
	{ 
		reset(); 
	}

	~static_stack_t()
	{
	}

	T operator[](int i)
	{
		return stack[i];
	}

	void add(T x)
	{
		stack[length] = x;
		length++;
	}

	void remove(T x)
	{
		int i;
		for (i = 0; i < length; i++) {
			if (stack[i] == x) {
				break;
			}
		}

		if (i < length) {
			// found the entry, remove it from the stack
			length--;
			for (; i < length; i++) {
				stack[i] = stack[i + 1];
			}
		}
	}

	void reset()
	{ 
		length = 0; 
	}

	int length;
	static_array_t<T, max_length> stack;
};
Obviously an ordered stack is more expensive than an unordered stack: if the order isn't important you can replace a removed element with the last element and decrement the length.

Obviously stl/boost contain all these things. I don't like the fact they include obscure rules that can lead to bug-prone code. I prefer "exactly as much as is needed" vs. "everything and more".
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

camsr asked me about the aeffect struct: VST2's primary interface.

You create an interface in C or C++ to maintain state. For example all the things NOT IN aeffect go in your interface, let's call this:

Code: Select all

struct my_vst_plugin
{
	...
};
So aeffect->object is a pointer to the C++ class "AudioEffectX", if it does not point to that interface (if you don't use the VSTSDK) it MUST BE NULL! (C++ = nullptr;)

aeffect->user is a pointer which MAY BE used by the plug-in if it does NOT USE the VSTSDK.

So in this case:

Code: Select all

aeffect->object = NULL;
aeffect->user = &my_vst_plugin;
Now when a function like aeffect->process(aeffect, ...) is called:

Code: Select all

process(aeffect *aeff_ptr, **inputs, **outputs, samples)
{
	my_vst_plugin *interface = (my_vst_plugin *)aeff_ptr->user;
	interface->process(inputs, outputs, samples);
}
aeffect->reserved[2] MUST BE zeroed in the entry point function before the new aeffect instance is returned. These are "host pointers" which the host MAY USE to point to anything it wants. These pointers MUST NOT be read, dereferenced or written by the plug-in or anywhere other than the host!

The host MUST NOT ever read, dereference or write either aeffect->object or aeffect->user!

I, lord Vhtthisy hath spoken.
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post

Thank you for the clarification.
Is your code above (using process) some kind of wrapper, or is it referencing itself?

EDIT: just realized that it's referencing a different function (but the use of the same identifier is bad imo)
Last edited by camsr on Sat Feb 03, 2018 7:49 pm, edited 1 time in total.

Post

Why would it not be thread safe, if each instance read and write to non-overlapping addresses?
I have only told what was happening in general because I haven't been working with the VST sdk for a long time and didn't really understood what you are trying to do. Global data is not thread safe for anything other than read-only access because it's shared after all. The only global data that is thread-safe to write access are those you declare to be 'thread local' and while these are 'global' they aren't shared among threads that's why they are both global and thread-safe. "Non-overlapping addresses" also means that that data is not shared among 'something', and whether it's thread safe or not depends on what that 'something' is. If 'something' refers simply to 'plugin instances', then obviously a single plugin instance contains multiple threads therefore it's not thread safe in that case.
~stratum~

Post

camsr wrote:Thank you for the clarification.
Is your code above (using process) some kind of wrapper, or is it referencing itself?

EDIT: just realized that it's referencing a different function (but the use of the same identifier is bad imo)
In C++ we use something called polymorphism which means 1000s of objects with 1000s of functions can have the same name: process()

In fact the proper way to do this is generally to use operator(), which makes the code look like:

Code: Select all

my_object &ref = *ptr;
auto result = ref(io_t(inputs, outputs, channels, flags));
operator() is the "invocation operator", so in other words you're calling the object as if it is a function itself. If an object has a core function like process(), it makes sense to do away with the function name entirely and use operator() in many cases.

Especially so when you're writing c++ template functions that use generic types and operator() (this is called a "functor") where you can specify behaviors as template parameters.

For example:

Code: Select all

process_stereo_t<tanh_t, oversampler_t> ws_tanh;
process_stereo_t<acos_t, oversampler_t> ws_acos;
process_stereo_t<parabola_t, oversampler_t> ws_parabola;
process_stereo_t<cubic_t, oversampler_t> ws_cubic;
process_stereo_t<sigmoid_t, oversampler_t> ws_sigmoid;

process(...)
{
io_t io(inputs, outputs, samples, flags);
switch (shaper.type) {
case shaper_type_t::tanh: ws_tanh(io); break;
case shaper_type_t::acos: ws_acos(io); break;
case shaper_type_t::parabola: ws_parabola(io); break;
case shaper_type_t::cubic: ws_cubic(io); break;
case shaper_type_t::sigmoid: ws_sigmoid(io); break;
}
Now all that decision making is done outside the loop, which is much more optimal than having a function pointer or switch statement inside the inner-most loop.

This can be expanded to as many dimensions as possible: for example why not have 30 waveshapers and 30 over-sampling modes with 30 filters and 30 types of modulation? That might seem incredibly complicated but you only need to handle 4 objects to accomplish this and it all works out to a single function call: process().

This makes code much easier to maintain and multiplies the scale of the code you can maintain by many orders of magnitude compared to C.

It is however dangerous if you make mistakes: "C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do it blows your whole leg off."
Free plug-ins for Windows, MacOS and Linux. Xhip Synthesizer v8.0 and Xhip Effects Bundle v6.7.
The coder's credo: We believe our work is neither clever nor difficult; it is done because we thought it would be easy.
Work less; get more done.

Post Reply

Return to “DSP and Plugin Development”