返回资讯中心
外部精选
软件工程
#开发者实时流

Welcome to Wlroots How to make a compositor from scratch

part two of the welcome to wlroots! series. if you haven’t yet, you should go read the part 1 of

Hacker News 最新nyeki2026/11/5 · 今天30 分钟阅读

以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。

Nov 4, 2026 • Nyeki • part two of the welcome to wlroots! series. if you haven’t yet, you should go read the part 1 of this series.

it has been some days since i last touched my compositor codebase, so we’ll start by checking out how make works to make it easy for us to be building the project. if you’re a c expert already and you’re comfortable with meson or other tool, feel free to use it instead. i’m using make because it’s the easiest for beginners (like me).

also, we’ll be using the clang compiler for this project. gcc should work just fine, yet i’ve heard clang has better error messages.

make files are shell scripts with rainbows and unicorns. compiling a c project usually includes a lot of chaining commands, which we’ll use make to do. make sure to have clang, make, and pkg-config installed in your system. you’ll also need wayland-server and wlroots-0.19. note that in ubuntu at least, wlroots is distributed as wlroots-0.19 and not as wlroots.

to begin with, create your project directory and put these things in it:

src/ main.c .gitignore makefile

in our main.c file, let’s put a basic main() to run our code.

int main(void) { return 0; }

then in our makefile, let’s write this (indented with tabs, spaces won’t work):

variable for all the clang flags we'll be using. it enables warnings, makes warnings fail to

compile, enables wlroots unstable features (wlroots has lots of things marked unstable), and

enables addresssanitizer to help us catch memory bugs.

cflags = -o build/compositor --std c23 -wall -wextra -wpedantic -werror -dwlr_use_unstable
-iwayland -g -o0 -fsanitize=address -fno-omit-frame-pointer

our build script, we just make an out dir and compile our main.c file.

build: mkdir -p build clang src/main.c $(cflags)

this tells make that build is a command and not a file we want to output.

.phony: build

if you run make or make build in your terminal, it should output a binary you can execute at build/compositor.

now that that’s done, time to start building our compositor!

every wayland and wlroots compositor has a wl_display ruling it all. it handles the event loop and incoming connections. let’s create one!

#include <assert.h> #include <wayland-server-core.h>

int main(void) { struct wl_display *wl_display = wl_display_create(); assert(wl_display);

return 0; }

that piece of code doesn’t compile just yet. that’s because we didn’t specify any dependencies in our makefile. let’s update it.

dependencies = wayland-server wlroots-0.19 dep_src_flags = $(shell pkg-config --cflags $(dependencies)) dep_lib_flags = $(shell pkg-config --libs $(dependencies))

flags = $(cflags) $(dep_src_flags) $(dep_lib_flags)

our build script, we just make an out dir and compile our main.c file.

build: mkdir -p build clang src/main.c $(flags)

we use pkg-config to get the compiler flags needed to compile using our dependencies. unlike other programming languages like rust or python, we don’t get a nice manifest with dependencies in there, rather point the compiler to where those dependencies are in our system. pkg-config lets us do that dynamically instead of hardcoding values.

back to our display, we’re not accepting any incoming connections yet, though, so let’s start it. wayland connections come in through a socket, usually at /run/user/1000/wayland-0 (replace 1000 with your linux uid). i’m on gnome shell, which means i’m already running a wayland compositor, so i’ll customize my socket’s name to way-nyeki.

const int socket = wl_display_add_socket(wl_display, "way-nyeki"); if (socket != 0) return 1;

now yes, time to start our compositor.

#define _posix_c_source 200112l // setenv() is posix-only, we enable it with this flag #include <stdlib.h> // -- snip --

int main(void) { // -- snip --

// this env var lets clients know what socket to use to connect to the wayland compositor. setenv("wayland_display", "way-nyeki", true); wl_display_run(wl_display);

// once the compositor is done running, disconnect everyone cleanly. wl_display_destroy_clients(wl_display); wl_display_destroy(wl_display); }

finally. we got a compositor that runs, let’s run it! make build in your terminal will yield your new compositor under build. paste build/compositor to run it, and…

it does nothing. but hey, it doesn’t crash!

compositors are useless without rendering something, so let’s get to rendering such something.

wlroots has a wlr_backend type which handles outputs and inputs. it supports 4 different backends: drm (directly to your screen), libinput (we won’t be touching inputs yet), wayland (rendering our wayland compositor inside another wayland compositor, as a window), and x11 (same as the wayland one but for x11).

wlroots can automatically choose the backend we’ll need, so we’ll just do that. it requires a reference to the event loop, so we’ll also grab it from our wl_display.

// -- snip -- #include <wlr/backend.h>

int main(void) { // -- snip -- assert(wl_display);

struct wl_event_loop *wl_event_loop = wl_display_get_event_loop(wl_display); assert(wl_event_loop);

// the null is a pointer to extra options. we don't need to pass any. struct wlr_backend *wlr_backend = wlr_backend_autocreate(wl_event_loop, null); assert(wlr_backend);

assert(wlr_backend_start(wlr_backend));

// -- snip -- }

now, before we keep going like this, our compositor state is growing big, so let’s make a type to hold it all.

struct nye_server { // wayland stuff struct wl_display *wl_display; struct wl_event_loop *wl_event_loop;

// wlroots stuff struct wlr_backend *wlr_backend; };

int main(void) { struct nye_server nye_server = {0};

nye_server.wl_display = wl_display_create(); assert(nye_server.wl_display);

nye_server.wl_event_loop = wl_display_get_event_loop(nye_server.wl_display); assert(nye_server.wl_event_loop);

// the null is a pointer to extra options. we don't need to pass any. nye_server.wlr_backend = wlr_backend_autocreate(nye_server.wl_event_loop, null); assert(nye_server.wlr_backend);

// -- snip -- }

also update all references to those variables. great.

now, before we can render something, there’s a topic i have to introduce: outputs. in your normal compositor-to-your-screen setup where the compositor is the one you’re actually using (and not a nested one like we currently have), each output usually corresponds to a monitor. since at the moment we’re running out compositor nested inside another compositor, each output will correspond to a window our compositor has within the wayland compositor.

our backend will fire an event for us to listen to when an output is created, and another one when that output is destroyed (window closed, monitor unplugged). wlroots keeps some track of this internally, yet we’ll also keep a reference to those output handles plus some state of ours to store extra state for our compositor.

the wayland library uses linked lists to store stuff, so we’ll use those too. wl_list is the type used for nodes within them. let’s create one for our outputs, and also a wrapper output type.

struct nye_server { // -- snip --

// our stuff struct wl_list nye_outputs; };

struct nye_output { struct wl_list link; // nye_server::nye_outputs

struct nye_server *nye_server; // easy access to our compositor state. struct wlr_output *wlr_output; };

now, let’s initialize our linked list.

int main(void) { // -- snip -- assert(nye_server.wlr_backend);

wl_list_init(&nye_server.nye_outputs); // -- snip -- }

great! that’s an empty list, though. we need to add our outputs when they appear, so let’s add an event listener for it.

event listeners in wayland are wrapped in wl_listner types. we’ll call ours on_{resource}_{action} (unless there’s no {action}) to make it clear which does what.

// -- snip -- #include <stdio.h>

struct nye_server { // -- snip --

struct wl_listener on_output_add; };

static void on_output_add(struct wl_listener *, void *) { printf("new output!\n"); }

int main(void) { // -- snip --

wl_list_init(&nye_server.nye_outputs); // set the wl_listener's callback to our new function. nye_server.on_output_add.notify = on_output_add; // hook our new listener to the backend's new_output event. wl_signal_add(&nye_server.wlr_backend->events.new_output, &nye_server.on_output_add);

// -- snip -- }

try running the compositor with make build && build/compositor. you’ll now see the following gets printed to the terminal:

new output!

that new output represents our wayland window just created.

now, the window is there but not rendering anything yet. it’s very possible that in a compositor like hyprland you don’t have a way of seeing it. in gnome shell, nothing pops up on the screen, but the application bar shows an extra empty vs code window.

now, back to our on_output_add function, we need to store new outputs in our nye_server::nye_outputs list. we’ll first get a reference to our nye_server through the wl_listener reference our callback gets and cast the void * to the original wlr_output type.

static void on_output_add(struct wl_listener *wl_listener, void *data) { struct nye_server *nye_server = wl_container_of(wl_listener, nye_server, on_output_add); struct wlr_output *wlr_output = data; }

ok but wait a sec, what’s wl_container_of doing here? black magic, plain and simple. it’s said that it uses some forgotten spells to get back arg2 from arg1 via arg2::arg3’s offset in memory.

anyway, now we have our wlroots output and our server, we can create our nye_output wrapper and store it in our nye_server.

static void on_output_add(struct wl_listener *wl_listener, void *data) { // -- snip --

// it's gonna outlive the function's call, so it goes to the heap. struct nye_output *nye_output = calloc(1, sizeof(struct nye_output)); nye_output->nye_server = nye_server; nye_output->wlr_output = wlr_output; // connect em wl_lists. wl_list_insert(&nye_server->nye_outputs, &nye_output->link); }

great! now we have them tracked.

as we said before, wayland outputs are usually monitors. such monitors have properties like refresh rate, height and width, and other properties. before we can use our outputs for apps, we need to do two things:

wlroots makes this dead simple, starting with setting the mode:

static void on_output_add(struct wl_listener *wl_listener, void *data) { // -- snip -- wl_signal_add(&nye_server.wlr_backend->events.new_output, &nye_server.on_output_add);

struct wlr_output_state wlr_output_state; wlr_output_state_init(&wlr_output_state); wlr_output_state_set_enabled(&wlr_output_state, true);

// nested wayland outputs don't have modes. we commit an empty state if that's the case. if (!wl_list_empty(&wlr_output->modes)) { // outputs can have many supported modes, we pick the preferred one (biggest, fastest). struct wlr_output_mode *wlr_output_mode = wlr_output_preferred_mode(wlr_output); wlr_output_state_set_mode(&wlr_output_state, wlr_output_mode); }

wlr_output_commit_state(wlr_output, &wlr_output_state); wlr_output_state_finish(&wlr_output_state);

struct nye_output *nye_output = calloc(1, sizeof(struct nye_output)); // -- snip -- }

and at the end of our handler function, we add this line to create the global:

wlr_output_create_global(wlr_output, nye_server->wl_display);

great! this is a bit of code that won’t produce any visible changes, but it’s the foundation for our rendering in a minute.

last but not least, our code currently leaks memory. we store outputs when they’re created, but we don’t free that memory when an output is destroyed. let’s create a listener that takes care of that.

struct nye_output { // -- snip -- struct wl_listener on_output_del; };

static void on_output_del(struct wl_listener *wl_listener, void *) { // more memory dark magic. struct nye_output *nye_output = wl_container_of(wl_listener, nye_output, on_output_del);

// remove the output from the linked list and unlink our handler. wl_list_remove(&nye_output->link

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →