Understanding Wayland: Deep Dive Through C Programming
Goal: By completing this guide, you will understand the modern Linux graphics stack from the wire protocol all the way down to scanout. You will be able to explain, in your own words, how a Wayland client negotiates globals, creates surfaces, submits buffers, and schedules frames, and how a compositor turns those buffers into pixels on a real display. You will also learn how the compositor centralizes security and policy, how input is routed, and how X11 compatibility works via Xwayland. By the end, you will have built a real Wayland client, a minimal compositor, a custom protocol, a panel, and an X11 comparison client, giving you the mental model and code artifacts needed to contribute to desktop environments, embedded UIs, or graphics infrastructure.
Introduction: What This Guide Covers
Wayland is a display protocol for modern Linux desktops. It defines how clients talk to a compositor: how windows are created, how pixel buffers are shared, how input events are delivered, and how the compositor enforces policy. Wayland is not a drawing API; the client renders pixels itself (CPU or GPU) and shares a buffer with the compositor. The compositor is both display server and window manager, responsible for compositing, input routing, and presentation timing.
What you will build:
- A bare-metal Wayland client (no GTK/Qt) that negotiates globals and draws using
wl_shm - A minimal compositor using wlroots, including output setup, input handling, and a scene graph
- A custom Wayland protocol extension with XML + generated bindings
- A layer-shell panel (status bar / overlay) integrated with the compositor
- A minimal X11 client to compare legacy vs modern architecture
Scope (included):
- Core Wayland protocol:
wl_display,wl_registry,wl_surface,wl_buffer,wl_seat - Shell protocols:
xdg-shell,layer-shell, scaling and activation - Buffer lifecycles: shared memory, DMA-BUF, release semantics
- Compositor responsibilities: scene graph, damage tracking, input focus
- Output pipeline: DRM/KMS, planes, atomic commit
- X11 interop: Xwayland architecture and limitations
Out of scope (intentionally):
- Writing GPU drivers or kernel display drivers
- Building a full toolkit (GTK, Qt) or advanced shader pipelines
- Full color management / HDR pipeline design
Big Picture (ASCII)
Client App (renders) Wayland Protocol Compositor (policy+render) Kernel/GPU Display
+-----------------------+ +------------------+ +---------------------------+ +------------+ +--------+
| wl_surface + buffers |-->| UNIX socket msgs |-->| scene graph + damage + KMS |-->| DRM/KMS/GBM |-->| monitor |
| wl_shm or DMA-BUF |<--| events + frame cb |<--| input routing + focus |<--| input evdev |-->| |
+-----------------------+ +------------------+ +---------------------------+ +------------+ +--------+
How to Use This Guide
- Read the primer first. The Theory Primer is your mini-book. Skim it once, then re-read relevant chapters before each project.
- Build projects in order. Each project depends on the mental model of the previous one.
- Observe the wire. Use
WAYLAND_DEBUG=1to see requests/events as they happen. - Treat this as a lab. Keep a scratchpad of diagrams, event traces, and questions.
- Compare with X11 early. After Project 1, do Project 5 while the contrast is vivid.
Prerequisites & Background Knowledge
Essential Prerequisites (Must Have)
- C programming: pointers, structs, manual memory management, function pointers
- Book: Effective C, 2nd Edition (Seacord) – Ch. 1-6
- Linux systems programming: file descriptors,
mmap, UNIX sockets, event loops- Book: The Linux Programming Interface (Kerrisk) – Ch. 4-5, 49, 63
- Basic graphics mental model: framebuffers, pixels, raster images
- Book: Computer Graphics from Scratch (Gambetta) – Ch. 1-2
Helpful But Not Required (You’ll learn while building)
- DRM/KMS basics, EGL/GL context creation
- Input stack (evdev, libinput, xkbcommon)
- Protocol design and versioning
Self-Assessment Questions
- Can you debug a segfault with
gdband fix a use-after-free? - Have you used
mmapand understood its lifetime rules? - Can you read and reason about
poll()orepoll()loops? - Can you compile a C project using
pkg-configand link to libraries?
Development Environment Setup
# Debian/Ubuntu
sudo apt install build-essential pkg-config libwayland-dev wayland-protocols \
libxkbcommon-dev libegl1-mesa-dev libgles2-mesa-dev libdrm-dev libgbm-dev \
libinput-dev libsystemd-dev meson ninja-build
# Fedora
sudo dnf install gcc make pkg-config wayland-devel wayland-protocols-devel \
libxkbcommon-devel mesa-libEGL-devel mesa-libgbm-devel libdrm-devel \
libinput-devel systemd-devel meson ninja-build
# Arch
sudo pacman -S base-devel pkg-config wayland wayland-protocols \
libxkbcommon mesa libdrm libinput systemd meson ninja
For compositor development (wlroots):
# Debian/Ubuntu
sudo apt install libwlroots-dev
# Fedora
sudo dnf install wlroots-devel
# Arch
sudo pacman -S wlroots
Time Investment (10-15 hours/week)
| Project | Time Range | Why |
|---|---|---|
| Project 1: Bare-Metal Client | 1-2 weeks | Protocol lifecycle + buffers |
| Project 2: Compositor | 3-4 weeks | Outputs + input + scene graph |
| Project 3: Custom Protocol | 1 week | XML + versioning |
| Project 4: Layer-Shell Panel | 1-2 weeks | Drawing + layout |
| Project 5: X11 Comparison | 1 week | Architecture contrast |
Important Reality Check
- Expect crashes from strict protocol ordering.
- Expect permission issues (
/dev/driaccess,videogroup). - Expect non-linear debugging (event-driven logic).
Big Picture / Mental Model
Render Loop (Pixels)
Client renders -> wl_buffer attach -> wl_surface commit -> compositor composites -> DRM/KMS scanout
Input Loop (Events)
Input HW -> kernel evdev -> libinput -> compositor seat -> Wayland events -> client callbacks
System Map (Layered View)
+--------------------------------------------------------------+
| Applications (Wayland clients) |
+--------------------------+-----------------------------------+
|
v
+--------------------------------------------------------------+
| libwayland-client (marshals requests, dispatches events) |
+--------------------------+-----------------------------------+
|
v
+--------------------------------------------------------------+
| Wayland protocol over UNIX domain socket |
+--------------------------+-----------------------------------+
|
v
+--------------------------------------------------------------+
| Compositor (libwayland-server + policy + renderer) |
| - scene graph + damage tracking |
| - input focus + window rules |
+--------------------------+-----------------------------------+
|
v
+--------------------------------------------------------------+
| DRM/KMS + GPU (scanout, planes, atomic commit) |
+--------------------------------------------------------------+
Theory Primer (Mini-Book)
This primer is structured as four deep chapters. Each chapter includes fundamentals, a deep dive, and concrete exercises. You should re-read the relevant chapter before each project.
Chapter 1: Protocol Model, Objects, Registry & Versioning
Fundamentals (Read this slowly – 500+ words)
Wayland is a protocol-first system. Every interaction between a client and a compositor is a message on a UNIX domain socket. Those messages are typed and organized around objects, and each object belongs to a specific interface (for example, wl_surface, wl_buffer, or wl_seat). The object model is the backbone of the entire system: if you understand how objects are created, how their IDs are assigned, and how messages map to interfaces, you can reason about any Wayland program.
When a client connects, it does not immediately know what the compositor supports. Instead, it asks for a registry object and waits for the compositor to announce globals. Each global is an interface instance (e.g., “I provide wl_compositor version 6” or “I provide xdg_wm_base version 5”). The client then binds to the globals it understands. This explicit binding is critical: it means the protocol is forward-compatible, and a client can choose the newest version it supports without breaking on older compositors. It also means you must write robust clients that can handle missing globals or lower versions.
Wayland is asynchronous. Unlike traditional, synchronous APIs where a function call immediately gives you an answer, Wayland requires you to send a request and then later receive an event. You are responsible for maintaining a local state machine that mirrors what the compositor expects. A key consequence is that the compositor can disconnect you immediately if you violate invariants (for example, if you send a request in the wrong state or with the wrong type). This harsh correctness model is deliberate: it keeps the system fast and avoids ambiguous behavior.
Under the hood, messages are encoded in a compact binary format. Every message has a header containing an object ID and an opcode, and message bodies contain typed arguments. All arguments are 32-bit aligned, and complex objects (like wl_array or file descriptors) are handled through specialized argument types. Understanding the wire format is not required to write most clients, but it is essential for debugging with tools like WAYLAND_DEBUG=1 and for designing protocol extensions. The “shape” of each interface is described in an XML file, and tools like wayland-scanner generate C code that encodes and decodes messages consistently with the spec.
One subtle but important concept is object lifetime. Clients often create objects (like wl_surface or wl_buffer) and then destroy them explicitly when done. The compositor tracks each object per-client; once destroyed, the ID is invalid and cannot be reused until the protocol permits. This makes resource management deterministic, but it also means you must pay attention to cleanup order, especially when handling errors or disconnects. The same principle applies to the compositor: it must destroy server-side resources when the client disconnects or violates the protocol.
Finally, versioning is not just a “nice to have.” The Wayland ecosystem depends on incremental protocol evolution. Interfaces are versioned, and new requests/events are added without breaking old clients. A careful client binds to the highest version it supports, not necessarily the highest version the compositor advertises. This principle keeps the ecosystem stable while allowing innovation.
Deep Dive (1000+ words)
Let’s zoom into the wire protocol and the object lifecycle because these details explain many of the “mysterious” bugs you will see in real code. A Wayland message begins with two 32-bit words: the object ID and a combined field containing the opcode (lower 16 bits) and message size (upper 16 bits). This means the protocol is self-delimiting: the receiver knows exactly how many bytes to read before the next message. The protocol requires 32-bit alignment for every argument, so strings are length-prefixed and padded, and arrays are sized with explicit byte lengths. This is why message logs look so regular: everything is a multiple of 4 bytes. The compactness is intentional; Wayland assumes local IPC, so there is no need for the heavy marshalling overhead of network-oriented protocols like X11.
Object IDs are assigned by the client for client-created objects, and by the compositor for server-created objects. The distinction matters. For example, when a client binds to a global (say wl_compositor), it allocates a new object ID and sends a bind request. The compositor then uses that ID to refer to the bound object in future events. Conversely, when the compositor emits a new object (like a new output object), it tells the client which ID to use. This bilateral object creation is why protocol logs show alternating “new id” semantics. If you mix these up, you will get protocol errors and immediate disconnects.
The registry mechanism is one of the most elegant parts of Wayland. Instead of a fixed set of globals like X11, the compositor declares exactly what it supports at runtime. The client listens to wl_registry.global events, each of which includes a name (an integer), an interface string, and a version. The “name” is not stable across runs; it is simply a handle for binding. A correct client therefore treats it as opaque and only uses it to bind. After binding, the registry name is no longer relevant; you hold a proxy object. That proxy carries the version you negotiated, and the libwayland marshaller ensures you cannot send requests that exceed that version. This is a key safety mechanism: it makes protocol evolution feasible without breaking older software.
Another crucial concept is event queues. Libwayland allows you to assign objects to custom event queues so that certain events are dispatched on different threads or at different times. Most simple programs use the default queue, but compositors and toolkits use multiple queues to avoid deadlocks and to integrate Wayland events into more complex main loops. The presence of event queues is why you sometimes see code that explicitly calls wl_display_dispatch_queue or creates a dedicated queue for registry events. If you later build multithreaded clients, this distinction will matter.
Error handling in Wayland is intentionally strict. The compositor sends a wl_display.error event on protocol violations, and then disconnects the client. This may feel harsh at first, but it provides a strong incentive to maintain correct client state. It also means you should design your client as a state machine with explicit transitions. For example, you cannot attach a buffer to a surface before receiving the initial configure event on an xdg_surface, and you cannot reuse a buffer before receiving wl_buffer.release. These are not “best practices”; they are mandatory protocol invariants. A good debugging habit is to log every request you send and compare it to the expected state transitions in the protocol spec.
The XML protocol files are a human-readable schema for the wire format. Each interface defines requests and events, with typed arguments (int, uint, fixed, object, new_id, fd, array, string). The new_id type is especially important because it clarifies who assigns the object ID. When you run wayland-scanner, it generates client headers (with proxy structs and request signatures) and server headers (with resource structs and event emitters). Understanding this code generation helps you design custom protocols. It also explains why the protocol is “object-oriented”: the generated code is essentially a thin vtable binding with methods, and the wire format is just the serialized method calls.
Versioning rules are strict. You can never remove a request or change the signature of an existing request. The only safe evolution path is to add new requests and events in a higher version. The compositor advertises the highest version it supports, and the client chooses the highest it understands. This is why you often see defensive code that clamps versions (e.g., min(server_version, client_max)). When you design your own protocol, you must think about how it will evolve. A good practice is to keep version 1 minimal and add new behavior in version 2+. You also need to decide whether your protocol is stable or unstable. Unstable protocols (typically in the zwp_ or zwlr_ namespaces) can change without warning, so you should avoid committing to them in long-lived product code unless you control both sides.
There is also an important security implication here: because the protocol is explicit and strict, the compositor can reason about what a client is allowed to do. For example, you cannot just “query” global input state; you only receive events when the compositor grants focus. You cannot fake window activation unless the compositor allows it. These constraints are enforced at the protocol level, not in the toolkit. That is why Wayland can provide strong security guarantees compared to X11.
Finally, remember that Wayland is local-first. It does not attempt to provide network transparency; the assumption is that applications run on the same machine. This decision enables a simpler protocol, lower latency, and stronger security. Remote desktop and screen sharing are handled through separate protocols and portals (e.g., PipeWire and xdg-desktop-portal), which live outside the core Wayland protocol. Understanding this design boundary helps you reason about what Wayland does and does not try to be.
How This Fits in Projects
This chapter underpins Projects 1, 2, 3, and 4. You will use registry discovery, object lifetimes, and version negotiation when binding globals, creating resources, and designing your custom protocol.
Definitions & Key Terms
- Object: A protocol instance identified by a numeric ID (e.g.,
wl_surface@12). - Proxy: Client-side handle that marshals requests.
- Resource: Server-side handle for a client-owned object.
- Global: A compositor-advertised interface (via registry).
- Request / Event: Client->server and server->client messages.
- Opcode: Numeric index for a request/event in an interface.
- Version: Interface version negotiated at bind time.
Mental Model Diagram (ASCII)
Client process Compositor process
----------------- -----------------
wl_surface proxy (id=12) <---- socket ----> wl_surface resource (id=12)
wl_buffer proxy (id=24) <---- socket ----> wl_buffer resource (id=24)
Message:
[object_id=12][opcode=attach|size][args...]
How It Works (Step-by-Step)
- Client connects with
wl_display_connect. - Client gets the registry and listens for globals.
- Client binds to globals with a chosen version.
- Client sends requests on bound objects and receives events.
- Compositor enforces state rules and disconnects on violations.
Minimal Concrete Example
struct wl_display *display = wl_display_connect(NULL);
struct wl_registry *registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, ®istry_listener, &state);
wl_display_roundtrip(display); // wait for globals
// Bind a global (clamp version)
if (strcmp(interface, "wl_compositor") == 0) {
uint32_t v = version < 6 ? version : 6;
state.compositor = wl_registry_bind(registry, name,
&wl_compositor_interface, v);
}
Common Misconceptions
- “Wayland draws my UI.” -> No, it only transports buffers and input events.
- “Requests are synchronous.” -> No, everything is asynchronous.
- “Registry names are stable.” -> No, they are ephemeral handles.
Check-Your-Understanding Questions
- Why does Wayland require binding instead of fixed global objects?
- What happens if a client calls a request from a higher version?
- How does
new_idaffect object lifetime rules?
Check-Your-Understanding Answers
- Binding allows capability discovery and forward compatibility.
- The client will violate protocol rules and be disconnected.
- It clarifies who allocates the object ID and when it is valid.
Real-World Applications
- Toolkits (GTK, Qt, SDL) use this model for all Wayland communication.
- Compositors enforce security policy through protocol validation.
- Custom protocols (like screen sharing) rely on the same object model.
Where You’ll Apply It
- Project 1 (client): registry binding and message flow
- Project 2 (compositor): server-side object handlers
- Project 3 (custom protocol): XML design + versioning
- Project 4 (panel): binding extra globals
References
- Wayland Core Protocol Spec: https://wayland.freedesktop.org/docs/html/
- Wayland Wire Protocol (ch04): https://wayland.freedesktop.org/docs/html/ch04.html
- The Wayland Book (object model, registry): https://wayland-book.com
Key Insight
A Wayland client is a state machine over a typed message stream; correctness is about obeying protocol invariants, not just calling functions.
Summary
The Wayland protocol is compact, object-oriented, and strictly versioned. Clients bind to globals at runtime, send asynchronous requests, and must obey state rules. The compositor is both policy engine and gatekeeper, enforcing correctness and security through protocol validation.
Homework / Exercises
- Trace a
WAYLAND_DEBUG=1log and identify each object ID and interface. - Write a tiny client that binds only to
wl_shmand exits cleanly. - Modify the client to intentionally send an invalid request and observe the disconnect.
Solutions
- Look for
new idand@markers; map them to registry globals and bind calls. - The client should connect, bind, then destroy and disconnect without creating a surface.
- For example, call
wl_surface_attachon a null buffer before configure; the compositor disconnects.
Chapter 2: Surfaces, Buffers, Frame Scheduling & Presentation
Fundamentals (500+ words)
A Wayland client does not “open a window” in the traditional sense. Instead, it creates a wl_surface, assigns a role, attaches a buffer, and commits the state. The surface is a generic container for visual content. It becomes a window only after you attach a role object, such as xdg_toplevel for normal windows or zwlr_layer_surface_v1 for panels and overlays. This role system is crucial: it allows the compositor to apply policy based on the role. A surface without a role is never shown. This is one of the most common sources of confusion for new Wayland developers.
Once a surface has a role, it participates in the configure/ack/commit cycle. The compositor sends a configure event that proposes size and state. The client must acknowledge this configure (with a serial), then attach a buffer of the appropriate size, then commit. This ensures the compositor always knows what size a client intends to render, and it can enforce constraints like minimum sizes or fullscreen. The surface state is double-buffered: changes are accumulated until you call wl_surface_commit, at which point the new state becomes visible to the compositor.
A buffer is a block of pixels shared between client and compositor. Wayland supports two major paths: shared memory (wl_shm) and DMA-BUF (linux-dmabuf). Shared memory buffers are simple: you allocate a file (often with memfd_create), map it into memory, draw pixels into it, and hand a file descriptor to the compositor. DMA-BUF buffers are GPU-friendly: the client renders with the GPU into a buffer that can be imported by the compositor without copying. DMA-BUF is how modern compositors achieve low-latency, zero-copy rendering. Both paths share the same conceptual lifecycle: create buffer -> attach -> commit -> wait for release.
Presentation is governed by frame callbacks and damage tracking. Instead of blindly rendering at 60 FPS, a Wayland client requests a frame callback (wl_surface.frame). The compositor sends a done event when it is ready for the next frame, typically synchronized with display refresh. This prevents runaway rendering and reduces power usage. Damage tracking allows a client to tell the compositor which parts of a surface changed, enabling partial redraws and reducing GPU work. Combined with frame callbacks, this creates a tight, efficient render loop where the compositor decides the cadence and the client submits updates only when needed.
Because all of this is asynchronous, the client must carefully manage buffers and state. You cannot reuse a buffer until the compositor signals wl_buffer.release. You cannot commit a buffer before you have acknowledged the latest configure. You cannot ignore frame callbacks without causing unnecessary load. These rules are not just “best practices” – they are part of the protocol contract. When you violate them, the compositor can disconnect you. This is why correct Wayland clients are disciplined about state transitions and buffer lifetimes.
Deep Dive (1000+ words)
The surface lifecycle begins with a wl_surface, but the key moment is role assignment. In Wayland, a surface can have exactly one role at a time. This prevents ambiguous window semantics. When you call xdg_wm_base_get_xdg_surface, you are assigning the xdg-shell role, and when you further call xdg_surface_get_toplevel, you refine that role into a normal top-level window. The compositor expects a very specific sequence: create surface -> create role -> commit once to trigger initial configure -> wait for configure -> ack -> attach buffer -> commit again. If you attach a buffer before the initial configure, or you fail to ack the serial, you are violating the role protocol. Many “my window doesn’t appear” bugs are simply a missing ack or a missing initial commit.
The xdg-shell specification makes this handshake explicit: the first commit exists to trigger the configure event, and the configure serial becomes a strict state token that must be acknowledged before the compositor will accept buffers for that surface. Treating this as a hard state machine (not a suggestion) is the difference between a stable client and one that randomly disconnects under strict compositors.
Buffers are the next major concept. In the wl_shm path, the client must create a shared memory file descriptor. Many developers use memfd_create, but shm_open or tmpfile can also work. The file must be large enough for the entire pixel buffer (height * stride), and it must be mapped with mmap. The stride is not always width*4; it must satisfy alignment requirements and might include padding. The client writes pixels into the mapped memory (often in XRGB8888 format). The buffer is then created with wl_shm_pool_create_buffer. At this point, the compositor can read the pixels directly. This simplicity is why wl_shm is perfect for learning, but it is also why it is slower: the compositor often needs to upload the CPU memory into the GPU for composition.
DMA-BUF is more complex but far more efficient. Instead of shared CPU memory, the buffer is allocated in GPU memory (typically via EGL/GBM). The buffer is exported as a DMA-BUF file descriptor and described by a set of planes, modifiers, and formats. The linux-dmabuf protocol lets the client tell the compositor exactly how to interpret the buffer. When the compositor supports the same format and modifier, it can import the DMA-BUF directly and composite without copying. This is the zero-copy path that makes Wayland so efficient on modern systems. But it introduces stricter constraints: not all formats are supported, modifiers can be vendor-specific, and synchronization becomes more subtle. You must also ensure that the GPU has finished rendering before the compositor uses the buffer, typically via implicit or explicit synchronization mechanisms.
Frame scheduling is another area where subtle bugs appear. The wl_surface.frame request does not guarantee a fixed FPS. It is a hint that the compositor is ready for the next frame. If your client ignores it and renders in a tight loop, you will waste CPU/GPU cycles and may cause stutter. If your client waits too long, you may miss vblank deadlines. The correct pattern is: request frame callback, render a frame, commit, then wait for the callback to schedule the next render. This loop naturally throttles to the output refresh rate (often 60Hz). In compositors with variable refresh rate or power-saving modes, the callback rate can change dynamically, which is why clients should treat frame callbacks as advisory.
Damage tracking is often misunderstood. When you call wl_surface_damage (or wl_surface_damage_buffer), you are not “drawing” – you are simply informing the compositor which regions of the surface changed since the last commit. The compositor can then decide whether to re-composite only those regions or to re-render the entire scene. In practice, many compositors will still repaint the whole scene, but damage is still valuable because it allows the compositor to optimize internal pipelines and avoid unnecessary work. If you provide incorrect damage (for example, marking nothing when you actually changed pixels), the compositor may skip updates, and your window will appear frozen. If you mark too much, you lose performance. Learning to calculate accurate damage is a key skill for smooth rendering.
Another deep concept is presentation time. The presentation-time protocol allows the compositor to report when a frame actually appeared on the screen. This is critical for animations, video playback, and latency-sensitive apps. For example, a video player can compare the presentation timestamp with the media timeline to adjust playback. Without this, you would only know when the compositor accepted your buffer, not when it was displayed. The difference matters because compositors often queue frames and present them at vsync boundaries.
Buffer lifecycle and synchronization are where bugs become particularly painful. Consider a double-buffered client: it allocates two buffers and alternates between them. The client must never write into a buffer that the compositor is still using. The only safe signal is wl_buffer.release. Many new clients forget this and end up writing into a buffer while it is being scanned out, causing flicker or corruption. Similarly, if the compositor never releases a buffer (due to an error or a stalled output), the client must handle that gracefully. A robust client uses a buffer queue and only reuses buffers after release, or it allocates additional buffers to avoid stalling.
The xdg-shell configure/ack/commit cycle also interacts with buffer lifetimes. When a compositor requests a resize, it expects the next committed buffer to match the new size. The client must allocate a new buffer at the new size, attach it, and commit. If the client continues to use the old size, the compositor can mark it as a protocol error. This strictness is one reason Wayland is more predictable than X11: the compositor always has authoritative knowledge of window geometry.
In terms of pixel formats, the most common is XRGB8888, but compositors may support additional formats. The wl_shm interface advertises supported formats, and the linux-dmabuf protocol advertises supported DMA-BUF formats and modifiers. This is a negotiation process: the client should pick a format the compositor supports. If you hard-code a format and the compositor doesn’t support it, your client will fail. This is especially relevant for embedded systems or unusual hardware.
Finally, the buffer path is where you start to see the importance of file descriptor passing. When you create a wl_shm buffer or a DMA-BUF, you are effectively sharing a file descriptor with the compositor. Wayland’s protocol supports file descriptor arguments, which are passed out-of-band over the UNIX socket using SCM_RIGHTS. That means the buffer data never travels through the protocol itself; only the descriptor does. This is a critical performance optimization and also a key reason why Wayland is local-first. Remote protocols would need an additional transport layer for these descriptors.
How This Fits in Projects
This chapter is the heart of Projects 1 and 4 and informs Project 2’s rendering path. You’ll apply the surface role model, buffer lifecycles, and frame scheduling in every render loop.
Definitions & Key Terms
- Surface: Client-owned drawable object.
- Role: Semantic meaning for a surface (window, popup, panel).
- Configure/Ack/Commit: Handshake that negotiates size/state.
- Buffer: Pixel data shared with compositor (
wl_shmor DMA-BUF). - Frame callback: Signal that compositor is ready for next frame.
- Damage: Region that changed between frames.
- Presentation time: Timestamp when frame hit the screen.
Mental Model Diagram (ASCII)
Render -> attach buffer -> commit -> compositor composites -> frame done -> render next
^ |
|------------------ wl_buffer.release ------------------------|
How It Works (Step-by-Step)
- Create
wl_surfaceand assign a role (xdg_toplevel). - Commit once to trigger initial configure.
- Receive configure, ack serial.
- Create buffer (shm or DMA-BUF), attach, and commit.
- Request frame callback and repeat.
Minimal Concrete Example
struct wl_callback *cb = wl_surface_frame(surface);
wl_callback_add_listener(cb, &frame_listener, state);
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_damage(surface, 0, 0, width, height);
wl_surface_commit(surface);
Common Misconceptions
- “I can draw before configure.” -> No; initial configure is required.
- “Stride is always width*4.” -> Alignment may add padding.
- “Frame callback equals fixed 60 FPS.” -> It is a pacing hint, not a timer.
Check-Your-Understanding Questions
- Why must you wait for
wl_buffer.releasebefore reusing a buffer? - What happens if you commit a buffer with the wrong size?
- Why is damage tracking important even if the compositor repaints everything?
Check-Your-Understanding Answers
- The compositor may still be scanning out the buffer; reusing it risks corruption.
- The compositor may treat it as a protocol error and disconnect you.
- Correct damage allows internal optimizations and avoids wasted work.
Real-World Applications
- Video players rely on presentation timing to sync playback.
- Toolkits use damage to optimize redraws.
- Games depend on frame callbacks for smooth animation pacing.
Where You’ll Apply It
- Project 1: shm buffers and frame loop
- Project 2: buffer import + damage tracking
- Project 4: panel redraws and resize handling
References
- Wayland Protocols Index: https://wayland.app/protocols/
- xdg-shell Protocol: https://wayland.app/protocols/xdg-shell
- Linux DMA-BUF Protocol: https://wayland.app/protocols/linux-dmabuf-v1
- Presentation Time Protocol: https://wayland.app/protocols/presentation-time
Key Insight
Wayland’s rendering model is a buffer lifecycle contract: you control pixels, the compositor controls when and how they appear.
Summary
Surfaces become windows through roles, buffers carry pixels, and the compositor controls when frames are presented. The client is responsible for correct buffer lifetimes, configure/ack ordering, and damage reporting. This disciplined lifecycle is the foundation of Wayland’s performance and predictability.
Homework / Exercises
- Implement a double-buffered shm client and log each buffer’s lifecycle.
- Add damage tracking so only a moving rectangle is repainted.
- Simulate a resize and verify correct configure/ack/commit order.
Solutions
- Use two buffers and a queue; reuse only after
wl_buffer.release. - Track the rectangle’s old and new position and mark both regions as damaged.
- Log configure serials and assert you never commit without acking the latest serial.
Chapter 3: Input, Security, and Shell Protocols
Fundamentals (500+ words)
Wayland’s input model is designed around security and clarity. In X11, any client can observe global input events, which enables keyloggers and screen scrapers. Wayland replaces that with a strict focus-based model: only the focused surface receives keyboard events, and pointer events are delivered only when the pointer is over a surface. This model is enforced by the compositor, not by the client. The compositor is therefore a security boundary: it decides who gets input and when.
The input pipeline starts in the kernel with evdev events. Libraries like libinput normalize those raw events into a consistent stream of motion, button, and touch events. The compositor consumes libinput events and translates them into Wayland protocol events on a seat, which represents a logical group of input devices. A seat can contain keyboard, pointer, and touch capabilities. Clients bind to wl_seat and listen for capability changes, then create the appropriate input objects (wl_pointer, wl_keyboard, wl_touch). This dynamic capability model is crucial for hotplugging devices and supporting different hardware profiles.
Shell protocols define window semantics. The xdg-shell protocol provides roles for normal windows (xdg_toplevel) and popups (xdg_popup). It handles window state transitions like maximize, minimize, fullscreen, and resize. The compositor controls these transitions through configure events, and the client must respond by adjusting its buffers. For panels and overlays, many compositors use the layer-shell protocol (zwlr_layer_shell_v1). This allows clients to declare that they should be anchored to the top/bottom/left/right of the screen and optionally reserve an exclusive zone so that other windows avoid overlapping them.
Scaling is another crucial aspect of modern desktops. Wayland separates logical coordinates from buffer coordinates. An output can be scaled (e.g., 2x for HiDPI), and clients must render at an appropriate buffer scale. Protocols like xdg-output, viewporter, and fractional-scale allow compositors and clients to agree on logical sizes and mapping. This separation avoids the blurry scaling often seen in X11, but it requires clients to be careful about size calculations and pixel density.
Finally, input methods and clipboard operations are mediated through separate protocols. This is part of Wayland’s security philosophy: potentially sensitive operations (global shortcuts, screen capture, clipboard access) are explicitly mediated by the compositor and often routed through portals. This can be frustrating for developers who are used to global access, but it is a deliberate trade-off that improves user security and privacy.
Deep Dive (1000+ words)
Let’s examine the input pipeline in detail. When a physical device (mouse, keyboard, touchpad) generates an event, the Linux kernel exposes it through the evdev subsystem, typically as a file under /dev/input/event*. The compositor opens these devices (often with libinput) and receives events such as absolute motion, relative motion, key presses, and touch contacts. Libinput performs device-specific normalization: it applies acceleration profiles, palm detection, tap-to-click logic, and device quirks. This is why compositors usually do not implement device-specific logic themselves; they rely on libinput to produce a clean stream of events.
Libinput is deliberately a compositor-facing library, not an application API. Applications are expected to consume input through the Wayland protocol, while the compositor owns device configuration and policy. Keeping this boundary clean is a key part of Wayland’s security model.
The compositor then maps these events into seat-specific state. A seat may represent a physical seat (keyboard+mouse) or a logical grouping (e.g., multiple seats for multi-user setups). Each seat has an associated pointer focus surface and keyboard focus surface. The compositor updates these focuses based on input events and policy (for example, click-to-focus vs focus-follows-mouse). When focus changes, the compositor sends enter and leave events to the relevant client objects. Clients must handle these transitions gracefully; a keyboard-focused surface should update its visual state, and a pointer-focused surface should respond to hover events.
Wayland’s security benefits stem from this model. Because only the focused client receives input events, there is no global keylogging. Clipboard access is also mediated; data transfer occurs through explicit data offers and acceptances, and many compositors gate clipboard and drag-and-drop operations with user interaction. Similarly, global shortcuts and screen capture are not part of the core protocol; compositors typically implement them through privileged protocols or portals. This reduces the attack surface and aligns with modern desktop security expectations.
Shell protocols build on top of this input foundation. The xdg-shell protocol defines how a normal window behaves: it receives configure events with size and state, and the client must respond with buffers of the correct size. A critical detail is the configure serial, which is used for synchronization. Clients must acknowledge the serial before committing a buffer, ensuring that the compositor can correlate the buffer with the configuration it requested. This serial-based handshake prevents race conditions where the client and compositor disagree about size.
Popups are a special case. xdg_popup surfaces are transient, typically tied to a parent surface, and their positioning is dictated by positioner objects. The positioner tells the compositor how to place the popup relative to the parent (e.g., drop-down menus or tooltips). The compositor may adjust the placement to keep the popup on-screen, which is another example of compositor policy. This is one reason why toolkit authors must handle configure events even for popups: the compositor might reposition them.
Layer-shell introduces a different set of semantics. A panel or overlay declares its layer (background, bottom, top, overlay), its anchors (top/bottom/left/right), and an optional exclusive zone. The exclusive zone tells the compositor how much space to reserve so that normal windows are not placed underneath. For example, a top panel with an exclusive zone of 30 pixels will force tiled windows to start below it. This model allows compositors to support bars, docks, and notifications without hardcoding behavior for each app.
Scaling and coordinate systems are another common source of bugs. Wayland uses logical coordinates for window management, but buffers are pixel-based. If an output has scale factor 2, then a logical size of 800x600 corresponds to a buffer size of 1600x1200. Clients must handle this mapping correctly. The wl_surface.set_buffer_scale request tells the compositor the scale factor of the buffer, and protocols like fractional-scale allow non-integer scales. viewporter allows a client to crop or scale a buffer to a logical size. If you ignore these protocols, your UI will appear blurry or incorrectly sized on HiDPI displays.
Input and shell protocols intersect with security in subtle ways. For example, pointer constraints (locking or confining the pointer) require explicit protocols (pointer-constraints and relative-pointer). These protocols exist because the compositor must decide when it is safe to grant such privileges; a malicious client should not be able to lock the pointer without user consent. Similarly, focus and activation are mediated through xdg-activation, which allows apps to request focus in response to user actions but prevents focus stealing. This is a deliberate design choice to improve usability and security.
Another often-overlooked area is text input. Input methods are not part of the core protocol; they live in separate text-input protocols. This means complex input (IME, emoji pickers, virtual keyboards) is mediated by the compositor or input method framework. If you build a custom client (like in Project 1), you will not handle text input directly unless you implement the text-input protocol. This is why many bare-metal clients accept only raw keycodes. Understanding this limitation is important when you expand your client into a real application.
Finally, it is worth noting that input policies vary between compositors. Some compositors implement focus-follows-mouse, others use click-to-focus. Some allow global shortcuts for screen capture, others rely on portals. This is not a weakness; it is a sign that the compositor is the policy layer. Wayland’s goal is to keep the protocol minimal and allow compositors to implement policy differently. As a client developer, you must therefore be tolerant of these differences and rely only on standardized protocols where possible.
How This Fits in Projects
This chapter drives input handling and shell behavior in Projects 1, 2, and 4. You’ll wire seats, focus, and shell protocols into both clients and your compositor.
Definitions & Key Terms
- Seat: Logical group of input devices.
- Focus: The surface that receives keyboard events.
- Pointer constraints: Protocol for locking/confined cursors.
- Relative pointer: Raw motion deltas for games.
- xdg-shell: Standard window protocol.
- layer-shell: Panel/overlay protocol used by wlroots compositors.
Mental Model Diagram (ASCII)
Input HW -> evdev -> libinput -> compositor seat -> wl_pointer/wl_keyboard events -> client
How It Works (Step-by-Step)
- Compositor reads input events via libinput.
- Compositor updates focus state based on policy.
- Compositor emits Wayland input events to focused surface.
- Client responds to keyboard/pointer/touch events.
Minimal Concrete Example
// Client side: listen for seat capabilities
static void seat_handle_capabilities(void *data, struct wl_seat *seat, uint32_t caps) {
if (caps & WL_SEAT_CAPABILITY_POINTER)
state->pointer = wl_seat_get_pointer(seat);
if (caps & WL_SEAT_CAPABILITY_KEYBOARD)
state->keyboard = wl_seat_get_keyboard(seat);
}
Common Misconceptions
- “Any client can read all input.” -> Wayland only delivers input to focused surfaces.
- “Layer-shell is universal.” -> It is common on wlroots compositors but not guaranteed.
- “Scaling is a toolkit issue only.” -> Protocols define how buffers map to logical space.
Check-Your-Understanding Questions
- Why does Wayland require compositor mediation for global shortcuts?
- How does a client learn whether a seat has a keyboard or pointer?
- Why do panels need an exclusive zone?
Check-Your-Understanding Answers
- To prevent untrusted apps from intercepting input or stealing focus.
- The compositor emits seat capability events; the client creates input objects accordingly.
- To reserve space so normal windows are not placed underneath the panel.
Real-World Applications
- Desktop shells (GNOME, KDE, Sway) rely on layer-shell or custom protocols.
- Games use relative pointer + pointer constraints for raw mouse input.
- HiDPI-aware apps use fractional scaling and viewporter.
Where You’ll Apply It
- Project 2: input routing + focus policy
- Project 4: layer-shell panel interactions
- Project 1: xdg-shell window lifecycle
References
- libinput Documentation: https://wayland.freedesktop.org/libinput/doc/latest/
- xdg-shell Protocol: https://wayland.app/protocols/xdg-shell
- wlr-layer-shell Protocol: https://wayland.app/protocols/wlr-layer-shell-unstable-v1
Key Insight
Wayland’s input model makes the compositor the security boundary, and shell protocols turn that boundary into concrete window semantics.
Summary
Input is routed by the compositor through seats and focus rules, not by clients. Shell protocols define how windows behave, and scaling protocols define how buffers map to logical space. These rules produce a secure, consistent desktop model at the cost of stricter client responsibilities.
Homework / Exercises
- Write a client that logs keyboard focus changes and pointer enter/leave events.
- Create a dummy layer-shell surface and observe how it affects window placement.
- Experiment with output scale changes and verify buffer sizing.
Solutions
- Use
wl_pointerandwl_keyboardlisteners to print focus changes. - Create a top-anchored layer surface with an exclusive zone and watch other windows move.
- Multiply logical size by output scale and confirm the compositor accepts the buffers.
Chapter 4: Compositor Architecture, DRM/KMS, Extensibility & X11 Interop
Fundamentals (500+ words)
A Wayland compositor is the center of the system. It is simultaneously a display server, a window manager, and a policy engine. Clients connect to it, submit buffers, and receive input events. The compositor collects all visible surfaces into a scene graph, applies transformations and stacking rules, and then renders the final frame. Because the compositor owns the display pipeline, it controls when frames are presented, which surfaces are visible, and how input is routed. This unification is one of Wayland’s biggest architectural differences from X11.
On the output side, compositors interact with the kernel through DRM/KMS (Direct Rendering Manager / Kernel Mode Setting). DRM/KMS exposes physical outputs (connectors), scanout engines (CRTCs), and hardware planes. The compositor selects a mode for each output, renders into a framebuffer, and performs an atomic commit to update the display. This is how modern compositors achieve tear-free presentation: they schedule updates at vblank boundaries using atomic commits.
Compositors rarely implement all this from scratch. Libraries like wlroots provide reusable building blocks: backend abstractions, output handling, input processing, and a scene graph. Weston, the reference compositor, provides a more monolithic but instructive codebase. Using these libraries lets you focus on policy (tiling vs floating, focus rules, animations) rather than raw kernel interactions. In Project 2, you will use wlroots, which is also the foundation of Sway, Hyprland, and other compositors.
Extensibility is another key part of compositor architecture. Wayland core remains small by design; most modern desktop features live in protocol extensions. Compositors can implement these protocols to expose features like screenshots, screencasting, pointer constraints, fractional scaling, or custom shell behavior. The protocol extension model allows compositors to innovate without changing the core protocol, but it also requires careful versioning discipline.
Finally, X11 compatibility is handled through Xwayland, an X server that runs as a Wayland client. X11 applications connect to Xwayland as usual; Xwayland translates their windows into Wayland surfaces. This means Wayland users can still run legacy apps, but it introduces a translation layer and limitations. Understanding how Xwayland fits into the compositor architecture will help you debug interoperability issues and appreciate why Wayland’s design is so different.
Deep Dive (1000+ words)
Let’s break the compositor into concrete subsystems. The first is client management. The compositor creates a listening socket (wl_display) and accepts client connections. Each client is assigned a set of resources, and the compositor creates server-side objects (resources) corresponding to the client’s requests. The compositor must maintain per-client state: which surfaces exist, which roles they have, which buffers are pending, and which serials are valid. This is why a compositor is effectively a protocol interpreter plus a window manager. It must validate every request and enforce the rules defined by protocol specifications.
The second subsystem is the scene graph. A scene graph is a tree (or DAG) representing visible surfaces, their positions, transforms, and stacking order. In wlroots, for example, a wlr_scene can contain subtrees, each representing a window, a layer-shell surface, or a popup. The scene graph is also used for input hit-testing: when you move the pointer, the compositor walks the graph to find which surface is under the cursor. This same structure drives rendering and input dispatch, which ensures consistent behavior between what you see and what receives input.
Rendering is typically performed with OpenGL or Vulkan. In wlroots, the renderer abstracts this away, so you can choose a backend. The compositor may render each surface into an offscreen texture or composite directly into the output framebuffer. If a surface can be scanned out directly using hardware planes (for example, a fullscreen video surface), the compositor can skip composition and perform direct scanout. This optimization is called direct scanout and is crucial for low-latency video playback. It requires that the surface buffer format and modifiers are compatible with the output plane.
The DRM/KMS pipeline is the final stage. The compositor opens /dev/dri/card*, queries connectors, CRTCs, and planes, and selects a mode for each connector. It creates framebuffers (often using GBM to allocate buffers backed by the GPU), then submits an atomic commit that binds the framebuffer to a CRTC and connector. Atomic commits allow the compositor to update multiple properties in a single transaction, ensuring consistent, tear-free updates. If a commit fails (for example, due to unsupported mode or busy resources), the compositor must recover gracefully and fall back to a safe configuration.
The kernel documentation makes the KMS object model explicit: connectors represent physical outputs, encoders convert pixel data for connectors, CRTCs drive scanout engines, and planes are hardware layers that can be blended or scanned out directly. Thinking in these objects helps you reason about why some combinations of outputs and planes are valid and why atomic commits can fail when resources are incompatible.
Input handling is another critical subsystem. The compositor uses libinput to read device events and converts them into Wayland input events. It tracks focus and determines which surface should receive each event. It must also handle special protocols like pointer constraints, relative pointer, and touch. This is why compositors often include extensive policy code: deciding when a client can lock the pointer, when a focus change is allowed, or how to interpret modifier keys. These policies are where different compositors differentiate themselves.
Protocol extensibility deserves special attention. The core Wayland protocol is intentionally minimal; it does not include global shortcuts, screenshots, or clipboard management details. These features are implemented as protocol extensions, which live in the wayland-protocols repository and are categorized as stable, staging, or unstable. When you implement an extension, you must follow versioning rules: never change existing request signatures, only add new requests or events, and bump the version. You also need to decide whether the protocol is stable enough for production. Unstable protocols are allowed to change across versions of the compositor and client, which means you must be careful about compatibility.
Xwayland adds another layer of complexity. It is an X server that runs as a Wayland client, using the same wl_surface and buffer mechanisms as native clients. From the compositor’s perspective, Xwayland is just another client, but it internally translates X11 requests into Wayland surfaces. This translation can introduce differences: X11 expects global coordinate spaces and global input, while Wayland is per-surface and focus-based. As a result, some X11 behaviors (like global key grabs or direct framebuffer access) cannot be replicated exactly. Xwayland provides compatibility but not perfect equivalence. Understanding these limits is crucial when you debug legacy applications.
Another subtle aspect of Xwayland is rootless mode. In rootless mode, each X11 window is mapped to an individual Wayland surface, and the compositor manages them like any other window. This allows better integration with Wayland compositors. In rootful mode, Xwayland draws into a single root window, which the compositor sees as one surface. Rootless mode is preferred for modern desktops but requires more integration. Knowing the difference explains why some X11 apps behave strangely on certain compositors.
The compositor also interacts with system services and session management. For example, many compositors integrate with systemd for user sessions and with D-Bus for desktop services. While not part of the core Wayland protocol, these integrations matter in real desktops. When you build a minimal compositor for learning, you can ignore them, but when you build a production compositor, you must account for them.
Finally, note the debugging model for compositors. Because they are central to the system, a compositor crash often kills your graphical session. Developers typically run compositors inside a nested environment (e.g., weston --backend=wayland or cage -d) to avoid crashing their main desktop. You will do the same in Project 2. This is an example of how architectural responsibilities influence development workflow.
How This Fits in Projects
This chapter powers Project 2 and frames the comparison in Project 5. You’ll use the compositor architecture and DRM/KMS pipeline when building your wlroots compositor.
Definitions & Key Terms
- Compositor: Wayland server + window manager + renderer.
- Scene graph: Hierarchical structure of visible surfaces.
- DRM/KMS: Kernel API for display configuration and scanout.
- CRTC / Plane / Connector: KMS objects for scanout engines, layers, and outputs.
- Atomic commit: Transactional update of KMS properties.
- Xwayland: X server running as a Wayland client.
Mental Model Diagram (ASCII)
Clients -> scene graph -> renderer -> framebuffer -> DRM atomic commit -> display
^ |
|--- input focus ---|
How It Works (Step-by-Step)
- Compositor opens DRM device and enumerates outputs.
- Compositor starts Wayland server and accepts clients.
- Client surfaces enter the scene graph on commit.
- Renderer composites surfaces into a framebuffer.
- DRM/KMS atomic commit presents the frame at vblank.
Minimal Concrete Example
// Pseudo outline of compositor loop
while (running) {
wl_display_dispatch(display);
render_scene(scene, output);
drm_atomic_commit(output);
}
Common Misconceptions
- “Compositor just blits buffers.” -> It also enforces policy and input security.
- “DRM/KMS is only for drivers.” -> Compositors use it directly.
- “Xwayland is the compositor.” -> It is a client of the compositor.
Check-Your-Understanding Questions
- Why does Wayland require compositors to use DRM/KMS directly?
- What is the purpose of atomic commit?
- Why can’t Xwayland perfectly emulate all X11 behaviors?
Check-Your-Understanding Answers
- The compositor owns presentation timing and must control scanout directly.
- To update multiple properties consistently at vblank, avoiding tearing.
- X11 allows global access and assumptions that Wayland forbids for security.
Real-World Applications
- Sway and Hyprland use wlroots to build compositors quickly.
- GNOME’s Mutter and KDE’s KWin implement their own compositors.
- Embedded UIs use minimal compositors for kiosks and devices.
Where You’ll Apply It
- Project 2: building a wlroots compositor
- Project 3: implementing protocol extensions
- Project 5: understanding X11 compatibility layer
References
- DRM/KMS Documentation: https://docs.kernel.org/gpu/drm-kms.html
- wlroots Documentation: https://wlroots.readthedocs.io/
- Weston Reference Compositor: https://wayland.freedesktop.org/documentation.html
- Xwayland Man Page: https://manpages.debian.org/unstable/xwayland/Xwayland.1.en.html
- X11 Protocol Spec: https://www.x.org/releases/X11R7.6/doc/xproto/x11protocol.html
Key Insight
A compositor is not just a renderer – it is the policy and security kernel of the desktop.
Summary
Compositors manage clients, scenes, input, and outputs. They render frames, commit them via DRM/KMS, and enforce protocol rules. Protocol extensions and Xwayland fit into this architecture as optional layers that extend functionality without changing the core protocol.
Homework / Exercises
- Read wlroots
tinywlsource and sketch its main loop. - Use
modetestto list connectors, CRTCs, and planes on your system. - Run an X11 app under Wayland and observe its behavior differences.
Solutions
- Identify where the compositor sets up the backend, seat, and scene graph.
modetest -c -pshows connectors and planes; map them to outputs.- Compare focus behavior and clipboard access between X11 and Wayland apps.
Glossary (High-Signal)
- wl_display: Connection object for all Wayland communication.
- wl_registry: Announces globals available from the compositor.
- Global: A compositor-advertised interface instance.
- Proxy / Resource: Client/server handles for protocol objects.
- wl_surface: Drawable surface; must be given a role to become visible.
- wl_buffer: Pixel data shared with compositor.
- wl_shm: Shared-memory buffer interface.
- DMA-BUF: Zero-copy buffer sharing mechanism via file descriptors.
- xdg-shell: Standard window protocol for normal applications.
- layer-shell: Protocol for panels/overlays (common in wlroots compositors).
- Seat: Logical group of input devices.
- Scene graph: Compositor structure describing visible surfaces.
- DRM/KMS: Kernel interface for output configuration.
- Atomic commit: Transactional display update.
- Xwayland: X11 server running as Wayland client.
Why Wayland Matters (Modern Context First)
Modern desktops are local, GPU-accelerated, and security-sensitive. X11 was designed for network transparency and server-side rendering, which creates complexity, latency, and security issues. Wayland modernizes the model by letting clients render and giving the compositor control over input and presentation.
Concrete benefits you will observe in the projects:
- Lower input-to-photon latency (fewer IPC hops)
- Tear-free presentation (compositor controls vblank via atomic commits)
- Stronger input isolation (focused surface only)
- Simpler rendering pipeline (no server-side drawing API)
Real-World Adoption (Examples with Sources)
- Fedora 25 (2016) switched GNOME Workstation to Wayland by default. Source: https://fedoraproject.org/wiki/Changes/WaylandByDefault
- Fedora 34 (2021) switched KDE Plasma to Wayland by default. Source: https://fedoraproject.org/wiki/Changes/WaylandByDefaultForPlasma
- Fedora 36 (2022) made Wayland the default for NVIDIA (proprietary driver) GNOME sessions. Source: https://fedoraproject.org/wiki/Changes/WaylandByDefaultOnNVIDIA
- Ubuntu 21.04 (2021) switched back to Wayland by default. Source: https://ubuntu.com/blog/ubuntu-21-04-is-here
- Debian 10 (2019) and newer use Wayland by default for GNOME sessions. Source: https://wiki.debian.org/Wayland
- KDE stated the vast majority of Plasma users are already on Wayland and announced a Wayland-only Plasma session starting in Plasma 6.8 (2025). Source: https://blogs.kde.org/2025/11/26/going-all-in-on-a-wayland-future/
Context & Evolution (Short)
Wayland began as a minimal replacement for X11, but its main innovation was architectural: clients render, compositors composite. This shift enabled better performance and security while simplifying the protocol.
Old vs New (ASCII)
X11 (old) Wayland (new)
+------------+ +-----------+ +------------+ +-------------+
| Client |-->| X Server |--+ => | Client |-->| Compositor |-->
+------------+ +-----------+ | +------------+ +-------------+
v |
+---------+ v
| Compositor | DRM/KMS + GPU
+---------+
Concept Summary Table
| Concept Cluster | What You Must Internalize |
|---|---|
| Protocol Model & Registry | Object IDs, requests/events, version negotiation, strict invariants. |
| Surfaces, Buffers & Frame Scheduling | Roles, configure/ack/commit, buffer lifecycles, frame callbacks. |
| Input, Shells & Security | Seats, focus routing, xdg-shell, layer-shell, scaling protocols. |
| Compositor, DRM/KMS & X11 Interop | Scene graph, output pipeline, atomic commit, Xwayland compatibility. |
Project-to-Concept Map
| Project | What It Builds | Primer Concepts It Uses |
|---|---|---|
| Project 1: Bare-Metal Wayland Client | A window rendered via wl_shm |
Protocol model, surfaces/buffers, frame scheduling |
| Project 2: wlroots Compositor | A working compositor | Compositor architecture, DRM/KMS, input/security |
| Project 3: Custom Protocol Extension | A new Wayland interface | Protocol model, versioning, codegen |
| Project 4: Layer-Shell Panel | A desktop panel/overlay | Input/shell protocols, buffers, frame scheduling |
| Project 5: X11 Comparison Client | Same window in Xlib | X11 interop + architectural differences |
Deep Dive Reading by Concept
Protocol Model & Registry
- The Wayland Book – Ch. 2-5 (Protocol design, registry, objects)
- The Linux Programming Interface – Ch. 61 (FD passing), Ch. 63 (event loops)
- Wayland Core Protocol Spec (overview + wire format): https://wayland.freedesktop.org/docs/html/
- Wayland Wire Protocol (ch04): https://wayland.freedesktop.org/docs/html/ch04.html
Surfaces, Buffers & Frame Scheduling
- The Wayland Book – Ch. 6-9 (shm, buffers, xdg-shell)
- Computer Graphics from Scratch – Ch. 1-2 (pixels and buffers)
- xdg-shell Protocol: https://wayland.app/protocols/xdg-shell
- Linux DMA-BUF Protocol: https://wayland.app/protocols/linux-dmabuf-v1
- Presentation Time Protocol: https://wayland.app/protocols/presentation-time
Input, Shells & Security
- The Wayland Book – Ch. 10-11 (outputs and input)
- libinput documentation (event processing and device config)
- wlr-layer-shell Protocol: https://wayland.app/protocols/wlr-layer-shell-unstable-v1
Compositor, DRM/KMS & X11 Interop
- The Wayland Book – Ch. 7 (compositor construction)
- DRM/KMS kernel docs (outputs, planes, atomic commits)
- X11 protocol spec (overview of legacy model)
- Xwayland Man Page (rootless/rootful behavior): https://manpages.debian.org/unstable/xwayland/Xwayland.1.en.html
Quick Start (First 48 Hours)
Day 1 (3-5 hours)
- Read Chapter 1 and skim Chapter 2.
- Run
wayland-infoorweston-infoto list globals. - Build a tiny program that connects to
wl_displayand exits cleanly.
Day 2 (3-5 hours)
- Build the Project 1 skeleton (registry + surface + xdg_toplevel).
- Use
WAYLAND_DEBUG=1and annotate the message flow. - Create a
wl_shmbuffer and draw a single color.
Recommended Learning Paths
- Desktop App Developer: Project 1 -> Project 4 -> Project 5 -> Project 2 (compositor last)
- Compositor Engineer: Project 2 -> Project 1 -> Project 3 -> Project 4
- Embedded UI Developer: Project 2 -> Project 4 -> Project 1
- Protocol Designer: Chapter 1 deep dive -> Project 3 -> Project 1
Success Metrics
- You can explain the configure/ack/commit cycle without notes.
- You can draw the render pipeline from client to KMS.
- You can implement a client that resizes correctly.
- You can build a compositor that accepts input and renders windows.
- You can explain why Xwayland exists and its limitations.
Optional Appendices
Tooling & Debugging Checklist
# Verify Wayland session
$ echo $XDG_SESSION_TYPE
# Inspect globals
$ wayland-info | head -n 40
$ weston-info | head -n 40
# Inspect DRM/KMS
$ ls -l /dev/dri/card*
$ modetest -c -p
# Inspect input events
$ sudo libinput debug-events
# Protocol tracing
$ WAYLAND_DEBUG=1 ./your_client 2> protocol.log
Common Failure Signatures
- Window never appears -> Missing initial commit or configure/ack ordering.
- Immediate disconnect -> Protocol error (check
WAYLAND_DEBUG=1). - No input events -> Seat not bound or focus not set.
- Compositor crash -> DRM/KMS permissions or backend failure.
Project Overview Table
| Project | What You’ll Build | Difficulty | Key Concepts | Time |
|---|---|---|---|---|
| Project 1: Bare-Metal Wayland Client | A raw wl_shm window with correct configure/ack/commit |
Intermediate | Registry, surfaces, buffers, frame callbacks | 1-2 weeks |
| Project 2: wlroots Compositor | A minimal compositor with outputs, input, and scene graph | Advanced | DRM/KMS, libinput, compositor policy | 3-4 weeks |
| Project 3: Custom Protocol Extension | A new XML protocol + client/server glue | Intermediate-Advanced | Protocol design, versioning, codegen | 1-2 weeks |
| Project 4: Layer-Shell Panel | A status bar/panel using layer-shell | Intermediate | Layer roles, outputs, rendering, damage | 1-2 weeks |
| Project 5: X11 Comparison Client | The same window built with Xlib | Intermediate | X11 request/reply, atoms, Expose | 1 week |
Project List
Project 1: Bare-Metal Wayland Client
- File: WAYLAND_X11_COMPOSITOR_LEARNING_PROJECTS.md
- Programming Language: C
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 3: Advanced
- Knowledge Area: Graphics / Window Systems
- Software or Tool: Wayland Protocol
- Main Book: “The Wayland Book” by Drew DeVault
What you’ll build: A Wayland client from scratch that displays a colored window using only libwayland-client and shared memory–no toolkits, no helpers.
Why it teaches Wayland: Forces you to understand the entire client lifecycle: connecting to the compositor, negotiating protocols via wl_registry, creating surfaces, attaching buffers, and handling the frame callback loop.
Core challenges you’ll face:
- Understanding the Wayland object model (proxies, listeners, callbacks)
- Negotiating global interfaces through
wl_registry - Creating and managing
wl_shmshared memory buffers - Implementing the
xdg-shellsurface lifecycle (map/unmap/configure) - Building a proper event loop with
wl_display_dispatch
Key Concepts:
- Wayland Protocol Basics: “The Wayland Book” by Drew DeVault (Chapters 1-4) - https://wayland-book.com
- Shared Memory in Unix: “Advanced Programming in the UNIX Environment” by Stevens & Rago (Chapter 15)
- C Memory Management: “Effective C, 2nd Edition” by Robert C. Seacord (Chapter 6)
- Event-Driven Programming: “The Linux Programming Interface” by Michael Kerrisk (Chapter 63)
Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: C programming, basic Linux/Unix concepts, familiarity with system calls
Real world outcome: A standalone executable that opens a window on your Wayland desktop with a solid color that you can resize and close. You’ll see your rectangle appear alongside your other applications.
Learning milestones:
- Connect to Wayland, enumerate globals -> understand the protocol negotiation model
- Create an
xdg_toplevelsurface -> understand shell protocols and window lifecycle - Attach a shared memory buffer and see pixels -> understand buffer management
- Handle resize events properly -> understand the configure/ack/commit cycle
Real World Outcome
When you complete this project, you’ll have a standalone executable that opens a resizable window on your Wayland desktop. Here’s exactly what you’ll experience:
Compilation and Execution
# Install dependencies (Debian/Ubuntu)
$ sudo apt install libwayland-dev wayland-protocols
# Compile your program
$ gcc -o wayland_client main.c \
-lwayland-client \
-I/usr/share/wayland-protocols
# Run it
$ ./wayland_client
What You’ll See
When you execute the program:
- A window appears on your desktop (if you’re running a Wayland compositor like GNOME Wayland, KDE Plasma Wayland, or Sway)
- The window has:
- A solid color background (e.g., a vibrant red, green, or blue - you choose the RGB values)
- A title bar with the window name you set (e.g., “My Wayland Client”)
- Standard window controls (minimize, maximize, close)
- Proper window decorations provided by your compositor
- Interactive behaviors you’ll observe:
- You can resize the window by dragging edges/corners - the color fills the entire new size
- You can move the window around your desktop
- You can minimize and maximize it like any other window
- You can close it using the window manager controls or Ctrl+C in the terminal
Terminal Output
Your program will print diagnostic information showing the Wayland protocol in action:
$ ./wayland_client
[Wayland Client] Connecting to Wayland display...
[Wayland Client] Connected successfully
[Registry] Global interface: wl_compositor v6
[Registry] Global interface: wl_subcompositor v1
[Registry] Global interface: wl_shm v1
[Registry] Global interface: xdg_wm_base v5
[Registry] Global interface: wl_seat v9
[Registry] Global interface: wl_output v4
[SHM] Creating shared memory pool (921,600 bytes for 800x600 @ 32bpp)
[SHM] Memory mapped at 0x7f8a4c000000
[Surface] Creating wl_surface...
[XDG] Creating xdg_surface...
[XDG] Creating xdg_toplevel with title "My Wayland Client"
[XDG] Configure event received:
- Width: 800, Height: 600
- States: activated
[XDG] Acknowledging configure (serial: 1234)
[Buffer] Attaching buffer to surface
[Buffer] Writing RGB color: #FF0000 (red)
[Surface] Committing surface...
[Display] Entering event loop...
[Frame] Frame callback received - ready for next frame
[XDG] Configure event received (resize):
- Width: 1024, Height: 768
- States: activated, resizing
[XDG] Acknowledging configure (serial: 1235)
[Buffer] Creating new buffer for size 1024x768
[Buffer] Writing RGB color: #FF0000 (red)
[Surface] Committing surface...
[Signal] Caught SIGINT - cleaning up...
[Cleanup] Destroying xdg_toplevel
[Cleanup] Destroying xdg_surface
[Cleanup] Destroying wl_surface
[Cleanup] Destroying wl_shm_pool
[Cleanup] Unmapping shared memory
[Cleanup] Disconnecting from Wayland
[Exit] Goodbye!
How It Differs from Toolkit-Based Code
Unlike using GTK or Qt where you’d write:
// With GTK (what you DON'T do in this project)
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_widget_show_all(window);
gtk_main();
You’ll instead write the raw Wayland protocol interactions:
// What you WILL write (simplified)
struct wl_display *display = wl_display_connect(NULL);
struct wl_registry *registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, ®istry_listener, &state);
wl_display_roundtrip(display);
// Create surface
struct wl_surface *surface = wl_compositor_create_surface(compositor);
// Set up xdg-shell
struct xdg_surface *xdg_surface = xdg_wm_base_get_xdg_surface(xdg_wm_base, surface);
struct xdg_toplevel *xdg_toplevel = xdg_surface_get_toplevel(xdg_surface);
// Create shared memory buffer
int fd = create_shm_file(size);
void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size);
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, WL_SHM_FORMAT_XRGB8888);
// Paint it
uint32_t *pixels = (uint32_t *)data;
for (int i = 0; i < width * height; i++) {
pixels[i] = 0xFFFF0000; // Red
}
// Show it
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_commit(surface);
// Event loop
while (wl_display_dispatch(display) != -1) {
// Handle events
}
This visceral experience of managing every protocol step yourself is what makes Wayland’s design philosophy clear: clients do their own rendering, and the compositor just composites finished buffers.
What You’ve Built
You now have a working Wayland client that:
- Speaks the Wayland protocol fluently
- Manages its own buffer lifecycle
- Responds to compositor events (resize, configure, close)
- Can be extended to do anything: draw shapes, render text, display images, create games
This is the foundation every Wayland application builds upon, even if they hide it behind Qt, GTK, or SDL.
The Core Question You’re Answering
“How does a graphical application actually display pixels on screen in a modern Linux desktop, and what is the conversation between my program and the compositor?”
Before you write any code, sit with this question. Most developers think “GTK draws my window” or “X11 shows my pixels,” but they can’t explain what that actually means at the protocol level.
The deeper question beneath this:
“Why does Wayland exist? What was so broken about X11 that the entire Linux graphics stack was redesigned from scratch?”
By building a bare-metal Wayland client, you’ll discover that:
- Wayland is asynchronous: You request things and get callbacks, not immediate responses
- Wayland is stateless on the compositor side: The compositor doesn’t store your window contents–you do
- Wayland is about buffer passing: You render pixels in shared memory; the compositor just displays them
- Wayland is object-oriented: Everything is an object (surface, buffer, seat) with interfaces and events
This project answers the fundamental question: “What is the minimal conversation needed between an application and a display server to show a window?”
The answer is surprising in its simplicity compared to X11, but also surprising in how much responsibility it places on the client.
Concepts You Must Understand First
Stop and research these before coding:
1. What is a Display Server?
- What does a “display server” actually do?
- Why do we need one? Why can’t applications draw directly to the framebuffer?
- What’s the difference between X11 (client-server model) and Wayland (compositor model)?
- What security implications come from the design differences?
- Book Reference: “How Linux Works, 3rd Edition” by Brian Ward - Chapter 14: “Introduction to Graphics”
2. The Wayland Protocol Philosophy
- How does Wayland’s protocol differ from X11’s protocol?
- What does “asynchronous” mean in the context of Wayland?
- Why does Wayland use an object-oriented design (interfaces, events, requests)?
- What is a “global” in Wayland terminology?
- Book Reference: “The Wayland Book” by Drew DeVault - Chapters 1-2 (available free at https://wayland-book.com)
3. Shared Memory in POSIX Systems
- What is
shm_open()and how does it differ frommalloc()? - What is
mmap()and why is it used for Wayland buffers? - How does shared memory allow zero-copy buffer sharing between processes?
- What happens when two processes map the same file descriptor?
- Book Reference: “The Linux Programming Interface” by Michael Kerrisk - Chapter 54: “POSIX Shared Memory”
- Alternative: “Advanced Programming in the UNIX Environment, 3rd Edition” by Stevens & Rago - Chapter 15
4. Event-Driven Programming
- What is an event loop?
- How does
poll()orepoll()work for waiting on file descriptors? - Why does Wayland use callbacks instead of blocking calls?
- How do you avoid blocking the event loop?
- Book Reference: “The Linux Programming Interface” by Michael Kerrisk - Chapter 63: “Alternative I/O Models”
5. The Registry Pattern (Wayland’s Global Discovery)
- What is the
wl_registryand why is it the first thing you interact with? - How does the registry announcement pattern work?
- What does “bind” mean in Wayland’s context?
- Why do different compositors advertise different globals?
- Book Reference: “The Wayland Book” - Chapter 4: “Registry”
6. XDG-Shell Protocol
- What is
xdg-shelland why isn’twl_shellused anymore? - What is the difference between
xdg_surfaceandxdg_toplevel? - What is the configure/ack/commit cycle?
- Why must you wait for a configure event before showing the window?
- Book Reference: “The Wayland Book” - Chapter 8: “Surfaces in Depth” & Chapter 9: “XDG-Shell”
7. Buffer Formats and Pixel Layout
- What is
WL_SHM_FORMAT_XRGB8888and how is it laid out in memory? - What is a stride and why might it differ from
width * 4? - What is the difference between ARGB, RGBA, XRGB, and ABGR?
- How do you write pixel data in the correct endianness?
- Book Reference: “Computer Systems: A Programmer’s Perspective” by Bryant & O’Hallaron - Chapter 2.1: “Information Storage”
8. File Descriptors and UNIX IPC
- What is a file descriptor beyond just “file handle”?
- How can a file descriptor represent shared memory?
- Why use anonymous files (
memfd_createorshm_open)? - How are file descriptors passed between processes in Wayland?
- Book Reference: “The Linux Programming Interface” by Michael Kerrisk - Chapter 4-5: “File I/O” basics
Questions to Guide Your Design
Before implementing, think through these:
1. Connection and Setup
- How do you connect to a Wayland compositor? (What’s the socket path?)
- What’s the first thing you must do after connecting? (Get the registry)
- How do you know which global interfaces are available?
- What happens if
xdg_wm_baseisn’t available? (Compositor doesn’t support xdg-shell)
2. Registry and Global Binding
- How do you “bind” to a global interface?
- Why do you need to specify a version when binding?
- Which globals are essential for this project? (
wl_compositor,wl_shm,xdg_wm_base) - How do you handle new globals appearing after initial enumeration?
3. Surface Creation
- What’s the order of creating objects? (compositor -> surface -> xdg_surface -> xdg_toplevel)
- Why can’t you just create a
wl_surfaceand attach a buffer? - What role does
xdg_surfaceplay versusxdg_toplevel? - When should you commit the surface for the first time?
4. Shared Memory Buffer Management
- How do you calculate the size needed? (
width * height * 4for 32-bit XRGB) - What’s the lifecycle of a buffer? (create -> attach -> commit -> frame callback -> potentially reuse or destroy)
- Can you reuse buffers, or must you create new ones on resize?
- How do you ensure the compositor is done with a buffer before rewriting it?
5. The Configure/Ack/Commit Dance
- Why does the compositor send a configure event?
- What must you do when you receive a configure? (acknowledge with the serial number)
- What happens if you don’t ack a configure?
- Why is there a serial number in the configure event?
6. Event Loop Design
- Should you use
wl_display_dispatch()orwl_display_dispatch_pending()? - How do you handle blocking versus non-blocking dispatch?
- When should you flush the display connection? (
wl_display_flush()) - How do you cleanly exit the event loop on Ctrl+C?
7. Cleanup and Resource Management
- What’s the proper order to destroy Wayland objects?
- Do you need to destroy everything, or does disconnecting clean up?
- How do you ensure shared memory is unmapped and the file descriptor closed?
- What happens if you don’t clean up properly?
Thinking Exercise
Design the Protocol Conversation on Paper
Before coding, draw out the sequence of messages between your client and the compositor:
COMPLETE WAYLAND CLIENT INITIALIZATION SEQUENCE
════════════════════════════════════════════════════════════════════════════
Client Compositor
| |
| 1. wl_display_connect(NULL) |
| (Opens Unix socket: $XDG_RUNTIME_DIR/wayland-0) |
|=================================================> |
| | Accepts connection
| | Creates wl_client
| |
| 2. wl_display_get_registry() |
| (Request: "Tell me what you support") |
|-------------------------------------------------> |
| |
| registry.global(1, "wl_compositor", 6) |
| (Announcement: interface available) |
| <-------------------------------------------------|
| registry.global(2, "wl_subcompositor", 1) |
| <-------------------------------------------------|
| registry.global(3, "wl_shm", 1) |
| <-------------------------------------------------|
| registry.global(4, "wl_output", 4) |
| <-------------------------------------------------|
| registry.global(5, "wl_seat", 9) |
| <-------------------------------------------------|
| registry.global(6, "xdg_wm_base", 5) |
| <-------------------------------------------------|
| registry.global(7, "zwp_linux_dmabuf_v1", 4) |
| <-------------------------------------------------|
| |
| 3. wl_display_roundtrip() |
| (Block until all pending events processed) |
|-------------------------------------------------> |
| |
| 4. wl_registry_bind(1, "wl_compositor", 4) |
| (Bind to compositor interface, create proxy) |
|-------------------------------------------------> |
| | Creates resource ID 100
| |
| 5. wl_registry_bind(3, "wl_shm", 1) |
|-------------------------------------------------> |
| | Creates resource ID 101
| |
| 6. wl_registry_bind(6, "xdg_wm_base", 5) |
|-------------------------------------------------> |
| | Creates resource ID 102
| |
| 7. wl_compositor.create_surface(new_id) |
| (Request: "Give me a surface object") |
|-------------------------------------------------> |
| | Allocates wl_surface
| | Assigns ID 200
| |
| 8. xdg_wm_base.get_xdg_surface(new_id, surface) |
| (Request: "Make surface ID 200 an xdg_surface") |
|-------------------------------------------------> |
| | Creates xdg_surface
| | Associates with wl_surface 200
| | Assigns ID 300
| |
| 9. xdg_surface.get_toplevel(new_id) |
| (Request: "This is a top-level window") |
|-------------------------------------------------> |
| | Creates xdg_toplevel
| | Assigns ID 400
| |
| 10. xdg_toplevel.set_title("My Window") |
|-------------------------------------------------> |
| |
| 11. wl_surface.commit() |
| (CRITICAL: First commit triggers configure) |
|-------------------------------------------------> |
| |
| xdg_toplevel.configure(800, 600) |
| (Suggestion: "Try size 800x600") |
| <-------------------------------------------------|
| xdg_surface.configure(serial=1234) |
| (State: "Surface configured") |
| <-------------------------------------------------|
| |
| 12. xdg_surface.ack_configure(serial=1234) |
| (Acknowledgment: "I received configure 1234") |
|-------------------------------------------------> |
| |
| 13. Create shared memory buffer |
| int fd = memfd_create("buffer", MFD_CLOEXEC) |
| ftruncate(fd, 800 * 600 * 4) |
| void *data = mmap(NULL, size, ..., fd, 0) |
| wl_shm.create_pool(fd, size) |
|-------------------------------------------------> |
| | Maps shared memory
| |
| 14. wl_shm_pool.create_buffer(offset, width, |
| height, stride, format) |
|-------------------------------------------------> |
| | Creates wl_buffer ID 500
| |
| 15. Paint pixels to shared memory |
| for (int i = 0; i < 800*600; i++) |
| pixels[i] = 0xFFFF0000; // Red |
| |
| 16. wl_surface.attach(buffer, x=0, y=0) |
| (Attach buffer 500 to surface 200) |
|-------------------------------------------------> |
| |
| 17. wl_surface.damage_buffer(x, y, width, height) |
| (Mark region that needs redraw) |
|-------------------------------------------------> |
| |
| 18. wl_surface.commit() |
| (CRITICAL: Atomically apply all changes) |
|=================================================> |
| |
| | ┌─────────────────┐
| | │ Compositor sees:│
| | │ - Surface role │
| | │ - Buffer attached│
| | │ - Configure ack'd│
| | │ -> Window ready! │
| | └─────────────────┘
| |
| | Composites buffer to screen
| | 🖥️ Window appears!
| |
| wl_buffer.release() |
| (Buffer no longer in use) |
| <-------------------------------------------------|
| |
| 19. wl_surface.frame(new_id) |
| (Request callback when frame is displayed) |
|-------------------------------------------------> |
| |
| wl_callback.done(time) |
| (Frame presented at timestamp) |
| <-------------------------------------------------|
| |
| Event loop continues... |
| |
KEY PROTOCOL CONCEPTS ILLUSTRATED:
═════════════════════════════════════════════════════════════════════════
1. REGISTRY PATTERN: Compositor advertises capabilities, client binds to what it needs
2. OBJECT HIERARCHY: Display -> Registry -> Compositor -> Surface -> XDG Surface -> Toplevel
3. DOUBLE-BUFFERED STATE: Changes accumulate in pending state, commit() applies atomically
4. CONFIGURE/ACK CYCLE: Compositor suggests size -> Client acknowledges -> Client commits
5. SHARED MEMORY: Zero-copy buffer passing via file descriptors
6. ASYNCHRONOUS: Requests and events flow independently, synchronize with roundtrip()
Questions to answer while drawing:
- After creating the
wl_surface, what must you create before you can show the window? - When do you create the shared memory buffer?
- When does the compositor send the first configure event?
- What triggers the compositor to actually display your pixels?
- What happens when the user resizes the window?
Trace a Resize Event
Imagine the user grabs the window corner and drags it to resize. Here’s the complete flow:
COMPLETE RESIZE EVENT FLOW
════════════════════════════════════════════════════════════════════════════
Time: T0
┌─────────────────────────────────────────────────────────────────┐
│ USER ACTION: Grabs window corner with mouse, starts dragging │
│ Current window size: 800x600 │
│ Mouse position: (795, 595) - bottom-right corner │
└─────────────────────────────────────────────────────────────────┘
│
▼
Time: T1
┌─────────────────────────────────────────────────────────────────┐
│ COMPOSITOR: Detects resize grab │
│ - Enters interactive resize mode │
│ - Calculates new size based on mouse movement │
│ - New size: 1024x768 (user dragged to expand) │
│ - Generates serial number: 5678 │
└─────────────────────────────────────────────────────────────────┘
│
▼
Time: T2 - Compositor sends configure events
═══════════════════════════════════════════════════════════════════════════
Compositor Client
| |
| xdg_toplevel.configure(width=1024, |
| height=768, |
| states=[activated, |
| resizing]) |
| (Hint: "User wants 1024x768") |
|--------------------------------------------> |
| | Stores: new_width=1024
| | new_height=768
| |
| xdg_surface.configure(serial=5678) |
| (Marks end of configure sequence) |
|--------------------------------------------> |
| |
| | ┌──────────────────────┐
| | │ Event loop receives │
| | │ configure events │
| | │ Callback invoked: │
| | │ handle_configure() │
| | └──────────────────────┘
|
▼
Time: T3 - Client acknowledges and prepares new buffer
═══════════════════════════════════════════════════════════════════════════
Client Process:
┌──────────────────────────────────────────────────────────────────┐
│ 1. ACKNOWLEDGE CONFIGURE │
│ xdg_surface.ack_configure(serial=5678) │
│ -> Tells compositor: "I understand the resize request" │
│ │
│ 2. DESTROY OLD BUFFER (optional, depends on implementation) │
│ wl_buffer.destroy(old_buffer) │
│ munmap(old_data, 800 * 600 * 4) │
│ │
│ 3. CREATE NEW SHARED MEMORY BUFFER │
│ int new_size = 1024 * 768 * 4; // 3,145,728 bytes │
│ int fd = memfd_create("resize-buffer", MFD_CLOEXEC); │
│ ftruncate(fd, new_size); │
│ void *data = mmap(NULL, new_size, PROT_READ | PROT_WRITE, │
│ MAP_SHARED, fd, 0); │
│ │
│ 4. PAINT NEW BUFFER │
│ uint32_t *pixels = (uint32_t *)data; │
│ for (int i = 0; i < 1024 * 768; i++) { │
│ pixels[i] = 0xFFFF0000; // Red color │
│ } │
│ │
│ 5. CREATE wl_buffer FROM POOL │
│ struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, │
│ new_size); │
│ struct wl_buffer *buffer = │
│ wl_shm_pool_create_buffer(pool, 0, │
│ 1024, 768, │
│ 1024 * 4, │
│ WL_SHM_FORMAT_XRGB8888); │
└──────────────────────────────────────────────────────────────────┘
│
▼
Time: T4 - Client commits the new buffer
═══════════════════════════════════════════════════════════════════════════
Client Compositor
| |
| wl_surface.attach(new_buffer, 0, 0) |
| (Attach new 1024x768 buffer) |
|--------------------------------------------> |
| | Pending state:
| | buffer = new_buffer
| |
| wl_surface.damage_buffer(0, 0, 1024, 768) |
| (Mark entire buffer as needing redraw) |
|--------------------------------------------> |
| | Pending state:
| | damage = full surface
| |
| wl_surface.commit() |
| (ATOMICALLY apply all pending changes) |
|============================================> |
| |
| | ┌──────────────────────┐
| | │ Compositor applies: │
| | │ 1. Checks ack serial │
| | │ 2. Validates buffer │
| | │ 3. Updates surface │
| | │ 4. Schedules repaint │
| | └──────────────────────┘
| |
| | Composites new buffer
| | to framebuffer
| |
| | 🖥️ Screen updates!
| | Window is now 1024x768
| |
| wl_buffer.release(old_buffer) |
| (Old buffer no longer needed) |
| <--------------------------------------------|
| |
| Close old buffer fd, free memory |
| |
| wl_surface.frame(new_id) |
| (Request next frame callback) |
|--------------------------------------------> |
| |
| wl_callback.done(timestamp) |
| <--------------------------------------------|
| |
CRITICAL TIMING RULES:
═══════════════════════════════════════════════════════════════════════════
1. MUST ack_configure() BEFORE commit()
❌ commit() -> ack_configure() (Protocol error!)
✅ ack_configure() -> commit() (Correct)
2. Can receive multiple configure events before committing
Compositor: configure(800, 700, serial=100)
Compositor: configure(900, 800, serial=101)
Compositor: configure(1024, 768, serial=102)
Client: Only needs to ack the LAST one (serial=102)
3. Window doesn't resize until BOTH:
- Client acks the configure
- Client commits a buffer matching the new size
4. Compositor may reject commit if:
- No ack_configure() was sent
- Buffer size doesn't match configured size
- Serial number is from an old configure
COMMON MISTAKES:
═══════════════════════════════════════════════════════════════════════════
❌ Attaching a buffer BEFORE receiving configure
-> Window may appear at wrong size, or protocol error
❌ Not creating a new buffer for the new size
-> Old buffer scaled/stretched (looks bad)
❌ Forgetting to ack_configure()
-> Protocol error: "xdg_surface has never been configured"
❌ Committing before ack_configure()
-> Protocol error: "buffer committed without configure ack"
✅ CORRECT SEQUENCE:
1. Receive configure event
2. ack_configure(serial)
3. Create new buffer at new size
4. Attach buffer
5. Commit
Work through this trace before implementing resize handling.
Mental Model Exercise: Buffer Lifecycle
Complete state machine showing all buffer lifecycle transitions:
WAYLAND BUFFER LIFECYCLE STATE MACHINE
════════════════════════════════════════════════════════════════════════════
┌──────────────────────┐
│ START │
└──────────┬───────────┘
│
│ wl_shm_pool.create_buffer()
│ (Allocate buffer object)
▼
┌──────────────────────┐
┌────▶│ 1. CREATED │◀────┐
│ │ │ │
│ │ State: │ │ wl_buffer.release event
│ │ - Exists in client │ │ (Compositor done with it)
│ │ - Not attached │ │
│ │ - Contains pixels │ │
│ └──────────┬───────────┘ │
│ │ │
│ │ wl_surface.attach(buffer, x, y)
│ │ (Buffer added to pending state)
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 2. ATTACHED │ │
│ │ │ │
│ │ State: │ │
│ │ - In pending state │ │
│ │ - Not yet visible │ │
│ │ - Can be detached │ │
│ └──────────┬───────────┘ │
│ │ │
│ │ wl_surface.commit()
│ │ (Atomically apply pending state)
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 3. PENDING │ │
│ │ │ │
│ │ State: │ │
│ │ - Committed │ │
│ │ - In commit queue │ │
│ │ - Waiting for scan │ │
│ └──────────┬───────────┘ │
│ │ │
│ │ Compositor's next frame
│ │ (Reads buffer, composites to screen)
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ 4. IN_USE │ │
│ │ │ │
│ │ State: │ │
│ │ - Compositor reads │ │
│ │ - Pixels on screen │ │
│ │ - DO NOT MODIFY! │ │
│ └──────────┬───────────┘ │
│ │ │
│ │ Compositor finishes
│ │ (Next frame uses different buffer
│ │ OR window unmapped)
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
└─────│ 5. RELEASED │─────┘
│ │
│ State: │
│ - Compositor done │
│ - Can be modified │
│ - Can be reattached │
│ - Or destroyed │
└──────────┬───────────┘
│
│ wl_buffer.destroy()
│ munmap(), close(fd)
│
▼
┌──────────────────────┐
│ 6. DESTROYED │
│ │
│ State: │
│ - Memory freed │
│ - Object gone │
└──────────────────────┘
DETAILED STATE TRANSITIONS:
════════════════════════════════════════════════════════════════════════════
State 1: CREATED
─────────────────
Entry: wl_shm_pool.create_buffer(pool, offset, width, height, stride, format)
Returns: wl_buffer object
What you can do:
✅ Paint pixels to the mmap'd shared memory
✅ Attach to a surface
✅ Destroy immediately if you change your mind
What you CANNOT do:
❌ Assume compositor can see it (not attached yet)
❌ Use it for rendering (needs to be committed)
Transition out: wl_surface.attach() -> State 2 (ATTACHED)
State 2: ATTACHED
──────────────────
Entry: wl_surface.attach(buffer, x_offset, y_offset)
(Buffer is now in the surface's pending state)
What you can do:
✅ Continue modifying pixels (commit hasn't happened yet)
✅ Attach a different buffer (overwrites pending state)
✅ Call commit() to apply
What you CANNOT do:
❌ Assume compositor sees it (pending, not applied)
Transition out: wl_surface.commit() -> State 3 (PENDING)
State 3: PENDING
─────────────────
Entry: wl_surface.commit()
(Buffer moves from pending state to committed state queue)
What happens:
• Pending state becomes current state
• Buffer enters compositor's processing queue
• Compositor will read it on next repaint cycle
• You should STOP modifying pixels now!
What you can do:
✅ Request frame callback: wl_surface.frame()
✅ Prepare next buffer (for double-buffering)
What you CANNOT do:
❌ Modify pixels (compositor might be reading them)
❌ Destroy buffer (compositor needs it)
Transition out: Compositor scans buffer -> State 4 (IN_USE)
State 4: IN_USE
────────────────
Entry: Compositor's repaint cycle reads buffer
What happens:
• Compositor reads pixel data
• Pixels are displayed on screen (composited with other surfaces)
• Buffer might be scanned multiple times (e.g., 60 FPS = 60 reads/sec)
• Continues until compositor commits to a NEW buffer or surface unmaps
CRITICAL: DO NOT MODIFY BUFFER CONTENTS!
⚠️ Modifying while IN_USE causes visual artifacts (tearing, corruption)
⚠️ This is YOUR responsibility (compositor doesn't lock the memory)
Transition out: Compositor sends wl_buffer.release -> State 5 (RELEASED)
State 5: RELEASED
──────────────────
Entry: wl_buffer.release() event received
What it means:
• Compositor guarantees it's not reading this buffer anymore
• Safe to modify, reuse, or destroy
What you can do:
✅ Modify pixels for next frame
✅ Attach and commit again (go back to State 2)
✅ Destroy: wl_buffer.destroy() -> State 6
What you SHOULD do:
• In double-buffering: Reuse this buffer (paint new content)
• In triple-buffering: Put in free pool
Transition out:
- wl_surface.attach() -> State 2 (ATTACHED)
- wl_buffer.destroy() -> State 6 (DESTROYED)
State 6: DESTROYED
───────────────────
Entry: wl_buffer.destroy()
munmap(data, size)
close(fd)
Final state:
• Memory unmapped
• File descriptor closed
• Wayland object destroyed
• No return from this state
PRACTICAL BUFFERING STRATEGIES:
════════════════════════════════════════════════════════════════════════════
Single Buffering (NOT RECOMMENDED):
─────────────────────────────────────
┌─────────┐
│ Buffer A│ -> attach -> commit -> IN_USE ──┐
└─────────┘ │
▲ │ release
└────────────────────────────────────┘
Problem: Must wait for release before painting next frame
Result: Low frame rate, stuttering
Double Buffering (RECOMMENDED):
─────────────────────────────────
┌─────────┐ ┌─────────┐
│ Buffer A│ -> commit -> IN_USE │ Buffer B│ <- paint here while A displayed
└─────────┘ │ └─────────┘
▲ │ release │
└──────────────┘ │ commit
paint <-─────── IN_USE ─┘
Benefit: Always have a free buffer to paint while one is displayed
Result: Smooth 60 FPS
Triple Buffering (FOR HIGH FPS):
─────────────────────────────────
Three buffers rotate: one IN_USE, one PENDING, one being painted
Benefit: Can sustain rendering faster than display refresh (e.g., 144 Hz)
COMMON ERRORS:
════════════════════════════════════════════════════════════════════════════
❌ ERROR: Modifying buffer while IN_USE
Symptom: Screen tearing, partial frames visible
Fix: Wait for wl_buffer.release before modifying
❌ ERROR: Destroying buffer while IN_USE
Symptom: Compositor displays garbage, or crashes
Fix: Only destroy after receiving wl_buffer.release
❌ ERROR: Not handling wl_buffer.release event
Symptom: Leak buffers, run out of memory
Fix: Listen for release events, reuse or free buffers
❌ ERROR: Attaching same buffer twice without release
Symptom: Works, but inefficient (can't paint while displayed)
Fix: Use double-buffering
✅ CORRECT PATTERN:
while (running) {
wait_for_event();
if (need_redraw && buffer_available) {
paint_to_buffer(free_buffer);
wl_surface_attach(surface, free_buffer, 0, 0);
wl_surface_commit(surface);
free_buffer = NULL; // Mark as in-use
}
if (buffer_release_event) {
free_buffer = released_buffer; // Now available
}
}
Questions:
- Can you attach a buffer that’s currently displayed?
- Can you destroy a buffer immediately after committing it?
- How do you know when it’s safe to reuse a buffer?
The Interview Questions They’ll Ask
Prepare to answer these about Wayland architecture:
Conceptual Questions
- “What is the fundamental difference between X11 and Wayland architecture?”
- Expected answer: X11 has a separate window manager and compositor; Wayland merges them. X11 server renders on behalf of clients; Wayland clients render themselves.
- “Why does Wayland use shared memory for buffers instead of sending pixel data over sockets?”
- Expected answer: Zero-copy efficiency. Shared memory allows the compositor to access pixel data without copying between processes.
- “What is the configure/ack/commit cycle in Wayland, and why is it necessary?”
- Expected answer: Configure tells the client the new size/state; ack confirms the client received it with the serial; commit applies the changes. This ensures the client and compositor stay in sync.
- “How does Wayland ensure security compared to X11?”
- Expected answer: X11 allowed any client to see/modify other clients’ windows (keylogging, screenshots). Wayland isolates clients; the compositor controls all access.
Implementation Questions
- “How do you create a shared memory buffer in Wayland?”
- Expected answer: Use
shm_open()ormemfd_create()to get an fd,ftruncate()to set size,mmap()to map it, thenwl_shm_create_pool()andwl_shm_pool_create_buffer().
- Expected answer: Use
- “What happens if you attach a buffer and commit without waiting for a configure event?”
- Expected answer: Protocol error. The compositor expects you to ack the initial configure before committing content.
- “What is a Wayland global, and how do you discover available globals?”
- Expected answer: A global is a compositor-provided interface (e.g.,
wl_compositor,wl_shm). You get the registry and listen for global announcements.
- Expected answer: A global is a compositor-provided interface (e.g.,
- “Why must you call
wl_display_roundtrip()orwl_display_dispatch()after requesting the registry?”- Expected answer: Wayland is asynchronous. You must process events for the registry announcements to arrive.
Debugging Questions
- “Your Wayland client compiles but shows no window. What are the most likely causes?”
- Expected answer: (a) Didn’t ack configure, (b) didn’t commit surface, (c) didn’t attach buffer, (d) not running on a Wayland compositor, (e) protocol error terminated connection.
- “How can you debug Wayland protocol interactions?”
- Expected answer: Set
WAYLAND_DEBUG=1environment variable to see all protocol messages.
- Expected answer: Set
Hints in Layers
Use these progressively if you get stuck. Try to solve each part before looking at the next hint.
Hint 1: Connecting and Getting the Registry
If you’re stuck on initial connection:
The very first code you write should look like this:
#include <wayland-client.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
struct wl_display *display = wl_display_connect(NULL);
if (!display) {
fprintf(stderr, "Failed to connect to Wayland display\n");
return 1;
}
printf("Connected to Wayland display\n");
wl_display_disconnect(display);
return 0;
}
Compile and run. If this works, you’re connected.
Next step: Get the registry and listen for globals.
Hint 2: Registry Listener Pattern
If you’re stuck on handling registry announcements:
Wayland uses a callback pattern. You must create a listener structure:
static void registry_global(void *data, struct wl_registry *registry,
uint32_t name, const char *interface,
uint32_t version) {
printf("Global: %s v%u\n", interface, version);
if (strcmp(interface, wl_compositor_interface.name) == 0) {
// Bind to compositor
struct wl_compositor *compositor = wl_registry_bind(
registry, name, &wl_compositor_interface, 4);
// Store compositor in your state struct
}
// Repeat for wl_shm, xdg_wm_base, etc.
}
static void registry_global_remove(void *data, struct wl_registry *registry,
uint32_t name) {
// Handle global removal (usually ignore for now)
}
static const struct wl_registry_listener registry_listener = {
.global = registry_global,
.global_remove = registry_global_remove,
};
// In main():
struct wl_registry *registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, ®istry_listener, &my_state);
wl_display_roundtrip(display); // Process events
Hint 3: Creating the Surface Hierarchy
If you’re stuck on creating surfaces:
The order is crucial:
// 1. Create Wayland surface
struct wl_surface *surface = wl_compositor_create_surface(compositor);
// 2. Create XDG surface from Wayland surface
struct xdg_surface *xdg_surface = xdg_wm_base_get_xdg_surface(xdg_wm_base, surface);
// 3. Add configure listener to xdg_surface
xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, &my_state);
// 4. Create toplevel role
struct xdg_toplevel *xdg_toplevel = xdg_surface_get_toplevel(xdg_surface);
// 5. Set window title
xdg_toplevel_set_title(xdg_toplevel, "My Wayland Client");
// 6. Commit the surface (this triggers initial configure)
wl_surface_commit(surface);
// 7. Wait for configure event before attaching buffers!
Hint 4: Shared Memory Buffer Creation
If you’re stuck on creating buffers:
Here’s the complete buffer creation pattern:
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
static int create_shm_file(off_t size) {
int fd = memfd_create("wayland-shm", MFD_CLOEXEC);
if (fd < 0) {
return -1;
}
if (ftruncate(fd, size) < 0) {
close(fd);
return -1;
}
return fd;
}
static struct wl_buffer* create_buffer(struct wl_shm *shm,
int width, int height,
void **out_data) {
int stride = width * 4; // 4 bytes per pixel (XRGB8888)
int size = stride * height;
int fd = create_shm_file(size);
if (fd < 0) {
return NULL;
}
void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (data == MAP_FAILED) {
close(fd);
return NULL;
}
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size);
struct wl_buffer *buffer = wl_shm_pool_create_buffer(
pool, 0, width, height, stride, WL_SHM_FORMAT_XRGB8888);
wl_shm_pool_destroy(pool);
close(fd);
*out_data = data;
return buffer;
}
Hint 5: Painting and Committing
If you’re stuck on making pixels appear:
After creating the buffer:
void *buffer_data;
struct wl_buffer *buffer = create_buffer(shm, 800, 600, &buffer_data);
// Paint red color
uint32_t *pixels = buffer_data;
for (int i = 0; i < 800 * 600; i++) {
pixels[i] = 0xFFFF0000; // Format: 0xAARRGGBB (X=ignored, R, G, B)
}
// Attach buffer to surface
wl_surface_attach(surface, buffer, 0, 0);
// Mark entire surface as damaged (needs redraw)
wl_surface_damage_buffer(surface, 0, 0, 800, 600);
// Commit!
wl_surface_commit(surface);
Hint 6: The Event Loop
If you’re stuck on keeping the window alive:
// Simple blocking event loop
while (wl_display_dispatch(display) != -1) {
// Events are processed in callbacks
}
// Or non-blocking:
while (running) {
wl_display_dispatch_pending(display);
wl_display_flush(display);
// Do other work
}
Hint 7: Debugging with WAYLAND_DEBUG
If nothing works and you don’t know why:
Run your program like this:
$ WAYLAND_DEBUG=1 ./wayland_client 2>&1 | less
You’ll see every protocol message:
[3436648.144] wl_display@1.get_registry(new id wl_registry@2)
[3436648.162] wl_display@1.sync(new id wl_callback@3)
[3436648.327] -> wl_registry@2.global(1, "wl_compositor", 6)
[3436648.339] -> wl_registry@2.global(2, "wl_shm", 1)
...
This shows exactly what your client is sending and receiving.
Hint 8: Minimal Complete Example Structure
If you need a complete skeleton:
#include <wayland-client.h>
#include "xdg-shell-client-protocol.h" // Generated from XML
#include <stdio.h>
#include <string.h>
struct app_state {
struct wl_display *display;
struct wl_registry *registry;
struct wl_compositor *compositor;
struct wl_shm *shm;
struct xdg_wm_base *xdg_wm_base;
struct wl_surface *surface;
struct xdg_surface *xdg_surface;
struct xdg_toplevel *xdg_toplevel;
struct wl_buffer *buffer;
void *buffer_data;
int width;
int height;
};
// Implement listeners: registry_listener, xdg_wm_base_listener,
// xdg_surface_listener, xdg_toplevel_listener
int main() {
struct app_state state = {0};
state.width = 800;
state.height = 600;
// 1. Connect
// 2. Get registry
// 3. Bind globals
// 4. Create surface hierarchy
// 5. Wait for configure
// 6. Create buffer
// 7. Paint and commit
// 8. Event loop
// 9. Cleanup
return 0;
}
Books That Will Help
| Topic | Book | Chapter/Section |
|---|---|---|
| Wayland Protocol Basics | “The Wayland Book” by Drew DeVault | Chapters 1-4 (Connection, Registry, Surfaces) - https://wayland-book.com |
| Wayland Protocol Basics | “The Wayland Book” by Drew DeVault | Chapter 8-9 (XDG-Shell, Surface Lifecycle) |
| Shared Memory Fundamentals | “The Linux Programming Interface” by Michael Kerrisk | Chapter 54: POSIX Shared Memory |
| Memory Mapping (mmap) | “The Linux Programming Interface” by Michael Kerrisk | Chapter 49: Memory Mappings |
| Alternative: Shared Memory | “Advanced Programming in the UNIX Environment, 3rd Edition” by Stevens & Rago | Chapter 15: Interprocess Communication |
| File Descriptors and I/O | “The Linux Programming Interface” by Michael Kerrisk | Chapters 4-5: File I/O Basics |
| Event-Driven Programming | “The Linux Programming Interface” by Michael Kerrisk | Chapter 63: Alternative I/O Models (poll, epoll) |
| How Graphics Work on Linux | “How Linux Works, 3rd Edition” by Brian Ward | Chapter 14: Introduction to Graphics |
| Understanding Binary Formats | “Computer Systems: A Programmer’s Perspective” by Bryant & O’Hallaron | Chapter 2.1: Information Storage (for pixel formats) |
| C Memory Management | “Effective C, 2nd Edition” by Robert C. Seacord | Chapter 6: Memory Management |
| POSIX Systems Programming | “The Linux Programming Interface” by Michael Kerrisk | Chapter 1-3: Fundamental Concepts |
| Protocol Design Philosophy | “The Sockets Networking API” by Stevens | Chapter 2: Interprocess Communications (IPC concepts) |
| Debugging Graphics Programs | “Low-Level Programming” by Igor Zhirkov | Chapter 11: Debugging (using gdb with graphics) |
Recommended Reading Order
Before you start coding (Week 1):
- “The Wayland Book” - Chapters 1-4 (understand the protocol model)
- “The Linux Programming Interface” - Chapter 54 (understand shared memory)
While implementing (Week 1-2):
- “The Wayland Book” - Chapters 8-9 (XDG-Shell specifics)
- “The Linux Programming Interface” - Chapter 49 (mmap details)
- “Effective C” - Chapter 6 (avoid memory bugs in your client)
For deeper understanding (After completion):
- “Computer Systems: A Programmer’s Perspective” - Ch. 2.1 (understand pixel format layouts)
- “The Linux Programming Interface” - Chapter 63 (improve event loop efficiency)
Online Resources
- Wayland Protocol XML:
/usr/share/wayland/wayland.xmlon your system - XDG-Shell Protocol XML:
/usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml - wayland-scanner: Use
wayland-scanner client-header < protocol.xml > protocol.hto generate headers - Example Code: Check
westonsimple clients: https://gitlab.freedesktop.org/wayland/weston/-/tree/main/clients
Common Pitfalls & Debugging
This section addresses the most common problems you’ll encounter and how to fix them.
Problem 1: “Failed to connect to Wayland display”
Symptoms:
$ ./wayland_client
Failed to connect to Wayland display
Why: Your program can’t find the Wayland compositor socket.
Root Causes:
- You’re running under X11 instead of Wayland
WAYLAND_DISPLAYenvironment variable is not set- The socket at
/run/user/1000/wayland-0doesn’t exist
Fix:
# Check if you're actually running Wayland:
echo $WAYLAND_DISPLAY
# Should print something like "wayland-0" or "wayland-1"
# If empty, you're likely on X11. Log out and choose "Wayland" session at login
loginctl show-session $(loginctl | grep $(whoami) | awk '{print $1}') -p Type
# Should show "Type=wayland"
# Manually set the display if needed:
export WAYLAND_DISPLAY=wayland-0
# Verify socket exists:
ls -la $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY
Quick test: Does wayland-info work? If not, your Wayland setup is broken.
Problem 2: “Protocol error: wl_surface@XX: error 2: invalid buffer size”
Symptoms:
The Wayland connection experienced a fatal error: Protocol error
Why: Your buffer dimensions or stride don’t match what you told the compositor.
Root Cause: Incorrect stride calculation or buffer size.
Fix:
// WRONG:
int stride = width * 4; // Might not be aligned!
// CORRECT:
int stride = width * 4;
// Ensure 4-byte alignment (required by most compositors):
stride = (stride + 3) & ~3;
// WRONG:
int buffer_size = width * height * 4;
// CORRECT:
int buffer_size = stride * height; // Use stride, not width!
Debugging:
# Enable protocol debugging:
WAYLAND_DEBUG=1 ./wayland_client 2>&1 | less
# Look for your wl_surface@XX object ID
# Find the wl_buffer.attach call
# Check if dimensions match create_buffer()
Problem 3: “Window appears but is completely white/black”
Symptoms: Window shows up but doesn’t display your expected colors.
Why: Pixel format mismatch or you’re writing to wrong buffer offset.
Root Causes:
- Wrong pixel format (ARGB vs XRGB)
- Endianness issues (rare on x86, common on ARM)
- Buffer not fully written before commit
Fix:
// Verify pixel format matches:
uint32_t format = WL_SHM_FORMAT_XRGB8888; // Most common
wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, format);
// XRGB8888 layout (little-endian):
// Byte 0: Blue
// Byte 1: Green
// Byte 2: Red
// Byte 3: Unused (X)
// To write a pixel (x, y) with color (R, G, B):
uint32_t *pixel = (uint32_t *)(data + y * stride + x * 4);
*pixel = (0xFF << 24) | (r << 16) | (g << 8) | b; // XRGB
// Or use macros:
#define PIXEL(r, g, b) ((0xFF << 24) | ((r) << 16) | ((g) << 8) | (b))
Quick test: Fill entire buffer with single color:
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
uint32_t *pixel = (uint32_t *)(data + y * stride + x * 4);
*pixel = 0xFF00FF00; // Bright green
}
}
wl_surface_commit(surface);
If you don’t see bright green, pixel format is wrong.
Problem 4: “Window doesn’t respond to close button”
Symptoms: Window opens but clicking X button does nothing.
Why: You’re not listening to xdg_toplevel close event.
Fix:
static void xdg_toplevel_handle_close(void *data,
struct xdg_toplevel *xdg_toplevel) {
struct client_state *state = data;
state->running = false; // Signal event loop to exit
}
static const struct xdg_toplevel_listener xdg_toplevel_listener = {
.configure = xdg_toplevel_handle_configure,
.close = xdg_toplevel_handle_close, // <- ADD THIS
};
// In main event loop:
while (state.running && wl_display_dispatch(display) != -1) {
// ...
}
Problem 5: “Segmentation fault in wl_display_dispatch()”
Symptoms: Crash with backtrace pointing to Wayland internals.
Why: You destroyed an object that still has pending events, or double-freed something.
Root Causes:
- Destroying objects in wrong order
- Not setting pointers to NULL after destroying
- Accessing freed memory in callbacks
Fix:
// WRONG ORDER:
wl_display_disconnect(display); // Destroys everything
wl_surface_destroy(surface); // <- SEGFAULT: surface already gone!
// CORRECT ORDER (reverse of creation):
if (frame_callback) wl_callback_destroy(frame_callback);
if (buffer) wl_buffer_destroy(buffer);
if (shm_pool) wl_shm_pool_destroy(shm_pool);
if (xdg_toplevel) xdg_toplevel_destroy(xdg_toplevel);
if (xdg_surface) xdg_surface_destroy(xdg_surface);
if (surface) wl_surface_destroy(surface);
if (shm) wl_shm_destroy(shm);
if (compositor) wl_compositor_destroy(compositor);
if (registry) wl_registry_destroy(registry);
wl_display_disconnect(display); // Last!
// Use defensive NULL checks:
if (surface) {
wl_surface_destroy(surface);
surface = NULL;
}
Problem 6: “Window doesn’t appear until I resize it”
Symptoms: wl_display_dispatch() runs but window never shows. Resizing the window makes it appear.
Why: You didn’t commit after the initial configure.
Root Cause: Missing wl_surface_commit() in configure handler.
Fix:
static void xdg_surface_handle_configure(void *data,
struct xdg_surface *xdg_surface,
uint32_t serial) {
xdg_surface_ack_configure(xdg_surface, serial);
// <- MUST COMMIT HERE!
struct client_state *state = data;
// Attach buffer if ready
if (state->buffer) {
wl_surface_attach(state->surface, state->buffer, 0, 0);
wl_surface_commit(state->surface); // <- CRITICAL!
}
}
Problem 7: “Linker error: undefined reference to wl_*”
Symptoms:
/usr/bin/ld: /tmp/client.o: undefined reference to `wl_display_connect'
Why: Not linking against libwayland-client.
Fix:
# Add to compilation:
gcc client.c -o client $(pkg-config --cflags --libs wayland-client) \
-lwayland-client -lxkbcommon
# Or in Makefile:
CFLAGS = $(shell pkg-config --cflags wayland-client)
LIBS = $(shell pkg-config --libs wayland-client)
client: client.c
$(CC) $(CFLAGS) client.c -o client $(LIBS)
Problem 8: “Memory leak detected by valgrind”
Symptoms:
==12345== 4,096 bytes in 1 blocks are definitely lost
==12345== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345== by 0x10A2C3: main (client.c:123)
Why: Forgot to unmap shared memory or destroy Wayland objects.
Fix:
// Don't forget to unmap:
if (data) {
munmap(data, buffer_size);
data = NULL;
}
// Don't forget to close FD:
if (fd >= 0) {
close(fd);
fd = -1;
}
// Common leak: forgetting to destroy frame callback
if (callback) {
wl_callback_destroy(callback);
callback = NULL;
}
Debugging:
# Run with valgrind:
valgrind --leak-check=full --show-leak-kinds=all ./wayland_client
# Look for "definitely lost" blocks
# Match line numbers to your code
Debugging Tools & Techniques
1. Protocol Debugging with WAYLAND_DEBUG
# See all protocol messages:
WAYLAND_DEBUG=1 ./wayland_client
# Filter for specific object:
WAYLAND_DEBUG=1 ./wayland_client 2>&1 | grep wl_surface@
# Save to file for analysis:
WAYLAND_DEBUG=1 ./wayland_client 2> protocol.log
What to look for:
- Request/event order (attach -> damage -> commit)
- Object IDs matching between client and server
- Error messages with protocol error codes
2. GDB Debugging
# Compile with debug symbols:
gcc -g -O0 client.c -o client $(pkg-config --cflags --libs wayland-client)
# Run in gdb:
gdb ./wayland_client
# Useful commands:
(gdb) break main
(gdb) run
(gdb) next # Step over
(gdb) step # Step into
(gdb) print surface # Inspect variable
(gdb) backtrace # Show call stack
3. wayland-info Tool
# List all globals your compositor supports:
wayland-info
# Look for:
# - wl_compositor (needed!)
# - wl_shm (needed!)
# - xdg_wm_base (needed for xdg-shell)
4. Strace for System Call Debugging
# See all system calls:
strace ./wayland_client 2>&1 | less
# Look for:
# - connect() calls to Wayland socket
# - mmap() for shared memory
# - sendmsg()/recvmsg() for protocol messages
General Debugging Strategy
- Start simple: Get
wl_display_connect()working first - Add logging: printf() liberally – know where your code is
- Check return values: Most Wayland functions return NULL on failure
- Use WAYLAND_DEBUG: See what’s actually happening on the wire
- Read the protocol XML:
/usr/share/wayland/wayland.xmlis the truth - Compare with working examples: weston simple clients are gold standard
- Ask for help: #wayland on OFTC IRC, people are friendly!
Definition of Done
wl_display_connectsucceeds and globals are bound (compositor, shm, xdg_wm_base)- A visible window appears and remains responsive for at least 60 seconds
- Configure/ack/commit ordering is correct (no protocol errors)
- Buffer lifecycle is correct (create -> attach -> commit -> destroy) with no leaks
- Frame callbacks are used (no busy loop or 100% CPU spin)
WAYLAND_DEBUG=1shows the expected request/event sequence without errors
Project 2: Simple Wayland Compositor with wlroots
- File: WAYLAND_X11_COMPOSITOR_LEARNING_PROJECTS.md
- Programming Language: C
- Coolness Level: Level 5: Pure Magic (Super Cool)
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 5: Master
- Knowledge Area: Window Managers / Graphics Pipeline
- Software or Tool: wlroots
- Main Book: “The Wayland Book” by Drew DeVault
What you’ll build: A minimal but functional Wayland compositor using the wlroots library that can display client windows, handle keyboard/mouse input, and manage window focus.
Why it teaches compositors: wlroots abstracts the hard parts (DRM/KMS, libinput) while exposing the compositor logic. You’ll implement window stacking, damage tracking, and the compositor’s view of the Wayland protocol.
Core challenges you’ll face:
- Understanding the compositor’s role as protocol implementer
- Managing the scene graph (which windows are where, z-order)
- Implementing focus and keyboard grab semantics
- Handling output (monitor) configuration
- Rendering the final composited frame
Resources for key challenges:
- “tinywl” reference compositor in wlroots - The canonical minimal example
- wlroots documentation and source code - https://gitlab.freedesktop.org/wlroots/wlroots
- wlroots backend overview (DRM/Wayland/X11/headless) - https://wlroots.readthedocs.io/
Key Concepts:
- Compositor Architecture: wlroots wiki and tinywl source code
- DRM/KMS Subsystem: “The Linux Programming Interface” by Kerrisk + kernel documentation
- Input Handling: libinput documentation
- Scene Graphs: wlroots scene API documentation
Difficulty: Advanced Time estimate: 1 month+ Prerequisites: Project 1 completed, understanding of graphics pipelines, event-driven C
Real world outcome: A working compositor you can log into! Launch it from a TTY, and it will display a background, let you open terminals and applications, move windows around, and switch focus with keyboard shortcuts.
Learning milestones:
- Display a background and handle output hotplug -> understand DRM/KMS
- Accept client connections and display their buffers -> understand server-side protocol
- Implement keyboard focus and pointer events -> understand input routing
- Add window decorations and move/resize -> understand interactive grabs
Real World Outcome
You’ll have a fully functional Wayland compositor that you can actually log into and use as your daily desktop environment:
How to launch from TTY:
- Switch to a virtual console (Ctrl+Alt+F2 on most systems)
- Log in with your username and password
- Run your compositor:
./my-compositor - Your screen clears, and you see your compositor’s background
What happens when it launches:
- The compositor takes over the graphics card (via DRM/KMS)
- Your screen displays a solid background color (configurable)
- A cursor appears, controllable with your mouse
- The compositor starts listening on the Wayland socket (
WAYLAND_DISPLAY=wayland-1) - You can launch applications that will appear as windows
How to use it:
- Launch a terminal:
WAYLAND_DISPLAY=wayland-1 footorweston-terminal - Windows appear on your screen, rendered by the clients themselves
- Click to focus windows–the focused window gets a subtle border
- Move windows: Hold Alt+left mouse button and drag
- Resize windows: Hold Alt+right mouse button and drag from edges
- Switch between windows: Alt+Tab cycles focus
- Close windows: The application’s close button (your compositor just handles the window)
- Exit compositor: Ctrl+Shift+Esc (or whatever keybinding you implement)
What you’ll actually see:
+----------------------------------------------------------------+
| Your Compositor |
| |
| +------------------------+ +---------------------------+ |
| | Terminal Window | | Firefox Window | |
| | user@host:~$ ls | | [Web content rendered | |
| | my-compositor | | by Firefox, displayed | |
| | Documents/ | | by your compositor] | |
| | Downloads/ | +---------------------------+ |
| | user@host:~$ _ | |
| +------------------------+ |
| |
| Mouse cursor moves smoothly across the entire screen |
| Windows can be moved, resized, focused |
| |
+----------------------------------------------------------------+
Key observations during use:
- Windows don’t flicker when moved–the compositor double-buffers everything
- Each application renders its own content–the compositor just positions the buffers
- Input goes to the focused window automatically–the compositor routes events
- Multiple monitors work independently–each output has its own scene
- Performance feels smooth because wlroots handles damage tracking efficiently
The “aha!” moment: When you launch your first graphical application and it appears on screen, positioned by your code, accepting input through your event routing, you realize you’ve built the core of a desktop environment. This is what Wayland is–a protocol for compositors to manage application windows.
Debugging workflow:
- Compositor crashes? You’re dropped back to the TTY console
- Add logging:
fprintf(stderr, "Focus changed to window %p\n", window); - Run clients manually from another TTY to test window management
- Use
weston-terminalas a reliable test client–it’s simple and well-behaved
The Core Question You’re Answering
“What does it really mean to be a Wayland compositor, and how do the pieces fit together to create a functional windowing system?”
This project forces you to answer fundamental questions about display servers:
- What is a compositor’s job? To negotiate protocol objects (surfaces, seats, outputs), route input events to the right clients, composite client buffers onto the screen, and maintain the windowing policy (focus, stacking, positioning)
- How does the protocol become reality? By implementing the server side–when a client calls
wl_compositor.create_surface(), your code allocates a surface object and sends back a resource ID - Where does rendering actually happen? Clients render to shared buffers (DMA-BUF or shared memory), your compositor just positions and blends them onto outputs
- How do you manage state? The compositor is stateful–it tracks which windows exist, where they are, which has focus, what input devices are connected
- What makes Wayland secure? Input isolation–only the focused window receives keyboard input, other windows can’t snoop
By building a compositor, you’ll discover:
- Why wlroots exists: Direct DRM/KMS and libinput are complex–wlroots provides scene graphs, output management, and input handling so you focus on policy (how windows behave) not mechanism (how pixels appear)
- Why Wayland is stateless from the client’s perspective: Clients render independently, the compositor just tells them when to draw–this enables security and smooth animation
- Why the protocol is extensible: Base protocol (wl_compositor, wl_surface) is minimal; extensions (xdg-shell for windows, layer-shell for panels) add features without breaking compatibility
- Why there’s no “Wayland window manager”: The compositor IS the window manager–you decide focus behavior, tiling vs floating, keybindings, everything
The deeper insight: A compositor is simultaneously a protocol server, a rendering pipeline, and a policy engine. It’s the Unix philosophy inverted–instead of small tools composed together, it’s one tool doing everything related to display. This is why Wayland compositors can be radically different (Sway vs Weston vs KWin) while speaking the same protocol.
Compositor Rendering Pipeline
Understanding the complete rendering pipeline from client buffer to screen pixels:
WAYLAND COMPOSITOR RENDERING PIPELINE (Frame-by-Frame)
════════════════════════════════════════════════════════════════════════════
PHASE 1: CLIENT RENDERING (Parallel, Independent)
──────────────────────────────────────────────────────────────────────────
┌─────────────────────────┐ ┌─────────────────────────┐ ┌──────────────────────┐
│ Client A (Terminal) │ │ Client B (Browser) │ │ Client C (Editor) │
│ │ │ │ │ │
│ ┌───────────────────┐ │ │ ┌───────────────────┐ │ │ ┌──────────────────┐ │
│ │ Render to Buffer │ │ │ │ Render to Buffer │ │ │ │ Render to Buffer │ │
│ │ (GPU or CPU) │ │ │ │ (GPU or CPU) │ │ │ │ (GPU or CPU) │ │
│ └─────────┬─────────┘ │ │ └─────────┬─────────┘ │ │ └─────────┬────────┘ │
│ │ │ │ │ │ │ │ │
│ ┌────────▼─────────┐ │ │ ┌────────▼─────────┐ │ │ ┌────────▼────────┐ │
│ │ wl_buffer │ │ │ │ wl_buffer │ │ │ │ wl_buffer │ │
│ │ (SHM or DMA-BUF) │ │ │ │ (SHM or DMA-BUF) │ │ │ │ (SHM or DMA-BUF)│ │
│ └────────┬─────────┘ │ │ └────────┬─────────┘ │ │ └────────┬────────┘ │
└───────────┼─────────────┘ └───────────┼─────────────┘ └───────────┼──────────┘
│ │ │
│ wl_surface.attach() │ │
│ wl_surface.commit() │ │
▼ ▼ ▼
PHASE 2: COMPOSITOR RECEIVES BUFFERS
──────────────────────────────────────────────────────────────────────────
┌───────────────────────────────────┐
│ Wayland Compositor │
│ (libwayland-server + wlroots) │
└───────────────┬───────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
┌───────▼────────┐ ┌───────▼────────┐ ┌──────▼─────────┐
│ wl_surface A │ │ wl_surface B │ │ wl_surface C │
│ - buffer ref │ │ - buffer ref │ │ - buffer ref │
│ - position │ │ - position │ │ - position │
│ - z-order │ │ - z-order │ │ - z-order │
│ - damage │ │ - damage │ │ - damage │
└────────────────┘ └────────────────┘ └────────────────┘
PHASE 3: SCENE GRAPH CONSTRUCTION (wlroots Scene API)
──────────────────────────────────────────────────────────────────────────
┌─────────────────────────┐
│ wlr_scene (Root) │
│ - All surfaces │
└────────────┬────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌───────▼────────┐ ┌────────▼────────┐ ┌───────▼────────┐
│ Background │ │ Window Layer │ │ Overlay Layer │
│ (Wallpaper) │ │ (Applications) │ │ (Panels/Bars) │
└────────────────┘ └────────┬────────┘ └────────────────┘
│
┌────────────┼────────────┐
│ │ │
┌──────▼──────┐ ┌──▼──────┐ ┌───▼──────┐
│ Surface A │ │Surface B│ │Surface C │
│ Z=100 │ │Z=101 │ │Z=102 │
│ (10, 10) │ │(400,200)│ │(50, 500) │
└─────────────┘ └─────────┘ └──────────┘
PHASE 4: DAMAGE TRACKING & OPTIMIZATION
──────────────────────────────────────────────────────────────────────────
Compositor analyzes what changed:
┌────────────────────────────────────────────────────────┐
│ Damage Regions (areas needing redraw): │
│ │
│ ┌──────────────────┐ │
│ │ Region 1: │ Terminal moved from (10,10) │
│ │ (10, 10, 100, 50)│ to (15, 10) -> damage old +new │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Region 2: │ Browser scrolled -> damage │
│ │ (400,200,800,600)│ entire browser window │
│ └──────────────────┘ │
│ │
│ Editor unchanged -> NO damage, skip rendering │
└────────────────────────────────────────────────────────┘
OPTIMIZATION: Only re-composite damaged regions
✓ Saves GPU power
✓ Enables 1000+ FPS on modern GPUs when nothing changes
PHASE 5: GPU COMPOSITING (OpenGL/Vulkan)
──────────────────────────────────────────────────────────────────────────
Compositor sends rendering commands to GPU:
┌───────────────────────────────────────────────────────────────┐
│ GPU Render Pass: │
│ │
│ 1. Clear framebuffer (or use previous frame as base) │
│ │
│ 2. For each damaged region: │
│ for each surface in Z-order (back to front): │
│ │
│ if surface.buffer is DMA-BUF: │
│ ┌──────────────────────────────────┐ │
│ │ glBindTexture(buffer.texture) │ Zero-copy! │
│ │ glDrawArrays(quad, position) │ GPU -> GPU │
│ └──────────────────────────────────┘ │
│ │
│ if surface.buffer is SHM (shared memory): │
│ ┌──────────────────────────────────┐ │
│ │ glTexImage2D(copy pixels to GPU) │ One copy: │
│ │ glDrawArrays(quad, position) │ CPU -> GPU │
│ └──────────────────────────────────┘ │
│ │
│ Apply effects (blur, shadows, transparency) │
│ │
│ 3. Render cursor on top (always last) │
│ │
│ Result: Framebuffer contains composited final image │
└───────────────────────────────────────────────────────────────┘
PHASE 6: DISPLAY OUTPUT (DRM/KMS Atomic Commit)
──────────────────────────────────────────────────────────────────────────
Compositor sends framebuffer to display hardware:
┌──────────────────────────────────────────────────────────────┐
│ DRM/KMS Atomic Commit: │
│ │
│ struct drm_atomic_state { │
│ .crtc = CRTC_0, │
│ .connector = HDMI-1, │
│ .mode = 1920x1080@60Hz, │
│ .framebuffer = composited_buffer, │
│ .flags = DRM_MODE_ATOMIC_NONBLOCK │
│ }; │
│ │
│ ioctl(drm_fd, DRM_IOCTL_MODE_ATOMIC_COMMIT, &state); │
│ │
│ Kernel schedules buffer swap at next VBLANK │
│ (vertical blanking interval = no tearing) │
└──────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ Physical Display │
│ ┌─────────────────┐ │
│ │ Terminal │Brwsr│ │
│ │ │ │ │
│ │ └─────┘ │
│ │ Editor │
│ │ │
│ └─────────────────┘ │
│ User sees this! │
└───────────────────────┘
PHASE 7: FRAME CALLBACKS & CLIENT NOTIFICATION
──────────────────────────────────────────────────────────────────────────
After frame is displayed, notify clients:
Compositor Clients
│ │
│ wl_callback.done(timestamp) │
│ (Frame presented at time T) │
│───────────────────────────────> Client A
│ │ Can now render next frame
│ │
│ wl_callback.done(timestamp) │
│───────────────────────────────> Client B
│ │
│ wl_buffer.release(old_buffer) │
│ (Old buffer no longer needed) │
│───────────────────────────────> Client A
│ Can reuse buffer
PERFORMANCE CHARACTERISTICS:
════════════════════════════════════════════════════════════════════════════
Typical frame time budget (60 Hz = 16.67 ms per frame):
Client rendering: 0-10 ms (parallel, doesn't block compositor)
Damage calculation: <0.1 ms (wlroots is optimized)
GPU compositing: 1-3 ms (depends on # of surfaces & effects)
DRM page flip: 0.1 ms (just a kernel call)
Waiting for VBLANK: 0-16 ms (synchronizes to display refresh)
──────────────────────────────
Total latency: ~16 ms (one refresh cycle)
Key optimizations:
✓ Damage tracking: Only redraw changed regions
✓ Zero-copy DMA-BUF: GPU textures shared directly (no CPU copy)
✓ Async page flip: Compositor doesn't block on display
✓ Parallel rendering: Clients render while compositor composites
COMPOSITOR RESPONSIBILITIES:
════════════════════════════════════════════════════════════════════════════
✓ Protocol Server:
- Accept client connections
- Implement wl_compositor, xdg_wm_base, wl_seat, wl_output interfaces
- Manage object lifecycle (surfaces, buffers, seats)
✓ Scene Management:
- Track all surfaces and their state (position, size, z-order)
- Build scene graph (layering, parent-child relationships)
- Handle surface mapping/unmapping
✓ Input Routing:
- Determine which surface is under cursor
- Route pointer, keyboard, touch events to correct client
- Enforce input isolation (security)
✓ Rendering Pipeline:
- Collect client buffers
- Composite to framebuffer (GPU or CPU)
- Send to display via DRM/KMS
- Track damage for optimization
✓ Policy Decisions:
- Window placement (floating, tiling, fullscreen)
- Focus management (click-to-focus, focus-follows-mouse)
- Resize/move semantics
- Decorations (client-side or server-side)
Concepts You Must Understand First
Before you can build a Wayland compositor, you need solid understanding of these foundational concepts:
| Concept | Why It Matters | Book Reference |
|---|---|---|
| Wayland protocol fundamentals | You need to understand the client side before implementing the server side–how surfaces work, what wl_registry does, the object model | “The Wayland Book” by Drew DeVault, Ch. 1-4 (complete client understanding first) |
| DRM/KMS subsystem | Your compositor directly controls the graphics hardware–you need to understand mode setting, page flipping, and the kernel graphics API | “The Linux Programming Interface” by Michael Kerrisk, Ch. 63 + Linux kernel DRM documentation |
| libinput event handling | Input devices (keyboards, mice, touchpads) generate events through libinput–you route these to Wayland clients | libinput documentation + “The Linux Programming Interface” Ch. 63 |
| Scene graphs and spatial data structures | You need to track which window is at which position, handle overlapping windows, and determine what to render | “Foundations of Game Engine Development, Volume 1: Mathematics” by Eric Lengyel, Ch. 3 (transforms and hierarchies) |
| EGL and OpenGL basics | Even though wlroots abstracts much of this, understanding how GPU rendering works helps debug issues | “OpenGL Programming Guide” (Red Book), Ch. 1-3 (context creation, basic rendering) |
| Event-driven architecture | Compositors are event loops–input events, client requests, and frame callbacks all happen asynchronously | “The Linux Programming Interface” Ch. 63 (epoll/poll) |
| Shared memory and buffer management | Windows exist as buffers (DMA-BUF or wl_shm)–you need to understand how clients share pixels with the compositor | “Advanced Programming in the UNIX Environment” Ch. 15 (IPC) |
| Protocol object lifecycle | Wayland is object-oriented–surfaces, buffers, seats are created, used, and destroyed with specific state transitions | “The Wayland Book” Ch. 2 (object model) |
Detailed Prerequisites:
- Complete Project 1 (Bare-Metal Wayland Client) (1-2 weeks)
- You MUST understand the client side before implementing the server
- Key insight: When you call
wl_surface_commit(), what happens on the compositor side? - Practice: Run your client with
WAYLAND_DEBUG=1to see protocol messages
- Understand DRM/KMS basics (3-4 days study)
- Read Linux kernel DRM documentation: “Mode Setting API”
- Understand: CRTCs, encoders, connectors, framebuffers, atomic mode setting
- Key insight: DRM is how you draw to the screen without X11–it’s the kernel’s graphics API
- Practice: Use
drm_infoormodetestto inspect your GPU’s capabilities
- Study libinput event model (2 days study)
- Read libinput documentation: “Event Processing”
- Understand: Devices, seats (logical grouping), event types, coordinates
- Key insight: libinput normalizes input from mice, touchpads, tablets into unified events
- Practice: Write a small program that opens
/dev/input/event*and prints events
- Grasp wlroots architecture (1 week deep study)
- Read wlroots documentation thoroughly
- Study the
tinywlreference compositor line-by-line - Understand:
wlr_backend,wlr_renderer,wlr_scene,wlr_output,wlr_seat - Key insight: wlroots gives you a scene graph–you add surfaces to it, wlroots renders them
- Practice: Build and run
tinywl, modify it to change background color
- Understand xdg-shell protocol (2-3 days study)
- Read “The Wayland Book” Ch. 6: “XDG Shell”
- Understand: xdg_surface, xdg_toplevel, xdg_popup, configure/ack cycle
- Key insight: xdg-shell defines how windows work–maximize, minimize, resize, etc.
- Practice: Read the
xdg-shell.xmlprotocol definition, trace a window creation
Minimum time before starting: 2-3 weeks of focused study if you’ve completed Project 1. Don’t skip the prerequisites–compositor development is unforgiving.
Questions to Guide Your Design
Before writing code, think through these design questions. Your answers will shape your compositor:
1. What window management policy will you implement?
- Floating windows (traditional desktop) or tiling (i3-style)?
- Will you support fullscreen? Maximized? Minimized?
- How do users move and resize windows–mouse only, or keyboard shortcuts?
2. How will you manage focus?
- Click-to-focus or focus-follows-mouse?
- When a window closes, which window gets focus next?
- How do you handle modal dialogs (popups that grab focus)?
3. What’s your rendering strategy?
- Will you use wlroots scene graph (recommended) or render manually?
- How will you handle multiple monitors with different scales?
- Do you need to support screen recording or screenshots?
4. How will you handle configuration?
- Hardcoded keybindings or config file?
- What’s the format–INI, JSON, or a custom DSL?
- Can configuration be reloaded without restarting?
5. What’s your error recovery approach?
- What happens if DRM/KMS fails to initialize?
- How do you handle client crashes–do their windows disappear gracefully?
- Can the compositor recover from GPU errors?
6. What’s your IPC mechanism?
- Will external programs control the compositor (like swaymsg for Sway)?
- Do you need to support a panel/bar–if so, which protocol (layer-shell)?
- How will you communicate workspace/window state to status bars?
Design principle: Start with floating windows and basic focus management. Don’t implement tiling on day one. Get windows appearing, focus working, and input routing correctly first. Add advanced features iteratively.
Thinking Exercise: Before You Code
Exercise: Trace the lifecycle of a window creation by hand, from client to compositor. This builds your mental model of the protocol.
Scenario: A client (terminal application) wants to create a window.
Step-by-step trace:
- Client connects to compositor
- Client: Opens
$XDG_RUNTIME_DIR/wayland-1socket - Compositor: Accepts connection, creates
wl_clientobject - What data structures does the compositor allocate?
- Client: Opens
- Client requests registry
- Client: Sends
wl_display.get_registryrequest - Compositor: Responds with
wl_registry.globalevents for each interface - Which globals will you advertise? (wl_compositor, xdg_wm_base, wlr_layer_shell, wl_seat, wl_output…)
- Client: Sends
- Client binds to compositor interface
- Client: Sends
wl_registry.bindforwl_compositor - Compositor: Creates and returns a
wl_compositorresource bound to this client - What state does this resource hold?
- Client: Sends
- Client creates surface
- Client: Calls
wl_compositor.create_surface - Compositor: Allocates
wlr_surfaceobject, sends back resource ID - What’s in your surface structure? (position, buffers, role, damage region)
- Client: Calls
- Client requests xdg_surface
- Client: Binds to
xdg_wm_base, callsxdg_wm_base.get_xdg_surface(surface) - Compositor: Creates
xdg_surface, associates with thewlr_surface - This gives the surface a “role”–it’s not just pixels, it’s a window
- Client: Binds to
- Client requests toplevel role
- Client: Calls
xdg_surface.get_toplevel - Compositor: Creates
xdg_toplevelobject, this is now a window - What information does toplevel carry? (title, app_id, min/max size)
- Client: Calls
- Compositor sends configure event
- Compositor: Sends
xdg_toplevel.configure(suggested size, states) - Client: Responds with
xdg_surface.ack_configure, renders content - Why must the compositor configure before the client commits?
- Compositor: Sends
- Client commits first buffer
- Client: Creates
wl_buffer, attaches to surface, commits - Compositor: Receives commit, now has pixels to display
- Where do you add this surface to the scene graph?
- Client: Creates
- Compositor renders frame
- Compositor: wlroots renders scene, composites all surfaces to output
- Window appears on screen!
- How often does this happen? (On damage, or every vsync?)
Question for you: At which step does the window become visible? What triggers the first render?
Expected realization: The window appears after step 8 (first commit). But the render only happens when you schedule an output frame. The protocol is asynchronous–events flow both ways.
The Interview Questions They’ll Ask
After completing this project, you should be able to confidently answer these questions in technical interviews:
Basic Level:
- “What’s the difference between X11 and Wayland architectures?”
- Expected answer: X11 separates the X server (rendering) and compositor (window management), requiring network protocol even locally. Wayland combines both into one compositor, clients render themselves and share buffers, compositor just positions and composites them. Simpler, more secure, better performance.
- “What does a Wayland compositor actually do?”
- Expected answer: Three jobs: (1) Implement protocol server–create surfaces, route input events, (2) Manage window policy–focus, positioning, stacking order, (3) Render composited output–blend client buffers onto screen using GPU.
- “How do clients and compositors share pixel data?”
- Expected answer: Shared memory (wl_shm) for software rendering, or DMA-BUF for GPU-rendered buffers. Client allocates buffer, renders to it, attaches to surface, commits. Compositor reads from the same buffer (zero-copy).
Intermediate Level:
- “Explain the xdg-shell configure/ack cycle. Why is it necessary?”
- Expected answer: Compositor sends configure (suggested size/state), client must ack before committing. This ensures client and compositor agree on window geometry before pixels appear. Prevents race conditions where window size changes mid-render.
- “How does input routing work in Wayland? Why is it more secure than X11?”
- Expected answer: Compositor decides which surface receives input based on cursor position and focus. Sends events only to that client. X11 allowed any client to read all input (keyloggers). Wayland isolates input to focused surface.
- “What’s the role of wlroots in compositor development?”
- Expected answer: wlroots abstracts DRM/KMS, libinput, and provides scene graphs, output management, rendering helpers. You implement policy (how windows behave), wlroots provides mechanism (how to draw them). Alternative to writing against libwayland-server directly.
Advanced Level:
- “How would you implement damage tracking to minimize redraws?”
- Expected answer: Track which surfaces changed (client commits), accumulate damage regions (rectangles), only re-render damaged areas. wlroots scene graph does this automatically. Reduces GPU load, saves power.
- “Explain how atomic mode setting works in DRM/KMS.”
- Expected answer: Instead of setting CRTC, connector, mode separately (non-atomic), atomic commit sets all properties in one ioctl. Either all changes apply or none. Prevents tearing, enables smooth transitions between modes.
- “How do you handle multiple outputs (monitors) with different refresh rates?”
- Expected answer: Each output has independent frame callback loop. Present frames at native refresh (60Hz vs 144Hz). Sync to fastest output or handle independently. wlroots manages per-output presentation timing.
Systems Design Level:
- “Design a tiling window manager on top of Wayland. What challenges does Wayland introduce compared to X11?”
- Expected answer: X11 allows WM to intercept and reposition client windows easily. Wayland compositors must implement tiling logic themselves–calculate tile positions, send configure events, handle client size constraints. More complex but also more flexible–no fighting with X11’s window management model.
Hints in Layers: When You Get Stuck
Layer 1: Gentle nudges (try these first)
Compositor won’t start?
- Check if you have permission to access
/dev/dri/card0(your GPU) - Try running as root first to verify it’s a permissions issue (add yourself to
videogroup) - Enable debug output:
WLR_DEBUG=1 ./my-compositor
No cursor visible?
- Did you create a
wlr_cursorand attach it to your output layout? - Are you handling pointer motion events and updating cursor position?
- Did you load a cursor theme with
wlr_xcursor_manager?
Clients can’t connect?
- Check that
WAYLAND_DISPLAYsocket is created (/run/user/1000/wayland-1) - Verify permissions on the socket file
- Look for “failed to bind socket” in compositor logs
Windows appear but input doesn’t work?
- Did you create a
wlr_seatand advertise it as a global? - Are you sending keyboard/pointer enter events when windows gain focus?
- Check that you’re routing events to the surface under the cursor
Layer 2: More specific guidance (if Layer 1 didn’t help)
Backend initialization fails?
// Common mistake: not handling multi-backend properly
struct wlr_backend *backend = wlr_backend_autocreate(display);
if (!backend) {
// Try multi-backend fallback
fprintf(stderr, "Failed to create backend\n");
// Check: Do you have DRM permissions? Is Wayland/X11 available?
}
Scene graph confusion?
- Create a root scene:
wlr_scene_create() - Add an output layout:
wlr_scene_attach_output_layout(scene, output_layout) - For each surface, create:
wlr_scene_surface_create(scene_tree, surface) - wlroots renders the scene automatically–you just position surfaces in the tree
Focus management issues?
// When user clicks a window:
// 1. Set keyboard focus
wlr_seat_keyboard_notify_enter(seat, surface, keyboard->keycodes,
keyboard->num_keycodes, &keyboard->modifiers);
// 2. Set pointer focus (for motion events)
wlr_seat_pointer_notify_enter(seat, surface, sx, sy);
// 3. Update your internal focus state
compositor->focused_surface = surface;
XDG surface configuration?
// When client creates xdg_toplevel, send initial configure:
xdg_toplevel = wlr_xdg_toplevel_from_wlr_surface(surface);
// Suggest a size (or send 0,0 for client to choose)
wlr_xdg_toplevel_set_size(xdg_toplevel, 800, 600);
// Send the configure event
wlr_xdg_surface_schedule_configure(xdg_surface);
Layer 3: Concrete examples (for persistent issues)
Example: Minimal compositor main loop
int main() {
struct wl_display *display = wl_display_create();
struct wlr_backend *backend = wlr_backend_autocreate(display);
struct wlr_renderer *renderer = wlr_renderer_autocreate(backend);
struct wlr_compositor *compositor = wlr_compositor_create(display, renderer);
struct wlr_scene *scene = wlr_scene_create();
struct wlr_xdg_shell *xdg_shell = wlr_xdg_shell_create(display);
// Connect signal: xdg_shell->events.new_surface -> handle_new_xdg_surface
const char *socket = wl_display_add_socket_auto(display);
setenv("WAYLAND_DISPLAY", socket, 1);
if (!wlr_backend_start(backend)) {
fprintf(stderr, "Failed to start backend\n");
return 1;
}
wl_display_run(display); // Event loop
wl_display_destroy(display);
}
Example: Handling new XDG surface
void handle_new_xdg_surface(struct wl_listener *listener, void *data) {
struct wlr_xdg_surface *xdg_surface = data;
if (xdg_surface->role != WLR_XDG_SURFACE_ROLE_TOPLEVEL) {
return; // We only handle toplevels (not popups)
}
// Create your window structure
struct my_window *window = calloc(1, sizeof(*window));
window->xdg_surface = xdg_surface;
window->scene_node = wlr_scene_xdg_surface_create(scene_root, xdg_surface);
// Listen for events
window->map.notify = handle_window_map;
wl_signal_add(&xdg_surface->events.map, &window->map);
window->destroy.notify = handle_window_destroy;
wl_signal_add(&xdg_surface->events.destroy, &window->destroy);
// Send initial configure
wlr_xdg_toplevel_set_size(xdg_surface->toplevel, 0, 0);
}
Example: Moving a window with mouse
void handle_pointer_motion(struct wl_listener *listener, void *data) {
struct wlr_event_pointer_motion *event = data;
if (server->grab_mode == GRAB_MOVE) {
// User is dragging a window
struct my_window *window = server->grabbed_window;
window->x += event->delta_x;
window->y += event->delta_y;
wlr_scene_node_set_position(window->scene_node, window->x, window->y);
} else {
// Normal cursor movement
wlr_cursor_move(server->cursor, event->device, event->delta_x, event->delta_y);
// Update which surface is under cursor
update_surface_under_cursor(server);
}
}
Layer 4: “Just show me working code” (last resort)
Complete minimal compositor example:
- Study
tinywl.cfrom wlroots source (it’s ~600 lines, well-commented) - Line-by-line walkthrough: https://drewdevault.com/2018/02/22/Writing-a-Wayland-compositor-1.html
- Alternative: Sway compositor source (production quality, but more complex)
Debugging strategy:
# Run with all debug output
WLR_DEBUG=1 WLR_RENDERER=pixman ./my-compositor 2>&1 | tee debug.log
# Test with a simple client
WAYLAND_DISPLAY=wayland-1 weston-terminal
# If weston-terminal doesn't appear, check protocol messages
WAYLAND_DEBUG=1 weston-terminal
Books That Will Help
These books provide the background knowledge and techniques you’ll need. Specific chapters are mapped to the concepts you’ll encounter:
| Topic | Book & Chapter | What You’ll Learn | When to Read It |
|---|---|---|---|
| Wayland protocol fundamentals | “The Wayland Book” by Drew DeVault, Ch. 1-7 | Complete understanding of client and server protocol, object model, xdg-shell | Before starting - ESSENTIAL |
| Linux graphics subsystem (DRM/KMS) | “The Linux Programming Interface” by Michael Kerrisk, Ch. 63 + Linux kernel DRM docs | How to control displays, mode setting, page flipping | Week 1 - when initializing backend |
| Event-driven programming | “The Linux Programming Interface” by Michael Kerrisk, Ch. 63.1-63.4 | epoll, file descriptor event loops, signal handling | Week 1 - understanding wl_event_loop |
| OpenGL and GPU rendering | “OpenGL Programming Guide” (Red Book), Ch. 1-4 | EGL context creation, basic rendering, textures | Week 2 - optional but helpful |
| Input device handling | libinput documentation + “Advanced Programming in the UNIX Environment” Ch. 18 | How input events flow from kernel to compositor | Week 1 - when setting up wlr_seat |
| Scene graphs and spatial structures | “Foundations of Game Engine Development, Vol. 1” by Eric Lengyel, Ch. 3 | Transform hierarchies, coordinate spaces, culling | Week 2 - understanding wlr_scene |
| Unix IPC and shared memory | “Advanced Programming in the UNIX Environment” by Stevens & Rago, Ch. 15 | How wl_shm works, buffer sharing, file descriptor passing | Week 2 - when debugging buffer issues |
| Protocol design patterns | “The Wayland Book” Ch. 5 | How to design extensible protocols, versioning, compatibility | Week 3 - if adding custom protocols |
| Systems programming patterns | “Low-Level Programming” by Igor Zhirkov, Ch. 8-11 | Error handling in systems code, resource management | Throughout - writing robust C |
Recommended reading order:
Before coding (1 week):
- “The Wayland Book” Ch. 1-4 - Understand the protocol thoroughly
- Complete Project 1 (Wayland Client) - See the client side
- “The Wayland Book” Ch. 6-7 - Server-side concepts, xdg-shell
During implementation:
- wlroots documentation + tinywl source - Your primary reference
- Linux kernel DRM documentation - When backend issues arise
- libinput documentation - When debugging input routing
After basic compositor works:
- “The Linux Programming Interface” Ch. 63 - Deepen understanding of event loops
- “OpenGL Programming Guide” Ch. 1-4 - If you want to add custom rendering
For production quality:
- Sway compositor source code - Learn from production implementation
- “The Art of Software Security Assessment” Ch. 5 - Secure parsing of client requests
Essential online resources:
- Drew DeVault’s blog series: “Writing a Wayland Compositor” (4-part series)
- freedesktop.org protocol documentation: wayland.xml, xdg-shell.xml
- wlroots API documentation and examples directory
Common Pitfalls & Debugging (Project 2)
Problem 1: “Permission denied” when opening DRM device
Symptoms:
$ ./my_compositor
Failed to open DRM device /dev/dri/card0: Permission denied
Why: Your user doesn’t have permission to access GPU hardware.
Fix:
# Add yourself to the 'video' group:
sudo usermod -a -G video $USER
# Log out and log back in for group change to take effect
# Verify:
groups | grep video
# Check device permissions:
ls -la /dev/dri/card*
# Should show: crw-rw---- 1 root video ...
# If still not working, check if seat0 owns the device:
loginctl seat-status seat0
Problem 2: Compositor starts but no output appears
Symptoms: Compositor runs without errors, but screen stays black.
Why: wlroots backend not initialized properly or no outputs detected.
Fix:
// Add debugging to see what's happening:
wlr_log_init(WLR_DEBUG, NULL); // Enable debug logging
// In new_output callback, add logging:
static void server_new_output(struct wl_listener *listener, void *data) {
struct wlr_output *output = data;
wlr_log(WLR_INFO, "New output: %s", output->name);
// Check if mode setting succeeds:
if (!wl_list_empty(&output->modes)) {
struct wlr_output_mode *mode = wlr_output_preferred_mode(output);
wlr_output_set_mode(output, mode);
wlr_log(WLR_INFO, "Set mode: %dx%d@%dHz",
mode->width, mode->height, mode->refresh);
}
// CRITICAL: Must commit!
if (!wlr_output_commit(output)) {
wlr_log(WLR_ERROR, "Failed to commit output");
}
}
Quick test: Run wlr-randr in another terminal to see if outputs are detected.
Problem 3: “No session found” or “Failed to create session”
Symptoms:
Failed to create session: No session found
Why: Not running from a proper login session or missing logind integration.
Fix:
# Check if you have an active session:
loginctl list-sessions
# Should show your current session
# Run from a TTY (Ctrl+Alt+F3), not from inside another compositor:
# This is WRONG (running from within GNOME/KDE):
$ ./my_compositor
# This is CORRECT (from TTY or via SSH with proper XDG_RUNTIME_DIR):
$ XDG_RUNTIME_DIR=/run/user/$(id -u) ./my_compositor
# For development, you can use wlroots' nested backend:
WLR_BACKENDS=wayland ./my_compositor # Runs inside existing Wayland session
WLR_BACKENDS=x11 ./my_compositor # Runs inside X11
Problem 4: Keyboard/mouse input not working
Symptoms: Compositor displays windows but input events don’t work.
Why: wlr_seat not configured properly or input devices not added.
Fix:
// Ensure you create a seat:
struct wlr_seat *seat = wlr_seat_create(display, "seat0");
// In new_input callback:
static void server_new_input(struct wl_listener *listener, void *data) {
struct wlr_input_device *device = data;
switch (device->type) {
case WLR_INPUT_DEVICE_KEYBOARD:
// MUST set keyboard for seat:
wlr_seat_set_keyboard(server->seat, wlr_keyboard_from_input_device(device));
break;
case WLR_INPUT_DEVICE_POINTER:
// Attach cursor to seat:
wlr_cursor_attach_input_device(server->cursor, device);
break;
}
// CRITICAL: Update seat capabilities!
uint32_t caps = 0;
if (!wl_list_empty(&server->keyboards))
caps |= WL_SEAT_CAPABILITY_KEYBOARD;
if (!wl_list_empty(&server->pointers))
caps |= WL_SEAT_CAPABILITY_POINTER;
wlr_seat_set_capabilities(server->seat, caps);
}
Problem 5: Clients connect but windows don’t appear
Symptoms: Clients successfully bind to compositor but surfaces aren’t visible.
Why: Not handling xdg-shell surface commits or scene graph not configured.
Fix:
// Must listen to surface commit events:
static void xdg_surface_commit(struct wl_listener *listener, void *data) {
struct my_xdg_surface *surface = wl_container_of(listener, surface, commit);
// Check if surface is mapped:
if (wlr_surface_has_buffer(surface->xdg_surface->surface)) {
if (!surface->mapped) {
surface->mapped = true;
wlr_scene_node_set_enabled(&surface->scene_tree->node, true);
}
}
}
// In xdg_toplevel_map handler:
static void xdg_toplevel_map(struct wl_listener *listener, void *data) {
struct my_xdg_surface *surface = wl_container_of(listener, surface, map);
// Add to visible surfaces list
wl_list_insert(&server->surfaces, &surface->link);
// Give keyboard focus:
wlr_seat_keyboard_notify_enter(server->seat,
surface->xdg_surface->surface,
NULL, 0, NULL);
}
Problem 6: Segfault when client closes
Symptoms: Compositor crashes when a client window is closed.
Why: Accessing freed memory or not removing listeners properly.
Fix:
// Proper cleanup in destroy handler:
static void xdg_surface_destroy(struct wl_listener *listener, void *data) {
struct my_xdg_surface *surface = wl_container_of(listener, surface, destroy);
// IMPORTANT: Remove all listeners!
wl_list_remove(&surface->map.link);
wl_list_remove(&surface->unmap.link);
wl_list_remove(&surface->destroy.link);
wl_list_remove(&surface->request_fullscreen.link);
// Remove from list:
wl_list_remove(&surface->link);
// Destroy scene node:
wlr_scene_node_destroy(&surface->scene_tree->node);
// Free the structure:
free(surface);
}
Problem 7: “Invalid framebuffer” or rendering artifacts
Symptoms: Screen shows garbage, tearing, or corrupted graphics.
Why: Output not properly configured or renderer issues.
Fix:
// Ensure output is properly initialized:
static void server_new_output(struct wl_listener *listener, void *data) {
struct wlr_output *wlr_output = data;
// Initialize with default mode:
wlr_output_init_render(wlr_output, server->allocator, server->renderer);
// Get preferred mode:
struct wlr_output_mode *mode = wlr_output_preferred_mode(wlr_output);
if (mode) {
wlr_output_set_mode(wlr_output, mode);
wlr_output_enable(wlr_output, true);
// MUST COMMIT:
if (!wlr_output_commit(wlr_output)) {
wlr_log(WLR_ERROR, "Failed to commit output");
return;
}
}
// Add to scene output layout:
struct wlr_output_layout_output *l_output =
wlr_output_layout_add_auto(server->output_layout, wlr_output);
struct wlr_scene_output *scene_output =
wlr_scene_output_create(server->scene, wlr_output);
wlr_scene_output_layout_add_output(server->scene_layout, l_output, scene_output);
}
Problem 8: High CPU usage or compositor not sleeping
Symptoms: Compositor uses 100% CPU even when idle.
Why: Event loop not properly yielding or frame scheduling issues.
Fix:
// Use proper frame scheduling:
static void output_frame(struct wl_listener *listener, void *data) {
struct my_output *output = wl_container_of(listener, output, frame);
struct wlr_scene *scene = output->server->scene;
// Check if damage occurred:
struct wlr_scene_output *scene_output =
wlr_scene_get_scene_output(scene, output->wlr_output);
// Render only if needed (damage tracking):
if (!wlr_scene_output_commit(scene_output)) {
// No damage, don't render
return;
}
// Schedule next frame callback:
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
wlr_scene_output_send_frame_done(scene_output, &now);
}
Debugging Tools for Compositors
1. wlr_log for Debug Output
// At start of main():
wlr_log_init(WLR_DEBUG, NULL);
// Add throughout code:
wlr_log(WLR_INFO, "Initializing compositor");
wlr_log(WLR_DEBUG, "Output %s: %dx%d", name, width, height);
wlr_log(WLR_ERROR, "Failed to create seat");
2. Running Nested for Testing
# Test inside existing Wayland session:
WLR_BACKENDS=wayland ./my_compositor
# Test inside X11:
WLR_BACKENDS=x11 ./my_compositor
# This lets you develop without switching to TTY!
3. Check DRM/KMS State
# See current DRM state:
sudo cat /sys/kernel/debug/dri/0/state
# List available outputs:
ls /sys/class/drm/
# Monitor DRM events:
sudo dmesg -w | grep drm
4. Use tinywl as Reference
# Clone wlroots:
git clone https://gitlab.freedesktop.org/wlroots/wlroots
cd wlroots/tinywl
# Build and run:
meson build
ninja -C build
./build/tinywl
# Compare your code to tinywl.c -- it's the minimal example
Definition of Done
- Compositor starts from a TTY and creates a Wayland socket (
WAYLAND_DISPLAY=wayland-1) - At least one client (e.g.,
weston-terminal) connects and is visible - Focus and input routing work (keyboard/mouse events go to focused surface)
- Windows can be moved and resized without flicker or protocol errors
- Multi-output layout works (or is explicitly handled as single-output)
- No crashes under basic stress (open/close 10 windows repeatedly)
Project 3: Custom Wayland Protocol Extension
- File: WAYLAND_X11_COMPOSITOR_LEARNING_PROJECTS.md
- Main Programming Language: C
- Alternative Programming Languages: Rust, C++, Zig
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: Level 1: The “Resume Gold”
- Difficulty: Level 3: Advanced (The Engineer)
- Knowledge Area: Graphics, Windowing Systems
- Software or Tool: Wayland, wayland-scanner
- Main Book: The Wayland Book by Drew DeVault
What you’ll build: Define and implement a custom Wayland protocol (XML) that extends functionality–for example, a “screenshot” protocol or a “system tray” protocol–with both client and compositor sides.
Why it teaches protocols: Wayland is extensible through XML protocol definitions that generate C code. Understanding this mechanism demystifies how xdg-shell, wlr-layer-shell, and other protocols work.
Core challenges you’ll face:
- Writing Wayland protocol XML definitions
- Using
wayland-scannerto generate C bindings - Implementing the server-side (compositor) interface
- Implementing the client-side usage
- Understanding versioning and backwards compatibility
Key Concepts:
- Protocol Design: “The Wayland Book” by Drew DeVault (Chapter 5: Protocol Design)
- Code Generation: wayland-scanner documentation
- Existing Protocols: wayland-protocols repository examples
Difficulty: Intermediate-Advanced Time estimate: 1-2 weeks Prerequisites: Project 1 completed
Real world outcome: A custom protocol that you can use to take screenshots from any Wayland client, or implement a system tray, or control your compositor’s behavior programmatically.
Learning milestones:
- Write XML and generate code -> understand protocol-first design
- Implement server-side in your compositor -> understand resource management
- Write a client using your protocol -> see both sides of the API
- Handle edge cases (client disconnect, version mismatch) -> understand robustness
Real World Outcome
When you complete this project, here is exactly what you will see and have created:
$ cd wayland-screenshot-protocol
$ tree
.
├── protocol/
│ └── screenshot.xml # Your custom protocol definition
├── server/
│ ├── screenshot-server.c # Compositor-side implementation
│ └── screenshot-protocol.c # Generated from XML
├── client/
│ ├── screenshot-client.c # Client application
│ └── screenshot-protocol.c # Generated from XML
└── Makefile
$ cat protocol/screenshot.xml
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="screenshot">
<copyright>
Copyright © 2025 Your Name
Permission is hereby granted...
</copyright>
<interface name="screenshot_manager" version="1">
<description summary="screenshot capture interface">
This protocol allows clients to request screenshots of outputs.
It provides a way to capture the current framebuffer content.
</description>
<request name="capture_output">
<description summary="capture an output's content">
Request a screenshot of the specified output.
The compositor will send the image data via a file descriptor.
</description>
<arg name="id" type="new_id" interface="screenshot_buffer"/>
<arg name="output" type="object" interface="wl_output"/>
<arg name="include_cursor" type="int" summary="1 to include cursor, 0 to exclude"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the screenshot manager"/>
</request>
</interface>
<interface name="screenshot_buffer" version="1">
<description summary="screenshot buffer object">
Represents a screenshot capture in progress.
</description>
<event name="ready">
<description summary="screenshot is ready">
Sent when the screenshot has been captured and is ready to read.
</description>
<arg name="fd" type="fd" summary="file descriptor with image data"/>
<arg name="width" type="uint" summary="image width in pixels"/>
<arg name="height" type="uint" summary="image height in pixels"/>
<arg name="stride" type="uint" summary="bytes per row"/>
<arg name="format" type="uint" summary="pixel format (WL_SHM format)"/>
</event>
<event name="failed">
<description summary="screenshot capture failed">
Sent if the screenshot could not be captured.
</description>
<arg name="reason" type="string" summary="human-readable error"/>
</event>
<request name="destroy" type="destructor"/>
</interface>
</protocol>
$ make
wayland-scanner private-code protocol/screenshot.xml server/screenshot-protocol.c
wayland-scanner client-header protocol/screenshot.xml client/screenshot-protocol.h
wayland-scanner server-header protocol/screenshot.xml server/screenshot-protocol.h
gcc -o screenshot-server server/screenshot-server.c server/screenshot-protocol.c \
-lwayland-server -lwlroots
gcc -o screenshot-client client/screenshot-client.c client/screenshot-protocol.c \
-lwayland-client
Build complete!
$ head -n 50 server/screenshot-protocol.c
/* Generated by wayland-scanner 1.21.0 */
/*
* Copyright © 2025 Your Name
* ...
*/
#include <stdlib.h>
#include <stdint.h>
#include "wayland-util.h"
extern const struct wl_interface wl_output_interface;
extern const struct wl_interface screenshot_buffer_interface;
static const struct wl_interface *screenshot_types[] = {
NULL,
&screenshot_buffer_interface,
&wl_output_interface,
NULL,
};
static const struct wl_message screenshot_manager_requests[] = {
{ "capture_output", "noi", screenshot_types + 1 },
{ "destroy", "", screenshot_types + 0 },
};
const struct wl_interface screenshot_manager_interface = {
"screenshot_manager", 1,
2, screenshot_manager_requests,
0, NULL,
};
...
$ ./screenshot-client --output HDMI-1 --include-cursor
Screenshot protocol initialized
Binding to screenshot_manager version 1
Requesting screenshot of output: HDMI-1 (include cursor)
Waiting for compositor response...
Screenshot ready! Received fd=5
Dimensions: 1920x1080
Stride: 7680 bytes/row
Format: ARGB8888
Reading 8,294,400 bytes...
Saving to screenshot-20250126-143022.raw
Done! Convert with: convert -size 1920x1080 -depth 8 rgba:screenshot-20250126-143022.raw output.png
$ convert -size 1920x1080 -depth 8 rgba:screenshot-20250126-143022.raw screenshot.png
$ file screenshot.png
screenshot.png: PNG image data, 1920 x 1080, 8-bit/color RGBA, non-interlaced
What you’ve created:
- XML Protocol Definition: A formal specification that defines:
- Two interfaces:
screenshot_managerandscreenshot_buffer - Requests clients can make:
capture_output - Events the compositor sends:
ready,failed - Data types for each argument (object references, file descriptors, integers)
- Version negotiation capabilities
- Two interfaces:
- Generated C Code: wayland-scanner produces:
screenshot-protocol.c: Message arrays, interface definitions, type tablesscreenshot-protocol.h(client): Function stubs for clients (screenshot_manager_capture_output())screenshot-protocol.h(server): Listener structs for the compositor
- Server Implementation: In your compositor, you’ve added:
- Global binding for
screenshot_manager - Request handlers that capture the framebuffer
- Code to create shared memory, copy pixels, and send the file descriptor
- Resource lifecycle management (cleanup on client disconnect)
- Global binding for
- Client Implementation: A command-line tool that:
- Connects to Wayland display
- Binds to your custom
screenshot_managerglobal - Makes a
capture_outputrequest - Receives the
readyevent with an FD - Reads the pixel data and saves it
How Client and Server Interact (Step-by-Step):
Timeline of a screenshot request:
T0: Client startup
Client: wl_display_connect() -> connects to compositor socket
Client: wl_display_roundtrip() -> get registry events
T1: Registry advertisement
Server: wl_registry.global event -> "screenshot_manager version 1 available"
Client: Receives global, stores name/version
T2: Client binds to protocol
Client: wl_registry_bind(screenshot_manager, version=1)
Client -> Server: [request] bind(name=15, interface="screenshot_manager", version=1, id=new_id@22)
Server: Creates screenshot_manager resource, id=22 for this client
T3: Client requests screenshot
Client: screenshot_manager_capture_output(mgr, new_id, output, include_cursor=1)
Client -> Server: [request] capture_output(id=new_id@23, output=obj@4, include_cursor=1)
T4: Server processes request
Server: Handler receives: screenshot_buffer id=23, output id=4, cursor=1
Server: Looks up wl_output resource from id=4
Server: Allocates shared memory (createMemfd())
Server: Copies framebuffer pixels to shm
Server: Sends ready event
T5: Ready event sent
Server -> Client: [event] screenshot_buffer.ready(fd=<ancillary>, width=1920, height=1080, stride=7680, format=0)
Client: Event handler called with fd, dimensions, format
T6: Client reads data
Client: read(fd, buffer, 8294400) -> gets all pixels
Client: Writes to file
Client: close(fd)
T7: Cleanup
Client: screenshot_buffer_destroy(buf)
Client -> Server: [request] destroy()
Server: Frees screenshot_buffer resource
Generated Code Structure (wayland-scanner output):
// In screenshot-protocol.h (client-side)
struct screenshot_manager;
struct screenshot_buffer;
struct screenshot_buffer_listener {
void (*ready)(void *data, struct screenshot_buffer *buf,
int32_t fd, uint32_t width, uint32_t height,
uint32_t stride, uint32_t format);
void (*failed)(void *data, struct screenshot_buffer *buf,
const char *reason);
};
void screenshot_manager_capture_output(struct screenshot_manager *mgr,
struct screenshot_buffer *buffer,
struct wl_output *output,
int32_t include_cursor);
// In screenshot-protocol.h (server-side)
struct screenshot_manager_interface {
void (*capture_output)(struct wl_client *client,
struct wl_resource *resource,
uint32_t id, // new screenshot_buffer object
struct wl_resource *output,
int32_t include_cursor);
void (*destroy)(struct wl_client *client,
struct wl_resource *resource);
};
void screenshot_buffer_send_ready(struct wl_resource *resource,
int32_t fd, uint32_t width,
uint32_t height, uint32_t stride,
uint32_t format);
The Core Question You’re Answering
“How do you extend a display server protocol in a forward-compatible, type-safe way that allows clients and compositors from different authors to communicate without ambiguity?”
This is the fundamental question that Wayland’s XML-based protocol design solves. You’re learning:
- How protocols are defined independently of any implementation language
- How code generation ensures both sides use exactly the same message format
- How interface versioning allows evolution without breaking old clients
- How object-oriented protocols create a type-safe namespace (interfaces are types)
- How resource lifecycle management prevents use-after-free across process boundaries
By building a custom protocol, you understand that Wayland itself is just one protocol among many–xdg-shell, wlr-layer-shell, linux-dmabuf are all just XML definitions that get compiled into C code the same way yours does.
Concepts You Must Understand First
Stop and research these before coding:
Before starting this project, ensure you deeply understand these foundational concepts. Each one is critical to successfully designing and implementing a Wayland protocol extension.
1. What is a Protocol in the Context of Wayland?
- What does “protocol” mean beyond just “communication”?
- How does a protocol differ from an API or library?
- Why does Wayland use XML to define protocols instead of C headers?
- What is the relationship between the XML definition and the generated C code?
- How does protocol-first design enable interoperability between different implementations?
- Book Reference: “The Wayland Book” by Drew DeVault - Chapter 5: “Protocol Design”
- Book Reference: “Computer Networks, Fifth Edition” by Tanenbaum & Wetherall - Chapter 1.4: “Protocol Hierarchies”
2. Wayland’s Object-Oriented Model
- What is a “proxy” on the client side?
- What is a “resource” on the server side?
- How do object IDs map between client and server?
- What happens when you create a new object in a protocol request?
- Why can’t you just pass raw pointers across process boundaries?
- How does the object model provide type safety in an IPC system?
- Book Reference: “The Wayland Book” by Drew DeVault - Chapter 4.1: “Wayland Objects”
- Book Reference: “Advanced Programming in the UNIX Environment” by Stevens & Rago - Chapter 15: “Interprocess Communication”
3. The Wire Protocol Format
- How are Wayland messages serialized on the wire?
- What does “32-bit alignment” mean for protocol arguments?
- Why does argument order matter in the XML definition?
- What types can be passed in Wayland protocols (int, uint, string, object, new_id, fd, array)?
- How does byte ordering (endianness) affect the wire format?
- Book Reference: “The Wayland Book” by Drew DeVault - Chapter 4.2: “Wire Format”
- Book Reference: “Computer Systems: A Programmer’s Perspective” by Bryant & O’Hallaron - Chapter 2.1: “Information Storage”
4. File Descriptor Passing via Unix Domain Sockets
- What are Unix domain sockets and how do they differ from TCP sockets?
- What is SCM_RIGHTS and how does it enable FD passing?
- Why is passing file descriptors useful for shared resources?
- What happens to a file descriptor when it’s sent to another process?
- How does Wayland use this for buffers and other resources?
- What are the security implications of FD passing?
- Book Reference: “The Linux Programming Interface” by Michael Kerrisk - Section 61.13.3: “Passing File Descriptors”
- Alternative: “Advanced Programming in the UNIX Environment” by Stevens & Rago - Chapter 17.4: “Passing File Descriptors”
- Book Reference: “The Sockets Networking API” by Stevens - Chapter 15: “Unix Domain Protocols”
5. XML Protocol Definition Syntax
- What is the structure of a Wayland protocol XML file?
- What are interfaces, requests, events, and arguments?
- How do you specify the type and interface of arguments?
- What is the purpose of the “since” attribute?
- How do you mark a request or event as a destructor?
- What are protocol summaries and descriptions used for?
- Book Reference: Wayland Protocol Specification (online) - https://wayland.freedesktop.org/docs/html/
6. Shared Memory (wl_shm) and memfd
- What is
memfd_create()and how does it differ from regular file I/O? - How does
mmap()allow zero-copy data sharing? - What is a “shared memory pool” in Wayland terms?
- When should you use shared memory vs file descriptors for data transfer?
- How do you prevent race conditions when multiple processes access shared memory?
- Book Reference: “The Linux Programming Interface” by Michael Kerrisk - Chapter 54: “POSIX Shared Memory”
- Alternative: “Advanced Programming in the UNIX Environment” by Stevens & Rago - Chapter 15.9: “POSIX Shared Memory”
- Book Reference: “The Linux Programming Interface” by Michael Kerrisk - Chapter 49: “Memory Mappings”
7. Wayland Globals and Registry Pattern
- What is the
wl_registryand why is it fundamental? - How do compositors advertise capabilities?
- What does “binding” to a global interface mean?
- How do you handle version negotiation when binding?
- Why is the registry pattern better than hardcoded assumptions?
- Book Reference: “The Wayland Book” by Drew DeVault - Chapter 5.1: “Globals”
8. Resource Lifecycle Management
- When should resources be created vs destroyed?
- What happens when a client disconnects unexpectedly?
- How do you prevent use-after-free in server code?
- What is a destructor request vs automatic cleanup?
- Why must you track resource ownership?
- How do you handle circular references in object graphs?
- Book Reference: “The Wayland Book” by Drew DeVault - Chapter 5.3: “Resource Management”
- Book Reference: “Effective C” by Robert Seacord - Chapter 2: “Objects, Functions, and Types”
9. Code Generation with wayland-scanner
- What is
wayland-scannerand what does it do? - What’s the difference between “client-header”, “server-header”, and “private-code”?
- How do the generated listener structs work?
- What code do you write vs what the scanner generates?
- Why is code generation preferred over manual protocol implementation?
- Book Reference: “The Wayland Book” by Drew DeVault - Chapter 5: “Protocol Design” (wayland-scanner section)
10. Versioning and Backwards Compatibility
- How do protocol versions work in Wayland?
- What does the “since” attribute do?
- How do you add new features without breaking old clients?
- When should you increment the interface version?
- What happens if a client binds to a newer version than the server supports?
- How do you deprecate old protocol features?
- Book Reference: “The Wayland Book” by Drew DeVault - Chapter 5.2: “Versioning”
Questions to Guide Your Design
Before implementing, think through these:
As you implement the protocol, ask yourself these critical questions that will shape your protocol design:
1. Protocol Purpose and Scope
- What does my protocol do? Define the use case clearly (screenshot, clipboard, notification, etc.)
- Why does this need to be a protocol extension? Could this be handled by existing protocols?
- What problem am I solving? Be specific about the gap you’re filling
2. Object Architecture
- What are the objects? Is there a manager (factory) and per-operation objects (like screenshot_buffer)?
- How do objects relate to each other? Parent-child? Peer-to-peer? Reference relationships?
- Should my protocol create new object types or extend existing ones?
3. Communication Patterns
- Who initiates? Does the client request (pull model) or does the server notify (push model)?
- Is the communication synchronous or asynchronous?
- Do I need bi-directional communication or just one way?
4. Data Transfer Strategy
- What data is transferred? Small values (send inline), large data (use file descriptor or wl_shm pool)?
- How much data are we talking about? Bytes? Kilobytes? Megabytes?
- Do I need streaming or one-shot transfers?
- Should the data be read-only or read-write?
5. Error Handling
- What can go wrong? List all possible failure scenarios
- How do I communicate errors? Event with error string? Protocol error?
- Should errors be recoverable or fatal?
- What happens if the client ignores an error?
6. Versioning Strategy
- How do I handle versions? If I add a new request later, old clients must still work (version field in interface)
- What’s my v1 minimum viable protocol?
- What features might I add in v2, v3?
- How do I detect if a client/server supports a feature I need?
7. Resource Lifecycle
- When are objects destroyed? Explicit destroy request? Implicit when parent dies? Compositor-triggered?
- Who owns the objects? Client? Server? Shared ownership?
- What happens on unexpected disconnect?
- Do I need cleanup handlers?
8. Protocol Composition
- How does this compose with existing protocols? Do I reference wl_output or wl_surface? How?
- What existing Wayland objects do I need? (wl_surface, wl_output, wl_seat, etc.)
- Am I creating a standalone protocol or extending an existing one?
Thinking Exercise
Before you start coding, think through this protocol design scenario:
You want to create a “clipboard” protocol. Requirements:
- Clients can offer data (text, images)
- Other clients can request that data
- Only one client has the clipboard at a time
- Data transfer should handle large files efficiently
Design questions:
- What interfaces do you need?
clipboard_manager(singleton)?clipboard_offer(per-offer object)?clipboard_source(client’s data provider)?
- What requests and events?
- Request:
set_selection(source)– client claims clipboard - Event:
selection_changed(offer)– notify clients of new clipboard - Request:
receive(mime_type, fd)– client wants to read data - Event:
send(mime_type, fd)– source must write data to fd
- Request:
- How do you handle MIME types?
- Offer must advertise:
offer.mime_typeevent (sent multiple times) - Client can query available types before deciding to receive
- Offer must advertise:
- What’s the object lifecycle?
- Manager: global, never destroyed
- Offer: lives until next
set_selection(compositor destroys old) - Source: lives until client destroys it or sets new selection
Now design the XML:
- Try writing just the interface and request names (don’t worry about perfect syntax)
- Think about which direction each message goes (request vs event)
- Consider what happens when client disconnects mid-transfer
After this exercise, look up wl_data_device_manager in the Wayland protocol spec–you’ll see your design is close to the real thing!
The Interview Questions They’ll Ask
After completing this project, you should be able to answer these interview questions:
- “How does Wayland protocol extensibility work?”
- Protocols are defined in XML with versioned interfaces
- wayland-scanner generates C code from XML
- Compositors advertise which protocols/versions they support via wl_registry
- Clients bind to the version they need (or lower if unsupported)
- “What is the difference between a Wayland request and an event?”
- Request: client -> server (e.g., “capture screenshot”)
- Event: server -> client (e.g., “screenshot ready”)
- Both are messages on the wire, just opposite directions
- “How are file descriptors passed in Wayland protocols?”
- Unix domain sockets support ancillary data (SCM_RIGHTS)
- wayland-scanner marks arguments as type=”fd”
- libwayland-client/server automatically sends FDs out-of-band
- Used for shared memory, DMA-BUF, pipes, etc.
- “What is a Wayland global and how do you bind to one?”
- Global: a named interface the compositor offers (advertised via wl_registry.global)
- Binding: client calls wl_registry.bind(name, interface, version) -> creates proxy
- Example: bind to “wl_compositor” version 4 -> get wl_compositor proxy
- “How do you handle protocol versioning?”
- Each interface has a version number in XML
- New versions add requests/events at the end (never change existing)
- Client binds to min(client_version, server_version)
- Check version before using new features:
if (version >= 2) { ... }
- “What happens when a Wayland client disconnects while holding resources?”
- Compositor receives HUP on socket
- Compositor iterates all resources owned by that wl_client
- Calls destroy hooks for each resource (compositor-defined cleanup)
- Example: screenshot_buffer cleanup cancels in-progress capture
- “How would you debug a protocol mismatch between client and server?”
- Set WAYLAND_DEBUG=1 -> logs all messages in human-readable format
- Check protocol XML version matches on both sides
- Verify wayland-scanner generated code was recompiled
- Use
wl_display_get_error()to detect protocol errors
Hints in Layers
If you get stuck, reveal hints one at a time. Try each level before moving to the next.
Hint 1 - Start with XML Skeleton
Copy an existing simple protocol XML (like wl_shell or a custom one from wlroots examples). Replace interface names and descriptions. Get wayland-scanner to accept it before adding complex arguments.
Hint 2 - Use wayland-scanner for Both Sides
Generate three files:
wayland-scanner private-code protocol.xml protocol.c # Both sides
wayland-scanner client-header protocol.xml client-protocol.h
wayland-scanner server-header protocol.xml server-protocol.h
Include the right header on each side.
Hint 3 - Server-Side Resource Creation
When a client makes a request that creates a new object (like capture_output), the server must:
struct wl_resource *screenshot_buffer_resource =
wl_resource_create(client, &screenshot_buffer_interface,
wl_resource_get_version(manager_resource), id);
wl_resource_set_implementation(screenshot_buffer_resource, &screenshot_buffer_impl,
user_data, resource_destroy_handler);
The id comes from the request arguments (new_id).
Hint 4 - Sending File Descriptors
On the server, create a memfd or pipe:
int fd = memfd_create("screenshot", MFD_CLOEXEC);
ftruncate(fd, total_bytes);
void *data = mmap(NULL, total_bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// ... copy framebuffer to data ...
munmap(data, total_bytes);
screenshot_buffer_send_ready(resource, fd, width, height, stride, format);
close(fd); // Client gets its own FD via SCM_RIGHTS
On the client, receive it in the event listener:
static void handle_ready(void *data, struct screenshot_buffer *buf,
int32_t fd, uint32_t width, ...) {
uint32_t total = height * stride;
void *pixels = mmap(NULL, total, PROT_READ, MAP_PRIVATE, fd, 0);
// ... use pixels ...
munmap(pixels, total);
close(fd);
}
Hint 5 - Version Handling Example
In XML:
<interface name="screenshot_manager" version="2">
<request name="capture_output" since="1">...</request>
<request name="capture_region" since="2">...</request> <!-- New in v2 -->
</interface>
In code:
uint32_t version = wl_resource_get_version(resource);
if (version >= 2) {
// OK to use capture_region
}
Books That Will Help
This section maps specific topics you’ll encounter to relevant book chapters for deeper understanding.
| Topic | Book | Chapter/Section |
|---|---|---|
| Protocol Design Fundamentals | “The Wayland Book” by Drew DeVault | Chapter 5: “Protocol Design” |
| Wayland Protocol Basics | “The Wayland Book” by Drew DeVault | Chapters 1-4: “Introduction through Surfaces” |
| Wayland Object Model | “The Wayland Book” by Drew DeVault | Chapter 4.1: “Wayland Objects” |
| Wire Protocol Format | “The Wayland Book” by Drew DeVault | Chapter 4.2: “Wire Format” |
| wayland-scanner Usage | “The Wayland Book” by Drew DeVault | Chapter 5: “Protocol Design” (scanner section) |
| Compositor-Side Implementation | “The Wayland Book” by Drew DeVault | Chapter 8: “Building a Compositor” |
| File Descriptor Passing | “The Linux Programming Interface” by Michael Kerrisk | Section 61.13.3: “Passing File Descriptors” |
| Unix Domain Sockets | “The Sockets Networking API” by W. Richard Stevens | Chapter 15: “Unix Domain Protocols” |
| Alternative: FD Passing | “Advanced Programming in the UNIX Environment” by Stevens & Rago | Chapter 17.4: “Passing File Descriptors” |
| Shared Memory (memfd) | “The Linux Programming Interface” by Michael Kerrisk | Chapter 54: “POSIX Shared Memory” |
| Memory Mapping (mmap) | “The Linux Programming Interface” by Michael Kerrisk | Chapter 49: “Memory Mappings” |
| Alternative: Shared Memory | “Advanced Programming in the UNIX Environment” by Stevens & Rago | Chapter 15.9: “POSIX Shared Memory” |
| Interprocess Communication | “Advanced Programming in the UNIX Environment” by Stevens & Rago | Chapter 15: “Interprocess Communication” |
| Protocol Versioning | “The Wayland Book” by Drew DeVault | Chapter 5.2: “Versioning” |
| API Design Principles | “Design and Build Great Web APIs” by Mike Amundsen | Chapters 2-3: API design patterns |
| Resource Management in C | “Effective C” by Robert C. Seacord | Chapter 2: “Objects, Functions, and Types” |
| Data Serialization | “Computer Systems: A Programmer’s Perspective” by Bryant & O’Hallaron | Chapter 2.1: “Information Storage” |
| Network Protocol Concepts | “Computer Networks, Fifth Edition” by Tanenbaum & Wetherall | Chapter 1.4: “Protocol Hierarchies” |
| Code Generation Patterns | “Language Implementation Patterns” by Terence Parr | Part II: “Analyzing Languages” (parser generation concepts) |
| System Programming in C | “The Linux Programming Interface” by Michael Kerrisk | Chapters 1-5: “Fundamental Concepts & File I/O” |
| Error Handling in Systems Code | “Effective C” by Robert Seacord | Chapter 6: “Dynamically Allocated Memory” |
Recommended Reading Order
Before you start coding (Week 1):
- “The Wayland Book” - Chapters 1-5 (complete protocol understanding)
- “The Linux Programming Interface” - Chapter 54 (shared memory basics)
- “The Linux Programming Interface” - Section 61.13.3 (file descriptor passing)
While implementing the XML (Week 1):
- “The Wayland Book” - Chapter 5 (protocol design and scanner usage)
- Study existing protocol XMLs in
/usr/share/wayland-protocols/
While implementing server side (Week 2):
- “The Wayland Book” - Chapter 8 (compositor implementation)
- “Effective C” - Chapter 2 (proper resource management)
- “The Linux Programming Interface” - Chapter 49 (mmap for data sharing)
For debugging and refinement:
- Wayland Protocol Specification (online reference)
- wlroots protocol examples (real-world implementations)
Online Resources and Documentation
- Wayland Protocol Specification: https://wayland.freedesktop.org/docs/html/
- Authoritative reference for core Wayland protocol
- Detailed wire format specifications
- wayland-protocols repository: https://gitlab.freedesktop.org/wayland/wayland-protocols
/stable- Stable protocol extensions (xdg-shell, etc.)/staging- Protocols under development/unstable- Experimental protocols
- wlroots protocol examples: https://gitlab.freedesktop.org/wlroots/wlr-protocols
- Real-world protocol extensions
- Layer-shell, screencopy, and more
- Wayland Protocol XML Files on Your System:
- Core:
/usr/share/wayland/wayland.xml - Extensions:
/usr/share/wayland-protocols/stable/and/staging/
- Core:
- wayland-scanner Documentation:
man wayland-scanneron your system- Shows usage and output options
Common Pitfalls & Debugging (Project 3)
Problem 1: wayland-scanner fails to generate code
Symptoms:
$ wayland-scanner client-header < my-protocol.xml > my-protocol.h
error: XML parse error
Why: XML syntax error in your protocol definition.
Fix:
# Validate XML syntax:
xmllint --noout my-protocol.xml
# Common mistakes:
# 1. Missing interface version:
<interface name="my_custom" version="1"> <!-- Must have version! -->
# 2. Misspelled types:
<arg name="surface" type="object" interface="wl_surface"/> <!-- "object", not "Object" -->
# 3. Unclosed tags:
<request name="do_something">
<arg name="value" type="uint"/>
</request> <!-- Must close request tag! -->
# Compare with working examples:
cat /usr/share/wayland/wayland.xml
Problem 2: Client can’t bind to custom global
Symptoms: Client connects but custom interface doesn’t appear in registry.
Why: Compositor not advertising the global or version mismatch.
Fix (Compositor side):
// Must create and advertise the global:
struct wl_global *my_global = wl_global_create(
display,
&my_custom_interface, // From generated code
1, // Version
server_data, // User data
my_custom_bind // Bind callback
);
// Bind callback must create resource:
static void my_custom_bind(struct wl_client *client, void *data,
uint32_t version, uint32_t id) {
struct wl_resource *resource = wl_resource_create(
client,
&my_custom_interface,
version,
id
);
wl_resource_set_implementation(resource, &my_custom_impl, data, NULL);
}
Debugging:
# Check if global is advertised:
WAYLAND_DEBUG=1 my_client 2>&1 | grep my_custom
# Should see:
# wl_registry@2.global(XX, "my_custom", 1)
Problem 3: Protocol error when calling custom request
Symptoms:
Protocol error: my_custom@5: invalid arguments for do_something
Why: Argument types don’t match protocol definition.
Fix:
// Protocol XML says:
<request name="do_something">
<arg name="surface" type="object" interface="wl_surface"/>
<arg name="value" type="uint"/>
</request>
// Client code MUST match:
my_custom_do_something(
my_custom_proxy,
surface, // wl_surface*, not wl_surface**
42 // uint32_t, not int
);
// Common mistakes:
// ❌ WRONG: Passing int instead of uint:
my_custom_do_something(proxy, surface, -1);
// ❌ WRONG: Passing pointer to pointer:
my_custom_do_something(proxy, &surface, 42);
// ✅ CORRECT:
my_custom_do_something(proxy, surface, 42);
Problem 4: Compositor crashes when receiving request
Symptoms: Compositor segfaults when client calls custom protocol method.
Why: NULL implementation or missing NULL check.
Fix:
// Server-side implementation struct:
static const struct my_custom_interface my_custom_impl = {
.do_something = my_custom_handle_do_something, // Can't be NULL!
.another_request = my_custom_handle_another,
};
// Implementation function:
static void my_custom_handle_do_something(struct wl_client *client,
struct wl_resource *resource,
struct wl_resource *surface_resource,
uint32_t value) {
// MUST validate object resources!
if (!surface_resource) {
wl_resource_post_error(resource,
MY_CUSTOM_ERROR_INVALID_SURFACE,
"Surface is null");
return;
}
struct wl_surface *surface = wl_resource_get_user_data(surface_resource);
if (!surface) {
wl_resource_post_error(resource,
MY_CUSTOM_ERROR_INVALID_SURFACE,
"Invalid surface");
return;
}
// Now safe to use surface and value
// ...
}
Problem 5: Events not received by client
Symptoms: Compositor sends events but client listener never fires.
Why: Client didn’t register listener or listener struct is wrong.
Fix (Client side):
// Must add listener AFTER binding:
struct my_custom *custom = wl_registry_bind(registry, name,
&my_custom_interface, 1);
// Listener struct must match protocol:
static const struct my_custom_listener custom_listener = {
.state_changed = handle_state_changed, // Must match event names!
};
// Add listener:
my_custom_add_listener(custom, &custom_listener, user_data);
// Event handler signature must match:
static void handle_state_changed(void *data,
struct my_custom *custom,
uint32_t state) {
// data = user_data passed to add_listener
printf("State changed to %u\n", state);
}
Problem 6: “Symbol not found” linker errors
Symptoms:
undefined reference to `my_custom_do_something'
Why: Forgot to compile generated code or link it.
Fix:
# Generate BOTH header and glue code:
wayland-scanner client-header < my-protocol.xml > my-protocol-client.h
wayland-scanner private-code < my-protocol.xml > my-protocol-client.c
# Compile the generated .c file:
gcc -c my-protocol-client.c -o my-protocol-client.o
# Link it with your client:
gcc my_client.c my-protocol-client.o -o my_client \
$(pkg-config --cflags --libs wayland-client)
# Or in Makefile:
my_client: my_client.c my-protocol-client.o
$(CC) $^ -o $@ $(LIBS)
Problem 7: Version mismatch between client and server
Symptoms: Client or compositor crashes with version-related error.
Why: Client requesting features from newer version than server supports.
Fix:
// In registry handler, check supported version:
static void registry_global(void *data, struct wl_registry *registry,
uint32_t name, const char *interface,
uint32_t version) {
if (strcmp(interface, my_custom_interface.name) == 0) {
// Server advertises version 2, but we only need version 1:
uint32_t bind_version = MIN(version, 1);
struct my_custom *custom = wl_registry_bind(
registry, name, &my_custom_interface, bind_version);
// Now we're guaranteed compatibility
}
}
// In protocol XML, mark version requirements:
<request name="new_feature" since="2"> <!-- Only in version 2+ -->
<arg name="value" type="uint"/>
</request>
// Client code:
if (custom_version >= 2) {
my_custom_new_feature(custom, 42); // Safe
} else {
// Fallback for version 1
}
Debugging Custom Protocols
1. Use WAYLAND_DEBUG
# See all protocol traffic:
WAYLAND_DEBUG=1 ./my_client 2>&1 | grep my_custom
# Look for:
# - Registry announcement: wl_registry@2.global(XX, "my_custom", 1)
# - Bind request: wl_registry@2.bind(XX, "my_custom", 1, new id my_custom@YY)
# - Requests: my_custom@YY.do_something(surface@5, 42)
# - Events: my_custom@YY.state_changed(1)
2. Validate Protocol XML
# Check against schema (if available):
xmllint --noout --schema wayland.xsd my-protocol.xml
# Compare with working examples:
diff -u /usr/share/wayland/wayland.xml my-protocol.xml
3. Test with Simple Client/Server
// Minimal test client:
// Just bind and call one request, see if it reaches server
// Minimal test server:
// Just advertise global and log when bind happens
wlr_log(WLR_INFO, "Client bound to my_custom");
Definition of Done
- Protocol XML validates (
xmllint --noout protocol.xml) wayland-scannergenerates client/server headers and private code- Compositor advertises the global and clients can bind to it
- Client request triggers a server action and returns an event with data
- Version checks prevent calling unsupported requests
- Client and server cleanly handle disconnects and resource destruction
Project 4: Wayland Panel/Bar (Layer Shell)
- File: WAYLAND_X11_COMPOSITOR_LEARNING_PROJECTS.md
- Programming Language: C
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Linux Desktop / Wayland Protocol
- Software or Tool: Layer Shell Protocol
- Main Book: “The Wayland Book” by Drew DeVault
What you’ll build: A status bar/panel using the wlr-layer-shell protocol–something like waybar or yambar but simpler, showing time, workspaces, and system info.
Why it teaches shell concepts: Layer shell lets you create surfaces that exist outside the normal window hierarchy (panels, overlays, backgrounds). This teaches you how desktop environments are built on top of Wayland.
Core challenges you’ll face:
- Using
zwlr_layer_shell_v1protocol - Anchoring to screen edges and reserving exclusive zones
- Rendering text and graphics (Cairo or custom)
- Responding to compositor events (workspace changes, etc.)
- Handling multiple outputs (monitors)
Key Concepts:
- Layer Shell Protocol: wlr-protocols documentation
- Cairo Graphics: cairographics.org tutorial
- Output Management: “The Wayland Book” (Outputs chapter)
- IPC Mechanisms: Understanding how bars communicate with compositors (e.g., Sway IPC)
Difficulty: Intermediate Time estimate: 2-3 weeks Prerequisites: Project 1 completed, some graphics/drawing knowledge helpful
Real world outcome: A functional panel at the top (or bottom) of your screen showing the current time, battery status, and workspace indicator that updates in real-time.
Learning milestones:
- Create a layer surface anchored to top edge -> understand layer shell semantics
- Draw text and shapes with Cairo -> understand rendering on Wayland
- Update content without flickering (damage tracking) -> understand efficient redraw
- Handle multi-monitor setups -> understand output enumeration
Real World Outcome
When you complete this project, you’ll have a persistent status bar that runs on your Wayland desktop, displaying real-time information and responding to compositor events.
Compilation and Execution
# Install dependencies (Debian/Ubuntu)
$ sudo apt install libwayland-dev wayland-protocols \
libcairo2-dev libpango1.0-dev \
libwlroots-dev
# Compile the protocol extension first
$ wayland-scanner client-header \
/usr/share/wlr-protocols/unstable/wlr-layer-shell-unstable-v1.xml \
wlr-layer-shell-protocol.h
$ wayland-scanner private-code \
/usr/share/wlr-protocols/unstable/wlr-layer-shell-unstable-v1.xml \
wlr-layer-shell-protocol.c
# Compile your panel
$ gcc -o wayland_panel \
main.c \
widgets.c \
wlr-layer-shell-protocol.c \
-lwayland-client \
-lcairo \
-lpango-1.0 \
-lpangocairo-1.0 \
-lrt
# Run it on your Wayland session
$ ./wayland_panel --position top --height 30
What You’ll See
When you execute the program on a Wayland compositor (Sway, Wayfire, Hyprland, or any wlroots-based compositor):
- A bar appears at the top (or bottom) of your screen
- The bar has:
- Fixed position: Anchored to the screen edge, doesn’t move with windows
- Exclusive zone: Normal windows can’t overlap it–they maximize under the bar
- Per-monitor instances: On multi-monitor setups, each screen gets its own bar instance
- Crisp rendering: Text and icons are rendered using Cairo with proper anti-aliasing
- The bar displays widgets (left to right):
- Workspace indicator: Shows active workspace, clickable to switch
- Window title: Shows currently focused window’s title
- System tray area: Icons for background applications (optional advanced feature)
- System info:
- CPU usage:
CPU: 34% - Memory usage:
RAM: 8.2G / 16G - Network status:
v 2.3 MB/s ^ 128 KB/s
- CPU usage:
- Clock:
Fri Jan 26 14:30:22(updates every second) - Battery:
🔋 85% (charging)or🔌 100%
Visual Representation
┌────────────────────────────────────────────────────────────────────────┐
│ [1] [2] [3●] [4] │ Terminal - ~/dev/project │ CPU:34% RAM:8.2G/16G │ v2.3MB/s ^128KB/s │ 🔋85% │ 14:30:22 │
└────────────────────────────────────────────────────────────────────────┘
│ │
│ ┌──────────────────────────┐ │
│ │ Terminal │ │
│ │ user@host:~/dev$ make │ <- Normal windows start below the bar │
│ │ │ │
│ └──────────────────────────┘ │
│ │
Runtime Behavior
Terminal output while running:
$ ./wayland_panel --position top --height 30
[Wayland Panel] Starting...
[Wayland Panel] Connecting to Wayland display...
[Wayland Panel] Connected successfully
[Registry] Global interface: wl_compositor v6
[Registry] Global interface: wl_shm v1
[Registry] Global interface: zwlr_layer_shell_v1 v4
[Registry] Global interface: wl_output v4 (name=HDMI-A-1)
[Registry] Global interface: wl_output v4 (name=eDP-1)
[Registry] Global interface: wl_seat v9
[Layer Shell] Binding to zwlr_layer_shell_v1 version 4
[Output] Found output: HDMI-A-1 (1920x1080 @ 60Hz)
[Output] Found output: eDP-1 (1920x1200 @ 144Hz)
[Panel] Creating panel for output: HDMI-A-1
[Surface] Creating wl_surface...
[Layer] Creating layer surface (TOP layer, TOP anchor, exclusive zone: 30px)
[Layer] Configure event received:
- Width: 1920, Height: 30
- Serial: 4567
[Layer] Acknowledging configure (serial: 4567)
[Cairo] Creating Cairo surface (1920x30, ARGB32)
[Buffer] Creating shared memory buffer (230,400 bytes for 1920x30 @ 32bpp)
[Render] Drawing widgets...
-> Workspaces: 4 workspaces, active=3
-> Window title: "Terminal - ~/dev/project"
-> CPU widget: 34%
-> Memory widget: 8.2G / 16G
-> Network widget: v2.3MB/s ^128KB/s
-> Battery widget: 85% (charging)
-> Clock widget: 14:30:22
[Surface] Committing surface with damage region (0,0,1920,30)
[Panel] Creating panel for output: eDP-1
[Surface] Creating wl_surface...
[Layer] Creating layer surface (TOP layer, TOP anchor, exclusive zone: 30px)
[Layer] Configure event received:
- Width: 1920, Height: 30
- Serial: 4568
[Layer] Acknowledging configure (serial: 4568)
[Render] Drawing widgets for eDP-1...
[Display] Entering event loop...
[Timer] Clock update (every 1 second)
[Render] Redrawing clock widget only (damage: 1720,0,200,30)
[Timer] System info update (every 2 seconds)
[Render] Redrawing CPU/RAM/Network widgets (damage: 600,0,1000,30)
[IPC] Connected to Sway IPC socket
[IPC] Subscribed to workspace events
[IPC] Event received: workspace::focus (workspace 4)
[Render] Updating workspace indicator (damage: 0,0,300,30)
[Mouse] Button press at (150, 15) - workspace 2 clicked
[IPC] Sending command: workspace 2
[IPC] Workspace switched successfully
[Render] Updating workspace indicator (damage: 0,0,300,30)
[Signal] Caught SIGINT - cleaning up...
[Cleanup] Destroying layer surfaces
[Cleanup] Destroying Cairo contexts
[Cleanup] Unmapping buffers
[Cleanup] Disconnecting from Wayland
[Exit] Goodbye!
Interactive Features
What the user can do:
- Workspace switching:
- Click on workspace numbers
[1] [2] [3]to switch - Active workspace has a dot:
[3●] - Workspace with windows has different styling
- Click on workspace numbers
- Window title updates:
- Title changes when you focus different windows
- Shows app name + document/path when available
- Real-time monitoring:
- CPU percentage updates every 2 seconds
- Memory usage updates with actual values from
/proc/meminfo - Network speed reads from
/sys/class/net/*/statistics/
- Multi-monitor awareness:
- Each monitor gets its own independent panel instance
- Shows correct workspace for that monitor
- Handles monitor hotplug (connect/disconnect)
- Battery status (laptops only):
- Reads from
/sys/class/power_supply/BAT0/ - Updates every 10 seconds
- Shows charging/discharging state
- Reads from
How It Differs from Regular Windows
Unlike a normal Wayland window (xdg-toplevel), your panel uses layer shell:
// NOT this (regular window):
struct xdg_surface *xdg_surface = xdg_wm_base_get_xdg_surface(wm_base, surface);
struct xdg_toplevel *toplevel = xdg_surface_get_toplevel(xdg_surface);
// But THIS (layer surface):
struct zwlr_layer_surface_v1 *layer_surface =
zwlr_layer_shell_v1_get_layer_surface(
layer_shell,
surface,
output,
ZWLR_LAYER_SHELL_V1_LAYER_TOP, // Layer: background, bottom, top, or overlay
"panel" // Namespace
);
// Configure anchoring and exclusive zone
zwlr_layer_surface_v1_set_anchor(layer_surface,
ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); // Stretch across top edge
zwlr_layer_surface_v1_set_exclusive_zone(layer_surface, 30); // Reserve 30px
zwlr_layer_surface_v1_set_size(layer_surface, 0, 30); // Width=0 means "full width"
wl_surface_commit(surface); // Apply configuration
Key differences:
| Feature | Regular Window (xdg-toplevel) | Layer Surface (wlr-layer-shell) |
|---|---|---|
| Position | User/WM controlled | Anchored to screen edges |
| Stacking | Among other windows | Fixed layer (background/bottom/top/overlay) |
| Exclusive zone | No | Yes–reserves screen real estate |
| Focus behavior | Can be focused | Optional keyboard interactivity |
| Per-output | No (exists in compositor space) | Yes (bound to specific wl_output) |
| Use case | Applications | Panels, backgrounds, notifications, lock screens |
What You’ve Built
A desktop shell component that:
- Uses Wayland layer shell protocol to exist “outside” the normal window stack
- Renders efficiently using Cairo for 2D graphics and Pango for text
- Implements damage tracking–only redraws changed regions
- Communicates with the compositor via IPC (for workspace info)
- Handles multiple monitors independently
- Responds to system events (window focus, workspace changes)
This is how waybar, yambar, eww, and other Wayland panels work under the hood. You’ve built the foundation of a desktop environment’s UI layer.
The Core Question You’re Answering
“How do desktop environments create UI elements that exist ‘outside’ the normal window hierarchy–panels that other windows can’t overlap, backgrounds that sit behind everything, and overlays that appear above all windows?”
Before Wayland’s layer shell, this was compositor-specific. X11 had _NET_WM_WINDOW_TYPE_DOCK hints, but enforcement was inconsistent. Wayland’s layer shell protocol solves this by defining explicit layers:
- Background layer: Wallpapers, desktop backgrounds
- Bottom layer: Desktop icons, widgets that sit below windows
- Top layer: Panels, status bars that sit above windows
- Overlay layer: Notifications, lock screens, on-screen displays
By building a panel, you answer these deeper questions:
- Why does layer shell exist?
- Normal windows can’t reliably reserve screen space–a maximized window shouldn’t cover the panel
- The compositor needs to know about panels to adjust window geometry (
xdg_toplevel.configureaccounts for exclusive zones) - Security: overlay layer is compositor-controlled for lock screens
- How does per-output binding work?
- Unlike xdg-toplevel (exists in compositor’s global space), layer surfaces bind to a specific
wl_output - This enables different panels on different monitors, or a wallpaper per screen
- Compositor handles output hotplug–your surface gets destroyed when monitor disconnects
- Unlike xdg-toplevel (exists in compositor’s global space), layer surfaces bind to a specific
- What is an exclusive zone?
- It’s a way to say “reserve this many pixels from the edge”
- Compositor adjusts
usable_areafor that output - Regular windows maximize within
usable_area, not full screen - Multiple exclusive zones stack (top panel + bottom panel both reserve space)
- How does the compositor prioritize?
- Layers have fixed stacking: background < bottom < windows < top < overlay
- Within a layer, surfaces stack by creation order
keyboard_interactivitycontrols whether the surface can steal focus
The insight: Layer shell is a spatial contract between panel and compositor. The protocol makes explicit what was implicit in X11, allowing for cleaner, more predictable desktop environments.
Concepts You Must Understand First
Before implementing a Wayland panel, you need to understand these foundational concepts:
1. Layer Shell Protocol Semantics
- What is
zwlr_layer_shell_v1and why is it “unstable”? - What are the four layers (background, bottom, top, overlay) and when to use each?
- How does anchoring work? (combination of edges:
TOP | LEFT | RIGHT= stretch across top) - What is an exclusive zone and how does it affect window geometry?
- How does
keyboard_interactivitycontrol focus behavior? - Book Reference: wlr-protocols repository documentation + “The Wayland Book” Chapter 10 (if available)
2. Wayland Output Management
- What is a
wl_outputobject? - How do you enumerate available outputs (monitors)?
- How do you get output properties (resolution, refresh rate, name)?
- What happens when an output is disconnected (hotplug)?
- How do you create per-output surfaces?
- Book Reference: “The Wayland Book” Chapter 7.4 (Outputs)
3. Cairo Graphics Library
- What is Cairo and why use it for 2D rendering?
- How do you create a Cairo surface from a Wayland buffer?
- What is a Cairo context and how does drawing work?
- How do you draw shapes (rectangles, rounded corners, lines)?
- How do you set colors (RGB/RGBA)?
- Book Reference: “Cairo Graphics Tutorial” (online) or “The Cairo Graphics Cookbook” (if available)
4. Pango Text Rendering
- Why use Pango instead of raw Cairo text APIs?
- How do you create a Pango layout?
- How do you set font (family, size, weight)?
- How do you measure text dimensions (for layout calculations)?
- How do you integrate Pango with Cairo (
pango_cairo_show_layout)? - Book Reference: Pango documentation (https://docs.gtk.org/Pango/)
5. Damage Tracking and Efficient Redraw
- What is “damage” in the context of Wayland?
- Why is it important to minimize redrawn areas?
- How do you use
wl_surface_damageorwl_surface_damage_buffer? - What’s the difference between full redraw and partial redraw?
- How do you calculate damage rectangles for widget updates?
- Book Reference: “The Wayland Book” Chapter 7.1 (Buffers and damage)
6. IPC with Wayland Compositors
- How does a panel get workspace information from the compositor?
- What is Sway’s IPC protocol? (JSON over Unix socket)
- How do you subscribe to events (workspace changes, window focus)?
- What about other compositors (Wayfire, Hyprland)? (different IPC protocols)
- How do you send commands back to the compositor?
- Book Reference: Sway IPC documentation (https://github.com/swaywm/sway/blob/master/sway-ipc.7.scdoc)
7. Timer-Based Updates
- How do you implement periodic updates (clock every second)?
- What timer mechanism to use? (
timerfd_create,timer_settime) - How do you integrate timers with the Wayland event loop?
- How do you avoid blocking the event loop?
- Book Reference: “The Linux Programming Interface” Chapter 23 (Timers)
8. Reading System Information
- How do you read CPU usage from
/proc/stat? - How do you get memory info from
/proc/meminfo? - How do you read battery status from
/sys/class/power_supply/? - How do you monitor network statistics from
/sys/class/net/? - How do you parse these efficiently without blocking?
- Book Reference: “The Linux Programming Interface” Chapter 12 (System and Process Information)
Questions to Guide Your Design
Before implementing, think through these design questions:
1. Panel Layout and Position
- Will the panel be at the top or bottom? (user-configurable?)
- What’s the panel height? (fixed 30px, or scale with DPI?)
- Should it span all monitors or just the primary?
- How do you handle ultrawide monitors? (different layout?)
2. Widget Architecture
- How will widgets be structured? (modular system or monolithic?)
- What’s the lifecycle of a widget? (init, update, render, destroy)
- How does a widget signal it needs a redraw?
- How do you lay out widgets? (fixed positions, or flex-like layout?)
3. Rendering Strategy
- Do you redraw the entire panel every update, or use damage tracking?
- How do you handle overlapping damage regions?
- Should you double-buffer to avoid tearing?
- How often should visual updates happen? (clock: 1Hz, CPU: 0.5Hz, network: 2Hz)
4. IPC and Compositor Integration
- Which compositor(s) will you support? (Sway only, or multi-compositor?)
- How do you gracefully degrade if IPC is unavailable?
- Should workspace switching be compositor-specific or generic?
- How do you handle IPC errors or disconnections?
5. Multi-Monitor Handling
- Do you create one panel instance per output, or one panel managing multiple outputs?
- How do you handle outputs appearing/disappearing at runtime?
- Should each monitor’s panel be independent, or synchronized?
- How do you identify outputs? (by name, or by index?)
6. Interaction and Input
- Should widgets be clickable? (workspace buttons, volume control)
- Do you need mouse hover effects?
- Should the panel accept keyboard input? (probably not–
keyboard_interactivity = NONE) - How do you handle touch input on touchscreens?
7. Performance and Efficiency
- How do you minimize CPU usage when idle? (only redraw on change)
- Should system info polling be adaptive? (slower when on battery?)
- How do you handle high-DPI displays? (scale factors)
- What’s the memory footprint of your rendering buffers?
Thinking Exercise
Design the Widget Layout on Paper
Before coding, sketch out the panel layout with pixel dimensions:
Panel height: 30px
Total width: 1920px (full screen)
┌──────────────────────────────────────────────────────────────────────────┐
│ Padding │ Workspaces │ Gap │ Window Title │ Flex Space │ Sys Info │ Clock │ Padding │
│ 5px │ 200px │ 10px│ 400px (flex) │ │ 300px │ 150px │ 5px │
└──────────────────────────────────────────────────────────────────────────┘
Questions to answer:
- Fixed vs. flexible widgets: Which widgets have fixed width (clock: 150px) vs. flex (window title: expands to fill)?
- Text overflow: What happens if the window title is longer than available space? (ellipsis? scroll?)
- Alignment: Should the clock be right-aligned, workspaces left-aligned?
- Spacing: How much padding between widgets? (visual hierarchy)
Trace a Clock Update
Walk through what happens when the clock ticks (every second):
T=0: Timer fires (1 second elapsed)
v
T=1: Timer event added to Wayland event loop
v
T=2: Event loop dispatches timer callback
v
T=3: Clock widget's update() function called
v
T=4: Get current time: time_t now = time(NULL); struct tm *tm = localtime(&now);
v
T=5: Format string: sprintf(buffer, "%H:%M:%S", tm->tm_hour, tm->tm_min, tm->tm_sec);
v
T=6: Has the string changed from last render? -> Yes
v
T=7: Mark clock widget region as damaged: damage(1720, 0, 200, 30)
v
T=8: Trigger panel redraw
v
T=9: Render only the damaged region:
- Get Cairo context
- Clip to damage region
- Clear background (draw rectangle with background color)
- Draw text with Pango
v
T=10: Attach buffer, set damage, commit surface
v
T=11: Compositor displays updated pixels
Questions:
- Can you skip the redraw if the time string hasn’t changed? (edge case: update at 14:30:00.999, then 14:30:01.001 - same second)
- Should you update at exactly 1-second intervals, or sync to wall-clock seconds? (so it shows 14:30:00.000 not 14:30:00.123)
- What if the render takes longer than expected (compositor is slow)? Does the timer queue up?
Mental Model: Exclusive Zone Geometry
Draw how the compositor calculates usable area with multiple exclusive zones:
Monitor resolution: 1920x1080
With no panels:
┌──────────────────────────────────────┐
│ │
│ Usable area = full screen │
│ (0, 0, 1920, 1080) │
│ │
└──────────────────────────────────────┘
With top panel (exclusive zone = 30px):
┌──────────────────────────────────────┐ <- Top panel (layer shell, exclusive=30)
├──────────────────────────────────────┤
│ │
│ Usable area for windows │
│ (0, 30, 1920, 1050) │ <- Windows maximize here
│ │
└──────────────────────────────────────┘
With top panel (30px) + bottom panel (40px):
┌──────────────────────────────────────┐ <- Top panel (exclusive=30)
├──────────────────────────────────────┤
│ │
│ Usable area for windows │
│ (0, 30, 920, 1010) │
│ │
├──────────────────────────────────────┤
└──────────────────────────────────────┘ <- Bottom panel (exclusive=40)
Question: What happens if you set exclusive zone to 0? (Panel can be overlapped by windows–useful for auto-hide panels)
The Interview Questions They’ll Ask
Conceptual Questions
- “What is the Wayland layer shell protocol, and why was it created?”
- Expected answer: Layer shell (
zwlr_layer_shell_v1) is a protocol extension that allows surfaces to exist in fixed layers outside the normal window stack. Created for panels, backgrounds, notifications, and lock screens. Before this, compositors had custom APIs–layer shell standardizes it.
- Expected answer: Layer shell (
- “Explain the difference between an xdg-toplevel window and a layer shell surface.”
- Expected answer: xdg-toplevel is a normal application window–user/WM controls position, can be moved/resized, stacks with other windows. Layer surface is bound to an output, anchored to edges, exists in a fixed layer (background/bottom/top/overlay), can reserve screen space via exclusive zone.
- “What is an exclusive zone, and how does it affect window management?”
- Expected answer: Exclusive zone tells the compositor to reserve N pixels from the anchored edge. Compositor reduces the “usable area” for normal windows by that amount. Windows maximize within the usable area, not the full screen. Allows panels to never be covered.
- “What are the four layers in the layer shell protocol?”
- Expected answer: Background (wallpapers), Bottom (desktop widgets below windows), Top (panels above windows), Overlay (lock screens, critical notifications). Each layer has fixed stacking relative to others; within a layer, stacking is by creation order.
Implementation Questions
- “How do you create a layer surface in Wayland?”
- Expected answer:
struct zwlr_layer_surface_v1 *layer_surface = zwlr_layer_shell_v1_get_layer_surface( layer_shell, surface, output, ZWLR_LAYER_SHELL_V1_LAYER_TOP, "namespace"); zwlr_layer_surface_v1_set_anchor(layer_surface, anchors); zwlr_layer_surface_v1_set_exclusive_zone(layer_surface, height); zwlr_layer_surface_v1_set_size(layer_surface, width, height); wl_surface_commit(surface);
- Expected answer:
- “How do you efficiently update a clock widget without redrawing the entire panel?”
- Expected answer: Calculate the bounding rectangle of the clock text. When the time changes, mark only that region as damaged using
wl_surface_damage_buffer(x, y, width, height). Redraw only the damaged region by setting a Cairo clip. Compositor only re-composites changed pixels.
- Expected answer: Calculate the bounding rectangle of the clock text. When the time changes, mark only that region as damaged using
- “How do you handle multi-monitor setups in a panel application?”
- Expected answer: Enumerate all
wl_outputglobals from the registry. For each output, create a separate layer surface bound to that output. Each surface has its own buffer, rendering context, and state. Handle thewl_registry.global_removeevent to destroy surfaces when monitors disconnect.
- Expected answer: Enumerate all
- “How would you integrate Cairo rendering with Wayland shared memory buffers?”
- Expected answer:
void *shm_data = /* mmap shared memory */; cairo_surface_t *cairo_surface = cairo_image_surface_create_for_data( shm_data, CAIRO_FORMAT_ARGB32, width, height, stride); cairo_t *cr = cairo_create(cairo_surface); /* draw with cairo_* functions */ cairo_destroy(cr); cairo_surface_flush(cairo_surface); /* Now shm_data contains pixels, attach to wl_surface */
- Expected answer:
Debugging Questions
- “Your panel appears but doesn’t reserve screen space–windows overlap it. What could be wrong?”
-
Expected answer: (a) Forgot to set exclusive zone, (b) set exclusive zone to 0, (c) anchoring is wrong (e.g., only TOP without LEFT RIGHT means narrow strip), (d) compositor doesn’t support exclusive zones (non-wlroots compositor).
-
- “The panel text looks blurry on your high-DPI monitor. What’s the issue?”
- Expected answer: Not accounting for scale factor. Wayland outputs have a scale factor (1, 2, etc.). Must read
wl_output.scaleevent, create buffer atwidth * scale, height * scale, tell Cairo about the scale (cairo_surface_set_device_scale), and usewl_surface.set_buffer_scale.
- Expected answer: Not accounting for scale factor. Wayland outputs have a scale factor (1, 2, etc.). Must read
Hints in Layers
Hint 1: Setting Up Layer Shell
If you’re stuck on creating the layer surface:
After binding to zwlr_layer_shell_v1 global, create a regular wl_surface first, then promote it:
struct wl_surface *surface = wl_compositor_create_surface(compositor);
struct zwlr_layer_surface_v1 *layer_surface =
zwlr_layer_shell_v1_get_layer_surface(
layer_shell,
surface,
output, // Specific wl_output, or NULL for compositor to choose
ZWLR_LAYER_SHELL_V1_LAYER_TOP,
"panel" // Namespace (arbitrary string for compositor debugging)
);
// Listen for configure event
zwlr_layer_surface_v1_add_listener(layer_surface, &layer_surface_listener, &state);
Hint 2: Anchoring and Sizing
If the panel appears in the wrong position or size:
Anchoring is a bitmask of edges. To create a top bar that spans the full width:
zwlr_layer_surface_v1_set_anchor(layer_surface,
ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT);
// Size: width=0 means "stretch to fill anchored edges", height is fixed
zwlr_layer_surface_v1_set_size(layer_surface, 0, 30);
// Reserve 30 pixels from the top edge
zwlr_layer_surface_v1_set_exclusive_zone(layer_surface, 30);
// Commit to apply
wl_surface_commit(surface);
// Wait for configure event before attaching buffer!
Anchor combinations:
TOP | LEFT | RIGHT= top bar, full widthBOTTOM | LEFT | RIGHT= bottom bar, full widthLEFT | TOP | BOTTOM= left sidebar, full heightTOP | LEFT= top-left corner (size determines dimensions)
Hint 3: Handling Configure Event
If the panel doesn’t appear:
Layer surfaces, like xdg-toplevels, require acknowledgment of the configure event:
static void layer_surface_configure(void *data,
struct zwlr_layer_surface_v1 *layer_surface,
uint32_t serial, uint32_t width, uint32_t height) {
struct panel_state *state = data;
// Store the size the compositor wants us to be
state->width = width;
state->height = height;
// Acknowledge
zwlr_layer_surface_v1_ack_configure(layer_surface, serial);
// Now safe to create buffer and attach
if (!state->configured) {
state->configured = true;
create_buffer(state, width, height);
render_panel(state);
wl_surface_attach(state->surface, state->buffer, 0, 0);
wl_surface_commit(state->surface);
}
}
static const struct zwlr_layer_surface_v1_listener layer_surface_listener = {
.configure = layer_surface_configure,
.closed = layer_surface_closed,
};
Hint 4: Cairo Rendering Setup
If you’re stuck on rendering:
Create a Cairo surface from your shared memory buffer:
// Assume you have shm buffer as in Project 1
void *shm_data = /* your mmap'd shared memory */;
int width = 1920;
int height = 30;
int stride = width * 4; // 4 bytes per pixel (ARGB32)
// Create Cairo surface wrapping the shared memory
cairo_surface_t *cairo_surface = cairo_image_surface_create_for_data(
(unsigned char *)shm_data,
CAIRO_FORMAT_ARGB32,
width,
height,
stride
);
// Create drawing context
cairo_t *cr = cairo_create(cairo_surface);
// Draw background
cairo_set_source_rgb(cr, 0.1, 0.1, 0.1); // Dark gray
cairo_paint(cr);
// Draw a rectangle
cairo_set_source_rgb(cr, 1.0, 0.0, 0.0); // Red
cairo_rectangle(cr, 10, 5, 100, 20);
cairo_fill(cr);
// Cleanup (but keep cairo_surface alive while buffer is in use!)
cairo_destroy(cr);
cairo_surface_flush(cairo_surface); // Ensure all drawing is written to shm_data
// Now attach the buffer to wl_surface as usual
Hint 5: Drawing Text with Pango
If text rendering is unclear:
#include <pango/pangocairo.h>
// Inside your render function with cairo_t *cr already created:
PangoLayout *layout = pango_cairo_create_layout(cr);
pango_layout_set_text(layout, "Hello, Wayland!", -1); // -1 = null-terminated
PangoFontDescription *desc = pango_font_description_from_string("Sans Bold 12");
pango_layout_set_font_description(layout, desc);
pango_font_description_free(desc);
// Position and draw
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); // White text
cairo_move_to(cr, 10, 8); // x=10, y=8
pango_cairo_show_layout(cr, layout);
g_object_unref(layout);
Hint 6: Damage Tracking for Partial Updates
If full redraws are inefficient:
Track which widgets changed and accumulate damage:
struct damage_region {
int x, y, width, height;
};
// When clock updates:
struct damage_region clock_damage = {
.x = 1700, // Clock widget starts at x=1700
.y = 0,
.width = 200,
.height = 30
};
// Redraw only the damaged region:
cairo_t *cr = cairo_create(cairo_surface);
cairo_rectangle(cr, clock_damage.x, clock_damage.y, clock_damage.width, clock_damage.height);
cairo_clip(cr); // Restrict drawing to this rectangle
// Clear the damaged area
cairo_set_source_rgb(cr, 0.1, 0.1, 0.1); // Background color
cairo_paint(cr);
// Redraw the clock widget
render_clock_widget(cr, 1700, 0);
cairo_destroy(cr);
cairo_surface_flush(cairo_surface);
// Tell Wayland only this region changed
wl_surface_damage_buffer(surface, clock_damage.x, clock_damage.y,
clock_damage.width, clock_damage.height);
wl_surface_commit(surface);
Hint 7: Timer Integration with Event Loop
If periodic updates don’t work:
Use timerfd integrated with the Wayland event loop:
#include <sys/timerfd.h>
#include <unistd.h>
int timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
// Set timer to fire every 1 second
struct itimerspec spec = {
.it_interval = {1, 0}, // Repeat every 1 second
.it_value = {1, 0} // First expiration after 1 second
};
timerfd_settime(timerfd, 0, &spec, NULL);
// Add to Wayland event loop (using wl_event_loop_add_fd if you have access,
// or manually with poll/epoll):
// Simple approach: check timer in main loop
while (running) {
struct pollfd fds[2];
fds[0].fd = wl_display_get_fd(display);
fds[0].events = POLLIN;
fds[1].fd = timerfd;
fds[1].events = POLLIN;
poll(fds, 2, -1);
if (fds[0].revents & POLLIN) {
wl_display_dispatch(display);
}
if (fds[1].revents & POLLIN) {
uint64_t expirations;
read(timerfd, &expirations, sizeof(expirations)); // Clear the timer
update_clock_widget(); // Trigger redraw
}
}
Hint 8: Reading System Information
If system stats aren’t working:
CPU usage (simplified):
#include <stdio.h>
double get_cpu_usage() {
static unsigned long long prev_total = 0, prev_idle = 0;
unsigned long long user, nice, system, idle, iowait, irq, softirq;
FILE *f = fopen("/proc/stat", "r");
fscanf(f, "cpu %llu %llu %llu %llu %llu %llu %llu",
&user, &nice, &system, &idle, &iowait, &irq, &softirq);
fclose(f);
unsigned long long total = user + nice + system + idle + iowait + irq + softirq;
unsigned long long diff_total = total - prev_total;
unsigned long long diff_idle = idle - prev_idle;
prev_total = total;
prev_idle = idle;
if (diff_total == 0) return 0.0;
return (double)(diff_total - diff_idle) / diff_total * 100.0;
}
Memory usage:
void get_memory_usage(long *total_mb, long *used_mb) {
FILE *f = fopen("/proc/meminfo", "r");
long mem_total = 0, mem_free = 0, buffers = 0, cached = 0;
char line[256];
while (fgets(line, sizeof(line), f)) {
if (sscanf(line, "MemTotal: %ld kB", &mem_total)) continue;
if (sscanf(line, "MemFree: %ld kB", &mem_free)) continue;
if (sscanf(line, "Buffers: %ld kB", &buffers)) continue;
if (sscanf(line, "Cached: %ld kB", &cached)) continue;
}
fclose(f);
*total_mb = mem_total / 1024;
*used_mb = (mem_total - mem_free - buffers - cached) / 1024;
}
Books That Will Help
| Topic | Book | Chapter/Section | When to Read It |
|---|---|---|---|
| Layer Shell Protocol | wlr-protocols documentation | wlr-layer-shell-unstable-v1.xml | Before starting - ESSENTIAL |
| Wayland Output Management | “The Wayland Book” by Drew DeVault | Chapter 7.4 (Outputs) | Week 1 - output enumeration |
| Cairo Graphics Basics | Cairo Graphics Tutorial (online) | Sections 1-5 (Drawing basics) | Week 1 - before rendering |
| Cairo Advanced Drawing | Cairo Graphics Tutorial | Sections 6-10 (Paths, transformations) | Week 2 - for widget styling |
| Pango Text Layout | Pango documentation | Layout and Rendering sections | Week 1 - text rendering |
| Wayland Buffer Management | “The Wayland Book” | Chapter 7.1 (Buffers) | Week 1 - creating buffers |
| Damage Tracking | “The Wayland Book” | Chapter 7.2 (Frame callbacks and damage) | Week 2 - optimization |
| Shared Memory (review) | “The Linux Programming Interface” by Kerrisk | Chapter 54 | Reference - buffer creation |
| Timers and Periodic Tasks | “The Linux Programming Interface” | Chapter 23 (Timers and Sleeping) | Week 1 - clock updates |
| Reading /proc and /sys | “The Linux Programming Interface” | Chapter 12 (System and Process Info) | Week 2 - system widgets |
| Sway IPC Protocol | Sway IPC manual | sway-ipc.7.scdoc (GitHub) | Week 2 - workspace integration |
| Unix Socket Programming | “The Linux Programming Interface” | Chapter 57 (Unix Domain Sockets) | Week 2 - IPC connection |
| poll/epoll for Event Loops | “The Linux Programming Interface” | Chapter 63.2-63.4 | Week 1 - integrating timers |
Recommended Reading Order
Before coding (Week 0):
- wlr-protocols documentation:
wlr-layer-shell-unstable-v1.xml- Read the protocol XML and comments - Cairo Graphics Tutorial - Sections 1-5 - Learn basic drawing
- “The Wayland Book” Chapter 7.4 - Understand outputs
During implementation (Week 1-2):
- Pango documentation - As you implement text widgets
- “The Linux Programming Interface” Chapter 23 - When adding timers
- “The Linux Programming Interface” Chapter 12 - When reading system stats
- “The Wayland Book” Chapter 7.2 - When optimizing with damage tracking
For advanced features (Week 3+):
- Sway IPC documentation - For workspace integration
- “The Linux Programming Interface” Chapter 57 - For IPC connection
- Cairo advanced topics - For custom widget effects
Essential Online Resources
- wlr-layer-shell protocol XML:
/usr/share/wlr-protocols/unstable/wlr-layer-shell-unstable-v1.xml - Cairo Graphics Tutorial: https://www.cairographics.org/tutorial/
- Pango Documentation: https://docs.gtk.org/Pango/
- Sway IPC Protocol: https://github.com/swaywm/sway/blob/master/sway-ipc.7.scdoc
- Example panels:
- waybar source: https://github.com/Alexays/Waybar
- yambar source: https://codeberg.org/dnkl/yambar
- lavalauncher (simpler): https://git.sr.ht/~leon_plickat/lavalauncher
Debugging Resources
- WAYLAND_DEBUG=1: See all protocol messages
- WLR_DAMAGE=rerender: Force full redraws (debug damage tracking)
- valgrind: Check for memory leaks in Cairo/Pango usage
- gdb with Cairo: Set breakpoints in your render functions
Layer Shell Positioning & Exclusive Zones
Understanding how layer shell differs from regular windows and how to position panels:
WLR-LAYER-SHELL PROTOCOL: POSITIONING SYSTEM
════════════════════════════════════════════════════════════════════════════
LAYER HIERARCHY (Z-Order from bottom to top)
──────────────────────────────────────────────────────────────────────────
┌────────────────────────────────────────────────────────────────────────┐
│ SCREEN (OUTPUT) │
│ │
│ Layer 1: BACKGROUND │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Wallpaper, desktop background images │ │
│ │ - Always below all other content │ │
│ │ - Typically fullscreen │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ Layer 2: BOTTOM │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Special desktop widgets, docks │ │
│ │ - Above background, below normal windows │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ Layer 3: TOP │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Status bars, panels, notifications │ │
│ │ - Above normal windows │ │
│ │ - This is where you put your panel! │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ Layer 4: OVERLAY │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Screen lockers, critical system overlays │ │
│ │ - Above everything else │ │
│ │ - Blocks all input to lower layers │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ Note: Regular application windows (xdg_toplevel) render │
│ BETWEEN layers BOTTOM and TOP │
└────────────────────────────────────────────────────────────────────────┘
ANCHORING TO SCREEN EDGES
══════════════════════════════════════════════════════════════════════════
Anchors determine which edges your surface sticks to:
┌─────────────────────────────────────────────────────────────────────┐
│ TOP │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Panel anchored to TOP edge │ │
│ │ Anchors: TOP | LEFT | RIGHT (stretches horizontally) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ LEFT RIGHT │
│ ┌──┐ ┌──┐ │
│ │ │ Regular windows appear in this space │ │ │
│ │S │ (not anchored) │ S│ │
│ │i │ │ i│ │
│ │d │ │ d│ │
│ │e │ │ e│ │
│ │ │ │ │ │
│ │b │ │ b│ │
│ │a │ Sidebars: Anchored to LEFT or RIGHT │ a│ │
│ │r │ Anchors: LEFT | TOP | BOTTOM (vertical stretch) │ r│ │
│ └──┘ └──┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Bottom bar anchored to BOTTOM edge │ │
│ │ Anchors: BOTTOM | LEFT | RIGHT │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ BOTTOM │
└─────────────────────────────────────────────────────────────────────┘
Anchor Combinations:
─────────────────────
- TOP only -> Centered horizontally at top
- TOP | LEFT -> Top-left corner
- TOP | LEFT | RIGHT -> Full-width bar at top (most common for panels)
- LEFT | TOP | BOTTOM -> Full-height sidebar on left
- (none) -> Centered on screen
EXCLUSIVE ZONES: WINDOW MANAGEMENT
══════════════════════════════════════════════════════════════════════════
Exclusive zones tell the compositor to avoid placing windows in that area.
Example 1: TOP PANEL WITH EXCLUSIVE ZONE
─────────────────────────────────────────
┌──────────────────────────────────────────────────────────────────┐
│ ██████████████████████████████████████████████████████████████ │ <- Panel
│ ██ Time: 14:32 │ CPU: 23% │ Battery: 78% ██████████████ │ Height: 30px
│ ██████████████████████████████████████████████████████████████ │ Exclusive: 30
├──────────────────────────────────────────────────────────────────┤ <- Reserved edge
│ │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Terminal │ │ Browser │ │
│ │ │ │ │ │
│ │ │ │ Windows are │ │
│ │ │ │ placed below │ │
│ │ │ │ the panel │ │
│ │ │ │ │ │
│ │ │ │ Maximized │ │
│ │ │ │ windows don't │ │
│ └─────────────────┘ │ cover panel │ │
│ └──────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
zwlr_layer_surface_v1_set_exclusive_zone(surface, 30);
-> Compositor reserves 30 pixels at the top
-> Regular windows start at Y=30, not Y=0
Example 2: NOTIFICATION (NO EXCLUSIVE ZONE)
────────────────────────────────────────────
┌──────────────────────────────────────────────────────────────────┐
│ ██████████████████████████████████████████████████████████████ │ <- Panel (Z above)
│ ██ Time: 14:32 │ CPU: 23% │ Battery: 78% ██████████████ │
│ ██████████████████████████████████████████████████████████████ │
│ │
│ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Terminal │ │ Browser │ │
│ │ │ │ │ │
│ │ │ │ Windows fill │ │
│ │ │ │ entire screen │ │
│ │ │ │ │ │
│ │ ┌────────────────────────┐ │ │
│ │ │ 🔔 Notification │ <- Exclusive: 0 │ │
│ │ │ Email received │ Floats above │ │
│ └─────│────────────────────────┘────────────────┘ │
│ └─ Anchored: TOP | RIGHT │
│ Size: 300x80 │
│ Margin: (10px from top/right) │
└──────────────────────────────────────────────────────────────────┘
zwlr_layer_surface_v1_set_exclusive_zone(surface, 0);
-> Compositor does NOT reserve space
-> Notification overlays on top of windows
-> Windows are not resized/moved
Example 3: EXCLUSIVE ZONE = -1 (IGNORE OTHER ZONES)
────────────────────────────────────────────────────
Used for fullscreen overlays (lock screens):
┌──────────────────────────────────────────────────────────────────┐
│ ████████████████████████████████████████████████████████████████ │
│ ██ 🔒 SCREEN LOCKED ██ │
│ ██ ██ │
│ ██ Enter Password: ____________ ██ │
│ ██ ██ │
│ ██ This surface IGNORES the panel's exclusive zone ██ │
│ ██ It stretches all the way to screen edges ██ │
│ ██ ██ │
│ ████████████████████████████████████████████████████████████████ │
│ (Panel is still there at Z-order TOP, but lock screen is │ │
│ at Z-order OVERLAY, so it covers everything) │ │
└──────────────────────────────────────────────────────────────────┘
zwlr_layer_surface_v1_set_exclusive_zone(surface, -1);
-> Surface extends to absolute screen edges
-> Ignores all other exclusive zones
MARGINS: FINE-TUNED POSITIONING
══════════════════════════════════════════════════════════════════════════
Margins add spacing between anchored edge and surface:
┌──────────────────────────────────────────────────────────────────┐
│ │
│ <-> Margin Top = 10px │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Panel with margins │ │
│ └──────────────────────────────────────────────────────────┘ │
│ <-> Margin Left/Right = 20px │
│ │
│ Panel does not touch screen edges │
│ Useful for rounded corners, aesthetic spacing │
└──────────────────────────────────────────────────────────────────┘
zwlr_layer_surface_v1_set_margin(surface,
top_margin=10,
right_margin=20,
bottom_margin=0,
left_margin=20);
SIZING: EXPLICIT vs COMPOSITOR-DECIDED
══════════════════════════════════════════════════════════════════════════
Option 1: Client specifies size
────────────────────────────────
zwlr_layer_surface_v1_set_size(surface, width=0, height=30);
- width=0: Stretch to fill (when anchored to both left and right)
- height=30: Fixed height of 30 pixels
Result: Panel is full-width, 30 pixels tall
Option 2: Compositor decides size
──────────────────────────────────
zwlr_layer_surface_v1_set_size(surface, width=0, height=0);
- Both 0: Compositor suggests size based on output dimensions
- Client receives configure event with suggested size
COMPLETE EXAMPLE: Creating a Top Panel
══════════════════════════════════════════════════════════════════════════
// 1. Get layer shell manager (from registry)
struct zwlr_layer_shell_v1 *layer_shell;
// ... bind to "zwlr_layer_shell_v1" global ...
// 2. Create base Wayland surface
struct wl_surface *surface = wl_compositor_create_surface(compositor);
// 3. Get layer surface from regular surface
struct zwlr_layer_surface_v1 *layer_surface =
zwlr_layer_shell_v1_get_layer_surface(
layer_shell,
surface,
output, // Which monitor (or NULL for compositor choice)
ZWLR_LAYER_SHELL_V1_LAYER_TOP, // Layer: TOP
"panel" // Namespace (for compositor identification)
);
// 4. Configure positioning
zwlr_layer_surface_v1_set_size(layer_surface,
0, // width=0: full width
30); // height=30: 30 pixels tall
zwlr_layer_surface_v1_set_anchor(layer_surface,
ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); // Stick to top, stretch horizontally
zwlr_layer_surface_v1_set_exclusive_zone(layer_surface, 30);
// Reserve 30 pixels at top for this panel
zwlr_layer_surface_v1_set_margin(layer_surface, 0, 0, 0, 0);
// No margins (flush with edges)
// 5. Commit to apply configuration
wl_surface_commit(surface);
// 6. Wait for configure event
// Compositor sends: zwlr_layer_surface_v1.configure(serial, width, height)
// 7. Acknowledge configure
zwlr_layer_surface_v1_ack_configure(layer_surface, serial);
// 8. Attach buffer and commit (same as regular Wayland client)
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_commit(surface);
// Panel appears on screen!
MULTI-MONITOR HANDLING
══════════════════════════════════════════════════════════════════════════
Each monitor (wl_output) can have its own layer surfaces:
Monitor 1 (Primary) Monitor 2 (Secondary)
┌──────────────────────────┐ ┌──────────────────────────┐
│ ████████████████████████ │ │ ████████████████████████ │
│ ██ Panel (output=1) ██ │ │ ██ Panel (output=2) ██ │
│ ████████████████████████ │ │ ████████████████████████ │
│ │ │ │
│ Windows on monitor 1 │ │ Windows on monitor 2 │
│ │ │ │
└──────────────────────────┘ └──────────────────────────┘
// Create one layer surface per output
for each output in outputs:
layer_surface = zwlr_layer_shell_v1_get_layer_surface(...,
output, // This monitor
...);
Result: Panel appears on all monitors independently
COMMON USE CASES:
══════════════════════════════════════════════════════════════════════════
1. Status Bar (Waybar, Yambar)
- Layer: TOP
- Anchor: TOP | LEFT | RIGHT
- Exclusive: bar_height (e.g., 30)
- Result: Full-width bar at top, windows below it
2. Dock (Plank-style)
- Layer: TOP or BOTTOM
- Anchor: BOTTOM | LEFT | RIGHT
- Exclusive: dock_height
- Result: Dock at bottom, windows avoid it
3. Wallpaper
- Layer: BACKGROUND
- Anchor: TOP | BOTTOM | LEFT | RIGHT (all)
- Size: output width/height
- Exclusive: 0 (don't affect windows)
- Result: Fullscreen background image
4. Notification
- Layer: TOP
- Anchor: TOP | RIGHT
- Exclusive: 0 (overlay on windows)
- Margin: (10, 10, 0, 0)
- Size: (300, 80)
- Result: Small popup in top-right corner
5. On-Screen Display (Volume/Brightness)
- Layer: OVERLAY
- Anchor: (none, centered)
- Exclusive: 0
- Result: Centered overlay, blocks all input
Common Pitfalls & Debugging (Project 4)
Problem 1: “Panel never appears”
- Why: You did not bind to
zwlr_layer_shell_v1or never created the layer surface. - Fix: Confirm the global exists with
wayland-infoand bind to it before creating the surface. - Quick test:
WAYLAND_DEBUG=1 ./panel | grep -E "zwlr_layer_shell|layer_surface"
Problem 2: “Panel appears but is zero size”
- Why: You never handled the configure event or acknowledged it.
- Fix: Wait for
zwlr_layer_surface_v1.configure, callack_configure, then render and commit. - Quick test: Log the configure width/height and verify non-zero values.
Problem 3: “Panel overlaps windows when it shouldn’t”
- Why: Exclusive zone is not set, or set to 0 instead of bar height.
- Fix:
zwlr_layer_surface_v1_set_exclusive_zone(surface, bar_height). - Quick test: Maximize a window and verify it stops below the bar.
Problem 4: “Clock updates cause 100% CPU”
- Why: You are redrawing in a tight loop instead of using timers + damage.
- Fix: Use
timerfdor a Wayland event source; only damage the region that changed. - Quick test:
topshould show near-idle CPU when the panel is static.
Problem 5: “Text is blurry or clipped”
- Why: Output scale not handled; you are drawing in logical vs buffer pixels incorrectly.
- Fix: Multiply sizes by the output scale and set buffer scale on the surface.
- Quick test: Print the
wl_outputscale and verify buffer dimensions = logical * scale.
Definition of Done
- Panel appears on the selected output with correct size and position
- Exclusive zone works (windows avoid the bar unless configured not to)
- Text and icons render correctly at different output scales
- Updates happen on a timer with low CPU usage
WAYLAND_DEBUG=1shows configure/ack/commit cycles without errors
Project 5: X11 Comparison Project - Bare-Metal X11 Client
- File: WAYLAND_X11_COMPOSITOR_LEARNING_PROJECTS.md
- Main Programming Language: C
- Alternative Programming Languages: C++, Rust, Zig
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: Level 1: The “Resume Gold”
- Difficulty: Level 2: Intermediate (The Developer)
- Knowledge Area: Graphics, Windowing Systems
- Software or Tool: X11, Xlib
- Main Book: Xlib Programming Manual (O’Reilly)
What you’ll build: The exact same colored window as Project 1, but using raw Xlib to viscerally feel the differences between X11 and Wayland.
Why it teaches X11 vs Wayland: Nothing teaches the difference better than implementing the same thing in both. You’ll feel X11’s complexity (window properties, atoms, event masks) vs Wayland’s simplicity (but also Wayland’s “you do the work” philosophy).
Core challenges you’ll face:
- Understanding X11’s request/reply model
- Dealing with X atoms and properties
- The expose event and redraw model (X11 stores nothing)
- Window manager hints (ICCCM, EWMH)
- Input focus model differences
Key Concepts:
- Xlib Programming: “Xlib Programming Manual” (O’Reilly, available online)
- ICCCM/EWMH: freedesktop.org window manager specifications
- X11 Protocol: X.org protocol documentation
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Project 1 completed (for comparison)
Real world outcome: An X11 window that looks identical to your Wayland window. The journey getting there will cement why Wayland exists.
Learning milestones:
- Open display, create window -> compare to
wl_display_connect - Handle Expose events -> understand why Wayland’s “client renders” is simpler
- Set WM_NAME, WM_HINTS -> understand X11’s property-based metadata
- Compare the two codebases -> internalize architectural differences
Real World Outcome
You will build a minimal X11 client that creates a window identical to Project 1 (solid color, simple title, close handling), then compare the code paths and event flow.
What you will see:
- A visible X11 window with a solid background and window title
- Expose events driving your redraw logic
- An event loop that looks and feels different from Wayland’s callback model
Command Line Outcome Example:
$ gcc -Wall -O2 -o x11_client x11_client.c -lX11
$ ./x11_client
X11 client starting...
Display: :0
Created window id=0x3c00007
Mapped window, waiting for Expose
Expose event: repainting
Press ESC or close the window to exit
What you will learn by contrast:
- X11 clients must redraw on Expose events (server does not retain content)
- Properties/atoms are the mechanism for metadata (WM_NAME, _NET_WM_PID)
- The request/reply model is explicit and chatty compared to Wayland
The Core Question You’re Answering
“What does an X11 client really do compared to a Wayland client, and why did the Linux desktop move away from this model?”
By implementing the same window in X11, you’ll feel the cost of the older model: properties, atoms, expose events, and implicit server-side drawing semantics. The contrast locks in why Wayland’s design choices matter.
Concepts You Must Understand First
Stop and research these before coding:
- X11 Client-Server Model
- How does the X server process requests?
- What is the role of the X server vs the window manager?
- Book Reference: “The Linux Programming Interface” Ch. 52 (overview of UNIX IPC)
- Xlib Event Loop
- What is an Expose event and why do you redraw?
- How do you select input events?
- Book Reference: “Advanced Programming in the UNIX Environment” Ch. 14-18 (event-driven patterns)
- Atoms and Properties
- What is an Atom? Why are window properties key/value pairs?
- What are WM_NAME and _NET_WM_PID?
- Book Reference: Xlib Programming Manual (O’Reilly) Ch. 1-3
- ICCCM / EWMH Basics
- What is the difference between ICCCM and EWMH?
- How do they influence window managers?
- Book Reference: freedesktop.org ICCCM/EWMH specs
Questions to Guide Your Design
- Event handling
- Which events must you select to handle expose, keypress, and close?
- How will you translate KeyPress into a clean exit?
- Window properties
- Which properties set the title and PID?
- How will you handle WM_DELETE_WINDOW gracefully?
- Drawing model
- Will you use XFillRectangle or XPutImage for pixels?
- How will you ensure repaint happens on Expose?
Thinking Exercise
Exercise: Compare event loops
Take a blank page and sketch two loops:
- Wayland: dispatch events, wait for frame callback, render, commit
- X11: wait for events, redraw on Expose, flush requests
Now answer:
- Which model gives the compositor control over presentation timing?
- Which model forces the client to redraw on demand?
The Interview Questions They’ll Ask
- “What is the biggest architectural difference between X11 and Wayland?”
- “Why does X11 require clients to handle Expose events?”
- “How do window properties/atoms work in X11?”
- “What is ICCCM vs EWMH?”
- “Why is input isolation stronger in Wayland?”
- “What is Xwayland and why is it needed?”
Hints in Layers
Hint 1: Minimal X11 window
Display *d = XOpenDisplay(NULL);
Window w = XCreateSimpleWindow(d, DefaultRootWindow(d), 10, 10, 400, 300, 1,
BlackPixel(d, 0), WhitePixel(d, 0));
XSelectInput(d, w, ExposureMask | KeyPressMask);
XMapWindow(d, w);
Hint 2: Expose redraw
if (ev.type == Expose) {
XSetForeground(d, gc, 0x3366cc);
XFillRectangle(d, w, gc, 0, 0, 400, 300);
}
Hint 3: WM_DELETE_WINDOW handling
Atom wmDelete = XInternAtom(d, "WM_DELETE_WINDOW", False);
XSetWMProtocols(d, w, &wmDelete, 1);
Hint 4: Compare with Wayland Run both clients side by side. Write down what you had to do in X11 that Wayland did not require.
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| X11 fundamentals | Xlib Programming Manual (O’Reilly) | Ch. 1-3 |
| Event-driven design | “The Linux Programming Interface” by Kerrisk | Ch. 63 |
| UNIX IPC refresher | “Advanced Programming in the UNIX Environment” | Ch. 14-18 |
| System view of graphics | “How Linux Works, 3rd Edition” by Brian Ward | Ch. 14 |
Common Pitfalls & Debugging (Project 5)
Problem 1: “XOpenDisplay returns NULL”
- Why: The DISPLAY environment variable is not set or not accessible.
- Fix: Ensure
DISPLAY=:0and that you have permission to access the X server. - Quick test:
echo $DISPLAYthen runxclock.
Problem 2: “Window appears but stays blank”
- Why: You never handled Expose events.
- Fix: Redraw on
Exposeand callXFlushafter drawing. - Quick test: Resize the window and verify you repaint.
Problem 3: “Window closes instantly”
- Why: You exit on any KeyPress without filtering.
- Fix: Only exit on Escape or WM_DELETE_WINDOW.
- Quick test: Print the keysym before deciding to exit.
Definition of Done
- X11 client compiles and runs with
-lX11 - Window appears with correct title and solid color
- Expose events trigger redraws without flicker
- WM_DELETE_WINDOW closes cleanly
- You can explain at least three concrete differences vs Wayland
Project Comparison Table
| Project | Difficulty | Time | Depth of Understanding | Fun Factor |
|---|---|---|---|---|
| Bare-Metal Wayland Client | Intermediate | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Compositor with wlroots | Advanced | 1 month+ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Custom Protocol Extension | Intermediate-Advanced | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Layer Shell Panel/Bar | Intermediate | 2-3 weeks | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| X11 Comparison Client | Intermediate | 1 week | ⭐⭐⭐ (comparative) | ⭐⭐⭐ |
Recommendation
Start with Project 1 (Bare-Metal Wayland Client), then immediately do Project 5 (X11 Comparison) while the Wayland code is fresh in your mind. This side-by-side experience will cement your understanding of why Wayland was designed the way it was.
Then progress to Project 2 (Compositor with wlroots)–this is where the real magic happens. Building a compositor gives you the “other side” of the protocol and reveals how desktop environments actually work.
Suggested progression:
Week 1-2: Project 1 (Wayland client)
Week 3: Project 5 (X11 comparison)
Week 4-5: Project 4 (Layer shell panel) - optional but fun
Week 6+: Project 2 (Compositor) - the deep dive
Final Capstone Project: Your Own Desktop Environment
What you’ll build: A complete (minimal) desktop environment: your compositor from Project 2 + your panel from Project 4 + a custom protocol for inter-component communication + a simple app launcher.
Why this is the ultimate test: A desktop environment is the integration of everything: compositor, shell protocols, input handling, configuration, and IPC. Building one–even a minimal one–means you truly understand the full stack.
Core challenges you’ll face:
- Session management (starting/stopping the compositor)
- Configuration system (keybindings, theme, layout)
- Application launching and focus management
- Multi-monitor support with per-output configuration
- Popup menus and overlay surfaces
Key Concepts:
- Session Management: systemd user sessions or custom session scripts
- D-Bus Integration: freedesktop.org specifications
- Configuration Formats: Your choice (INI, TOML, Lua, etc.)
- Desktop Entry Spec: freedesktop.org desktop entry specification
Difficulty: Advanced Time estimate: 2-3 months Prerequisites: Projects 1, 2, and 4 completed
Real world outcome: Boot your computer, log in, and use your desktop environment. Open apps, arrange windows, see your panel, use your keybindings. This is your Linux desktop.
Learning milestones:
- Compositor + panel working together -> understand component integration
- Keybindings and app launcher functional -> understand input routing and process spawning
- Multi-monitor with configuration -> understand output management
- Daily-driveable (for an hour) -> you’ve built a desktop environment
Essential Resources
- “The Wayland Book” by Drew DeVault - https://wayland-book.com - High-signal narrative guide
- Wayland core protocol spec - https://wayland.freedesktop.org/docs/html/
- Wayland protocol index - https://wayland.app/protocols
- xdg-shell - https://wayland.app/protocols/xdg-shell
- xdg-output - https://wayland.app/protocols/xdg-output-unstable-v1
- xdg-activation - https://wayland.app/protocols/xdg-activation-v1
- xdg-decoration - https://wayland.app/protocols/xdg-decoration-unstable-v1
- wlr-layer-shell - https://wayland.app/protocols/wlr-layer-shell-unstable-v1
- linux-dmabuf - https://wayland.app/protocols/linux-dmabuf-v1
- viewporter - https://wayland.app/protocols/viewporter
- fractional-scale - https://wayland.app/protocols/fractional-scale-v1
- presentation-time - https://wayland.app/protocols/presentation-time
- pointer-constraints - https://wayland.app/protocols/pointer-constraints-unstable-v1
- relative-pointer - https://wayland.app/protocols/relative-pointer-unstable-v1
- wlroots documentation - https://wlroots.readthedocs.io/
- wlroots source code - https://gitlab.freedesktop.org/wlroots/wlroots
- libinput documentation - https://wayland.freedesktop.org/libinput/doc/latest/
- DRM/KMS documentation - https://docs.kernel.org/gpu/drm-kms.html
- wayland-protocols repository - https://gitlab.freedesktop.org/wayland/wayland-protocols
- Sway source code - https://github.com/swaywm/sway - Production compositor built on wlroots
- Xwayland man page - https://www.x.org/releases/X11R7.7/doc/man/man1/Xwayland.1.xhtml
- X11 protocol specification - https://www.x.org/releases/X11R7.6/doc/xproto/x11protocol.html