How to build a simple screenshot tool for Wayland.

Table of Contents

1. Welcome

Hello, after struggling with the Wayland API for a while, I realized that after some time it becomes understandable, and for this reason, I decided to write a simple document about it.

1.1. Making a screenshot program for Wayland

Required dependencies (I use Void Linux, so I wrote their names accordingly; you should find the equivalents for your own distribution):

  • wayland-devel
  • wlroots-devel
  • wlr-screencopy-unstable-v1 protocol. (In the next section, you can find what it is and how to obtain it; this is not a distribution-specific dependency.)

1.1.1. What is wlr-screencopy

This protocol lets clients request the compositor to copy screen content to their buffers. It’s experimental and may change incompatibly until it becomes stable.

  1. How can I install it or how can I get it?

    Actually, I will give you an example from the .xml file I provided in my project; you can use it too. Of course, if you want, copies can also be found web.

    First, we will generate header and source files from the XML file I provided, to use them in our project.

    # HEADER FILE 
    wayland-scanner client-header ./protocol/wlr-screencopy-unstable-v1.xml wlr-screencopy-unstable-v1.h
    
    # SOURCE FILE 
    wayland-scanner private-code ./protocol/wlr-screencopy-unstable-v1.xml wlr-screencopy-unstable-v1.c
    

    We have two files, and we will use them in our project.

1.2. THE PROGRAM BASICS

Actually, working with Wayland from scratch at a low level in C is easy but takes a long time to fully understand. Right now, I still don’t even know how to handle multiple monitors. Anyway, first of all, since I don’t want to bother explaining everything to you, I’m giving you the lines you need to #include in your C code from the very beginning. If you really want to, and you’re not an experienced C developer, you can research them one by one.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <png.h>
#include <zlib.h>
#include <sys/syscall.h>
#include <wayland-client.h>
#include <linux/memfd.h>
#include "./wlr-screencopy-unstable-v1.h"

These #include statements are the libraries we will use.

1.3. Connecting to Wayland Server and Registry

I defined two global variables to connect to the Wayland server.

struct wl_display *display = NULL;
struct wl_registry *registry = NULL;

And along with that, I connected to the Wayland display using these functions.

int main(int argc, char *argv[]) {
  display = wl_display_connect(NULL);
  if (!display) { // ERROR CHECKING.
    fprintf(stderr, "Unable to connect wayland display.");
    return 1;
  }
}

Now, assuming we have connected to the Wayland server, our first goal is to register objects and protocols using wl_display_get_registry. Additionally, we will set up a listener because there are things we need to catch!

Again, in our main code

registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &reg_listener, NULL); // REGISTRY LISTENER

Now we have created the registry and opened a listener, but which function will this listener work with? Here is where our reg_listener, which we created as a wl_registry_listener struct, comes into play.

struct wl_registry_listener reg_listener = {
  .global = reg_glob,
  .global_remove = reg_glob_remove,
};

Now, let's explain what .global and .global-remove are. First of all, you can create an empty function for .global-remove because in a small-scale program, no one will care about it. The function we assign to .global will handle the catch process.

1.4. Registry Binding

Now, let’s start writing the function named reg_glob.

void reg_glob(
              void *data, struct wl_registry *registry,
              uint32_t id, const char *interface, uint32_t version
              ) {}

First of all, our function will be defined like this.

There are two things I will explain here: when binding the registry, we need an id, interface, and version. Actually, the struct handles the id and version for us; we only need to catch the necessary things with the interface.

if (strcmp(interface, "wl_output") == 0) {
  st.output = wl_registry_bind(registry, id, &wl_output_interface, version);
}

Our first piece of code — let’s explain this code first. Here, we simply catch wl_output using the interface and bind it to a struct named st via registry bind. And yes, we don’t actually have a struct named st yet — so let’s define it now.

struct state {
  struct wl_output *output;
  struct wl_shm *shm;
  struct zwlr_screencopy_manager_v1 *scrcopy;
};

struct state st;

Since explaining everything would take too much time, our state struct will look something like this. Also, we will declare st as a global variable.

Now let's continue with our function.

else if (strcmp(interface, "wl_shm") == 0) {
 st.shm = wl_registry_bind(registry, id, &wl_shm_interface, version);
} else if (strcmp(interface, "zwlr_screencopy_manager_v1") == 0) {
 st.scrcopy = wl_registry_bind(registry, id, &zwlr_screencopy_manager_v1_interface, version);
}

Now we are binding wl_shm, which is Wayland shared memory, and then we are binding screencopy. As a result, we have bound everything we need.

Now our regglob function is complete.

Additionally, we need to add this to our main code.

wl_display_roundtrip(display);

This function synchronizes the communication between the Wayland client and server.

Additionally, we need to perform a disconnect at the end of our main code.

wl_display_disconnect(display);
return 0;

1.5. Screenshot request to Wayland

Now we have bound the necessary components. Next, our goal is to send a screenshot request to Wayland. Actually, this is the part where things start to get a bit complicated, as it's important to understand this section well.

struct zwlr_screencopy_frame_v1 *shot = zwlr_screencopy_manager_v1_capture_output(st.scrcopy, 0, st.output);
if (!shot) { // ERROR CHECKING.
  fprintf(stderr, "Failed to capture output\n");
  clean(framed);
  return;
}

Now, the wl_output in the st is our output monitor, and scrcopy is what we need to copy the screen. You might ask what the 0 in between is; that 0 is a simple setting that specifies whether the cursor is visible or not.

Now, we will also use a listener for this.

zwlr_screencopy_frame_v1_add_listener(shot, &frame_l, framed);

Additionally, frame_l should be defined this way. Since unused functions won't be of any use to us, we are required to define them and leave them empty.

// UNUSED CALLBACKS
void buffer_done() {}
void damage(void *data, struct zwlr_screencopy_frame_v1 *frame,
            uint32_t x, uint32_t y, uint32_t width, uint32_t height) {}
void linux_dmabuf(void *data, struct zwlr_screencopy_frame_v1 *frame,
                  uint32_t arg1, uint32_t arg2, uint32_t arg3) {}
void flags(void *data, struct zwlr_screencopy_frame_v1 *frame, uint32_t flags) {}
struct zwlr_screencopy_frame_v1_listener frame_l = {
  .buffer = buffer,
  .flags = flags,
  .ready = ready,
  .failed = failed,
  .damage = damage,
  .linux_dmabuf = linux_dmabuf,
  .buffer_done = buffer_done,
};

Alright, let's start with our buffer function. The buffer will be the function that transfers the screen image, and the lowest level code you can see will be written here. Buckle up!

First, we are creating a struct named frame_d to use in the buffer and in different places.

struct frame_d {
  uint32_t width;
  uint32_t height;
  uint32_t sd;
  uint32_t format;
  struct wl_buffer *buffer;
  void *data;
  size_t size;
  int fd;
};

At first glance, you can understand what each element is. sd = stride. I won't explain everything one by one; you can research them individually and learn their purposes based on where they are used.

1.6. Buffer Function

Now, let's move step by step to the buffer function.

void buffer(
            void *data, struct zwlr_screencopy_frame_v1 *frame,
            uint32_t format, uint32_t width, uint32_t height,
            uint32_t sd
           ) {}

Our function is defined like this; now let's write it out step by step, explaining its content.

struct frame_d *framed = (struct frame_d *)data;

framed->width = width;
framed->height = height;
framed->format = format;
framed->sd = sd;
framed->size = sd * height;

We created a structure of type frame_l named framed and assigned it with parameters. The expression sd * height calculates the memory space occupied by the image. I won't go into a lengthy explanation.

Next, we will create a memory file using system calls with syscall.

framed->fd = syscall(SYS_memfd_create, "scrshot", MFD_CLOEXEC);
if (framed->fd == -1) {
  fprintf(stderr, "memfd failed");
  return;
}

Now, let's set framed->fd to the size of framed->size.

if (ftruncate(framed->fd, framed->size) == -1) {
  perror("ftruncate failed");
  close(framed->fd);
  return;
}

Alright, now let's map the memory area.

framed->data = mmap(NULL, framed->size, PROT_READ | PROT_WRITE, MAP_SHARED, framed->fd, 0);
if (framed->data == MAP_FAILED) {
  perror("mmap failed");
  close(framed->fd);
  return;
}

A brief error handling.

if (!st.shm) {
  fprintf(stderr, "Error: st.shm is NULL!\n");
  clean(framed);
  return;
}

Now, let's create a shared memory pool using the wl_shm_create_pool function.

struct wl_shm_pool *pool = wl_shm_create_pool(st.shm, framed->fd, framed->size);
if (!pool) { // ERROR CHECKING
  fprintf(stderr, "Failed to create SHM pool\n");
  clean(framed);
  return;
}

Now, let's create a shared memory buffer using the wl_shm_pool_create_buffer function.

framed->buffer = wl_shm_pool_create_buffer(pool, 0, width, height, sd, format);
if (!framed->buffer) {
  fprintf(stderr, "Failed to create buffer\n");
  clean(framed);
  return;
}

Actually, we're almost done; we just need to write the cleanup functions and close everything.

wl_shm_pool_destroy(pool);
zwlr_screencopy_frame_v1_copy(frame, framed->buffer);

Next, we have our ready function, and before that, we embark on a journey for libpng!

1.7. Libpng implementation

