X Tutup
Skip to content

Wayland: Implement game embedding#107435

Merged
Repiteo merged 1 commit intogodotengine:masterfrom
deralmas:wl-proxy
Nov 20, 2025
Merged

Wayland: Implement game embedding#107435
Repiteo merged 1 commit intogodotengine:masterfrom
deralmas:wl-proxy

Conversation

@deralmas
Copy link
Contributor

@deralmas deralmas commented Jun 12, 2025

As per tradition, I summarized this description and updated it with all the new developments ever since I opened this PR. If you want to see the original, check out the edit history.

This PR implements game embedding on Wayland, using a novel experimental approach.

The code is basically feature complete, with the exception of tablet support, due to various quirks.

image

Testing

Obviously, the editor must be running with the Prefer Wayland option on in the editor settings.

(Previous iterations of this PR required the project to specify the Wayland driver too, this is not the case anymore)

When debugging, you can uncomment #define WAYLAND_EMBED_DEBUG_LOGS_ENABLED, which will completely blast your terminal with a lot of information, ranging from hex dumps to various debug notes. Because of this, whenever it is defined I recommend teeing the whole thing to a file. I usually run it like this:

path/to/godot --verbose 2>&1 | tee proxy.log

These logs are extremely verbose on purpose and, if you need to trim them down when sharing, please keep a full copy as I might need more data later.

That said, please be aware that: as with any protocol dump, these logs contain every event, including key presses sent to the editor/game (not the whole OS), just like WAYLAND_DEBUG=1. I'm open to receiving logs privately if that makes you feel safer.

How it works

This code acts as a shim between the user's compositor and the editor (templates and the project managers skip it for stability). It intercepts all Wayland messages and manipulates them to achieve things not normally possible such as redirecting a window to a subsurface, all without modifying the embedded client. This shim is referred as the "embedder" in the code.

The embedding client can access a "fake" Wayland global offered by the shim with various methods to handle all embedded clients.

In practice it "merges" multiple clients into one single connection, which has various pro and cons. I'll explain below why I chose this approach.

Additionally I tried to make the code as "automatic" as possible; In most instances all it takes is to make the embedder aware of an interface and it takes care of any requests through a generic handler.

I'll document the approach properly at least after this PR gets ready but here's a very quick and rambly summary of what comes to mind:

Details

This is basically a compositor without all the actual logic (except when we need to emulate stuff for stuff like fake windows obviously). The first client to connect is designated as the "main" client, which creates actual windows, while all new clients get embedded as soon as they make a new toplevel by intercepting the request. Everything else still gets created (buffers, etc.), which means that we can simply take the buffer that was meant for a toplevel and place it into a subsurface. We need to emulate just enough of a toplevel to keep the client happy and both the compositor and the client will do all the heavy lifting for us.

The most basic building block in this project is that there are various "ID spaces": since we're handling multiple clients, and the Wayland protocol enforces reusing IDs (with an assertion in libwayland), we need a "global" ID space (with all the "real" objects from the actual compositor) and a per-client "local" ID space, with relative mappings between both.

I made sure to add plenty of utility methods and some wrappers to hopefully make handling them both easier.

We also need to manage the real compositor's global objects (what I call "registry objects" to disambiguate) since we offer the control object (we already have an IPC after all, why reinvent the wheel? :P ).

The most annoying part was a certain mechanism I had to implement for compatibility with older compositors because it looks like you couldn't cleanup certain objects (mainly core registry globals) until relatively recently.

Thus a compatibility mechanism was born: "registry globals instances", basically if we detect that an object does not have a destructor method we instance it once and reuse it by creating fake objects. It works surprisingly well although it requires some extra state tracking for some specific objects.

It's not implemented perfectly (there are some missing edge cases) but as all globals now implement destructor methods hopefully it should kick in less and less over time.

It definitely adds complexity and in all honestly I should've considered just disabling the feature on old compositors but since I was still experimenting I did not understand fully the issue yet 😅

How it's written

It started from a standalone C prototype which got grafted into the Godot codebase. I refactored it extensively since then to follow Godot's style and data structures. If something is weird, that's probably why.

This code went through lots of iterations due to its experimental nature. I'm not super proud about how the data structures are laid out and the names are kinda eh. I'll keep improving it as much as possible but I don't expect it to become perfect by any means. There are a lot of moving parts and stuff works, so I'm not going to break it if I'm not forced to :P

I plan to eventually create a third-party library that implements this approach or at the very least to fully document the current architecture. Hopefully that will give further insights into simplifying it further.

There are also some naively written parts and dumb things in general but luckily performance seems to not be an issue: #107435 (comment)

