Skip to content

Wayland basics: window + event loop - #129

Merged
Try merged 13 commits into
Try:masterfrom
LordKobra:wayland-basics
Oct 22, 2026
Merged

Try merged 13 commits into
Try:masterfrom
LordKobra:wayland-basics

Conversation

@LordKobra

Copy link
Copy Markdown
Contributor

First part of the Wayland backend (split from #121)

Included:

  • Window creation (normal, maximized, fullscreen, minimized) with server-side decorations
  • Window title, close button
  • Resize and fullscreen switch
  • Fractional scaling (HiDpi scaling)
  • Rendering pauses while the compositor suspends the window (e.g. minimized)
  • Event loop
  • TEMPEST_DISABLE_WAYLAND=1 uses X11 instead (for development/comparison)

Not yet: keyboard and mouse (next PR).

Tested on KDE Plasma (NVIDIA).

Comment thread Engine/system/api/waylandapi.cpp
Comment thread Engine/system/api/waylandapi.cpp
Comment thread Engine/system/api/waylandapi.cpp
Comment thread Engine/system/api/waylandapi.cpp Outdated
Rect WaylandApi::implWindowClientRect(SystemApi::Window* w) {
// Wayland has no global window position.
auto ww = toWWindow(w);
return Rect(0, 0, ww->physicalWidth(), ww->physicalHeight());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This might be an issue.
My understanding, that Wayland state is async to application (similar to X11/android). In that case Vulkan swapchain wont be able to access exact window size at construction time.
In X11, swapchain has create loop specifically to address such use-case.

@LordKobra LordKobra Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Shouldn't be an issue for three reasons:

  1. Client picks buffer size without approval of compositor. This is a local state.
  2. Window size is roundtravels on first configure, which happens already in Window creation. So after implCreateWindow we already know it.
  3. When compositor the sends an update, we already update the state before sending the ack.

Comment thread Engine/system/api/waylandapi.cpp Outdated
Comment thread Engine/system/api/waylandapi.cpp Outdated
@Try

Try commented Oct 15, 2026

Copy link
Copy Markdown
Owner

Only very general comment for now - I probably would have to read it a few times, while iterating on PR

@LordKobra

Copy link
Copy Markdown
Contributor Author

Okay, all points in your first review have been addressed.

I reworked the event processing system. I write this comment for explanation, because it's not trivial to see in code why I did this:

  1. Tempest wants one app event per call.
  2. libwayland only allows processing the pending queue completely.
  3. Right now there is just one app dispatch event inside callback (close), but there would be many more with input handling coming.

The solution: The entire Wayland event queue is processed in sequence. Whenever an app event would be triggered, instead we queue the app event into a second queue only for app events.

The implProcessEvents function will only process one appEvent per call and return. Otherwise it reads the wayland event queue and processes that and checks for appEvent again, otherwise it will render.

An app event that is processed is taken out of the queue, so if Dialog::exec performs any nesting, it can continue right at the next app event.

Wayland events only relevant to rendering will record their state and only apply before render (e.g. swapchain resize). This avoids freeze on resize etc.

Comment thread Engine/system/api/waylandapi.cpp
Comment thread Engine/system/api/waylandapi.cpp Outdated
Comment thread Engine/system/api/waylandapi.cpp Outdated
Comment thread Engine/system/api/waylandapi.cpp Outdated
Comment thread Engine/system/api/waylandapi.cpp Outdated
Comment thread Engine/system/api/waylandapi.cpp
Comment thread Engine/system/api/waylandapi.cpp
@LordKobra

Copy link
Copy Markdown
Contributor Author

The Wayland API now throws in the constructor if the connection fails.

Note: Tempest currently reaches the backend on device creation checkPresentationSupport -> WaylandApi::display() , so with throw a Wayland build needs a compositor even for window-less programs like unit tests.

@Try Try left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Uh, last few question on resize stuff

Comment thread Engine/system/api/waylandapi.cpp
Comment thread Engine/system/api/waylandapi.cpp Outdated
int32_t appRequestedHeight = 0;

// size the app last saw; updateState() dispatches a resize when it differs.
int32_t dispatchedWidth = 0; // physical

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

what is exact life-cycle of window size?
pending.width - that from configuration callback, that about to be confirmed
width - last confirmed
appRequestedWidth - initial from constructor, just to be fallback once.
dispatchedWidth - last delivered to game.

dispatchedWidth is probably wont be needed, once resize event is implemented: Tempest already checks Window::w()/Window::h(), so we may insert resize event into event queue, from onXdgSurfaceConfigure?

appRequestedWidth - no a big deal, but sad to waste memory for one-time use-case.

How this will interact with current flow of window construction, when game creates swapchain right-away in MainWindow constructor? Is there a risk of getting 0x0 client-rect size into swapchain?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, the life-cycle is correct.

appRequestedWidth - no a big deal, but sad to waste memory for one-time use-case.

Removed it by handling this directly during construction.

dispatchedWidth is probably wont be needed

Remove it by using owner->w()/h(). Also updateState() is the right place for the resize, it means one update per frame. Otherwise you get a laggy resize. Now it's really smooth. Also covers scale changes, which come without a configure.

0x0

Not a risk. createWindow will wait for first configure event and sets everything up right away. Also if all input were 0 from both app and compositor, then it would fallback to 1x1. But that would require an already weird setup.

Comment thread Engine/system/api/waylandapi.cpp
@Try
Try merged commit 17b0b5b into Try:master Oct 22, 2026
4 checks passed
@Try

Try commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Merged!
Thanks for your big work 👍

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants