From 2c6c21409c882411f0f26bcc63b641f7df8115d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Bernon?= Date: Thu, 27 Feb 2025 12:29:02 +0100 Subject: [PATCH 01/26] Fix window wrapping by implementing a minimal ICCCM window manager. --- src/wine-host/editor.cpp | 95 +++++++++++++++++++++++++++++++++++++++- src/wine-host/editor.h | 7 +++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 5186d406..6d4dfd2f 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -77,7 +77,8 @@ 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 | +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; @@ -268,6 +269,8 @@ Editor::Editor(MainContext& main_context, dnd_proxy_handle_(WineXdndProxy::get_handle()), client_area_(get_maximum_screen_dimensions(*x11_connection_)), wrapper_window_size_({128, 128}), + host_window_config_({}), + parent_window_config_({}), // 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 @@ -427,6 +430,8 @@ void Editor::resize(uint16_t width, uint16_t height) { const std::array 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()); // NOTE: This lets us skip resize requests in CLAP plugins when the plugin @@ -558,6 +563,47 @@ void Editor::handle_x11_events() noexcept { std::to_string(event->window); }); + // 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. + if (event->window == host_window_ && + is_synthetic_event) { + host_window_config_ = *event; + } + if (event->window == parent_window_ && + host_window_ != parent_window_ && + !is_synthetic_event) { + parent_window_config_ = *event; + } + + // 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_) { + 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 = event->width; + translated_event.height = event->height; + translated_event.x = host_window_config_.x + parent_window_config_.x; + translated_event.y = host_window_config_.y + parent_window_config_.y; + + xcb_send_event( + x11_connection_.get(), false, wine_window_, + XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY, + reinterpret_cast(&translated_event)); + xcb_flush(x11_connection_.get()); + } + if (event->window == host_window_ || event->window == parent_window_ || event->window == wrapper_window_.window_) { @@ -580,6 +626,53 @@ void Editor::handle_x11_events() noexcept { } } } 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( + 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 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( + 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 values{XCB_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; // 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. diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index 18636c1f..e2b60e14 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -27,6 +27,7 @@ #pragma push_macro("_WIN32") #undef _WIN32 #include +#include #pragma pop_macro("_WIN32") #include "../common/configuration.h" @@ -394,6 +395,12 @@ class Editor { */ 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_; + /** * The handle for the window created through Wine that the plugin uses to * embed itself in. From 02fb140b197f3a31ed1d5ba1233e9261d9cbeaf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Bernon?= Date: Thu, 27 Feb 2025 12:41:06 +0100 Subject: [PATCH 02/26] Remove now unnecessary fix_local_coordinates workaround. --- src/wine-host/editor.cpp | 139 ++------------------------------------- src/wine-host/editor.h | 37 ----------- 2 files changed, 4 insertions(+), 172 deletions(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 6d4dfd2f..554688da 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -267,7 +267,6 @@ Editor::Editor(MainContext& main_context, 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}), host_window_config_({}), parent_window_config_({}), @@ -282,23 +281,10 @@ Editor::Editor(MainContext& main_context, reinterpret_cast(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), @@ -454,13 +440,6 @@ void Editor::resize(uint16_t width, uint16_t height) { SetWindowPos(win32_window_.handle_, nullptr, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOREDRAW | SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_DEFERERASE | SWP_NOCOPYBITS); - - // 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(); } } @@ -474,17 +453,6 @@ void Editor::handle_x11_events() noexcept { // 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 generic_event; while (generic_event.reset(xcb_poll_for_event(x11_connection_.get())), generic_event != nullptr) { @@ -603,28 +571,6 @@ void Editor::handle_x11_events() noexcept { reinterpret_cast(&translated_event)); xcb_flush(x11_connection_.get()); } - - 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(); - } - } - } } break; // We're listening for `ConfigureRequest` events on the // wrapper window. This is received whenever Wine wants @@ -723,10 +669,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 @@ -875,67 +817,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 - 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(&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 @@ -1077,18 +958,6 @@ std::optional 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 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; diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index e2b60e14..1e700d42 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -231,18 +231,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 @@ -318,12 +306,6 @@ class Editor { */ std::optional 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 @@ -379,16 +361,6 @@ 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. @@ -469,15 +441,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`. */ From ccc22bfabc1c146239a817a384e504e02acfafd8 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sat, 1 Mar 2025 16:34:24 +0100 Subject: [PATCH 03/26] Add a changelog entry for the GUI fixes --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84dedc44..c44ea583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,18 @@ 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 From 983a19169b174cbdf96756083661e6b28731470b Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sat, 1 Mar 2025 16:54:59 +0100 Subject: [PATCH 04/26] Don't include xcb_icccm.h We just need this constant, so there's no real need to add the additional dependency. There's also no real harm in adding it, but it would result in additional work for every packager. --- src/wine-host/editor.cpp | 28 ++++++++++++++++++++++------ src/wine-host/editor.h | 2 -- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 554688da..c283389d 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -90,9 +90,22 @@ 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. + * and `xwininfo`, we'll only consider windows with this property set, although + * we won't filter on whether or not the window is actually visible because it + * may be minimalized when the plugin's GUI is being opened. */ -constexpr char wm_state_property_name[] = "WM_STATE"; +constexpr char icccm_wm_state_property_name[] = "WM_STATE"; + +/** + * 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: + * + * + */ +constexpr uint32_t icccm_wm_state_normal = 1; // `xdnd_aware_property_name` was moved to `editor.h` so the unity build // succeeds @@ -302,7 +315,7 @@ 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)), parent_window_(parent_window_handle), wrapper_window_( x11_connection_, @@ -614,9 +627,12 @@ void Editor::handle_x11_events() noexcept { }); xcb_map_window(x11_connection_.get(), wine_window_); - const std::array values{XCB_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()); + const std::array 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; // Start the XEmbed procedure when the window becomes visible, diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index 1e700d42..4d4148f0 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -18,7 +18,6 @@ #include #include -#include #include #include @@ -27,7 +26,6 @@ #pragma push_macro("_WIN32") #undef _WIN32 #include -#include #pragma pop_macro("_WIN32") #include "../common/configuration.h" From ccf5402c1c2672609a54f3b0be7e7a42f9788917 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sat, 1 Mar 2025 18:44:00 +0100 Subject: [PATCH 05/26] Reformat with clang-format --- src/wine-host/editor.cpp | 106 +++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index c283389d..4b9829e1 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -77,10 +77,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_SUBSTRUCTURE_REDIRECT | - 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 @@ -429,8 +428,8 @@ void Editor::resize(uint16_t width, uint16_t height) { const std::array 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_configure_window(x11_connection_.get(), wine_window_, value_mask, + values.data()); xcb_flush(x11_connection_.get()); // NOTE: This lets us skip resize requests in CLAP plugins when the plugin @@ -528,13 +527,13 @@ 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( @@ -544,29 +543,28 @@ void Editor::handle_x11_events() noexcept { std::to_string(event->window); }); - // 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. - if (event->window == host_window_ && - is_synthetic_event) { + // 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. + if (event->window == host_window_ && is_synthetic_event) { host_window_config_ = *event; } if (event->window == parent_window_ && - host_window_ != parent_window_ && - !is_synthetic_event) { + host_window_ != parent_window_ && !is_synthetic_event) { parent_window_config_ = *event; } // 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. + // 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_) { xcb_configure_notify_event_t translated_event{}; @@ -575,24 +573,26 @@ void Editor::handle_x11_events() noexcept { translated_event.window = wine_window_; translated_event.width = event->width; translated_event.height = event->height; - translated_event.x = host_window_config_.x + parent_window_config_.x; - translated_event.y = host_window_config_.y + parent_window_config_.y; + translated_event.x = + host_window_config_.x + parent_window_config_.x; + translated_event.y = + host_window_config_.y + parent_window_config_.y; xcb_send_event( x11_connection_.get(), false, wine_window_, - XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY, + XCB_EVENT_MASK_STRUCTURE_NOTIFY | + XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY, reinterpret_cast(&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. + // 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( @@ -601,22 +601,22 @@ void Editor::handle_x11_events() noexcept { 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 values{0, 0, event->width, event->height}; + const uint16_t value_mask = + XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | + XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT; + const std::array 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. + // 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( @@ -627,8 +627,8 @@ void Editor::handle_x11_events() noexcept { }); xcb_map_window(x11_connection_.get(), wine_window_); - const std::array values{ - icccm_wm_state_normal, 0}; + const std::array values{icccm_wm_state_normal, + 0}; xcb_change_property( x11_connection_.get(), XCB_PROP_MODE_REPLACE, wine_window_, xcb_wm_state_property_, From 0f9eea2558beb53b6bfd5fbab130aa1093b0433e Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sat, 1 Mar 2025 19:07:02 +0100 Subject: [PATCH 06/26] Drop the SWP_NOCOPYBITS on resize hack This shouldn't be necessary anymore. It can always be added back later if there is still flickering. --- src/wine-host/editor.cpp | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 4b9829e1..82c4726c 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -1183,19 +1183,6 @@ LRESULT CALLBACK window_proc(HWND handle, handle, GWLP_USERDATA, static_cast(reinterpret_cast(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( - GetWindowLongPtr(handle, GWLP_USERDATA)); - if (!editor || editor->use_xembed_) { - break; - } - - WINDOWPOS* info = reinterpret_cast(lParam); - info->flags |= SWP_DEFERERASE | SWP_NOCOPYBITS; - } break; case WM_TIMER: { auto editor = reinterpret_cast( GetWindowLongPtr(handle, GWLP_USERDATA)); From 51e4d61004c656e908c6f8d21e56245182bfa362 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sat, 1 Mar 2025 19:13:54 +0100 Subject: [PATCH 07/26] Remove the editor_xembed option --- CHANGELOG.md | 2 + README.md | 11 --- docs/architecture.md | 25 ++---- src/common/configuration.cpp | 6 -- src/common/configuration.h | 9 --- src/plugin/bridges/common.h | 3 - src/wine-host/editor.cpp | 145 ++++++----------------------------- src/wine-host/editor.h | 36 ++------- 8 files changed, 39 insertions(+), 198 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c44ea583..8cef69a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Versioning](https://semver.org/spec/v2.0.0.html). ### Removed +- The `editor_xembed` compatibility option has been removed. This option hasn't + worked properly for the last couple major Wine releases. - 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 diff --git a/README.md b/README.md index 6019b67a..c6df1a70 100644 --- a/README.md +++ b/README.md @@ -337,7 +337,6 @@ you load a new plugin._ | `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` | `` | 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. | @@ -368,9 +367,6 @@ group = "toneboosters" ["PSPaudioware"] editor_coordinate_hack = true -["Analog Lab 3.so"] -editor_xembed = true - ["Chromaphone 3.so"] hide_daw = true @@ -570,13 +566,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. diff --git a/docs/architecture.md b/docs/architecture.md index 72be5947..257323e6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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, diff --git a/src/common/configuration.cpp b/src/common/configuration.cpp index e0b55278..8c5aa30c 100644 --- a/src/common/configuration.cpp +++ b/src/common/configuration.cpp @@ -115,12 +115,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(); diff --git a/src/common/configuration.h b/src/common/configuration.h index 7536e372..20d59ddc 100644 --- a/src/common/configuration.h +++ b/src/common/configuration.h @@ -112,14 +112,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. @@ -199,7 +191,6 @@ class Configuration { [](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); diff --git a/src/plugin/bridges/common.h b/src/plugin/bridges/common.h index 8a54b89a..5d45f5d1 100644 --- a/src/plugin/bridges/common.h +++ b/src/plugin/bridges/common.h @@ -298,9 +298,6 @@ class PluginBridge { 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) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 82c4726c..fcdc5bb6 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -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; /** @@ -109,21 +108,6 @@ 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. */ @@ -275,7 +259,6 @@ Editor::Editor(MainContext& main_context, std::optional> timer_proc) : use_coordinate_hack_(config.editor_coordinate_hack), use_force_dnd_(config.editor_force_dnd), - use_xembed_(config.editor_xembed), logger_(logger), x11_connection_(xcb_connect(nullptr, nullptr), xcb_disconnect), dnd_proxy_handle_(WineXdndProxy::get_handle()), @@ -370,24 +353,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_, @@ -399,22 +374,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) { @@ -635,25 +599,6 @@ void Editor::handle_x11_events() noexcept { xcb_wm_state_property_, 32, 2, values.data()); xcb_flush(x11_connection_.get()); } 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( - generic_event.get()); - logger_.log_editor_trace([&]() { - return "DEBUG: VisibilityNotify for window " + - std::to_string(event->window); - }); - - if (event->window == host_window_ || - event->window == parent_window_) { - if (use_xembed_) { - do_xembed(); - } - } - } 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 @@ -1069,27 +1014,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(&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); @@ -1139,28 +1063,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, @@ -1415,15 +1317,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 window_class_name{0}; GetClassName(windows_window, window_class_name.data(), window_class_name.size()); diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index 4d4148f0..9d6449c0 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -126,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. @@ -202,8 +205,8 @@ class Editor { /** * 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; @@ -277,12 +280,6 @@ class Editor { */ 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 @@ -320,29 +317,11 @@ 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 - */ - void send_xembed_message(xcb_window_t window, - uint32_t message, - uint32_t detail, - uint32_t data1, - uint32_t data2) const noexcept; - /** * 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. */ @@ -449,9 +428,4 @@ class Editor { * `supports_ewmh_active_window()`. */ mutable std::optional supports_ewmh_active_window_cache_; - - /** - * The atom corresponding to `_XEMBED`. - */ - xcb_atom_t xcb_xembed_message_; }; From 66840d4d16b894ae28f197bcbb49ddbab0c2bfa1 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sat, 1 Mar 2025 19:17:29 +0100 Subject: [PATCH 08/26] Remove the editor_coordinate_hack option I'm not sure if this would still be necessary with the new embedding approach, but it definitely does cause more problems than it solves. --- CHANGELOG.md | 3 +++ README.md | 4 ---- src/common/configuration.cpp | 6 ------ src/common/configuration.h | 13 ------------- src/plugin/bridges/common.h | 3 --- src/wine-host/editor.cpp | 19 +------------------ src/wine-host/editor.h | 7 ------- 7 files changed, 4 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cef69a2..b4ee42f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ Versioning](https://semver.org/spec/v2.0.0.html). - 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 diff --git a/README.md b/README.md index c6df1a70..33d76863 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,6 @@ you load a new plugin._ | Option | Values | Description | | ----------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `disable_pipes` | `{true,false,}` | 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`. | | `frame_rate` | `` | 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`. | @@ -364,9 +363,6 @@ group = "melda" ["ToneBoosters"] group = "toneboosters" -["PSPaudioware"] -editor_coordinate_hack = true - ["Chromaphone 3.so"] hide_daw = true diff --git a/src/common/configuration.cpp b/src/common/configuration.cpp index 8c5aa30c..979d9335 100644 --- a/src/common/configuration.cpp +++ b/src/common/configuration.cpp @@ -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(); diff --git a/src/common/configuration.h b/src/common/configuration.h index 20d59ddc..12bcbffe 100644 --- a/src/common/configuration.h +++ b/src/common/configuration.h @@ -91,18 +91,6 @@ class Configuration { */ std::optional 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 @@ -189,7 +177,6 @@ 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.ext(frame_rate, bitsery::ext::InPlaceOptional(), [](S& s, auto& v) { s.value4b(v); }); diff --git a/src/plugin/bridges/common.h b/src/plugin/bridges/common.h index 5d45f5d1..ad4b7622 100644 --- a/src/plugin/bridges/common.h +++ b/src/plugin/bridges/common.h @@ -289,9 +289,6 @@ 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"); } diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index fcdc5bb6..de895dbb 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -257,8 +257,7 @@ Editor::Editor(MainContext& main_context, Logger& logger, const size_t parent_window_handle, std::optional> timer_proc) - : use_coordinate_hack_(config.editor_coordinate_hack), - use_force_dnd_(config.editor_force_dnd), + : use_force_dnd_(config.editor_force_dnd), logger_(logger), x11_connection_(xcb_connect(nullptr, nullptr), xcb_disconnect), dnd_proxy_handle_(WineXdndProxy::get_handle()), @@ -401,22 +400,6 @@ void Editor::resize(uint16_t width, uint16_t height) { // 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); - } } void Editor::show() noexcept { diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index 9d6449c0..7b4786f6 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -267,13 +267,6 @@ 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. From eb8bf93f24dddadcc4ec9d3c99c565134bd9f915 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sat, 1 Mar 2025 19:41:15 +0100 Subject: [PATCH 09/26] Resize the Wine window using SetWindowPos This seems to behave more reliably than resizing the window with `xcb_configure_window()`, solving some of the client area issues with Wine 9.21. --- src/wine-host/editor.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index de895dbb..7666e51c 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -391,10 +391,14 @@ void Editor::resize(uint16_t width, uint16_t height) { const std::array 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_NOOWNERZORDER | SWP_DEFERERASE); + // 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. From e18d598c7eeb7da6a7b727f23b810693fd89ab24 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Sun, 2 Mar 2025 22:56:57 +0100 Subject: [PATCH 10/26] Remove function to get X11 screen size This is no longer used. --- src/wine-host/editor.cpp | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 7666e51c..42597911 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -167,11 +167,6 @@ std::optional 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. @@ -1231,27 +1226,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; From 999404330619b9531a0c885304f268dc326d3ab1 Mon Sep 17 00:00:00 2001 From: Asahi Lina Date: Thu, 22 May 2025 11:00:41 +0900 Subject: [PATCH 11/26] Use WM_WINDOW_ROLE instead of WM_STATE to detect the host window Ardour brings up the plugin UI before the host window is visible, and it is missing some properties. Using WM_WINDOW_ROLE instead of WM_STATE makes it work. For reference, these are the properties that are set on the window at the time of plugin GUI instantiation: _NET_WM_ICON(CARDINAL) = Icon (16 x 16): WM_WINDOW_ROLE(STRING) = "plugin_ui" _NET_WM_SYNC_REQUEST_COUNTER(CARDINAL) = 6294918 _NET_WM_WINDOW_TYPE(ATOM) = _NET_WM_WINDOW_TYPE_UTILITY _NET_WM_USER_TIME_WINDOW(WINDOW): window id # 0x600d85 WM_CLIENT_LEADER(WINDOW): window id # 0x600001 _NET_WM_PID(CARDINAL) = 1604801 WM_LOCALE_NAME(STRING) = "en_US.UTF-8" WM_CLIENT_MACHINE(STRING) = "homura" WM_NORMAL_HINTS(WM_SIZE_HINTS): program specified size: 521 by 46 WM_PROTOCOLS(ATOM): protocols WM_DELETE_WINDOW, WM_TAKE_FOCUS, _NET_WM_PING, _NET_WM_SYNC_REQUEST WM_CLASS(STRING) = "ardour-8.12.0", "Ardour-8.12.0" WM_ICON_NAME(STRING) = "Audio 1: Melodyne (by Celemony) [VST3]" _NET_WM_ICON_NAME(UTF8_STRING) = "Audio 1: Melodyne (by Celemony) [VST3]" WM_NAME(STRING) = "Audio 1: Melodyne (by Celemony) [VST3]" _NET_WM_NAME(UTF8_STRING) = "Audio 1: Melodyne (by Celemony) [VST3]" --- src/wine-host/editor.cpp | 20 ++++++++++++++------ src/wine-host/editor.h | 5 +++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 42597911..40c01e9d 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -87,13 +87,18 @@ constexpr uint32_t wrapper_event_mask = 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, although - * we won't filter on whether or not the window is actually visible because it - * may be minimalized when the plugin's GUI is being opened. + * We'll set this property on the Wine window to emulate the behavior of + * a minimal window manager. */ 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 @@ -292,6 +297,9 @@ Editor::Editor(MainContext& main_context, }), xcb_wm_state_property_( 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_, @@ -316,7 +324,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_); @@ -931,7 +939,7 @@ 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_) + xcb_wm_window_role_property_) .value_or(parent_window_); if (new_host_window == host_window_) { return; diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index 7b4786f6..52eaa4f5 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -374,6 +374,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. */ From b2db9cc0a6266bef99787579e88b8602b6e7883b Mon Sep 17 00:00:00 2001 From: Asahi Lina Date: Thu, 22 May 2025 11:38:22 +0900 Subject: [PATCH 12/26] VST3: Add HiDPI scaling hack Sometimes the plugin size might be off-by-one due to HiDPI scaling. If so, pretend it's actually the size that was requested, to avoid ending up in an infinite loop. --- src/wine-host/bridges/vst3.cpp | 20 ++++++++++++++++++-- src/wine-host/bridges/vst3.h | 8 ++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/wine-host/bridges/vst3.cpp b/src/wine-host/bridges/vst3.cpp index 777a76a1..35d59cce 100644 --- a/src/wine-host/bridges/vst3.cpp +++ b/src/wine-host/bridges/vst3.cpp @@ -899,8 +899,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 +948,8 @@ void Vst3Bridge::run() { request.new_size.getHeight()); } + instance.last_set_size = request.new_size; + return result; }); }, diff --git a/src/wine-host/bridges/vst3.h b/src/wine-host/bridges/vst3.h index 1b1ef271..75054893 100644 --- a/src/wine-host/bridges/vst3.h +++ b/src/wine-host/bridges/vst3.h @@ -256,6 +256,14 @@ struct Vst3PluginInstance { * processing. */ std::optional 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; }; /** From 6f65613292821c90855a88474fc19a0009888e5a Mon Sep 17 00:00:00 2001 From: Asahi Lina Date: Thu, 22 May 2025 14:05:14 +0900 Subject: [PATCH 13/26] VST3: Create the window with the plugin-returned initial size Instead of creating the window as 128x128 and then resizing, create it with the size requested by the plugin. Fixes Korg VST3 plugins. --- src/wine-host/bridges/vst3.cpp | 11 +++++++++-- src/wine-host/editor.cpp | 5 +++-- src/wine-host/editor.h | 3 ++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/wine-host/bridges/vst3.cpp b/src/wine-host/bridges/vst3.cpp index 35d59cce..eb004526 100644 --- a/src/wine-host/bridges/vst3.cpp +++ b/src/wine-host/bridges/vst3.cpp @@ -799,9 +799,16 @@ void Vst3Bridge::run() { // be done in the main UI thread return main_context_ .run_in_context([&, &instance = instance]() -> tresult { + Steinberg::ViewRect size; + std::optional initial_size; + if (instance.plug_view_instance->plug_view->getSize( + &size) == Steinberg::kResultOk) { + initial_size.emplace(size.getWidth(), + size.getHeight()); + } Editor& editor_instance = instance.editor.emplace( - main_context_, config_, generic_logger_, - x11_handle); + main_context_, config_, generic_logger_, x11_handle, + std::nullopt, initial_size); const tresult result = instance.plug_view_instance->plug_view->attached( editor_instance.win32_handle(), type.c_str()); diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 40c01e9d..11316c34 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -256,12 +256,13 @@ Editor::Editor(MainContext& main_context, const Configuration& config, Logger& logger, const size_t parent_window_handle, - std::optional> timer_proc) + std::optional> timer_proc, + std::optional 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()), - wrapper_window_size_({128, 128}), + wrapper_window_size_(initial_size.value_or(Size(128, 128))), host_window_config_({}), parent_window_config_({}), // Create a window without any decoratiosn for easy embedding. The diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index 52eaa4f5..a23dfd4b 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -194,7 +194,8 @@ class Editor { const Configuration& config, Logger& logger, const size_t parent_window_handle, - std::optional> timer_proc = std::nullopt); + std::optional> timer_proc = std::nullopt, + std::optional initial_size = std::nullopt); /** * Resize the `wrapper_window_` to this new size. We need to manually call From dd36bb3d3b32c9d267857c7ed9ea893fbafa8a24 Mon Sep 17 00:00:00 2001 From: Asahi Lina Date: Fri, 23 May 2025 13:06:35 +0900 Subject: [PATCH 14/26] Resize the Wine window as well as the wrapper window This fixes issues with various plugins such as PG-8X and SCVA. --- src/wine-host/editor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 11316c34..3768943f 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -395,6 +395,8 @@ void Editor::resize(uint16_t width, uint16_t height) { const std::array 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 From 2e1dcb2316427741339bcb602fd10388faf07f0b Mon Sep 17 00:00:00 2001 From: Asahi Lina Date: Fri, 23 May 2025 13:53:57 +0900 Subject: [PATCH 15/26] Handle absolute ConfigureNotify events for parent window For VST2 in Ardour, it seems Ardour is doing its own WM/wrapper window thing too. In this case, the plugin gets absolute ConfigureNotify events that are already in the root coordinate space, so just use those coordinates and ignore the host window. Also fix the dimensions, which should always be those of the parent window, not the host window. If the parent is the host window, still track its geometry separately, and just don't add in the host window dimensions in this case either. --- src/wine-host/editor.cpp | 43 ++++++++++++++++++++++++++++++---------- src/wine-host/editor.h | 1 + 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 3768943f..80b2a6fb 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -265,6 +265,7 @@ Editor::Editor(MainContext& main_context, 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 @@ -497,7 +498,12 @@ void Editor::handle_x11_events() noexcept { 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 the host window is different from the parent window @@ -507,12 +513,17 @@ void Editor::handle_x11_events() noexcept { // 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 (event->window == host_window_ && is_synthetic_event) { host_window_config_ = *event; } - if (event->window == parent_window_ && - host_window_ != parent_window_ && !is_synthetic_event) { + if (event->window == parent_window_) { parent_window_config_ = *event; + parent_window_config_abs_ = is_synthetic_event; } // Window managers are expected to send ConfigureNotify to @@ -528,13 +539,25 @@ void Editor::handle_x11_events() noexcept { translated_event.response_type = XCB_CONFIGURE_NOTIFY; translated_event.event = wine_window_; translated_event.window = wine_window_; - translated_event.width = event->width; - translated_event.height = event->height; - translated_event.x = - host_window_config_.x + parent_window_config_.x; - translated_event.y = - host_window_config_.y + parent_window_config_.y; - + 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; + } + 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 | diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index a23dfd4b..7405f65d 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -343,6 +343,7 @@ class Editor { */ 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 From 17a95fdf992cbb8be0194c32a15b0a6d41093635 Mon Sep 17 00:00:00 2001 From: Asahi Lina Date: Fri, 23 May 2025 14:14:18 +0900 Subject: [PATCH 16/26] Ignore relative ConfigureNotify events after absolute ones Once we get a single absolute ConfigureNotify event, we assume they will keep coming and ignore any relative ones. The relative computation only works if the parent window is a direct descendant of the host window, which may not be the case. To fully fix this in the general case (only relative ConfigureNotify events) we would have to walk the window hierarchy and add up all the offsets until the host window, but so far the only known case of an extra level (Ardour VST2) also sends absolute ConfigureNotify events, so we can just use those. --- src/wine-host/editor.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 80b2a6fb..c38ccc50 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -518,12 +518,24 @@ void Editor::handle_x11_events() noexcept { // 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_) { - parent_window_config_ = *event; - parent_window_config_abs_ = is_synthetic_event; + 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; + } } // Window managers are expected to send ConfigureNotify to From 42e0794d7cebe516f320cc9c44b0a5d8ebe71971 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 1 Sep 2025 23:38:13 +0200 Subject: [PATCH 17/26] Allow triggering CI builds by hand GitHub removes artifacts after four months, so there may be situations where this is useful. --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5ebb939..8b13e17c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,7 @@ on: pull_request: branches: - master + workflow_dispatch: defaults: run: From cca31898558142e21dfac4881f95016f674183ce Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Mon, 17 Nov 2025 21:35:06 +0100 Subject: [PATCH 18/26] Fix pkgconfig deprecation warning in cross file --- cross-wine.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cross-wine.conf b/cross-wine.conf index f2c0d4de..3e3301ac 100644 --- a/cross-wine.conf +++ b/cross-wine.conf @@ -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' From 44d047e9e2e1e6c6fe0a12008523649d0b7d037e Mon Sep 17 00:00:00 2001 From: Jakob Steltner Date: Sat, 3 Jan 2026 23:06:36 +0100 Subject: [PATCH 19/26] Work around wrong offset in plugin windows --- src/wine-host/editor.cpp | 27 ++++++++++++++++++++++++++- src/wine-host/editor.h | 5 +++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index c38ccc50..f2f13cd6 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -417,6 +417,27 @@ void Editor::show() noexcept { ShowWindow(win32_window_.handle_, SW_SHOWNORMAL); } +std::array 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 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 @@ -454,7 +475,6 @@ void Editor::handle_x11_events() noexcept { ", generated from " + std::to_string(event->event); }); - redetect_host_window(); // If the `editor_force_dnd` option is set, we'll strip @@ -560,6 +580,11 @@ void Editor::handle_x11_events() noexcept { translated_event.x += host_window_config_.x; translated_event.y += host_window_config_.y; } + if (!is_synthetic_event) { + const std::array 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) + diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index 7405f65d..cfeb950f 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -311,6 +311,11 @@ class Editor { */ void redetect_host_window() noexcept; + /** + * Get offset of parent window to fix mouse coordinates. + */ + std::array get_parent_window_offset(); + /** * Reparent `child` to `new_parent`. This includes the flush. */ From 174b25be0541c514ea32484df7325f7725b7f751 Mon Sep 17 00:00:00 2001 From: Jakob Steltner Date: Sat, 3 Jan 2026 23:19:08 +0100 Subject: [PATCH 20/26] Fix some whitespace --- src/wine-host/editor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index f2f13cd6..8fef7412 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -437,7 +437,6 @@ std::array Editor::get_parent_window_offset() { 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 @@ -475,7 +474,8 @@ void Editor::handle_x11_events() noexcept { ", generated from " + std::to_string(event->event); }); - redetect_host_window(); + + redetect_host_window(); // If the `editor_force_dnd` option is set, we'll strip // `XdndAware` from all of `wine_window_`'s ancestors From a2be8fcf17c829a330693bce96c3318f701f596c Mon Sep 17 00:00:00 2001 From: Jakob Steltner Date: Sat, 3 Jan 2026 23:21:14 +0100 Subject: [PATCH 21/26] Fix some whitespace --- src/wine-host/editor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 8fef7412..5d33f080 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -475,7 +475,7 @@ void Editor::handle_x11_events() noexcept { std::to_string(event->event); }); - redetect_host_window(); + redetect_host_window(); // If the `editor_force_dnd` option is set, we'll strip // `XdndAware` from all of `wine_window_`'s ancestors From 945528cd7f898d717d772b93f939343dad122d91 Mon Sep 17 00:00:00 2001 From: Juuso Kaitila Date: Sun, 11 Jan 2026 17:52:04 +0100 Subject: [PATCH 22/26] Fix cursor offset after moving the plugin window --- src/wine-host/editor.cpp | 41 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 5d33f080..53e01c00 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -159,6 +159,20 @@ std::optional 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 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. @@ -1003,7 +1017,10 @@ void Editor::redetect_host_window() noexcept { const xcb_window_t new_host_window = find_host_window(*x11_connection_, parent_window_, xcb_wm_window_role_property_) - .value_or(parent_window_); + .value_or(find_host_window_from_query_tree(*x11_connection_, + parent_window_) + .value_or(parent_window_)); + if (new_host_window == host_window_) { return; } @@ -1256,6 +1273,28 @@ std::optional find_host_window( return std::nullopt; } +std::optional 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 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) From 604375b7567f12f6137fd5ac34863f36be3e2193 Mon Sep 17 00:00:00 2001 From: Juuso Kaitila Date: Fri, 16 Jan 2026 20:21:50 +0200 Subject: [PATCH 23/26] 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. --- src/wine-host/bridges/vst3.cpp | 31 ++++++++++++++++++++++++++++++- src/wine-host/editor.cpp | 28 ++++++++++++++++++++++++++++ src/wine-host/editor.h | 9 +++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/wine-host/bridges/vst3.cpp b/src/wine-host/bridges/vst3.cpp index eb004526..d2eec2f5 100644 --- a/src/wine-host/bridges/vst3.cpp +++ b/src/wine-host/bridges/vst3.cpp @@ -806,9 +806,38 @@ void Vst3Bridge::run() { 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, - std::nullopt, initial_size); + std::move(resize_watchdog), initial_size); const tresult result = instance.plug_view_instance->plug_view->attached( editor_instance.win32_handle(), type.c_str()); diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index 53e01c00..b7da3465 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -427,6 +427,34 @@ void Editor::resize(uint16_t width, uint16_t height) { wrapper_window_size_.height = height; } +std::optional 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 geom( + xcb_get_geometry_reply(x11_connection_.get(), cookie, &error)); + + 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); } diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index cfeb950f..398d5d32 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -204,6 +204,15 @@ 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 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, From 8ec0f8b8978a8f6ad4f24150e66b500a49b6312b Mon Sep 17 00:00:00 2001 From: Juuso Kaitila Date: Fri, 16 Jan 2026 19:28:39 +0200 Subject: [PATCH 24/26] VST3: Fix making plugin windows larger not working in Carla Fixes making VST3 plugins larger than their original size in Carla. The plugin would resize but not get drawn past the original window size. --- .../bridges/vst3-impls/plug-frame-proxy.cpp | 14 +++++++++++-- src/wine-host/bridges/vst3.cpp | 20 +++++++++++++++++++ src/wine-host/bridges/vst3.h | 8 ++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/wine-host/bridges/vst3-impls/plug-frame-proxy.cpp b/src/wine-host/bridges/vst3-impls/plug-frame-proxy.cpp index f7026d25..f1e2b933 100644 --- a/src/wine-host/bridges/vst3-impls/plug-frame-proxy.cpp +++ b/src/wine-host/bridges/vst3-impls/plug-frame-proxy.cpp @@ -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()'" diff --git a/src/wine-host/bridges/vst3.cpp b/src/wine-host/bridges/vst3.cpp index d2eec2f5..b48548e8 100644 --- a/src/wine-host/bridges/vst3.cpp +++ b/src/wine-host/bridges/vst3.cpp @@ -1388,6 +1388,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()); diff --git a/src/wine-host/bridges/vst3.h b/src/wine-host/bridges/vst3.h index 75054893..3bef4c36 100644 --- a/src/wine-host/bridges/vst3.h +++ b/src/wine-host/bridges/vst3.h @@ -318,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 From f504da9e46aca0b04df21787c9c00d6fab9df555 Mon Sep 17 00:00:00 2001 From: Juuso Kaitila Date: Tue, 20 Jan 2026 18:18:36 +0200 Subject: [PATCH 25/26] VST3: Fix reparenting error when opening certain plugins Fixes an issue where reparenting fails when the initial size returned by the plugin is 0x0, i.e., invalid. --- src/wine-host/bridges/vst3.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/wine-host/bridges/vst3.cpp b/src/wine-host/bridges/vst3.cpp index b48548e8..43c3cf5d 100644 --- a/src/wine-host/bridges/vst3.cpp +++ b/src/wine-host/bridges/vst3.cpp @@ -801,8 +801,13 @@ void Vst3Bridge::run() { .run_in_context([&, &instance = instance]() -> tresult { Steinberg::ViewRect size; std::optional 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) == Steinberg::kResultOk && + size.getWidth() > 0 && size.getHeight() > 0) { initial_size.emplace(size.getWidth(), size.getHeight()); } From ba7022df0aee1e91cde62d7f0e940d3bc43a82b0 Mon Sep 17 00:00:00 2001 From: Juuso Kaitila Date: Wed, 21 Jan 2026 16:15:44 +0200 Subject: [PATCH 26/26] Fix some plugins not resizing with their expansion toggles DMG Audio Compassion has a expand/contract toggle button that adds a new section below the main GUI. Without SWP_NOMOVE it would most of the time fail to expand and the parent_window_ would sometimes fail to resize accordingly if the expansion/contraction succeeded resulting in the GUI locking up. SWP_NOOWNERZORDER is the same as SWP_NOREPOSITION so having them both was redundant. --- src/wine-host/editor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index b7da3465..f5555141 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -418,7 +418,7 @@ void Editor::resize(uint16_t width, uint16_t height) { // `handle_x11_events()` SetWindowPos( win32_window_.handle_, nullptr, 0, 0, width, height, - SWP_NOREPOSITION | SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_DEFERERASE); + 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