The following describes a bug that exists within the VCV API as of VCV Rack version 2.4.1.
Every rack module maintains a pair of Module::Expander
objects, one for the left neighbor, and one for the right. Among other things, each Expander
object has a module*
pointer to the neighbor module, as well as the moduleId
of the neighbor. If there is no neighbor, then the module* = NULL
, and the moduleId = -1
.
The Module::onExpanderChange()
event handler is supposed to be called whenever a neighbor is added, deleted, or changed. It works great as long as a neighbor is added or changed due to an insertion or move. But if a neighbor is deleted, then VCV is failing to call onExpanderChange()
. This wreaks havoc on my Venom plugin that supports chains of expanders. My code relies on being properly informed of changes because I cache the expander module pointers after being re-cast as VenomModule*
. If my code is not informed of changes properly, then it can lead to crashing VCV Rack because the code tries to access an invalid pointer.
The problem may be masked if “Smart rearrangement” is enabled and the module that is deleted has a neighbor on both sides. This is because the gap is closed, and a new neighbor is established. Buf if the deleted module is on the end, or “Smart rearrangement” is disabled, then onExpanderChange()
is not called.
I have analyzed the VCV code and diagnosed where the problem is.
When a neighbor changes due to an insertion or move, RackWidget::updateExpanders()
sets the Expander moduleId
to -1, but leaves the module*
pointer as is. Later on, Engine::updateExpander_NoLock()
detects the mismatch between the module*
and the moduleId
, sets the module*
to the correct value, and then calls onExpanderChange()
. Perfect.
But when a module is deleted, Engine::removeModule_NoLock()
sets the moduleID
to -1 and sets the module*
to NULL. Later on, Engine::updateExpander_NoLock()
fails to detect any mismatch between moduleId
and module*
, so onExpanderChange()
never gets called. The Expanders have the correct information, but plugin code is never informed there was a change.
I have informed VCV support of the problem.
My workaround solution is pretty simple, and I think elegant. It should work for any plugin as long as you only care about expander changes that involve one of your plugin modules. It will not work if you need to be informed of expander changes involving foreign modules.
The onRemove()
event is called before Engine::removeModule_NoLock()
updates the expander info. All of my modules are derived from a VenomModule
class (struct). So within VenomModule
I define onRemove()
that updates the expander moduleId
and module*
the same way as removeModule_NoLock()
, and then my code calls the onExpanderChange()
. My onExpanderChange()
receives the correct information, and all is good.
Here is the actual code:
// Hack workaround for VCV bug when deleting a module - failure to trigger onExpanderChange()
// Remove if/when VCV fixes the bug
void onRemove(const RemoveEvent& e) override {
Module::ExpanderChangeEvent event;
Module::Expander expander = getRightExpander();
if (expander.module){
expander.module->getLeftExpander().module = NULL;
expander.module->getLeftExpander().moduleId = -1;
event.side = 0;
expander.module->onExpanderChange(event);
}
expander = getLeftExpander();
if (expander.module){
expander.module->getRightExpander().module = NULL;
expander.module->getRightExpander().moduleId = -1;
event.side = 1;
expander.module->onExpanderChange(event);
}
}
Assuming there is not a VCV bug fix soon, I will be releasing a new Venom version with the bug fix (and new modules!) in the near future.
I hope this helps others.