Welcome to Wlroots! Part 3
This blog post is part of the Welcome to Wlroots! series I’m writing. In case you haven’t yet, it’s better if you go back to read parts 1 and 2 of this series. This post develops on what we worked in those two.
Rendering windows! Yes, now we’re gonna be doing the fun part. In this post, we’ll only be getting some windows on our screen, not input nor moving them around nor anything else yet.
We’ll keep track of the surfaces we receive and of XDG windows, and show them on screen without any flashy layout yet. This should be a relatively short post, though at the time of writing this I still have no clue of how that’ll work. We’ll see on the go.
Initializing XDG
We currently have barely any globals set up. XDG requires one, so let’s create it. The types for
our global will mostly live under wlr/types/wlr_xdg_shell.h.
#include <wlr/types/wlr_xdg_shell.h>
You may already know that Wayland protocols are described via XML and that we have to autogenerate code to use those protocols. If you didn’t know that, all good. It’s just adding some commands to our Makefile.
# The directory where our protocols are defined.
PROTOCOLS_XML=$(shell pkg-config --variable=pkgdatadir wayland-protocols)
# The path to the XDG shell protocol definition.
XDG_SHELL_XML=$(PROTOCOLS_XML)/stable/xdg-shell/xdg-shell.xml
We’ll be storing our autogenerated code in a new folder called wayland/ under our project. Make
sure to have libwayland-dev and wayland-protocols installed on your system.
Our build command will then look like this:
build:
mkdir -p wayland
wayland-scanner server-header "$(XDG_SHELL_XML)" wayland/xdg-shell-protocol.h
wayland-scanner private-code "$(XDG_SHELL_XML)" wayland/xdg-shell-protocol.c
# markdown formater is not letting me keep this line empty, remove this comment
mkdir -p build
clang src/main.c $(FLAGS)
VS Code tip!
The C/CPP language server won’t automatically detect these and will show errors even when the project compiles just fine. To fix them, you can add the following to your
.vscode/c_cpp_properties.json(create it if it doesn’t exist).{ "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/**", "${workspaceFolder}/wayland", "/usr/include/wlroots-0.19", "/usr/include/pixman-1" ], "defines": [], "cStandard": "c23", "cppStandard": "c++23", "intelliSenseMode": "linux-clang-x64", "configurationProvider": "mesonbuild.mesonbuild", "compilerPath": "/usr/bin/clang" } ], "version": 4 }
Run make and the artifacts will be autogenerated, your binary will compile just fine.
Now yes! Time to create our XDG shell global.
struct nye_server
{
// -- snip --
struct wlr_xdg_shell *wlr_xdg_shell;
// -- snip --
};
int main(void) {
// -- snip --
nye_server.wlr_xdg_shell = wlr_xdg_shell_create(nye_server.wl_display, 6);
assert(nye_server.wlr_xdg_shell);
assert(wlr_backend_start(nye_server.wlr_backend));
// -- snip --
}
A note on the version, we’re using number 6 even when the latest version is number 7 because
wlroots-0.19 does not come with support for number 7. At the time of writing, wlroots-0.20 is
already available with support for the latest version. I have not checked it out, it may work out
of the box but you may have to do some investigation on your own to see if anything has changed in
the code we’re using here.
Now that we’re creating our second global, it’s a good time to welcome in wayland-info.
wayland-info is a linux command that lets us inspect the globals our compositor exposes. It comes
in the wayland-utils package.
$ WAYLAND_DISPLAY=way-nyeki wayland-info
interface: 'xdg_wm_base', version: 6, name: 1
interface: 'wl_output', version: 4, name: 2
name: WL-1
description: Wayland output 1
x: 0, y: 0, scale: 1,
physical_width: 0 mm, physical_height: 0 mm,
make: 'Unknown', model: 'Unknown',
subpixel_orientation: unknown, output_transform: normal,
mode:
width: 1280 px, height: 720 px, refresh: 0.000 Hz,
flags: current
As you can see, we only expose two globals. Our output shows a 0.000 Hz refresh rate because nested Wayland outputs don’t have modes. In a real DRM monitor, you’ll see your monitor’s actual values.
More Globals
We’ll start by handling top-level windows. They’re called top-levels because they’re the highest in the window hierarchy. Popups and dropdown menus will always be children of a top-level.
Top-levels and popups are just wrappers over a surface. They get extra methods to call and events to receive based on the role (top-level and popups are “roles”) the surface has. E.g. you can request to full-screen a top-level, but not a popup.
Same as we did with our outputs, we’ll listen for new window events and add them to our scene.
struct nye_server {
// -- snip --
struct wl_listener on_xdg_toplevel_add;
// -- snip --
};
static void on_xdg_toplevel_add(struct wl_listener*, void*) {
printf("New toplevel!\n");
}
int main(void) {
// -- snip --
nye_server.wlr_xdg_shell = wlr_xdg_shell_create(nye_server.wl_display, 6);
assert(nye_server.wlr_xdg_shell);
nye_server.on_xdg_toplevel_add.notify = on_xdg_toplevel_add;
wl_signal_add(&nye_server.wlr_xdg_shell->events.new_toplevel, &nye_server.on_xdg_toplevel_add);
// -- snip --
}
To test things out, I’ll be using weston-simple-shm from the weston compositor package. Weston
is Wayland’s official reference compositor, so the package comes with the compositor too. We only
want it for the dev tooling.
$ WAYLAND_DISPLAY=way-nyeki weston-simple-shm
No wl_shm global
Wayland clients send the compositor their window graphics by SHaring Memory. This makes the transport way more efficient than by making a copy of all pixel data and sending it over the socket connection directly. There are also other globals that do similar things, like Linux’s dmabuf (for rendering on the GPU instead of on the CPU).
Fortunately, initializing wl_shm is as simple as calling a function on our display. Let’s do
that.
// -- snip --
#include <wlr/types/wlr_shm.h>
int main(void) {
// -- snip ---
wlr_shm_create_with_renderer(nye_server.wl_display, 2, nye_server.wlr_renderer);
// -- snip --
}
Again, the wl_shm global’s latest version at the time of writing is number 3. However, we’re
using wlroots-0.19 which does not have support for that version, so we use number 2 instead.
Now, let’s re-compile and rerun our compositor. Also run weston-simple-shm in a separate
terminal.
$ WAYLAND_DISPLAY=way-nyeki weston-simple-shm
Segmentation fault (core dumped) WAYLAND_DISPLAY=way-nyeki weston-simple-shm
I don’t think the segmentation fault is intentional, but in this case it tells us there’s something
missing. We can get wayland logs by setting the WAYLAND_DEBUG environment variable to 1, so let’s
do that and see what it tells us.
$ WAYLAND_DEBUG=1 WAYLAND_DISPLAY=way-nyeki weston-simple-shm
[ 991461.134] {Default Queue} -> wl_display#1.get_registry(new id wl_registry#2)
[ 991461.152] {Default Queue} -> wl_display#1.sync(new id wl_callback#3)
[ 991461.355] {Display Queue} wl_display#1.delete_id(3)
[ 991461.364] {Default Queue} wl_registry#2.global(1, "xdg_wm_base", 6)
[ 991461.375] {Default Queue} -> wl_registry#2.bind(1, "xdg_wm_base", 1, new id [unknown]#4)
[ 991461.378] {Default Queue} wl_registry#2.global(2, "wl_shm", 2)
[ 991461.381] {Default Queue} -> wl_registry#2.bind(2, "wl_shm", 1, new id [unknown]#5)
[ 991461.383] {Default Queue} wl_registry#2.global(3, "wl_output", 4)
[ 991461.385] {Default Queue} wl_callback#3.done(0)
[ 991461.388] {Default Queue} -> wl_display#1.sync(new id wl_callback#3)
[ 991461.479] {Display Queue} wl_display#1.delete_id(3)
[ 991461.483] {Default Queue} wl_shm#5.format(0)
[ 991461.486] {Default Queue} wl_shm#5.format(1)
[ 991461.487] {Default Queue} wl_shm#5.format(875709016)
[ 991461.489] {Default Queue} wl_shm#5.format(875708993)
[ 991461.491] {Default Queue} wl_shm#5.format(875710274)
[ 991461.492] {Default Queue} wl_shm#5.format(842094674)
[ 991461.494] {Default Queue} wl_shm#5.format(842088786)
[ 991461.496] {Default Queue} wl_shm#5.format(892426322)
[ 991461.498] {Default Queue} wl_shm#5.format(892420434)
[ 991461.499] {Default Queue} wl_shm#5.format(909199186)
[ 991461.501] {Default Queue} wl_shm#5.format(808665688)
[ 991461.503] {Default Queue} wl_shm#5.format(808665665)
[ 991461.504] {Default Queue} wl_shm#5.format(1211384408)
[ 991461.506] {Default Queue} wl_shm#5.format(1211384385)
[ 991461.508] {Default Queue} wl_shm#5.format(942948952)
[ 991461.509] {Default Queue} wl_shm#5.format(942948929)
[ 991461.511] {Default Queue} wl_callback#3.done(0)
Segmentation fault (core dumped) WAYLAND_DEBUG=1 WAYLAND_DISPLAY=way-nyeki weston-simple-shm
It’s not obvious by those logs, but what’s missing is another global, wl_compositor.
wl_compositor is not actually our compositor, rather the global clients use to create surfaces to
draw on. Initializing this global is just as easy as with the wl_shm one.
// -- snip --
#include <wlr/types/wlr_compositor.h>
int main(void) {
// -- snip --
wlr_compositor_create(nye_server.wl_display, 6, nye_server.wlr_renderer);
wlr_shm_create_with_renderer(nye_server.wl_display, 2, nye_server.wlr_renderer);
// -- snip --
}
You guessed right! The latest version of wl_compositor is 7, but wlroots-0.19, so we use number
6 instead.
Once that’s done, we can recompile and check weston-simple-shm’s logs again. The first part of
the logs is almost identical. The only difference is that now it displays our new wl_compositor
global too. The interesting part is what happens where we got a segfault before.
$ WAYLAND_DEBUG=1 WAYLAND_DISPLAY=way-nyeki weston-simple-shm
...
[1495901.534] {Default Queue} wl_callback#3.done(0)
[1495901.536] {Default Queue} -> wl_compositor#5.create_surface(new id wl_surface#3)
[1495901.538] {Default Queue} -> xdg_wm_base#4.get_xdg_surface(new id xdg_surface#7, wl_surface#3)
[1495901.541] {Default Queue} -> xdg_surface#7.get_toplevel(new id xdg_toplevel#8)
[1495901.543] {Default Queue} -> xdg_toplevel#8.set_title("simple-shm")
[1495901.545] {Default Queue} -> xdg_toplevel#8.set_app_id("org.freedesktop.weston.simple-shm")
[1495901.546] {Default Queue} -> wl_surface#3.commit()
[1495901.550] {Default Queue} -> wl_surface#3.damage(0, 0, 250, 250)
In our compositor’s logs, you’ll now also see
New toplevel!
Listening for New Windows
Great! Now we’re getting new windows to fire the callback in our compositor, so we can now do stuff about it.
The first thing we want to do is to create YAW (yet another wrapper) for toplevels.
struct nye_server {
// -- snip --
struct wl_list nye_xdg_toplevels; // nye_xdg_toplevel::link
// -- snip --
};
struct nye_xdg_toplevel {
struct wl_list link; // nye_server::nye_xdg_toplevels
struct nye_server *nye_server;
struct wlr_xdg_toplevel *wlr_xdg_toplevel;
struct wl_listener on_xdg_toplevel_del;
};
static void on_xdg_toplevel_del(struct wl_listener*, void*) {
printf("Bye toplevel!\n");
}
static void on_xdg_toplevel_add(struct wl_listener*, void*) {
printf("New toplevel!\n");
}
int main(void) {
// -- snip --
wl_list_init(&nye_server.nye_xdg_toplevels);
nye_server.on_xdg_toplevel_add.notify = on_xdg_toplevel_add;
// -- snip --
}
Next, we’ll have to create and store our new wrappers in the nye_xdg_toplevel wrapper.
XDG surfaces have a little handshake before they can be rendered on the screen. They use this to
send the compositor their initial properties, such as app name and window title, and to send their
ideal properties, such as size. Size is ideal because the app may want to have a specific size,
but it’s ultimately on the compositor to decide what size it actually gets. When the compositor is
done setting all those properties, it sends a configure event, the client acknowledges it, and
the rendering starts.
XDG surfaces work by sending over pending changes to the compositor and then committing them with a
commit() call. The flow is more or less like this:
- The client creates a surface
- The client creates a top-level to wrap it
- The client sets its inital data (e.g. window title, app name)
- The client commits all the initial data with a
commit() - The compositor stores that data and sends the client a
configureevent with its resulting initial state - The client acknowledges that configuration
- The client starts rendering
First, let’s handle adding and removing the XDG toplevels from our server’s list to be done with the boilerplate.
static void on_xdg_toplevel_del(struct wl_listener *wl_listener, void *)
{
printf("Bye toplevel!\n");
struct nye_xdg_toplevel *nye_xdg_toplevel = wl_container_of(wl_listener, nye_xdg_toplevel, on_xdg_toplevel_del);
wl_list_remove(&nye_xdg_toplevel->link);
wl_list_remove(&nye_xdg_toplevel->on_xdg_toplevel_del.link);
free(nye_xdg_toplevel);
}
static void on_xdg_toplevel_add(struct wl_listener *wl_listener, void *data)
{
printf("New toplevel!\n");
struct nye_server *nye_server = wl_container_of(wl_listener, nye_server, on_xdg_toplevel_add);
struct wlr_xdg_toplevel *wlr_xdg_toplevel = data;
struct nye_xdg_toplevel *nye_xdg_toplevel = calloc(1, sizeof(struct nye_xdg_toplevel));
nye_xdg_toplevel->nye_server = nye_server;
nye_xdg_toplevel->wlr_xdg_toplevel = wlr_xdg_toplevel;
nye_xdg_toplevel->on_xdg_toplevel_del.notify = on_xdg_toplevel_del;
wl_signal_add(&wlr_xdg_toplevel->events.destroy, &nye_xdg_toplevel->on_xdg_toplevel_del);
wl_list_insert(&nye_server->nye_xdg_toplevels, &nye_xdg_toplevel->link);
}
Boilerplate done, time to get to the fun part.
wlr_output has a wlr_scene_output, but our wlr_xdg_toplevel will get a wlr_scene_tree. This
is because, as we explained before, xdg_toplevel is the parent of its popups and other surfaces
that will get rendered with it.
Our toplevel is just a surface underneath, so let’s add that surface to the scene.
struct nye_xdg_toplevel
{
// -- snip --
struct wlr_scene_tree *wlr_scene_tree;
// -- snip --
};
static void on_xdg_toplevel_add(struct wl_listener *wl_listener, void *data)
{
// -- snip --
nye_xdg_toplevel->wlr_xdg_toplevel = wlr_xdg_toplevel;
nye_xdg_toplevel->wlr_scene_tree = wlr_scene_xdg_surface_create(&nye_server->wlr_scene->tree, wlr_xdg_toplevel->base);
// -- snip --
}
wlr_xdg_toplevel::base is the XDG surface the top-level wraps, which is itself a wrapper over a
wl_surface.
🤝
wlr_scene won’t send the initial configuration to the client, so it won’t render anything until
we handle the handshake ourselves. This part is easy, we’ll just tell the client to choose its own
size.
Configuration events get fired after the client sends its first commit, so let’s wait for it first.
struct nye_xdg_toplevel
{
// -- snip --
struct wl_listener on_xdg_surface_commit;
};
static void on_xdg_surface_commit(struct wl_listener *, void *) {}
static void on_xdg_toplevel_del(struct wl_listener *wl_listener, void *)
{
// -- snip --
wl_list_remove(&nye_xdg_toplevel->on_xdg_toplevel_del.link);
wl_list_remove(&nye_xdg_toplevel->on_xdg_surface_commit.link);
free(nye_xdg_toplevel);
}
static void on_xdg_toplevel_add(struct wl_listener *wl_listener, void *data)
{
// -- snip --
nye_xdg_toplevel->on_xdg_surface_commit.notify = on_xdg_surface_commit;
wl_signal_add(&wlr_xdg_toplevel->base->surface->events.commit, &nye_xdg_toplevel->on_xdg_surface_commit);
wl_list_insert(&nye_server->nye_xdg_toplevels, &nye_xdg_toplevel->link);
}
Great! Now that that’s done, let’s just check if the commit we received is the first commit. If it is, we set its size to 0x0 (which means to let the client pick its own size).
static void on_xdg_surface_commit(struct wl_listener *wl_listener, void *) {
struct nye_xdg_toplevel *nye_xdg_toplevel = wl_container_of(wl_listener, nye_xdg_toplevel, on_xdg_surface_commit);
// Our wrapper -> XDG top-level -> XDG surface -> Is this the first commit?
if (nye_xdg_toplevel->wlr_xdg_toplevel->base->initial_commit) {
// Calling wlr_xdg_toplevel_set_* schedules a configure event. It'll be sent when this
// callback is done running. Calling multiple set_* will batch them into a single configure
// event.
wlr_xdg_toplevel_set_size(nye_xdg_toplevel->wlr_xdg_toplevel, 0, 0);
}
}
Compile it and run it, then spawn weston-simple-shm on another terminal.
IT WORKS! You’ll see some hypnosis, don’t get hooked.
Now that this is done, you’re on solid ground to start playing with it. You can try changing its position, size, adding a solid sharp border with rects, or whatever you think would be cool to see in a real compositor.