BR: PluginDescription::uid for VST3 don't match between windows and macOS

If a user saves a project on Windows that uses a VST3 plugin and then tries to load the project on macOS (or vice versa) the plugin won’t be found since JUCE plugins generate different plugin uids between windows and all other platforms.

The offending code is here is in the function getUUIDForVST2ID:

     // VST3 doesn't use COM compatible UUIDs on non windows platforms
    #if ! JUCE_WINDOWS
     q0.word = ByteOrder::swap (q0.word);
     q1.half = ByteOrder::swap (q1.half);
     q2.half = ByteOrder::swap (q2.half);

Comment about says:

// NB: Nasty old-fashioned code in here because it's copied from the Steinberg example code.

So apparently it is ‘correct’ to swap these bytes in the uid?

Then in createPluginDescription to identify the plugin:

description.uid = getHashForTUID (info.cid);

So I’m not sure this is ‘fixable’ without breaking all existing hosts. And is using this cid even the correct way to uniquely identify plugins? If so, the cids should all be converted to the same format before hashing.

Any thoughts on how to identify a mac and Windows VST3 as being ‘the same’ plugin?

Steps to reproduce:

  • Create JUCE VST3 plugin
  • Run AudioPluginHost, add VST3 plugin
  • Save, transfer save file from Windows to macOS
  • Load file in AudioPluginHost, plugin doesn’t appear

I’m not sure whether it’s valid to use the PClassInfo::cid to identify a specific plugin. I don’t see anything in the VST3 docs which requires/recommends the cid to have the same value on all platforms. Unfortunately I can’t find any other information about how a plugin might be uniquely identified, so I suppose the cid is probably the best option - although, as noted, I don’t think this can easily be changed for existing plugins which must have backwards-compatible FUIDs.

Another data point is that the DECLARE_CLASS_IID macro provided in the VST3 SDK to generate class iids produces different results on Windows to other platforms, because the INLINE_UID macro uses a different byte ordering depending on whether COM_COMPATIBLE is set, and this flag is only enabled on Windows. This being the case, it seems unlikely that Steinberg expect the CID to be used to identify a plugin portably.

Being able to uniquely identify VST3s across platforms seems like a useful feature to have, but I’m not sure of the best way to achieve it at the moment.

I don’t know if it will help anyone or not, but in order to support our old pre-JUCE VST3s, and to allow porting between Mac and PC, we have modified the VST3 wrapper with this code (which depends on Preprocessor Definitions we put in our JUCE projects):

#if (defined(OLD_VST3_4CHAR_ID) && defined(OLD_VST3_PLUGIN_NAME))

// If the JUCE project defines these two fields
// (e.g. 'APan' and "Auto-Tune EFX 3" for EFX 3 as the old VST3 version),
// then define the factory iids as was done in the old VST3 software.

FUID convertVST2UID_To_FUID (int32 myVST2UID_4Chars, const char* pluginName, bool forControllerUID)
	char uidString[33];
	// Remove ~s from plugin name. We had to use ~s in .jucer file instead of spaces in Preprocessor Definitions
	size_t len = strlen (pluginName);
	char localName[64];
	strncpy( localName, pluginName, 63 );
	localName[63] = 0;
	len = strlen(localName);
	for (int i = 0; i < len; ++i)
		if (localName[i] == '~')
			localName[i] = ' '; // replace ~ with space
	int32 vstfxid;
	if (forControllerUID)
		vstfxid = (('V' << 16) | ('S' << 8) | 'E');
		vstfxid = (('V' << 16) | ('S' << 8) | 'T');
	char vstfxidStr[7] = {0};
	sprintf (vstfxidStr, "%06X", (unsigned int)vstfxid);
	char uidStr[9] = {0};
	sprintf (uidStr, "%08X", (unsigned int)myVST2UID_4Chars);
	strcpy (uidString, vstfxidStr);
	strcat (uidString, uidStr);
	char nameidStr[3] = {0};
	// !!!the pluginName has to be lower case!!!!
	for (uint16 i = 0; i <= 8; i++)
		uint8 c = i < len ? localName[i] : 0;
		sprintf (nameidStr, "%02X", c);
		strcat (uidString, nameidStr);
	FUID newOne;
	newOne.fromString (uidString);
	return newOne;
const Steinberg::FUID JuceVST3Component     ::iid (convertVST2UID_To_FUID (OLD_VST3_4CHAR_ID, OLD_VST3_PLUGIN_NAME, false));
const Steinberg::FUID JuceVST3EditController::iid (convertVST2UID_To_FUID (OLD_VST3_4CHAR_ID, OLD_VST3_PLUGIN_NAME, true));

#elif (defined(OLD_VST3_IDLONG1) && defined(OLD_VST3_IDLONG2) && defined(OLD_VST3_IDLONG3) && defined(OLD_VST3_IDLONG4))
// Just reverse controller ID fields from the plugin ID fields here
 DEF_CLASS_IID (JuceVST3EditController)

 DEF_CLASS_IID (JuceVST3Component)
<original code>

Thanks, that’s interesting.

I’ve posted a query on the VST3 forum on this topic, so I’ll revisit this once I’ve had some responses.

1 Like