Why this approach

The "canonical" approach for Wayland embedding would be to implement a more or less full fledged compositor1, usually with a library like wlroots.

I initially looked into it but it looks like all compositor libraries (understandably) expect that you will be running them under less capable environments (DRM/libinput, X11, older compositors...). Thus we would need to normally re-implement basically every single event handler as a pass-through, so I started considering handling the issue differently.

The advantages overall are very interesting:

  • Maintainability: the idea is that most Wayland protocols can simply "pass through" so, by introspecting into the generated headers, all you need to do to "implement" a protocol is adding its static interface pointer into a list.

  • Size and simplicity: the actual embedder is substantially smaller than your standard compositor, comments and all. By doing our trick, we get a lot of stuff "for free".

  • Integration: at least wlroots renders everything on a single buffer (I assume all compositor libraries do). We instead simply "trick" the secondary clients into rendering onto a subsurface. This means that we don't need a renderer and most importantly that popups and other extra surfaces can go outside of the embedded window, which I think is a huge plus.

  • Rendering performance?: I'm not sure how to verify this but I suppose that because we're not doing an extra step(?) of rendering and simply redirecting the embedded client to different surfaces that there should be minimal overhead.

There are also disadvantages I should point out:

  • Everything runs under the embedder, editor included. This means that, if the embedded game does "naughty things" and the real compositor disconnects it, it will actually kick both the editor and the game. This is not critical as we trust the client and could do the kicking ourselves but that's note-worthy. Also note that the embedder is enabled only with the editor, the project manager and non-embedded projects will run "on bare metal" right away.

  • This only works for Wayland clients. Honestly, this is not that big of a deal because there are drop-in solutions like xwayland-satellite that can handle xwayland for us if we ever feel the need. Some compositors like niri went with the same approach: https://github.com/YaLTeR/niri/wiki/Xwayland Update: This is even less of an issue now as we simply pass --display-driver wayland to the embedded game.

  • You need to register every protocol used, even low level ones. We thus need to add and track extra XML definitions for stuff used by the WSI. It's one line in the actual code but still, worthy of note.

  • You need to register every protocol used. Yes, this makes the barrier of entry to contribution ever so slightly high but if the protocol does not touch the few things the embedder "overrides" it should be just a line in the embedder code.

Yea.

Old conclusion (It felt wrong to change it)

This was meant to be short to not waste too much of my energy but that's apparently what short looks like lmao. It did not help that I accidentally lost half of this description right before sending it and had to rewrite it from scratch.

Really, there's a lot I've omitted, such as the constraints the wire format imposes and the actual architecture of this code. I'll document everything (and more) once it's in a stable state. This is only the beginning.

You have no idea how much work went into getting at this point. It was brutal. I won't lie, I might've underestimated it and that might have contributed to my... lack of energy. I don't think it was the only reason though.

This is the most experimental thing I've ever done: I'm still waiting for the huge wall into which everything will crash spectacularly but right now I've went through every single hurdle, which makes it quite hard to keep the enthusiasm in control. If I actually nailed it, it should be all downhill from now on.

I really believe this might be it, otherwise I would not have spent 3+ hours writing this wall of text almost twice. I want to get this out, to let everybody know what has been brewing behind the scenes and give them the chance to try it instead of sitting on this code waiting for the worst to happen.

However it goes, I learnt a lot and had a blast playing with the wire itself.

Thank you for your patience.

The original conclusion is outdated in some ways but I'm keeping it because it sums exactly the feelings I had while working on this thing. I don't want to get personal so I'll limit myself by saying that those annoying times for unrelated reasons and I went pretty darn emotional for a PR description. I can't write normal PRs for the life of me, sorry xD

That said, as per the original text, this was way more complex than I thought but I still genuinely believe that this makes more sense than using a compositor library like wlroots. In both cases you're writing a compositor and that's a hard thing to do, there's no way around that.

Hopefully I'm right :P

Footnotes

  1. To be completely fair, another option mentioned in the Wayland docs themselves would be to pass the game buffer out-of-band like with the MacOS embedder. I personally think that in our particular case, given Wayland's architecture, the approach I'm implementing is the right choice.

@deralmas deralmas force-pushed the wl-proxy branch 2 times, most recently from 4de4f77 to 8852b2e Compare June 12, 2025 02:58
@deralmas deralmas changed the title [WIP] Wayland: Implement game embeddding [WIP] Wayland: Implement game embedding Jun 12, 2025
@AThousandShips AThousandShips added this to the 4.x milestone Jun 12, 2025
@cg9999
Copy link
Contributor

cg9999 commented Jun 14, 2025

Thank you, seems to be working great!

I'm using Cosmic Desktop on Arch. A few warning/error messages seen so far:

`WARNING: FIFO protocol not found! Frame pacing will be degraded.
at: init (platform/linuxbsd/wayland/wayland_thread.cpp:4430)

ERROR: Condition "p_window_id != MAIN_WINDOW_ID" is true. Returning: INVALID_SCREEN
at: window_get_current_screen (platform/linuxbsd/wayland/display_server_wayland.cpp:1053)

When stopping game:
ERROR: Can't read message header: Connection reset by peer
at: handle_sock (platform/linuxbsd/wayland/snooper.cpp:1951)

ERROR: Condition "((size_t)head_rec) != vec.iov_len" is true. Returning: false
at: handle_sock (platform/linuxbsd/wayland/snooper.cpp:1952)`

I can upload full logs if any of these is interesting.

@ArchercatNEO
Copy link
Contributor

Interestingly I can't seem to embed at all on KDE Plasma, NixOS.

Screenshot_20250614_092610

There aren't any relevant errors opening the window and closing the window only says the client disconnected.
Something even more strange is that when closing the editor I get spammed with ERROR: Condition "((size_t)head_rec) != vec.iov_len" is true. Returning: false and then we crash with an index out of bounds. We were already closing the editor so it crashing isn't as big as it crashing while stuff is running but it is a significant thing

@deralmas
Copy link
Contributor Author

Hi people, thank you for testing!

@cg9999, the first two errors are unrelated to this PR, so that's fine. One is for the new FIFO support which is not yet implemented in most compositors and the other is a new bug which is getting discussed. I think I have a solution for the latter.

Regarding the last two errors, those are fine really. Those happen when the game is disconnecting and it just complains that it's not getting enough data, which is fine. I'll make sure to silence those during disconnection when I wrap up the thing.

@ArchercatNEO, that's unfortunate :(

What's the pic showing? I see a window with something embedded in it i guess? Does the thing look wonky? Note that you need to set both backends (editor and project) to Wayland or it will not work properly.

I'll first do a quick test on a KDE VM I have lying around. Hopefully the issue is trivial.

Something even more strange is that when closing the editor I get spammed with ERROR: Condition "((size_t)head_rec) != vec.iov_len" is true. Returning: false and then we crash with an index out of bounds. We were already closing the editor so it crashing isn't as big as it crashing while stuff is running but it is a significant thing

The index out of bounds is weird, but I wouldn't worry about it too much since there's no cleanup logic still.

@deralmas
Copy link
Contributor Author

deralmas commented Jun 14, 2025

@ArchercatNEO I think I figured out what's happening.

What version of NixOS are you running? Mesa, apparently, only recently (about the end of march) started supporting a newer protocol for DRM selection which I registered into the snooper. All versions before that used wayland-drm which I completely ignored exactly because a few compositors started deprecating it.

Since I am on a rolling release distro I did not notice that and I suppose neither @cg9999 xD

I'll add that protocol to the supported lists. I haven't confirmed that this will fix it mind you, but I'm now quite sure that we need to add it anyways to the supported protocols.

Edit: oof it's so old that it doesn't have a destructor and thus triggers a failsafe... Since it reports some data on bind I need to account for that. I'm onto it.

Edit 2: I managed to replicate this on my workstation running the editor on top of LLVMpipe. This will speed up iteration a lot :D

Edit 3: From my testing, it looks like the issue was not wl_drm. Oops.

@deralmas
Copy link
Contributor Author

Aight @ArchercatNEO the issue should be fixed. Looks like wl_drm, while important, was not the issue here. I think that its logic might be useless right now because I made sure to only proxy the actual main godot display instead of blindly setting WAYLAND_DISPLAY (you see why I added the custom logic? :P) so I can't even hit its handling code lol, it must be used from a different mesa connection or something.

That said I'm currently keeping it in the code despite being untested in case I eventually do actually get everything through the proxy. It has its own advantages actually.

The fix was actually related to wl_shm, as we did not report its state on binding due to a special "instance" mechanism I implemented for some old protocol versions.

Now I can run it on an emulated Fedora KDE 42 using LLVMpipe, and on an updated copy of Arch with LLVMpipe and a modded sway without the dmabuf protocol.

On KDE though for some reason the game surface is actually slightly out of place, which is weird. I still haven't investigated that though, but I suppose it might be related to libdecor or something.

Please tell me if this fixed your issue :D


If anyone's curious, I'll take advantage of this message to explain a special hurdle I had to overcome to get this proxy working: indestructible objects.

This proxy tries as hard as possible to offload stuff to the real compositor, 1) because I'm lazy 2) because it'd be tedious to do by hand, but not everything can be blindly passed. One of those is global binding for when the object in question does not have a destructor method.

It might sound crazy, but originally most core globals (including infamously wl_registry) could not be deleted once created. They were eventually updated but unfortunately old versions are still common. This means that, if we were to just bind everything to the compositor, once the embedded game restarts we'd have "unreferenced objects" we can't destroy wandering in memory, basically turning into a leak.

Because of this we need to take a bit more extra responsibility: if the object does not have a destroy or release method, we "instance" it from the first object that was created. Basically we recycle said objects, pointing all requests to the "real" object and mirroring all events from it to all its instances.

Luckily this thing only applies basically to core objects and other very old protocols (like wl_drm). All new protocols have destructors and even new eligible versions are automatically included in the check so as time goes on we should be seeing this mechanism being hit less and less, with everything eventually passed through and handled by the compositor natively.

In this case, the issue we had is that a few globals (like wl_shm) send init events on bind. If the object is not indestructible that's fine, but if it's not then we do not actually create a new one so the events are never sent. I was aware of this quirk and that I'd have to track those events and re-send them manually but I kept that for last because I did not think that it'd be that bad xD

There's still a few objects I need to track but hopefully they won't give too many issues in the meantime.


(Sorry for the rambliness but It's quite late so I don't have much time to explain it in a cleaner way)

@ArchercatNEO
Copy link
Contributor

@Riteo unfortunately it seems there are more issues and I still can't embed

There was nothing wrong with the window it just wasn't embedding (I hadn't set up the images to actually fit in the screen)
I am on NixOS but have my repo set to nixos-unstable which should mean I'm on a rolling release too (my Plasma is 6.3.5)
I have both x11 and libdecor disabled in the compilation flags which may mean something

Here's what I mean by the window not embedding, there's nothing wrong with the content but it just isn't in the game tab
Screencast_20250615_102539.webm

Also a MUCH weirder bug, despite not having libdecor both windows have decorations but this warning is still raised somehow. This didn't happen last time I tried it but it does now.
image

@ArchercatNEO
Copy link
Contributor

No clue what changed but it works now.
I ran the editor under gdb and it embedded right so I tried again without gdb and got embedding.

Screenshot_20250615_105613

@ArchercatNEO
Copy link
Contributor

Random crash with this log. The embeded window wasn't even open so this is something with the main window itself

[PROXY]  === START PACKET ===
[PROXY] Received bytes: 710000000100100000bd010000750000
[PROXY] dir: compositor, id: 0x71, bytes: 16, opcode: 1
[PROXY] Client: 0.
ERROR: No object found for r0x71
   at: handle_msg_info (platform/linuxbsd/wayland/snooper.cpp:1911)
ERROR: Caller thread can't call this function in this node (/root). Use call_deferred() or call_thread_group() instead.
   at: propagate_notification (scene/main/node.cpp:2543)

@cg9999
Copy link
Contributor

cg9999 commented Jun 15, 2025

Got a crash using the latest commits:


[PROXY]  === START PACKET ===
[PROXY] Received bytes: 5e0000000100100000c0030000fa0100
[PROXY] dir: compositor, id: 0x5e, bytes: 16, opcode: 1
[PROXY] Client: 0.
ERROR: No object found for r0x5e
   at: handle_msg_info (platform/linuxbsd/wayland/snooper.cpp:1911)
ERROR: Caller thread can't call this function in this node (/root). Use call_deferred() or call_thread_group() instead.
   at: propagate_notification (scene/main/node.cpp:2543)

================================================================
handle_crash: Program crashed with signal 4
Engine version: Godot Engine v4.5.dev.custom_build (730c519bed03606f620a549c7b4896319197bec8)
Dumping the backtrace. Please include this when reporting the bug on: https://github.com/godotengine/godot/issues
[1] /usr/lib/libc.so.6(+0x3def0) [0x7fc4e87c9ef0] (??:0)
[2] WaylandEmbedderProxy::handle_msg_info(WaylandEmbedderProxy::Client*, WaylandEmbedderProxy::msg_info const*, unsigned int*, int*) (/home/cromos/projects/godot/Riteo_godot/platform/linuxbsd/wayland/snooper.cpp:1911 (discriminator 8))
[3] WaylandEmbedderProxy::handle_sock(int, int) (/home/cromos/projects/godot/Riteo_godot/platform/linuxbsd/wayland/snooper.cpp:2192)
[4] WaylandEmbedderProxy::handle_fd(int, int) (/home/cromos/projects/godot/Riteo_godot/platform/linuxbsd/wayland/snooper.cpp:2370)
[5] WaylandEmbedderProxy::poll_sockets() (/home/cromos/projects/godot/Riteo_godot/platform/linuxbsd/wayland/snooper.cpp:445 (discriminator 8))
[6] WaylandEmbedderProxy::_thread_loop(void*) (/home/cromos/projects/godot/Riteo_godot/platform/linuxbsd/wayland/snooper.cpp:2204 (discriminator 2))
[7] Thread::callback(unsigned long, Thread::Settings const&, void (*)(void*), void*) (/home/cromos/projects/godot/Riteo_godot/core/os/thread.cpp:66)
[8] void std::__invoke_impl<void, void (*)(unsigned long, Thread::Settings const&, void (*)(void*), void*), unsigned long, Thread::Settings, void (*)(void*), void*>(std::__invoke_other, void (*&&)(unsigned long, Thread::Settings const&, void (*)(void*), void*), unsigned long&&, Thread::Settings&&, void (*&&)(void*), void*&&) (/usr/include/c++/15.1.1/bits/invoke.h:63)
[9] std::__invoke_result<void (*)(unsigned long, Thread::Settings const&, void (*)(void*), void*), unsigned long, Thread::Settings, void (*)(void*), void*>::type std::__invoke<void (*)(unsigned long, Thread::Settings const&, void (*)(void*), void*), unsigned long, Thread::Settings, void (*)(void*), void*>(void (*&&)(unsigned long, Thread::Settings const&, void (*)(void*), void*), unsigned long&&, Thread::Settings&&, void (*&&)(void*), void*&&) (/usr/include/c++/15.1.1/bits/invoke.h:99)
[10] void std::thread::_Invoker<std::tuple<void (*)(unsigned long, Thread::Settings const&, void (*)(void*), void*), unsigned long, Thread::Settings, void (*)(void*), void*> >::_M_invoke<0ul, 1ul, 2ul, 3ul, 4ul>(std::_Index_tuple<0ul, 1ul, 2ul, 3ul, 4ul>) (/usr/include/c++/15.1.1/bits/std_thread.h:303)
[11] std::thread::_Invoker<std::tuple<void (*)(unsigned long, Thread::Settings const&, void (*)(void*), void*), unsigned long, Thread::Settings, void (*)(void*), void*> >::operator()() (/usr/include/c++/15.1.1/bits/std_thread.h:310)
[12] std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(unsigned long, Thread::Settings const&, void (*)(void*), void*), unsigned long, Thread::Settings, void (*)(void*), void*> > >::_M_run() (/usr/include/c++/15.1.1/bits/std_thread.h:255)
[13] /home/cromos/projects/godot/Riteo_godot/bin/godot.linuxbsd.editor.dev.x86_64(+0x93f2154) [0x56060c205154] (thread.o:?)
[14] /usr/lib/libc.so.6(+0x957eb) [0x7fc4e88217eb] (??:0)
[15] /usr/lib/libc.so.6(+0x11918c) [0x7fc4e88a518c] (??:0)
-- END OF C++ BACKTRACE --
================================================================

Full log available if needed

@deralmas
Copy link
Contributor Author

@ArchercatNEO

unfortunately it seems there are more issues and I still can't embed

At least we're moving forwards :)

Here's what I mean by the window not embedding, there's nothing wrong with the content but it just isn't in the game tab

It looks like it's embedding fine to me ;)

You're using the floating game workspace. That bar with the various modes and selections is the editor, which in turn wraps the game window. Kinda weird, ik, but that's why you can't click "Game", that's the actual game tab, detached from the main window.

All you need to fix it (as you might have accidentally done already) is to untick "Make Game Workspace Floating on Next Play" from the 3 dots in the "debug bar" (no idea how it's actually called):

Top portion of the game tab, with 3 dots menu open

So, in conclusion, it's a feature, not a bug :D


@ArchercatNEO and @cg9999:

Random crash with this log. The embeded window wasn't even open so this is something with the main window itself

Got a crash using the latest commits

Unfortunately those kind of crashes can only be debugged by reading the whole log. Please take in consideration that these will contain hexdumps of all data sent to the editor/game including keystrokes so if you have no idea what might be in them, you can send contact me via email at riteo that funny symbol that scrapers look for posteo.net or on chat.godotengine.org as riteo. We can even setup a magic-wormhole encrypted transfer if that tickles your fancy.

@cg9999
Copy link
Contributor

cg9999 commented Jun 15, 2025

Full log for #107435 (comment)
debug2.log.gz

Nothing secret here, just trying things out on wayland

@deralmas
Copy link
Contributor Author

@cg9999 thank you for the log file!

Apparently it's related to the pointer constraints logic, which is also used for pointer warping (a new protocol just came out but we don't have it hooked up yet).

I can replicate it by simply triggering a pointer warp in the editor by using infinite panning.

We need to redirect requests only from the embedded window but it looks like I always skipped the generic handler (and thus the creation of an object) by returning the wrong value 😅

Hopefully the latest changes I just pushed will fix your issue! :D

@ArchercatNEO
Copy link
Contributor

@Riteo

All you need to fix it (as you might have accidentally done already) is to untick "Make Game Workspace Floating on Next Play" from the 3 dots in the "debug bar" (no idea how it's actually called):

Yep that's what it was and looking back it looks like embedding worked even before the wl_display fix it was just in its own window (I really should've noticed the debug tabs on the window oops)

As for fixing changes, ever since pulling I have gotten no crashes while developing a game and no significant errors either.
There are still error logs but so far they don't seem to do anything so all good!

@ArchercatNEO
Copy link
Contributor

Looking through the protocols used I noticed you added the linux-explicit-synchronization protocol but according to wayland.app it's been deprecated in favour of linux-drm-syncobj. Would this be like wayland-drm where the WSI in older vulkan versions uses the unstable protocol so we have to expose it? If so shouldn't we also add linux-drm-syncobj for newer versions of WSI?

@deralmas
Copy link
Contributor Author

@ArchercatNEO great catch! Yea, we need to add that too, along with FIFO actually.

Would this be like wayland-drm where the WSI in older vulkan versions uses the unstable protocol so we have to expose it?

Yes, exactly. We need to have all most used protocols in there for compatibility, as any good compositor would do.

That's also why I turned the embedder off for anything but the editor, so that we don't need to worry about this stuff for shipped projects.

@ArchercatNEO
Copy link
Contributor

Sorry to do this in 2 parts but I looked to mesa's master branch to see which protocols they use in https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/loader/meson.build#L8-16

(Transcripted)

'fifo-v1': 'staging/fifo/fifo-v1.xml',
'commit-timing-v1': 'staging/commit-timing/commit-timing-v1.xml',
'linux-dmabuf-unstable-v1': 'unstable/linux-dmabuf/linux-dmabuf-unstable-v1.xml',
'presentation-time': 'stable/presentation-time/presentation-time.xml',
'tearing-control-v1': 'staging/tearing-control/tearing-control-v1.xml',
'linux-drm-syncobj-v1': 'staging/linux-drm-syncobj/linux-drm-syncobj-v1.xml',
'color-management-v1': 'staging/color-management/color-management-v1.xml',

I am guessing that these are the protocols we need to relay down to the embedded process since wsi/egl will attempt to use protocols even if we don't. It also seems that regardless of egl/vulkan these are all the protocols we need to worry about.

Of these it seems we are missing commit-timing-v1, presentation-time, tearing-control-v1 and color-management-v1.
We will actually make use of color-management-v1 but in a different PR, so whether it gets added here or in that one mostly comes down to which PR gets merged first.

@AndreaMonzini
Copy link

AndreaMonzini commented Jun 19, 2025

Thank you @Riteo for the incredible work !

Question:

In any case it seems really a lot of work and i will test as soon as possible !

@ArchercatNEO
Copy link
Contributor

Yes, from the footnote in the first post

To be completely fair, another option mentioned in the Wayland docs themselves would be to pass the game buffer out-of-band like with the MacOS embedder. I personally think that in our particular case, given Wayland's architecture, the approach I'm implementing is the right choice.

@deralmas
Copy link
Contributor Author

Thank you @Riteo for the incredible work !

Hi @AndreaMonzini, thank you!

have you considered a different approach like the #105884 ?

As @ArchercatNEO pointed out, I considered that, yes. Though neither the de facto standard approach of an embedded compositor nor the buffer-sharing approach used on MacOS really convinced me, given Wayland's architecture. That's how I came up with this experiment.

For what i understand in this case there is not a window management hack.

To be clear, this PR does not make use of the same windowing hacks as on X11 and Windows. The end result is a single window just like on MacOS and is hopefully just as sturdy.

The main difference between MacOS' approach and this is that the MacOS embedder has to explicitly (de)serialize all state (buffer data, input, etc.) using Godot's custom IPC. This PR instead skips that (de)serialization step by working directly with Wayland's IPC, allowing us to move all custom logic to the editor and even embed unmodified clients.

Since we interact directly with Wayland objects, it should be easier to integrate with more advanced features (e.g. popups). We also get a lot of features "for free"; All we have to do is broadcast to the client whatever message the compositor is already sending us.

Don't get me wrong, the MacOS approach is awesome! The only reason that I can do something like this is because the Wayland protocol allows me to do that. Even the official documentation notes that it serves well for embedding so I suppose that it was probably designed with that in mind. I wouldn't be surprised if no other platform could do something like this.

I hope that it answers your question :D

In any case it seems really a lot of work and i will test as soon as possible !

Thank you! Don't hesitate to ask if you need further info or if you find an issue :D

@deralmas
Copy link
Contributor Author

Sorry to do this in 2 parts but I looked to mesa's master branch to see which protocols they use

@ArchercatNEO sorry for the late response, I forgot to answer 😅

Thank you for looking into all remaining WSI protocols!

So, let's take a look. We definitely need to also add commit-timing-v1 for better FIFO (kinda forgot it was a thing lol) and tearing-control-v1 for, well... tearing

I'm not really sure that we need to add presentation-time as it's used only for video playback - it tells the client exactly when a frame has been presented so that it can sync the audio precisely - so I think that we can do without it for now.

Regarding color-management-v1 as you pointed out we can just add it if the HDR PR gets merged first.

@ArchercatNEO
Copy link
Contributor

I had a question about the additional boilerplate we're adding.

The real compositor will report all interfaces it supports with an interface name, id and a version; even those we do not make use of. Since it is up to the client to have the full interface to check against shouldn't it be possible to just keep track of all protocols supported by the real compositor in an array and simply relay all of them? This way we intercept the few protocols we care about and can just forward all the ones we don't to the client.

@AndreaMonzini
Copy link

Thank you! Don't hesitate to ask if you need further info or if you find an issue :D

Thank you for the detailed answer !
Looking forward with the development updates.

@Repiteo Repiteo merged commit 688a6d0 into godotengine:master Nov 20, 2025
20 checks passed
@Repiteo
Copy link
Contributor

Repiteo commented Nov 20, 2025

Thanks! Fantastic work!

@AndreaMonzini
Copy link

AndreaMonzini commented Nov 20, 2025

Thank you @deralmas ! a lot of work !
Personally it was the last block to overcome before switching to Wayland !

@JamesMowery
Copy link

Yay yay yay! Super excited to try the new changes! Thanks so much for all the amazing work!

@Fireye04
Copy link

Holy shit we're merged! Congrats and thanks a bunch @deralmas!

Comment on lines +1536 to +1542
struct xdg_toplevel *toplevel = ws->xdg_toplevel;

if (toplevel == nullptr && ws->libdecor_frame) {
toplevel = libdecor_frame_get_xdg_toplevel(ws->libdecor_frame);
}

