Merge branch 'new-wine10-embedding'

This implements an alternative approach to window embedding that works
with the X11 changes in Wine 9.22+. Thanks everyone for helping out
here! See the following two issues for more details:

https://github.com/robbert-vdh/yabridge/issues/382
https://github.com/robbert-vdh/yabridge/issues/409
This commit is contained in:
Robbert van der Helm
2026-04-26 16:26:12 +02:00
12 changed files with 450 additions and 475 deletions
+13
View File
@@ -10,13 +10,26 @@ Versioning](https://semver.org/spec/v2.0.0.html).
### Fixed
- Fixed a compatibility issue with **Wine 9.22** and above that caused mouse
clicks in plugin GUIs to not register properly. A massive thanks to Rémi
Bernon for looking into this!
- Worked around an interaction between **Ubuntu 24.10** and certain hosts like
**Ardour** that would cause yabridge to hang and eventually crash the host by
consuming too much memory. This only affected the prebuilt binaries from the
releases page.
- As a side effect of the Wine 9.22 fix, plugin GUIs are now also no longer
offset when the plugin window is dragged offscreen on the top and/or left
sides of the screen.
- Similarly, popups should no longer spuriously appear in the wrong placeo n
screen. This mostly affected _MeldaProduction_ plugin
### Removed
- The `editor_xembed` compatibility option has been removed. This option hasn't
worked properly for the last couple major Wine releases.
- The `editor_coordinate_hack` compatibility option has been removed. This was a
very specific option to work around a very specific problem, and its existence
resulted in more confusion than it solved problems.
- Out of the box support for building a 32-bit version of yabridge for use in
64-bit machines has been dropped as part of solving a compatibility issue with
newer Meson versions
-15
View File
@@ -346,10 +346,8 @@ you load a new plugin._
| Option | Values | Description |
| ----------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `disable_pipes` | `{true,false,<string>}` | When this option is enabled, yabridge will redirect the Wine plugin host's output streams to a file without any further processing. See the [known issues](#known-issues-and-fixes) section for a list of plugins where this may be useful. This can be set to a boolean, in which case the output will be written to `$XDG_RUNTIME_DIR/yabridge-plugin-output.log`, or to an absolute path (with no expansion for tildes or environment variables). Defaults to `false`. |
| `editor_coordinate_hack` | `{true,false}` | Compatibility option for plugins that rely on the absolute screen coordinates of the window they're embedded in. Since the Wine window gets embedded inside of a window provided by your DAW, these coordinates won't match up and the plugin would end up drawing in the wrong location without this option. Currently the only known plugins that require this option are _PSPaudioware E27_ and _Soundtoys Crystallizer_. Defaults to `false`. |
| `editor_disable_host_scaling` | `{true,false}` | Disable host-driven HiDPI scaling for VST3 and CLAP plugins. Wine currently does not have proper fractional HiDPI support, so you might have to enable this option if you're using a HiDPI display. In most cases setting the font DPI in `winecfg`'s graphics tab to 192 will cause plugins to scale correctly at 200% size. Defaults to `false`. |
| `editor_force_dnd` | `{true,false}` | This option forcefully enables drag-and-drop support in _REAPER_. Because REAPER's FX window supports drag-and-drop itself, dragging a file onto a plugin editor will cause the drop to be intercepted by the FX window. This makes it impossible to drag files onto plugins in REAPER under normal circumstances. Setting this option to `true` will strip drag-and-drop support from the FX window, thus allowing files to be dragged onto the plugin again. Defaults to `false`. |
| `editor_xembed` | `{true,false}` | Use Wine's XEmbed implementation instead of yabridge's normal window embedding method. Some plugins will have redrawing issues when using XEmbed and editor resizing won't always work properly with it, but it could be useful in certain setups. You may need to use [this Wine patch](https://github.com/psycha0s/airwave/blob/master/fix-xembed-wine-windows.patch) if you're getting blank editor windows. Defaults to `false`. |
| `frame_rate` | `<number>` | The rate at which Win32 events are being handled and usually also the refresh rate of a plugin's editor GUI. When using plugin groups all plugins share the same event handling loop, so in those the last loaded plugin will set the refresh rate. Defaults to `60`. |
| `hide_daw` | `{true,false}` | Don't report the name of the actual DAW to the plugin. See the [known issues](#known-issues-and-fixes) section for a list of situations where this may be useful. This affects VST2, VST3, and CLAP plugins. Defaults to `false`. |
| `vst3_prefer_32bit` | `{true,false}` | Use the 32-bit version of a VST3 plugin instead the 64-bit version if both are installed and they're in the same VST3 bundle inside of `~/.vst3/yabridge`. You likely won't need this. |
@@ -377,12 +375,6 @@ group = "melda"
["ToneBoosters"]
group = "toneboosters"
["PSPaudioware"]
editor_coordinate_hack = true
["Analog Lab 3.so"]
editor_xembed = true
["Chromaphone 3.so"]
hide_daw = true
@@ -582,13 +574,6 @@ Aside from that, these are some known caveats:
VST 2.4 has no way to let the host know that those labels have been updated.
Deactivating and reactivating the plugin will cause these labels to be updated
again for the current patch.
- The Cinnamon desktop environment has some quirks with its window management
that affect yabridge's plugin editor embedding. Most notably some plugins may
flicker while dragging windows around, and there may be [rendering
issues](https://github.com/robbert-vdh/yabridge/issues/89) when using multiple
monitors depending on which screen has been set as primary. Enabling the
XEmbed [compatibility option](#compatibility-options) may help, but Wine's
XEmbed implementation also introduces other rendering issues.
There are also some (third party) plugin API extensions for that have not been
implemented yet. See the [roadmap](./ROADMAP.md) for a list of future plans.
+1 -1
View File
@@ -4,7 +4,7 @@ cpp = 'wineg++'
ar = 'ar'
strip = 'strip'
# Needs to be specified explicitely for Fedora 32
pkgconfig = 'pkg-config'
pkg-config = 'pkg-config'
# Useful for packaging so Meson can resolve dependencies without a pkg-config
# file from the repositories
cmake = 'cmake'
+8 -17
View File
@@ -109,23 +109,14 @@ size or when the plugin resizes its own window. For embedding the Wine window
into the host's window we support two different implementations:
- The main approach involves reparenting the Wine window to the host window, and
then manually sending X11 `ConfigureNotify` events to the corresponding X11
window whenever its size or position on the screen changes. This is needed
because while the reparented Wine window is located at the (relative)
coordinates `(0, 0)`, Wine willl think that these coordinates are absolute
screen coordinates and without sending this event a lot of Windows
applications will either render in the wrong location or have broken knobs and
sliders. By manually sending the event instead of actually reconfiguring the
window Wine will think the window is located at its actual screen coordinates
and user interaction works as expected.
- Alternatively there's an option to use Wine's own XEmbed implementation.
XEmbed is the usual solution for embedding one application window into
approach. However this sadly does have a few quirks, including flickering with
some plugins that use VSTGUI and windows that don't properly rendering until
they are reopened in some hosts. Because of that the above embedding behaviour
that essentially fakes this XEmbed support is the default and XEmbed can be
enabled separately on a plugin by plugin basis by setting a flag in a
`yabridge.toml` config file.
then acting as a minimal X11 window manager that sits between the host window
and Wine's window. This is needed because while the reparented Wine window is
located at the (relative) coordinates `(0, 0)` in the host's window, Wine will
think that these coordinates are absolute screen coordinates and without
sending this event a lot of Windows applications will either render in the
wrong location or have broken knobs and sliders. We solve this by intercepting
window configuration events to let the window know where on screen it is
located.
Aside from embedding the window we also manage keyboard focus grabbing. Since
it's not possible for us to know when the Windows plugin wants keyboard focus,
-12
View File
@@ -97,12 +97,6 @@ Configuration::Configuration(const fs::path& config_path,
} else {
invalid_options.emplace_back(key);
}
} else if (key == "editor_coordinate_hack") {
if (const auto parsed_value = value.as_boolean()) {
editor_coordinate_hack = parsed_value->get();
} else {
invalid_options.emplace_back(key);
}
} else if (key == "editor_disable_host_scaling") {
if (const auto parsed_value = value.as_boolean()) {
editor_disable_host_scaling = parsed_value->get();
@@ -115,12 +109,6 @@ Configuration::Configuration(const fs::path& config_path,
} else {
invalid_options.emplace_back(key);
}
} else if (key == "editor_xembed") {
if (const auto parsed_value = value.as_boolean()) {
editor_xembed = parsed_value->get();
} else {
invalid_options.emplace_back(key);
}
} else if (key == "frame_rate") {
if (const auto parsed_value = value.as_floating_point()) {
frame_rate = parsed_value->get();
-22
View File
@@ -91,18 +91,6 @@ class Configuration {
*/
std::optional<ghc::filesystem::path> disable_pipes;
/**
* If this is set to `true`, then the after every resize we will move the
* embedded Wine window back to `(0, 0)` and then do the coordinate fixing
* trick again. This may be useful with buggy plugins that draw their GUI
* based on the (top level) window's position. Otherwise those GUIs will be
* offset by the window's actual position on screen. The only plugins I've
* encountered where this was necessary were PSPaudioware E27 and Soundtoys
* Crystallizer. This is not enabled by default, because it also interferes
* with resize handles.
*/
bool editor_coordinate_hack = false;
/**
* If set to `true`, we'll remove the `XdndAware` property all ancestor
* windows in `editor.cpp`. This is needed for REAPER as REAPER implements
@@ -112,14 +100,6 @@ class Configuration {
*/
bool editor_force_dnd = false;
/**
* Use XEmbed instead of yabridge's normal editor embedding method. Wine's
* XEmbed support is not very polished yet and tends to lead to rendering
* issues, so this is disabled by default. Also, editor resizing won't work
* reliably when XEmbed is enabled.
*/
bool editor_xembed = false;
/**
* The number of times per second we'll handle the event loop. In most
* plugins this also controls the plugin editor GUI's refresh rate.
@@ -197,9 +177,7 @@ class Configuration {
s.ext(disable_pipes, bitsery::ext::InPlaceOptional(),
[](S& s, auto& v) { s.ext(v, bitsery::ext::GhcPath{}); });
s.value1b(editor_coordinate_hack);
s.value1b(editor_force_dnd);
s.value1b(editor_xembed);
s.ext(frame_rate, bitsery::ext::InPlaceOptional(),
[](S& s, auto& v) { s.value4b(v); });
s.value1b(hide_daw);
-6
View File
@@ -289,18 +289,12 @@ class PluginBridge {
"hack: pipes disabled, plugin output will go to \"" +
config_.disable_pipes->string() + "\"");
}
if (config_.editor_coordinate_hack) {
other_options.push_back("editor: coordinate hack");
}
if (config_.editor_disable_host_scaling) {
other_options.push_back("editor: no host DPI scaling");
}
if (config_.editor_force_dnd) {
other_options.push_back("editor: force drag-and-drop");
}
if (config_.editor_xembed) {
other_options.push_back("editor: XEmbed");
}
if (config_.frame_rate) {
std::ostringstream option;
option << "frame rate: " << std::setprecision(2)
@@ -52,8 +52,18 @@ Vst3PlugFrameProxyImpl::resizeView(Steinberg::IPlugView* /*view*/,
// We have to use this special sending function here so we can handle
// calls to `IPlugView::onSize()` from this same thread (the UI thread).
// See the docstring for more information.
return bridge_.send_mutually_recursive_message(YaPlugFrame::ResizeView{
.owner_instance_id = owner_instance_id(), .new_size = *newSize});
const tresult result =
bridge_.send_mutually_recursive_message(YaPlugFrame::ResizeView{
.owner_instance_id = owner_instance_id(), .new_size = *newSize});
// Some hosts (like Carla) don't call onSize() after accepting a
// resizeView() request. This causes the plugin to not know about its
// new size, so it doesn't draw beyond the original size. Call the
// plugin's onSize() ourselves to ensure it knows the new size. If the
// host already called onSize(), this will be a harmless duplicate call.
bridge_.notify_plugin_on_new_size(owner_instance_id(), *newSize);
return result;
} else {
std::cerr
<< "WARNING: Null pointer passed to 'IPlugFrame::resizeView()'"
+81 -4
View File
@@ -799,9 +799,50 @@ void Vst3Bridge::run() {
// be done in the main UI thread
return main_context_
.run_in_context([&, &instance = instance]() -> tresult {
Steinberg::ViewRect size;
std::optional<Size> initial_size;
// Only accept the initial size from the plugin if it's
// valid. Some plugins like Spectrasonics' Omnisphere 2
// return 0x0 for the initial size, which breaks our
// reparenting in the editor.
if (instance.plug_view_instance->plug_view->getSize(
&size) == Steinberg::kResultOk &&
size.getWidth() > 0 && size.getHeight() > 0) {
initial_size.emplace(size.getWidth(),
size.getHeight());
}
// HACK: Create a resize watchdog that periodically
// verifies the wrapper window size matches the expected
// size. This works around VST3 resize issues (mostly)
// in Ardour during mutual recursion where X11
// operations may not be applied and the wrapper window
// remains smaller or larger than the wine window. The
// goal here is eventual consistency
auto resize_watchdog = [&instance = instance] {
if (instance.editor) {
if (const auto expected =
instance.editor
->check_size_mismatch()) {
// Resize the plugin view to propagate the
// target size everywhere.
if (instance.plug_view_instance) {
Steinberg::ViewRect rect{
0, 0,
(expected->width),
(expected->height)};
instance.plug_frame_proxy->resizeView(
instance.plug_view_instance
->plug_view,
&rect);
}
}
}
};
Editor& editor_instance = instance.editor.emplace(
main_context_, config_, generic_logger_,
x11_handle);
main_context_, config_, generic_logger_, x11_handle,
std::move(resize_watchdog), initial_size);
const tresult result =
instance.plug_view_instance->plug_view->attached(
editor_instance.win32_handle(), type.c_str());
@@ -899,8 +940,22 @@ void Vst3Bridge::run() {
get_instance(request.owner_instance_id);
std::lock_guard lock(instance.get_size_mutex);
return instance.plug_view_instance->plug_view->getSize(
&size);
auto result =
instance.plug_view_instance->plug_view->getSize(
&size);
// HACK: Sometimes, due to HiDPI scaling, plugins might
// end up with a size that is off by one pixel
// from the requested size. To avoid ending up in
// an infinite loop, just return the size that the
// host requested in this case.
if (result == Steinberg::kResultOk &&
abs(size.getWidth() -
instance.last_set_size.getWidth()) <= 1 &&
abs(size.getHeight() -
instance.last_set_size.getHeight()) <= 1) {
size = instance.last_set_size;
}
return result;
});
return YaPlugView::GetSizeResponse{.result = result,
@@ -934,6 +989,8 @@ void Vst3Bridge::run() {
request.new_size.getHeight());
}
instance.last_set_size = request.new_size;
return result;
});
},
@@ -1336,6 +1393,26 @@ bool Vst3Bridge::resize_editor(size_t instance_id,
}
}
void Vst3Bridge::notify_plugin_on_new_size(size_t instance_id,
Steinberg::ViewRect& new_size) {
const auto& [instance, _] = get_instance(instance_id);
if (instance.plug_view_instance) {
// Skip if the host already called onSize() with this size during
// resizeView(). This is detected by checking if last_set_size already
// matches new_size (the OnSize handler updates last_set_size).
if (instance.last_set_size.getWidth() == new_size.getWidth() &&
instance.last_set_size.getHeight() == new_size.getHeight()) {
return;
}
instance.plug_view_instance->plug_view->onSize(&new_size);
// Update last_set_size so getSize() returns consistent values
instance.last_set_size = new_size;
}
}
void Vst3Bridge::register_context_menu(Vst3ContextMenuProxyImpl& context_menu) {
const auto& [owner_instance, _] =
get_instance(context_menu.owner_instance_id());
+16
View File
@@ -256,6 +256,14 @@ struct Vst3PluginInstance {
* processing.
*/
std::optional<Steinberg::Vst::ProcessSetup> process_setup;
/**
* The last size that was set with onSize(). We use this to fudge the
* return value of getSize() if it is off by one pixel, which can happen
* due to HiDPI rounding. Otherwise, DAWs like Ardour might go into an
* infinite loop trying to adjust the size to a specific target.
*/
Steinberg::ViewRect last_set_size;
};
/**
@@ -310,6 +318,14 @@ class Vst3Bridge : public HostBridge {
*/
bool resize_editor(size_t instance_id, const Steinberg::ViewRect& new_size);
/**
* Notify the plugin of its new size by calling `IPlugView::onSize()`.
* This is called after `resize_editor()` for hosts that don't call
* `onSize()` after accepting a `resizeView()` request (like Carla).
*/
void notify_plugin_on_new_size(size_t instance_id,
Steinberg::ViewRect& new_size);
/**
* Register a context with with `context_menu`'s ID and owner in
* `object_instances`. This will be called during the constructor of
+289 -322
View File
@@ -54,8 +54,7 @@ constexpr size_t idle_timer_id = 1337;
* The X11 event mask for the host window, which in most DAWs except for Ardour
* and REAPER will be the same as `parent_window_`.
*/
constexpr uint32_t host_event_mask =
XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_VISIBILITY_CHANGE;
constexpr uint32_t host_event_mask = XCB_EVENT_MASK_STRUCTURE_NOTIFY;
/**
* The X11 event mask for the parent window. We'll use this for input focus
@@ -64,7 +63,7 @@ constexpr uint32_t host_event_mask =
* reparents.
*/
constexpr uint32_t parent_event_mask =
host_event_mask | XCB_EVENT_MASK_FOCUS_CHANGE |
XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_FOCUS_CHANGE |
XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW;
/**
@@ -77,9 +76,9 @@ constexpr uint32_t parent_event_mask =
* slightly when the mouse is already inside of the editor window when
* opening it.
*/
constexpr uint32_t wrapper_event_mask = XCB_EVENT_MASK_STRUCTURE_NOTIFY |
XCB_EVENT_MASK_KEY_PRESS |
XCB_EVENT_MASK_KEY_RELEASE;
constexpr uint32_t wrapper_event_mask =
XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | XCB_EVENT_MASK_STRUCTURE_NOTIFY |
XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE;
/**
* The name of the X11 property on the root window used to denote the active
@@ -88,29 +87,32 @@ constexpr uint32_t wrapper_event_mask = XCB_EVENT_MASK_STRUCTURE_NOTIFY |
constexpr char active_window_property_name[] = "_NET_ACTIVE_WINDOW";
/**
* We'll use this property to filter windows for `host_window_`. Like `xprop`
* and `xwininfo`, we'll only consider windows with this property set.
* We'll set this property on the Wine window to emulate the behavior of
* a minimal window manager.
*/
constexpr char wm_state_property_name[] = "WM_STATE";
constexpr char icccm_wm_state_property_name[] = "WM_STATE";
/**
* We'll use this property to filter windows for `host_window_`. WM_STATE
* can end up being set too late during window initialization, so we miss
* identifying the host window.
*/
constexpr char icccm_wm_window_role_property_name[] = "WM_WINDOW_ROLE";
/**
* This `WM_STATE` property value indicates that a window is a visible top level
* window. Needs to be set on Wine's window as part of emulating the behavior of
* a minimal window manager.
*
* Taken from the ICCCM specification:
*
* <https://x.org/releases/X11R7.6/doc/xorg-docs/specs/ICCCM/icccm.html#wm_state_property>
*/
constexpr uint32_t icccm_wm_state_normal = 1;
// `xdnd_aware_property_name` was moved to `editor.h` so the unity build
// succeeds
/**
* Client message name for XEmbed messages. See
* https://specifications.freedesktop.org/xembed-spec/xembed-spec-latest.html.
*/
constexpr char xembed_message_name[] = "_XEMBED";
// Constants from the XEmbed spec
constexpr uint32_t xembed_protocol_version = 0;
constexpr uint32_t xembed_embedded_notify_msg = 0;
constexpr uint32_t xembed_window_activate_msg = 1;
constexpr uint32_t xembed_focus_in_msg = 4;
constexpr uint32_t xembed_focus_first = 1;
/**
* The default arrow cursor used in Windows.
*/
@@ -157,6 +159,20 @@ std::optional<xcb_window_t> find_host_window(xcb_connection_t& x11_connection,
xcb_window_t starting_at,
xcb_atom_t xcb_wm_state_property);
/**
* Uses xcb_query_tree to find out what the parent of the given window is. This
* seems to be able to find the host if find_host_window fails.
*
* @param x11_connection The X11 connection to use.
* @param window_id The window we want to know the parent window of.
*
* @return The host's editor window, or a nullopt if we cannot find a valid
* window.
*/
std::optional<xcb_window_t> find_host_window_from_query_tree(
xcb_connection_t& x11_connection,
xcb_window_t window_id);
/**
* Check whether `child` is a descendant of `parent` or the same window. Used
* during focus checks to only grab focus when needed.
@@ -170,11 +186,6 @@ std::optional<xcb_window_t> find_host_window(xcb_connection_t& x11_connection,
bool is_child_window_or_same(xcb_connection_t& x11_connection,
xcb_window_t child,
xcb_window_t parent);
/**
* Compute the size a window would have to be to be allowed to fullscreened on
* any of the connected screens.
*/
Size get_maximum_screen_dimensions(xcb_connection_t& x11_connection) noexcept;
/**
* Get the root window for the specified window. The returned root window will
* depend on the screen the window is on.
@@ -259,15 +270,16 @@ Editor::Editor(MainContext& main_context,
const Configuration& config,
Logger& logger,
const size_t parent_window_handle,
std::optional<fu2::unique_function<void()>> timer_proc)
: use_coordinate_hack_(config.editor_coordinate_hack),
use_force_dnd_(config.editor_force_dnd),
use_xembed_(config.editor_xembed),
std::optional<fu2::unique_function<void()>> timer_proc,
std::optional<Size> initial_size)
: use_force_dnd_(config.editor_force_dnd),
logger_(logger),
x11_connection_(xcb_connect(nullptr, nullptr), xcb_disconnect),
dnd_proxy_handle_(WineXdndProxy::get_handle()),
client_area_(get_maximum_screen_dimensions(*x11_connection_)),
wrapper_window_size_({128, 128}),
wrapper_window_size_(initial_size.value_or(Size(128, 128))),
host_window_config_({}),
parent_window_config_({}),
parent_window_config_abs_(false),
// Create a window without any decoratiosn for easy embedding. The
// combination of `WS_EX_TOOLWINDOW` and `WS_POPUP` causes the window to
// be drawn without any decorations (making resizes behave as you'd
@@ -279,23 +291,10 @@ Editor::Editor(MainContext& main_context,
reinterpret_cast<LPCSTR>(get_window_class()),
"yabridge plugin",
WS_POPUP,
// NOTE: With certain DEs/WMs (notably,
// Cinnamon), Wine does not render the
// window at all when using a primary
// display that's positioned to the
// right of another display. Presumably
// it tries to manually clip the client
// rendered client area to the physical
// display. During the reparenting and
// `fix_local_coordinates()` the window
// will be moved to `(0, 0)` anyways,
// but setting its initial position
// according to the primary display
// fixes these rendering issues.
GetSystemMetrics(SM_XVIRTUALSCREEN),
GetSystemMetrics(SM_YVIRTUALSCREEN),
client_area_.width,
client_area_.height,
0,
0,
wrapper_window_size_.width,
wrapper_window_size_.height,
nullptr,
nullptr,
GetModuleHandle(nullptr),
@@ -313,7 +312,10 @@ Editor::Editor(MainContext& main_context,
}
}),
xcb_wm_state_property_(
get_atom_by_name(*x11_connection_, wm_state_property_name)),
get_atom_by_name(*x11_connection_, icccm_wm_state_property_name)),
xcb_wm_window_role_property_(
get_atom_by_name(*x11_connection_,
icccm_wm_window_role_property_name)),
parent_window_(parent_window_handle),
wrapper_window_(
x11_connection_,
@@ -338,7 +340,7 @@ Editor::Editor(MainContext& main_context,
wine_window_(get_x11_handle(win32_window_.handle_)),
host_window_(find_host_window(*x11_connection_,
parent_window_,
xcb_wm_state_property_)
xcb_wm_window_role_property_)
.value_or(parent_window_)) {
logger.log_editor_trace([&]() {
return "DEBUG: host_window: " + std::to_string(host_window_);
@@ -369,24 +371,16 @@ Editor::Editor(MainContext& main_context,
<< std::endl;
}
// When using XEmbed we'll need the atoms for the corresponding properties
xcb_xembed_message_ =
get_atom_by_name(*x11_connection_, xembed_message_name);
// When not using XEmbed, Wine will interpret any local coordinates as
// global coordinates. To work around this we'll tell the Wine window it's
// located at its actual coordinates on screen rather than somewhere within.
// For robustness's sake this should be done both when the actual window the
// Wine window is embedded in (which may not be the parent window) is moved
// or resized, and when the user moves his mouse over the window because
// this is sometimes needed for plugin groups. We also listen for
// EnterNotify and LeaveNotify events on the Wine window so we can grab and
// release input focus as necessary. And lastly we'll look out for
// reparents, so we can make sure that the window does not get stolen by the
// window manager and that we correctly handle the host reparenting
// `parent_window_` themselves.
// If we do enable XEmbed support, we'll also listen for visibility changes
// and trigger the embedding when the window becomes visible
// If you naively reparent `wine_window_` to `parent_window_`, Wine will
// interpret any local coordinates as global coordinates. To work around
// this, we'll tell the Wine window where on screen it's located in the
// `XCB_CONFIGURE_NOTIFY` handler. This happens any time the window the Wine
// window is embedded in (which may not be the parent window) is moved or
// resized. We also listen for EnterNotify and LeaveNotify events on the
// Wine window so we can grab and release input focus as necessary. And
// lastly we'll look out for reparents, so we can make sure that the window
// does not get stolen by the window manager and that we correctly handle
// the host reparenting `parent_window_` themselves.
xcb_change_window_attributes(x11_connection_.get(), host_window_,
XCB_CW_EVENT_MASK, &host_event_mask);
xcb_change_window_attributes(x11_connection_.get(), parent_window_,
@@ -398,22 +392,11 @@ Editor::Editor(MainContext& main_context,
// First reparent our dumb wrapper window to the host's window, and then
// embed the Wine window into our wrapper window
do_reparent(wrapper_window_.window_, parent_window_);
xcb_map_window(x11_connection_.get(), wrapper_window_.window_);
xcb_flush(x11_connection_.get());
if (use_xembed_) {
// This call alone doesn't do anything. We need to call this function a
// second time on visibility change because Wine's XEmbed implementation
// does not work properly (which is why we remvoed XEmbed support in the
// first place).
do_xembed();
} else {
// Embed the Win32 window into the window provided by the host. Instead
// of using the XEmbed protocol, we'll register a few events and manage
// the child window ourselves. This is a hack to work around the issue's
// described in `Editor`'s docstring'.
do_reparent(wine_window_, wrapper_window_.window_);
}
do_reparent(wine_window_, wrapper_window_.window_);
}
void Editor::resize(uint16_t width, uint16_t height) {
@@ -427,59 +410,81 @@ void Editor::resize(uint16_t width, uint16_t height) {
const std::array<uint32_t, 2> values{width, height};
xcb_configure_window(x11_connection_.get(), wrapper_window_.window_,
value_mask, values.data());
xcb_configure_window(x11_connection_.get(), wine_window_, value_mask,
values.data());
xcb_flush(x11_connection_.get());
// This will trigger the `XCB_CONFIGURE_REQUEST` handler in
// `handle_x11_events()`
SetWindowPos(
win32_window_.handle_, nullptr, 0, 0, width, height,
SWP_NOREPOSITION | SWP_NOACTIVATE | SWP_DEFERERASE | SWP_NOMOVE);
// NOTE: This lets us skip resize requests in CLAP plugins when the plugin
// tries to resize to its current size. This fixes resize loops when
// using the CLAP JUCE Extensions.
wrapper_window_size_.width = width;
wrapper_window_size_.height = height;
}
// When the `editor_coordinate_hack` option is enabled, we will make sure
// that the window is actually placed at (0, 0) coordinates. Otherwise some
// plugins that rely on screen coordinates, like the Soundtoys plugins and
// older PSPaudioware plugins, will draw their GUI at the wrong location
// because they look at the (top level) window's screen coordinates instead
// of their own relative coordinates. We don't do by default as this also
// interferes with resize handles.
if (use_coordinate_hack_) {
logger_.log_editor_trace([]() {
return "DEBUG: Resetting Wine window position back to (0, 0)";
});
SetWindowPos(win32_window_.handle_, nullptr, 0, 0, 0, 0,
SWP_NOSIZE | SWP_NOREDRAW | SWP_NOACTIVATE |
SWP_NOOWNERZORDER | SWP_DEFERERASE | SWP_NOCOPYBITS);
std::optional<Size> Editor::check_size_mismatch() {
xcb_generic_error_t* error = nullptr;
const xcb_get_geometry_cookie_t cookie =
xcb_get_geometry(x11_connection_.get(), wrapper_window_.window_);
const std::unique_ptr<xcb_get_geometry_reply_t> geom(
xcb_get_geometry_reply(x11_connection_.get(), cookie, &error));
// Make sure that after the resize the screen coordinates always match
// up properly. Without this Soundtoys Crystallizer might appear choppy
// or skip a frame during their resize animation (which somehow calls
// `audioMasterSizeWindow()` with the same size a bunch of times in a
// row).
fix_local_coordinates();
if (error) {
free(error);
return std::nullopt;
}
if (geom && (geom->width != wrapper_window_size_.width ||
geom->height != wrapper_window_size_.height)) {
logger_.log_editor_trace([&]() {
return "DEBUG: Size mismatch detected. Actual: " +
std::to_string(geom->width) + "x" +
std::to_string(geom->height) + ", Expected: " +
std::to_string(wrapper_window_size_.width) + "x" +
std::to_string(wrapper_window_size_.height);
});
return wrapper_window_size_;
}
return std::nullopt;
}
void Editor::show() noexcept {
ShowWindow(win32_window_.handle_, SW_SHOWNORMAL);
}
std::array<int16_t, 2> Editor::get_parent_window_offset() {
xcb_generic_error_t* error = nullptr;
const xcb_window_t root =
get_root_window(*x11_connection_, parent_window_);
const xcb_translate_coordinates_cookie_t coord_cookie =
xcb_translate_coordinates(x11_connection_.get(), parent_window_, root, 0, 0);
const std::unique_ptr<xcb_translate_coordinates_reply_t> coord_reply(
xcb_translate_coordinates_reply(x11_connection_.get(), coord_cookie, &error));
THROW_X11_ERROR(error);
logger_.log_editor_trace([&]() {
return "DEBUG: Parent window offset " +
std::to_string(coord_reply->dst_x) +
"x" +
std::to_string(coord_reply->dst_y);
});
return {coord_reply->dst_x, coord_reply->dst_y};
}
void Editor::handle_x11_events() noexcept {
// NOTE: Ardour will unmap the window instead of closing the editor. When
// the window is unmapped `wine_window_` doesn't exist and any X11
// function calls involving it will fail. All functions called from
// here should be able to handle that cleanly.
try {
// HACK: See the docstrings on `should_fix_local_coordinates_` and
// `fix_local_coordinates()`
if (should_fix_local_coordinates_ && !is_mouse_button_held()) {
logger_.log_editor_trace([&]() {
return "DEBUG: Performing spooled local coordinate fix";
});
fix_local_coordinates();
should_fix_local_coordinates_ = false;
}
std::unique_ptr<xcb_generic_event_t> generic_event;
while (generic_event.reset(xcb_poll_for_event(x11_connection_.get())),
generic_event != nullptr) {
@@ -542,63 +547,153 @@ void Editor::handle_x11_events() noexcept {
} break;
// We're listening for `ConfigureNotify` events on the host's
// window (i.e. the window that's actually going to get dragged
// around the by the user). In most cases this is the same as
// `parent_window_`. When either this window gets moved, or
// when the user moves his mouse over our window, the local
// coordinates should be updated. The additional `EnterWindow`
// check is sometimes necessary for using multiple editor
// windows within a single plugin group.
// window (i.e. the window that's actually going to get dragged
// around the by the user). In most cases this is the same as
// `parent_window_`. When either this window gets moved, or when
// the user moves his mouse over our window, the local
// coordinates should be updated. The additional `EnterWindow`
// check is sometimes necessary for using multiple editor
// windows within a single plugin group.
case XCB_CONFIGURE_NOTIFY: {
const auto event =
reinterpret_cast<xcb_configure_notify_event_t*>(
generic_event.get());
logger_.log_editor_trace([&]() {
return "DEBUG: ConfigureNotify for window " +
std::to_string(event->window);
std::to_string(event->window) + " : " +
std::to_string(event->width) + "x" +
std::to_string(event->height) + "+" +
std::to_string(event->x) + "+" +
std::to_string(event->y) +
(is_synthetic_event ? " (synthetic)" : "");
});
if (event->window == host_window_ ||
event->window == parent_window_ ||
event->window == wrapper_window_.window_) {
if (!use_xembed_) {
// NOTE: See the docstring on this field. This
// avoids flickering with some window manager
// and plugin combinations when dragging
// plugin windows around.
if (is_mouse_button_held()) {
logger_.log_editor_trace([&]() {
return "DEBUG: ConfigureNotify received "
"while mouse button is held down, "
"spooling the coordinate fix";
});
should_fix_local_coordinates_ = true;
} else {
fix_local_coordinates();
}
// If the host window is different from the parent window
// then the Wine window is at a non-zero offset from the
// top-left corner. The host window will always receive
// absolute position information in its events, sent from
// the window manager, while the parent window might receive
// position changes relative to the host window when it is a
// child window.
//
// For VST2 plugins in Ardour, there seems to be an
// intermediate wrapper window. However, this window
// receives synthetic (absolute) ConfigureNotify events, so
// in this case we keep track of its position directly.
// If this happens once, we ignore all real ConfigureNotify
// events, as the relative position will not be correct if
// there is another offset window between the parent window
// and the host window (as is the case in Ardour). However,
// we still accept the new dimensions of real
// ConfigureNotify events, as this is necessary for resizing
// to work properly.
if (event->window == host_window_ && is_synthetic_event) {
host_window_config_ = *event;
}
if (event->window == parent_window_) {
if (is_synthetic_event || !parent_window_config_abs_) {
parent_window_config_ = *event;
parent_window_config_abs_ = is_synthetic_event;
} else {
parent_window_config_.width = event->width;
parent_window_config_.height = event->height;
}
}
} break;
// Start the XEmbed procedure when the window becomes visible,
// since most hosts will only show the window after the plugin
// has embedded itself into it.
case XCB_VISIBILITY_NOTIFY: {
const auto event =
reinterpret_cast<xcb_visibility_notify_event_t*>(
generic_event.get());
logger_.log_editor_trace([&]() {
return "DEBUG: VisibilityNotify for window " +
std::to_string(event->window);
});
// Window managers are expected to send ConfigureNotify to
// their managed windows whenever the window is being moved
// or resized by the user, so that application don't have to
// do all the relative positioning computation themselves.
// Wine also expects this and ignores position changes on
// its window parents, and its window position would get out
// of sync without this event.
if (event->window == host_window_ ||
event->window == parent_window_) {
if (use_xembed_) {
do_xembed();
xcb_configure_notify_event_t translated_event{};
translated_event.response_type = XCB_CONFIGURE_NOTIFY;
translated_event.event = wine_window_;
translated_event.window = wine_window_;
translated_event.width = parent_window_config_.width;
translated_event.height = parent_window_config_.height;
translated_event.x = parent_window_config_.x;
translated_event.y = parent_window_config_.y;
if (!parent_window_config_abs_ &&
parent_window_ != host_window_) {
translated_event.x += host_window_config_.x;
translated_event.y += host_window_config_.y;
}
if (!is_synthetic_event) {
const std::array<int16_t, 2> offset = get_parent_window_offset();
translated_event.x = offset[0];
translated_event.y = offset[1];
}
logger_.log_editor_trace([&]() {
return "DEBUG: Translated coords: " +
std::to_string(translated_event.window) +
" : " +
std::to_string(translated_event.width) +
"x" +
std::to_string(translated_event.height) +
"+" + std::to_string(translated_event.x) +
"+" + std::to_string(translated_event.y);
});
xcb_send_event(
x11_connection_.get(), false, wine_window_,
XCB_EVENT_MASK_STRUCTURE_NOTIFY |
XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
reinterpret_cast<char*>(&translated_event));
xcb_flush(x11_connection_.get());
}
} break;
// We're listening for `ConfigureRequest` events on the wrapper
// window. This is received whenever Wine wants to configure its
// window, and we need to adjust the configuration so that it
// stays within our wrapper. Here, wwe could translate window
// position changes by moving the wrapper window itself but this
// isn't really necessary. Instead, we prevent Wine from
// actually moving its window.
case XCB_CONFIGURE_REQUEST: {
const auto event =
reinterpret_cast<xcb_configure_request_event_t*>(
generic_event.get());
logger_.log_editor_trace([&]() {
return "DEBUG: ConfigureRequest for window " +
std::to_string(event->window);
});
const uint16_t value_mask =
XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y |
XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT;
const std::array<uint32_t, 4> values{0, 0, event->width,
event->height};
xcb_configure_window(x11_connection_.get(), wine_window_,
value_mask, values.data());
xcb_flush(x11_connection_.get());
} break;
// We're listening for `MapRequest` events on the wrapper
// window. This is received whenever Wine wants to map its
// window, and we need to forward the request to the X server.
// Wine also expects the window manager to change the WM_STATE
// property whenever it has finished mapping the window. We
// effectively implement a sub window manager here, so update
// the property as we should.
case XCB_MAP_REQUEST: {
const auto event =
reinterpret_cast<xcb_map_request_event_t*>(
generic_event.get());
logger_.log_editor_trace([&]() {
return "DEBUG: MapRequest for window " +
std::to_string(event->window);
});
xcb_map_window(x11_connection_.get(), wine_window_);
const std::array<uint32_t, 2> values{icccm_wm_state_normal,
0};
xcb_change_property(
x11_connection_.get(), XCB_PROP_MODE_REPLACE,
wine_window_, xcb_wm_state_property_,
xcb_wm_state_property_, 32, 2, values.data());
xcb_flush(x11_connection_.get());
} break;
// We want to grab keyboard input focus when the user hovers
// over our embedded Wine window AND that window is a child of
// the currently active window. This ensures that the behavior
@@ -630,10 +725,6 @@ void Editor::handle_x11_events() noexcept {
if (window == parent_window_ ||
window == wrapper_window_.window_) {
if (!use_xembed_) {
fix_local_coordinates();
}
// In case the WM somehow does not support
// `_NET_ACTIVE_WINDOW`, a more naive focus grabbing
// method implemented in the `WM_PARENTNOTIFY` handler
@@ -782,67 +873,6 @@ HWND Editor::win32_handle() const noexcept {
return win32_window_.handle_;
}
void Editor::fix_local_coordinates() const {
if (use_xembed_) {
return;
}
// We're purposely not using XEmbed here. This has the consequence that wine
// still thinks that any X and Y coordinates are relative to the x11 window
// root instead of the parent window provided by the DAW, causing all sorts
// of GUI interactions to break. To alleviate this we'll just lie to Wine
// and tell it that it's located at the parent window's location on the root
// window. We also will keep the child window at its largest possible size
// to allow for smooth resizing. This works because the embedding hierarchy
// is DAW window -> Win32 window (created in this class) -> VST plugin
// window created by the plugin itself. In this case it doesn't matter that
// the Win32 window is larger than the part of the client area the plugin
// draws to since any excess will be clipped off by the parent window.
const xcb_window_t root = get_root_window(*x11_connection_, parent_window_);
// We can't directly use the `event.x` and `event.y` coordinates because the
// parent window may also be embedded inside another window.
// NOTE: Tracktion Waveform uses client side decorations, and for VST2
// plugins they forgot to add a separate parent window that's already
// offset correctly. Instead, they'll have the plugin embed itself
// inside directly inside of the dialog, and Waveform then moves the
// window 27 pixels down. That's why we cannot use `parent_window_`
// here.
xcb_generic_error_t* error = nullptr;
const xcb_translate_coordinates_cookie_t translate_cookie =
xcb_translate_coordinates(x11_connection_.get(),
wrapper_window_.window_, root, 0, 0);
const std::unique_ptr<xcb_translate_coordinates_reply_t>
translated_coordinates(xcb_translate_coordinates_reply(
x11_connection_.get(), translate_cookie, &error));
THROW_X11_ERROR(error);
xcb_configure_notify_event_t translated_event{};
translated_event.response_type = XCB_CONFIGURE_NOTIFY;
translated_event.event = wine_window_;
translated_event.window = wine_window_;
// This should be set to the same sizes the window was created on. Since
// we're not using `SetWindowPos` to resize the Window, Wine can get a bit
// confused when we suddenly report a different client area size. Without
// this certain plugins (such as those by Valhalla DSP) would break.
translated_event.width = client_area_.width;
translated_event.height = client_area_.height;
translated_event.x = translated_coordinates->dst_x;
translated_event.y = translated_coordinates->dst_y;
logger_.log_editor_trace([&]() {
return "DEBUG: Spoofing local coordinates to (" +
std::to_string(translated_event.x) + ", " +
std::to_string(translated_event.y) + ")";
});
xcb_send_event(
x11_connection_.get(), false, wine_window_,
XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
reinterpret_cast<char*>(&translated_event));
xcb_flush(x11_connection_.get());
}
void Editor::set_input_focus(bool grab) const {
// NOTE: When grabbing focus, you can hold down the shift key to focus the
// Wine window directly. This allows you to use the space key in
@@ -984,18 +1014,6 @@ std::optional<POINT> Editor::get_current_pointer_position() const noexcept {
.y = query_pointer_reply->root_y + (win32_pos.top - x11_y_pos)};
}
bool Editor::is_mouse_button_held() const {
xcb_generic_error_t* error = nullptr;
const xcb_query_pointer_cookie_t pointer_query_cookie =
xcb_query_pointer(x11_connection_.get(), host_window_);
const std::unique_ptr<xcb_query_pointer_reply_t> pointer_query_reply(
xcb_query_pointer_reply(x11_connection_.get(), pointer_query_cookie,
&error));
THROW_X11_ERROR(error);
return pointer_query_reply->mask != 0;
}
bool Editor::is_wine_window_active() const {
if (!supports_ewmh_active_window()) {
return false;
@@ -1026,8 +1044,11 @@ bool Editor::is_wine_window_active() const {
void Editor::redetect_host_window() noexcept {
const xcb_window_t new_host_window =
find_host_window(*x11_connection_, parent_window_,
xcb_wm_state_property_)
.value_or(parent_window_);
xcb_wm_window_role_property_)
.value_or(find_host_window_from_query_tree(*x11_connection_,
parent_window_)
.value_or(parent_window_));
if (new_host_window == host_window_) {
return;
}
@@ -1091,27 +1112,6 @@ bool Editor::supports_ewmh_active_window() const {
return active_window_property_exists;
}
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
void Editor::send_xembed_message(xcb_window_t window,
uint32_t message,
uint32_t detail,
uint32_t data1,
uint32_t data2) const noexcept {
xcb_client_message_event_t event{};
event.response_type = XCB_CLIENT_MESSAGE;
event.type = xcb_xembed_message_;
event.window = window;
event.format = 32;
event.data.data32[0] = XCB_CURRENT_TIME;
event.data.data32[1] = message;
event.data.data32[2] = detail;
event.data.data32[3] = data1;
event.data.data32[4] = data2;
xcb_send_event(x11_connection_.get(), false, window,
XCB_EVENT_MASK_NO_EVENT, reinterpret_cast<char*>(&event));
}
void Editor::do_reparent(xcb_window_t child, xcb_window_t new_parent) const {
const xcb_void_cookie_t reparent_cookie = xcb_reparent_window_checked(
x11_connection_.get(), child, new_parent, 0, 0);
@@ -1161,28 +1161,6 @@ void Editor::do_reparent(xcb_window_t child, xcb_window_t new_parent) const {
xcb_flush(x11_connection_.get());
}
void Editor::do_xembed() const {
if (!use_xembed_) {
return;
}
// If we're embedding using XEmbed, then we'll have to go through the whole
// XEmbed dance here. See the spec for more information on how this works:
// https://specifications.freedesktop.org/xembed-spec/xembed-spec-latest.html#lifecycle
do_reparent(wine_window_, wrapper_window_.window_);
// Let the Wine window know it's being embedded into the parent window
send_xembed_message(wine_window_, xembed_embedded_notify_msg, 0,
wrapper_window_.window_, xembed_protocol_version);
send_xembed_message(wine_window_, xembed_focus_in_msg, xembed_focus_first,
0, 0);
send_xembed_message(wine_window_, xembed_window_activate_msg, 0, 0, 0);
xcb_flush(x11_connection_.get());
xcb_map_window(x11_connection_.get(), wine_window_);
xcb_flush(x11_connection_.get());
}
LRESULT CALLBACK window_proc(HWND handle,
UINT message,
WPARAM wParam,
@@ -1205,19 +1183,6 @@ LRESULT CALLBACK window_proc(HWND handle,
handle, GWLP_USERDATA,
static_cast<LONG_PTR>(reinterpret_cast<size_t>(editor)));
} break;
// Setting `SWP_NOCOPYBITS` somewhat reduces flickering on
// `fix_local_coordinates()` calls with plugins that don't do double
// buffering since it speeds up the redrawing process.
case WM_WINDOWPOSCHANGING: {
auto editor = reinterpret_cast<Editor*>(
GetWindowLongPtr(handle, GWLP_USERDATA));
if (!editor || editor->use_xembed_) {
break;
}
WINDOWPOS* info = reinterpret_cast<WINDOWPOS*>(lParam);
info->flags |= SWP_DEFERERASE | SWP_NOCOPYBITS;
} break;
case WM_TIMER: {
auto editor = reinterpret_cast<Editor*>(
GetWindowLongPtr(handle, GWLP_USERDATA));
@@ -1336,6 +1301,28 @@ std::optional<xcb_window_t> find_host_window(
return std::nullopt;
}
std::optional<xcb_window_t> find_host_window_from_query_tree(
xcb_connection_t& x11_connection,
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
const xcb_window_t window_id) {
const xcb_query_tree_cookie_t cookie =
xcb_query_tree(&x11_connection, window_id);
xcb_generic_error_t* error = nullptr;
const std::unique_ptr<xcb_query_tree_reply_t, decltype(&free)> reply(
xcb_query_tree_reply(&x11_connection, cookie, &error), free);
if (!error && reply) {
if (const xcb_window_t actual_parent = reply->parent;
actual_parent != XCB_NONE) {
return actual_parent;
}
} else {
free(error);
}
return std::nullopt;
}
bool is_child_window_or_same(
xcb_connection_t& x11_connection,
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
@@ -1377,27 +1364,6 @@ xcb_atom_t get_atom_by_name(xcb_connection_t& x11_connection,
return atom_reply->atom;
}
Size get_maximum_screen_dimensions(xcb_connection_t& x11_connection) noexcept {
xcb_screen_iterator_t iter =
xcb_setup_roots_iterator(xcb_get_setup(&x11_connection));
// Find the maximum dimensions the window would have to be to be able to be
// fullscreened on any screen, disregarding the possibility that someone
// would try to stretch the window accross all displays (because who would
// do such a thing?)
Size maximum_screen_size{};
while (iter.rem > 0) {
maximum_screen_size.width =
std::max(maximum_screen_size.width, iter.data->width_in_pixels);
maximum_screen_size.height =
std::max(maximum_screen_size.height, iter.data->height_in_pixels);
xcb_screen_next(&iter);
}
return maximum_screen_size;
}
xcb_window_t get_root_window(xcb_connection_t& x11_connection,
xcb_window_t window) {
xcb_generic_error_t* error = nullptr;
@@ -1450,15 +1416,16 @@ bool is_cursor_in_wine_window(
if (HWND windows_window = WindowFromPoint(*windows_pointer_pos);
windows_window && windows_window != windows_desktop_window) {
// NOTE: Because resizing reparented Wine windows without XEmbed is a
// bit janky, yabridge creates windows with client areas large
// enough to fit the entire screen, and the plugin then embeds its
// own GUI in a portion of that. The result is that
// `WindowFromPoint()` will still return that same huge window
// when we hover over an area to the right or to the bottom of a
// plugin GUI. We can easily detect by just checking the window
// class name. Also check out the `WM_NCHITTEST` implementation in
// the message loop above.
// NOTE: It could happen that the reparented Wine window extends beyond
// the host's window. In that case `WindowFromPoint()` will still
// think we're hovering over the plugin's GUI when hovering over
// an area to the right or to the bottom of a plugin GUI. We can
// easily detect by just checking the window class name. Also
// check out the `WM_NCHITTEST` implementation in the message loop
// above.
//
// This may not be necessary anymore with the new window
// configuration approach.
std::array<char, 64> window_class_name{0};
GetClassName(windows_window, window_class_name.data(),
window_class_name.size());
+30 -74
View File
@@ -18,7 +18,6 @@
#include <memory>
#include <optional>
#include <string>
#include <windows.h>
#include <function2/function2.hpp>
@@ -127,6 +126,9 @@ class DeferredWin32Window {
};
/**
* 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.
@@ -192,7 +194,8 @@ class Editor {
const Configuration& config,
Logger& logger,
const size_t parent_window_handle,
std::optional<fu2::unique_function<void()>> timer_proc = std::nullopt);
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
@@ -201,10 +204,19 @@ class Editor {
*/
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
* or in `do_xembed()`, but it needs to be. Thanks Waves.
* There's absolutely zero reason why this can't be done in the constructor,
* but it can't be. Thanks Waves.
*/
void show() noexcept;
@@ -230,18 +242,6 @@ class Editor {
*/
bool supports_ewmh_active_window() const;
/**
* Lie to the Wine window about its coordinates on the screen for
* reparenting without using XEmbed. See the comment at the top of the
* implementation on why this is needed.
*
* One of the events that trigger this is `ConfigureNotify` messages. Some
* WMs may continuously send this message while dragging a window around. To
* avoid flickering, the main `handle_x11_events()` function will wait to
* call this function until the all mouse buttons have been released.
*/
void fix_local_coordinates() 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
@@ -277,25 +277,12 @@ class Editor {
*/
inline Size size() const noexcept { return wrapper_window_size_; }
/**
* Whether to reposition `win32_window_` to (0, 0) every time the window
* resizes. This can help with buggy plugins that use the (top level)
* window's screen coordinates when drawing their GUI.
*/
const bool use_coordinate_hack_;
/**
* 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_;
/**
* Whether to use XEmbed instead of yabridge's normal window embedded. Wine
* with XEmbed tends to cause rendering issues, so it's disabled by default.
*/
const bool use_xembed_;
private:
/**
* Get the X11 event mask containing the current keyboard modifiers. Because
@@ -317,12 +304,6 @@ class Editor {
*/
std::optional<POINT> get_current_pointer_position() const noexcept;
/**
* Checks whether any mouse button is held. Used to defer calling
* `fix_local_coordinates()` when dragging windows around.
*/
bool is_mouse_button_held() const;
/**
* Returns `true` if the currently active window (as per
* `_NET_ACTIVE_WINDOW`) contains `wine_window_`. If the window manager does
@@ -340,28 +321,15 @@ class Editor {
void redetect_host_window() noexcept;
/**
* Send an XEmbed message to a window. This does not include a flush. See
* the spec for more information:
*
* https://specifications.freedesktop.org/xembed-spec/xembed-spec-latest.html#lifecycle
* Get offset of parent window to fix mouse coordinates.
*/
void send_xembed_message(xcb_window_t window,
uint32_t message,
uint32_t detail,
uint32_t data1,
uint32_t data2) const noexcept;
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;
/**
* Start the XEmbed procedure when `use_xembed_` is enabled. This should be
* rerun whenever visibility changes.
*/
void do_xembed() const;
/**
* The logger instance we will print debug tracing information to.
*/
@@ -378,22 +346,19 @@ class Editor {
*/
WineXdndProxy::Handle dnd_proxy_handle_;
/**
* The Wine window's client area, or the maximum size of that window. This
* will be set to a size that's large enough to be able to enter full screen
* on a single display. This is more of a theoretical maximum size, as the
* plugin will only use a portion of this window to draw to. Because we're
* not changing the size of the Wine window and only resize the wrapper
* window it's been embedded in, resizing will feel smooth and native.
*/
const Size client_area_;
/**
* 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.
@@ -425,6 +390,11 @@ class Editor {
*/
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.
*/
@@ -462,15 +432,6 @@ class Editor {
*/
xcb_window_t host_window_;
/**
* Used to delay calling `fix_local_coordinates()` when dragging windows
* around with the mouse. Some WMs will continuously send `ConfigureNotify`
* messages when dragging windows around, and the `fix_local_coordinates()`
* function may cause the window to blink. This becomes a but jarring if it
* happens 60 times per second while dragging windows around.
*/
bool should_fix_local_coordinates_ = false;
/**
* The atom corresponding to `_NET_ACTIVE_WINDOW`.
*/
@@ -481,9 +442,4 @@ class Editor {
* `supports_ewmh_active_window()`.
*/
mutable std::optional<bool> supports_ewmh_active_window_cache_;
/**
* The atom corresponding to `_XEMBED`.
*/
xcb_atom_t xcb_xembed_message_;
};