First, we will create a function named save_png to use libpng cleanly.

void save_png(struct frame_d *framed) {}

Now, it's going to be a long journey with libpng, but I'll go through it quickly without too many explanations. The documentation for libpng is much more comprehensive, so feel free to take a look if you want.

First, let's open the file using fopen(), and then we'll create a png_ptr.

FILE *file = fopen("screenshot.png", "wb");
if (!file) {
  perror("File could not be opened.");
  return;
}

png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr) {
  fprintf(stderr, "Unable to create png write struct.");
  fclose(file);
  return;
}
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
  fprintf(stderr, "Unable to create png info struct.");
  png_destroy_write_struct(&png_ptr, NULL);
  fclose(file);
  return;
}

if (setjmp(png_jmpbuf(png_ptr))) {
  fprintf(stderr, "Failed to create png file.");
  png_destroy_write_struct(&png_ptr, &info_ptr);
  fclose(file);
  return;
}

I won't explain in detail because it's quite understandable, and if you want, you can refer to the libpng documentation, as I'm here to explain Wayland, not libpng.

png_set_compression_level(png_ptr, Z_DEFAULT_COMPRESSION);
png_set_IHDR(png_ptr, info_ptr, framed->width, framed->height, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE , PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png_ptr, info_ptr);
uint32_t *pixels = (uint32_t *)framed->data;
png_byte **row_pointers = (png_byte **)malloc(sizeof(png_byte *) * framed->height);
for (int y = 0; y < framed->height;y++) {
  row_pointers[y] = (png_byte*)malloc(png_get_rowbytes(png_ptr, info_ptr));
  for (int x = 0;x < framed->width;x++) {
    uint32_t pixel = pixels[y * (framed->sd / 4) + x];
    row_pointers[y][x * 3 + 0] = (pixel >> 16) & 0xFF;
    row_pointers[y][x * 3 + 1] = (pixel >> 8) & 0xFF;
    row_pointers[y][x * 3 + 2] = pixel & 0xFF;
  }
}

png_write_image(png_ptr, row_pointers);
png_write_end(png_ptr, NULL);

for (int i = 0; i < framed->height;i++) {
  free(row_pointers[i]);
}
free(row_pointers);

png_destroy_write_struct(&png_ptr, &info_ptr);
fclose(file);
printf("Screenshot saved succesfully");

We are making a few small adjustments for PNG and performing calculations for it, that's all.

1.8. Final Touches

Now, let's move on to our ready function, which will be very brief. We will create a framed of type frame_l and provide parameters for save_png. I won't write a lengthy clean function; I'll give you a simple version, and those who are curious and want to learn can research it.

void ready(
           void *data, struct zwlr_screencopy_frame_v1 *frame,
           uint32_t tv_sec_hi, //  REFERANCE: https://github.com/swaywm/wlroots/blob/master/examples/screencopy.c#L133
           uint32_t tv_sec_lo, uint32_t tv_nsec // REFERANCE: https://github.com/swaywm/wlroots/blob/master/examples/screencopy.c#L133
           ) {
  struct frame_d *framed = (struct frame_d *)data;
  save_png(framed);
  clean(framed);
  zwlr_screencopy_frame_v1_destroy(frame);
  ok = 1;
}

Additionally, you should place the clean function above the failed and other functions.

void failed(void *data, struct zwlr_screencopy_frame_v1 *frame) {
  struct frame_d *framed = (struct frame_d *)data;
  clean(framed);
  zwlr_screencopy_frame_v1_destroy(frame);
}

// CLEAN FUNCTION

void clean(struct frame_d *framed) {
  if (framed->data && framed->data != MAP_FAILED ) {
    munmap(framed->data, framed->size);
  }

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

  if (framed->fd >= 0) {
    close(framed->fd);
  }
}

In your main function, add this under the zwlr_screencopy_frame_v1_add_listener listener

while(wl_display_dispatch(display) != -1) {
  if (ok == 1) {
    break;
  }
}

Additionally, add this at the very beginning of your main function:

memset(&st, 0, sizeof(st));

Also, we forgot to define this; make sure to add it as well.

if (!st.output || !st.scrcopy || !st.shm) {
  fprintf(stderr, "Could not found interfaces.");
  wl_display_disconnect(display);
  return;
}

struct frame_d *framed = malloc(sizeof(struct frame_d));
memset(framed, 0, sizeof(struct frame_d));
framed->fd = -1;

2. The End

Since we've reached the end, it's essential to grasp the logic rather than just copy/paste the code. I wrote this simple document to help you understand the logic and create your own code. If it's not working, it means you haven't understood the logic!

Thank you for reading. My references are written in the repository's README.md section.

Author: 0l3d

Created: 2025-07-11 Fri 03:12

Validate