ERR_FAIL_NULL_V(toplevel, ERR_CANT_CREATE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're using libdecor without an #ifdef LIBDECOR_ENABLED flag here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, a classic mistake of mine, thank you for catching that 😅

@ndbn
Copy link

ndbn commented Nov 25, 2025

scons target=editor production=no compiledb=yes debug_symbols=yes dev_mode=yes

image

@deralmas
Copy link
Contributor Author

deralmas commented Nov 27, 2025

Hi @ndbn thank you for reporting this compile errror. Dunno why no-one caught this warning before, warnings in general seem somewhat flaky, in my experience 😅

It makes sense, as both branches only contain the debug macro, which gets stubbed out with an #ifdef, so they're identical indeed.

I'll make a PR soon.

@AThousandShips
Copy link
Member

AThousandShips commented Nov 27, 2025

This was raised in RC as well after the PR was merged

@deralmas
Copy link
Contributor Author

Oh, sorry about that. I completely missed it, or maybe just forgot about it 😅

@JamesMowery
Copy link

JamesMowery commented Dec 2, 2025

screenrecording-2025-12-01_23-18-16.mp4

(Note: The video shows the correct behavoir first with X11, then I switch over to Wayland to show what's happening.)

Unfortunately I'm getting some really strange menu behavior with the latest dev5 build that just went live when switching to Wayland. The dropdown menus are offset awkwardly.

screenrecording-2025-12-01_23-19-47.mp4

After playing around, it looks like after you click to show the menu, if you then click again to close the menu, it shifts back into place momentarily (although that might just be purely visual; I tried to see if I could quickly hover over it but no go).

When things are awkwardly offset like this, my immediate thought goes to the fact that I'm using fractional scaling for my monitors (running 1440p monitors at 1.25 scaling). Not sure if it's related at all, but just throwing that out there since it's often the case when I see things offset like this.

(But in good news, the embedded editor for Wayland is working!)

Running the CachyOS with Wayland + Hyprland + Nvidia RTX 5090.

@JamesMowery
Copy link

JamesMowery commented Dec 2, 2025

screenrecording-2025-12-01_23-25-07.mp4

This offsetting behavior also appears on other dropdowns as well (like in the sidebar).

(Edit: I missed the second attempt in the recording, but trust me it was still offset).

image

(Edit 2: Confirmed that this does not happen on Niri, and I also have fractional scaling on Niri, so I guess we can elminate that theory.)

(Edit 3: Went ahead and removed fractional scaling from Hyprland, just to be sure, and confirmed that this was still bugged even with normal scaling.)

@eobet
Copy link
Contributor

eobet commented Dec 7, 2025

Sorry if this is the wrong place for this comment, but from a user perspective I find it weird that this requires "single window mode" to be off when it appears to not use a second window (unlike what I've been forced to use on Linux for the last year where the game opens in a new window).

I find this problematic not just because it's illogical (from a UX perspective) but because window sizes seem to be all over the place in Godot with Wayland and display scaling (whereas in single window mode, this was not an issue).

@deralmas
Copy link
Contributor Author

deralmas commented Dec 7, 2025

Hi @eobet. Nothing stops us from embedding in single-window mode, technically. Actually, nothing technically stops us from embedding in single-window mode on X11/Windows too.

Originally (before Wayland embedding), it was enabled in single-window mode too, but it got disabled in #101936.

The issue is, in both cases, that the embedded game covers popups and the like. While we don't use a different window, we use a different window "layer" (wl_subsurface), which still composites over any eventual popups.

We use a subsurface to avoid reading and re-compositing everything, which would be a lot more complex, as mentioned in this PR description.

but because window sizes seem to be all over the place in Godot with Wayland and display scaling (whereas in single window mode, this was not an issue).

This is a bug, and something that will be fixed. There are various interconnected things at play, but I've got a workaround ready, and improvements to the engine API are in the works. You can track #110643 and perhaps give its linked PR a shot.

Please note that the Wayland backend is marked as experimental and is still not polished as we'd like. That's why it's currently hidden behind an opt-in switch.

Feel free to ask if you've got further questions, I'm happy to help.

@mahkoh
Copy link

mahkoh commented Jan 13, 2026

@deralmas: FYI I've taken your idea and turned it into a rust crate: https://lore.freedesktop.org/wayland-devel/CAHijbEUuMG9-dCPFrDRMrEzB60YgCbjdeRDgUZaZV3JyMfJ-0w@mail.gmail.com/T/#u

Just wanted to let you know that I thought this was pretty neat.

@deralmas
Copy link
Contributor Author

@mahkoh That's wonderful! Thank you for letting me know, I'll check it out. It sounds very nifty, especially with all the utilities you built around it.

Just wanted to let you know that I thought this was pretty neat.

Thank you, I'm really glad :D

@musjj
Copy link

musjj commented Feb 15, 2026

@JamesMowery

Hey did you ever get this feature to work on Hyprland, Niri or other tiling compositors? I already set both my editor and game to Wayland, but I still can't get this embedded window feature to work.

@JamesMowery
Copy link

JamesMowery commented Feb 15, 2026

@musjj Working quite fine for me on v4.6.1-rc1. This is on Niri. Be sure to uncheck the option I have highlighted.

image

@musjj
Copy link

musjj commented Feb 15, 2026

Oh wow thanks that works!!!
I wonder why that's the default?

@orowith2os
Copy link

I'm curious if @deralmas would be interested in splitting off the Wayland interception code into a separate library? I'd like to use the work to do almost this exact thing for my own use cases, and having a C/C++ implementation (Rust is too annoying for me to use with this) would help a lot.

@deralmas
Copy link
Contributor Author

deralmas commented Mar 1, 2026

Hi @orowith2os, yes, I'm definitely interested in turning this into a library!

Please note that currently it's pretty hacky (see the PR description for more context), uses a lot of internal (but public) libwayland types (wl_argument, wl_interface...), and depends on Godot's custom STD library. I also have to juggle other things too, like fixing various Godot Wayland bugs, so it's going to take a while to do properly.

If you're working on something right now, it might be easier to bundle the required Godot templates and use godot_embedder.{cpp,h} directly. Since it's a thread and can be interacted with through Wayland requests I think that you can make it work as-is with minimal modifications. Not ideal, but hopefully does the job for the meantime.

Please feel free to contact me directly if you need anything :D

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

X Tutup