Load x64 Plug-ins (like VSTs) from your Arm Code using Arm64EC

Pete Brown

Support x86-64 (x64) VSTs from your Arm DAW Code using Arm64EC

Windows on Arm

Since the early days of Windows 8, we’ve continued to refine and enhance Arm support in Windows on Arm, as well as our own hardware support through the SQ1 and SQ2 processors developed with Qualcomm, Surface devices, Project Volterra, and of course, third-party Windows on Arm PCs. Both Windows and the Arm64 devices it runs on are continuing to evolve and improve as we move forward.

Compatibility with existing Intel-architecture apps has been an important part of our Windows on Arm development. In Windows 10, we introduced the ability to run unmodified 32 bit x86 apps on Arm devices. Although this is a solution for most at the time, the music industry has been using x64 apps for longer than many other industries, so it wasn’t really a solution for us.

Windows 11 enables running unmodified x64 Windows apps on Arm64 devices, so your existing x64 DAWs and plug-ins should just work on Arm devices in most cases. However, for the best experience including battery life and performance, compiling your apps to native Arm64 is recommended. Arm64EC, which is native Arm64, is one of the more flexible ways to get there, enabling a partial migration to Arm64 while continuing to use existing x64 DLLs, including plug-ins.

About Arm64EC

Arm64EC (Emulation Compatible) is an ABI which enables developers to port existing x64 apps while taking advantage of Arm performance. Arm64EC is native Arm, but has the ability to load and interop with x64 binaries into the same process, under emulation in Windows 11. What’s important for us here, is that this does not require any recompilation of the x64 plug-in DLLs which are being loaded into the Arm64EC DAW process.

Arm64EC Host Diagram

Arm64EC is a Windows 11 technology, and not a solution for Windows 10.

What this leads us to is a solution to compatibility with the huge ecosystem of 64 bit VST, RTAS, CLAP, and other plug-ins which are available. Many of those plug-ins will be recompiled to Arm64 native, but in the near-term, a solution to use existing acquired libraries of plug-ins, and new plug-ins which haven’t yet been ported, is necessary.

Although plug-ins are available for many different ecosystems and standards, I’m going to focus on musician technology and VST3 here.

What you need to get started

So here’s what you need:

  • An Arm64 Windows PC, running Windows 11. I’m using a Surface Pro X which I’ve upgraded to Windows 11.
  • The latest Windows SDK (I’m not running an Insider build on either my x64 or Arm PCs, but it’s technically a requirement)
  • Latest Visual Studio 2022 Preview (Visual Studio 2022 Previews have been my primary dev environment, and I’ve found them to be quite stable)

The requirements and setup instructions can be found here.

Note that in my setup, my primary dev PC is an x64 PC, and my secondary PC is an Arm64 Surface Pro X. I haven’t tested this with Arm64EC, but with the latest Visual Studio 2022 on Arm, you should be able to do everything on an Arm64 PC, so no need to use a setup like this.

A simple example

Let’s start by showing how to load a 64-bit Intel DLL from an Arm64EC host project running on an Arm device. I’m keeping things simple here, and going with a C-style DLL with a single function exported.

The DLL code for the header:

#pragma once

#define MYFUNCTIONS_API __declspec(dllexport)

extern "C" MYFUNCTIONS_API long get_add_result(const int a, const int b);

And the implementation:

#include "pch.h"
#include "MyFunctions.h"

long get_add_result(const int a, const int b)
{
    return a + b;
}

I then compiled that DLL to x64 to represent the state of the plug-in industry today.

For the host, I created a new project using the C++ Windows Desktop Application template. Nothing fancy, and mostly boilerplate code. Some old-school stuff here.

Visual Studio New Windows Desktop App

With that done, the first thing you’ll need to do for your project, after adding the Arm64EC workload to Visual Studio, is add a new Arm64EC solution platform in the configuration manager. This is documented in the Arm64EC for Windows 11 apps on Arm page in our docs.

Arm64EC In Visual Studio Configuration Manager

You’ll end up with two projects, the DLL configured for x64 and the host configured for Arm64EC.

Arm64EC and x64 in Configuration Manager

If Arm64EC wasn’t offered as an option, you don’t have that workload installed in Visual Studio. See the link above for instructions.

For the first sample, I added two menu options to the app: Load basic DLL and get result. The first dynamically loads the DLL, and the second displays the result, showing that the API call worked.

A Simple Plug-in Host

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_COMMAND:
        {
            int wmId = LOWORD(wParam);
            // Parse the menu selections:
            switch (wmId)
            {
            case IDM_FILE_LOADBASIC:
                LoadSimpleDll();
                break;
            case IDM_FILE_GETRESULT:
                DisplaySimpleResult();
                break;
                ...
            }
        }
        break;
        ...
    case WM_DESTROY:

        UnloadSimpleDll();

        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

Here’s the implementation for loading and unloading the DLL. This is all standard dynamic library binding handling, so you can do this in whichever way your framework of choice uses.

typedef long(__cdecl* GetResultProcDef)(const int, const int);
GetResultProcDef ResultProc;

void LoadSimpleDll()
{
    hSimpleDll = LoadLibrary(L"Intel64BitPlugin.dll");

    if (!hSimpleDll)
    {
        // couldn't load basic dll
        MessageBox(NULL, L"Could not load simple DLL", L"DLL Load", MB_ICONERROR | MB_OK);
    }
    else
    {
        // get the function
        ResultProc = (GetResultProcDef)GetProcAddress(hSimpleDll, "get_add_result");

        if (!ResultProc)
        {
            MessageBox(NULL, L"Could not get proc address", L"DLL Load", MB_ICONERROR | MB_OK);
        }
        else
        {
            MessageBox(NULL, L"Library loaded and function found.", L"DLL Load", MB_ICONINFORMATION | MB_OK);
        }
    }
}

void UnloadSimpleDll()
{
    if (hSimpleDll)
    {
        FreeLibrary(hSimpleDll);
        ResultProc = NULL;
    }
}

And finally, the implementation for calling the DLL exported function and displaying the result. Don’t take this as an example of correct/best string processing in Windows. 🙂

void DisplaySimpleResult()
{
    const int a = 31;
    const int b = 11;
    long result;

    if (ResultProc != NULL)
    {
        result = (ResultProc)(a, b);

        std::wstring str = L"The result of " + std::to_wstring(a) +
            L" and " + std::to_wstring(b) + L" is " +
            std::to_wstring(result);

        MessageBox(NULL, str.c_str(), L"DLL Result", MB_ICONINFORMATION | MB_OK);
    }
}

When you run the project (from a folder which contains both the exe and dll binaries) you should see this result when loading the library

Simple Plug-in DLL Loaded

and then this result when you select the menu option to get the result.

DLL Function Call Result

Of course, if you’ve done all of this so far using x64 on an x64 PC as I have, it’s going to work on that PC. That shows us only that the code works, not that Arm64EC is a solution.

Now change, if you haven’t already, the build configuration to Arm64EC so that the plug-in host is compiled to Arm64EC and the plug-in itself is still compiled to x64. Copy this to your Arm PC (if you’re not already developing on one, or using the remote tools) and try it out. You should see the same results. Note that if you don’t have Visual Studio on the Arm PC, you’ll likely need to install the latest Visual C++ Runtime Arm64 redist package as well.

The Plug-in Host on the Surface Pro X

The app looks a little janky here because I’ve done nothing for DPI awareness, and the Surface Pro X is running at high DPI. It’s not an Arm64EC limitation. Note the Architecture string in Task Manager. “ARM64 (x64 compatible)” means Arm64EC.

Loading a VST

That was easy enough, so let’s apply the same approach to some pre-existing code in a slightly more real-world example. We’ll load a VST plug-in just like a DAW would.

What’s a VST?

Most reading this will be familiar with VSTs, but for those who are not, here’s a little primer.

VST (Virtual Studio Technology) is a cross-platform (Linux, macOS, Windows) and cross-processor (x86, x64, Arm64) Steinberg standard that has become the dominant standard for audio processing plug-ins on Windows. It defines an interface which applications can use to identify and load a DLL which contains audio processing or generating functionality. You’ll find VSTs used in all sorts of applications including DAWs, of course, but also video editors, audio editors, and more.

There are other standards, including AU (Audio Units) on Apple devices, RTAS for Pro Tools, Rack Extensions for Reason, and the new (at the time of this writing) CLAP standard created by U-He and Bitwig, and a number of other standards including several on Linux.

These plug-ins can vary significantly in complexity and cost. Some are simple single-window affairs which do one thing, like delay, reverb, or compression. Others are entire processing suites which can substitute as an entire mastering chain complete with presets and sometimes even AI-based processing. Still others are complex sample libraries and players which can perform as an entire orchestra, or synthesizers with gigabytes of waveform data.

The investments musicians have in VSTs can also be significant, with individual plug-ins varying from free, through to thousands of dollars. My own modest VST collection has cost several thousand dollars over years, including plug-ins and libraries like Native Instruments Komplete, Spectrasonics Omnisphere, Izotope production suite, Eric Whitacre Choir (and other Spitfire Audio libraries), and a bunch more.

But let’s pick a relatively simple (but amazing-sounding) VST to use for this demo: Valhalla DSP’s Vintage Verb. Valhalla Shimmer was the first plug-in I bought from him, but I now have all the Valhalla DSP plug-ins, which at only $50 each for a lifetime, are a real bargain. Vintage Verb is one of the greats, offering classic reverb models with just enough funk in the UI :). The plug-ins all sound great, but I also appreciate the design aesthetic and the simple and tight UI.

The host program

Rather than spin up an app from scratch, I downloaded the Steinberg VST 3 SDK. The SDK includes a command-line program “editorhost” which can be used to load a VST plug-in and display its UI.

The SDK doesn’t come with a configuration for Arm64EC, but the tooling in Visual Studio 2022 made it easy to create an appropriate entry.

Arm64EC Configuration

Once compiled to Arm64EC, I used it on the Surface Pro X to load the existing x64 Valhalla VintageVerb DLL. So yes, that’s an Intel architecuture x64 plug-in, unchanged, being loaded by an Arm64EC process on my Arm64 PC, with zero code changes to any of the pieces involved.

The VST on Arm64

And, just like magic, it worked. 🙂

Caveats

As with any technology like this, there’s the possibility it will not be appropriate for 100% of the scenarios. If you run into scenarios in the music industry where this doesn’t work, don’t hesitate to reach out to me.

There are also some caveats to consider.

Arm64EC is no longer experimental, but it is still in preview. Arm64EC will become production ready with Visual Studio 2022 17.3, coming soon (Visual Studio 2022 17.3 is currently in Preview 2). In the meantime, it can’t be used to build and deploy a production app. However, you can start your project work in it today.

Mixing Arm and Arm64EC-compiled code

Arm64EC enables interop between x64 and Arm64EC compiled binaries in the same process. Any plug-ins, when recompiled for Arm, need to be recompiled to Arm64EC to ensure this works. So our guidance for plug-in authors is to maximize compatibility by compiling to both x64 and Arm64EC to natively support both Intel x64 and Arm64 architecture PCs. Similarly, our guidance to DAW authors is to compile to the same two platforms: x64 and Arm64EC.

OS PC Architecture Host Process Compatible Plug-in Architectures
Windows 10 Intel x86 Intel x86 Intel x86
Windows 10/11 Intel x64 Intel x64 Intel x64*
Windows 10/11 Intel x64 Intel x86 Intel x86
Windows 10/11 Arm64 Arm64 Classic Arm64 Classic
Windows 11 Arm64 Arm64EC Arm64EC, Intel x64
Windows 11 Arm64 Intel x64 Arm64EC, Intel x64

In the table above, x86 means 32-bit Intel x86 code or architecture. Arm64 Classic is Arm64 that is not emulation compatible. Also, I have not included Arm32 as it is not relevant to our discussion of DAWs and plug-ins.

*Existing solutions to load x86 plug-ins (additional process, etc.) continue to work on Windows x64 as they have in the past. Similar solutions may work for loading x86 plug-ins on Arm64, under 32-bit emulation.

32-bit Support

Everything I’ve done so far assumes you’re working with 64 bit plug-ins. Arm64EC doesn’t offer any magic to enable you to load 32 bit x86 code. I know there are still plenty of 32 bit plug-ins out there, but the world has largely moved to 64 bit and VST3. For folks who still rely on 32 bit, they’ll need to keep a compatible machine around for those, and use something like Vienna Ensemble Pro (VEP) to remote the VST calls across the network. At the time of this writing, there is no Arm-specific version of VEP, so your mileage may vary.

