wayland: Add the ability to import and wrap external surfaces

Add the ability to import and wrap external surfaces from external toolkits such as Qt and GTK.

Wayland surfaces and windows are more intrinsically tied to the client library than other windowing systems, so it is necessary to provide a way to initialize SDL with an existing wl_display object, which needs to be set prior to video system initialization, or export the internal SDL wl_display object for use by external applications or toolkits. For this, the global property SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER is used.

A Wayland example was added to testnative, and a basic example of Qt 6 interoperation is provided in the Wayland readme to demonstrate the use of external windows with both SDL owning the wl_display, and an external toolkit owning it.
main
Frank Praznik 2024-01-09 11:38:38 -05:00
parent 99f6bcf504
commit 4f3d4bd110
11 changed files with 548 additions and 64 deletions

View File

@ -59,15 +59,165 @@ successfully created, the `wl_display` and `wl_surface` objects can then be retr
Surfaces don't receive any size change notifications, so if an application changes the window size, it must inform SDL Surfaces don't receive any size change notifications, so if an application changes the window size, it must inform SDL
that the surface size has changed by calling SDL_SetWindowSize() with the new dimensions. that the surface size has changed by calling SDL_SetWindowSize() with the new dimensions.
Custom surfaces will automatically handle scaling internally if the window was created with the `high-pixel-density` Custom surfaces will automatically handle scaling internally if the window was created with the
property set to `SDL_TRUE`. In this case, applications should not manually attach viewports or change the surface scale `SDL_PROPERTY_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN` property set to `SDL_TRUE`. In this case, applications should
value, as SDL will handle this internally. Calls to `SDL_SetWindowSize()` should use the logical size of the window, and not manually attach viewports or change the surface scale value, as SDL will handle this internally. Calls
`SDL_GetWindowSizeInPixels()` should be used to query the size of the backbuffer surface in pixels. If this property is to `SDL_SetWindowSize()` should use the logical size of the window, and `SDL_GetWindowSizeInPixels()` should be used to
not set or is `SDL_FALSE`, applications can attach their own viewports or change the surface scale manually, and the SDL query the size of the backbuffer surface in pixels. If this property is not set or is `SDL_FALSE`, applications can
backend will not interfere or change any values internally. In this case, calls to `SDL_SetWindowSize()` should pass the attach their own viewports or change the surface scale manually, and the SDL backend will not interfere or change any
requested surface size in pixels, not the logical window size, as no scaling calculations will be done internally. values internally. In this case, calls to `SDL_SetWindowSize()` should pass the requested surface size in pixels, not
the logical window size, as no scaling calculations will be done internally.
All window functions that control window state aside from `SDL_SetWindowSize()` are no-ops with custom surfaces. All window functions that control window state aside from `SDL_SetWindowSize()` are no-ops with custom surfaces.
Please see the minimal example in tests/testwaylandcustom.c for an example of how to use a custom, roleless surface and Please see the minimal example in `tests/testwaylandcustom.c` for an example of how to use a custom, roleless surface
attach it to an application-managed toplevel window. and attach it to an application-managed toplevel window.
## Importing external surfaces into SDL windows
Wayland windows and surfaces are more intrinsically tied to the client library than other windowing systems, therefore,
when importing surfaces, it is necessary for both SDL and the application or toolkit to use the same `wl_display`
object. This can be set/queried via the global `SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER` property. To
import an external `wl_display`, set this property before initializing the SDL video subsystem, and read the value to
export the internal `wl_display` after the video subsystem has been initialized. Setting this property after the video
subsystem has been initialized has no effect, and reading it when the video subsystem is uninitialized will either
return the user provided value, if one was set while in the uninitialized state, or NULL.
Once this is done, and the application has created or obtained the `wl_surface` to be wrapped in an `SDL_Window`, the
window is created with `SDL_CreateWindowWithProperties()` with the
`SDL_PROPERTY_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER` property to set to the `wl_surface` object that is to be
imported by SDL.
SDL receives no notification regarding size changes on external surfaces or toplevel windows, so if the external surface
needs to be resized, SDL must be informed by calling SDL_SetWindowSize() with the new dimensions.
If desired, SDL can automatically handle the scaling for the surface by setting the
`SDL_PROPERTY_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN` property to `SDL_TRUE`, however, if the surface being imported
already has, or will have, a viewport/fractional scale manager attached to it by the application or an external toolkit,
a protocol violation will result. Avoid setting this property if importing surfaces from toolkits such as Qt or GTK.
If the window is flagged as high pixel density, calls to `SDL_SetWindowSize()` should pass the logical size of the
window and `SDL_GetWindowSizeInPixels()` should be used to retrieve the backbuffer size in pixels. Otherwise, calls to
`SDL_SetWindowSize()` should pass the requested surface size in pixels, not the logical window size, as no scaling
calculations will be done internally.
All window functions that control window state aside from `SDL_SetWindowSize()` are no-ops with external surfaces.
An example of how to use external surfaces with a `wl_display` owned by SDL can be seen in `tests/testnativewayland.c`,
and the following is a minimal example of interoperation with Qt 6, with Qt owning the `wl_display`:
```c++
#include <QApplication>
#include <QWindow>
#include <qpa/qplatformnativeinterface.h>
#include <SDL3/SDL.h>
int main(int argc, char *argv[])
{
int ret = -1;
int done = 0;
SDL_PropertiesID props;
SDL_Event e;
SDL_Window *sdlWindow = NULL;
SDL_Renderer *sdlRenderer = NULL;
struct wl_display *display = NULL;
struct wl_surface *surface = NULL;
/* Initialize Qt */
QApplication qtApp(argc, argv);
QWindow qtWindow;
/* The windowing system must be Wayland. */
if (QApplication::platformName() != "wayland") {
goto exit;
}
{
/* Get the wl_display object from Qt */
QNativeInterface::QWaylandApplication *qtWlApp = qtApp.nativeInterface<QNativeInterface::QWaylandApplication>();
display = qtWlApp->display();
if (!display) {
goto exit;
}
}
/* Set SDL to use the existing wl_display object from Qt and initialize. */
SDL_SetProperty(SDL_GetGlobalProperties(), SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER, display);
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
/* Create a basic, frameless QWindow */
qtWindow.setFlags(Qt::FramelessWindowHint);
qtWindow.setGeometry(0, 0, 640, 480);
qtWindow.show();
{
/* Get the native wl_surface backing resource for the window */
QPlatformNativeInterface *qtNative = qtApp.platformNativeInterface();
surface = (struct wl_surface *)qtNative->nativeResourceForWindow("surface", &qtWindow);
if (!surface) {
goto exit;
}
}
/* Create a window that wraps the wl_surface from the QWindow.
* Qt objects should not be flagged as DPI-aware or protocol violations will result.
*/
props = SDL_CreateProperties();
SDL_SetProperty(props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER, surface);
SDL_SetBooleanProperty(props, SDL_PROPERTY_WINDOW_CREATE_OPENGL_BOOLEAN, SDL_TRUE);
SDL_SetNumberProperty(props, SDL_PROPERTY_WINDOW_CREATE_WIDTH_NUMBER, 640);
SDL_SetNumberProperty(props, SDL_PROPERTY_WINDOW_CREATE_HEIGHT_NUMBER, 480);
sdlWindow = SDL_CreateWindowWithProperties(props);
SDL_DestroyProperties(props);
if (!sdlWindow) {
goto exit;
}
/* Create a renderer */
sdlRenderer = SDL_CreateRenderer(sdlWindow, NULL, 0);
if (!sdlRenderer) {
goto exit;
}
/* Draw a blue screen for the window until ESC is pressed or the window is no longer visible. */
while (!done) {
while (SDL_PollEvent(&e)) {
if (e.type == SDL_EVENT_KEY_DOWN && e.key.keysym.sym == SDLK_ESCAPE) {
done = 1;
}
}
qtApp.processEvents();
/* Update the backbuffer size if the window scale changed. */
qreal scale = qtWindow.devicePixelRatio();
SDL_SetWindowSize(sdlWindow, SDL_lround(640. * scale), SDL_lround(480. * scale));
if (qtWindow.isVisible()) {
SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 255, 255);
SDL_RenderClear(sdlRenderer);
SDL_RenderPresent(sdlRenderer);
} else {
done = 1;
}
}
ret = 0;
exit:
/* Cleanup */
if (sdlRenderer) {
SDL_DestroyRenderer(sdlRenderer);
}
if (sdlWindow) {
SDL_DestroyWindow(sdlWindow);
}
SDL_Quit();
return ret;
}
```

View File

@ -44,6 +44,22 @@ extern "C" {
typedef Uint32 SDL_DisplayID; typedef Uint32 SDL_DisplayID;
typedef Uint32 SDL_WindowID; typedef Uint32 SDL_WindowID;
/**
* Global video properties
*
* - `SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER`: the pointer to
* the global `wl_display` object used by the Wayland video backend. Can be
* set before the video subsystem is initialized to import an external
* `wl_display` object from an application or toolkit for use in SDL, or
* read after initialization to export the `wl_display` used by the
* Wayland video backend. Setting this property after the video subsystem
* has been initialized has no effect, and reading it when the video
* subsystem is uninitialized will either return the user provided value,
* if one was set prior to initialization, or NULL. See
* docs/README-wayland.md for more information.
*/
#define SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER "video.wayland.wl_display"
/** /**
* System theme * System theme
*/ */
@ -875,6 +891,9 @@ extern DECLSPEC SDL_Window *SDLCALL SDL_CreatePopupWindow(SDL_Window *parent, in
* - `SDL_PROPERTY_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN - true if * - `SDL_PROPERTY_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN - true if
* the application wants an associated `wl_egl_window` object to be created, * the application wants an associated `wl_egl_window` object to be created,
* even if the window does not have the OpenGL property or flag set. * even if the window does not have the OpenGL property or flag set.
* - `SDL_PROPERTY_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER` - the wl_surface
* associated with the window, if you want to wrap an existing window. See
* docs/README-wayland.md for more information.
* *
* These are additional supported properties on Windows: * These are additional supported properties on Windows:
* *
@ -931,6 +950,7 @@ extern DECLSPEC SDL_Window *SDLCALL SDL_CreateWindowWithProperties(SDL_Propertie
#define SDL_PROPERTY_WINDOW_CREATE_COCOA_VIEW_POINTER "cocoa.view" #define SDL_PROPERTY_WINDOW_CREATE_COCOA_VIEW_POINTER "cocoa.view"
#define SDL_PROPERTY_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN "wayland.surface_role_custom" #define SDL_PROPERTY_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN "wayland.surface_role_custom"
#define SDL_PROPERTY_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN "wayland.create_egl_window" #define SDL_PROPERTY_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN "wayland.create_egl_window"
#define SDL_PROPERTY_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER "wayland.wl_surface"
#define SDL_PROPERTY_WINDOW_CREATE_WIN32_HWND_POINTER "win32.hwnd" #define SDL_PROPERTY_WINDOW_CREATE_WIN32_HWND_POINTER "win32.hwnd"
#define SDL_PROPERTY_WINDOW_CREATE_WIN32_PIXEL_FORMAT_HWND_POINTER "win32.pixel_format_hwnd" #define SDL_PROPERTY_WINDOW_CREATE_WIN32_PIXEL_FORMAT_HWND_POINTER "win32.pixel_format_hwnd"
#define SDL_PROPERTY_WINDOW_CREATE_X11_WINDOW_NUMBER "x11.window" #define SDL_PROPERTY_WINDOW_CREATE_X11_WINDOW_NUMBER "x11.window"

View File

@ -539,18 +539,12 @@ static void pointer_handle_enter(void *data, struct wl_pointer *pointer,
return; return;
} }
/* check that this surface belongs to one of the SDL windows */
if (!SDL_WAYLAND_own_surface(surface)) {
return;
}
/* This handler will be called twice in Wayland 1.4 /* This handler will be called twice in Wayland 1.4
* Once for the window surface which has valid user data * Once for the window surface which has valid user data
* and again for the mouse cursor surface which does not have valid user data * and again for the mouse cursor surface which does not have valid user data
* We ignore the later * We ignore the later
*/ */
window = Wayland_GetWindowDataForOwnedSurface(surface);
window = (SDL_WindowData *)wl_surface_get_user_data(surface);
if (window) { if (window) {
input->pointer_focus = window; input->pointer_focus = window;
@ -576,12 +570,12 @@ static void pointer_handle_leave(void *data, struct wl_pointer *pointer,
{ {
struct SDL_WaylandInput *input = data; struct SDL_WaylandInput *input = data;
if (!surface || !SDL_WAYLAND_own_surface(surface)) { if (!surface) {
return; return;
} }
if (input->pointer_focus) { if (input->pointer_focus) {
SDL_WindowData *wind = (SDL_WindowData *)wl_surface_get_user_data(surface); SDL_WindowData *wind = Wayland_GetWindowDataForOwnedSurface(surface);
if (wind) { if (wind) {
/* Clear the capture flag and raise all buttons */ /* Clear the capture flag and raise all buttons */
@ -980,14 +974,14 @@ static void touch_handler_down(void *data, struct wl_touch *touch, uint32_t seri
struct SDL_WaylandInput *input = (struct SDL_WaylandInput *)data; struct SDL_WaylandInput *input = (struct SDL_WaylandInput *)data;
SDL_WindowData *window_data; SDL_WindowData *window_data;
/* Check that this surface belongs to one of the SDL windows */ /* Check that this surface is valid. */
if (!SDL_WAYLAND_own_surface(surface)) { if (!surface) {
return; return;
} }
touch_add(id, fx, fy, surface); touch_add(id, fx, fy, surface);
Wayland_UpdateImplicitGrabSerial(input, serial); Wayland_UpdateImplicitGrabSerial(input, serial);
window_data = (SDL_WindowData *)wl_surface_get_user_data(surface); window_data = Wayland_GetWindowDataForOwnedSurface(surface);
if (window_data) { if (window_data) {
float x, y; float x, y;
@ -1430,19 +1424,18 @@ static void keyboard_handle_enter(void *data, struct wl_keyboard *keyboard,
return; return;
} }
if (!SDL_WAYLAND_own_surface(surface)) { window = Wayland_GetWindowDataForOwnedSurface(surface);
if (!window) {
return; return;
} }
window = wl_surface_get_user_data(surface);
if (window) {
input->keyboard_focus = window; input->keyboard_focus = window;
window->keyboard_device = input; window->keyboard_device = input;
/* Restore the keyboard focus to the child popup that was holding it */ /* Restore the keyboard focus to the child popup that was holding it */
SDL_SetKeyboardFocus(window->keyboard_focus ? window->keyboard_focus : window->sdlwindow); SDL_SetKeyboardFocus(window->keyboard_focus ? window->keyboard_focus : window->sdlwindow);
}
#ifdef SDL_USE_IME #ifdef SDL_USE_IME
if (!input->text_input) { if (!input->text_input) {
SDL_IME_SetFocus(SDL_TRUE); SDL_IME_SetFocus(SDL_TRUE);
@ -1479,16 +1472,18 @@ static void keyboard_handle_leave(void *data, struct wl_keyboard *keyboard,
SDL_WindowData *wind; SDL_WindowData *wind;
SDL_Window *window = NULL; SDL_Window *window = NULL;
if (!surface || !SDL_WAYLAND_own_surface(surface)) { if (!surface) {
return;
}
wind = Wayland_GetWindowDataForOwnedSurface(surface);
if (!wind) {
return; return;
} }
wind = wl_surface_get_user_data(surface);
if (wind) {
wind->keyboard_device = NULL; wind->keyboard_device = NULL;
window = wind->sdlwindow; window = wind->sdlwindow;
window->flags &= ~SDL_WINDOW_MOUSE_CAPTURE; window->flags &= ~SDL_WINDOW_MOUSE_CAPTURE;
}
/* Stop key repeat before clearing keyboard focus */ /* Stop key repeat before clearing keyboard focus */
keyboard_repeat_clear(&input->keyboard_repeat); keyboard_repeat_clear(&input->keyboard_repeat);
@ -1935,11 +1930,9 @@ static void data_device_handle_enter(void *data, struct wl_data_device *wl_data_
/* find the current window */ /* find the current window */
if (surface) { if (surface) {
if (SDL_WAYLAND_own_surface(surface)) { SDL_WindowData *window = Wayland_GetWindowDataForOwnedSurface(surface);
SDL_WindowData *window = (SDL_WindowData *)wl_surface_get_user_data(surface);
if (window) { if (window) {
data_device->dnd_window = window->sdlwindow; data_device->dnd_window = window->sdlwindow;
}
} else { } else {
data_device->dnd_window = NULL; data_device->dnd_window = NULL;
} }
@ -2609,11 +2602,7 @@ static void tablet_tool_handle_proximity_in(void *data, struct zwp_tablet_tool_v
return; return;
} }
if (!SDL_WAYLAND_own_surface(surface)) { window = Wayland_GetWindowDataForOwnedSurface(surface);
return;
}
window = (SDL_WindowData *)wl_surface_get_user_data(surface);
if (window) { if (window) {
input->tool_focus = window; input->tool_focus = window;

View File

@ -106,12 +106,45 @@ SDL_bool SDL_WAYLAND_own_output(struct wl_output *output)
return wl_proxy_get_tag((struct wl_proxy *)output) == &SDL_WAYLAND_output_tag; return wl_proxy_get_tag((struct wl_proxy *)output) == &SDL_WAYLAND_output_tag;
} }
/* External surfaces may have their own user data attached, the modification of which
* can cause problems with external toolkits. Instead, external windows are kept in
* their own list, and a search is conducted to find a matching surface.
*/
static struct wl_list external_window_list;
void Wayland_AddWindowDataToExternalList(SDL_WindowData *data)
{
WAYLAND_wl_list_insert(&external_window_list, &data->external_window_list_link);
}
void Wayland_RemoveWindowDataFromExternalList(SDL_WindowData *data)
{
WAYLAND_wl_list_remove(&data->external_window_list_link);
}
SDL_WindowData *Wayland_GetWindowDataForOwnedSurface(struct wl_surface *surface)
{
if (SDL_WAYLAND_own_surface(surface)) {
return (SDL_WindowData *)wl_surface_get_user_data(surface);
} else if (!WAYLAND_wl_list_empty(&external_window_list)) {
SDL_WindowData *p;
wl_list_for_each (p, &external_window_list, external_window_list_link) {
if (p->surface == surface) {
return p;
}
}
}
return NULL;
}
static void Wayland_DeleteDevice(SDL_VideoDevice *device) static void Wayland_DeleteDevice(SDL_VideoDevice *device)
{ {
SDL_VideoData *data = device->driverdata; SDL_VideoData *data = device->driverdata;
if (data->display) { if (data->display && !data->display_externally_owned) {
WAYLAND_wl_display_flush(data->display); WAYLAND_wl_display_flush(data->display);
WAYLAND_wl_display_disconnect(data->display); WAYLAND_wl_display_disconnect(data->display);
SDL_ClearProperty(SDL_GetGlobalProperties(), SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER);
} }
if (device->wakeup_lock) { if (device->wakeup_lock) {
SDL_DestroyMutex(device->wakeup_lock); SDL_DestroyMutex(device->wakeup_lock);
@ -125,7 +158,9 @@ static SDL_VideoDevice *Wayland_CreateDevice(void)
{ {
SDL_VideoDevice *device; SDL_VideoDevice *device;
SDL_VideoData *data; SDL_VideoData *data;
struct wl_display *display; struct wl_display *display = SDL_GetProperty(SDL_GetGlobalProperties(),
SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER, NULL);
SDL_bool display_is_external = !!display;
/* Are we trying to connect to or are currently in a Wayland session? */ /* Are we trying to connect to or are currently in a Wayland session? */
if (!SDL_getenv("WAYLAND_DISPLAY")) { if (!SDL_getenv("WAYLAND_DISPLAY")) {
@ -139,11 +174,13 @@ static SDL_VideoDevice *Wayland_CreateDevice(void)
return NULL; return NULL;
} }
if (!display) {
display = WAYLAND_wl_display_connect(NULL); display = WAYLAND_wl_display_connect(NULL);
if (!display) { if (!display) {
SDL_WAYLAND_UnloadSymbols(); SDL_WAYLAND_UnloadSymbols();
return NULL; return NULL;
} }
}
data = SDL_calloc(1, sizeof(*data)); data = SDL_calloc(1, sizeof(*data));
if (!data) { if (!data) {
@ -154,7 +191,9 @@ static SDL_VideoDevice *Wayland_CreateDevice(void)
data->initializing = SDL_TRUE; data->initializing = SDL_TRUE;
data->display = display; data->display = display;
data->display_externally_owned = display_is_external;
WAYLAND_wl_list_init(&data->output_list); WAYLAND_wl_list_init(&data->output_list);
WAYLAND_wl_list_init(&external_window_list);
/* Initialize all variables that we clean on shutdown */ /* Initialize all variables that we clean on shutdown */
device = SDL_calloc(1, sizeof(SDL_VideoDevice)); device = SDL_calloc(1, sizeof(SDL_VideoDevice));
@ -165,6 +204,11 @@ static SDL_VideoDevice *Wayland_CreateDevice(void)
return NULL; return NULL;
} }
if (!display_is_external) {
SDL_SetProperty(SDL_GetGlobalProperties(),
SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER, display);
}
device->driverdata = data; device->driverdata = data;
device->wakeup_lock = SDL_CreateMutex(); device->wakeup_lock = SDL_CreateMutex();

View File

@ -79,6 +79,7 @@ struct SDL_VideoData
struct wl_list output_list; struct wl_list output_list;
int relative_mouse_mode; int relative_mouse_mode;
SDL_bool display_externally_owned;
}; };
struct SDL_DisplayData struct SDL_DisplayData
@ -107,6 +108,10 @@ extern void SDL_WAYLAND_register_output(struct wl_output *output);
extern SDL_bool SDL_WAYLAND_own_surface(struct wl_surface *surface); extern SDL_bool SDL_WAYLAND_own_surface(struct wl_surface *surface);
extern SDL_bool SDL_WAYLAND_own_output(struct wl_output *output); extern SDL_bool SDL_WAYLAND_own_output(struct wl_output *output);
extern SDL_WindowData *Wayland_GetWindowDataForOwnedSurface(struct wl_surface *surface);
void Wayland_AddWindowDataToExternalList(SDL_WindowData *data);
void Wayland_RemoveWindowDataFromExternalList(SDL_WindowData *data);
extern SDL_bool Wayland_LoadLibdecor(SDL_VideoData *data, SDL_bool ignore_xdg); extern SDL_bool Wayland_LoadLibdecor(SDL_VideoData *data, SDL_bool ignore_xdg);
extern SDL_bool Wayland_VideoReconnect(SDL_VideoDevice *_this); extern SDL_bool Wayland_VideoReconnect(SDL_VideoDevice *_this);

View File

@ -2057,10 +2057,13 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
{ {
SDL_WindowData *data; SDL_WindowData *data;
SDL_VideoData *c; SDL_VideoData *c;
const SDL_bool custom_surface_role = SDL_GetBooleanProperty(create_props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN, SDL_FALSE); struct wl_surface *external_surface = (struct wl_surface *)SDL_GetProperty(create_props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER,
(struct wl_surface *)SDL_GetProperty(create_props, "sdl2-compat.external_window", NULL));
const SDL_bool custom_surface_role = (external_surface != NULL) || SDL_GetBooleanProperty(create_props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN, SDL_FALSE);
const SDL_bool create_egl_window = !!(window->flags & SDL_WINDOW_OPENGL) || const SDL_bool create_egl_window = !!(window->flags & SDL_WINDOW_OPENGL) ||
SDL_GetBooleanProperty(create_props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN, SDL_FALSE); SDL_GetBooleanProperty(create_props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN, SDL_FALSE);
data = SDL_calloc(1, sizeof(*data)); data = SDL_calloc(1, sizeof(*data));
if (!data) { if (!data) {
return -1; return -1;
@ -2102,10 +2105,20 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
data->requested_window_width = window->w; data->requested_window_width = window->w;
data->requested_window_height = window->h; data->requested_window_height = window->h;
if (!external_surface) {
data->surface = wl_compositor_create_surface(c->compositor); data->surface = wl_compositor_create_surface(c->compositor);
wl_surface_add_listener(data->surface, &surface_listener, data); wl_surface_add_listener(data->surface, &surface_listener, data);
wl_surface_set_user_data(data->surface, data);
SDL_WAYLAND_register_surface(data->surface); SDL_WAYLAND_register_surface(data->surface);
} else {
window->flags |= SDL_WINDOW_EXTERNAL;
data->surface = external_surface;
/* External surfaces are registered by being put in a list, as changing tags or userdata
* can cause problems with external toolkits.
*/
Wayland_AddWindowDataToExternalList(data);
}
/* Must be called before EGL configuration to set the drawable backbuffer size. */ /* Must be called before EGL configuration to set the drawable backbuffer size. */
ConfigureWindowGeometry(window); ConfigureWindowGeometry(window);
@ -2123,9 +2136,12 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
wl_callback_add_listener(data->gles_swap_frame_callback, &gles_swap_frame_listener, data); wl_callback_add_listener(data->gles_swap_frame_callback, &gles_swap_frame_listener, data);
} }
/* No frame callback on external surfaces as it may already have one attached. */
if (!external_surface) {
/* Fire a callback when the compositor wants a new frame to set the surface damage region. */ /* Fire a callback when the compositor wants a new frame to set the surface damage region. */
data->surface_frame_callback = wl_surface_frame(data->surface); data->surface_frame_callback = wl_surface_frame(data->surface);
wl_callback_add_listener(data->surface_frame_callback, &surface_frame_listener, data); wl_callback_add_listener(data->surface_frame_callback, &surface_frame_listener, data);
}
if (window->flags & SDL_WINDOW_TRANSPARENT) { if (window->flags & SDL_WINDOW_TRANSPARENT) {
if (_this->gl_config.alpha_size == 0) { if (_this->gl_config.alpha_size == 0) {
@ -2152,7 +2168,13 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
Wayland_input_lock_pointer(c->input); Wayland_input_lock_pointer(c->input);
} }
if (c->fractional_scale_manager) { /* Don't attach a fractional scale manager to surfaces unless they are
* flagged as DPI-aware. Under non-scaled operation, the scale will
* always be 1.0, and external/custom surfaces may already have, or
* will try to attach, their own fractional scale manager, which will
* result in a protocol violation.
*/
if (c->fractional_scale_manager && (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY)) {
data->fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale(c->fractional_scale_manager, data->surface); data->fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale(c->fractional_scale_manager, data->surface);
wp_fractional_scale_v1_add_listener(data->fractional_scale, wp_fractional_scale_v1_add_listener(data->fractional_scale,
&fractional_scale_listener, data); &fractional_scale_listener, data);
@ -2175,7 +2197,7 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
} }
} /* All other cases will be WAYLAND_SURFACE_UNKNOWN */ } /* All other cases will be WAYLAND_SURFACE_UNKNOWN */
} else { } else {
/* Roleless surfaces are always considered to be in the shown state by the backend. */ /* Roleless and external surfaces are always considered to be in the shown state by the backend. */
data->shell_surface_type = WAYLAND_SURFACE_CUSTOM; data->shell_surface_type = WAYLAND_SURFACE_CUSTOM;
data->surface_status = WAYLAND_SURFACE_STATUS_SHOWN; data->surface_status = WAYLAND_SURFACE_STATUS_SHOWN;
} }
@ -2433,7 +2455,11 @@ void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window)
wl_callback_destroy(wind->surface_frame_callback); wl_callback_destroy(wind->surface_frame_callback);
} }
if (!(window->flags & SDL_WINDOW_EXTERNAL)) {
wl_surface_destroy(wind->surface); wl_surface_destroy(wind->surface);
} else {
Wayland_RemoveWindowDataFromExternalList(wind);
}
SDL_free(wind); SDL_free(wind);
WAYLAND_wl_display_flush(data->display); WAYLAND_wl_display_flush(data->display);

View File

@ -130,6 +130,8 @@ struct SDL_WindowData
SDL_bool show_hide_sync_required; SDL_bool show_hide_sync_required;
SDL_HitTestResult hit_test_result; SDL_HitTestResult hit_test_result;
struct wl_list external_window_list_link;
}; };
extern void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window); extern void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window);

View File

@ -288,9 +288,20 @@ if(APPLE)
endif() endif()
elseif(WINDOWS) elseif(WINDOWS)
add_sdl_test_executable(testnative BUILD_DEPENDENT NEEDS_RESOURCES TESTUTILS SOURCES testnative.c testnativew32.c) add_sdl_test_executable(testnative BUILD_DEPENDENT NEEDS_RESOURCES TESTUTILS SOURCES testnative.c testnativew32.c)
elseif(HAVE_X11) elseif(HAVE_X11 OR HAVE_WAYLAND)
add_sdl_test_executable(testnative BUILD_DEPENDENT NEEDS_RESOURCES TESTUTILS SOURCES testnative.c testnativex11.c) add_sdl_test_executable(testnative BUILD_DEPENDENT NO_C90 NEEDS_RESOURCES TESTUTILS SOURCES testnative.c)
if(HAVE_X11)
target_sources(testnative PRIVATE testnativex11.c)
target_link_libraries(testnative PRIVATE X11) target_link_libraries(testnative PRIVATE X11)
endif()
if(HAVE_WAYLAND)
set_property(SOURCE ${SDL3_BINARY_DIR}/wayland-generated-protocols/xdg-shell-protocol.c PROPERTY GENERATED 1)
target_sources(testnative PRIVATE testnativewayland.c ${SDL3_BINARY_DIR}/wayland-generated-protocols/xdg-shell-protocol.c)
# Needed to silence the documentation warning in the generated header file
target_compile_options(testnative PRIVATE -Wno-documentation-unknown-command)
target_link_libraries(testnative PRIVATE wayland-client)
endif ()
endif() endif()
find_package(Python3) find_package(Python3)

View File

@ -30,6 +30,9 @@ static NativeWindowFactory *factories[] = {
#ifdef TEST_NATIVE_WINDOWS #ifdef TEST_NATIVE_WINDOWS
&WindowsWindowFactory, &WindowsWindowFactory,
#endif #endif
#ifdef TEST_NATIVE_WAYLAND
&WaylandWindowFactory,
#endif
#ifdef TEST_NATIVE_X11 #ifdef TEST_NATIVE_X11
&X11WindowFactory, &X11WindowFactory,
#endif #endif
@ -47,10 +50,10 @@ static SDLTest_CommonState *state;
static void static void
quit(int rc) quit(int rc)
{ {
SDL_Quit();
if (native_window && factory) { if (native_window && factory) {
factory->DestroyNativeWindow(native_window); factory->DestroyNativeWindow(native_window);
} }
SDL_Quit();
SDLTest_CommonDestroyState(state); SDLTest_CommonDestroyState(state);
/* Let 'main()' return normally */ /* Let 'main()' return normally */
if (rc != 0) { if (rc != 0) {
@ -149,6 +152,9 @@ int main(int argc, char *argv[])
} }
props = SDL_CreateProperties(); props = SDL_CreateProperties();
SDL_SetProperty(props, "sdl2-compat.external_window", native_window); SDL_SetProperty(props, "sdl2-compat.external_window", native_window);
SDL_SetBooleanProperty(props, SDL_PROPERTY_WINDOW_CREATE_OPENGL_BOOLEAN, SDL_TRUE);
SDL_SetNumberProperty(props, SDL_PROPERTY_WINDOW_CREATE_WIDTH_NUMBER, WINDOW_W);
SDL_SetNumberProperty(props, SDL_PROPERTY_WINDOW_CREATE_HEIGHT_NUMBER, WINDOW_H);
window = SDL_CreateWindowWithProperties(props); window = SDL_CreateWindowWithProperties(props);
SDL_DestroyProperties(props); SDL_DestroyProperties(props);
if (!window) { if (!window) {

View File

@ -36,6 +36,11 @@ typedef struct
extern NativeWindowFactory WindowsWindowFactory; extern NativeWindowFactory WindowsWindowFactory;
#endif #endif
#ifdef SDL_VIDEO_DRIVER_WAYLAND
#define TEST_NATIVE_WAYLAND
extern NativeWindowFactory WaylandWindowFactory;
#endif
#ifdef SDL_VIDEO_DRIVER_X11 #ifdef SDL_VIDEO_DRIVER_X11
#define TEST_NATIVE_X11 #define TEST_NATIVE_X11
extern NativeWindowFactory X11WindowFactory; extern NativeWindowFactory X11WindowFactory;

226
test/testnativewayland.c Normal file
View File

@ -0,0 +1,226 @@
/*
Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely.
*/
#include "testnative.h"
#ifdef TEST_NATIVE_WAYLAND
#include <SDL3/SDL.h>
#include <wayland-client.h>
#include <xdg-shell-client-protocol.h>
static void *native_userdata_ptr = (void *)0xBAADF00D;
static const char *native_surface_tag = "SDL_NativeSurfaceTag";
static void *CreateWindowWayland(int w, int h);
static void DestroyWindowWayland(void *window);
NativeWindowFactory WaylandWindowFactory = {
"wayland",
CreateWindowWayland,
DestroyWindowWayland
};
/* Encapsulated in a struct to silence shadow variable warnings */
static struct _state
{
struct wl_display *wl_display;
struct wl_registry *wl_registry;
struct wl_compositor *wl_compositor;
struct xdg_wm_base *xdg_wm_base;
struct wl_surface *wl_surface;
struct xdg_surface *xdg_surface;
struct xdg_toplevel *xdg_toplevel;
} state;
static void xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial)
{
xdg_surface_ack_configure(state.xdg_surface, serial);
}
static const struct xdg_surface_listener xdg_surface_listener = {
.configure = xdg_surface_configure,
};
static void xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height, struct wl_array *states)
{
/* NOP */
}
static void xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel)
{
SDL_Event event;
SDL_zero(event);
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
static void xdg_toplevel_configure_bounds(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height)
{
/* NOP */
}
static void xdg_toplevel_wm_capabilities(void *data, struct xdg_toplevel *xdg_toplevel, struct wl_array *capabilities)
{
/* NOP */
}
static const struct xdg_toplevel_listener xdg_toplevel_listener = {
.configure = xdg_toplevel_configure,
.close = xdg_toplevel_close,
.configure_bounds = xdg_toplevel_configure_bounds,
.wm_capabilities = xdg_toplevel_wm_capabilities
};
static void xdg_wm_base_ping(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial)
{
xdg_wm_base_pong(state.xdg_wm_base, serial);
}
static const struct xdg_wm_base_listener xdg_wm_base_listener = {
.ping = xdg_wm_base_ping,
};
static void registry_global(void *data, struct wl_registry *wl_registry, uint32_t name, const char *interface, uint32_t version)
{
if (SDL_strcmp(interface, wl_compositor_interface.name) == 0) {
state.wl_compositor = wl_registry_bind(state.wl_registry, name, &wl_compositor_interface, SDL_min(version, 4));
} else if (SDL_strcmp(interface, xdg_wm_base_interface.name) == 0) {
state.xdg_wm_base = wl_registry_bind(state.wl_registry, name, &xdg_wm_base_interface, 1);
xdg_wm_base_add_listener(state.xdg_wm_base, &xdg_wm_base_listener, NULL);
}
}
static void registry_global_remove(void *data, struct wl_registry *wl_registry, uint32_t name)
{
/* NOP */
}
static const struct wl_registry_listener wl_registry_listener = {
.global = registry_global,
.global_remove = registry_global_remove,
};
static void *CreateWindowWayland(int w, int h)
{
/* Export the display object from SDL and use it to create a registry object,
* which will enumerate the wl_compositor and xdg_wm_base protocols.
*/
state.wl_display = SDL_GetProperty(SDL_GetGlobalProperties(), SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER, NULL);
if (!state.wl_display) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Invalid 'wl_display' object!");
goto error;
}
state.wl_registry = wl_display_get_registry(state.wl_display);
wl_registry_add_listener(state.wl_registry, &wl_registry_listener, NULL);
/* Roundtrip to enumerate registry objects. */
wl_display_roundtrip(state.wl_display);
/* Protocol sanity check */
if (!state.wl_compositor) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "'wl_compositor' protocol not found!");
goto error;
}
if (!state.xdg_wm_base) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "'xdg_wm_base' protocol not found!");
goto error;
}
/* Crate the backing wl_surface for the window. */
state.wl_surface = wl_compositor_create_surface(state.wl_compositor);
/* Set the native tag and userdata values, which should be the same at exit. */
wl_proxy_set_tag((struct wl_proxy *)state.wl_surface, &native_surface_tag);
wl_surface_set_user_data(state.wl_surface, native_userdata_ptr);
/* Create the xdg_surface from the wl_surface. */
state.xdg_surface = xdg_wm_base_get_xdg_surface(state.xdg_wm_base, state.wl_surface);
xdg_surface_add_listener(state.xdg_surface, &xdg_surface_listener, NULL);
/* Create the xdg_toplevel from the xdg_surface. */
state.xdg_toplevel = xdg_surface_get_toplevel(state.xdg_surface);
xdg_toplevel_add_listener(state.xdg_toplevel, &xdg_toplevel_listener, NULL);
xdg_toplevel_set_title(state.xdg_toplevel, "Native Wayland Window");
/* Return the wl_surface to be wrapped in an SDL_Window. */
return state.wl_surface;
error:
if (state.xdg_toplevel) {
xdg_toplevel_destroy(state.xdg_toplevel);
state.xdg_toplevel = NULL;
}
if (state.xdg_surface) {
xdg_surface_destroy(state.xdg_surface);
state.xdg_surface = NULL;
}
if (state.wl_surface) {
wl_surface_destroy(state.wl_surface);
state.wl_surface = NULL;
}
if (state.xdg_wm_base) {
xdg_wm_base_destroy(state.xdg_wm_base);
state.xdg_wm_base = NULL;
}
if (state.wl_compositor) {
wl_compositor_destroy(state.wl_compositor);
state.wl_compositor = NULL;
}
if (state.wl_registry) {
wl_registry_destroy(state.wl_registry);
state.wl_registry = NULL;
}
return NULL;
}
static void DestroyWindowWayland(void *window)
{
if (state.xdg_toplevel) {
xdg_toplevel_destroy(state.xdg_toplevel);
state.xdg_toplevel = NULL;
}
if (state.xdg_surface) {
xdg_surface_destroy(state.xdg_surface);
state.xdg_surface = NULL;
}
if (state.wl_surface) {
/* Surface sanity check; these should be unmodified. */
if (wl_proxy_get_tag((struct wl_proxy *)state.wl_surface) != &native_surface_tag) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "The wl_surface tag was modified, this indicates a problem inside of SDL.");
}
if (wl_surface_get_user_data(state.wl_surface) != native_userdata_ptr) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "The wl_surface user data was modified, this indicates a problem inside of SDL.");
}
wl_surface_destroy(state.wl_surface);
state.wl_surface = NULL;
}
if (state.xdg_wm_base) {
xdg_wm_base_destroy(state.xdg_wm_base);
state.xdg_wm_base = NULL;
}
if (state.wl_compositor) {
wl_compositor_destroy(state.wl_compositor);
state.wl_compositor = NULL;
}
if (state.wl_registry) {
wl_registry_destroy(state.wl_registry);
state.wl_registry = NULL;
}
}
#endif