Project 1: Bare-Metal Wayland Client

Project 1: Bare-Metal Wayland Client

Project Overview

Attribute Value
Difficulty Intermediate (Level 3)
Time Estimate 1-2 weeks
Programming Language C
Knowledge Area Graphics / Window Systems
Main Book โ€œThe Wayland Bookโ€ by Drew DeVault
Coolness Level Level 4: Hardcore Tech Flex
Business Potential Resume Gold

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.


Learning Objectives

By completing this project, you will be able to:

  1. Explain the Wayland protocol model - Describe how Waylandโ€™s asynchronous, object-oriented protocol differs from X11โ€™s request/reply model
  2. Implement a Wayland client from scratch - Write C code that connects to a compositor, negotiates interfaces, and displays pixels
  3. Manage shared memory buffers - Use POSIX shared memory (memfd_create, mmap) for zero-copy buffer sharing
  4. Handle the xdg-shell lifecycle - Implement the configure/ack/commit cycle correctly
  5. Debug Wayland applications - Use WAYLAND_DEBUG=1 to trace protocol messages
  6. Compare client-side vs server-side rendering - Understand why Wayland puts rendering responsibility on clients

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?โ€


Deep Theoretical Foundation

1. Display Server Architecture: X11 vs Wayland

Understanding why Wayland exists requires understanding what was wrong with X11:

X11 Architecture (Complex 3-Layer Stack):
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Application   โ”‚         โ”‚   Application   โ”‚         โ”‚   Application   โ”‚
โ”‚   (Client)      โ”‚         โ”‚   (Client)      โ”‚         โ”‚   (Client)      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚                           โ”‚                           โ”‚
         โ”‚  X11 Protocol (Network)   โ”‚                           โ”‚
         โ”‚  - Draw requests          โ”‚                           โ”‚
         โ”‚  - Event messages         โ”‚                           โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                     โ”‚
                          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                          โ”‚       X Server                       โ”‚
                          โ”‚  - Manages resources                 โ”‚
                          โ”‚  - Executes draw commands            โ”‚
                          โ”‚  - Routes input events               โ”‚
                          โ”‚  - Handles network protocol          โ”‚
                          โ”‚  - Stores window contents            โ”‚
                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                     โ”‚ Rendered frames
                                     โ”‚
                          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                          โ”‚   Compositor (Separate Process)      โ”‚
                          โ”‚  - Reads from X server               โ”‚
                          โ”‚  - Applies effects                   โ”‚
                          โ”‚  - Composites to framebuffer         โ”‚
                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                     โ”‚
                          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                          โ”‚   GPU / Display Hardware             โ”‚
                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Problems with X11:
  โ€ข Extensive IPC (client โ†’ X server โ†’ compositor)
  โ€ข X server as middleman adds latency
  โ€ข Security: any client can spy on other windows
  โ€ข X server stores window contents (memory overhead)
  โ€ข Network protocol overhead even for local apps


Wayland Architecture (Direct, Simple 2-Layer):
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Application   โ”‚         โ”‚   Application   โ”‚         โ”‚   Application   โ”‚
โ”‚   (Client)      โ”‚         โ”‚   (Client)      โ”‚         โ”‚   (Client)      โ”‚
โ”‚                 โ”‚         โ”‚                 โ”‚         โ”‚                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚         โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚         โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ Renders   โ”‚  โ”‚         โ”‚  โ”‚ Renders   โ”‚  โ”‚         โ”‚  โ”‚ Renders   โ”‚  โ”‚
โ”‚  โ”‚ Own Windowโ”‚  โ”‚         โ”‚  โ”‚ Own Windowโ”‚  โ”‚         โ”‚  โ”‚ Own Windowโ”‚  โ”‚
โ”‚  โ”‚ to Buffer โ”‚  โ”‚         โ”‚  โ”‚ to Buffer โ”‚  โ”‚         โ”‚  โ”‚ to Buffer โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚         โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚         โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
         โ”‚                           โ”‚                           โ”‚
         โ”‚ Wayland Protocol          โ”‚                           โ”‚
         โ”‚ - Share buffer FD         โ”‚                           โ”‚
         โ”‚ - Input events            โ”‚                           โ”‚
         โ”‚ - Surface config          โ”‚                           โ”‚
         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                     โ”‚
                  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                  โ”‚    Wayland Compositor (All-in-One)          โ”‚
                  โ”‚                                             โ”‚
                  โ”‚  Display Server + Window Manager + Effects  โ”‚
                  โ”‚                                             โ”‚
                  โ”‚  โ€ข Receives client buffers (zero-copy)      โ”‚
                  โ”‚  โ€ข Manages window positions & focus         โ”‚
                  โ”‚  โ€ข Routes input to correct client           โ”‚
                  โ”‚  โ€ข Composites buffers to screen             โ”‚
                  โ”‚  โ€ข Controls DRM/KMS (display hardware)      โ”‚
                  โ”‚  โ€ข Enforces security isolation              โ”‚
                  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                     โ”‚ Direct GPU access
                                     โ”‚
                  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                  โ”‚       GPU / Display Hardware                โ”‚
                  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Benefits of Wayland:
  โœ“ Clients render directly โ†’ no middleman
  โœ“ Zero-copy buffer sharing โ†’ better performance
  โœ“ Security by design โ†’ input isolation
  โœ“ Compositor has full control โ†’ smooth animations
  โœ“ Simpler protocol โ†’ easier to implement
  โœ“ No network overhead for local apps

2. The Wayland Object Model

Wayland uses an object-oriented protocol where everything is an object with:

  • A unique ID within the connection
  • An interface (like a class)
  • Requests (methods you call on the object)
  • Events (callbacks the object fires)
WAYLAND OBJECT HIERARCHY
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

wl_display (ID=1, always exists)
    โ”‚
    โ”œโ”€โ”€ wl_registry (ID=2, requested from display)
    โ”‚       โ”‚
    โ”‚       โ”œโ”€โ”€ wl_compositor (global, bind to create surfaces)
    โ”‚       โ”‚       โ”‚
    โ”‚       โ”‚       โ””โ”€โ”€ wl_surface (created from compositor)
    โ”‚       โ”‚               โ”‚
    โ”‚       โ”‚               โ””โ”€โ”€ xdg_surface (role assignment)
    โ”‚       โ”‚                       โ”‚
    โ”‚       โ”‚                       โ””โ”€โ”€ xdg_toplevel (window behavior)
    โ”‚       โ”‚
    โ”‚       โ”œโ”€โ”€ wl_shm (global, shared memory management)
    โ”‚       โ”‚       โ”‚
    โ”‚       โ”‚       โ””โ”€โ”€ wl_shm_pool (created from shm)
    โ”‚       โ”‚               โ”‚
    โ”‚       โ”‚               โ””โ”€โ”€ wl_buffer (created from pool)
    โ”‚       โ”‚
    โ”‚       โ”œโ”€โ”€ xdg_wm_base (global, xdg-shell entry point)
    โ”‚       โ”‚
    โ”‚       โ”œโ”€โ”€ wl_seat (global, input device group)
    โ”‚       โ”‚       โ”‚
    โ”‚       โ”‚       โ”œโ”€โ”€ wl_keyboard
    โ”‚       โ”‚       โ”œโ”€โ”€ wl_pointer
    โ”‚       โ”‚       โ””โ”€โ”€ wl_touch
    โ”‚       โ”‚
    โ”‚       โ””โ”€โ”€ wl_output (global, each monitor)
    โ”‚
    โ””โ”€โ”€ wl_callback (one-shot callback objects)

3. Shared Memory and Zero-Copy Rendering

Wayland achieves high performance through zero-copy buffer sharing:

SHARED MEMORY BUFFER FLOW
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

Client Process                        Compositor Process
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                             โ”‚       โ”‚                         โ”‚
โ”‚ 1. Create shared memory:    โ”‚       โ”‚                         โ”‚
โ”‚    fd = memfd_create()      โ”‚       โ”‚                         โ”‚
โ”‚    ftruncate(fd, size)      โ”‚       โ”‚                         โ”‚
โ”‚                             โ”‚       โ”‚                         โ”‚
โ”‚ 2. Map into client memory:  โ”‚       โ”‚                         โ”‚
โ”‚    data = mmap(fd)          โ”‚       โ”‚                         โ”‚
โ”‚    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”‚       โ”‚                         โ”‚
โ”‚    โ”‚ Shared Memory   โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚    โ”‚ (Client's View) โ”‚      โ”‚       โ”‚                   โ”‚     โ”‚
โ”‚    โ”‚ pixels[0..n]    โ”‚      โ”‚       โ”‚                   โ”‚     โ”‚
โ”‚    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ”‚       โ”‚                   โ”‚     โ”‚
โ”‚            โ”‚                โ”‚       โ”‚                   โ”‚     โ”‚
โ”‚ 3. Create wl_shm_pool:      โ”‚       โ”‚                   โ”‚     โ”‚
โ”‚    pool = wl_shm_create_poolโ”‚       โ”‚                   โ”‚     โ”‚
โ”‚    (shm, fd, size)          โ”‚โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ Compositor maps   โ”‚     โ”‚
โ”‚                             โ”‚       โ”‚ the same fd       โ”‚     โ”‚
โ”‚                             โ”‚       โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚                             โ”‚       โ”‚ โ”‚ Shared Memory   โ”‚     โ”‚
โ”‚                             โ”‚       โ”‚ โ”‚ (Compositor's   โ”‚     โ”‚
โ”‚                             โ”‚       โ”‚ โ”‚  View) - SAME   โ”‚     โ”‚
โ”‚                             โ”‚       โ”‚ โ”‚  PHYSICAL MEM   โ”‚     โ”‚
โ”‚                             โ”‚       โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚ 4. Create buffer:           โ”‚       โ”‚                         โ”‚
โ”‚    buffer = pool.create_buf โ”‚       โ”‚                         โ”‚
โ”‚                             โ”‚       โ”‚                         โ”‚
โ”‚ 5. Paint pixels:            โ”‚       โ”‚                         โ”‚
โ”‚    for (i=0; i<n; i++)      โ”‚       โ”‚                         โ”‚
โ”‚      pixels[i] = 0xFFRRGGBB โ”‚       โ”‚                         โ”‚
โ”‚                             โ”‚       โ”‚                         โ”‚
โ”‚ 6. Attach and commit:       โ”‚       โ”‚                         โ”‚
โ”‚    wl_surface_attach(buf)   โ”‚โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ 7. Compositor reads     โ”‚
โ”‚    wl_surface_commit()      โ”‚       โ”‚    directly from        โ”‚
โ”‚                             โ”‚       โ”‚    shared memory!       โ”‚
โ”‚                             โ”‚       โ”‚                         โ”‚
โ”‚                             โ”‚       โ”‚    NO COPY NEEDED       โ”‚
โ”‚                             โ”‚       โ”‚                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key Insight: The file descriptor (fd) is passed over the Unix socket
using SCM_RIGHTS. Both processes map the same underlying memory.
The compositor never copies pixel dataโ€”it reads directly from your buffer.

4. The Configure/Ack/Commit Cycle

One of the most confusing aspects of xdg-shell is the configure cycle:

XDG-SHELL CONFIGURE CYCLE
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

Why This Exists:
  - Compositor suggests window size/state
  - Client must acknowledge before committing content
  - Prevents race conditions (client draws at wrong size)
  - Ensures synchronized state between compositor and client

The Flow:

Client                              Compositor
  โ”‚                                     โ”‚
  โ”‚ xdg_surface.get_toplevel()          โ”‚
  โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
  โ”‚                                     โ”‚
  โ”‚ wl_surface.commit() [initial]       โ”‚
  โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
  โ”‚                                     โ”‚ "Client wants a window"
  โ”‚                                     โ”‚
  โ”‚     xdg_toplevel.configure(w,h,s)   โ”‚ "Try this size/state"
  โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
  โ”‚     xdg_surface.configure(serial)   โ”‚ "Configuration complete"
  โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
  โ”‚                                     โ”‚
  โ”‚ xdg_surface.ack_configure(serial)   โ”‚ "I understand"
  โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
  โ”‚                                     โ”‚
  โ”‚ [Create buffer at suggested size]   โ”‚
  โ”‚ [Paint content to buffer]           โ”‚
  โ”‚                                     โ”‚
  โ”‚ wl_surface.attach(buffer)           โ”‚
  โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
  โ”‚ wl_surface.commit()                 โ”‚
  โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
  โ”‚                                     โ”‚
  โ”‚                                     โ”‚ Window appears on screen!

Critical Rules:
  โœ— DO NOT commit content before receiving configure
  โœ— DO NOT commit without acking first
  โœ— DO NOT ignore the suggested size (you can, but it's rude)
  โœ“ DO ack even if you use a different size
  โœ“ DO handle multiple configures (only ack the last one)

Complete Project Specification

Functional Requirements

  1. Connection: Connect to the default Wayland display ($WAYLAND_DISPLAY or wayland-0)
  2. Protocol Negotiation: Bind to required globals: wl_compositor, wl_shm, xdg_wm_base
  3. Surface Creation: Create a wl_surface with an xdg_toplevel role
  4. Buffer Management: Create and manage shared memory buffers
  5. Rendering: Fill the window with a solid color (configurable)
  6. Event Handling: Handle configure events and window resize
  7. Cleanup: Properly destroy all resources on exit

Non-Functional Requirements

  • Must compile with only -lwayland-client (plus generated protocol code)
  • Must not use any toolkit (GTK, Qt, SDL)
  • Must handle Ctrl+C gracefully
  • Must not leak memory or file descriptors

Expected Behavior

When running, the program should:

  1. Display diagnostic output showing protocol negotiation
  2. Show a solid-colored window that can be resized
  3. Respond to window close events
  4. Clean up properly on exit

Solution Architecture

High-Level Design

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                        main()                                   โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚ 1. Initialize state structure                              โ”‚  โ”‚
โ”‚  โ”‚ 2. Connect to Wayland display                              โ”‚  โ”‚
โ”‚  โ”‚ 3. Get registry, bind globals                              โ”‚  โ”‚
โ”‚  โ”‚ 4. Create surface hierarchy                                โ”‚  โ”‚
โ”‚  โ”‚ 5. Enter event loop                                        โ”‚  โ”‚
โ”‚  โ”‚ 6. Cleanup on exit                                         โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                              โ”‚
           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
           โ–ผ                  โ–ผ                  โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Registry       โ”‚ โ”‚  Surface        โ”‚ โ”‚  Buffer         โ”‚
โ”‚  Handling       โ”‚ โ”‚  Management     โ”‚ โ”‚  Management     โ”‚
โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚                 โ”‚
โ”‚ - Enumerate     โ”‚ โ”‚ - Create        โ”‚ โ”‚ - Create pool   โ”‚
โ”‚   globals       โ”‚ โ”‚   wl_surface    โ”‚ โ”‚ - Create buffer โ”‚
โ”‚ - Bind to       โ”‚ โ”‚ - Create        โ”‚ โ”‚ - Paint pixels  โ”‚
โ”‚   interfaces    โ”‚ โ”‚   xdg_surface   โ”‚ โ”‚ - Attach/commit โ”‚
โ”‚ - Handle        โ”‚ โ”‚ - Handle        โ”‚ โ”‚ - Handle resize โ”‚
โ”‚   removal       โ”‚ โ”‚   configure     โ”‚ โ”‚ - Handle releaseโ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

State Structure Design

struct client_state {
    // Display connection
    struct wl_display *display;
    struct wl_registry *registry;

    // Bound globals
    struct wl_compositor *compositor;
    struct wl_shm *shm;
    struct xdg_wm_base *xdg_wm_base;

    // Surface objects
    struct wl_surface *surface;
    struct xdg_surface *xdg_surface;
    struct xdg_toplevel *xdg_toplevel;

    // Buffer state
    struct wl_buffer *buffer;
    void *buffer_data;
    size_t buffer_size;

    // Window state
    int32_t width;
    int32_t height;
    uint32_t last_configure_serial;
    bool configured;
    bool running;

    // Appearance
    uint32_t color;  // XRGB format
};

Listener Architecture

Each Wayland object that sends events needs a listener:

LISTENER CALLBACKS
โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

wl_registry_listener:
  โ”œโ”€โ”€ global()       โ†’ Called for each available global
  โ””โ”€โ”€ global_remove() โ†’ Called when global is removed

xdg_wm_base_listener:
  โ””โ”€โ”€ ping()         โ†’ Compositor liveness check, must pong

xdg_surface_listener:
  โ””โ”€โ”€ configure()    โ†’ Surface configuration with serial

xdg_toplevel_listener:
  โ”œโ”€โ”€ configure()    โ†’ Size/state suggestion
  โ””โ”€โ”€ close()        โ†’ User requested close

wl_buffer_listener:
  โ””โ”€โ”€ release()      โ†’ Buffer can be reused/destroyed

Phased Implementation Guide

Phase 1: Connection and Registry (Day 1)

Goal: Connect to Wayland and enumerate available globals

Steps:

  1. Create basic project structure with Makefile
  2. Include necessary headers
  3. Implement wl_display_connect()
  4. Get registry and add listener
  5. Print all available globals

Verification:

$ ./wayland_client
Connected to Wayland display
Global: wl_compositor v6
Global: wl_shm v1
Global: xdg_wm_base v5
...

Key Code Pattern:

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) {
        state->compositor = wl_registry_bind(registry, name,
            &wl_compositor_interface, 4);
    }
    // ... bind other globals
}

Phase 2: Surface Creation (Day 2-3)

Goal: Create a surface with xdg-shell role

Steps:

  1. Bind to wl_compositor, wl_shm, xdg_wm_base
  2. Create wl_surface from compositor
  3. Implement xdg_wm_base ping handler
  4. Create xdg_surface and xdg_toplevel
  5. Set window title
  6. Initial commit to trigger configure

Verification:

$ WAYLAND_DEBUG=1 ./wayland_client 2>&1 | grep xdg
[...] xdg_wm_base@6.get_xdg_surface(...)
[...] xdg_surface@7.get_toplevel(...)
[...] -> xdg_toplevel@8.configure(800, 600, ...)
[...] -> xdg_surface@7.configure(1234)

Phase 3: Buffer Management (Day 3-4)

Goal: Create shared memory buffer and paint pixels

Steps:

  1. Implement create_shm_file() using memfd_create()
  2. Calculate buffer size (width * height * 4)
  3. Create wl_shm_pool
  4. Create wl_buffer from pool
  5. Paint solid color to buffer
  6. Implement buffer release handler

Key Functions:

static int create_shm_file(size_t size);
static struct wl_buffer* create_buffer(struct client_state *state);
static void paint_buffer(struct client_state *state, uint32_t color);

Phase 4: Configure Handling (Day 4-5)

Goal: Properly handle the configure/ack/commit cycle

Steps:

  1. Store configure serial
  2. Acknowledge configure
  3. Create buffer at configured size
  4. Paint and attach buffer
  5. Commit surface

Critical Pattern:

static void xdg_surface_configure(void *data,
                                   struct xdg_surface *xdg_surface,
                                   uint32_t serial) {
    struct client_state *state = data;

    xdg_surface_ack_configure(xdg_surface, serial);

    if (state->buffer) {
        wl_buffer_destroy(state->buffer);
    }

    state->buffer = create_buffer(state);
    paint_buffer(state, state->color);

    wl_surface_attach(state->surface, state->buffer, 0, 0);
    wl_surface_damage_buffer(state->surface, 0, 0,
                             state->width, state->height);
    wl_surface_commit(state->surface);

    state->configured = true;
}

Phase 5: Event Loop and Cleanup (Day 5-6)

Goal: Main event loop and proper resource cleanup

Steps:

  1. Implement main event loop with wl_display_dispatch()
  2. Handle SIGINT for graceful shutdown
  3. Implement cleanup in reverse order of creation
  4. Verify no leaks with valgrind

Event Loop:

while (state.running && wl_display_dispatch(state.display) != -1) {
    // Events are handled in callbacks
}

Phase 6: Resize Handling (Day 6-7)

Goal: Properly handle window resize

Steps:

  1. Handle xdg_toplevel.configure with new dimensions
  2. Store new width/height
  3. Recreate buffer at new size on xdg_surface.configure
  4. Handle size (0, 0) meaning โ€œclientโ€™s choiceโ€

Testing Strategy

Manual Testing Checklist

  • Window appears on screen with correct color
  • Window can be resized - color fills new area
  • Window can be minimized and restored
  • Window can be maximized
  • Window can be closed with window controls
  • Program exits cleanly on Ctrl+C
  • No visual artifacts during resize

Automated Verification

# Check for memory leaks
valgrind --leak-check=full ./wayland_client

# Verify protocol correctness
WAYLAND_DEBUG=1 ./wayland_client 2>&1 | grep -E "(error|warning)"

# Test on different compositors
# - Sway
# - GNOME Wayland
# - weston

Expected Protocol Trace

A successful run should show this sequence in WAYLAND_DEBUG=1:

wl_display@1.get_registry(new id wl_registry@2)
wl_registry@2.bind(1, "wl_compositor", 4)
wl_registry@2.bind(3, "wl_shm", 1)
wl_registry@2.bind(6, "xdg_wm_base", 5)
wl_compositor@100.create_surface(new id wl_surface@200)
xdg_wm_base@102.get_xdg_surface(new id xdg_surface@300, wl_surface@200)
xdg_surface@300.get_toplevel(new id xdg_toplevel@400)
xdg_toplevel@400.set_title("My Wayland Client")
wl_surface@200.commit()
 -> xdg_toplevel@400.configure(800, 600, array)
 -> xdg_surface@300.configure(1234)
xdg_surface@300.ack_configure(1234)
wl_shm@101.create_pool(new id wl_shm_pool@500, fd 3, 1920000)
wl_shm_pool@500.create_buffer(new id wl_buffer@600, 0, 800, 600, 3200, 1)
wl_surface@200.attach(wl_buffer@600, 0, 0)
wl_surface@200.damage_buffer(0, 0, 800, 600)
wl_surface@200.commit()

Common Pitfalls and Debugging

Problem: Window doesnโ€™t appear

Possible causes:

  1. Forgot to call wl_surface_commit() after initial setup
  2. Didnโ€™t ack configure before committing content
  3. Buffer not attached before commit
  4. Running on X11 (check echo $XDG_SESSION_TYPE)

Debug:

WAYLAND_DEBUG=1 ./wayland_client 2>&1 | grep -i error

Problem: Black window instead of colored

Possible causes:

  1. Painting after commit (too late)
  2. Wrong pixel format (using ARGB instead of XRGB)
  3. Buffer size mismatch with surface size

Debug:

// Add after painting
fprintf(stderr, "Buffer: %dx%d, first pixel = 0x%08x\n",
        state->width, state->height,
        ((uint32_t*)state->buffer_data)[0]);

Problem: Crash on resize

Possible causes:

  1. Destroying buffer while still in use by compositor
  2. Not handling wl_buffer.release
  3. Memory corruption from size calculations

Fix: Use double buffering or wait for release event

Problem: Protocol error on startup

Common message: โ€œinvalid objectโ€

Cause: Using object before itโ€™s created, or after itโ€™s destroyed

Debug: Check object creation order matches protocol requirements


Extensions and Challenges

Challenge 1: Gradient Background

Instead of solid color, render a gradient from top to bottom.

Challenge 2: Double Buffering

Implement proper double buffering with two buffers.

Challenge 3: Animation

Animate the color over time using frame callbacks.

Challenge 4: Keyboard Input

Bind to wl_seat, get keyboard, handle key events to change color.

Challenge 5: Multi-Monitor

Enumerate wl_output globals and display on a specific monitor.


Real-World Connections

How This Relates to Real Applications

  • GTK4: Uses similar code internally with GdkWaylandDisplay
  • Qt: QWaylandWindow implements the same surface lifecycle
  • SDL2: SDL_CreateWindow on Wayland does exactly this
  • Firefox: WebRender uses shared memory buffers like this
  • Electron: Ozone layer implements Wayland using these primitives

Career Applications

  • Graphics Driver Development: Understanding buffer sharing is essential
  • Window Manager Development: This is the client side; compositor is the other
  • Embedded Linux: Automotive/IVI systems use Wayland extensively
  • Gaming: Game engines need to understand display server protocols
  • Desktop Environment Development: GNOME, KDE rely on these concepts

Resources

Essential Reading

Resource Purpose
โ€œThe Wayland Bookโ€ Ch. 1-4 Protocol fundamentals
โ€œThe Wayland Bookโ€ Ch. 8-9 XDG-Shell specifics
โ€œThe Linux Programming Interfaceโ€ Ch. 54 Shared memory
โ€œEffective Cโ€ Ch. 6 Memory management patterns

Code References

  • weston/clients/simple-shm.c - Canonical minimal client
  • wayland/tests/ - Test clients from libwayland
  • GTK4 source: gdk/wayland/ - Production implementation

Online Documentation

  • https://wayland-book.com - The Wayland Book (free online)
  • https://wayland.app - Protocol documentation browser
  • /usr/share/wayland/wayland.xml - Core protocol definition
  • /usr/share/wayland-protocols/ - Extension protocols

Self-Assessment Checklist

Before considering this project complete, verify you can:

  • Explain why Wayland exists and how it differs from X11
  • Describe the role of wl_registry and global binding
  • Explain the configure/ack/commit cycle without looking at notes
  • Draw the buffer lifecycle state machine from memory
  • Debug a protocol error using WAYLAND_DEBUG=1
  • Modify the color without recompiling (via command line)
  • Add keyboard input to your client
  • Explain what happens when the user resizes the window
  • Describe how shared memory enables zero-copy rendering
  • Compare your code to a GTK4 simple window (how much is hidden?)

Completing this project gives you a solid foundation in Wayland client development. You now understand what every Wayland application does under the hood, even when hidden by toolkits. Next, consider Project 5 (X11 Comparison) while this knowledge is fresh, then progress to Project 2 (Compositor) to see the other side of the protocol.