hardmode-triangle-1
Grokking the wayland protocol and creating a window.
Summary
In this part, we will be exploring wayland: what it is, what it does, how it works, and how know that its working before we have our window on screen.
Parts
- Part 0
- Introduction and why I decided to write an OpenGL hello-triangle program the hard way: no sdl, no glfw, only Linux platform libraries.
- Part 1
- Current page
- Part 2
- Initializing our OpenGL context and using EGL to connect it to our window.
Overview
Wayland is a protocol for communicating between a graphical application and the host system. The program which has a some sort of image to present, like our humble triangle, is called a wayland client, and the program that ultimately decides where on screen that image shows up is called the compositor or server.
When we say wayland there are technically two things we could be describing:
- wayland protocol — a specification which defines messages that can be sent between processes and how they are encoded and decoded.
- libwayland — a C library which implements functions to encode, decode, and send wayland protocol messages as well as extra utilities like an event loop.
While it is possible to write a wayland application without using libwayland, we will be using libwayland for this guide.
By the end of this guide, we will understand the basic functionality of wayland!
Getting Started
First thing you will need is to make sure you have the wayland libraries installed on your system.
As previously mentioned, we need to install libwayland. On my system I
needed to install libwayland-dev
(Ubuntu) to get the header files, etc.
# libwayland-bin contains wayland-scanner and more
Once our dependencies are installed, we can begin our zig project. Create a new directory and then create a new project in that directory.
&&
We will be using the zig-wayland
package as the zig bindings to the wayland
protocol. As of writing this article, it does not support the package manager,
so we will need to use a git submodule. We can create a folder called deps
and add the submodule.
Wayland Protocol
The base wayland protocol defines a few basic types and how to encode/decode
them. This is called the wire format. libwayland
also has conventions
on how to transport those messages between client and server, usually over a
Unix socket. These messages are passed back-and-forth asynchronously.
The wayland protocol works by first creating protocol objects and then essentially calling functions on those objects. However, because protocol messages are asynchronous, instead the client makes requests to the server and the server sends events to the client.
The valid requests and events for each object are defined by their interface. Some requests will create a new object of a particular interface and create an ID for that object. That ID then both identifies which interface and which object is being referred to. But that leaves us with a problem: if we need to send a message to create an object, but we also have to have an object in order to send a message, how do we get the first object?
That brings us to wl_display
. This interface is special in that it is the
entry point to wayland. It always implicitly exists and thus we can use it to
begin creating interface objects which will let us send requests and events.
Building Wayland
Before we start using wayland it can be helpful to understand a bit more about
the build process: If we take a closer look at the libwayland-dev
package using
dpkg -L
we can see that it installs an XML file called wayland.xml
.
|
The wayland.xml
file defines the protocol interfaces:
31
32
33 34
35
36 <!-- truncated -->
37
There is an application called wayland-scanner
which can read sections of the
protocol XML and automatically generate the C code necessary to send or receive
the various requests and events. This allows for protocol extensions to be
described only in terms of the protocol itself, with the necessary C code being
automatically generated.
Following this pattern, zig-wayland
also reads the wayland protocol XML to
generate zig code. Thanks to the zig build system, we can generate this code
in our build.zig
file by importing the Scanner
defined by zig-wayland
.
1 const std = @import("std");
2 const Scanner = @import("deps/zig-wayland/build.zig").Scanner;
We can then create a scanner and create a new module from the code that it
generates, and also link to the system library for libwayland-client
, as we
will be depending on its functionality.
28 // The scanner generates the code necessary to read and write different
29 // wayland protocol messages to communicate with the compositor, but only
30 // for the protocols and versions we need.
31 const scanner = Scanner.create(b, .{});
32 const wayland = b.createModule(.{ .source_file = scanner.result });
33 exe.addModule("wayland", wayland);
34 exe.linkLibC();
35
36 exe.linkSystemLibrary("wayland-client");
37
38 // TODO: remove when https://github.com/ziglang/zig/issues/131 is implemented
39 scanner.addCSource(exe);
Note that we haven't actually used the scanner to explicitly generate any code for any protocol interfaces yet. There are a few that are automatically generated:
wl_display
— represents our connection between client and server and used for core protocol features.wl_registry
— manages singleton interfaces, also a way to find out the capabilities of the server, and declare the desire to use them.wl_callback
— a way for a client to be notified when a request completes.wl_buffer
— rectangular buffer of pixels to be displayed.
So with that done, we can turn our attention to our main function and start our very first interactions with the wayland protocol: connecting to a display.
globals
In this case, globals are singleton objects that are entry-points into
different aspects of the protocol. The wl_shm
global, for example, allows
for creating buffers of memory on the CPU for sharing rendered images from our
application to the window manager using file descriptors.
The first thing we need to do is connect to our display; zig-wayland
defines
a few types for the client to use functionality from libwayland-client
. We
will create a client.wl.Display
, which will set up the transport (over a Unix
socket) between our application and the server.
1 const std = @import("std");
2
3 const wayland = @import("wayland");
4 const wl = wayland.client.wl;
5
6 pub fn main() !void {
7 std.log.info("Hardmode triangle.", .{});
8
9 const display = try wl.Display.connect(null);
10 defer display.disconnect();
11 }
Let's look at a simplified snippet of the wayland.xml
definition of the
wl_display
interface to see what we can do with it.
In order to get access to more functionality, we need to use the registry to get access to the global IDs.
9 const display = try wl.Display.connect(null);
10 defer display.disconnect();
11 const registry = try display.getRegistry();
12 defer registry.destroy();
If we try running our app with zig build run
nothing has really happened yet.
Hopefully we haven't gotten any errors, but without any other feedback it can
be frustrating or disappointing, or just hard to really understand what is going
on. An immensely useful trick that really helped me figure out problems I was
running into was using the WAYLAND_DEBUG=1
environment variable:
WAYLAND_DEBUG=1 zig build run
This will log every incoming and outgoing protocol message, allowing us to
ensure things are happening and working, and we will use this throughout the
guide. If we run with our trusty WAYLAND_DEBUG=1
variable set like above, we
will now see the following output:
info: Hardmode triangle.
[ 811200.514] -> wl_display@1.get_registry(new id wl_registry@2)
The second line there shows the out-going request associated with the call
to Display.getRegistry
. Its meaning is pretty straightforward:
-->
— Indicates this is a request made by the clientwl_display@1
— The interface and ID of the objectget_registry
— The request being callednew id wl_registry@2
— The arguments list of the request
Notice that from this request, however, that the client tells the server
the ID (2
) of the new wl_registry
object.
Now let us use the registry!
From perusing the registry XML, or from looking at the zig
code in your generated wayland_client.zig
, you will find that the registry
interface exposes the following requests and events:
- bind. request
- Bind a client-created global to the server, announcing both the ID and interface.
- global. event
- Notifies the client that a particular global is available.
- global_remove. event
- Notifies the client when a particular global is no longer available and needs to be destroyed.
The server will use the global event to announce its capabilities, 1-by-1. The client then tells the server that it will use one of those capabilities by sending a bind request.
But how do we get events?
In zig-wayland
and libwayland
, this is done using callbacks. The
wl.client.Registry
struct has a function setListener
which sets the
callback function. When an event of the right type is received, the callback
gets invoked with the event data, as well as some data
that we provide to
enable us affect the state of the program.
181 pub inline fn setListener(
182 _registry: *Registry,
183 comptime T: type,
184 _listener: *const fn ( void,
185 _data: T,
186 ) void
Let's create a type which will store the globals we bind in the registry, and create a no-op callback function.
6 const Globals = struct {};
7
8 fn registryListener(
9 registry: *wl.Registry,
10 event: wl.Registry.Event,
11 globals: *Globals,
12 ) void {
13 _ = globals;
14 _ = event;
15 _ = registry;
16 }
Then let us bind the listener to the registry in our main function:
23 const registry = try display.getRegistry();
24 defer registry.destroy();
25
26 var globals = Globals{};
27 registry.setListener(*Globals, registryListener, &globals);
Now we should be all set to receive events! If we run with WAYLAND_DEBUG=1
,
we should, in theory, see the events popping up in our terminal.
info: Hardmode triangle.
[2339656.103] -> wl_display@1.get_registry(new id wl_registry@2)
However, we don't receive any events! The wayland protocol is asynchronous.
We haven't done anything to wait for or parse any events at this point.
libwayland-client
provides some useful event-loop functionality to help with
waiting for events. Let's use the wl.Display.roundtrip()
function:
26 var globals = Globals{};
27 registry.setListener(*Globals, registryListener, &globals);
28
29 if (display.roundtrip() != .SUCCESS) return error.RoundtripFailure;
Now we should see the events for the registry, as well as something else (truncated for clarity):
info: Hardmode triangle.
[3953717.007] -> wl_display@1.get_registry(new id wl_registry@2)
[3953717.043] -> wl_display@1.sync(new id wl_callback@3)
[3953717.321] wl_display@1.delete_id(3)
[3953717.354] wl_registry@2.global(1, "wl_compositor", 5)
[3953717.370] wl_registry@2.global(2, "wl_drm", 2)
[3953717.383] wl_registry@2.global(3, "wl_shm", 1)
[3953717.394] wl_registry@2.global(4, "wl_output", 4)
[3953717.453] wl_registry@2.global(9, "xdg_wm_base", 4)
[3953717.521] wl_registry@2.global(15, "wl_seat", 8)
[3953717.600] wl_registry@2.global(22, "zwp_linux_dmabuf_v1", 4)
[3953717.656] wl_registry@2.global(27, "xdg_activation_v1", 1)
[3953717.667] wl_callback@3.done(185275)
There is now a sync request being called for the display, which creates a callback object; that callback object will get a done event back when the display has handled all the requests up to the sync request.
So what the roundtrip
function is doing for us is making a sync request and
listing to all the events until the callback done event comes back with
the matching ID.
Note that the global events received on your device may differ, as each wayland compositor will support different functionality.
Compositor
The first real global we need now is the wl_compositor
. The compositor
interface is responsible for combining all the things we want to show on
screen into a single cohesive output.
Rather than windows, we display things to surfaces. From wayland.xml
's
description: A surface is a rectangular area that may be displayed
on zero or more outputs, and shown any number of times at the compositor's
discretion. They can present
Surfaces can fill a number of roles from
top-level application window, to popup or modal window, to even being the
cursor.wl_buffer
s, receive user input, and define a
local coordinate system.
We can use the compositor to create a surface:
190
191
192 A compositor. This object is a singleton global. The
193 compositor is in charge of combining the contents of multiple
194 surfaces into one displayable output.
195
196
197
198
199 Ask the compositor to create a new surface.
200
201
202
203
204 <!-- truncated -->
205
However, the wl_compositor
interface is not included among the default
interfaces that are generated by zig-wayland
. Before we can use it, we need
to generate that interface in our build.zig
file:
31 const scanner = Scanner.create(b, .{});
32 const wayland = b.createModule(.{ .source_file = scanner.result });
33
34 scanner.generate("wl_compositor", 1);
35
36 exe.addModule("wayland", wayland);
This will read our wayland.xml
file and generate the wl_compositor
interface at the specified version (1).
Now, in main.zig
, we can update our Globals
struct to contain a compositor
and bind it in our registry listener function:
6 const Globals = struct {
7 compositor: ?*wl.Compositor = null,
8 };
9
10 fn registryListener(
11 registry: *wl.Registry,
12 event: wl.Registry.Event,
13 globals: *Globals,
14 ) void {
15 switch (event) {
16 .global => |global| {
17 const compositor = wl.Compositor.getInterface().name;
18 if (std.mem.orderZ(u8, global.interface, compositor) == .eq) {
19 globals.compositor = registry.bind(
20 global.name,
21 wl.Compositor,
22 1,
23 ) catch return;
24 }
25 },
26 .global_remove => {},
27 }
28 }
Our listener will take a single event type for the interface as a union(enum)
;
each variant will correspond to one of the event types, and its fields are the args
of the event.
If we take a closer look at the wl.Registry.Event
's definition in the generated
wayland_client.zig
file:
171 pub const Event = union(enum) {
172 global: struct {
173 name: u32,
174 interface: [*:0]const u8,
175 version: u32,
176 },
177 global_remove: struct {
178 name: u32,
179 },
180 };
The global event identifies the interface
with a null terminated string and the
instance with a 32-bit number as its name
. When our client application binds the
instance, it will identify the global with its name
. We receive one event for
every global, so we must match the name of the global to see if it is one of the
interfaces we are interested in.
If we check the output with WAYLAND_DEBUG
one more time, among all the events
we will see our outgoing bind request:
[1684599.075] wl_registry@2.global(1, "wl_compositor", 5)
[1684599.093] -> wl_registry@2.bind(1, "wl_compositor", 1, new id [unknown]@4)
We can see the bind request in response to the global event. After this point, we are able to use the compositor interface to create a surface like so:
41 if (display.roundtrip() != .SUCCESS) return error.RoundtripFailure;
42
43 const compositor = globals.compositor orelse return error.NoWlCompositor;
44 const surface = try compositor.createSurface();
45 defer surface.destroy();
Make sure to insert this after the roundtrip
has completed; otherwise, the
compositor will still be null
!
Roles
Now that we have a surface
, the compositor won't necessarily know what to
do with our surface right off the bat, so we need to passing our surface a
role.
For this we need to use an extension called xdg shell. If you search
your wayland.xml
file, you will notice that
xdg
does not appear in any interface
names.
xdg — Cross Desktop Group, an organization based at freedesktop.org dedicated to interoperability between open-source desktop environments. They maintain standards like wayland and its predecessor Xorg.
Since the protocol extensions for the xdg shell are not located in our
base wayland.xml
file, we need to load the extensions in our build.zig
files. Wayland protocols can be installed system-wide at a file-path that
should be something like /usr/share/wayland-protocols
; from within there,
you should find a directory for stable protocols, unstable protocols, and
staging protocols. xdg shell is a stable protocol, so on my system
xdg-shell.xml
is located at:
/usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml
On my Ubuntu system, these protocol files come from the wayland-protocols
package, which may be different on your system.
The first interface inside should be xdg_wm_base
which says: The
xdg_wm_base
interface is exposed as a global object enabling clients to turn
their wl_surfaces
into windows in a desktop environment. It defines the basic
functionality needed for clients and the compositor to create windows that can
be dragged, resized, maximized, etc., as well as creating transient windows such
as popup menus.
So, in order to build the code necessary to interact with the xdg shell
interface, we add the protocol XML to our scanner and then generate our global
xdg_wm_base
interface:
34 scanner.generate("wl_compositor", 1);
35
36 // Relative to wayland-protocols filepath
37 scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
38 scanner.generate("xdg_wm_base", 1);
This will generate code that lives in xdg_shell_client.zig
in the
zig-cache/o
directory.
I should note it is also possible to create or use custom extensions, like river window manager does.
Since these interfaces are now coming from a new XML file with a new prefix, they will also be generated in a new namespace.
3 const wayland = @import("wayland");
4 const wl = wayland.client.wl;
5 const xdg = wayland.client.xdg; // xdg_* interfaces
6
7 const Globals = struct {
8 compositor: ?*wl.Compositor = null,
9 wm_base: ?*xdg.WmBase = null,
10 };
11
And then, inside the match for the .global
event in our registryListener
function, we can bind the xdg_wm_base
global:
27 const wm_base = xdg.WmBase.getInterface().name;
28 if (std.mem.orderZ(u8, global.interface, wm_base) == .eq) {
29 globals.wm_base = registry.bind(
30 global.name,
31 xdg.WmBase,
32 1,
33 ) catch return;
34 }
If we run again with our WAYLAND_DEBUG=1
environment variable, we will
see the bind request triggering when we receive the global event for the
xdg_wm_base
.
[1161268.904] wl_registry@2.global(9, "xdg_wm_base", 4)
[1161268.917] -> wl_registry@2.bind(9, "xdg_wm_base", 1, new id [unknown]@5)
Now we can start assigning roles. If we look in our xdg-shell.xml
file,
we find that the xdg_wm_base
has a request called get_xdg_surface
:
It takes our wl_surface
and provides us with an xdg_surface
which allows us to
assign a role. There are two roles currently; according to xdg-shell.xml
:
xdg_toplevel
This interface defines an
xdg_surface
role which allows a surface to, among other things, set window-like properties such as maximize, full-screen, and minimize, set application-specific metadata like title and ID, and well as trigger user interactive operations such as interactive resize and move.xdg_popup
A popup surface is a short-lived, temporary surface. It can be used to implement for example menus, popovers, tool-tips and other similar user interface concepts.
We will use the xdg_toplevel
interface for our surface like so:
53 const compositor = globals.compositor orelse return error.NoWlCompositor;
54 const wm_base = globals.wm_base orelse return error.NoWmBase;
55
56 const surface = try compositor.createSurface();
57 defer surface.destroy();
58 const xdg_surface = try wm_base.getXdgSurface(surface);
59 defer xdg_surface.destroy();
60 const xdg_toplevel = try xdg_surface.getToplevel();
61 defer xdg_toplevel.destroy();
62
63 xdg_toplevel.setTitle("Hardmode Triangle");
With this we have successfully:
- Extended our surface with the
xdg_surface
interface - Assigned it the
xdg_toplevel
role - Set the window title to "Hardmode triangle"
However, our application exits as quickly as it starts, we don't have a window
yet, and there are events from the wm_base
, xdg_surface
, and xdg_toplevel
interfaces that you should handle in a real application, but we can ignore some
of them for now as they will not prevent us from presenting to the screen.
If we add a listener to our xdg_toplevel
, we can listen for a close event:
And in our fn main
:
74 // Set running to false when we get an event signaling our toplevel surface
75 // was closed.
76 var running = true;
77 xdg_toplevel.setListener(*bool, xdgToplevelListener, &running);
Now, if we wanted, we could add a main loop. However, since we don't have anything to present to our surface just yet anyway, we can hold off on that for now.
And with that, we are done with part 1, feel free to move on to part 2. The rest of this document will cover some optional improvements that can be made to the zig code.
Comptime
You may have noticed that the code for retrieving each of our globals has the same structure for each global:
12 fn registryListener(
13 registry: *wl.Registry,
14 event: wl.Registry.Event,
15 globals: *Globals,
16 ) void {
17 switch (event) {
18 .global => |global| {
19 // Bind wl_compositor
20 const compositor = wl.Compositor.getInterface().name;
21 if (std.mem.orderZ(u8, global.interface, compositor) == .eq) {
22 globals.compositor = registry.bind(
23 global.name,
24 wl.Compositor,
25 1,
26 ) catch return;
27 }
28
29 // Bind xdg_wm_base
30 const wm_base = xdg.WmBase.getInterface().name;
31 if (std.mem.orderZ(u8, global.interface, wm_base) == .eq) {
32 globals.wm_base = registry.bind(
33 global.name,
34 xdg.WmBase,
35 1,
36 ) catch return;
37 }
38 },
39 .global_remove => {},
40 }
41 }
- get the string identifying the type of the global to compare against the
global.interface
by calling:GlobalType.getInterface().name
- compare the type of the interface from the event to the type of the global
we are trying to bind using
std.mem.orderZ
- populate the bind request using the identifying number in
global.name
and the type of our global soregistry.bind
can send the request and return an instance of our global object of the right type.
From this we can make an observation: all the information we need can be
inferred from the type of each field. Or, in other words, we can use each
field's @typeInfo
to handle the global event.
We can use an inline for
to iterate through the fields to start off with:
19 inline for (@typeInfo(Globals).Struct.fields) |field | {
20 const global_type = field.type;
21 _ = global_type;
22 }
However, there is a catch: the fields on our Globals
struct are not of the
interface type, they are optional pointers. This means we cannot directly call
global_type.getInterface().name
. Instead, we've got to use @typeInfo
a couple
more times to unwrap the Optional
and the Pointer
and get the child type.
19 inline for (@typeInfo(Globals).Struct.fields) |field | {
20 const pointer_type = @typeInfo(field.type).Optional.child;
21 const global_type = @typeInfo(pointer_type).Pointer.child;
22 const interface = global_type.getInterface().name;
23
24 _ = interface;
25 }
Now we can follow along with the implementation for comparing the interface
and binding the global as before, using @field
and the field.name
from
our field's metadata to access the field:
19 inline for (@typeInfo(Globals).Struct.fields) |field | {
20 const pointer_type = @typeInfo(field.type).Optional.child;
21 const global_type = @typeInfo(pointer_type).Pointer.child;
22 const interface = global_type.getInterface().name;
23
24 // Compare the interface types to see if they match
25 if (std.mem.orderZ(u8, global.interface, interface) == .eq) {
26
27 // Bind the global to the appropriate field
28 @field(globals, field.name) = registry.bind(
29 global.name,
30 global_type,
31 1,
32 ) catch return;
33 }
34 }
With this in place, any time we add a new global, we only have to add a
field for it to our Globals
struct, enabling a more declarative style of
programming. Running WAYLAND_DEBUG=1 zig build run
should produce the same
results as before.
References
This page is referenced by the following documents: