Files
yabridge/src/wine-host/editor.h
T
Juuso Kaitila 604375b756 VST3: Fix broken resizing in Ardour
Adds timer_proc lambda for VST3 to check for size mismatches and trigger
a new resize to correct it through eventual consistency.

This is done to workaround an X11 sync issue where the plugin view would
end up smaller or larger than its wrapper window. In Ardour this could
result in the plugin becoming uninteractable.
2026-04-26 16:24:39 +02:00

446 lines
17 KiB
C++

// yabridge: a Wine plugin bridge
// Copyright (C) 2020-2024 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <memory>
#include <optional>
#include <windows.h>
#include <function2/function2.hpp>
// Use the native version of xcb
#pragma push_macro("_WIN32")
#undef _WIN32
#include <xcb/xcb.h>
#pragma pop_macro("_WIN32")
#include "../common/configuration.h"
#include "../common/logging/common.h"
#include "utils.h"
#include "xdnd-proxy.h"
/**
* The most significant bit in an X11 event's response type is used to indicate
* the event source.
*/
constexpr uint8_t xcb_event_type_mask = 0b0111'1111;
/**
* The name of the X11 property that indicates whether a window supports
* drag-and-drop. If the `editor_force_dnd` option is enabled we'll remove this
* property from all of `parent_window_`'s ancestors to work around a bug in
* REAPER.
*/
constexpr char xdnd_aware_property_name[] = "XdndAware";
/**
* The name of the Win32 window class we'll use for the Win32 window that the
* plugin can embed itself in.
*/
constexpr char yabridge_window_class_name[] = "yabridge plugin";
/**
* Get the atom with the specified name. May throw when
* `xcb_intern_atom_reply()` returns an error. Returns `XCB_ATOM_NONE` when the
* atom doesn't exist. We define this here because we'll also need to fetch a
* whole bunch of atoms for the We define this here because we'll also need to
* fetch a whole bunch of atoms for the XDND protocol in `xdnd-proxy.cpp`.
*/
xcb_atom_t get_atom_by_name(xcb_connection_t& x11_connection,
const char* atom_name);
/**
* Check if the cursor is within a Wine window. We can of course only detect
* Wine applications within the current prefix. This ignores the extended client
* area of yabridge windows. (so it will consider other Wine windows to the
* right or to the bottom of a yabridge plugin editor, but not the extended
* client area itself)
*
* @param windows_pointer_pos The screen coordinates to use for the query. If
* this is left at a nullopt, we will simply use `GetCursorPos()`. Note that
* this value only updates once every 100 milliseconds.
*/
bool is_cursor_in_wine_window(
std::optional<POINT> windows_pointer_pos = std::nullopt) noexcept;
/**
* Used to store the maximum width and height of a screen.
*/
struct Size {
uint16_t width;
uint16_t height;
};
/**
* A RAII wrapper around windows created using `CreateWindow()` that will post a
* `WM_CLOSE` message to the window's message loop so it can clean itself up
* later. Directly calling `DestroyWindow()` might hang for a second or two, so
* deferring this increases responsiveness. We actually defer this even further
* by calling this function a little while after the editor has closed to
* prevent any potential delays.
*
* This is essentially an alternative around `std::unique_ptr` with a non-static
* custom deleter.
*/
class DeferredWin32Window {
public:
/**
* Manage a window so that it will be asynchronously closed when this object
* gets dropped.
*
* @param main_context This application's main IO context running on the GUI
* thread.
* @param x11_connection The X11 connection handle we're using for this
* editor.
* @param window A `HWND` obtained through a call to `CreateWindowEx`
*/
DeferredWin32Window(MainContext& main_context,
std::shared_ptr<xcb_connection_t> x11_connection,
HWND window) noexcept;
/**
* Post a `WM_CLOSE` message to the `handle_`'s message queue as described
* above.
*/
~DeferredWin32Window() noexcept;
const HWND handle_;
private:
MainContext& main_context_;
std::shared_ptr<xcb_connection_t> x11_connection_;
};
/**
* TODO: This documentation needs to be updated with the recent changes to how
* embedding is handled.
*
* A wrapper around the win32 windowing API to create and destroy editor
* windows. We can embed this window into the window provided by the host, and a
* VST plugin can then later embed itself in the window create here.
*
* This was originally implemented using XEmbed. Even though that sounded like
* the right thing to do, there were a few small issues with Wine's XEmbed
* implementation. The most important of which is that resizing GUIs sometimes
* works fine, but often fails to expand the embedded window's client area
* leaving part of the window inaccessible. There are also some a small number
* of plugins (such as Serum) that have rendering issues when using XEmbed but
* otherwise draw fine when running standalone or when just reparenting the
* window without using XEmbed. If anyone knows how to work around these two
* issues, please let me know and I'll switch to using XEmbed again.
*
* This workaround was inspired by LinVst.
*
* As of yabridge 3.0 XEmbed is back as an option, but it's disabled by default
* because of the issues mentioned above.
*
* In yabridge 3.5.0 we added another layer to the embedding structure. This is
* to prevent the host from directly using the size of `wine_window_`, which has
* a client area the size of the entire root window so the window can resized
* and fullscreened at will. Some hosts, like Carla 2.3.1 (this didn't happen in
* earlier versions), may directly resize their editor window depending on the
* child window's size even without using XEmbed. To combat this, we need to
* manually manage a window that sits in between the parent window and wine's
* window. The embedding structure thus ends up looking like:
*
* ```
* [host_window ->] parent_window -> wrapper_window -> wine_window
* ```
*
* Where `host_window` and `parent_window` may be the same window (which will be
* the case for most hosts), and `wine_window` is the X11 window backing the
* window we created using `CreateWindowEx()`. We will need to manually resize
* `wrapper_window` to match size changes coming from and going to the plugin
* belonging to `wine_window`.
*/
class Editor {
public:
/**
* Open a window, embed it into the DAW's parent window and create a handle
* to the new Win32 window that can be used by the hosted VST plugin.
*
* @param main_context The application's main IO context running on the GUI
* thread. We use this to defer closing the window in
* `DestroyWindow::~DestroyWindow()`.
* @param config This instance's configuration, used to enable alternative
* editor behaviours.
* @param logger A logger instance created with
* `Logger::create_wine_stderr()`. We'll use this to print editor tracing
* information only when needed.
* @param parent_window_handle The X11 window handle passed by the VST host
* for the editor to embed itself into.
* @param timer_proc A function to run on a timer. This is used for VST2
* plugins to periodically call `effEditIdle` from the message loop
* thread, even when the GUI is blocked.
*
* @see win32_window_
*/
Editor(
MainContext& main_context,
const Configuration& config,
Logger& logger,
const size_t parent_window_handle,
std::optional<fu2::unique_function<void()>> timer_proc = std::nullopt,
std::optional<Size> initial_size = std::nullopt);
/**
* Resize the `wrapper_window_` to this new size. We need to manually call
* this whenever the plugin requests a resize, or when the host resizes the
* window (using the plugin API). Before yabridge 3.5.0 this was implicit.
*/
void resize(uint16_t width, uint16_t height);
/**
* Check if the wrapper window's actual X11 size matches the expected size.
* Returns the expected size if there's a mismatch, or nullopt if sizes
* match. This is used as a workaround for VST3 plugins where rapid
* resizing during mutual recursion can cause the X11 window to get stuck
* at an intermediate size.
*/
std::optional<Size> check_size_mismatch();
/**
* Show the window, should be called after the plugin has embedded itself.
* There's absolutely zero reason why this can't be done in the constructor,
* but it can't be. Thanks Waves.
*/
void show() noexcept;
/**
* Handle X11 events sent to the window our editor is embedded in.
*/
void handle_x11_events() noexcept;
/**
* Get the Win32 window handle so it can be passed to an `effEditOpen()`
* call.
*/
HWND win32_handle() const noexcept;
/**
* Returns `true` if the window manager supports the EWMH active window
* protocol through the `_NET_ACTIVE_WINDOW` attribute. Some more
* minimalistic window managers may not support this. In that case we'll
* show a warning and fall back to a more hacky approach to grabbing input
* focus. This involves checking whether the `_NET_ACTIVE_WINDOW` atom
* exists and whether the property is set on the root window. The result is
* cached in `supports_ewmh_active_window_cache`.
*/
bool supports_ewmh_active_window() const;
/**
* Steal or release keyboard focus. This is done whenever the user clicks on
* the window since we don't have a way to detect whether the client window
* is calling `SetFocus()`. See the comment inside of this function for more
* details on when this is used.
*
* NOTE: There's a little bit of special behaviour in here. When the shift
* key is held while grabbing input focus, then we'll focus
* `wine_window_` directly instead of focussing `wrapper_window_`.
* This allows you to temporarily override the default focus grabbing
* behaviour, allowing you to use the space key in plugins GUIs in
* Bitwig and to enter text in Voxengo settings and license dialogs.
* This can also help with plugins that use popups but still rely on
* the parent window's keyboard events to come up to control those
* popups.
*
* @param grab Whether to grab input focus (if `true`) or to give back input
* focus to `host_window_` (if `false`).
*/
void set_input_focus(bool grab) const;
/**
* Run the X11 event loop plus the timer proc function passed to the
* constructor, if one was passed.
*
* @see idle_timer_
* @see idle_timer_proc_
*/
void run_timer_proc();
/**
* Get the editor's (or, the wrapper window's) current size.
*/
inline Size size() const noexcept { return wrapper_window_size_; }
/**
* Whether the `editor_force_dnd` workaround for REAPER should be activated.
* See the implementation in `editor.cpp` for more details.
*/
const bool use_force_dnd_;
private:
/**
* Get the X11 event mask containing the current keyboard modifiers. Because
* we don't want to link with `xcb-xkb` and we also can't really use
* key/motion events for this, we'll do this by querying the pointer
* position instead. Will return a nullopt if that query fails.
*/
std::optional<uint16_t> get_active_modifiers() const noexcept;
/**
* Get the current cursor position, in Win32 screen coordinates. This is
* needed for our `LeaveNotify` handling because `GetCursorPos()` only
* updates once every 100 ms. This takes the X11 mouse cursor position, and
* then adds to that the difference between `wine_window_`'s X11 coordinates
* and its Win32 coordinates. This is kind of a workaround for Wine's
* X11drv's `root_to_virtual_screen()` function not being exposed.
*
* If we cannot obtain the X11 cursor position, then this returns a nullopt.
*/
std::optional<POINT> get_current_pointer_position() const noexcept;
/**
* Returns `true` if the currently active window (as per
* `_NET_ACTIVE_WINDOW`) contains `wine_window_`. If the window manager does
* not support this hint, this will always return false.
*
* @see Editor::supports_ewmh_active_window
*/
bool is_wine_window_active() const;
/**
* After `parent_window_` gets reparented, we may need to redetect which
* toplevel-ish window the host is using and adjust the events we're
* subscribed to accordingly.
*/
void redetect_host_window() noexcept;
/**
* Get offset of parent window to fix mouse coordinates.
*/
std::array<int16_t, 2> get_parent_window_offset();
/**
* Reparent `child` to `new_parent`. This includes the flush.
*/
void do_reparent(xcb_window_t child, xcb_window_t new_parent) const;
/**
* The logger instance we will print debug tracing information to.
*/
Logger& logger_;
/**
* Every editor window gets its own X11 connection.
*/
std::shared_ptr<xcb_connection_t> x11_connection_;
/**
* A handle for our Wine->X11 drag-and-drop proxy. We only have one of these
* per process, and it gets freed again when the last handle gets dropped.
*/
WineXdndProxy::Handle dnd_proxy_handle_;
/**
* The size of the wrapper window. We'll prevent CLAP resize requests when
* the wrapper window is already at the correct size.
*/
Size wrapper_window_size_;
/**
* Last received configurations for the host and parent windows.
*/
xcb_configure_notify_event_t host_window_config_;
xcb_configure_notify_event_t parent_window_config_;
bool parent_window_config_abs_;
/**
* The handle for the window created through Wine that the plugin uses to
* embed itself in.
*/
DeferredWin32Window win32_window_;
/**
* A timer we'll use to periodically run the X11 event loop plus
* `idle_timer_proc_`, if that is set. We handle X11 events from within the
* Win32 event loop because that allows us to still process those while the
* GUI is blocked. Additionally for VST2 plugins we also need this
* `idle_timer_proc_`, as they expected the host to periodically send an
* idle event. We used to just pass through the calls from the host before
* yabridge 3.x, but doing it ourselves here makes things m much more
* manageable and we'd still need a timer anyways for when the GUI is
* blocked.
*/
Win32Timer idle_timer_;
/**
* A function to call when the Win32 timer procs. This is used to
* periodically call `handle_x11_events()`, as well as `effEditIdle()` for
* VST2 plugins even if the GUI is being blocked.
*/
fu2::unique_function<void()> idle_timer_proc_;
/**
* The atom corresponding to `WM_STATE`.
*/
xcb_atom_t xcb_wm_state_property_;
/**
* The atom corresponding to `WM_WINDOW_ROLE`.
*/
xcb_atom_t xcb_wm_window_role_property_;
/**
* The window handle of the editor window created by the DAW.
*/
const xcb_window_t parent_window_;
/**
* A window that sits between `parent_window_` and `wine_window_`. The
* entire purpose of this is to prevent the host from responding to the
* `ConfigureNotify` events we send to `wine_window_` when the host
* subscribes to `SubStructureNotify` events on `parent_window_`.
*/
X11Window wrapper_window_;
/**
* The X11 window handle of the window belonging to `win32_window_`.
*/
const xcb_window_t wine_window_;
/**
* The toplevel X11 window `parent_window_` is contained in, or
* `parent_window_` if the host doesn't do any fancy window embedding. We'll
* find this by looking for the topmost ancestor window of `parent_window_`
* that has `WM_STATE` set. This is similar to how `xprop` and `xwininfo`
* select windows. In most cases this is going to be the same as
* `parent_window_`, but some DAWs (such as REAPER) embed `parent_window_`
* into another window. We have to listen for configuration changes on this
* topmost window to know when the window is being dragged around, and when
* returning keyboard focus to the host we'll focus this window.
*
* NOTE: When reopening a REAPER FX window that has previously been closed,
* REAPER will initialize the first plugin's editor first before
* opening the window. This means that the topmost FX window doesn't
* actually exist yet at that point, so we need to redetect this
* later.
* NOTE: Taking the very topmost window is not an option, because for some
* reason REAPER will only process keyboard input for that window when
* the mouse is within the window.
*/
xcb_window_t host_window_;
/**
* The atom corresponding to `_NET_ACTIVE_WINDOW`.
*/
xcb_atom_t active_window_property_;
/**
* Whether the root window supports the `_NET_ACTIVE_WINDOW` hint. We'll
* check this once and then cache the results in
* `supports_ewmh_active_window()`.
*/
mutable std::optional<bool> supports_ewmh_active_window_cache_;
};