VCV API module deletion / onExpanderChange() bug and work around - resolved

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. :frowning: 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.

5 Likes

It does help a lot. Thanks for the detailed explanation and practical workaround. I’ve wasted a lot of time trying make my expanders work correctly. This will simplify things considerably.

Still hoping for the onExpanderChange() call by vcv, (or perhaps onExpanderDelete/Undelete to prevent existing code from stuttering) but until then I’ll be using this.

PS: Did you also test the case where you delete an expander inside a chain and then undo the delete using the keyboard? I had to override onAdd() as well.

Yes, undo of deletion simply worked for me without any additional coding.

One last thing regarding my hack.

If a module is deleted while Smart rearrangement is enabled, then my hack will first clear the expander and call onExpanderChange(). Then the widgets are rearranged, closing the gap, and the widget code sets the expander to the new neighbor. Then Engine::updateExpander_NoLock() calls onExpanderChange() a second time. So onExpanderChange could be called twice within one frame using my hack.

Without the hack, VCV module deletion first clears the expander, then the widget sets the expander to new level, and then Engine::updateExpander_NoLock() calls onExpanderChange() only once. This would be true even after VCV fixes the bug.

I shouldn’t think calling onExpanderChange() twice in one frame would cause a problem for most code. I know my code has no problem with that. But I imagine someone could be doing something weird / intricate where two calls to onExpanderChange() in one frame could cause a problem.

Good news - after reporting the bug to VCV support, I got a message from Andrew stating the bug has been fixed in the next version of VCV Rack. However, I have no idea when the next version will be released.

4 Likes

:raised_hands:

:smile:

Thanks Dave!