Files
test/intern/ghost/intern/GHOST_WindowWayland.cc
Campbell Barton ebfa7edeb1 Cleanup: use snake case, replace "m_" prefix with "_" suffix
Follow our own C++ conventions for GHOST.
2025-08-16 16:14:18 +10:00

2888 lines
96 KiB
C++

/* SPDX-FileCopyrightText: 2020-2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup GHOST
*/
#include "GHOST_WindowWayland.hh"
#include "GHOST_SystemWayland.hh"
#include "GHOST_WaylandUtils.hh"
#include "GHOST_WindowManager.hh"
#include "GHOST_utildefines.hh"
#include "GHOST_Event.hh"
#include "GHOST_ContextNone.hh"
#ifdef WITH_OPENGL_BACKEND
# include "GHOST_ContextEGL.hh"
#endif
#ifdef WITH_VULKAN_BACKEND
# include "GHOST_ContextVK.hh"
#endif
#include <wayland-client-protocol.h>
#ifdef WITH_OPENGL_BACKEND
# ifdef WITH_GHOST_WAYLAND_DYNLOAD
# include <wayland_dynload_egl.h>
# endif
# include <wayland-egl.h>
#endif
#include <algorithm> /* For `std::find`. */
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
# ifdef WITH_GHOST_WAYLAND_DYNLOAD
# include <wayland_dynload_libdecor.h>
# endif
# include <libdecor.h>
#endif
/* Generated by `wayland-scanner`. */
#include <fractional-scale-v1-client-protocol.h>
#include <viewporter-client-protocol.h>
#include <xdg-activation-v1-client-protocol.h>
#include <xdg-decoration-unstable-v1-client-protocol.h>
#include <xdg-shell-client-protocol.h>
#include <atomic>
#include <optional>
#include <cstring> /* For `memcpy`. */
#include <malloc.h> /* For `malloc_usable_size`. */
/* Logging, use `ghost.wl.*` prefix. */
#include "CLG_log.h"
/* Disable, as this can "lock" the GUI even with the *pending* version of dispatch is used. */
#if 0
/**
* Note that for almost all cases a call to `wl_display_dispatch_pending` is *not* needed.
* Without the dispatch though, calls to set the cursor while the event loop is
* not being processed causes a resource allocation failure - disconnecting the
* WAYLAND compositor (effectively crashing).
* While this only happens when many calls are made, that's exactly what happens in the
* case of the "progress" feature which uses the cursor to display a number.
* See: #141846.
*
* Observed behavior when changing cursors without a dispatch:
* - Eventually exits with an error on all compositors tested (KDE/GNOME/WLROOTS based).
* - Won't re-display at all (on KDE 6.4).
* Note that this could be a bug in KDE as it works in GNOME & WLROOTS based compositors.
*/
# define USE_CURSOR_IMMEDIATE_DISPATCH
#endif
/**
* LIBDECOR support committing a window-configuration in the main-thread that was
* handled in a non-main-thread.
*
* This is needed for proper order of operations as the commit needs to occur after the buffer
* is resized (something that can't be done from a non-main thread).
* Without this, the window-frame can get out of sync with its contents and resizing is slow.
*
* Eventually LIBDECOR should support copying & freeing a configuration, even when it does,
* older versions of the library will to be supported and the workaround will need to be kept.
* See: https://gitlab.freedesktop.org/libdecor/libdecor/-/issues/64
*/
#ifdef USE_EVENT_BACKGROUND_THREAD
# ifdef WITH_GHOST_WAYLAND_LIBDECOR
# ifdef HAVE_MALLOC_USABLE_SIZE
# define USE_LIBDECOR_CONFIG_COPY_WORKAROUND
# endif
# endif
#endif
/**
* Queue configuration calls until a valid window frame size is available.
* This is needed because LIBDECOR has an issue where windows initialized with a zero
* size don't have a usable title bar. see #117583.
*/
#ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
# define USE_LIBDECOR_CONFIG_COPY_QUEUE
#endif
#ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
static libdecor_configuration *ghost_wl_libdecor_configuration_copy(
const libdecor_configuration *configuration);
static void ghost_wl_libdecor_configuration_free(libdecor_configuration *configuration);
#endif
static const xdg_activation_token_v1_listener *xdg_activation_listener_get();
static constexpr size_t base_dpi = 96;
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
/* Access `use_libdecor` in #GHOST_SystemWayland. */
# define use_libdecor GHOST_SystemWayland::use_libdecor_runtime()
#endif
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
struct GWL_LibDecor_Window {
libdecor_frame *frame = nullptr;
/**
* Defer calling #libdecor_frame_commit.
* \note Accessing the members must lock on `win->frame_pending_mutex`.
*/
struct {
/** When set, ACK configure is expected. */
bool ack_configure = false;
/** The new size to use. */
int size[2] = {0, 0};
libdecor_configuration *configuration = nullptr;
# ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
bool configuration_needs_free = false;
# endif
# ifdef USE_LIBDECOR_CONFIG_COPY_QUEUE
/**
* Queue configurations which cannot be applied because the window size isn't know.
* All items in this list are allocated.
*/
std::vector<libdecor_configuration *> configuration_queue;
# endif
} pending;
/** The window has been configured (see #xdg_surface_ack_configure). */
bool initial_configure_seen = false;
std::optional<GHOST_TWindowState> initial_configure_state = std::nullopt;
};
static void gwl_libdecor_window_destroy(GWL_LibDecor_Window *decor)
{
# ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
if (decor->pending.configuration_needs_free) {
ghost_wl_libdecor_configuration_free(decor->pending.configuration);
}
# endif /* USE_LIBDECOR_CONFIG_COPY_WORKAROUND */
# ifdef USE_LIBDECOR_CONFIG_COPY_QUEUE
for (libdecor_configuration *configuration : decor->pending.configuration_queue) {
ghost_wl_libdecor_configuration_free(configuration);
}
decor->pending.configuration_queue.clear();
# endif /* USE_LIBDECOR_CONFIG_COPY_QUEUE */
libdecor_frame_unref(decor->frame);
delete decor;
}
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */
struct GWL_XDG_Decor_Window {
xdg_surface *surface = nullptr;
zxdg_toplevel_decoration_v1 *toplevel_decor = nullptr;
xdg_toplevel *toplevel = nullptr;
enum zxdg_toplevel_decoration_v1_mode mode = (enum zxdg_toplevel_decoration_v1_mode)0;
/**
* Defer calling #xdg_surface_ack_configure.
* \note Accessing the members must lock on `win->frame_pending_mutex`.
*/
struct {
/** When set, ACK configure is expected. */
bool ack_configure = false;
/** The serial to pass to ACK configure. */
uint32_t ack_configure_serial = 0;
} pending;
/** The window has been configured (see #xdg_surface_ack_configure). */
bool initial_configure_seen = false;
/** The maximum bounds on startup, monitor size minus docs for example. */
int initial_bounds[2] = {0, 0};
};
static void gwl_xdg_decor_window_destroy(GWL_XDG_Decor_Window *decor)
{
if (decor->toplevel_decor) {
zxdg_toplevel_decoration_v1_destroy(decor->toplevel_decor);
}
xdg_toplevel_destroy(decor->toplevel);
xdg_surface_destroy(decor->surface);
delete decor;
}
/* -------------------------------------------------------------------- */
/** \name Rounding Utilities
* \{ */
static void gwl_round_int_by(int *value_p, const int round_value)
{
GHOST_ASSERT(round_value > 0, "Invalid rounding value!");
*value_p = (*value_p / round_value) * round_value;
}
static void gwl_round_int2_by(int value_p[2], const int round_value)
{
GHOST_ASSERT(round_value > 0, "Invalid rounding value!");
value_p[0] = (value_p[0] / round_value) * round_value;
value_p[1] = (value_p[1] / round_value) * round_value;
}
/**
* Return true if the value is already rounded by `round_value`.
*/
static bool gwl_round_int_test(const int value, const int round_value)
{
return value == ((value / round_value) * round_value);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Window-Viewport/Wayland to/from Scale Conversion
* \{ */
struct GWL_WindowScaleParams {
bool is_fractional = false;
/**
* When fractional:
* Scale is multiplied by #FRACTIONAL_DENOMINATOR.
* Otherwise scale is an integer.
*/
wl_fixed_t scale = 0;
};
wl_fixed_t gwl_window_scale_wl_fixed_to(const GWL_WindowScaleParams &scale_params,
wl_fixed_t value)
{
if (scale_params.is_fractional) {
return (value * scale_params.scale) / FRACTIONAL_DENOMINATOR;
}
return value * scale_params.scale;
}
wl_fixed_t gwl_window_scale_wl_fixed_from(const GWL_WindowScaleParams &scale_params,
wl_fixed_t value)
{
if (scale_params.is_fractional) {
return (value * FRACTIONAL_DENOMINATOR) / scale_params.scale;
}
return value / scale_params.scale;
}
int gwl_window_scale_int_to(const GWL_WindowScaleParams &scale_params, const int value)
{
return wl_fixed_to_int(gwl_window_scale_wl_fixed_to(scale_params, wl_fixed_from_int(value)));
}
int gwl_window_scale_int_from(const GWL_WindowScaleParams &scale_params, const int value)
{
return wl_fixed_to_int(gwl_window_scale_wl_fixed_from(scale_params, wl_fixed_from_int(value)));
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal #GWL_WindowCursorCustomShape
* \{ */
static void gwl_window_cursor_custom_free(GHOST_CursorGenerator *cg)
{
cg->free_fn(cg);
}
static void gwl_window_cursor_custom_clear(GHOST_CursorGenerator **cg)
{
if (*cg == nullptr) {
return;
}
gwl_window_cursor_custom_free(*cg);
*cg = nullptr;
}
static GHOST_TSuccess gwl_window_cursor_custom_load(const GHOST_CursorGenerator &cg,
GHOST_SystemWayland *system)
{
return system->cursor_shape_custom_set(cg);
}
static GHOST_TSuccess gwl_window_cursor_shape_refresh(GHOST_TStandardCursor shape,
const GHOST_CursorGenerator *cg,
GHOST_SystemWayland *system)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
GHOST_ASSERT(system->main_thread_id == std::this_thread::get_id(), "Only from main thread!");
#endif
if (shape == GHOST_kStandardCursorCustom) {
const GHOST_TSuccess ok = gwl_window_cursor_custom_load(*cg, system);
if (ok == GHOST_kSuccess) {
return ok;
}
shape = GHOST_kStandardCursorDefault;
system->cursor_shape_set(shape);
return GHOST_kFailure;
}
return system->cursor_shape_set(shape);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Window
* \{ */
#ifdef USE_EVENT_BACKGROUND_THREAD
enum eGWL_PendingWindowActions {
/**
* The state of the window frame has changed, apply the state from #GWL_Window::frame_pending.
*/
PENDING_WINDOW_FRAME_CONFIGURE = 0,
/**
* The DPI for a monitor has changed or the monitors (outputs)
* this window is visible on may have changed. Recalculate the windows scale.
*/
PENDING_OUTPUT_SCALE_UPDATE,
/**
* Workaround for a bug/glitch in WLROOTS based compositors (RIVER for example).
* Deferring the scale update one even-loop cycle resolves a bug
* where the output enter/exit events cause the surface buffer being an invalid size.
*
* While these kinds of glitches might be ignored in some cases,
* this caused newly created windows to immediately loose the connection to WAYLAND
* (crashing from a user perspective).
*/
PENDING_OUTPUT_SCALE_UPDATE_DEFERRED,
/**
* The surface needs a commit to run.
* Use this to avoid committing immediately which can cause flickering when other operations
* have not yet been performed - such as refreshing the window size.
*/
PENDING_WINDOW_SURFACE_COMMIT,
/**
* The window has gained focus and the cursor shape needs to be refreshed.
*/
PENDING_WINDOW_CURSOR_SHAPE_REFRESH,
};
# define PENDING_NUM (PENDING_WINDOW_CURSOR_SHAPE_REFRESH + 1)
#endif /* USE_EVENT_BACKGROUND_THREAD */
struct GWL_WindowFrame {
/**
* The frame size (in GHOST window coordinates).
*
* These must be converted to WAYLAND relative coordinates when the window is scaled
* by Hi-DPI/fractional scaling.
*/
int32_t size[2] = {0, 0};
bool is_maximised = false;
bool is_fullscreen = false;
bool is_active = false;
/** Disable when the fractional scale is a whole number. */
int fractional_scale = 0;
/**
* Store the value of #wp_fractional_scale_v1_listener::preferred_scale
* before it's applied.
*/
int fractional_scale_preferred = 0;
/** The scale passed to #wl_surface_set_buffer_scale. */
int buffer_scale = 0;
/** Scale has been set (for the first time). */
bool is_scale_init = false;
};
struct GWL_Window {
/** Wayland core types. */
struct {
wl_surface *surface = nullptr;
} wl;
/** Wayland native types. */
struct {
wp_viewport *viewport = nullptr;
/**
* When set, only respond to the #wp_fractional_scale_v1_listener::preferred_scale callback
* and ignore updated scale based on #wl_surface_listener::enter & exit events.
*/
wp_fractional_scale_v1 *fractional_scale_handle = nullptr;
} wp;
/** XDG native types. */
struct {
/** A temporary token used for the window to be notified of it's activation. */
xdg_activation_token_v1 *activation_token = nullptr;
} xdg;
struct {
#ifdef WITH_OPENGL_BACKEND
wl_egl_window *egl_window = nullptr;
#endif
#ifdef WITH_VULKAN_BACKEND
GHOST_ContextVK_WindowInfo *vulkan_window_info = nullptr;
#endif
} backend;
GHOST_WindowWayland *ghost_window = nullptr;
GHOST_SystemWayland *ghost_system = nullptr;
GHOST_TDrawingContextType ghost_context_type = GHOST_kDrawingContextTypeNone;
/**
* Outputs on which the window is currently shown on.
*
* This is an ordered set (whoever adds to this is responsible for keeping members unique).
* In practice this is rarely manipulated and is limited by the number of physical displays.
*/
std::vector<GWL_Output *> outputs;
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
GWL_LibDecor_Window *libdecor = nullptr;
#endif
GWL_XDG_Decor_Window *xdg_decor = nullptr;
/**
* The current value of frame, copied from `frame_pending` when applying updates.
* This avoids the need for locking when reading from `frame`.
*/
GWL_WindowFrame frame;
GWL_WindowFrame frame_pending;
#ifdef USE_EVENT_BACKGROUND_THREAD
/**
* Needed so calls such as #GHOST_Window::setClientSize
* doesn't conflict with WAYLAND callbacks that may run in a thread.
*/
std::mutex frame_pending_mutex;
#endif
GHOST_CursorGenerator *cursor_generator = nullptr;
std::string title;
bool is_dialog = false;
/** True once the window has been initialized. */
bool is_init = false;
/** True when the GPU context is valid. */
bool is_valid_setup = false;
/** Currently only initialized on access (avoids allocations & allows to keep private). */
GWL_WindowScaleParams scale_params;
#ifdef USE_EVENT_BACKGROUND_THREAD
/**
* These pending actions can't be performed when WAYLAND handlers are running from a thread.
* Postpone their execution until the main thread can handle them.
*/
std::atomic<bool> pending_actions[PENDING_NUM] = {false};
#endif /* USE_EVENT_BACKGROUND_THREAD */
};
static void gwl_window_resize_for_backend(GWL_Window *win, const int32_t size[2])
{
#ifdef WITH_OPENGL_BACKEND
if (win->ghost_context_type == GHOST_kDrawingContextTypeOpenGL) {
/* Null on window initialization. */
if (win->backend.egl_window) {
wl_egl_window_resize(win->backend.egl_window, UNPACK2(size), 0, 0);
}
}
#endif
#ifdef WITH_VULKAN_BACKEND
if (win->ghost_context_type == GHOST_kDrawingContextTypeVulkan) {
win->backend.vulkan_window_info->size[0] = size[0];
win->backend.vulkan_window_info->size[1] = size[1];
}
#endif
}
static void gwl_window_title_set(GWL_Window *win, const char *title)
{
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (use_libdecor) {
GWL_LibDecor_Window &decor = *win->libdecor;
libdecor_frame_set_title(decor.frame, title);
}
else
#endif
{
GWL_XDG_Decor_Window &decor = *win->xdg_decor;
xdg_toplevel_set_title(decor.toplevel, title);
}
win->title = title;
}
static GHOST_TWindowState gwl_window_state_get(const GWL_Window *win)
{
if (win->frame.is_fullscreen) {
return GHOST_kWindowStateFullScreen;
}
if (win->frame.is_maximised) {
return GHOST_kWindowStateMaximized;
}
return GHOST_kWindowStateNormal;
}
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
/**
* \note Keep in sync with #gwl_window_state_set_for_xdg.
*/
static bool gwl_window_state_set_for_libdecor(libdecor_frame *frame,
const GHOST_TWindowState state,
const GHOST_TWindowState state_current)
{
switch (state) {
case GHOST_kWindowStateNormal:
/* Unset states. */
switch (state_current) {
case GHOST_kWindowStateMaximized: {
libdecor_frame_unset_maximized(frame);
break;
}
case GHOST_kWindowStateFullScreen: {
libdecor_frame_unset_fullscreen(frame);
break;
}
default: {
break;
}
}
break;
case GHOST_kWindowStateMaximized: {
libdecor_frame_set_maximized(frame);
break;
}
case GHOST_kWindowStateMinimized: {
libdecor_frame_set_minimized(frame);
break;
}
case GHOST_kWindowStateFullScreen: {
libdecor_frame_set_fullscreen(frame, nullptr);
break;
}
}
return true;
}
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */
/**
* \note Keep in sync with #gwl_window_state_set_for_libdecor.
*/
static bool gwl_window_state_set_for_xdg(xdg_toplevel *toplevel,
const GHOST_TWindowState state,
const GHOST_TWindowState state_current)
{
switch (state) {
case GHOST_kWindowStateNormal:
/* Unset states. */
switch (state_current) {
case GHOST_kWindowStateMaximized: {
xdg_toplevel_unset_maximized(toplevel);
break;
}
case GHOST_kWindowStateFullScreen: {
xdg_toplevel_unset_fullscreen(toplevel);
break;
}
default: {
break;
}
}
break;
case GHOST_kWindowStateMaximized: {
xdg_toplevel_set_maximized(toplevel);
break;
}
case GHOST_kWindowStateMinimized: {
xdg_toplevel_set_minimized(toplevel);
break;
}
case GHOST_kWindowStateFullScreen: {
xdg_toplevel_set_fullscreen(toplevel, nullptr);
break;
}
}
return true;
}
static bool gwl_window_state_set(GWL_Window *win, const GHOST_TWindowState state)
{
const GHOST_TWindowState state_current = gwl_window_state_get(win);
bool result;
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (use_libdecor) {
result = gwl_window_state_set_for_libdecor(win->libdecor->frame, state, state_current);
}
else
#endif
{
result = gwl_window_state_set_for_xdg(win->xdg_decor->toplevel, state, state_current);
}
return result;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Window Viewport Setup/Tear-down
*
* A viewport is needed to implement fractional scale,
* as the outputs scale may change at runtime, support creating & clearing the viewport as needed.
* \{ */
/**
* Scale a value from a viewport value to Wayland windowing.
* Scale down or not at all.
*/
static int gwl_window_fractional_to_viewport(const GWL_WindowFrame &frame, const int value)
{
GHOST_ASSERT(frame.fractional_scale != 0, "Not fractional or called before initialized!");
return (value * frame.fractional_scale) / FRACTIONAL_DENOMINATOR;
}
/**
* Scale a value from a Wayland windowing value to the viewport.
* Scales up or not at all.
*/
static int gwl_window_fractional_from_viewport(const GWL_WindowFrame &frame, const int value)
{
GHOST_ASSERT(frame.fractional_scale != 0, "Not fractional or called before initialized!");
return (value * FRACTIONAL_DENOMINATOR) / frame.fractional_scale;
}
/* NOTE: rounded versions are needed for window-frame dimensions conversions.
* (rounding is part of the WAYLAND spec). All other conversions such as cursor coordinates
* can used simple integer division as rounding is not defined in this case. */
static int gwl_window_fractional_to_viewport_round(const GWL_WindowFrame &frame, const int value)
{
GHOST_ASSERT(frame.fractional_scale != 0, "Not fractional or called before initialized!");
return lroundf(double(value * frame.fractional_scale) / double(FRACTIONAL_DENOMINATOR));
}
static int gwl_window_fractional_from_viewport_round(const GWL_WindowFrame &frame, const int value)
{
GHOST_ASSERT(frame.fractional_scale != 0, "Not fractional or called before initialized!");
return lroundf(double(value * FRACTIONAL_DENOMINATOR) / double(frame.fractional_scale));
}
static bool gwl_window_viewport_set(GWL_Window *win,
bool *r_surface_needs_commit,
bool *r_surface_needs_buffer_scale)
{
if (win->wp.viewport != nullptr) {
return false;
}
wp_viewporter *viewporter = win->ghost_system->wp_viewporter_get();
if (viewporter == nullptr) {
return false;
}
win->wp.viewport = wp_viewporter_get_viewport(viewporter, win->wl.surface);
if (win->wp.viewport == nullptr) {
return false;
}
/* Set the buffer scale to 1 since a viewport will be used. */
if (win->frame.buffer_scale != 1) {
win->frame.buffer_scale = 1;
if (r_surface_needs_buffer_scale) {
*r_surface_needs_buffer_scale = true;
}
else {
wl_surface_set_buffer_scale(win->wl.surface, win->frame.buffer_scale);
}
if (r_surface_needs_commit) {
*r_surface_needs_commit = true;
}
else {
wl_surface_commit(win->wl.surface);
}
}
return true;
}
static bool gwl_window_viewport_unset(GWL_Window *win,
bool *r_surface_needs_commit,
bool *r_surface_needs_buffer_scale)
{
if (win->wp.viewport == nullptr) {
return false;
}
wp_viewport_destroy(win->wp.viewport);
win->wp.viewport = nullptr;
GHOST_ASSERT(win->frame.buffer_scale == 1, "Unexpected scale!");
if (win->frame_pending.buffer_scale != win->frame.buffer_scale) {
win->frame.buffer_scale = win->frame_pending.buffer_scale;
if (r_surface_needs_buffer_scale) {
*r_surface_needs_buffer_scale = true;
}
else {
wl_surface_set_buffer_scale(win->wl.surface, win->frame.buffer_scale);
}
if (r_surface_needs_commit) {
*r_surface_needs_commit = true;
}
else {
wl_surface_commit(win->wl.surface);
}
}
return true;
}
static bool gwl_window_viewport_size_update(GWL_Window *win)
{
if (win->wp.viewport == nullptr) {
return false;
}
/* Setting `wp_viewport_set_source` isn't necessary as an unset value is ensured on creation
* and documented to use the entire buffer, further this can crash with NVIDIA, see: #117531. */
wp_viewport_set_destination(
win->wp.viewport,
gwl_window_fractional_from_viewport_round(win->frame, win->frame.size[0]),
gwl_window_fractional_from_viewport_round(win->frame, win->frame.size[1]));
return true;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Window Activation
* \{ */
/**
* Unlike #GHOST_WindowWayland::activate which responds to WAYLAND having set the window active.
* This function makes a request to WAYLAND for the window to become active.
*
* \note The request may be ignored and may not work on all compositors.
*/
static void gwl_window_activate(GWL_Window *win)
{
GHOST_SystemWayland *system = win->ghost_system;
xdg_activation_v1 *activation_manager = system->xdg_activation_manager_get();
if (UNLIKELY(activation_manager == nullptr)) {
return;
}
if (win->xdg.activation_token) {
/* We're about to overwrite this with a new request. */
xdg_activation_token_v1_destroy(win->xdg.activation_token);
}
win->xdg.activation_token = xdg_activation_v1_get_activation_token(activation_manager);
xdg_activation_token_v1_add_listener(
win->xdg.activation_token, xdg_activation_listener_get(), win);
xdg_activation_token_v1_set_app_id(win->xdg.activation_token,
GHOST_SystemWayland::xdg_app_id_get());
/* The serial of the input device requesting activation. */
{
uint32_t serial = 0;
wl_seat *seat = system->wl_seat_active_get_with_input_serial(serial);
if (seat) {
xdg_activation_token_v1_set_serial(win->xdg.activation_token, serial, seat);
}
}
/* The surface of the window requesting activation. */
{
GHOST_WindowWayland *ghost_window_active = static_cast<GHOST_WindowWayland *>(
system->getWindowManager()->getActiveWindow());
if (ghost_window_active) {
wl_surface *surface = ghost_window_active->wl_surface_get();
if (surface) {
xdg_activation_token_v1_set_surface(win->xdg.activation_token, surface);
}
}
}
xdg_activation_token_v1_commit(win->xdg.activation_token);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Window Pending Actions
* \{ */
static void gwl_window_frame_pending_fractional_scale_set_notest(
GWL_Window *win, bool *r_surface_needs_commit, bool *r_surface_needs_buffer_scale)
{
if (win->frame_pending.fractional_scale) {
win->frame.fractional_scale = win->frame_pending.fractional_scale;
gwl_window_viewport_set(win, r_surface_needs_commit, r_surface_needs_buffer_scale);
gwl_window_viewport_size_update(win);
}
else {
if (win->wp.viewport) {
gwl_window_viewport_unset(win, r_surface_needs_commit, r_surface_needs_buffer_scale);
}
else {
win->frame.buffer_scale = win->frame_pending.buffer_scale;
if (r_surface_needs_buffer_scale) {
*r_surface_needs_buffer_scale = true;
}
else {
wl_surface_set_buffer_scale(win->wl.surface, win->frame.buffer_scale);
}
if (r_surface_needs_commit) {
*r_surface_needs_commit = true;
}
else {
wl_surface_commit(win->wl.surface);
}
}
}
}
static void gwl_window_frame_pending_fractional_scale_set(GWL_Window *win,
bool *r_surface_needs_commit,
bool *r_surface_needs_buffer_scale)
{
if (win->frame_pending.fractional_scale == win->frame.fractional_scale &&
win->frame_pending.buffer_scale == win->frame.buffer_scale)
{
return;
}
gwl_window_frame_pending_fractional_scale_set_notest(
win, r_surface_needs_commit, r_surface_needs_buffer_scale);
}
static void gwl_window_frame_pending_size_set(GWL_Window *win,
bool *r_surface_needs_commit,
bool *r_surface_needs_resize_for_backend,
bool *r_surface_needs_buffer_scale)
{
if (win->frame_pending.size[0] == 0 || win->frame_pending.size[1] == 0) {
return;
}
win->frame.size[0] = win->frame_pending.size[0];
win->frame.size[1] = win->frame_pending.size[1];
if (win->frame_pending.fractional_scale != win->frame.fractional_scale ||
win->frame_pending.buffer_scale != win->frame.buffer_scale)
{
gwl_window_frame_pending_fractional_scale_set(
win, r_surface_needs_commit, r_surface_needs_buffer_scale);
}
else {
gwl_window_viewport_size_update(win);
}
if (r_surface_needs_resize_for_backend) {
*r_surface_needs_resize_for_backend = true;
}
else {
gwl_window_resize_for_backend(win, win->frame.size);
}
win->ghost_window->notify_size();
win->frame_pending.size[0] = 0;
win->frame_pending.size[1] = 0;
}
static void gwl_window_frame_update_from_pending(GWL_Window *win);
#ifdef USE_EVENT_BACKGROUND_THREAD
static void gwl_window_pending_actions_tag(GWL_Window *win, enum eGWL_PendingWindowActions type)
{
win->pending_actions[int(type)].store(true);
win->ghost_system->has_pending_actions_for_window.store(true);
}
static void gwl_window_pending_actions_handle(GWL_Window *win)
{
/* Ensure pending actions always use the state when the function starts
* because one actions may trigger other pending actions an in that case
* exact behavior depends on the order functions are called here.
* Without this, configuring the frame will trigger the surface
* commit immediately instead of the next time pending actions are handled. */
bool actions[PENDING_NUM];
for (size_t i = 0; i < ARRAY_SIZE(actions); i++) {
actions[i] = win->pending_actions[i].exchange(false);
}
if (actions[PENDING_WINDOW_FRAME_CONFIGURE]) {
gwl_window_frame_update_from_pending(win);
}
if (actions[PENDING_OUTPUT_SCALE_UPDATE_DEFERRED]) {
gwl_window_pending_actions_tag(win, PENDING_OUTPUT_SCALE_UPDATE);
/* Force postponing scale update to ensure all scale information has been taken into account
* before the actual update is performed. Failing to do so tends to cause flickering. */
actions[PENDING_OUTPUT_SCALE_UPDATE] = false;
}
if (actions[PENDING_OUTPUT_SCALE_UPDATE]) {
win->ghost_window->outputs_changed_update_scale();
}
if (actions[PENDING_WINDOW_SURFACE_COMMIT]) {
wl_surface_commit(win->wl.surface);
}
if (actions[PENDING_WINDOW_CURSOR_SHAPE_REFRESH]) {
gwl_window_cursor_shape_refresh(
win->ghost_window->getCursorShape(), win->cursor_generator, win->ghost_system);
}
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
/**
* Update the window's #GWL_WindowFrame.
* The caller must handle locking & run from the main thread.
*/
static void gwl_window_frame_update_from_pending_no_lock(GWL_Window *win)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
GHOST_ASSERT(win->ghost_system->main_thread_id == std::this_thread::get_id(),
"Only from main thread!");
#endif
const bool dpi_changed = win->frame_pending.fractional_scale != win->frame.fractional_scale;
bool surface_needs_commit = false;
bool surface_needs_resize_for_backend = false;
bool surface_needs_buffer_scale = false;
if (win->frame_pending.size[0] != 0 && win->frame_pending.size[1] != 0) {
if ((win->frame.size[0] != win->frame_pending.size[0]) ||
(win->frame.size[1] != win->frame_pending.size[1]))
{
gwl_window_frame_pending_size_set(win,
&surface_needs_commit,
&surface_needs_resize_for_backend,
&surface_needs_buffer_scale);
}
}
if (win->frame_pending.fractional_scale || win->frame.fractional_scale) {
gwl_window_frame_pending_fractional_scale_set(
win, &surface_needs_commit, &surface_needs_buffer_scale);
}
else {
if (win->frame_pending.buffer_scale != win->frame.buffer_scale) {
win->frame.buffer_scale = win->frame_pending.buffer_scale;
surface_needs_buffer_scale = true;
}
}
if (surface_needs_resize_for_backend) {
gwl_window_resize_for_backend(win, win->frame.size);
}
if (surface_needs_buffer_scale) {
wl_surface_set_buffer_scale(win->wl.surface, win->frame.buffer_scale);
}
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (use_libdecor) {
GWL_LibDecor_Window &decor = *win->libdecor;
if (decor.pending.ack_configure) {
surface_needs_commit = true;
decor.pending.ack_configure = false;
libdecor_state *state = libdecor_state_new(UNPACK2(decor.pending.size));
# ifdef USE_LIBDECOR_CONFIG_COPY_QUEUE
GHOST_ASSERT(decor.pending.size[0] != 0 && decor.pending.size[1] != 0, "Invalid size");
for (libdecor_configuration *configuration : decor.pending.configuration_queue) {
libdecor_frame_commit(decor.frame, state, configuration);
ghost_wl_libdecor_configuration_free(configuration);
}
decor.pending.configuration_queue.clear();
# endif
libdecor_frame_commit(decor.frame, state, decor.pending.configuration);
libdecor_state_free(state);
decor.pending.size[0] = 0;
decor.pending.size[1] = 0;
if (decor.initial_configure_seen == false) {
decor.initial_configure_seen = true;
if (decor.initial_configure_state) {
xdg_toplevel *toplevel = libdecor_frame_get_xdg_toplevel(decor.frame);
gwl_window_state_set_for_xdg(
toplevel, decor.initial_configure_state.value(), gwl_window_state_get(win));
decor.initial_configure_state = std::nullopt;
}
}
# ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
if (decor.pending.configuration_needs_free) {
ghost_wl_libdecor_configuration_free(decor.pending.configuration);
decor.pending.configuration_needs_free = false;
}
# endif
decor.pending.configuration = nullptr;
}
}
else
#endif
if (win->xdg_decor)
{
GWL_XDG_Decor_Window &decor = *win->xdg_decor;
if (decor.pending.ack_configure) {
xdg_surface_ack_configure(decor.surface, decor.pending.ack_configure_serial);
/* The XDG spec states a commit event is required after ACK configure. */
surface_needs_commit = true;
decor.pending.ack_configure = false;
decor.pending.ack_configure_serial = 0;
decor.initial_configure_seen = true;
}
}
if (surface_needs_commit) {
#ifdef USE_EVENT_BACKGROUND_THREAD
/* Postponing the commit avoids flickering when moving between monitors of different scale. */
gwl_window_pending_actions_tag(win, PENDING_WINDOW_SURFACE_COMMIT);
#else
wl_surface_commit(win->wl.surface);
#endif
}
if (dpi_changed) {
GHOST_SystemWayland *system = win->ghost_system;
system->pushEvent(new GHOST_Event(
system->getMilliSeconds(), GHOST_kEventWindowDPIHintChanged, win->ghost_window));
}
if (win->frame.is_active != win->frame_pending.is_active) {
if (win->frame_pending.is_active) {
win->ghost_window->activate();
}
else {
win->ghost_window->deactivate();
}
}
else if (false) {
/* Disabled, this can happen during debugging
* when the window changed while the process has been paused. */
GHOST_ASSERT(
win->frame.is_active ==
(win->ghost_system->getWindowManager()->getActiveWindow() == win->ghost_window),
"GHOST internal active state does not match WAYLAND!");
}
win->frame_pending.size[0] = win->frame.size[0];
win->frame_pending.size[1] = win->frame.size[1];
win->frame = win->frame_pending;
/* Signal not to apply the scale unless it's configured. */
win->frame_pending.size[0] = 0;
win->frame_pending.size[1] = 0;
}
[[maybe_unused]] static void gwl_window_frame_update_from_pending(GWL_Window *win)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_frame_guard{win->frame_pending_mutex};
#endif
gwl_window_frame_update_from_pending_no_lock(win);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Internal Utilities
* \{ */
/**
* Return -1 if `output_a` has a scale smaller than `output_b`, 0 when there equal, otherwise 1.
*/
static int output_scale_cmp(const GWL_Output *output_a, const GWL_Output *output_b)
{
if (output_a->scale < output_b->scale) {
return -1;
}
if (output_a->scale > output_b->scale) {
return 1;
}
if (output_a->has_scale_fractional || output_b->has_scale_fractional) {
const int scale_fractional_a = output_a->has_scale_fractional ?
output_a->scale_fractional :
(output_a->scale * FRACTIONAL_DENOMINATOR);
const int scale_fractional_b = output_b->has_scale_fractional ?
output_b->scale_fractional :
(output_b->scale * FRACTIONAL_DENOMINATOR);
if (scale_fractional_a < scale_fractional_b) {
return -1;
}
if (scale_fractional_a > scale_fractional_b) {
return 1;
}
}
return 0;
}
static int outputs_max_scale_or_default(const std::vector<GWL_Output *> &outputs,
const int32_t scale_default,
int *r_scale_fractional)
{
const GWL_Output *output_max = nullptr;
for (const GWL_Output *reg_output : outputs) {
if (!output_max || (output_scale_cmp(output_max, reg_output) == -1)) {
output_max = reg_output;
}
}
if (output_max) {
if (r_scale_fractional) {
*r_scale_fractional = output_max->has_scale_fractional ?
output_max->scale_fractional :
(output_max->scale * FRACTIONAL_DENOMINATOR);
}
return output_max->scale;
}
if (r_scale_fractional) {
*r_scale_fractional = scale_default * FRACTIONAL_DENOMINATOR;
}
return scale_default;
}
static int outputs_uniform_scale_or_default(const std::vector<GWL_Output *> &outputs,
const int32_t scale_default,
int *r_scale_fractional)
{
const GWL_Output *output_uniform = nullptr;
for (const GWL_Output *reg_output : outputs) {
if (!output_uniform) {
output_uniform = reg_output;
}
else if (output_scale_cmp(output_uniform, reg_output) != 0) {
/* Non-uniform. */
output_uniform = nullptr;
break;
}
}
if (output_uniform) {
if (r_scale_fractional) {
*r_scale_fractional = output_uniform->has_scale_fractional ?
output_uniform->scale_fractional :
(output_uniform->scale * FRACTIONAL_DENOMINATOR);
}
return output_uniform->scale;
}
if (r_scale_fractional) {
*r_scale_fractional = scale_default * FRACTIONAL_DENOMINATOR;
}
return scale_default;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name LIBDECOR Configuration Workaround
*
* These workarounds are needed until LIBDECOR supports copy/free.
* \{ */
#ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
static libdecor_configuration *ghost_wl_libdecor_configuration_copy(
const libdecor_configuration *configuration)
{
size_t configuration_size = malloc_usable_size((void *)configuration);
libdecor_configuration *configuration_copy = (libdecor_configuration *)malloc(
configuration_size);
memcpy((void *)configuration_copy, (const void *)configuration, configuration_size);
return configuration_copy;
}
static void ghost_wl_libdecor_configuration_free(libdecor_configuration *configuration)
{
free((void *)configuration);
}
#endif /* USE_LIBDECOR_CONFIG_COPY_WORKAROUND */
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (XDG Top Level), #xdg_toplevel_listener
* \{ */
static CLG_LogRef LOG_WL_XDG_TOPLEVEL = {"ghost.wl.handle.xdg_toplevel"};
#define LOG (&LOG_WL_XDG_TOPLEVEL)
static void xdg_toplevel_handle_configure(void *data,
xdg_toplevel * /*xdg_toplevel*/,
const int32_t width,
const int32_t height,
wl_array *states)
{
/* TODO: log `states`, not urgent. */
CLOG_DEBUG(LOG, "configure (size=[%d, %d])", width, height);
GWL_Window *win = static_cast<GWL_Window *>(data);
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_frame_guard{win->frame_pending_mutex};
#endif
const int32_t size[2] = {width, height};
for (int i = 0; i < 2; i++) {
if (size[i] == 0) {
/* Values may be zero, in this case the client should choose. */
continue;
}
win->frame_pending.size[i] = win->frame.fractional_scale ?
gwl_window_fractional_to_viewport_round(win->frame, size[i]) :
(size[i] * win->frame.buffer_scale);
}
win->frame_pending.is_maximised = false;
win->frame_pending.is_fullscreen = false;
win->frame_pending.is_active = false;
enum xdg_toplevel_state *state;
WL_ARRAY_FOR_EACH (state, states) {
switch (*state) {
case XDG_TOPLEVEL_STATE_MAXIMIZED:
win->frame_pending.is_maximised = true;
break;
case XDG_TOPLEVEL_STATE_FULLSCREEN:
win->frame_pending.is_fullscreen = true;
break;
case XDG_TOPLEVEL_STATE_ACTIVATED:
win->frame_pending.is_active = true;
break;
default:
break;
}
}
}
static void xdg_toplevel_handle_close(void *data, xdg_toplevel * /*xdg_toplevel*/)
{
CLOG_DEBUG(LOG, "close");
GWL_Window *win = static_cast<GWL_Window *>(data);
win->ghost_window->close();
}
static void xdg_toplevel_handle_configure_bounds(void *data,
xdg_toplevel * /*xdg_toplevel*/,
const int32_t width,
const int32_t height)
{
/* Only available in interface version 4. */
CLOG_DEBUG(LOG, "configure_bounds (size=[%d, %d])", width, height);
/* No need to lock as this only runs on window creation. */
GWL_Window *win = static_cast<GWL_Window *>(data);
GWL_XDG_Decor_Window &decor = *win->xdg_decor;
if (decor.initial_configure_seen == false) {
decor.initial_bounds[0] = width;
decor.initial_bounds[1] = height;
}
}
static void xdg_toplevel_handle_wm_capabilities(void * /*data*/,
xdg_toplevel * /*xdg_toplevel*/,
wl_array * /*capabilities*/)
{
/* Only available in interface version 5. */
CLOG_DEBUG(LOG, "wm_capabilities");
/* NOTE: this would be useful if blender had CSD. */
}
static const xdg_toplevel_listener xdg_toplevel_listener = {
/*configure*/ xdg_toplevel_handle_configure,
/*close*/ xdg_toplevel_handle_close,
/*configure_bounds*/ xdg_toplevel_handle_configure_bounds,
/*wm_capabilities*/ xdg_toplevel_handle_wm_capabilities,
};
#undef LOG
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (XDG Activation), #xdg_activation_v1_interface
*
* Used by #gwl_window_activate.
* \{ */
static void xdg_activation_handle_done(void *data,
xdg_activation_token_v1 *xdg_activation_token_v1,
const char *token)
{
GWL_Window *win = static_cast<GWL_Window *>(data);
if (xdg_activation_token_v1 != win->xdg.activation_token) {
return;
}
GHOST_SystemWayland *system = win->ghost_system;
xdg_activation_v1 *activation_manager = system->xdg_activation_manager_get();
xdg_activation_v1_activate(activation_manager, token, win->wl.surface);
xdg_activation_token_v1_destroy(win->xdg.activation_token);
win->xdg.activation_token = nullptr;
}
static const xdg_activation_token_v1_listener xdg_activation_listener = {
/*done*/ xdg_activation_handle_done,
};
static const xdg_activation_token_v1_listener *xdg_activation_listener_get()
{
return &xdg_activation_listener;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Fractional Scale), #wp_fractional_scale_manager_v1_interface
*
* Used by #gwl_window_activate.
* \{ */
static CLG_LogRef LOG_WL_FRACTIONAL_SCALE = {"ghost.wl.handle.fractional_scale"};
#define LOG (&LOG_WL_FRACTIONAL_SCALE)
static void wp_fractional_scale_handle_preferred_scale(
void *data, wp_fractional_scale_v1 * /*wp_fractional_scale_v1*/, uint preferred_scale)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_frame_guard{static_cast<GWL_Window *>(data)->frame_pending_mutex};
#endif
CLOG_DEBUG(LOG,
"preferred_scale (preferred_scale=%.6f)",
double(preferred_scale) / FRACTIONAL_DENOMINATOR);
GWL_Window *win = static_cast<GWL_Window *>(data);
if (win->frame_pending.fractional_scale_preferred != int(preferred_scale)) {
win->frame_pending.fractional_scale_preferred = preferred_scale;
win->ghost_window->outputs_changed_update_scale_tag();
}
}
static const wp_fractional_scale_v1_listener wp_fractional_scale_listener = {
/*preferred_scale*/ wp_fractional_scale_handle_preferred_scale,
};
#undef LOG
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (LibDecor Frame), #libdecor_frame_interface
* \{ */
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
static CLG_LogRef LOG_WL_LIBDECOR_FRAME = {"ghost.wl.handle.libdecor_frame"};
# define LOG (&LOG_WL_LIBDECOR_FRAME)
static void libdecor_frame_handle_configure(libdecor_frame *frame,
libdecor_configuration *configuration,
void *data)
{
CLOG_DEBUG(LOG, "configure");
# ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_frame_guard{static_cast<GWL_Window *>(data)->frame_pending_mutex};
const bool is_main_thread = [data] {
const GWL_Window *win = static_cast<GWL_Window *>(data);
const GHOST_SystemWayland *system = win->ghost_system;
return system->main_thread_id == std::this_thread::get_id();
}();
# endif
GWL_WindowFrame &frame_pending = static_cast<GWL_Window *>(data)->frame_pending;
/* Set the size. */
int size_next[2] = {0, 0};
{
GWL_Window *win = static_cast<GWL_Window *>(data);
const int fractional_scale = win->frame.fractional_scale;
/* It's important `fractional_scale` has a fractional component or rounding up will fail
* to produce the correct whole-number scale. */
GHOST_ASSERT((fractional_scale == 0) ||
(gwl_round_int_test(fractional_scale, FRACTIONAL_DENOMINATOR) == false),
"Fractional scale has no fractional component!");
/* The size from LIBDECOR wont use the GHOST windows buffer size.
* so it's important to calculate the buffer size that would have been used
* if fractional scaling wasn't supported. */
const int scale = fractional_scale ? (fractional_scale / FRACTIONAL_DENOMINATOR) + 1 :
win->frame.buffer_scale;
const int scale_as_fractional = scale * FRACTIONAL_DENOMINATOR;
if (libdecor_configuration_get_content_size(
configuration, frame, &size_next[0], &size_next[1]))
{
if (fractional_scale) {
frame_pending.size[0] = gwl_window_fractional_to_viewport_round(win->frame, size_next[0]);
frame_pending.size[1] = gwl_window_fractional_to_viewport_round(win->frame, size_next[1]);
}
else if (fractional_scale && (fractional_scale != (scale * FRACTIONAL_DENOMINATOR))) {
/* The windows `preferred_scale` is not yet available,
* set the size as if fractional scale is available. */
frame_pending.size[0] = ((size_next[0] * scale) * fractional_scale) / scale_as_fractional;
frame_pending.size[1] = ((size_next[1] * scale) * fractional_scale) / scale_as_fractional;
}
else {
frame_pending.size[0] = size_next[0] * scale;
frame_pending.size[1] = size_next[1] * scale;
}
/* Account for buffer rounding requirement, once fractional scaling is enabled
* the buffer scale will be 1, rounding is a requirement until then. */
gwl_round_int2_by(frame_pending.size, win->frame.buffer_scale);
}
else {
/* These values are cleared after use & will practically always be zero.
* Read them because it's possible multiple configure calls run before they can be handled.
*/
const GWL_LibDecor_Window &decor = *win->libdecor;
size_next[0] = decor.pending.size[0];
size_next[1] = decor.pending.size[1];
}
}
/* Set the state. */
{
enum libdecor_window_state window_state;
if (libdecor_configuration_get_window_state(configuration, &window_state)) {
frame_pending.is_maximised = window_state & LIBDECOR_WINDOW_STATE_MAXIMIZED;
frame_pending.is_fullscreen = window_state & LIBDECOR_WINDOW_STATE_FULLSCREEN;
frame_pending.is_active = window_state & LIBDECOR_WINDOW_STATE_ACTIVE;
}
}
{
GWL_Window *win = static_cast<GWL_Window *>(data);
GWL_LibDecor_Window &decor = *win->libdecor;
# ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
/* Unlikely but possible a previous configuration is unhandled. */
if (decor.pending.configuration_needs_free) {
ghost_wl_libdecor_configuration_free(decor.pending.configuration);
decor.pending.configuration_needs_free = false;
}
# endif /* USE_LIBDECOR_CONFIG_COPY_WORKAROUND */
decor.pending.size[0] = size_next[0];
decor.pending.size[1] = size_next[1];
decor.pending.configuration = configuration;
decor.pending.ack_configure = true;
# ifdef USE_EVENT_BACKGROUND_THREAD
if (!is_main_thread) {
# ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
decor.pending.configuration = ghost_wl_libdecor_configuration_copy(configuration);
decor.pending.configuration_needs_free = true;
# else
/* Without a way to copy the configuration,
* the configuration will be ignored as it can't be postponed. */
decor.pending.configuration = nullptr;
# endif /* !USE_LIBDECOR_CONFIG_COPY_WORKAROUND */
}
# endif
# ifdef USE_LIBDECOR_CONFIG_COPY_QUEUE
if (!(size_next[0] && size_next[1])) {
/* Always copy. */
if (decor.pending.configuration_needs_free == false) {
decor.pending.configuration = ghost_wl_libdecor_configuration_copy(
decor.pending.configuration);
decor.pending.configuration_needs_free = true;
}
/* Transfer ownership to the queue. */
decor.pending.configuration_queue.push_back(decor.pending.configuration);
decor.pending.configuration = nullptr;
decor.pending.configuration_needs_free = false;
/* Wait until we have a valid size. */
decor.pending.ack_configure = false;
}
# endif /* USE_LIBDECOR_CONFIG_COPY_QUEUE */
}
/* Apply & commit the changes. */
{
GWL_Window *win = static_cast<GWL_Window *>(data);
# ifdef USE_EVENT_BACKGROUND_THREAD
if (!is_main_thread) {
gwl_window_pending_actions_tag(win, PENDING_WINDOW_FRAME_CONFIGURE);
}
else
# endif
{
gwl_window_frame_update_from_pending_no_lock(win);
}
}
}
static void libdecor_frame_handle_close(libdecor_frame * /*frame*/, void *data)
{
CLOG_DEBUG(LOG, "close");
GWL_Window *win = static_cast<GWL_Window *>(data);
win->ghost_window->close();
}
static void libdecor_frame_handle_commit(libdecor_frame * /*frame*/, void *data)
{
CLOG_DEBUG(LOG, "commit");
# if 0
GWL_Window *win = static_cast<GWL_Window *>(data);
win->ghost_window->notify_decor_redraw();
# else
(void)data;
# endif
}
/* NOTE: cannot be `const` because of the LIBDECOR API. */
static libdecor_frame_interface libdecor_frame_iface = {
/*configure*/ libdecor_frame_handle_configure,
/*close*/ libdecor_frame_handle_close,
/*commit*/ libdecor_frame_handle_commit,
};
# undef LOG
#endif /* WITH_GHOST_WAYLAND_LIBDECOR. */
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (XDG Decoration Listener), #zxdg_toplevel_decoration_v1_listener
* \{ */
static CLG_LogRef LOG_WL_XDG_TOPLEVEL_DECORATION = {"ghost.wl.handle.xdg_toplevel_decoration"};
#define LOG (&LOG_WL_XDG_TOPLEVEL_DECORATION)
static void xdg_toplevel_decoration_handle_configure(
void *data, zxdg_toplevel_decoration_v1 * /*zxdg_toplevel_decoration_v1*/, const uint32_t mode)
{
CLOG_DEBUG(LOG, "configure (mode=%u)", mode);
GWL_Window *win = static_cast<GWL_Window *>(data);
win->xdg_decor->mode = (zxdg_toplevel_decoration_v1_mode)mode;
}
static const zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration_v1_listener = {
/*configure*/ xdg_toplevel_decoration_handle_configure,
};
#undef LOG
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (XDG Surface Handle Configure), #xdg_surface_listener
* \{ */
static CLG_LogRef LOG_WL_XDG_SURFACE = {"ghost.wl.handle.xdg_surface"};
#define LOG (&LOG_WL_XDG_SURFACE)
static void xdg_surface_handle_configure(void *data,
xdg_surface *xdg_surface,
const uint32_t serial)
{
GWL_Window *win = static_cast<GWL_Window *>(data);
if (win->xdg_decor->surface != xdg_surface) {
CLOG_DEBUG(LOG, "configure (skipped)");
return;
}
CLOG_DEBUG(LOG, "configure");
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_frame_guard{static_cast<GWL_Window *>(data)->frame_pending_mutex};
#endif
win->xdg_decor->pending.ack_configure = true;
win->xdg_decor->pending.ack_configure_serial = serial;
#ifdef USE_EVENT_BACKGROUND_THREAD
const GHOST_SystemWayland *system = win->ghost_system;
const bool is_main_thread = system->main_thread_id == std::this_thread::get_id();
if (!is_main_thread) {
/* NOTE(@ideasman42): this only gets one redraw,
* I could not find a case where this causes problems. */
gwl_window_pending_actions_tag(win, PENDING_WINDOW_FRAME_CONFIGURE);
}
else
#endif
{
gwl_window_frame_update_from_pending_no_lock(win);
}
}
static const xdg_surface_listener xdg_surface_listener = {
/*configure*/ xdg_surface_handle_configure,
};
#undef LOG
/** \} */
/* -------------------------------------------------------------------- */
/** \name Listener (Surface), #wl_surface_listener
* \{ */
static CLG_LogRef LOG_WL_SURFACE = {"ghost.wl.handle.surface"};
#define LOG (&LOG_WL_SURFACE)
static void surface_handle_enter(void *data, wl_surface * /*wl_surface*/, wl_output *wl_output)
{
if (!ghost_wl_output_own(wl_output)) {
CLOG_DEBUG(LOG, "enter (skipped)");
return;
}
CLOG_DEBUG(LOG, "enter");
GWL_Output *reg_output = ghost_wl_output_user_data(wl_output);
GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(data);
if (win->outputs_enter(reg_output)) {
win->outputs_changed_update_scale_tag();
}
}
static void surface_handle_leave(void *data, wl_surface * /*wl_surface*/, wl_output *wl_output)
{
if (!ghost_wl_output_own(wl_output)) {
CLOG_DEBUG(LOG, "leave (skipped)");
return;
}
CLOG_DEBUG(LOG, "leave");
GWL_Output *reg_output = ghost_wl_output_user_data(wl_output);
GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(data);
if (win->outputs_leave(reg_output)) {
win->outputs_changed_update_scale_tag();
}
}
#if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) && \
defined(WL_SURFACE_PREFERRED_BUFFER_TRANSFORM_SINCE_VERSION)
static void surface_handle_preferred_buffer_scale(void * /*data*/,
wl_surface * /*wl_surface*/,
const int32_t factor)
{
/* Only available in interface version 6. */
CLOG_DEBUG(LOG, "handle_preferred_buffer_scale (factor=%d)", factor);
}
static void surface_handle_preferred_buffer_transform(void * /*data*/,
wl_surface * /*wl_surface*/,
const uint32_t transform)
{
/* Only available in interface version 6. */
CLOG_DEBUG(LOG, "handle_preferred_buffer_transform (transform=%u)", transform);
}
#endif /* WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION && \
* WL_SURFACE_PREFERRED_BUFFER_TRANSFORM_SINCE_VERSION */
static const wl_surface_listener wl_surface_listener = {
/*enter*/ surface_handle_enter,
/*leave*/ surface_handle_leave,
#if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) && \
defined(WL_SURFACE_PREFERRED_BUFFER_TRANSFORM_SINCE_VERSION)
/*preferred_buffer_scale*/ surface_handle_preferred_buffer_scale,
/*preferred_buffer_transform*/ surface_handle_preferred_buffer_transform,
#endif
};
#undef LOG
/** \} */
/* -------------------------------------------------------------------- */
/** \name GHOST Implementation
*
* WAYLAND specific implementation of the #GHOST_Window interface.
* \{ */
GHOST_WindowWayland::GHOST_WindowWayland(GHOST_SystemWayland *system,
const char *title,
const int32_t /*left*/,
const int32_t /*top*/,
const uint32_t width,
const uint32_t height,
const GHOST_TWindowState state,
const GHOST_IWindow *parent_window,
const GHOST_TDrawingContextType type,
const bool is_dialog,
const GHOST_ContextParams &context_params,
const bool exclusive,
const GHOST_GPUDevice &preferred_device)
: GHOST_Window(width, height, state, context_params, exclusive),
system_(system),
window_(new GWL_Window),
preferred_device_(preferred_device)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system->server_mutex};
#endif
window_->ghost_window = this;
window_->ghost_system = system;
window_->ghost_context_type = type;
wl_display *display = system->wl_display_get();
/* NOTE(@ideasman42): The scale set here to avoid flickering on startup.
* When all monitors use the same scale (which is quite common) there aren't any problems.
*
* When monitors have different scales there may still be a visible window resize on startup.
* Ideally it would be possible to know the scale this window will use however that's only
* known once #surface_enter callback runs (which isn't guaranteed to run at all).
*
* Using the maximum scale is best as it results in the window first being smaller,
* avoiding a large window flashing before it's made smaller.
*
* For fractional scaling the buffer will eventually be 1. Setting it to 1 now
* (to avoid window size rounding and buffer size switching) has some down-sides.
* It means the window will be drawn larger for a moment then smaller once fractional scaling
* is detected and enabled. Unfortunately, it doesn't seem possible to receive the
* #wp_fractional_scale_v1_listener::preferred_scale information before the window is created
* So leave the buffer scaled up because there is no *guarantee* the fractional scaling support
* will run which could result in an incorrect buffer scale. */
int scale_fractional_from_output;
const int buffer_scale_from_output = outputs_uniform_scale_or_default(
system_->outputs_get(), 0, &scale_fractional_from_output);
window_->frame.size[0] = int32_t(width);
window_->frame.size[1] = int32_t(height);
window_->is_dialog = is_dialog;
/* Window surfaces. */
window_->wl.surface = wl_compositor_create_surface(system_->wl_compositor_get());
ghost_wl_surface_tag(window_->wl.surface);
wl_surface_add_listener(window_->wl.surface, &wl_surface_listener, window_);
wp_fractional_scale_manager_v1 *fractional_scale_manager =
system->wp_fractional_scale_manager_get();
if (fractional_scale_manager) {
window_->wp.fractional_scale_handle = wp_fractional_scale_manager_v1_get_fractional_scale(
fractional_scale_manager, window_->wl.surface);
wp_fractional_scale_v1_add_listener(
window_->wp.fractional_scale_handle, &wp_fractional_scale_listener, window_);
}
/* NOTE: The limit is in points (not pixels) so Hi-DPI will limit to larger number of pixels.
* This has the advantage that the size limit is the same when moving the window between monitors
* with different scales set. If it was important to limit in pixels it could be re-calculated
* when the `window_->frame.buffer_scale` changed. */
const int32_t size_min[2] = {320, 240};
const char *xdg_app_id = GHOST_SystemWayland::xdg_app_id_get();
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (use_libdecor) {
window_->libdecor = new GWL_LibDecor_Window;
GWL_LibDecor_Window &decor = *window_->libdecor;
/* create window decorations */
decor.frame = libdecor_decorate(
system_->libdecor_context_get(), window_->wl.surface, &libdecor_frame_iface, window_);
libdecor_frame_set_min_content_size(decor.frame, UNPACK2(size_min));
libdecor_frame_set_app_id(decor.frame, xdg_app_id);
if (parent_window) {
GWL_LibDecor_Window &decor_parent =
*dynamic_cast<const GHOST_WindowWayland *>(parent_window)->window_->libdecor;
libdecor_frame_set_parent(decor.frame, decor_parent.frame);
}
}
else
#endif
{
window_->xdg_decor = new GWL_XDG_Decor_Window;
GWL_XDG_Decor_Window &decor = *window_->xdg_decor;
decor.surface = xdg_wm_base_get_xdg_surface(system_->xdg_decor_shell_get(),
window_->wl.surface);
decor.toplevel = xdg_surface_get_toplevel(decor.surface);
xdg_toplevel_set_min_size(decor.toplevel, UNPACK2(size_min));
xdg_toplevel_set_app_id(decor.toplevel, xdg_app_id);
xdg_surface_add_listener(decor.surface, &xdg_surface_listener, window_);
xdg_toplevel_add_listener(decor.toplevel, &xdg_toplevel_listener, window_);
if (parent_window && is_dialog) {
GWL_XDG_Decor_Window &decor_parent =
*dynamic_cast<const GHOST_WindowWayland *>(parent_window)->window_->xdg_decor;
xdg_toplevel_set_parent(decor.toplevel, decor_parent.toplevel);
}
}
gwl_window_title_set(window_, title);
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (use_libdecor) {
/* Postpone mapping the window until after the app-id & title have been set.
* While this doesn't seem to be a requirement, LIBDECOR example code does this. */
libdecor_frame_map(window_->libdecor->frame);
}
#endif
wl_surface_set_user_data(window_->wl.surface, this);
/* NOTE: the method used for XDG & LIBDECOR initialization (using `initial_configure_seen`)
* follows the method used in SDL 3.16. */
/* Causes a glitch with `libdecor` for some reason. */
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (use_libdecor) {
/* Pass. */
}
else
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */
{
GWL_XDG_Decor_Window &decor = *window_->xdg_decor;
if (system_->xdg_decor_manager_get()) {
decor.toplevel_decor = zxdg_decoration_manager_v1_get_toplevel_decoration(
system_->xdg_decor_manager_get(), decor.toplevel);
zxdg_toplevel_decoration_v1_add_listener(
decor.toplevel_decor, &xdg_toplevel_decoration_v1_listener, window_);
zxdg_toplevel_decoration_v1_set_mode(decor.toplevel_decor,
ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
}
/* Commit needed to so configure callback runs. */
wl_surface_commit(window_->wl.surface);
/* Failure exits with an error, simply prevent an eternal loop. */
while (!decor.initial_configure_seen && !ghost_wl_display_report_error_if_set(display)) {
wl_display_flush(display);
wl_display_dispatch(display);
}
}
/* If the scale is known early, setup the window scale.
* Otherwise accept an unsightly flicker once the outputs scale can be found. */
int early_buffer_scale = 0;
int early_fractional_scale = 0;
if (const int test_fractional_scale =
fractional_scale_manager ? (window_->frame_pending.fractional_scale_preferred ?
window_->frame_pending.fractional_scale_preferred :
scale_fractional_from_output) :
0)
{
if (!gwl_round_int_test(test_fractional_scale, FRACTIONAL_DENOMINATOR)) {
early_fractional_scale = test_fractional_scale;
}
else {
/* Rounded, use simple integer buffer scaling. */
early_buffer_scale = test_fractional_scale / FRACTIONAL_DENOMINATOR;
early_fractional_scale = 0;
}
}
else if (buffer_scale_from_output) {
early_buffer_scale = buffer_scale_from_output;
}
if (early_fractional_scale != 0) {
/* Fractional scale is known. */
window_->frame.fractional_scale_preferred = early_fractional_scale;
window_->frame.fractional_scale = early_fractional_scale;
window_->frame.buffer_scale = 1;
window_->frame_pending.fractional_scale_preferred = early_fractional_scale;
window_->frame_pending.fractional_scale = early_fractional_scale;
window_->frame_pending.buffer_scale = 1;
/* The scale is considered initialized now. */
window_->frame_pending.is_scale_init = true;
/* Always commit and set the scale. */
bool surface_needs_commit_dummy = false, surface_needs_buffer_scale_dummy = false;
gwl_window_frame_pending_fractional_scale_set_notest(
window_, &surface_needs_commit_dummy, &surface_needs_buffer_scale_dummy);
}
else if (early_buffer_scale != 0) {
/* Non-fractional scale is known. */
/* No fractional scale, simple initialization. */
window_->frame.buffer_scale = early_buffer_scale;
window_->frame_pending.buffer_scale = early_buffer_scale;
/* The scale is considered initialized now. */
window_->frame_pending.is_scale_init = true;
/* The window surface must be rounded to the scale,
* failing to do so causes the WAYLAND-server to close the window immediately. */
gwl_round_int2_by(window_->frame.size, window_->frame.buffer_scale);
}
else {
/* Scale isn't known (the windows size may flicker when #outputs_changed_update_scale runs). */
window_->frame.buffer_scale = 1;
window_->frame_pending.buffer_scale = 1;
GHOST_ASSERT(window_->frame_pending.is_scale_init == false,
"An initialized scale is not expected");
}
if (window_->frame_pending.is_scale_init) {
/* If the output scale changes here it means the scale settings were not properly set. */
GHOST_ASSERT(outputs_changed_update_scale() == false,
"Fractional scale was not properly initialized");
}
wl_surface_set_buffer_scale(window_->wl.surface, window_->frame.buffer_scale);
/* Apply Bounds.
* Important to run after the buffer scale is known & before the buffer is created. */
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (use_libdecor) {
/* Pass (unsupported). */
}
else
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */
{
const GWL_XDG_Decor_Window &decor = *window_->xdg_decor;
if (decor.initial_bounds[0] && decor.initial_bounds[1]) {
window_->frame.size[0] = std::min(window_->frame.size[0],
decor.initial_bounds[0] * window_->frame.buffer_scale);
window_->frame.size[1] = std::min(window_->frame.size[1],
decor.initial_bounds[1] * window_->frame.buffer_scale);
}
}
/* Postpone binding the buffer until after it's decor has been configured:
* - Ensure the window is sized properly (with XDG window decorations), see: #113059.
* - Avoids flickering on startup.
*/
#ifdef WITH_OPENGL_BACKEND
if (type == GHOST_kDrawingContextTypeOpenGL) {
window_->backend.egl_window = wl_egl_window_create(
window_->wl.surface, int(window_->frame.size[0]), int(window_->frame.size[1]));
}
#endif
#ifdef WITH_VULKAN_BACKEND
if (type == GHOST_kDrawingContextTypeVulkan) {
window_->backend.vulkan_window_info = new GHOST_ContextVK_WindowInfo;
window_->backend.vulkan_window_info->size[0] = window_->frame.size[0];
window_->backend.vulkan_window_info->size[1] = window_->frame.size[1];
window_->backend.vulkan_window_info->is_color_managed = true;
}
#endif
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
# ifdef WITH_VULKAN_BACKEND
const bool libdecor_wait_for_window_init = (type == GHOST_kDrawingContextTypeVulkan);
# else
const bool libdecor_wait_for_window_init = false;
# endif
#endif
/* Drawing context. */
if (setDrawingContextType(type) == GHOST_kFailure) {
/* This can happen when repeatedly creating windows, see #123096.
* In this case #GHOST_WindowWayland::getValid will return false. */
GHOST_PRINT("Failed to create drawing context" << std::endl);
}
else {
window_->is_valid_setup = true;
}
if (window_->is_valid_setup == false) {
/* Don't attempt to setup the window if there is no context.
* This window is considered invalid and will be removed. */
}
else
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (use_libdecor && libdecor_wait_for_window_init)
{
/* Ensuring the XDG window has been created is *not* supported by VULKAN.
*
* Although this was once supported using a temporary SHM buffer,
* a DMA buffer is now required by some drivers which turns out to be
* impractical to create here, specially since it's only for a temporary buffer.
*
* Workaround the problem by postponing changes to the window state.
* This causes minor but noticeable glitch when starting maximized,
* where a rectangle is first shown before maximizing.
* With EGL this also happens however maximizing is almost immediate.
*
* This can't be avoided at the moment since LIBDECOR requires the window
* to be created before it's configured (sigh!).
* This can be removed if CSD are implemented, see: #113795. */
GWL_LibDecor_Window &decor = *window_->libdecor;
decor.initial_configure_state = state;
}
else if (use_libdecor) {
/* Commit needed so the top-level callbacks run (and `toplevel` can be accessed). */
wl_surface_commit(window_->wl.surface);
GWL_LibDecor_Window &decor = *window_->libdecor;
/* Additional round-trip is needed to ensure `xdg_toplevel` is set. */
wl_display_roundtrip(display);
/* NOTE: LIBDECOR requires the window to be created & configured before the state can be set.
* Workaround this by using the underlying `xdg_toplevel` */
/* Failure exits with an error, simply prevent an eternal loop. */
while (!decor.initial_configure_seen && !ghost_wl_display_report_error_if_set(display)) {
wl_display_flush(display);
wl_display_dispatch(display);
}
xdg_toplevel *toplevel = libdecor_frame_get_xdg_toplevel(decor.frame);
gwl_window_state_set_for_xdg(toplevel, state, gwl_window_state_get(window_));
/* NOTE(@ideasman42): Round trips are necessary with LIBDECOR on GNOME
* because resizing later on and redrawing does *not* update as it should, see #119871.
*
* Without the round-trip here:
* - The window will be created and this function will return using the requested buffer size,
* instead of the window size which ends up being used (causing a visible flicker).
* This has the down side that Blender's internal window state has the outdated size
* which then gets immediately resized, causing a noticeable glitch.
* - The window decorations will be displayed at the wrong size before refreshing
* at the new size.
* - On GNOME-Shell 46 shows the previous buffer-size under some conditions.
*
* In principle this could be used with XDG too however it causes problems with KDE
* and some WLROOTS based compositors.
*/
wl_display_roundtrip(display);
}
else
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */
{
gwl_window_state_set(window_, state);
}
/* Commit after setting the buffer.
* While postponing until after the buffer drawing is context is set
* isn't essential, it reduces flickering. */
wl_surface_commit(window_->wl.surface);
#ifdef WITH_OPENGL_BACKEND
if (type == GHOST_kDrawingContextTypeOpenGL) {
/* NOTE(@ideasman42): Set the swap interval to 0 (disable VSync) to prevent blocking.
* This was reported for SDL in 2021 so it may be good to revisit this decision
* at some point since forcing the VSync setting seems heavy-handed,
* especially if the issue gets resolved up-stream.
*
* For reference: https://github.com/libsdl-org/SDL/issues/4335
* From the report the compositor causing problems was GNOME's Mutter. */
setSwapInterval(0);
}
#endif
window_->is_init = true;
}
GHOST_WindowWayland::~GHOST_WindowWayland()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
releaseNativeHandles();
#ifdef WITH_OPENGL_BACKEND
if (window_->ghost_context_type == GHOST_kDrawingContextTypeOpenGL) {
wl_egl_window_destroy(window_->backend.egl_window);
}
#endif
#ifdef WITH_VULKAN_BACKEND
if (window_->ghost_context_type == GHOST_kDrawingContextTypeVulkan) {
delete window_->backend.vulkan_window_info;
}
#endif
if (window_->xdg.activation_token) {
xdg_activation_token_v1_destroy(window_->xdg.activation_token);
window_->xdg.activation_token = nullptr;
}
if (window_->wp.fractional_scale_handle) {
wp_fractional_scale_v1_destroy(window_->wp.fractional_scale_handle);
window_->wp.fractional_scale_handle = nullptr;
}
if (window_->wp.viewport) {
wp_viewport_destroy(window_->wp.viewport);
window_->wp.viewport = nullptr;
}
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (use_libdecor) {
gwl_libdecor_window_destroy(window_->libdecor);
}
else
#endif
{
gwl_xdg_decor_window_destroy(window_->xdg_decor);
}
/* Clear any pointers to this window. This is needed because there are no guarantees
* that flushing the display will the "leave" handlers before handling events. */
system_->window_surface_unref(window_->wl.surface);
wl_surface_destroy(window_->wl.surface);
/* NOTE(@ideasman42): Flushing will often run the appropriate handlers event
* (#wl_surface_listener.leave in particular) to avoid attempted access to the freed surfaces.
* This is not fool-proof though, hence the call to #window_surface_unref, see: #99078. */
wl_display_flush(system_->wl_display_get());
if (window_->cursor_generator) {
gwl_window_cursor_custom_free(window_->cursor_generator);
}
delete window_;
}
#ifdef USE_EVENT_BACKGROUND_THREAD
GHOST_TSuccess GHOST_WindowWayland::swapBuffers()
{
GHOST_ASSERT(system_->main_thread_id == std::this_thread::get_id(), "Only from main thread!");
return GHOST_Window::swapBuffers();
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
GHOST_TSuccess GHOST_WindowWayland::hasCursorShape(GHOST_TStandardCursor cursor_shape)
{
return system_->cursor_shape_check(cursor_shape);
}
GHOST_TSuccess GHOST_WindowWayland::setWindowCursorGrab(GHOST_TGrabCursorMode mode)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
GHOST_Rect bounds_buf;
const GHOST_Rect *bounds = nullptr;
if (cursor_grab_ == GHOST_kGrabWrap) {
if (getCursorGrabBounds(bounds_buf) == GHOST_kFailure) {
getClientBounds(bounds_buf);
}
bounds = &bounds_buf;
}
if (system_->window_cursor_grab_set(mode,
cursor_grab_,
cursor_grab_init_pos_,
bounds,
cursor_grab_axis_,
window_->wl.surface,
this->scale_params_get()))
{
return GHOST_kSuccess;
}
return GHOST_kFailure;
}
GHOST_TSuccess GHOST_WindowWayland::setWindowCursorShape(GHOST_TStandardCursor shape)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
const bool is_active = this == static_cast<const GHOST_WindowWayland *>(
system_->getWindowManager()->getActiveWindow());
gwl_window_cursor_custom_clear(&window_->cursor_generator);
cursor_shape_ = shape;
GHOST_TSuccess ok;
if (is_active) {
ok = system_->cursor_shape_set(cursor_shape_);
GHOST_TSuccess ok_test = ok;
if (ok == GHOST_kFailure) {
/* Failed, try again with the default cursor. */
cursor_shape_ = GHOST_kStandardCursorDefault;
ok_test = system_->cursor_shape_set(cursor_shape_);
}
wl_display *display = system_->wl_display_get();
#ifdef USE_CURSOR_IMMEDIATE_DISPATCH
if (ok == GHOST_kSuccess || ok_test == GHOST_kSuccess) {
wl_display_flush(display);
wl_display_dispatch_pending(display);
}
else
#endif /* USE_CURSOR_IMMEDIATE_DISPATCH */
if (ok_test == GHOST_kFailure) {
/* For the cursor to display when the event queue isn't being handled. */
wl_display_flush(display);
}
}
else {
/* Set later when activating the window. */
ok = system_->cursor_shape_check(shape);
if (ok == GHOST_kFailure) {
cursor_shape_ = GHOST_kStandardCursorDefault;
}
}
return ok;
}
bool GHOST_WindowWayland::getCursorGrabUseSoftwareDisplay()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
return system_->cursor_grab_use_software_display_get(cursor_grab_);
}
GHOST_TSuccess GHOST_WindowWayland::setWindowCustomCursorGenerator(
GHOST_CursorGenerator *cursor_generator)
{
/* Before this, all logic is just setting up the cursor. */
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
cursor_shape_ = GHOST_kStandardCursorCustom;
if (window_->cursor_generator) {
gwl_window_cursor_custom_free(window_->cursor_generator);
}
window_->cursor_generator = cursor_generator;
GHOST_TSuccess success = cursor_shape_refresh();
/* Let refresh handle applying the changes. */
if (success == GHOST_kSuccess) {
wl_display *display = system_->wl_display_get();
/* For the cursor to display when the event queue isn't being handled. */
wl_display_flush(display);
#ifdef USE_CURSOR_IMMEDIATE_DISPATCH
wl_display_dispatch_pending(display);
#endif
}
return success;
}
GHOST_TSuccess GHOST_WindowWayland::setWindowCustomCursorShape(const uint8_t *bitmap,
const uint8_t *mask,
const int size[2],
const int hot_spot[2],
const bool can_invert_color)
{
/* This is no longer needed as all cursors are generated on demand. */
GHOST_ASSERT(false, "All cursors must be generated!");
(void)bitmap;
(void)mask;
(void)size;
(void)hot_spot;
(void)can_invert_color;
return GHOST_kFailure;
}
GHOST_TSuccess GHOST_WindowWayland::getCursorBitmap(GHOST_CursorBitmapRef *bitmap)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
return system_->cursor_bitmap_get(bitmap);
}
bool GHOST_WindowWayland::getValid() const
{
return GHOST_Window::getValid() && window_->is_valid_setup;
}
void GHOST_WindowWayland::setTitle(const char *title)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
gwl_window_title_set(window_, title);
}
std::string GHOST_WindowWayland::getTitle() const
{
/* No need to lock `server_mutex` (WAYLAND never changes this). */
return window_->title.empty() ? "untitled" : window_->title;
}
void GHOST_WindowWayland::getWindowBounds(GHOST_Rect &bounds) const
{
getClientBounds(bounds);
}
void GHOST_WindowWayland::getClientBounds(GHOST_Rect &bounds) const
{
/* No need to lock `server_mutex` (WAYLAND never changes this in a thread). */
bounds.set(0, 0, UNPACK2(window_->frame.size));
}
GHOST_TSuccess GHOST_WindowWayland::setClientWidth(const uint32_t width)
{
return setClientSize(width, uint32_t(window_->frame.size[1]));
}
GHOST_TSuccess GHOST_WindowWayland::setClientHeight(const uint32_t height)
{
return setClientSize(uint32_t(window_->frame.size[0]), height);
}
GHOST_TSuccess GHOST_WindowWayland::setClientSize(const uint32_t width, const uint32_t height)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system_->server_mutex};
std::lock_guard lock_frame_guard{window_->frame_pending_mutex};
#endif
GWL_WindowFrame &frame_pending = window_->frame_pending;
frame_pending.size[0] = width;
frame_pending.size[1] = height;
gwl_round_int2_by(frame_pending.size, frame_pending.buffer_scale);
gwl_window_frame_pending_size_set(window_, nullptr, nullptr, nullptr);
return GHOST_kSuccess;
}
void GHOST_WindowWayland::screenToClient(const int32_t inX,
const int32_t inY,
int32_t &outX,
int32_t &outY) const
{
outX = inX;
outY = inY;
}
void GHOST_WindowWayland::clientToScreen(const int32_t inX,
const int32_t inY,
int32_t &outX,
int32_t &outY) const
{
outX = inX;
outY = inY;
}
uint16_t GHOST_WindowWayland::getDPIHint()
{
/* No need to lock `server_mutex`
* (`outputs_changed_update_scale` never changes values in a non-main thread). */
if (window_->frame.fractional_scale) {
return gwl_window_fractional_to_viewport(window_->frame, base_dpi);
}
return window_->frame.buffer_scale * base_dpi;
}
GHOST_TSuccess GHOST_WindowWayland::setWindowCursorVisibility(const bool visible)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
const GHOST_TSuccess ok = system_->cursor_visibility_set(visible);
if (ok == GHOST_kSuccess) {
wl_display *display = system_->wl_display_get();
/* For the cursor to display when the event queue isn't being handled. */
wl_display_flush(display);
#ifdef USE_CURSOR_IMMEDIATE_DISPATCH
wl_display_dispatch_pending(display);
#endif
}
return ok;
}
GHOST_TSuccess GHOST_WindowWayland::setState(GHOST_TWindowState state)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
return gwl_window_state_set(window_, state) ? GHOST_kSuccess : GHOST_kFailure;
}
GHOST_TWindowState GHOST_WindowWayland::getState() const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
return gwl_window_state_get(window_);
}
GHOST_TSuccess GHOST_WindowWayland::invalidate()
{
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_WindowWayland::setOrder(GHOST_TWindowOrder order)
{
/* NOTE(@ideasman42): only activation is supported (on X11 & Cocoa for example)
* both activation and raising is performed. Since WAYLAND only supports activation,
* do that as the compositor will likely raise the window as well.
* Although it's not ideal that raising does something but lowering a window is ignored. */
if (order == GHOST_kWindowOrderTop) {
gwl_window_activate(window_);
}
return GHOST_kSuccess;
}
bool GHOST_WindowWayland::isDialog() const
{
return window_->is_dialog;
}
GHOST_Context *GHOST_WindowWayland::newDrawingContext(GHOST_TDrawingContextType type)
{
switch (type) {
case GHOST_kDrawingContextTypeNone: {
GHOST_Context *context = new GHOST_ContextNone(want_context_params_);
return context;
}
#ifdef WITH_VULKAN_BACKEND
case GHOST_kDrawingContextTypeVulkan: {
GHOST_ContextVK *context = new GHOST_ContextVK(want_context_params_,
GHOST_kVulkanPlatformWayland,
0,
nullptr,
window_->wl.surface,
system_->wl_display_get(),
window_->backend.vulkan_window_info,
1,
2,
preferred_device_);
if (context->initializeDrawingContext()) {
return context;
}
delete context;
return nullptr;
}
#endif
#ifdef WITH_OPENGL_BACKEND
case GHOST_kDrawingContextTypeOpenGL: {
for (int minor = 6; minor >= 3; --minor) {
GHOST_Context *context = new GHOST_ContextEGL(
system_,
want_context_params_,
EGLNativeWindowType(window_->backend.egl_window),
EGLNativeDisplayType(system_->wl_display_get()),
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
4,
minor,
GHOST_OPENGL_EGL_CONTEXT_FLAGS |
(want_context_params_.is_debug ? EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR : 0),
GHOST_OPENGL_EGL_RESET_NOTIFICATION_STRATEGY,
EGL_OPENGL_API);
if (context->initializeDrawingContext()) {
return context;
}
delete context;
}
return nullptr;
}
#endif
default:
/* Unsupported backend. */
return nullptr;
}
}
#ifdef WITH_INPUT_IME
void GHOST_WindowWayland::beginIME(
const int32_t x, const int32_t y, const int32_t w, const int32_t h, const bool completed)
{
system_->ime_begin(this, x, y, w, h, completed);
}
void GHOST_WindowWayland::endIME()
{
system_->ime_end(this);
}
#endif
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Direct Data Access
*
* Expose some members via methods.
* \{ */
int GHOST_WindowWayland::scale_get() const
{
return window_->frame.buffer_scale;
}
const GWL_WindowScaleParams &GHOST_WindowWayland::scale_params_get() const
{
/* NOTE(@ideasman42): This could be kept initialized,
* since it's such a small struct it's not so important. */
GWL_WindowScaleParams *scale_params = &window_->scale_params;
scale_params->is_fractional = (window_->frame.fractional_scale != 0);
scale_params->scale = scale_params->is_fractional ? window_->frame.fractional_scale :
window_->frame.buffer_scale;
return *scale_params;
}
wl_fixed_t GHOST_WindowWayland::wl_fixed_from_window(wl_fixed_t value) const
{
if (window_->frame.fractional_scale) {
return gwl_window_fractional_from_viewport(window_->frame, value);
}
return value / window_->frame.buffer_scale;
}
wl_fixed_t GHOST_WindowWayland::wl_fixed_to_window(wl_fixed_t value) const
{
if (window_->frame.fractional_scale) {
return gwl_window_fractional_to_viewport(window_->frame, value);
}
return value * window_->frame.buffer_scale;
}
wl_surface *GHOST_WindowWayland::wl_surface_get() const
{
return window_->wl.surface;
}
const std::vector<GWL_Output *> &GHOST_WindowWayland::outputs_get()
{
return window_->outputs;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Window Level Functions
*
* High Level Windowing Utilities.
* \{ */
GHOST_TSuccess GHOST_WindowWayland::close()
{
return system_->pushEvent_maybe_pending(
new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowClose, this));
}
GHOST_TSuccess GHOST_WindowWayland::activate()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
const bool is_main_thread = system_->main_thread_id == std::this_thread::get_id();
if (is_main_thread)
#endif
{
/* This can run while the window being initialized.
* In this case, skip setting the window active but add the event, see: #120465. */
if (window_->is_init) {
if (system_->getWindowManager()->setActiveWindow(this) == GHOST_kFailure) {
return GHOST_kFailure;
}
}
}
const GHOST_TSuccess success = system_->pushEvent_maybe_pending(
new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowActivate, this));
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (success == GHOST_kSuccess) {
if (use_libdecor) {
/* Ensure there is a swap-buffers, needed for the updated window borders to refresh. */
notify_decor_redraw();
}
}
#endif
return success;
}
GHOST_TSuccess GHOST_WindowWayland::deactivate()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
/* Actual activation is handled when processing pending events. */
const bool is_main_thread = system_->main_thread_id == std::this_thread::get_id();
if (is_main_thread)
#endif
{
/* See code comments for #GHOST_WindowWayland::activate. */
if (window_->is_init) {
system_->getWindowManager()->setWindowInactive(this);
}
}
const GHOST_TSuccess success = system_->pushEvent_maybe_pending(
new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowDeactivate, this));
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (success == GHOST_kSuccess) {
if (use_libdecor) {
/* Ensure there is a swap-buffers, needed for the updated window borders to refresh. */
notify_decor_redraw();
}
}
#endif
return success;
}
GHOST_TSuccess GHOST_WindowWayland::notify_size()
{
return system_->pushEvent_maybe_pending(
new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowSize, this));
}
GHOST_TSuccess GHOST_WindowWayland::notify_decor_redraw()
{
/* NOTE: we want to `swapBuffers`, however this may run from a thread and
* when this windows OpenGL context is not active, so send and update event instead. */
return system_->pushEvent_maybe_pending(
new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowUpdateDecor, this));
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Utility Functions
*
* Functionality only used for the WAYLAND implementation.
* \{ */
GHOST_TSuccess GHOST_WindowWayland::cursor_shape_refresh()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
if (system_->main_thread_id != std::this_thread::get_id()) {
gwl_window_pending_actions_tag(window_, PENDING_WINDOW_CURSOR_SHAPE_REFRESH);
return GHOST_kSuccess;
}
#endif
return gwl_window_cursor_shape_refresh(cursor_shape_, window_->cursor_generator, system_);
}
void GHOST_WindowWayland::outputs_changed_update_scale_tag()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
/* NOTE: if deferring causes problems, it could be isolated to the first scale initialization
* See: #GWL_WindowFrame::is_scale_init. */
gwl_window_pending_actions_tag(window_, PENDING_OUTPUT_SCALE_UPDATE_DEFERRED);
#else
outputs_changed_update_scale();
#endif
}
bool GHOST_WindowWayland::outputs_changed_update_scale()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
if (system_->main_thread_id != std::this_thread::get_id()) {
gwl_window_pending_actions_tag(window_, PENDING_OUTPUT_SCALE_UPDATE);
return false;
}
#endif
int fractional_scale_next = -1;
int fractional_scale_from_output = 0;
int scale_next = outputs_max_scale_or_default(outputs_get(), 0, &fractional_scale_from_output);
if (UNLIKELY(scale_next == 0)) {
return false;
}
#ifdef USE_EVENT_BACKGROUND_THREAD
std::lock_guard lock_frame_guard{window_->frame_pending_mutex};
#endif
if (window_->wp.fractional_scale_handle) {
/* Let the #wp_fractional_scale_v1_listener::preferred_scale callback handle
* changes to the windows scale. */
if (window_->frame_pending.fractional_scale_preferred != 0) {
fractional_scale_next = window_->frame_pending.fractional_scale_preferred;
scale_next = fractional_scale_next / FRACTIONAL_DENOMINATOR;
}
}
if (fractional_scale_next == -1) {
fractional_scale_next = fractional_scale_from_output;
scale_next = fractional_scale_next / FRACTIONAL_DENOMINATOR;
}
bool changed = false;
const bool is_fractional_prev = window_->frame.fractional_scale != 0;
const bool is_fractional_next = (fractional_scale_next % FRACTIONAL_DENOMINATOR) != 0;
/* When non-fractional, never use fractional scaling! */
window_->frame_pending.fractional_scale = is_fractional_next ? fractional_scale_next : 0;
window_->frame_pending.buffer_scale = is_fractional_next ?
1 :
fractional_scale_next / FRACTIONAL_DENOMINATOR;
const int fractional_scale_prev = window_->frame.fractional_scale ?
window_->frame.fractional_scale :
window_->frame.buffer_scale * FRACTIONAL_DENOMINATOR;
const int scale_prev = fractional_scale_prev / FRACTIONAL_DENOMINATOR;
/* Resizing implies updating. */
bool do_frame_resize = false;
bool do_frame_update = false;
if (window_->frame_pending.is_scale_init == false) {
window_->frame_pending.is_scale_init = true;
/* NOTE(@ideasman42): Needed because new windows are created at their previous pixel-dimensions
* as the window doesn't save it's DPI. Restore the window size under the assumption it's
* opening on the same monitor so a window keeps it's previous size on a users system.
*
* To support anything more sophisticated, windows would need to be created with a scale
* argument (representing the scale used when the window was stored, for example). */
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
if (use_libdecor) {
/* LIBDECOR needs its own logic. Failing to do this causes the window border
* not to follow the GHOST window on startup - with multiple monitors,
* each with different fractional scale, see: #109194.
*
* Note that the window will show larger, then resize to be smaller soon
* after opening. This would be nice to avoid but would require DPI
* to be stored in the window (as noted above). */
int size_next[2] = {0, 0};
int size_orig[2] = {0, 0};
/* Leave `window_->frame_pending` as-is, only change the window frame. */
for (size_t i = 0; i < ARRAY_SIZE(window_->frame_pending.size); i++) {
const int value = size_next[i] ? window_->frame_pending.size[i] : window_->frame.size[i];
size_orig[i] = value;
if (is_fractional_prev || is_fractional_next) {
size_next[i] = lroundf((value * double(FRACTIONAL_DENOMINATOR)) /
double(fractional_scale_next));
}
else {
size_next[i] = value / scale_prev;
}
if (window_->frame_pending.buffer_scale > 1) {
gwl_round_int_by(&size_next[i], window_->frame_pending.buffer_scale);
}
}
if (size_orig[0] != size_next[0] || size_orig[1] != size_next[1]) {
GWL_LibDecor_Window &decor = *window_->libdecor;
libdecor_state *state = libdecor_state_new(UNPACK2(size_next));
libdecor_frame_commit(decor.frame, state, nullptr);
libdecor_state_free(state);
}
}
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */
/* Leave `window_->frame_pending` as-is, so changes are detected and updates are applied. */
do_frame_resize = false;
do_frame_update = true;
/* If the buffer scale changes, the window size (and underlying buffer-size)
* must always be a multiple of the buffer size. Resizing ensures this is the case.
* See replies to #135764 for details.
*
* NOTE: We could skip resize if the current window size is a multiple of the buffer scale,
* avoids this as it will result in unpredictable behavior based on single pixel differences
* in window size. */
if (window_->frame_pending.buffer_scale != window_->frame.buffer_scale) {
do_frame_resize = true;
}
}
else {
/* Test if the scale changed. */
if ((fractional_scale_prev != fractional_scale_next) ||
(window_->frame_pending.buffer_scale != window_->frame.buffer_scale))
{
do_frame_resize = true;
}
}
if (do_frame_resize) {
/* Resize the window failing to do so results in severe flickering with a
* multi-monitor setup when multiple monitors have different scales.
*
* NOTE: some flickering is still possible even when resizing this
* happens when dragging the right hand side of the title-bar in KDE
* as expanding changed the size on the RHS, this may be up to the compositor to fix. */
for (size_t i = 0; i < ARRAY_SIZE(window_->frame_pending.size); i++) {
const int value = window_->frame_pending.size[i] ? window_->frame_pending.size[i] :
window_->frame.size[i];
if (is_fractional_prev || is_fractional_next) {
window_->frame_pending.size[i] = lroundf((value * double(fractional_scale_next)) /
double(fractional_scale_prev));
}
else {
window_->frame_pending.size[i] = (value * scale_next) / scale_prev;
}
if (window_->frame_pending.buffer_scale > 1) {
gwl_round_int_by(&window_->frame_pending.size[i], window_->frame_pending.buffer_scale);
}
}
do_frame_update = true;
}
if (do_frame_update) {
gwl_window_frame_update_from_pending_no_lock(window_);
changed = true;
}
return changed;
}
bool GHOST_WindowWayland::outputs_enter(GWL_Output *output)
{
std::vector<GWL_Output *> &outputs = window_->outputs;
auto it = std::find(outputs.begin(), outputs.end(), output);
if (it != outputs.end()) {
return false;
}
outputs.push_back(output);
return true;
}
bool GHOST_WindowWayland::outputs_leave(GWL_Output *output)
{
std::vector<GWL_Output *> &outputs = window_->outputs;
auto it = std::find(outputs.begin(), outputs.end(), output);
if (it == outputs.end()) {
return false;
}
outputs.erase(it);
return true;
}
#ifdef USE_EVENT_BACKGROUND_THREAD
void GHOST_WindowWayland::pending_actions_handle()
{
/* Caller must lock `server_mutex`, while individual actions could lock,
* it's simpler to lock once when handling all window actions. */
GWL_Window *win = window_;
GHOST_ASSERT(win->ghost_system->main_thread_id == std::this_thread::get_id(),
"Run from main thread!");
gwl_window_pending_actions_handle(win);
}
#endif /* USE_EVENT_BACKGROUND_THREAD */
/** \} */