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:
- 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.
- 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.
- 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 aroundWeak
smart pointers. They can only be accessed in callbacks by callingrun
on them, which performs additional safety checks to ensure theHandle
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,
wlroots_dehandle
will be used later in this book since it is the most convenient way to use handles. So please read its documentation.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:
- 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.
- 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.
Getting to the Point
Now that the compositor can be started and stopped successfully, it's about time that it actually starts doing what it's designed to do: composite some stuff.
There are, generally speaking, two types of resources the compositor will render: contents of buffers owned by the compositor and contents of buffers owned by clients.
Client provided buffers are more complicated to handle in general. They must synchronize their buffers with the compositor so that it can render without screen tearing. Clients can also damage only part of their buffers to reduce the amount of redrawing per frame. Because of this complexity, clients will be dealt with in a later chapter.
Between different compositors the amount of compositor-owned buffers varies. For example, some compositors render their own background, status bars, and window decorations whilst others leave all that up to clients. Neither is more right or wrong than the other, but each leads to certain design trade-offs.
However, at least among desktop compositors, there is at least one compositor owned object they all generally have to render: the cursor.
As simple as that sounds, it's actually very complicated rendering a cursor correctly. Thankfully wlroots makes it much easier.
Suggested reading for an in-depth dive on how input handling works in wlroots.
A basic cursor
There are two main problems that need to be solved to handle a single cursor on the screen:1
- Keeping track of where the mouse is.
- Rendering the mouse where it is.
The first problem can be solved with two numbers and an event listener. The second can be solved with xcursor.
Keeping track of the mouse
In order to keep track of the mouse a new input device will need to be managed by the compositor. For this purpose wlroots provides a Pointer. It abstracts over all types of common mouse input2, courtesy of libinput.
Like Keyboard, a Pointer is instantiated using the input builder:
struct PointerHandler; impl pointer::Handler for PointerHandler { // By default, all events are ignored } fn pointer_added(_compositor_handle: compositor::Handle, _pointer_handle: pointer::Handle) -> Option<Box<pointer::Handler>> { Some(Box::new(PointerHandler)) } fn main() { let input_builder = wlroots::input::manager::Builder::default() .pointer_added(pointer_added) // Other setup elided... }
The event that is emitted when the mouse is moved is the motion
event.
This event is provided in the pointer::Handler::on_motion
callback.
This event provides deltas corresponding to the movement amount. By keeping a
running sum, the absolute position of the mouse, in output coordinates, can be
determined.
Different coordinate types
Throughout this book different coordinate types will be used. Each coordinate is a number representing a position inside some viewport.
The main types of coordinates used are:
- Output coordinates
- Output layout coordinates
- View coordinates
They are generally distinguished in the docs and code by prepending a letter to the variable name. For example
lx
is the x position in relation to the output layout.The origin point will always be in the top left corner.
The coordinates can be stored in the PointerHandler
and updated on each event:
# #![allow(unused_variables)] #fn main() { struct PointerHandler { /// The x coordinate in relation to the output. ox: f64, /// The y coordinate in relation to the output. oy: f64 } impl pointer::Handler for PointerHandler { fn on_motion(&mut self, compositor_handle: compositor::Handle, _pointer_handle: pointer::Handle, motion_event: &pointer::event::Motion) { let (delta_x, delta_y) = motion_event.delta(); self.x += delta_x; self.y += delta_y; } } #}
Rendering a mouse with xcursor
Now that the mouse position can be tracked it's time to render it to the screen.
The xcursor library doesn't render anything itself, it just provides images from the system. In a typical desktop environment the cursor changes its icon depending on what's under it, which requires a manager to keep track of all these types of images.
In fact, in Wayland clients can dictate what the cursor looks like. When a client is receiving input from the mouse it can provide its own cursor image. Though the compositor is not obligated to use this mouse, it is common to do so.
An xcursor
theme can
be
loaded
at the start of the program and stored in the CompositorState
.3
Now that the image has been obtained there needs to be something to render it onto. Thus far the compositor has not been aware of any outputs, beyond the auto detection it does during backend setup.
Outputs
An Output represents a rectangular view port on which clients and other content are rendered. Generally this means a monitor plugged into the computer, though if the Wayland or X11 backends are used then it will instead be a window as a client to the host system.
Setting up an output is done in the same as setting up an input. There is only one crucial difference: when setting up an output there needs to be a mode set for the output using the builder passed into the function.
struct OutputHandler; impl output::Handler for OutputHandler {} fn output_added<'output>(compositor: compositor::Handle, builder: output::Builder<'output>) -> Option<output::BuilderResult<'output>> { Some(builder.build_best_mode(OutputHandler)) } fn main() { let output_builder = wlroots::output::manager::Builder::default() .output_added(output_added); let compositor = compositor::Builder::new() .gles2(true) .input_manager(input_builder) .output_manager(output_builder) }
Rendering is done in the on frame callback, however for cursors this is not necessary. wlroots provides a special output cursor which abstracts over rendering a cursor. This is because many backends support "hardware" cursors. This is a feature provided by GPUs that allow moving a cursor around the screen without redrawing anything underneath it.
If hardware cursors aren't supported the output::Cursor
will revert to using
software cursors automatically.
Using this new type this is a complete basic cursor implementation with wlroots:
struct OutputHandler; impl output::Handler for OutputHandler {} #[wlroots_dehandle] fn output_added<'output>(compositor: compositor::Handle, builder: output::Builder<'output>) -> Option<output::BuilderResult<'output>> { let result = builder.build_best_mode(OutputHandler); { #[dehandle] let compositor = compositor; #[dehandle] let output = &result.output; let state: &mut CompositorState = compositor.downcast(); let mut cursor = output::Cursor::new(output) .expect("Could not create output cursor"); let xcursor = state.theme.get_cursor("left_ptr".into()) .expect("Could not load default cursor set"); let image: wlroots::render::Image = xcursor.image(0) .expect("xcursor had no images").into(); cursor.set_image(&image) .expect("Could not set cursor image"); state.cursor = Some(cursor); } Some(result) } struct PointerHandler; impl pointer::Handler for PointerHandler { #[wlroots_dehandle] fn on_motion(&mut self, compositor_handle: compositor::Handle, _pointer_handle: pointer::Handle, motion_event: &pointer::event::Motion) { #[dehandle] let compositor = compositor_handle; let &mut CompositorState { ref mut cursor, .. } = compositor.downcast(); if let Some(cursor) = cursor.as_mut() { let (delta_x, delta_y) = motion_event.delta(); let (cur_x, cur_y) = cursor.coords(); cursor.move_relative(cur_x + delta_x, cur_y + delta_y) .expect("Could not move cursor"); } } } fn pointer_added(_compositor_handle: compositor::Handle, _pointer_handle: pointer::Handle) -> Option<Box<pointer::Handler>> { Some(Box::new(PointerHandler)) } struct CompositorState { theme: xcursor::Theme, cursor: Option<wlroots::output::Cursor> } fn main() { init_logging(WLR_DEBUG, None); let theme = xcursor::Theme::load_theme(None, 16) .expect("Could not create xcursor manager"); let output_builder = wlroots::output::manager::Builder::default() .output_added(output_added); let input_builder = wlroots::input::manager::Builder::default() .pointer_added(pointer_added) .keyboard_added(keyboard_added); let compositor = compositor::Builder::new() .gles2(true) .input_manager(input_builder) .output_manager(output_builder) .build_auto(CompositorState { theme, cursor: None }); compositor.run(); }
Box of the Socratic Teaching Style
Before continuing, I suggest you think for a moment on some complications or desirable features we ignored in this design. Try using the above example yourself and see if there's any bugs in it.
When considering features for your compositor, it's important to consider setups different from your own, which can help ensure your compositor is flexible enough to withstand the "real world".
Problems with this approach
Unfortunately, this is a very bad solution to the problem. One problem that's obvious if the above example is tried is that there are no bounds checks for when the cursor goes outside the output.
Another problem, which is more difficult to solve, is when multiple outputs are connected. When this happens only the last one gets a cursor and the others are inaccessible. This is because each output is its own buffer and have no relation to the others. So a relationship between outputs must be establish where it's possible to "move" to another output when the edge of another is reached.
Finally, this solution also doesn't address how to react to drawing tablets or touch screens.
These problems are all very complicated, not to mention very boring. In order to solve them wlroots provides two semi-connected abstractions: a Cursor and a Layout.
1 Note that in Wayland there are no restrictions on the number of cursors. Multiple cursors can be rendered at the same time and can be controlled by any number of other input devices (including fake ones).
2 Non-exhaustive list: common two/three button mice, multi-button mice, trackpoints, touchpads, and trackballs.
3 Generally a theme manager is used to generate these themes, but this will be ignored for now.
Setting up a Cursor
and an output::Layout
A Cursor
can be created at any
time.
Cursors use handles just like all resources provided in wlroots callbacks, so to
use the rest of the methods it must be upgraded.1
A Cursor
can be
attached
to an output::Layout
which will constrain the input to remain within the
region. The layout also keeps track of where the outputs are in relation to each
other, so when the cursor reaches the edge of two outputs it will automatically
warp to the next one.
An output::Layout
can be created just like a
Cursor
.
Here is the new compositor setup code that uses Cursor
, output::Layout
, and
xcursor::Manager
2:
pub struct CompositorState { xcursor_manager: wlroots::cursor::xcursor::Manager, cursor_handle: wlroots::cursor::Handle, output_layout_handle: wlroots::output::layout::Handle } fn main() { init_logging(WLR_DEBUG, None); let compositor_state = setup_compositor_state(); let output_builder = wlroots::output::manager::Builder::default() .output_added(output_added); let input_builder = wlroots::input::manager::Builder::default() .pointer_added(pointer_added) .keyboard_added(keyboard_added); let compositor = compositor::Builder::new() .input_manager(input_builder) .output_manager(output_builder) .build_auto(compositor_state); compositor.run(); } #[wlroots_dehandle] pub fn setup_compositor_state() -> CompositorState { use wlroots::{cursor::{Cursor, xcursor}, output::layout::Layout}; use crate::{pointer::CursorHandler, output::LayoutHandler}; let output_layout_handle = Layout::create(Box::new(LayoutHandler)); let cursor_handle = Cursor::create(Box::new(CursorHandler)); let xcursor_manager = xcursor::Manager::create("default".to_string(), 24) .expect("Could not create xcursor manager"); xcursor_manager.load(1.0); #[dehandle] let output_layout = output_layout_handle.clone(); #[dehandle] let cursor = cursor_handle.clone(); cursor.attach_output_layout(output_layout); CompositorState { xcursor_manager, cursor_handle, output_layout_handle } }
Using output::Layout
Outputs can be added to the layout with
Layout::add_auto
once they are advertised to the compositor:3 This will allow the
cursor to warp to the next output when the edge is reached between two outputs
in the output layout coordinate space.
# #![allow(unused_variables)] #fn main() { #[wlroots_dehandle] pub fn output_added<'output>(compositor: compositor::Handle, builder: output::Builder<'output>) -> Option<output::BuilderResult<'output>> { let result = builder.build_best_mode(OutputHandler); #[dehandle] let compositor = compositor; let CompositorState { ref output_layout_handle, .. } = compositor.downcast(); #[dehandle] let output = result.output.clone(); #[dehandle] let output_layout = output_layout_handle; output_layout.add_auto(output); Some(result) } #}
Moving the Pointer
There is no longer any need to keep track of the current pointer location. This
is tracked by the Cursor
and can be updated using move_relative
and warp
.
We also should update the cursor image when a pointer is added so that the correct state can be rendered.
Finally here is the code that updates the cursor when the pointer moves:
# #![allow(unused_variables)] #fn main() { pub struct PointerHandler; impl pointer::Handler for PointerHandler { /// Triggered when the pointer is moved on the Wayland and X11 backends. #[wlroots_dehandle] fn on_motion_absolute(&mut self, compositor_handle: compositor::Handle, _pointer_handle: pointer::Handle, absolute_motion_event: &pointer::event::AbsoluteMotion) { #[dehandle] let compositor = compositor_handle; let &mut CompositorState { ref cursor_handle, .. } = compositor.downcast(); #[dehandle] let cursor = cursor_handle; let (x, y) = absolute_motion_event.pos(); cursor.warp_absolute(absolute_motion_event.device(), x, y); } #[wlroots_dehandle] /// Triggered when the pointer is moved in the DRM backend. fn on_motion(&mut self, compositor_handle: compositor::Handle, _pointer_handle: pointer::Handle, motion_event: &pointer::event::Motion) { #[dehandle] let compositor = compositor_handle; let &mut CompositorState { ref cursor_handle, .. } = compositor.downcast(); #[dehandle] let cursor = cursor_handle; let (delta_x, delta_y) = motion_event.delta(); cursor.move_to(None, delta_x, delta_y); } } #[wlroots_dehandle] pub fn pointer_added(compositor_handle: compositor::Handle, pointer_handle: pointer::Handle) -> Option<Box<pointer::Handler>> { #[dehandle] let compositor = compositor_handle; #[dehandle] let pointer = pointer_handle; let CompositorState { ref cursor_handle, ref mut xcursor_manager, .. } = compositor.downcast(); #[dehandle] let cursor = cursor_handle; xcursor_manager.set_cursor_image("left_ptr".to_string(), cursor); cursor.attach_input_device(pointer.input_device()); Some(Box::new(PointerHandler) as Box<pointer::Handler>) } #}
1 Unlike most resources in wlroots, Cursor
lifetimes are entirely
dictated by your code. It will hang around until you destroy
it.
However, it acts like the other resources for consistency and because their
lifetimes are tied to other resources. There are two other types like this:
output::Layout
and Seat
.
2 Since the code is getting very long large parts of it will be elided going forward. The full source can always be found in the book repo.
3 Normally you'd want to add the output at a specific point in the layout. However this requires user configuration, which is out of the scope of this book. Currently there is no xrandr equivalent for Wayland.
Exercises
One Cursor Per Output
If you have multiple outputs lets do something a little different.
Instead of storing the cursor::Handle
in CompositorState
try storing it in
the OutputHandler
. This will give each output its own cursor. You should add a
keybinding to switch what the "current" one is.
Multiple Input Cursors
If you have multiple input devices hanging around then lets get a little crazy.
Instead of storing the cursor::Handle
in CompositorState
try storing it in
the PointerHandler
. This will allow each pointer to have its own, separate
cursor.
Configuring Outputs
If you have multiple outputs you probably noticed that the cursor can reach across all of them. However, it is probably not going across the correct edge since wlroots has no way to know how the monitors are physically set up in the world.
Using the non-auto functions in
output::Layout
,
and a configuration description of your choice make it possible for the user to
set up their outputs how they like.
Without endorsing any particular configuration format, it is suggested that you nevertheless use serde as that is the standard way in Rust to encode and decode arbitrary formats.