Geometry handling in KWin/Wayland

It perhaps comes as no surprise that handling window position (or geometry, in general) is one of the most important responsibilities of the window manager. While it may look a very trivial task at quick glance, unfortunately, it’s not like that. In this blog post, I will describe how KWin/Wayland manages window geometry, which can be hopefully useful to people wishing to understand how KWin works.

Warning: that’s a bit technical blog post and requires some knowledge of KWin’s internals.

X11

So, let’s start off with an easy case. On X11, if the window manager wants to move or resize a window, it simply makes an XConfigureWindow() call and that’s it, the window manager doesn’t wait for the client to repaint the window.

While things look simple and obvious from the window manager’s perspective, the compositing manager may have troubles coping with that. For example, if a window is resized, but the application hasn’t repainted it yet and the compositing manager wants to perform compositing, what should be painted in a newly exposed region of the window? Without any synchronization between the compositing manager and the application, you will most likely see noise or other types of visual artifacts. Fortunately, it’s possible to alleviate the resizing issues by using the _NET_WM_SYNC_REQUEST protocol if the window manager is also a compositing manager, which is the case with KWin.

With the _NET_WM_SYNC_REQUEST, the client has to indicate that it’s willing to participate in that protocol by listing _NET_WM_SYNC_REQUEST in the WM_PROTOCOLS property and storing the id of XSync counter in the _NET_WM_SYNC_REQUEST_COUNTER property that will be used for synchronization between the window manager and the client.

The window manager will send the client a _NET_WM_SYNC_REQUEST client message with a sync counter value that the client has to put in _NET_WM_SYNC_REQUEST in order to indicate that it’s done handling resize.

While it allows the window manager to avoid flooding the client window with resize requests and the compositing manager to “freeze” the window until it’s repainted, it does nothing to help with synchronizing compositor’s GPU commands with the app’s GPU commands, which can still result in “noisy” resize.

KWin uses the _NET_WM_SYNC_REQUEST protocol only during interactive resize and to determine when it can start painting the window’s first frame. In the remaining cases, e.g. changing window’s maximize or fullscreen mode, a ConfigureNotify event will be sent to the client window and the window will be painted on the screen without any synchronization.

Wayland

Wayland has no single window type like on X11, instead a wl_surface object is given a role that indicates how the surface should be mapped or how it can be interacted with. The surface role defines how the surface can be resized. In this blog post, I will describe how xdg-toplevel surfaces are resized because they are used to build regular application windows.

In order to resize a window, the compositor has to send the client an xdg_toplevel.configure event indicating the desired window geometry size. Depending on the window state, the client should either obey the size in the configure event or treat the size as the maximum size. For example, if the window is maximized, the window must be repainted with the same exact size as in the configure event; if the window is being interactively resized, the window can be repainted at a smaller size to account for geometry constraints, for example aspect ratio. The window will be repainted with the new size after the client acknowledges the configure event and provides a buffer with the new size.

The fact that the compositor can send the maximum window geometry size in a configure event is unorthodox. On X11, the window manager knows for sure where the window will land after XConfigureWindow(), but on Wayland, the compositor only has a rough estimate what the geometry will be after the configure event is acknowledged and a new buffer is provided. It takes a while to get used to such a model.

For every window, KWin maintains four geometries – move resize geometry, frame geometry, client geometry, and buffer geometry. The last three geometries have already been described in the CSD support in KWin post. The move resize geometry is used only to resize or move the window.

In order to help better understand how all four geometries work together, let’s consider that a window needs to be resized to (400×300). The first step that has to be taken is to change the move resize geometry’s size to (400×300). After that, a configure event will be sent with the new size. Note that the other three geometries are left unchanged, they will be updated only after the client provides a new buffer. It’s worth pointing out that the frame geometry can be different after resize, for example it can be (300×300) if 1:1 aspect ratio is forced.

What if you want to maximize a window? Two things should happen – the window has to be moved to the top-left corner of the work area and it should be resized so its size matches the work area size. The simplest way to implement it would be to move the window immediately to the target position without waiting for an acknowledgement from the client, and resize the window after a new buffer is provided. It’s easy to implement, but if there needs to be a smooth transition from the normal state to the maximize state, such an animation won’t look good. In order to fix it, the window needs to be moved and resized at the same time when a new buffer is provided.

So, just put the window position in configure events, easy, right? Well, not really. What should happen if the user wants to move the window while there’s a pending move? If it’s not handled properly, the older window position from the configure event can override the newer window position.

That problem is solved by adding a special flag to every configure event that indicates whether it affects the window position and amending pending configure events (unsetting the ConfigurePosition flag) if the window needs to be moved immediately

class XdgSurfaceConfigure
{
public:
   enum ConfigureFlag {
       ConfigurePosition = 0x1,
   };
   Q_DECLARE_FLAGS(ConfigureFlags, ConfigureFlag)

   ...
   ConfigureFlags flags;
};

There are three functions to change the geometry of a window – move(), resize(), and moveResize(). Neither function is interchangeable because each of them has a specific semantic meaning. For example, moveResize() indicates that the window has to be moved and resized in one atomic operation, while move() says that the window has to be moved to the given position now regardless of whether there are pending moves.

There’s still one tiny issue. If the window is resized by dragging its top-left corner, the bottom-right corner should remain static. In case the window has custom geometry constraints, the window can bounce during resize if its bottom-right corner is not snapped properly. Similar to handling of pending moves, this issue is solved by adding more information to configure events so KWin can calculate correct frame geometry when a new buffer is committed.

class XdgSurfaceConfigure
{
public:
   enum ConfigureFlag {
       ConfigurePosition = 0x1,
   };
   Q_DECLARE_FLAGS(ConfigureFlags, ConfigureFlag)

   QRect bounds;
   Gravity gravity;
   ...
   ConfigureFlags flags;
};

There are two new things in the configure event – the bounding rectangle and the gravity. The bounding rectangle is the same as the move resize geometry. The gravity indicates the direction in which the geometry changes during resize. If the window is resized by dragging its top-left corner, the gravity will point in the top-left direction, i.e. only the top-left window corner can move. The bounding rectangle is needed to compute how much the frame geometry has to move so its bottom-right corner is static.

Just to recap, if a window needs to be resized, the first thing that will happen is that the move resize geometry size will be updated and a configure event will be sent to the client. After the configure event is acknowledged and a new buffer is provided, kwin will compute the new frame geometry, client geometry, and buffer geometry based on the information stored in the configure event.

If a window needs to be moved immediately, the move resize geometry position will be updated. All pending configure events will be amended so they don’t have the ConfigurePosition flag set and, finally, the position of the frame geometry, the client geometry, and the buffer geometry will be updated to the new position.

And that’s all I have for today. Hope this was useful.