There are potentially other ways to load 32 bit x86 plug-ins on Arm, but your mileage (and performance) may vary, and may involve spinning up a separate host process for those plug-ins.

That brings us to the next obvious question: If we can load x64 DLLs into an Arm process, and ASIO drivers are just 64 bit DLLs loaded into the application process, can we load these using Arm64EC?

What about other dynamically-loaded components? ASIO?

You can certainly use Arm64EC to have a phased migration of a DAW or other app which has many components deployed as individual DLLs. This is exactly why Arm64EC exists today, so that you don’t need to convert everything at once. This can be especially important for DAWs which use bundled third-party DLLs for various features.

In its simplest form, an ASIO driver is just another DLL loaded into the DAW process. Despite being called a “driver”, it is not really a driver in the typical Windows sense of the word. It’s an interface which allows an application (DAW) to speak directly, or nearly directly, to the hardware audio device using an established API. This is an important distinction because Windows Drivers all need to be compiled to Arm64 to work on an Windows on Arm device, and cannot be Intel x64, but process-loaded components like VSTs and a the main ASIO interface DLL do not have that same requirement.

There are many different implementations of ASIO drivers out there. Although they all conform to the interface standard, some talk directly to hardware, while others have associated Windows services and other libraries or components or kernel drivers they rely on. As a result, a blanket statement that you can use Arm64EC to load ASIO drivers isn’t something I can make. Instead, they’d need to be tested individually. This also includes verifying that the installers will work on an Arm device.

Of course, the ideal situation here is that hardware vendors all offer Arm64 versions of their ASIO drivers.

Installers

One big caveat is in getting the VSTs installed on Arm devices to begin with. That requires that the installer not block on installing on an Arm device. In my case, I could simply copy the DLL file and its license file over. More complex plug-ins, especially synths with large libraries, will require more effort, or may not work at all.

If you run across plug-in installers which do not work on Arm devices, please let me know.

Should I take this approach for my DAW code?

Musicians tend to be a conservative lot when it comes to their computers, and usually with good reason. Plug-ins can be expensive to replace, often many multiples of the cost of the DAW software itself, most charge for version upgrades, and many developers don’t offer new platform compiles for older versions of plug-ins for free. It’s going to be a long time before folks exhaust their collections of x64 plug-ins and drivers, and so DAWs and other plug-in-loading software will want to support the existing plug-ins for as long as possible, even if using loader code that is side-by-side with code which loads native Arm-compiled components. The good thing is that the host program doesn’t need to spin up another process just to host the x64 plug-ins, and compiling to Arm64EC can be both incremental and relatively painless.

If you are a plug-in developer (or create ASIO drivers or other components used in DAWs) we highly encourage you to offer Arm64EC compiled versions of your products along with the x64 versions. This will help take out any guesswork about whether or not something will work, and will also enable you to tweak your code to best take advantage of what each processor can offer you.

When developing, please be sure to stay up to date with the latest Visual Studio tooling for Arm64EC and Windows Development. That way you’ll have the latest bug fixes and security fixes, and the best possible experience. We also encourage you to leave feedback for problems and suggestions using the feedback button in the toolbar in Visual Studio.

TLDR: Please offer both Arm64EC and x64 versions of your DAWs and plug-ins, and please stay up to date with the latest developer tooling and SDKs. 🙂

In the meantime, I hope this example helps all the DAW and other plug-in host authors out there.

If you are a developer working on technology for musicians or pro audio, as always, you can reach me on twitter @pete_brown or via my email at pete dot brown at microsoft dot com.

This post is full of useful links. Here are a few of the more important ones:

Tools

Windows

Hardware

Steinberg VST SDKs

1 comment

Discussion is closed. Login to edit/delete existing comments.

  • Ianier Munoz 1

    ARM64EC is amazing technology, but in practice only apps where the main executable is compiled with VC++ can currently take full advantage of it.
    Why not making ARM64EC available for .Net Native so UWP Store apps developed in C# targeting the ARM architecture could load DLLs that are only available as x64 binaries? TensorFlow is a good example of such library.
    I get that .Net Native is in maintenance mode and so on, but enabling ARM64EC support for it (or even forcing ARM64 builds to always use ARM64EC) shouldn’t be a huge investment.

Feedback usabilla icon