How to Make a Wayland Compositor (in Rust)

What is Wayland?

Wayland is the replacement for the X Window System, colloquially known as X11.

It's recommended that you read the official Wayland website, specifically the FAQ, architecture, and documentation, to familiarize yourself with the project. You should also keep this in your back pocket as you read this book if you need clarification on any terminology or concepts you don't understand.

What is a Wayland Compositor?

If you only learn one thing from this book, it should be that:

Wayland is not a compositor

Wayland is not a window manager

Wayland is not a display server

Rather, Wayland is a protocol for clients and compositors to speak to each other

In a typical X11 system, there are two necessary components: the X server and the window manager. The X server handles rendering and hardware interactions, and the window manager handles user interaction and window arrangement. Almost all X11 desktops use the Xorg server as the X server, and write their own window managers to provide the design and behavior of their desktop. Gnome, KDE, i3, and AwesomeWM, for example, have a very different user experience but are all still based on the Xorg server. Fundamentally, the window manager is an X11 client like any other, and all X11 clients are able to interact with your desktop in the same way.

A Wayland compositor is different. It is the sole source of authority on both rendering/hardware and window management. The right to arrange windows on screen and drive user interactions is reserved by the compositor.

Here is a list of compositors that will be referenced later in this book:

  • Way Cooler
    • Written by yours truly, Preston Carpenter. It was the first Wayland compositor written in Rust.
  • Fireplace
    • The second Wayland compositor written in Rust, it's goal is to be written completely in Rust including the Wayland implementation.
  • sway
    • A clone of i3 by Drew Devault, the first popular tiling Wayland compositor. The wlroots project is overseen by sway.
  • KWin
    • The KDE Wayland compositor, is the direct continuation of the X11 Plasma desktop (which is deprecated).
  • mutter
    • The Gnome Wayland compositor, is the direct continuation of the X11 Gnome desktop (which is deprecated).

These compositors will be mentioned either because I know them well enough to draw experience and stories from in order to help educate you (such as in Way Cooler's case), because they are doing something unique (in Fireplace's case) or because they made meaningful or politically significant decisions that have impacted the ecosystem (sway, mutter, and KWin).

Prerequisites for understanding reading this book

You should know how to program in Rust. You should have the latest stable version of Rust, any edition.

You should also be able to read C. Even though there will be no C in this book most of the Wayland ecosystem, including the reference implementation and wlroots (the framework we will be using), is written in C.

Introduction to wlroots

What is a compositor framework?

Since Wayland is just a protocol, and a compositor has to do all the things the xserver used to do, a Wayland compositor needs to use more than just Wayland in order to be functional.

The reason it's in charge of so much is manifold:

  1. It means Wayland can be used in a non desktop setup, such on a phone or in an embedded device where a "cursor" and other such features may not make sense.
  2. By consolidating the jobs into one process it has the potential to be efficient because there's more information it can use to make a more informed decision.
  3. Wayland developers are more or less ex-XOrg developers who don't want it to become big, old, and slow like X11 is. By making libwayland itself simple and pushing most of the work to the compositors it will more likely survive the years intact.

In order to implement all this additional functionality most compositors use a few other libraries. The important ones to know are:

  • KMS/DRM (Kernal Mode Settings/ Direct Rendering Manager)
    • Interfaces with GPUs of modern video cards to render to screens.
  • libinput
    • A library for handling the hardware portions of keyboard, pointers, touch devices, etc. that make up Wayland seats.
  • XWayland
    • A compatibility layer that lets users run deprecated X11 apps in Wayland. This is optional but is generally used by all compositors.
  • Systemd
    • For handling user sessions. This is also optional but broadly supported since logind is standard in most Linux distributions.
  • xkbcommon
    • Handling keyboard descriptions and to process key events.

Not all compositors use frameworks, some of them just use Wayland and the other libraries directly. Mutter and KWin do not use frameworks. Sway and Way Cooler used to use wlc but they now use wlroots. Fireplace used to use wlc but they now use Smithay, a framework written completely in Rust.

Which framework will this guide use?

wlroots is the compositor framework that will be used in this book to build a compositor.1 As of this writing it is the most mature Wayland compositor framework. There are 3 known other compositor frameworks, but have various problems:

  • wlc
    • Deprecated. It was found to abstract too much from the Wayland protocol, though it was immensely simpler than wlroots. The time measured to get a working compositor can be measured in hours instead of the expected couple of days or weeks it will take with wlroots. However even basic use cases, such as rendering borders around clients, is difficult to do well in wlc. Some use cases are outright impossible.
  • Smithay
    • A framework written entirely in Rust. Like most things in Rust however it is unstable and attempting to rewrite the entire stack in Rust.
  • libweston
    • A library based on the reference Weston compositor. Essentially you're just getting a new flavor of Weston instead of your own compositor, which makes it suffer from the same problems as wlc. As of this writing it's also largely unused outside of Weston.

Here is the elevator pitch for wlroots, taken straight from their README:

Pluggable, composable, unopinionated modules for building a Wayland compositor; or about 50,000 lines of code you were going to write anyway.

  • wlroots provides backends that abstract the underlying display and input hardware, including KMS/DRM, libinput, Wayland, X11, and headless backends, plus any custom backends you choose to write, which can all be created or destroyed at runtime and used in concert with each other.
  • wlroots provides unopinionated, mostly standalone implementations of many Wayland interfaces, both from wayland.xml and various protocol extensions. We also promote the standardization of portable extensions across many compositors.
  • wlroots provides several powerful, standalone, and optional tools that implement components common to many compositors, such as the arrangement of outputs in physical space.
  • wlroots provides an Xwayland abstraction that allows you to have excellent Xwayland support without worrying about writing your own X11 window manager on top of writing your compositor.
  • wlroots provides a renderer abstraction that simple compositors can use to avoid writing GL code directly, but which steps out of the way when your needs demand custom rendering code.

1 It is written in C, but there are safe Rust bindings written by me which is what will be used. The only thing the Rust library adds is memory safety and some more structure to the library. All the real features are implemented in wlroots.

Hello World

Each chapter's code contents will have its own folder with a name prefixed by the chapter number. For example, this chapter's code is stored in 1-hello-world/. The code can be found here.

The only dependency used, apart from the standard library, will be wlroots. A more useful compositor will want to use other libraries, but it is not done here in order to avoid choosing favorites while also being self contained and complete.

An important note to copy pasters: This document as well as the example code base is under the CC0 license.

So copy and paste liberally, you can use this code as a jumping off point.

This does not apply to wlroots-rs or wlroots, both of which are under the MIT license.

Boring setup

wlroots-rs is in crates.io. If you want to install it, simply add this to your Cargo.toml:

[dependencies]
wlroots = {version = "0.2", features = ["unstable", "static"]}

The "unstable" feature flag enables the wlroots features whose API hasn't stabilized yet. For now, this is necessary to build a compositor. In the future this restriction will be gradually lifted as the library matures.

The "static" feature flag statically links the wlroots library to the binary. This is optional, but encouraged since there's no stable ABI guarantee and it makes it easier to distribute the compositor to others.

Because the library is changing constantly however, it is suggested you add it as a git submodule to your project instead of using crates.io.

A minimal compositor

Here is the smallest, simplest compositor you can make with wlroots:

extern crate wlroots;

use wlroots::{compositor, utils::log::{WLR_DEBUG, init_logging}};

fn main() {
    init_logging(WLR_DEBUG, None);
    compositor::Builder::new().build_auto(()).run()
}

This compositor is useless. In fact, it's dangerously useless. However it's also very instructive considering how short it is.

You can compile and run1 the above in any existing X11 window manager or Wayland compositor and it will run in a nested window.2 However if you run it in a separate TTY it will use the DRM backend. This is usually the backend that will be used when you're not testing the compositor. If you run this code on DRM, you can't escape the compositor. If you do this you will need to reboot to escape.

Because no callbacks were set up for the events the compositor will just keep running forever doing nothing. You can't even switch TTYs because that's a feature that the compositor needs to implement itself.

This example is a little silly, but it highlights just how much needs to be implemented -- our compositor can't even shut itself off.


1 If you are running on a system with systemd and have the feature enabled (it is by default) it should "just work". If not, you'll need set the setuid bit on the binary with chmod u+s.

2 This is a wlroots feature that is built into the build_auto function. It is very useful for debugging.

Analyzing the code

After explaining what each change gives us, I'll then explain what each line of code does.

At the end of each chapter there will be a list of suggestions and challenges which I suggest you at least read over if not try. They exist to encourage you to read through the wlroots-rs documentation and Wayland documentation in order to better familiarize yourself.

Logging setup


# #![allow(unused_variables)]
#fn main() {
    init_logging(WLR_DEBUG, None);
#}

This line is not strictly necessary for the compositor to run. wlroots (and wlroots-rs) prints a log message each time something interesting happens which is useful for debugging. In general, you should always have this line in your compositor.

The first parameter is the minimum level that is logged. The second parameter is an optional callback that will be called each time a message is logged.

You can log a message using this system by using the wlr_log! macro1. Here is an example:


# #![allow(unused_variables)]
#fn main() {
// It has the same syntax as println! or format!
wlr_log!(WLR_DEBUG, "This is an example {:?}", some_struct)
#}

The first parameter is the log level you want to log at. Any arguments after that are passed to format! with the format string being the second argument.


# #![allow(unused_variables)]
#fn main() {
    compositor::Builder::new().build_auto(()).run()
#}

This is the real meat of the program.

This creates a builder for a Compositor. There can only be one Compositor object per process2 . The builder is how Wayland globals and their callbacks are set up.

In this case no callbacks are set up the Compositor is just immediately built. When you build the compositor, just like you build any object in wlroots-rs, you need to give it user state. In this case, there is no state to store so you can just pass the unit type ().

Once the Compositor is set up then run can be called. This will put it in the main Wayland event loop listening for events and dispatching to the callbacks. It will keep running until wlroots::terminate is called. Since thi s is never call it in this compositor it won't happen until it's kill it via a signal.


1 Don't forget to import macros by prepending #[macro_use] to extern crate wlroots.

2 wlroots-rs is not designed to be thread safe with its objects. Most objects are !Send and !Sync.

Analyzing the Wayland protocol

This is the log output from our compositor when it's ran as a nested Wayland instance:

[wlroots-sys/wlroots/backend/wayland/backend.c:186] Creating wayland backend
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_data_device_manager v3
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_shm v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_linux_dmabuf_v1 v3
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_drm v2
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zxdg_output_manager_v1 v2
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_compositor v4
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_subcompositor v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zxdg_shell_v6 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: xdg_wm_base v2
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_shell v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwlr_layer_shell_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_tablet_manager_v2 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: gamma_control_manager v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwlr_gamma_control_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: orbital_screenshooter v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwlr_export_dmabuf_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: org_kde_kwin_server_decoration_manager v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: gtk_primary_selection_device_manager v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: org_kde_kwin_idle v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_idle_inhibit_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwlr_input_inhibit_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_input_method_manager_v2 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_text_input_manager_v3 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_virtual_keyboard_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwlr_screencopy_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zxdg_decoration_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_pointer_constraints_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wp_presentation v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_output v3
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_seat v6
[wlroots-sys/wlroots/backend/wayland/wl_seat.c:398] seat 0x7fb3a2235e10 offered pointer
[wlroots-sys/wlroots/backend/wayland/wl_seat.c:411] seat 0x7fb3a2235e10 offered keyboard
[wlroots-sys/wlroots/render/egl.c:149] Using EGL 1.4
[wlroots-sys/wlroots/render/egl.c:150] Supported EGL extensions: EGL_ANDROID_blob_cache EGL_ANDROID_native_fence_sync EGL_EXT_buffer_age EGL_EXT_create_context_robustness EGL_EXT_image_dma_buf_import EGL_EXT_image_dma_buf_import_modifiers EGL_EXT_swap_buffers_with_damage EGL_IMG_context_priority EGL_KHR_config_attribs EGL_KHR_create_context EGL_KHR_create_context_no_error EGL_KHR_fence_sync EGL_KHR_get_all_proc_addresses EGL_KHR_gl_colorspace EGL_KHR_gl_renderbuffer_image EGL_KHR_gl_texture_2D_image EGL_KHR_gl_texture_3D_image EGL_KHR_gl_texture_cubemap_image EGL_KHR_image_base EGL_KHR_no_config_context EGL_KHR_reusable_sync EGL_KHR_surfaceless_context EGL_KHR_swap_buffers_with_damage EGL_EXT_pixel_format_float EGL_KHR_wait_sync EGL_MESA_configless_context EGL_MESA_drm_image EGL_MESA_image_dma_buf_export EGL_WL_bind_wayland_display EGL_WL_create_wayland_buffer_from_image 
[wlroots-sys/wlroots/render/egl.c:151] EGL vendor: Mesa Project
[wlroots-sys/wlroots/render/egl.c:97] Supported dmabuf buffer formats: AR30 XR30 AB30 XB30 AR24 AB24 XR24 XB24 AR15 RG16 R8   R16  GR88 GR32 YUV9 YU11 YU12 YU16 YU24 YVU9 YV11 YV12 YV16 YV24 NV12 NV16 YUYV UYVY 
[wlroots-sys/wlroots/render/gles2/renderer.c:553] Using OpenGL ES 3.2 Mesa 18.3.1
[wlroots-sys/wlroots/render/gles2/renderer.c:554] GL vendor: Intel Open Source Technology Center
[wlroots-sys/wlroots/render/gles2/renderer.c:555] Supported GLES2 extensions: GL_EXT_blend_minmax GL_EXT_multi_draw_arrays GL_EXT_texture_filter_anisotropic GL_EXT_texture_compression_s3tc GL_EXT_occlusion_query_boolean GL_EXT_texture_compression_dxt1 GL_EXT_texture_format_BGRA8888 GL_OES_compressed_ETC1_RGB8_texture GL_OES_depth24 GL_OES_element_index_uint GL_OES_fbo_render_mipmap GL_OES_mapbuffer GL_OES_rgb8_rgba8 GL_OES_standard_derivatives GL_OES_stencil8 GL_OES_texture_3D GL_OES_texture_float GL_OES_texture_float_linear GL_OES_texture_half_float GL_OES_texture_half_float_linear GL_OES_texture_npot GL_OES_vertex_half_float GL_EXT_texture_sRGB_decode GL_OES_EGL_image GL_OES_depth_texture GL_OES_packed_depth_stencil GL_EXT_texture_type_2_10_10_10_REV GL_OES_get_program_binary GL_APPLE_texture_max_level GL_EXT_discard_framebuffer GL_EXT_read_format_bgra GL_EXT_frag_depth GL_NV_fbo_color_attachments GL_OES_EGL_image_external GL_OES_EGL_sync GL_OES_vertex_array_object GL_OES_viewport_array GL_ANGLE_texture_compression_dxt3 GL_ANGLE_texture_compression_dxt5 GL_EXT_robustness GL_EXT_texture_rg GL_EXT_unpack_subimage GL_NV_draw_buffers GL_NV_read_buffer GL_NV_read_depth GL_NV_read_depth_stencil GL_NV_read_stencil GL_EXT_draw_buffers GL_EXT_map_buffer_range GL_KHR_debug GL_KHR_robustness GL_KHR_texture_compression_astc_ldr GL_OES_depth_texture_cube_map GL_OES_required_internalformat GL_OES_surfaceless_context GL_EXT_color_buffer_float GL_EXT_separate_shader_objects GL_EXT_shader_framebuffer_fetch GL_EXT_shader_integer_mix GL_EXT_tessellation_point_size GL_EXT_tessellation_shader GL_INTEL_conservative_rasterization GL_INTEL_performance_query GL_ANDROID_extension_pack_es31a GL_EXT_base_instance GL_EXT_compressed_ETC1_RGB8_sub_texture GL_EXT_copy_image GL_EXT_draw_buffers_indexed GL_EXT_draw_elements_base_vertex GL_EXT_gpu_shader5 GL_EXT_polygon_offset_clamp GL_EXT_primitive_bounding_box GL_EXT_render_snorm GL_EXT_shader_io_blocks GL_EXT_texture_border_clamp GL_EXT_texture_buffer GL_EXT_texture_cube_map_array GL_EXT_texture_norm16 GL_KHR_blend_equation_advanced GL_KHR_blend_equation_advanced_coherent GL_KHR_context_flush_control GL_KHR_robust_buffer_access_behavior GL_NV_image_formats GL_OES_copy_image GL_OES_draw_buffers_indexed GL_OES_draw_elements_base_vertex GL_OES_gpu_shader5 GL_OES_primitive_bounding_box GL_OES_sample_shading GL_OES_sample_variables GL_OES_shader_io_blocks GL_OES_shader_multisample_interpolation GL_OES_tessellation_point_size GL_OES_tessellation_shader GL_OES_texture_border_clamp GL_OES_texture_buffer GL_OES_texture_cube_map_array GL_OES_texture_stencil8 GL_OES_texture_storage_multisample_2d_array GL_OES_texture_view GL_EXT_blend_func_extended GL_EXT_buffer_storage GL_EXT_geometry_point_size GL_EXT_geometry_shader GL_EXT_shader_samples_identical GL_KHR_no_error GL_KHR_texture_compression_astc_sliced_3d GL_NV_fragment_shader_interlock GL_OES_EGL_image_external_essl3 GL_OES_geometry_point_size GL_OES_geometry_shader GL_OES_shader_image_atomic GL_EXT_clip_cull_distance GL_EXT_disjoint_timer_query GL_MESA_shader_integer_functions GL_EXT_shader_framebuffer_fetch_non_coherent GL_MESA_framebuffer_flip_y 
[src/compositor.rs:434] Running compositor on wayland display wayland-2
[src/compositor.rs:497] Starting compositor
[wlroots-sys/wlroots/backend/wayland/backend.c:104] Initializating wayland backend
[GLES2] FS SIMD8 shader: 5 inst, 0 loops, 24 cycles, 0:0 spills:fills, Promoted 0 constants, compacted 80 to 48 bytes.
[GLES2] FS SIMD16 shader: 5 inst, 0 loops, 34 cycles, 0:0 spills:fills, Promoted 0 constants, compacted 80 to 48 bytes.
[GLES2] VS SIMD8 shader: 28 inst, 0 loops, 116 cycles, 0:0 spills:fills, Promoted 0 constants, compacted 448 to 336 bytes.
[GLES2] FS SIMD16 shader: 2 inst, 0 loops, 0 cycles, 0:0 spills:fills, Promoted 0 constants, compacted 32 to 32 bytes.
 

Your output will probably not match exactly, but it should roughly have this output.

Backend Setup

When build_auto is called on Compositor it will dynamically detect which backend makes the most sense to spin up. If the compositor is ran in X11 or a Wayland compositor then it will run as a client with all the contents rendered to a window. If ran on a TTY then it uses the kernel's DRM module. It also possible to specify a backend directly.

[wlroots-sys/wlroots/backend/wayland/backend.c:186] Creating wayland backend

This first line shows which backend was selected. The Wayland backend was selected here, because it was ran in another Wayland instance.1

Wayland globals

[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_data_device_manager v3
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_shm v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_linux_dmabuf_v1 v3
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_drm v2
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zxdg_output_manager_v1 v2
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_compositor v4
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_subcompositor v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zxdg_shell_v6 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: xdg_wm_base v2
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_shell v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwlr_layer_shell_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_tablet_manager_v2 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: gamma_control_manager v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwlr_gamma_control_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: orbital_screenshooter v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwlr_export_dmabuf_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: org_kde_kwin_server_decoration_manager v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: gtk_primary_selection_device_manager v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: org_kde_kwin_idle v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_idle_inhibit_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwlr_input_inhibit_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_input_method_manager_v2 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_text_input_manager_v3 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_virtual_keyboard_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwlr_screencopy_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zxdg_decoration_manager_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: zwp_pointer_constraints_v1 v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wp_presentation v1
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_output v3
[wlroots-sys/wlroots/backend/wayland/backend.c:65] Remote wayland global: wl_seat v6

This is a list of all the globals that the parent compositor was advertising. This is output is specific to the Wayland backend. Protocols that are unstable have their names prepended with z by convention.

Our compositor also exposes some globals even in this minimal state. Globals are the way clients can start up communication with the Wayland compositor. There are some default ones that come bundled with Wayland, such as wl_compositor, and then there are custom ones defined on a per compositor basis. wlroots comes with some popular custom protocols already implemented, but you have to explicitly opt in to using them explicitly in the builder. xdg shell, for example, is an optional protocol that wasn't used in this example.

In order to see what globals the toy compositor is advertising you need to use a useful Wayland utility called weston-info.2 It lists the Wayland globals advertised by the current compositor. The current compositor is determined by looking at the $WAYLAND_DISPLAY environment variable similar to how in X the current xserver is determined with $DISPLAY.

In the log output it prints out the $WAYLAND_DISPLAY:

[src/compositor.rs:434] Running compositor on wayland display wayland-2

Lets see what globals are being advertised by our compositor:

interface: 'wl_compositor', version: 4, name: 1
interface: 'wl_subcompositor', version: 1, name: 2

weston-info prints out the name of each exposed interface on a new line along with the highest advertised interface version and the relative, atomically increasing index (confusingly prepended with "name").

wl_compositor and wl_subcompositor are both standard Wayland interfaces. wl_subcompositor is automatically started by wlroots when a wl_compositor is used. A wl_compositor is the base global that all clients depend on. From this global a client can create a wl_surface and a wl_region.

A wl_surface is a basic building block for drawing and rendering contents to the screen in Wayland. A client needs more than a wl_surface in order to render to the screen, but that is the basic object a compositor needs in order to render.3

A wl_region is the object that allows clients to tell the compositor where, in surface level coordinates4, it wants to handle input and where it is rendering content. Where it wants input is very important, but the default is that it accepts input everywhere in the surface. Specifying an area where the client is rendering content is important because it allows the compositor to know that any content behind that doesn't need to be redrawn. As a very simple example of this if there is a moving background on the screen and there is a fullscreen window then there is no need to draw the background saving precious cycles.

The ability to specify only parts of the screen to update is a major feature of Wayland which will be totally ignored until a much later chapter. When starting out it's simple enough to simply redraw the entire screen each time a new frame is available. For non-toy compositors though it is vital that proper damage tracking (as the feature is called) is implemented. It reduces power consumption and makes the compositor faster.

Seat offerings

[wlroots-sys/wlroots/backend/wayland/wl_seat.c:398] seat 0x7fb3a2235e10 offered pointer
[wlroots-sys/wlroots/backend/wayland/wl_seat.c:411] seat 0x7fb3a2235e10 offered keyboard

Rootson automatically offers the keyboard and mouse to all new windows that appear. This allows input to passthrough directly to the toy compositor, but it also hints at this concept of Wayland "seats".

A Wayland seat is a collection of inputs devices usually handled under the hood by libinput. Seats are created by the compositor, advertised to any new clients including when new input methods are added, and are used to facilitate user input to clients including drag-in-drop.

Seats are necessary to communicate properly with clients and will be explored in a later chapter.

EGL Setup

[wlroots-sys/wlroots/render/egl.c:149] Using EGL 1.4
[wlroots-sys/wlroots/render/egl.c:150] Supported EGL extensions: EGL_ANDROID_blob_cache EGL_ANDROID_native_fence_sync EGL_EXT_buffer_age EGL_EXT_create_context_robustness EGL_EXT_image_dma_buf_import EGL_EXT_image_dma_buf_import_modifiers EGL_EXT_swap_buffers_with_damage EGL_IMG_context_priority EGL_KHR_config_attribs EGL_KHR_create_context EGL_KHR_create_context_no_error EGL_KHR_fence_sync EGL_KHR_get_all_proc_addresses EGL_KHR_gl_colorspace EGL_KHR_gl_renderbuffer_image EGL_KHR_gl_texture_2D_image EGL_KHR_gl_texture_3D_image EGL_KHR_gl_texture_cubemap_image EGL_KHR_image_base EGL_KHR_no_config_context EGL_KHR_reusable_sync EGL_KHR_surfaceless_context EGL_KHR_swap_buffers_with_damage EGL_EXT_pixel_format_float EGL_KHR_wait_sync EGL_MESA_configless_context EGL_MESA_drm_image EGL_MESA_image_dma_buf_export EGL_WL_bind_wayland_display EGL_WL_create_wayland_buffer_from_image 
[wlroots-sys/wlroots/render/egl.c:151] EGL vendor: Mesa Project
[wlroots-sys/wlroots/render/egl.c:97] Supported dmabuf buffer formats: AR30 XR30 AB30 XB30 AR24 AB24 XR24 XB24 AR15 RG16 R8   R16  GR88 GR32 YUV9 YU11 YU12 YU16 YU24 YVU9 YV11 YV12 YV16 YV24 NV12 NV16 YUYV UYVY 
[wlroots-sys/wlroots/render/gles2/renderer.c:553] Using OpenGL ES 3.2 Mesa 18.3.1
[wlroots-sys/wlroots/render/gles2/renderer.c:554] GL vendor: Intel Open Source Technology Center
[wlroots-sys/wlroots/render/gles2/renderer.c:555] Supported GLES2 extensions: GL_EXT_blend_minmax GL_EXT_multi_draw_arrays GL_EXT_texture_filter_anisotropic GL_EXT_texture_compression_s3tc GL_EXT_occlusion_query_boolean GL_EXT_texture_compression_dxt1 GL_EXT_texture_format_BGRA8888 GL_OES_compressed_ETC1_RGB8_texture GL_OES_depth24 GL_OES_element_index_uint GL_OES_fbo_render_mipmap GL_OES_mapbuffer GL_OES_rgb8_rgba8 GL_OES_standard_derivatives GL_OES_stencil8 GL_OES_texture_3D GL_OES_texture_float GL_OES_texture_float_linear GL_OES_texture_half_float GL_OES_texture_half_float_linear GL_OES_texture_npot GL_OES_vertex_half_float GL_EXT_texture_sRGB_decode GL_OES_EGL_image GL_OES_depth_texture GL_OES_packed_depth_stencil GL_EXT_texture_type_2_10_10_10_REV GL_OES_get_program_binary GL_APPLE_texture_max_level GL_EXT_discard_framebuffer GL_EXT_read_format_bgra GL_EXT_frag_depth GL_NV_fbo_color_attachments GL_OES_EGL_image_external GL_OES_EGL_sync GL_OES_vertex_array_object GL_OES_viewport_array GL_ANGLE_texture_compression_dxt3 GL_ANGLE_texture_compression_dxt5 GL_EXT_robustness GL_EXT_texture_rg GL_EXT_unpack_subimage GL_NV_draw_buffers GL_NV_read_buffer GL_NV_read_depth GL_NV_read_depth_stencil GL_NV_read_stencil GL_EXT_draw_buffers GL_EXT_map_buffer_range GL_KHR_debug GL_KHR_robustness GL_KHR_texture_compression_astc_ldr GL_OES_depth_texture_cube_map GL_OES_required_internalformat GL_OES_surfaceless_context GL_EXT_color_buffer_float GL_EXT_separate_shader_objects GL_EXT_shader_framebuffer_fetch GL_EXT_shader_integer_mix GL_EXT_tessellation_point_size GL_EXT_tessellation_shader GL_INTEL_conservative_rasterization GL_INTEL_performance_query GL_ANDROID_extension_pack_es31a GL_EXT_base_instance GL_EXT_compressed_ETC1_RGB8_sub_texture GL_EXT_copy_image GL_EXT_draw_buffers_indexed GL_EXT_draw_elements_base_vertex GL_EXT_gpu_shader5 GL_EXT_polygon_offset_clamp GL_EXT_primitive_bounding_box GL_EXT_render_snorm GL_EXT_shader_io_blocks GL_EXT_texture_border_clamp GL_EXT_texture_buffer GL_EXT_texture_cube_map_array GL_EXT_texture_norm16 GL_KHR_blend_equation_advanced GL_KHR_blend_equation_advanced_coherent GL_KHR_context_flush_control GL_KHR_robust_buffer_access_behavior GL_NV_image_formats GL_OES_copy_image GL_OES_draw_buffers_indexed GL_OES_draw_elements_base_vertex GL_OES_gpu_shader5 GL_OES_primitive_bounding_box GL_OES_sample_shading GL_OES_sample_variables GL_OES_shader_io_blocks GL_OES_shader_multisample_interpolation GL_OES_tessellation_point_size GL_OES_tessellation_shader GL_OES_texture_border_clamp GL_OES_texture_buffer GL_OES_texture_cube_map_array GL_OES_texture_stencil8 GL_OES_texture_storage_multisample_2d_array GL_OES_texture_view GL_EXT_blend_func_extended GL_EXT_buffer_storage GL_EXT_geometry_point_size GL_EXT_geometry_shader GL_EXT_shader_samples_identical GL_KHR_no_error GL_KHR_texture_compression_astc_sliced_3d GL_NV_fragment_shader_interlock GL_OES_EGL_image_external_essl3 GL_OES_geometry_point_size GL_OES_geometry_shader GL_OES_shader_image_atomic GL_EXT_clip_cull_distance GL_EXT_disjoint_timer_query GL_MESA_shader_integer_functions GL_EXT_shader_framebuffer_fetch_non_coherent GL_MESA_framebuffer_flip_y 
...
[GLES2] FS SIMD8 shader: 5 inst, 0 loops, 24 cycles, 0:0 spills:fills, Promoted 0 constants, compacted 80 to 48 bytes.
[GLES2] FS SIMD16 shader: 5 inst, 0 loops, 34 cycles, 0:0 spills:fills, Promoted 0 constants, compacted 80 to 48 bytes.
[GLES2] VS SIMD8 shader: 28 inst, 0 loops, 116 cycles, 0:0 spills:fills, Promoted 0 constants, compacted 448 to 336 bytes.
[GLES2] FS SIMD16 shader: 2 inst, 0 loops, 0 cycles, 0:0 spills:fills, Promoted 0 constants, compacted 32 to 32 bytes.

Currently all backends need a renderer in wlroots which is automatically setup when you create one. This output is from the Wayland backend setting up the EGL drawing for rendering. In the future this may change, as the rendering API is in flux.

Everything after run is called

[src/compositor.rs:434] Running compositor on wayland display wayland-2
[src/compositor.rs:497] Starting compositor
[wlroots-sys/wlroots/backend/wayland/backend.c:104] Initializating wayland backend

Everything after these lines, including these lines, is printed to the log after run is called. Since there are no clients that connected there is no logging from them and since there are no event handlers nothing else happens.


1 On my machine it was ran in rootson, the wlroots reference compositor, which is why Wayland was selected.

2 In most Linux distributions this utility is packaged along with weston, the reference Wayland compositor.

3 Usually a surface is wrapped in a shell. What a shell adds to a wl_surface is context. Without the proper context a compositor doesn't know if the surface it was just handed by the client is a standalone window, a popup, a background, a status bar, or a cursor to be rendered. All of them need to be handled differently and they are all handled using a dedicated wayland "shell" or a specialized non-shell protocol.

4 It has to be surface level because clients doesn't know about anything but the content it renders.

Exercises

Russian Doll Compositor

You can trick a Wayland compositor to run as the child of a compositor you're not currently in by overriding the WAYLAND_DISPLAY variable.

Using this, get the toy compositor to run inside the toy compositor.

Reimplement build_auto

Explore some of the options for the compositor::Builder.

Reimplement build_auto using the explicit build functions.

Use $DISPLAY to detect when running nested in X11 and $WAYLAND_DISPLAY to detect running nested in Wayland.

Goodbye World

The compositor from the previous section has a bug: it can't be exited from if it is started in DRM. This is a pretty serious bug, one that will be addressed in two ways in this section.

Gracefully shutting Down

The first is the most extreme, and easiest to implement, option: adding a shut down key sequence. The compositor will be configured so if the user presses Ctrl+Shift+Escape it will gracefully terminate with a zero exit status. This will be useful in debugging the compositor as it makes it easy to shut down even in DRM.1

TTY switching

The second escape access is a feature that is often taken for granted: the ability to switch TTYs. The standard Ctrl+Alt+F# sequence will be implemented to switch TTYs when the compositor is running on DRM. When it's running on another backend it will simply ignore that (since it won't have the proper access controls to do the context switch).

What you'll learn

This chapter will primarily concern itself with setting up handlers for the first time, handling keyboard input, and learning to use wlroots-rs handles.

A graceful shutdown

Before the compositor can begin listening for keyboard input it needs to listen for keyboards.

For each resource type that can be created there is a manager module that provides a builder and some function signatures for the compositor writer to describe how a resource should be managed. Here is the input device resource manager module.

To specify that a keyboard should be managed by the compositor a function needs to be defined matching the keyboard resource manager signature. This function will be later called by wlroots when a keyboard is announced to the compositor through libinput.

Once the function is defined with the necessary signature it needs to be put into the resource builder and the resource builder passed to the compositor::Builder.

A Minimal Keyboard Handler

This is the simplest function that implements the signature:


# #![allow(unused_variables)]
#fn main() {
fn keyboard_added(_compositor_handle: compositor::Handle,
                  _keyboard_handle: keyboard::Handle)
                  -> Option<Box<keyboard::Handler>> {
    None
}
#}

This is how it's passed to the builder:

fn main() {
    init_logging(WLR_DEBUG, None);
    let input_builder = input::manager::Builder::default()
        .keyboard_added(keyboard_added);
    compositor::Builder::new()
        .input_manager(input_builder)
        .build_auto(())
        .run()
}

With the provided implementation whenever a keyboard is announced wlroots-rs will call keyboard_added. Since the function unconditionally returns None a keyboard resource handler will never be allocated and the resource will be dropped.

Holding on to the resource

In order to hang on to the resource a handler must be defined and allocated using Box to make a trait object. The handler defines how to deal with event the resource can trigger, including when a key is pressed.

Since a resource handler is a trait object each resource handler has a piece of state it holds between callbacks separate from the other resources. It is here where the "shift" and "ctrl" pressed state will be held:


# #![allow(unused_variables)]
#fn main() {
#[derive(Default)]
struct KeyboardHandler {
    shift_pressed: bool,
    ctrl_pressed: bool
}
#}

In order to be able to return a Box-ed version of this struct in the keyboard_added function keyboard::Handler will need to be implemented:


# #![allow(unused_variables)]
#fn main() {
fn keyboard_added(_compositor_handle: compositor::Handle,
                  _keyboard_handle: keyboard::Handle)
                  -> Option<Box<keyboard::Handler>> {
    Some(Box::new(KeyboardHandler::default()))
}

impl keyboard::Handler for KeyboardHandler {
    // All handler methods have a default implementation that does nothing.
    // So because no methods are define here, every event on the keyboard
    // is ignored.
}
#}

Listening for keyboard modifiers

When a key is pressed this method receives the event. The key event has a couple methods but the most important one is pressed_keys. It will provide all the keys as seen by xkb that were pressed when the event fired.

You can also see if the key was pressed or not with key_state. This is necessary to determine the boolean state in KeyboardHandler.

Using the keysyms module from the reexported xkbcommon crate the list of keys can be iterated over and pattern matched. Here is all that put together to toggle the booleans when the appropriate keys are pressed:


# #![allow(unused_variables)]
#fn main() {
impl keyboard::Handler for KeyboardHandler {
    fn on_key(&mut self,
              compositor_handle: compositor::Handle,
              _keyboard_handle: keyboard::Handle,
              key_event: &keyboard::event::Key) {
        for key in key_event.pressed_keys() {
            match key {
                keysyms::KEY_Control_L | keysyms::KEY_Control_R =>
                    self.ctrl_pressed = key_event.key_state() == WLR_KEY_PRESSED,
                keysyms::KEY_Shift_L | keysyms::KEY_Shift_R =>
                    self.shift_pressed = key_event.key_state() == WLR_KEY_PRESSED,
                _ => { /* Do nothing */ }
            }
        }
    }
}
#}

The last piece of the puzzle to stopping the compositor is the terminate function. It can be called at any time and will gracefully kill clients, destroy resource managers, and then wind back up the stack to where run was called.

Here is the complete code for a compositor that will close itself when Ctrl+Shift+Escape is pressed:

extern crate wlroots;

use wlroots::{compositor,
              input::{self, keyboard},
              utils::log::{WLR_DEBUG, init_logging},
              xkbcommon::xkb::keysyms,
              wlr_key_state::WLR_KEY_PRESSED};

fn main() {
    init_logging(WLR_DEBUG, None);
    let input_builder = input::manager::Builder::default()
        .keyboard_added(keyboard_added);
    compositor::Builder::new()
        .input_manager(input_builder)
        .build_auto(())
        .run()
}

fn keyboard_added(_compositor_handle: compositor::Handle,
                  _keyboard_handle: keyboard::Handle)
                  -> Option<Box<keyboard::Handler>> {
    Some(Box::new(KeyboardHandler::default()))
}

#[derive(Default)]
struct KeyboardHandler {
    shift_pressed: bool,
    ctrl_pressed: bool
}

impl keyboard::Handler for KeyboardHandler {
    fn on_key(&mut self,
              compositor_handle: compositor::Handle,
              _keyboard_handle: keyboard::Handle,
              key_event: &keyboard::event::Key) {
        for key in key_event.pressed_keys() {
            match key {
                keysyms::KEY_Control_L | keysyms::KEY_Control_R =>
                    self.ctrl_pressed = key_event.key_state() == WLR_KEY_PRESSED,
                keysyms::KEY_Shift_L | keysyms::KEY_Shift_R =>
                    self.shift_pressed = key_event.key_state() == WLR_KEY_PRESSED,
                keysyms::KEY_Escape => {
                    if self.shift_pressed && self.ctrl_pressed {
                        wlroots::compositor::terminate()
                    }
                },
                _ => { /* Do nothing */ }
            }
        }
    }
}

1 Huge caveat to this: if the system is deadlocked, such as by an innocuous loop {}, then the compositor can no longer process input including the escape sequence. Either test all of your features in a nested instance (where input can still be processed by the parent system) or have ssh as a backup to pkill the process.

Switching TTYs

Implementing the ability to switch TTYs is not much more difficult once the relevant function is located on the Session struct. However getting to that struct from the callback requires explaining wlroot-rs handles.

Handles in wlroots-rs

Handles represent the wlroots-rs solution to the complicated lifetimes of Wayland resources.

In Rust normally you can either own a value or borrow it for some lifetime. However, you can't "own" a keyboard because you don't control its lifetime. At any point, for example, the keyboard could be yanked out by the user and then it will need to be cleaned up.

You also can't have these be defined via lifetimes on borrows because lifetimes behave like a compile-time read-write lock on data. That does mean, however, that there can be callbacks that takes a borrow, for example:


# #![allow(unused_variables)]
#fn main() {
/// Callback for when a key is pressed
fn on_key(keyboard: &Keyboard) {
    // A Keyboard will be valid here because wlroots is single threaded.
}
#}

Unfortunately, now that resource can't escape the callback. Often these resources will want to be used beyond this limited scope.

To solve this a Handle is used to refer indirectly to resources. Handles are essentially thin wrappers around Weak smart pointers. They can only be accessed in callbacks by calling run on them, which performs additional safety checks to ensure the Handle is valid.

In order to make using handles easier there are also two macros that make them much easier to use: with_handles and wlroots_dehandle. Either, or neither, can be used. They are only implemented as a convenience. However, they will be used later in this book so you should learn them now.

Please read the handle documentation in order to better understand Handles.

Accessing the Session from the Compositor

A Session is obtained from a Backend. A Backend can be obtained from a &Compositor. To get a reference to the Compositor the compositor_handle must be upgraded.

When you upgrade a handle it can potentially fail according to its signature. The possible error values indicate the two requirements for upgrading a handle are:

  1. Two handles to the same resource can not be upgraded at the same time. If this were allowed there could be two mutable references to the same resource which is against Rust's memory model.
  2. If the resource behind the handle has been destroyed then the handle can never be upgraded again.1

Because these errors should not occur for the compositor handle, it is sufficient to simply unwrap the result:


# #![allow(unused_variables)]
#fn main() {
impl keyboard::Handler for KeyboardHandler {
    fn on_key(&mut self,
              compositor_handle: compositor::Handle,
              _keyboard_handle: keyboard::Handle,
              key_event: &keyboard::event::Key) {
        for key in key_event.pressed_keys() {
            match key {
                keysyms::KEY_XF86Switch_VT_1 ..= keysyms::KEY_XF86Switch_VT_12 => {
                    compositor_handle.run(|compositor| {
                        let backend = compositor.backend_mut();
                        if let Some(mut session) = backend.get_session() {
                            session.change_vt(key - keysyms::KEY_XF86Switch_VT_1 + 1);
                        }
                    }).unwrap();
                }
                _ => { /* Do nothing */ }
            }
        }
    }
}
#}

1 In this case the Compositor lives for the life of the compositor, so it will never be AlreadyDropped. This is not the case for other resources, such as keyboards.

Exercises

Sharing keyboard state

Because the ctrl and shift booleans are implemented on the KeyboardHandler each keyboard gets its own state. That means if you have multiple keyboards plugged in then the key combination must all be done on the same keyboard.

Modify the compositor to not have this limitation.

Hint: Instead of a global, try replacing the state passed to the compositor::Builder. The compositor can be downcasted to this state.