We released a JUCE 6 plugin and it’s possible to open an OSX saved cubase project on Windows and the VST3 loads.
I remember there was a bigger problem with the CMAKE build in the early JUCE 6 releases that generated a wrong VST3 ID on windows. After the fix the user was able to work on the same projects on windows and OS X.

The PRODUCT_NAME (PLUGIN_NAME) seems to be the identifier for VST3’s.

But we having problems with the VST2 to VST3 transition in some hosts. Maybe this is related?

1 Like

Would be interesting to know what hosts?
Some hosts also don’t support such transition.

With JUCE flags it’s possible to make VST2<>VST3 compatible plug-in that will have different parameters (due to Legacy, non-ID parameters always the case on VST2), breaking automation(s).

1 Like

We having issues in Studio One with the VST2 <> VST3 compatible plugins. When the user switches from the older VST2 plugin to the VST2 <> VST3 plugin, then it fails to load the VST3 (it does not find the plugin). This works for example in bitwig or cubase. If i remember right the problem occurs when the product name does not match the plugin name.

1 Like

I’ve added a new JUCE_VST3_HOST_CROSS_PLATFORM_UID config flag which will produce the same uid on all platforms. This only affects hosts built with JUCE.

In new hosts, it’s recommended to enable this flag. In existing hosts, you should leave this disabled to retain compatibility with stored uids.

I guess this is a Breaking Change worthy?

It’s only enabled by a config flag, so it shouldn’t affect existing hosts unless they opt in.

So unlike other changes like JUCE_USE_STUDIO_ONE_COMPATIBLE_PARAMETERS that are now the ‘default’ for new project (Projucer) since CMake has not defaults / official generators yet.
While ‘recommended’, it’s not being defaulted on new projects.

So anyone creating new host would actually use the ‘not’ recommended way unless they’ve set it explicitly?
(Just to make sure I understand the logic :slight_smile: )

1 Like

If JUCE_VST3_HOST_CROSS_PLATFORM_UID will be enabled on an existing host, will this break the old projects on Windows Or Macintosh side?

(so one can write an auto-update for older project-files, which updates the uids)

Yes, this flag will change the value of the uid field, which will make graceful upgrading of project files difficult.

I’m currently looking at improving this situation - it’s probably best not to use JUCE_VST3_HOST_CROSS_PLATFORM_UID for the moment after all, as I think we can do better.

I’m aiming to get a slightly more complete fix out next week.

1 Like

I’ve now pushed a new fix for this:

The old uid field is deprecated, and replaced with a new uniqueId field. The value of the old uid is still available in the deprecatedUid field, which should allow hosts to implement logic that will attempt to find plugins with matching uniqueIds, but fall back to using the deprecatedUid if no match is found. I’ve updated the AudioPluginHost to show one approach for achieving this.

Please let me know if you encounter any new issues with this change.


I know this is a bit of a tough one to solve but this is going to cause quite a few broken features for us.
We use PluginDescription::createIdentifierString in a lot of places to unique identify plugins and store things like plugin images, favourites, recents lists and meta tags.

I’m thinking at the moment I’ll need to write a local copy of the old PluginDescription::createIdentifierString function and convert file names and IDs etc. to the new format if possible.

I’m sure this will be problematic in places though, for example, if a plugin has been removed from the system and we don’t have the PluginDescription we won’t be able to update it.

Do you have any recommendations about how to approach things like this?

I think the approach you outlined (replicating the createIdentifierString function and upgrading from the old ID to the new one) sounds reasonable. I’m not sure whether there’s any way we could handle the problematic case you mentioned - we can’t ‘unhash’ the uid field and reconstruct the correct hash, so I think it’s necessary for all used plugins to be installed in order for projects to be updated successfully. In the case where a plugin is missing, you could just keep the ‘old’ UID in your project file, and try to search for the plugin again at a later date. That is, you may not need to update the full project in one go.

I’m sorry I don’t have a perfect solution for this problem. I’m happy to consider suggestions if there’s any additions that would make your life easier.

Are you likely to remove the deprecatedUid at some point in the future?

Unless it ends up adding massive complexity to future PluginDescription modifications, I don’t think we’ll need to remove it.

So as predicted, this change has broken a lot of stuff in subtle ways.
It’s basically impossible to keep track of where old IDs are used and where new ones are without any strong typing (and that they’re converted to and from strings all the time) and the fact that it’s not possible to convert the old IDs to the new ones.

We’ve attempted to circumvent this change by using our own internal getDeprecatedPluginDescSuffix function so our saved IDs still work. However, this is now incompatible with PluginDescription::matchesIdentifierString and therefore the KnownPluginList e.g. KnownPluginList::getTypeForIdentifierString.

What do you think of this addition to PluginDescription so it matches both the new and old IDs?
They’re not likely to collide between plugins are they?

static String getDeprecatedPluginDescSuffix (const juce::PluginDescription& d)
    return "-" + String::toHexString (d.fileOrIdentifier.hashCode())
         + "-" + String::toHexString (d.deprecatedUid);

bool PluginDescription::matchesIdentifierString (const String& identifierString) const
    return identifierString.endsWithIgnoreCase (getPluginDescSuffix (*this))
        || identifierString.endsWithIgnoreCase (getDeprecatedPluginDescSuffix (*this));