Migrating v1 Plugins to v2

This document is a work-in-progress and will be finalized when the Rack 2 ABI is declared stable.

The API of VCV Rack 2 has been designed to be nearly backward-compatible with v1 plugins. This means that 90% of plugins will only require a version update and a recompile (a 1-line edit, 15 seconds of work).

For the other 10% of plugins using advanced or unstable API features, updating to v2 is easy and involves following a few search-and-replace steps.

Additionally, Rack 2 includes several new optional enhancements that plugin developers can use to improve usability of their plugins in section 3.

1.1) Update plugin.json version string to v2

The major version number (MAJOR.MINOR.REVISION) of your plugin must match the major version of Rack, so change the version number in your plugin’s plugin.json manifest to 2. For example, change

{
  "slug": "Fundamental",
  "version": "1.2.3",
  ...

to

{
  "slug": "Fundamental",
  "version": "2.0.0",
  ...

If you wish, you may keep the minor and revision numbers unchanged, e.g. 2.2.3.

Download the latest Rack 2 SDK. Clean and compile the plugin.

export RACK_SDK=/path/to/Rack-SDK
make clean
make dist

If your plugin compiles successfully, you can skip ahead to Potential runtime bugs. If not, read on to solve potential errors or warnings.

1.2) ParamWidget::paramQuantity has been replaced by getParamQuantity()

If your ParamWidget methods use the paramQuantity member variable, replace it with the getter method, or add this line to the top of your method.

ParamQuantity* paramQuantity = getParamQuantity();

1.3) ParamWidget::reset() and ParamWidget::randomize() has been removed

If you would like to disable resetting or randomization of a particular parameter when the user resets/randomizes the module, set these properties in your Module constructor.

getParamQuantity(MY_PARAM)->resetEnabled = false;
getParamQuantity(MY_PARAM)->randomizeEnabled = false;

Alternatively, you can override the following methods in your Module class to implement custom behavior for all parameters and internal state.

void onReset(const ResetEvent& e) override {
	params[MY_PARAM].setValue(0.f);
	// ...

	// Call super method if you wish to include default behavior
	// Module::onReset(e);
}

void onRandomize(const RandomizeEvent& e) override {
	params[MY_PARAM].setValue(random::uniform());
	// ...

	// Call super method if you wish to include default behavior
	// Module::onRandomize(e);
}

1.4) Window::loadSvg() has been deprecated and moved to Svg::load()

Search and replace:

perl -p -i -e 's/APP->window->loadSvg\b/Svg::load/g' src/*.{cpp,hpp}

If your plugin still has build errors, open a thread in the VCV development forum or contact VCV support to describe your build error.

2) Potential runtime bugs

You might encounter these issues while testing.

2.1) Don’t store Font and Image references across multiple frames

The DAW plugin version of Rack uses a different OpenGL context each time the plugin editor window is closed and reopened. This means that if you load and store a Font or Image in a widget’s constructor with font = APP->window->loadFont(...) or image = APP->window->loadImage(...), its OpenGL reference will be invalid if the window is reopened later.

Instead, save only its path, and fetch the font/image each frame in draw(). Example:

void draw(const DrawArgs& args) override {
	std::shared_ptr<Font> font = APP->window->loadFont(fontPath);
	if (font) {
		nvgFontFaceId(args.vg, font->handle);
		...
	}
}

loadFont() and loadImage() caches the font/image by its path, so this operation can be efficiently called in draw() every frame.

3) New optional v2 API features

While not required, these new API features in Rack 2 can enhance the usability of your modules.

3.1) Add Port and Light labels

Add names to your ports and lights which appear in tooltips.

For example, in your Module constructor:

configInput(PITCH_INPUT, "1V/oct pitch");
configOutput(SIN_OUTPUT, "Sine");
configLight(PHASE_LIGHT, "Phase");

3.2) Replace configParam() with configButton() or configSwitch() as applicable

For momentary buttons and multi-state switches, displaying a real-valued parameter to the user doesn’t make much sense.

Instead, use configButton() to hide the text field in its context menu, or configSwitch() to offer a list of choices.

configButton(TAP_PARAM);
configSwitch(SYNC_PARAM, "Sync mode", {"Soft", "Hard"});

3.3) Add bypass routes

If your module is bypassed by the user (via the context menu or key command), you can route certain inputs directly to outputs, bypassing all processing. This would make sense for a filter or reverb, or even a clock divider or quantizer, but it would not make sense for a VCO, since it generates a signal rather than processes one.

For example, in your Module constructor:

configBypass(LEFT_INPUT, LEFT_OUTPUT);
configBypass(RIGHT_INPUT, RIGHT_OUTPUT);

Alternatively, override Module::processBypass() to implement custom bypass behavior.

3.4) Store large data in the module’s patch storage directory

Instead of serializing large (>100 KB) buffers in toJson/fromJson() methods which could lag the UI, read/write to the directory returned by createPatchStorageDirectory() and getPatchStorageDirectory().

Example:

void onAdd(const AddEvent& e) override {
	std::string path = system::join(createPatchStorageDirectory(), "wavetable.wav");
	// Read file...
}

void onSave(const SaveEvent& e) override {
	std::string path = system::join(createPatchStorageDirectory(), "wavetable.wav");
	// Write file...
}

Note: You cannot call these methods in the Module constructor since it is not added to the Engine at that point.

3.5) Draw custom light-like widgets at full brightness

Rack 2 allows users to decrease the brightness of the rack, leaving lights at full brightness.

If your custom widget should emit light, set the global tint to white in its draw() method.

void draw(const DrawArgs& args) override {
	nvgGlobalTint(args.vg, color::WHITE);
	...
}