1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-25 15:37:42 +00:00

Wayland: Implement game embedding

This patch introduces a new protocol proxy, which multiplxes Wayland
clients into a single connection, allowing us to redirect calls (e.g.
create toplevel -> create subsurface). Mixed with some state tracking
and emulation, we can embed a full-featured client into the editor.
This commit is contained in:
Dery Almas
2025-11-15 23:38:41 +01:00
parent ef34c3d534
commit bbf65ae72f
25 changed files with 6053 additions and 23 deletions

View File

@@ -662,6 +662,13 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re
if (strcmp(interface, FIFO_INTERFACE_NAME) == 0) {
registry->wp_fifo_manager_name = name;
}
if (strcmp(interface, godot_embedding_compositor_interface.name) == 0) {
registry->godot_embedding_compositor = (struct godot_embedding_compositor *)wl_registry_bind(wl_registry, name, &godot_embedding_compositor_interface, 1);
registry->godot_embedding_compositor_name = name;
godot_embedding_compositor_add_listener(registry->godot_embedding_compositor, &godot_embedding_compositor_listener, memnew(EmbeddingCompositorState));
}
}
void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry *wl_registry, uint32_t name) {
@@ -1092,6 +1099,25 @@ void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry
if (name == registry->wp_fifo_manager_name) {
registry->wp_fifo_manager_name = 0;
}
if (name == registry->godot_embedding_compositor_name) {
registry->godot_embedding_compositor_name = 0;
EmbeddingCompositorState *es = godot_embedding_compositor_get_state(registry->godot_embedding_compositor);
ERR_FAIL_NULL(es);
es->mapped_clients.clear();
for (struct godot_embedded_client *client : es->clients) {
godot_embedded_client_destroy(client);
}
es->clients.clear();
memdelete(es);
godot_embedding_compositor_destroy(registry->godot_embedding_compositor);
registry->godot_embedding_compositor = nullptr;
}
}
void WaylandThread::_wl_surface_on_enter(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) {
@@ -2064,6 +2090,8 @@ void WaylandThread::_wl_keyboard_on_keymap(void *data, struct wl_keyboard *wl_ke
xkb_state_unref(ss->xkb_state);
ss->xkb_state = xkb_state_new(ss->xkb_keymap);
xkb_state_update_mask(ss->xkb_state, ss->mods_depressed, ss->mods_latched, ss->mods_locked, 0, 0, ss->current_layout_index);
}
void WaylandThread::_wl_keyboard_on_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface, struct wl_array *keys) {
@@ -2124,6 +2152,15 @@ void WaylandThread::_wl_keyboard_on_leave(void *data, struct wl_keyboard *wl_key
msg->event = DisplayServer::WINDOW_EVENT_FOCUS_OUT;
wayland_thread->push_message(msg);
ss->shift_pressed = false;
ss->ctrl_pressed = false;
ss->alt_pressed = false;
ss->meta_pressed = false;
if (ss->xkb_state != nullptr) {
xkb_state_update_mask(ss->xkb_state, 0, 0, 0, 0, 0, 0);
}
DEBUG_LOG_WAYLAND_THREAD(vformat("Keyboard unfocused window %d.", ws->id));
}
@@ -2177,14 +2214,21 @@ void WaylandThread::_wl_keyboard_on_modifiers(void *data, struct wl_keyboard *wl
SeatState *ss = (SeatState *)data;
ERR_FAIL_NULL(ss);
xkb_state_update_mask(ss->xkb_state, mods_depressed, mods_latched, mods_locked, ss->current_layout_index, ss->current_layout_index, group);
ss->shift_pressed = xkb_state_mod_name_is_active(ss->xkb_state, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_DEPRESSED);
ss->ctrl_pressed = xkb_state_mod_name_is_active(ss->xkb_state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_DEPRESSED);
ss->alt_pressed = xkb_state_mod_name_is_active(ss->xkb_state, XKB_MOD_NAME_ALT, XKB_STATE_MODS_DEPRESSED);
ss->meta_pressed = xkb_state_mod_name_is_active(ss->xkb_state, XKB_MOD_NAME_LOGO, XKB_STATE_MODS_DEPRESSED);
ss->mods_depressed = mods_depressed;
ss->mods_latched = mods_latched;
ss->mods_locked = mods_locked;
ss->current_layout_index = group;
if (ss->xkb_state == nullptr) {
return;
}
xkb_state_update_mask(ss->xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group);
ss->shift_pressed = xkb_state_mod_name_is_active(ss->xkb_state, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE);
ss->ctrl_pressed = xkb_state_mod_name_is_active(ss->xkb_state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE);
ss->alt_pressed = xkb_state_mod_name_is_active(ss->xkb_state, XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE);
ss->meta_pressed = xkb_state_mod_name_is_active(ss->xkb_state, XKB_MOD_NAME_LOGO, XKB_STATE_MODS_EFFECTIVE);
}
void WaylandThread::_wl_keyboard_on_repeat_info(void *data, struct wl_keyboard *wl_keyboard, int32_t rate, int32_t delay) {
@@ -3036,15 +3080,82 @@ void WaylandThread::_xdg_activation_token_on_done(void *data, struct xdg_activat
DEBUG_LOG_WAYLAND_THREAD(vformat("Received activation token and requested window activation."));
}
void WaylandThread::_godot_embedding_compositor_on_client(void *data, struct godot_embedding_compositor *godot_embedding_compositor, struct godot_embedded_client *godot_embedded_client, int32_t pid) {
EmbeddingCompositorState *state = (EmbeddingCompositorState *)data;
ERR_FAIL_NULL(state);
EmbeddedClientState *client_state = memnew(EmbeddedClientState);
client_state->embedding_compositor = godot_embedding_compositor;
client_state->pid = pid;
godot_embedded_client_add_listener(godot_embedded_client, &godot_embedded_client_listener, client_state);
DEBUG_LOG_WAYLAND_THREAD(vformat("New client %d.", pid));
state->clients.push_back(godot_embedded_client);
}
void WaylandThread::_godot_embedded_client_on_disconnected(void *data, struct godot_embedded_client *godot_embedded_client) {
EmbeddedClientState *state = (EmbeddedClientState *)data;
ERR_FAIL_NULL(state);
EmbeddingCompositorState *ecomp_state = godot_embedding_compositor_get_state(state->embedding_compositor);
ERR_FAIL_NULL(ecomp_state);
ecomp_state->clients.erase_unordered(godot_embedded_client);
ecomp_state->mapped_clients.erase(state->pid);
memfree(state);
godot_embedded_client_destroy(godot_embedded_client);
DEBUG_LOG_WAYLAND_THREAD(vformat("Client %d disconnected.", state->pid));
}
void WaylandThread::_godot_embedded_client_on_window_embedded(void *data, struct godot_embedded_client *godot_embedded_client) {
EmbeddedClientState *state = (EmbeddedClientState *)data;
ERR_FAIL_NULL(state);
EmbeddingCompositorState *ecomp_state = godot_embedding_compositor_get_state(state->embedding_compositor);
ERR_FAIL_NULL(ecomp_state);
state->window_mapped = true;
ERR_FAIL_COND_MSG(ecomp_state->mapped_clients.has(state->pid), "More than one Wayland client per PID tried to create a window.");
ecomp_state->mapped_clients[state->pid] = godot_embedded_client;
}
void WaylandThread::_godot_embedded_client_on_window_focus_in(void *data, struct godot_embedded_client *godot_embedded_client) {
EmbeddedClientState *state = (EmbeddedClientState *)data;
ERR_FAIL_NULL(state);
EmbeddingCompositorState *ecomp_state = godot_embedding_compositor_get_state(state->embedding_compositor);
ERR_FAIL_NULL(ecomp_state);
ecomp_state->focused_pid = state->pid;
DEBUG_LOG_WAYLAND_THREAD(vformat("Embedded client pid %d focus in", state->pid));
}
void WaylandThread::_godot_embedded_client_on_window_focus_out(void *data, struct godot_embedded_client *godot_embedded_client) {
EmbeddedClientState *state = (EmbeddedClientState *)data;
ERR_FAIL_NULL(state);
EmbeddingCompositorState *ecomp_state = godot_embedding_compositor_get_state(state->embedding_compositor);
ERR_FAIL_NULL(ecomp_state);
ecomp_state->focused_pid = -1;
DEBUG_LOG_WAYLAND_THREAD(vformat("Embedded client pid %d focus out", state->pid));
}
// NOTE: This must be started after a valid wl_display is loaded.
void WaylandThread::_poll_events_thread(void *p_data) {
Thread::set_name("Wayland Events");
ThreadData *data = (ThreadData *)p_data;
ERR_FAIL_NULL(data);
ERR_FAIL_NULL(data->wl_display);
struct pollfd poll_fd;
struct pollfd poll_fd = {};
poll_fd.fd = wl_display_get_fd(data->wl_display);
poll_fd.events = POLLIN | POLLHUP;
poll_fd.events = POLLIN;
while (true) {
// Empty the event queue while it's full.
@@ -3188,6 +3299,15 @@ WaylandThread::OfferState *WaylandThread::wp_primary_selection_offer_get_offer_s
return nullptr;
}
WaylandThread::EmbeddingCompositorState *WaylandThread::godot_embedding_compositor_get_state(struct godot_embedding_compositor *p_compositor) {
// NOTE: No need for tag check as it's a "fake" interface - nothing else exposes it.
if (p_compositor) {
return (EmbeddingCompositorState *)godot_embedding_compositor_get_user_data(p_compositor);
}
return nullptr;
}
// This is implemented as a method because this is the simplest way of
// accounting for dynamic output scale changes.
int WaylandThread::window_state_get_preferred_buffer_scale(WindowState *p_ws) {
@@ -3356,15 +3476,20 @@ void WaylandThread::seat_state_lock_pointer(SeatState *p_ss) {
ERR_FAIL_NULL(p_ss);
if (p_ss->wl_pointer == nullptr) {
WARN_PRINT("Can't lock - no pointer?");
return;
}
if (registry.wp_pointer_constraints == nullptr) {
WARN_PRINT("Can't lock - no constraints global.");
return;
}
if (p_ss->wp_locked_pointer == nullptr) {
struct wl_surface *locked_surface = window_get_wl_surface(p_ss->pointer_data.last_pointed_id);
if (locked_surface == nullptr) {
locked_surface = window_get_wl_surface(DisplayServer::MAIN_WINDOW_ID);
}
ERR_FAIL_NULL(locked_surface);
p_ss->wp_locked_pointer = zwp_pointer_constraints_v1_lock_pointer(registry.wp_pointer_constraints, locked_surface, p_ss->wl_pointer, nullptr, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
@@ -4445,7 +4570,52 @@ Error WaylandThread::init() {
KeyMappingXKB::initialize();
wl_display = wl_display_connect(nullptr);
#ifdef TOOLS_ENABLED
String embedder_socket_path;
bool embedder_enabled = true;
if (OS::get_singleton()->get_environment("GODOT_WAYLAND_DISABLE_EMBEDDER") == "1") {
print_verbose("Disabling Wayland embedder as per GODOT_WAYLAND_DISABLE_EMBEDDER.");
embedder_enabled = false;
}
if (embedder_enabled && Engine::get_singleton()->is_editor_hint() && !Engine::get_singleton()->is_project_manager_hint()) {
print_verbose("Initializing Wayland embedder.");
Error embedder_status = embedder.init();
ERR_FAIL_COND_V_MSG(embedder_status != OK, ERR_CANT_CREATE, "Can't initialize Wayland embedder.");
embedder_socket_path = embedder.get_socket_path();
ERR_FAIL_COND_V_MSG(embedder_socket_path.is_empty(), ERR_CANT_CREATE, "Wayland embedder returned invalid path.");
OS::get_singleton()->set_environment("GODOT_WAYLAND_DISPLAY", embedder_socket_path);
// Debug
if (OS::get_singleton()->get_environment("GODOT_DEBUG_EMBEDDER_SINGLE_INSTANCE") == "1") {
print_line("Pausing as per GODOT_DEBUG_EMBEDDER_SINGLE_INSTANCE.");
pause();
}
} else if (Engine::get_singleton()->is_embedded_in_editor()) {
embedder_socket_path = OS::get_singleton()->get_environment("GODOT_WAYLAND_DISPLAY");
#if 0
// Debug
OS::get_singleton()->set_environment("WAYLAND_DEBUG", "1");
int fd = open("/tmp/gdembedded.log", O_CREAT | O_RDWR, 0666);
dup2(fd, 1);
dup2(fd, 2);
#endif
}
if (embedder_socket_path.is_empty()) {
print_verbose("Connecting to the default Wayland display.");
wl_display = wl_display_connect(nullptr);
} else {
print_verbose("Connecting to the Wayland embedder display.");
wl_display = wl_display_connect(embedder_socket_path.utf8().get_data());
}
#endif // TOOLS_ENABLED
ERR_FAIL_NULL_V_MSG(wl_display, ERR_CANT_CREATE, "Can't connect to a Wayland display.");
thread_data.wl_display = wl_display;
@@ -4465,12 +4635,19 @@ Error WaylandThread::init() {
ERR_FAIL_NULL_V_MSG(registry.wl_compositor, ERR_UNAVAILABLE, "Can't obtain the Wayland compositor global.");
ERR_FAIL_NULL_V_MSG(registry.xdg_wm_base, ERR_UNAVAILABLE, "Can't obtain the Wayland XDG shell global.");
if (!registry.xdg_decoration_manager) {
// Embedded games can't access the decoration and icon protocol.
if (!Engine::get_singleton()->is_embedded_in_editor()) {
if (!registry.xdg_decoration_manager) {
#ifdef LIBDECOR_ENABLED
WARN_PRINT("Can't obtain the XDG decoration manager. Libdecor will be used for drawing CSDs, if available.");
WARN_PRINT("Can't obtain the XDG decoration manager. Libdecor will be used for drawing CSDs, if available.");
#else
WARN_PRINT("Can't obtain the XDG decoration manager. Decorations won't show up.");
WARN_PRINT("Can't obtain the XDG decoration manager. Decorations won't show up.");
#endif // LIBDECOR_ENABLED
}
if (!registry.xdg_toplevel_icon_manager_name) {
WARN_PRINT("xdg-toplevel-icon protocol not found! Cannot set window icon.");
}
}
if (!registry.xdg_activation) {
@@ -4487,10 +4664,6 @@ Error WaylandThread::init() {
WARN_PRINT("FIFO protocol not found! Frame pacing will be degraded.");
}
if (!registry.xdg_toplevel_icon_manager_name) {
WARN_PRINT("xdg-toplevel-icon protocol not found! Cannot set window icon.");
}
// Wait for seat capabilities.
wl_display_roundtrip(wl_display);
@@ -5010,6 +5183,17 @@ bool WaylandThread::is_suspended() const {
return true;
}
struct godot_embedding_compositor *WaylandThread::get_embedding_compositor() {
return registry.godot_embedding_compositor;
}
OS::ProcessID WaylandThread::embedded_compositor_get_focused_pid() {
EmbeddingCompositorState *ecomp_state = godot_embedding_compositor_get_state(registry.godot_embedding_compositor);
ERR_FAIL_NULL_V(ecomp_state, -1);
return ecomp_state->focused_pid;
}
void WaylandThread::destroy() {
if (!initialized) {
return;
@@ -5045,6 +5229,10 @@ void WaylandThread::destroy() {
}
#endif // LIBDECOR_ENABLED
if (ws.xdg_toplevel_decoration) {
zxdg_toplevel_decoration_v1_destroy(ws.xdg_toplevel_decoration);
}
if (ws.xdg_toplevel) {
xdg_toplevel_destroy(ws.xdg_toplevel);
}
@@ -5132,6 +5320,22 @@ void WaylandThread::destroy() {
wl_output_destroy(wl_output);
}
if (registry.godot_embedding_compositor) {
EmbeddingCompositorState *es = godot_embedding_compositor_get_state(registry.godot_embedding_compositor);
ERR_FAIL_NULL(es);
es->mapped_clients.clear();
for (struct godot_embedded_client *client : es->clients) {
godot_embedded_client_destroy(client);
}
es->clients.clear();
memdelete(es);
godot_embedding_compositor_destroy(registry.godot_embedding_compositor);
}
if (wl_cursor_theme) {
wl_cursor_theme_destroy(wl_cursor_theme);
}
@@ -5212,6 +5416,8 @@ void WaylandThread::destroy() {
wl_registry_destroy(wl_registry);
}
wl_display_roundtrip(wl_display);
if (wl_display) {
wl_display_disconnect(wl_display);
}