Overview
I've been redesigning my core library and I wanted to share the way that I manage Windows on the VST3 Controller side and some other Windows-related issues that I've come across.
Roadmap

We will be focusing on the Win32 Child Window process.
SFML / Graphics
VST3 is a bring-your-own-graphics-library and for those who might not know, I use SFML v3 (https://www.sfml-dev.org/) as mine. Not only is it an enjoyable library to use, but it provides a very useful feature for us, in that it can take over an existing window from a native Windows handle, i.e., you create a Window using the Win32 API and then you can hand it off to SFML without too much pain.
Embedded Win32 Window
VST3 hosts hand you a parent window. Your job is to create a child window using your OS's API, attach it to the parent window that VST3 gives you, and then bind your window to an implementation of VST3's IPluginView to return back to the host.
So we need to do something like the following:
class MyPluginView : public Steinberg::IPluginView
{
public:
// this guy is our focus right now
// void * parent on Windows is HWND
// FIDString type is the platform type string
tresult PLUGIN_API attached( void * parent, FIDString type ) override
{
// create a native Win32 child window
HWND childHwnd = createChildFromParent( static_cast< HWND >( parent ) );
// create a SFML window with an OpenGL context based on the child window
attachChildToSFML( childHwnd );
// tell Windows to regularly notify us, so we can process
// a game loop frame, e.g., processEvent, update, render
startWindowsGameLoop();
// no issues (hopefully)!
return Steinberg::kResultOk;
}
};
Creating a Win32 Child Window
Window creation is a three-step process in Windows for an embedded environment:
- Get the Module Handle
- An opaque handle to our plugin DLL
- Register Window Class
- We typically only need to do this once per process
- Create the child window
- We do this with the parent's handle (
HWND), provided by the host
- We do this with the parent's handle (
And there are some variants that Windows offers, which I won't get into too much here.
Get the Module
Windows documents gives a generic explanation of this.
Type: **HINSTANCE**
A handle to the instance that contains the window procedure for the class.
hInstance is the module handle of the DLL/EXE that contains the window class's window procedure (WndProc). In a plugin, that should be the plugin DLL's module handle, not the host EXE's. It's basically an opaque handle we need to pass cleanly between us and Windows.
We're gonna dive briefly into Systems Programming to discuss this because it keeps me sharp and hopefully you'll find it interesting.
A VST3 is essentially a DLL in Windows (a library that gets loaded at runtime rather than compiled into the executable). And a host (among many things) is a plugin management system. That host gets executed and given an address space by the OS, i.e., a range of addresses that the process is allowed to use for memory. That address space can include DLLs. Each DLL loaded is mapped to a specified address within the process's address space. The module or HINSTANCE is the address of the DLL and not the host's.
So how do we get this? We ask the OS for it.
Getting HINSTANCE cleanly
There are few ways we can get it, including using VirtualQuery with a dummy static variable that lives inside our DLL's .data section, but it's a bit of a hack and Windows recommends one of two ways instead:
- Cache the
HINSTANCEfromDllMain
BOOL APIENTRY DllMain(HINSTANCE hinst, DWORD reason, LPVOID)
{
// cache hinst
}
- Use
GetModuleHandleEx
Since the VST3 API doesn't hand off DllMain or forward any information to us from it, that leaves GetModuleHandleEx.
static HINSTANCE getWindowsModuleHandle()
{
HMODULE mod = nullptr;
// any address inside our DLL works, so a function pointer is perfect
if ( !::GetModuleHandleEx(
// the first flag tells the function that we will use an address
// inside of this DLL as a reference (see param #2)
// rather than using a filepath
// the second flag tells the function to not change the internal
// reference count whenever our host loads and frees our plugin
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
// we just reference ourselves as a static function since
// this function belongs inside our DLL/VST3
reinterpret_cast< LPCWSTR >( &getWindowsModuleHandle ),
// this is the HINSTANCE that we will get back, if successful
&mod ) )
{
LOG_ERROR( "unable to get module handle: {}", ::GetLastError() );
return nullptr;
}
return mod;
}
The Raymond Chen Method
Worth mentioning is Raymond Chen's method for getting HINSTANCE. It's not as robust as GetModuleHandleEx, but it's worth knowing. I have used it in my own plugins before:
https://devblogs.microsoft.com/oldnewthing/20041025-00/?p=37483
Registering the Window Class
Before we can create a Win32 window, we first need to register a window class.
This is essentially a blueprint that tells Windows two important things (as far as we're concerned):
- Which
WndProcfunction should receive messages for this window - How much per-window storage we want to reserve (via
cbWndExtra)
That second point matters in embedded environments like VST3, because SFML uses GWLP_USERDATA internally. By reserving our own pointer-sized slot in cbWndExtra, we can safely attach this to the window without fighting with SFML.
A window class only needs to be registered once per process per class name. After that, we can create as many child windows as we want using the same class.
Finally, because plugin hosts may instantiate multiple plugin editors in parallel, class registration should be done in a thread-safe way (for example, using std::call_once) to avoid race conditions and ERROR_CLASS_ALREADY_EXISTS.
static inline const auto INTERNAL_WINDOW_NAME = L"beSomewhatUniqueWithThis";
// param 1 = WndProc callback (will discuss more)
// param 2 = module handle we got from Windows above
static bool registerWin32Class(
LRESULT ( *childWndProc )( HWND, UINT, WPARAM, LPARAM ),
HINSTANCE moduleHandle )
{
static std::once_flag registeredOnce;
static std::atomic ok = true;
// this is a cool C++11 callback that ensures this only ever gets
// run ONE time. it sets our m_registeredOnce flag and then we rely
// on the m_isRegistered to indicate whether that one time was ever
// successful.
std::call_once( registeredOnce, [ & ]()
{
WNDCLASSEXW wincl = {}; // zero-initialize the class
wincl.cbSize = sizeof( wincl );
wincl.hInstance = moduleHandle; // our HINSTANCE we asked Windows for
wincl.lpszClassName = INTERNAL_WINDOW_NAME; // any name you want
wincl.hbrBackground = nullptr; // letting Win erase bg may flicker
wincl.lpfnWndProc = childWndProc; // <-- Our Callback!
wincl.style = CS_DBLCLKS; // allow double-clicks
wincl.hIcon = ::LoadIcon( nullptr, IDI_APPLICATION );
wincl.hIconSm = wincl.hIcon;
wincl.hCursor = ::LoadCursor(nullptr, IDC_ARROW );
// SFML co-opts the GWLP_USERDATA that typically gets used,
// so we'll reserve a pointer for our own data here and then
// assign it using SetWindowLongPtr after registration & window
// creation
wincl.cbWndExtra = sizeof( void* );
if ( ::RegisterClassExW( &wincl ) == 0 )
{
const auto err = ::GetLastError();
// we shouldn't encounter this, but just in case
if ( err != ERROR_CLASS_ALREADY_EXISTS )
{
LOG_ERROR( "Failed to register window class: {}", err );
ok = false;
}
}
});
return ok;
}
Creating the Child Window
This is fairly straightforward. The one thing I would like to point out is that the final nullptr is typically where we'd be able to store a pointer to ourselves, but because I use SFML and I've looked at the source code for SFML, I know that it takes over that data for its own purposes.
static HWND createChildWindow(
LRESULT (*childWndProc)( HWND, UINT, WPARAM, LPARAM ),
const HWND parent,
const sf::IntRect r )
{
if ( !parent )
{
LOG_CRITICAL( "Parent HWND must not be null!" );
return nullptr;
}
const auto moduleHandle = getWindowsModuleHandle();
if ( !registerWin32Class( childWndProc, moduleHandle ) ) return nullptr;
const HWND handle = ::CreateWindowExW( 0,
INTERNAL_WINDOW_NAME, // window class name
WINDOW_NAME, // Title Text
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS, // flags
r.position.x, r.position.y, r.size.x, r.size.y,
parent, // the parent window
nullptr, // menu
moduleHandle, // the plugin's HINSTANCE
nullptr ); // SFML co-opts this, so we set it to nullptr
if ( handle == nullptr )
{
LOG_ERROR( "CreateWindowEx failed: {}", ::GetLastError() );
return nullptr;
}
) );
return handle;
}
Handing Control of HWND to SFML
Once we've successfully gone through the rigamarole of the 3-step process, it's now time to give our child HWND to SFML and let it attach an OpenGL context. There are still a few gotchas involved, however:
// we leave this as a member variable of a Win32View class
// and there's no need to initialize it further.
sf::RenderWindow m_sfWindow;
void Win32View::initializeRenderWindow()
{
// hand it off, along with whatever SFML context we want to provide
m_sfWindow.create( m_win32.childHwnd, m_sfContext );
// framerate is at the mercy of the host. i will discuss WDM more.
m_sfWindow.setVerticalSyncEnabled( false );
m_sfWindow.setFramerateLimit( 0 );
// we want to immediately bring the child to the foreground, if possible
if ( !::AllowSetForegroundWindow( ASFW_ANY ) )
{
LOG_WARN( "failed to allow foreground." )
}
// this does NOT make the window active. it only sets the
// opengl context active on this window
if ( !m_sfWindow.setActive( true ) )
{
LOG_WARN( "failed to activate window for OpenGL usage." );
}
// initialize with a clear and display
m_sfWindow.requestFocus();
m_sfWindow.clear();
m_sfWindow.display();
}
WndProc Callback
This is the glue layer. Windows will receive a callback and then we will access our class through it, and then we will exercise the SFML game loop as we normally would.
static LRESULT CALLBACK childWindowProc(
HWND hwnd, // our child HWND
UINT msg,
WPARAM wParam,
LPARAM lParam )
{
if ( msg == WM_NCCREATE )
{
// we need to allow this to pass through regardless
return ::DefWindowProcW( hwnd, msg, wParam, lParam );
}
// get our class instance from index 0,
// which is what we set it as in SetWindowLongPtrW
auto * self =
reinterpret_cast< Win32TimerQueueView * >(
::GetWindowLongPtrW( hwnd, 0 ) );
switch ( msg )
{
// keep this WM_RUN_FRAME in mind, we will come back to it
// and we will also discuss executeFrame(...)
case WM_RUN_FRAME:
self->executeFrame( self->m_sfWindow) ;
return 0;
// this is the contract. we don't do anything with it, we just
// promise to use it.
case WM_PAINT:
{
PAINTSTRUCT ps{};
::BeginPaint( hwnd, &ps );
::EndPaint( hwnd, &ps );
return 0;
}
// we can choose to exercise this through windows
// although we should prefer the VST3 API's version instead
case WM_SIZE:
{
if ( !self ) return 0;
// check our special window states
const auto state = static_cast< UINT >( wParam );
if ( state == SIZE_MINIMIZED ) return 0;
const int width = LOWORD( lParam );
const int height = HIWORD( lParam );
self->onSize( width, height );
return 0;
}
case WM_ERASEBKGND:
{
// we want to tell Windows to avoid background erasing
// in order to prevent flickering.
return 1;
}
case WM_DPICHANGED:
{
// lParam points to a suggested new window rect
// in our case (for child windows), we often just rescale the UI.
return 0;
}
default:
break;
}
return ::DefWindowProc( hwnd, msg, wParam, lParam );
}
Starting the Timer
We use a Win32 timer only to schedule frames. Rendering still happens on the UI thread by posting a custom message (WM_RUN_FRAME) back to the child window, so SFML/OpenGL stays on the thread that owns the context.
and there are two ways of doing it:
- Use
SetTimer&KillTimer - Use
CreateTimerQueueTimer&DeleteTimerQueueTimer
SetTimer is message-based and generally coarse/coalesced, while CreateTimerQueueTimer runs from the threadpool timer queue and tends to be more flexible for sub-16ms scheduling. However, neither is a real-time guarantee. And to make matters more complicated, hosts can pause UI pumping, e.g., when minimized. This ends up being best effort.
bool Win32View::startMessagePump()
{
// get our *attempted* framerate in milliseconds
assert( m_requestFrameRate != 0 );
const int intervalMs = std::max( 1, 1000 / m_requestFrameRate );
const BOOL success = ::CreateTimerQueueTimer(
&m_redrawTimerHandle, // gets assigned here. KEEP track of it.
nullptr,
[]( PVOID lpParam, BOOLEAN )
{
auto * self = static_cast< Win32TimerQueueView* >( lpParam );
if ( self && ::IsWindow( self->m_win32.childHwnd ) )
{
// despite the timer running on a threadpool, PostMessage queues
// the message on the thread that owns childHwnd
::PostMessage( self->m_win32.childHwnd,
WM_RUN_FRAME, // our custom WM message of WM_APP + 1
0, // you can pass custom data in here as wParam
0 );
}
},
this,
0, // start immediately
intervalMs, // frame interval we calculated at the beginning
WT_EXECUTEDEFAULT // default threadpool timer callback (keep work minimal; we only PostMessage)
);
return success == TRUE;
}
We also need know how to stop the timer:
void Win32View::stopMessagePump()
{
// stop the callbacks
if ( m_redrawTimerHandle != nullptr )
{
const auto result = ::DeleteTimerQueueTimer(
nullptr,
m_redrawTimerHandle, // this is assigned by CreateTimerQueueTimer
nullptr ); // nullptr = mark immediately for deletion
if ( result != TRUE )
{
const auto err = ::GetLastError();
// according to the docs, ERROR_IO_PENDING can happen
// and we shouldn't call DeleteTimerQueueTimer again
if ( err != ERROR_IO_PENDING )
{
LOG_ERROR( "DeleteTimerQueueTimer failed: {}", err );
}
}
m_redrawTimerHandle = nullptr;
}
}
Framerate Side Note
On modern Windows, the Desktop Window Manager (DWM) composites the final desktop at (typically) the monitor refresh rate. As a plugin child window, we don't control the host's message loop pacing or the compositor, so even if you schedule frames faster, you may not present faster in practice. Most DAW hosts feel like ~60 Hz, and the best we can do is render efficiently and avoid blocking the UI thread.
Executing the Game Loop
This is what it has all led to. The Timer expires, our WndProc sees our WM_RUN_FRAME, and we run one frame:
bool executeFrame( sf::RenderWindow & window )
{
if ( !window.isOpen() ) return false;
while ( const std::optional event = window.pollEvent() )
processEvent( window, event );
// this is a topic for another day. this is a bridge between
// VST's control over the keyboard and SFML
while ( const std::optional event = m_keyBridge.pollEvent() )
processEvent( window, event );
// update
{
const auto delta = m_clock.restart();
m_scene->update( window, delta );
}
// draw
{
window.clear();
m_scene->render( window );
window.display();
}
return true;
}
There’s no outer while ( running ) loop here because the Win32 timer drives the cadence.
Conclusion
VST3 and SFML don't solve the embedding story end-to-end, so the last mile is OS code. The good news is that once you get this nailed down, it becomes stable infrastructure you rarely have to revisit.