Linux Graphics Stack on the Arduino UNO Q

What I like most about the Arduino Uno Q is its ability to directly drive a display for graphical output, eliminating the need for an additional computer for data visualization.
This motivated me to take a closer look at the Linux graphics stack running on the Arduino. In the following posts, I will share my findings, starting with the most fundamental layer: DRM/KMS (Direct Rendering Manager / Kernel Mode Setting). More advanced topics such as OpenGL and Wayland will follow later.

Prerequisites
By default, the Arduino Uno Q boots into standalone mode and starts an xorg server running the XFCE desktop environment. To get closer to the graphics hardware and work directly with the Linux graphics stack, we should boot into a pure text console instead of the graphical environment.

You can change the default boot target to multi-user (console mode) using:

sudo systemctl set-default multi-user.target

After rebooting, the system will start in text mode, without any graphical layer.
(To return to the desktop environment, type startx.)

Depending on your keyboard layout, you may need to install the kbd package:

sudo apt install kbd

To inspect the DRM subsystem and compile the example program, install:

sudo apt install drm-info
sudo apt install build-essential pkg-config libdrm-dev

Graphics Building Blocks
Let’s start with the QRB2210 block diagram:

We are mainly interested in the Multimedia section.

In this block, we find: Adreno 702 GPU (Graphics Processing Unit) and Adreno 920 DPU (Display Processing Unit).

Interestingly, in our first low-level approach we are not dealing with the GPU, but primarily with the DPU.

The DPU is responsible for scanout and display control. It retrieves pixel data from memory and sends it to the display peripherals via standardized interfaces. In other words, the DPU forms the link between system memory and the physical display panel.

Here you can find further information about the DPU.

DRM, KMS and GEM
The Linux graphics subsystem is built around DRM (Direct Rendering Manager) in the kernel.

DRM provides:

  • Access to graphics hardware
  • Buffer management
  • Display configuration
  • Synchronization

KMS (Kernel Mode Setting) is the part of DRM responsible for configuring the display pipeline:

  • Setting resolution and refresh rate
  • Selecting connectors
  • Assigning framebuffers
  • Controlling CRTCs and planes

Another important component inside DRM is GEM (Graphics Execution Manager). GEM is responsible for managing graphics buffers in kernel space.

DRM Devices
If you enter:

ls /dev/dri

you will typically see:

card0  renderD128
  • card0 β†’ represents the display controller (DPU, modesetting node)
  • renderD128 β†’ represents the GPU render node

dri stands for Direct Rendering Infrastructure.

For direct KMS access, we use /dev/dri/card0, which controls display modes and scanout.

Linux Fundamentals
Linux is fundamentally divided into kernel space and user space.

The kernel is the central core of the Linux operating system. It manages hardware resources such as CPU, memory, and peripheral devices, and provides controlled access to them through well-defined system calls. Device drivers β€” including DRM/KMS β€” run in kernel space and have direct access to the hardware.

User space is the environment in which normal applications execute. Programs in user space cannot directly access hardware; instead, they communicate with the kernel via system calls. This strict separation protects system stability and security.

libdrm
The interface between a user-space application and the DRM driver in kernel space is provided by libdrm. This is a thin user-space library wrapping the DRM kernel API (which internally uses ioctl() calls). It simplifies buffer allocation, modesetting, and resource management.

On the Arduino Uno Q, the DRM driver is the msm driver (Qualcomm Snapdragon DRM), part of the Linux kernel mainline and maintained as open source.

drm_info

Using the installed tool:

drm_info

shows (reduced)

Node: /dev/dri/card0
 β”œβ”€β”€β”€Driver: msm (MSM Snapdragon DRM) version 1.12.0 (0)
 β”‚ β”œβ”€β”€β”€DRM_CLIENT_CAP_STEREO_3D supported
 β”‚ β”œβ”€β”€β”€DRM_CLIENT_CAP_UNIVERSAL_PLANES supported
 β”‚ β”œβ”€β”€β”€DRM_CLIENT_CAP_ATOMIC supported
 β”œβ”€β”€β”€Device: platform qcom,qcm2290-dpu
 β”‚ └───Available nodes: primary, render
 β”œβ”€β”€β”€Framebuffer size
 β”‚ β”œβ”€β”€β”€Width: [0, 16383]
 β”‚ └───Height: [0, 16383]
 β”œβ”€β”€β”€Connectors
 β”‚ └───Connector 0
 β”‚ β”œβ”€β”€β”€Object ID: 34
 β”‚ β”œβ”€β”€β”€Type: DisplayPort
 β”‚ β”œβ”€β”€β”€Status: connected
 β”‚ β”œβ”€β”€β”€Physical size: 350x190 mm
 β”‚ β”œβ”€β”€β”€Subpixel: unknown
 β”‚ β”œβ”€β”€β”€Encoders: {0}
 β”‚ β”œβ”€β”€β”€Modes
 β”‚ β”‚ β”œβ”€β”€β”€1920x1080@60.00 preferred driver phsync pvsync 
 β”‚ └───Properties
 β”‚ β”œβ”€β”€β”€"EDID" (immutable): blob = 48
 β”‚ β”œβ”€β”€β”€"DPMS": enum {On, Standby, Suspend, Off} = On
 β”‚ β”œβ”€β”€β”€"link-status": enum {Good, Bad} = Good
 β”‚ β”œβ”€β”€β”€"non-desktop" (immutable): range [0, 1] = 0
 β”‚ β”œβ”€β”€β”€"TILE" (immutable): blob = 0
 β”‚ └───"CRTC_ID" (atomic): object CRTC = 47
 β”œβ”€β”€β”€Encoders
 β”‚ └───Encoder 0
 β”‚ β”œβ”€β”€β”€Object ID: 33
 β”‚ β”œβ”€β”€β”€Type: DSI
 β”‚ β”œβ”€β”€β”€CRTCS: {0}
 β”‚ └───Clones: {0}
 β”œβ”€β”€β”€CRTCs
 β”‚ └───CRTC 0
 β”‚ β”œβ”€β”€β”€Object ID: 47
 β”‚ β”œβ”€β”€β”€Legacy info
 β”‚ β”‚ β”œβ”€β”€β”€Mode: 1920x1080@60.00 preferred driver phsync pvsync 
 β”‚ β”‚ └───Gamma size: 0
 β”‚ └───Properties
 β”‚ β”œβ”€β”€β”€"ACTIVE" (atomic): range [0, 1] = 1
 β”‚ β”œβ”€β”€β”€"MODE_ID" (atomic): blob = 50
 β”‚ β”‚ └───1920x1080@60.00 preferred driver phsync pvsync 
 β”‚ β”œβ”€β”€β”€"OUT_FENCE_PTR" (atomic): range [0, UINT64_MAX] = 0
 β”‚ β”œβ”€β”€β”€"VRR_ENABLED": range [0, 1] = 0
 β”‚ └───"CTM": blob = 0
 └───Planes
 β”œβ”€β”€β”€Plane 0
 β”‚ β”œβ”€β”€β”€Object ID: 35
 β”‚ β”œβ”€β”€β”€CRTCs: {0, 1, 2, 3, 4, 5, 6, 7}
 β”‚ β”œβ”€β”€β”€Legacy info
 β”‚ β”‚ β”œβ”€β”€β”€FB ID: 49
 β”‚ β”‚ β”‚ β”œβ”€β”€β”€Object ID: 49
 β”‚ β”‚ β”‚ β”œβ”€β”€β”€Size: 1920x1080
 β”‚ β”‚ β”‚ β”œβ”€β”€β”€Format: XRGB8888 (0x34325258)
 β”‚ β”‚ β”‚ β”œβ”€β”€β”€Modifier: DRM_FORMAT_MOD_LINEAR (0x0)
 β”‚ β”‚ β”‚ └───Planes:
 β”‚ β”‚ β”‚ └───Plane 0: offset = 0, pitch = 7680 bytes
 β”‚ β”‚ └───Formats:
 β”‚ β”‚ β”œβ”€β”€β”€ARGB8888 (0x34325241) 

we obtain detailed information about:

  • The driver (msm)
  • The device (qcm2290-dpu)
  • Available connectors
  • Supported modes
  • Framebuffer formats
  • Planes and CRTCs

For example:

  • Resolution: 1920x1080 @ 60 Hz
  • Pixel format: XRGB8888 / ARGB8888
  • Connector type: DisplayPort

Overview

(see also the following example program)

The control path therefore looks like this:

Application
β†’ libdrm
β†’ ioctl()
β†’ DRM Core
β†’ msm driver
β†’ DPU

This path configures the display pipeline via KMS.

The actual pixel data does not flow through ioctl().

  1. A GEM (Graphic Execution Manager) buffer is allocated in kernel space.
  2. This buffer is mapped into user space using mmap().
  3. The application receives a pointer (e.g. uint32_t *p) to this shared memory.
  4. The CPU writes pixel values directly into this memory.
  5. The DPU reads the same memory via DMA and sends it to the display.

The memory is allocated and managed by the kernel but shared with user space via virtual memory mapping. No copying of pixel data is required.

Example Code – Minimal KMS Program
Using libdrm, we can write directly to a framebuffer that is scanned out by the DPU.

The following minimal C program demonstrates:

  • Opening the DRM device
  • Selecting connector and mode
  • Creating a dumb buffer (GEM buffer)
  • Mapping it into user space
  • Writing pixel data
  • Assigning the framebuffer via KMS

No error handling is included for simplicity.

// kms-mini.c
#include <fcntl.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>

#include <drm/drm.h>
#include <drm/drm_mode.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

int main() {

    // Open DRM device (display controller / DPU)
    int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);

    // Query DRM resources (connectors, encoders, CRTCs)
    drmModeRes *res = drmModeGetResources(fd);

    // Select first connected display and its first mode
    drmModeConnector *conn = drmModeGetConnector(fd, res->connectors[0]);
    drmModeEncoder *enc = drmModeGetEncoder(fd, conn->encoder_id);
    drmModeModeInfo mode = conn->modes[0];

    // Create a simple ("dumb") GEM buffer in kernel memory
    struct drm_mode_create_dumb creq = {0};
    creq.width  = mode.hdisplay;
    creq.height = mode.vdisplay;
    creq.bpp    = 32;
    ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq);

    // Create DRM framebuffer object (fb) referencing the GEM buffer
    uint32_t fb;
    drmModeAddFB(fd, creq.width, creq.height,
                 24, 32, creq.pitch, creq.handle, &fb);

    // Bind DRM framebuffer (fb) to CRTC and Connector for output
    drmModeSetCrtc(fd, enc->crtc_id, fb, 0, 0, &conn->connector_id, 1, &mode);

    // Map GEM buffer into user space
    struct drm_mode_map_dumb mreq = {0};
    mreq.handle = creq.handle;
    ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq);

    uint32_t *pixels = mmap(0, creq.size,
                            PROT_READ | PROT_WRITE,
                            MAP_SHARED, fd, mreq.offset);
    
    while (1) {

        // Fill entire framebuffer with red (ARGB8888)
        
        for (uint64_t i = 0; i < creq.size / 4; i++)
            pixels[i] = 0xFFFF0000u;

        sleep(2);

        // Green
        for (uint64_t i = 0; i < creq.size / 4; i++)
            pixels[i] = 0xFF00FF00u;

        sleep(2);

        // Blue
        for (uint64_t i = 0; i < creq.size / 4; i++)
            pixels[i] = 0xFF0000FFu;

        sleep(2);
    }
}

Compile and Run

gcc kms-mini.c -o kms-mini $(pkg-config --cflags --libs libdrm)
./kms-mini

The screen cycle through red, green and blue every 2 seconds until you stop the program with Ctrl+C .

More about Memory Management within the DRM-system can be found here:

Conclusion
This minimal example demonstrates the fundamental architecture of the Linux graphics stack at its lowest level. We have seen that the graphics pipeline is split into two distinct paths:

  • The control path, where libdrm communicates with the DRM subsystem via ioctl() to configure the display pipeline (KMS).
  • The data path, where a kernel-allocated GEM buffer is mapped into user space via mmap(), allowing the CPU to write pixel data directly into shared memory.

The Display Processing Unit (DPU) then reads this memory via DMA and transfers the pixel data to the physical display.

However, writing directly to the framebuffer using the CPU is not performance-efficient. The GPU is specialized for rendering graphics into buffers and should be used instead. This will be covered in the next chapter. In the final step, we will take a look at window composition using Wayland and Weston.

This is not my area and yet I will try to learn something as I have an UNO Q setup.

On the topic of DRM/KMS, Wayland also works well.

The XFCE Wayland session is busted but others like Sway run great (and don't have the flickering cursor issue that X11 does)

Would love to use KDE Plasma but sadly not enough space to pull that off.

Yes, I use Weston on Wayland. There I'm running Firefox 147 to experiment with WebGPU.

I would like to return to the very first post in this thread -this might not be interesting for everyone. :wink:

After setting up KMS/DRM, we finally ended up with a passive framebuffer that directly represents the display. Writing to this memory using the CPU immediately produces visible pixels on the screen. In a sense, this brings us back to the early days of microcomputing β€” the era of the first IBM PCs, the Commodore 64, or the Atari ST in the 1980s. These are the machines I grew up with. ;-)

To revive those memories, I extended the KMS/DRM C example as follows:

  • Implemented init_framebuffer(), which performs the complete KMS/DRM setup and returns a usable framebuffer structure.
  • Added clear() to fill the framebuffer with a uniform ARGB color.
  • Added put_pixel(), which writes a single pixel at coordinate (x, y) in ARGB8888 format.
  • Implemented the Bresenham algorithm in draw_line(), drawing a line between (x0, y0) and (x1, y1) using put_pixel().
  • Added two helper functions: get_seconds() for time measurement and random_int() for generating random coordinates.

In main(), I generate 100,000 random lines (line_count) and store them in an array line_list[]. The program then draws all 100,000 lines to the framebuffer while measuring the execution time. This procedure is repeated periodically, with a one-second pause between iterations.

The goal here is simple: measure how fast the CPU can render a large number of random lines directly into a KMS dumb buffer.

Below is the complete source code:

// gcc kms-line-perf.c -o kms-line-perf \
        $(pkg-config --cflags --libs libdrm)
#include <fcntl.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>

#include <drm/drm.h>
#include <drm/drm_mode.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

#include <stdlib.h>
#include <time.h>
#include <stdio.h>
#include <inttypes.h>

uint32_t plot_counter = 0;

typedef struct {
   int x0;
   int y0;
   int x1;
   int y1;
   uint32_t c;
} line_t;

typedef struct {
   uint32_t *pixels;
   uint32_t width;
   uint32_t height;
   uint32_t pitch;
   uint32_t size;
} framebuffer_t;

static inline double get_seconds()
{
   struct timespec ts;
   clock_gettime(CLOCK_MONOTONIC,&ts);
   return ts.tv_sec + ts.tv_nsec * 1e-9;
}

static inline int random_int(int min, int max)
{
   return min + (int)(random() % (unsigned)(max - min + 1));
}

static inline void clear(framebuffer_t *fb, uint32_t argb)
{
   uint8_t *base = (uint8_t *)fb->pixels;

   for (uint32_t y = 0; y < fb->height; y++) {
      uint32_t *row = (uint32_t *)(base + (uint64_t)y * fb->pitch);
      for (uint32_t x = 0; x < fb->width; x++) {
         row[x] = argb;
      }
   }
}

static inline void put_pixel(framebuffer_t *fb, int x, int y, uint32_t argb)
{
   //if ((unsigned)x >= fb->width || (unsigned)y >= fb-> height) return;

   uint8_t  *base = (uint8_t *)fb->pixels;
   uint32_t *row  = (uint32_t *)(base + (uint64_t)y * fb->pitch);
   row[x] = argb;
   plot_counter++;
}

static inline void draw_line(framebuffer_t *fb, int x0, int y0, int x1, int y1, uint32_t argb)
{
   int dx = (x1 > x0) ? (x1 - x0) : (x0 - x1);
   int sx = (x0 < x1) ? 1 : -1;
   int dy = (y1 > y0) ? (y0 - y1) : (y1 - y0);
   int sy = (y0 < y1) ? 1 : -1;
   int err = dx + dy;

   for(;;) {
      put_pixel(fb, x0, y0, argb);
      if (x0 == x1 && y0 == y1) break;
      int e2 = 2 * err;
      if (e2 >= dy) {err += dy; x0 += sx; }
      if (e2 <= dx) {err += dx; y0 += sy; }
   }
}

framebuffer_t init_framebuffer()
{
   // Open DRM device (display controller / DPU)
   int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
   // Query DRM ressources (connectors, encoders, CRTCs)
   drmModeRes *res = drmModeGetResources(fd);
   // Select first connected display and its first mode
   drmModeConnector *conn = drmModeGetConnector(fd, res->connectors[0]);
   drmModeEncoder *enc =  drmModeGetEncoder(fd, conn->encoder_id);
   drmModeModeInfo mode = conn->modes[0];
   // Mode provides >> hdisplay, vdisplay

   // GEM = Graphical Execution Manager
   // Create a simple ("dumb") GEM buffer in kernel memory
   struct drm_mode_create_dumb creq = {0};
   creq.width = mode.hdisplay;
   creq.height = mode.vdisplay;
   creq.bpp = 32;
   ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq);
   // creq.width / creq.height, creq.pitch >> length of row in bytes
   // creq.size >> size of entire buffer in bytes
   // Create DRM framebuffer object referencing the GEM buffer
   uint32_t fb;
   drmModeAddFB(fd,creq.width, creq.height, 24, 32, creq.pitch, creq.handle, &fb);
   // Bind DRM framebuffer to CRTC and connector
   drmModeSetCrtc(fd, enc->crtc_id, fb, 0, 0, &conn->connector_id, 1, &mode);

   // Map GEM buffer into user space
   struct drm_mode_map_dumb mreq = {0};
   mreq.handle = creq.handle;
   ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq);

   uint32_t *p = mmap(0, creq.size,PROT_READ|PROT_WRITE, MAP_SHARED, fd, mreq.offset);

   framebuffer_t  fb_t = {
       .pixels = (uint32_t *)p,
       .width = creq.width,
       .height = creq.height,
       .pitch  = creq.pitch,
       .size   = creq.size,
   };
   return fb_t;
}

int main() {
   int line_count = 100000;
   line_t line_list[line_count];

   framebuffer_t fb_t = init_framebuffer();
   srandom(time(NULL));
   while(1){
      clear(&fb_t,0xFF000000u);
      plot_counter = 0;

      double t0 = get_seconds();
      for (int i = 0; i < line_count; i++) {
        line_list[i].x0 = random_int(0, fb_t.width-1);
        line_list[i].y0 = random_int(0, fb_t.height-1);
        line_list[i].x1 = random_int(0, fb_t.width-1);
        line_list[i].y1 = random_int(0, fb_t.height-1);
        line_list[i].c  =  0xFF000000u | (random() & 0x00FFFFFFu);
      }
      double t1 = get_seconds();

      for (int i = 0; i < line_count; i++) {
        draw_line(&fb_t,line_list[i].x0,line_list[i].y0,
                        line_list[i].x1,line_list[i].y1,
                        line_list[i].c);
      }
      double t2 = get_seconds();
      printf("Create Vert: %.6f sec \n", (t1 - t0));
      printf("Draw Lines : %.6f sec \n", (t2 - t1));
      printf("Total Time : %.6f sec \n", (t2 - t0));
      printf("Plot_Counter: %" PRIu32 "\n\n", plot_counter);

      sleep(1);
   }
}

The time measurement distinguishes between generating the line_list and rendering the lines.

Typical values on the Arduino Uno Q are:

  • Create Vert : 0.032 sec
  • Draw Lines : 3.050 sec
  • Total Time : 3.082 sec

This corresponds to roughly 32,500 lines per second.

The plot_counter reports approximately 73,100,000 calls to put_pixel(), which results in:

  • an average line length of about
    73,200,000 / 100,000 β‰ˆ 732 pixels per line
  • a total pixel fill rate of
    32,500 Γ— 732 β‰ˆ 24 MPixels/s

In other words, the CPU achieves a sustained write rate of roughly 24 million 32-bit pixels per second when rendering random lines into a KMS dumb buffer. Without plot_counter++ in put_pixel(), drawing is even about 10% faster.

Here is a short video of the screen output:

https://www.youtube.com/watch?v=r3dY_AcoBC4

Best viewed in native 1080p β€” rapidly changing random lines are a nightmare for video encoders. :wink:

Continuing from the previous post above:

Plotting 100,000 independent lines looks like a perfect candidate for parallelization. Therefore, I modified the single-threaded version from above into a simple multithreaded application.

The overall program logic is unchanged β€” the only difference is that the expensive work (vertex generation + line drawing) is moved into worker threads. The constant THREADS in main() controls how many worker threads are created, and into how many chunks the line_list is split.

Of course, this approach is not β€œperfect scaling”: all threads write into the same framebuffer, so there will be cache effects and memory contention (and the resulting writes are effectively random). The goal of this example is not to solve those issues, but simply to demonstrate what kind of speedup is achievable by parallelizing the workload on the CPU.

// gcc kms-line-perf-mt.c -O3 -o kms-line-perf-mt \
//     $(pkg-config --cflags --libs libdrm) -pthread

#include <fcntl.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <pthread.h>

#include <drm/drm.h>
#include <drm/drm_mode.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

#include <stdlib.h>
#include <time.h>
#include <stdio.h>
#include <inttypes.h>

uint32_t plot_counter = 0;

typedef struct {
   int x0;
   int y0;
   int x1;
   int y1;
   uint32_t c;
} line_t;

typedef struct {
   uint32_t *pixels;
   uint32_t width;
   uint32_t height;
   uint32_t pitch;
   uint32_t size;
} framebuffer_t;

typedef struct {
   framebuffer_t *fb;
   line_t *line_list;
   int start;
   int end;
} thread_arg_t;

static inline double get_seconds()
{
   struct timespec ts;
   clock_gettime(CLOCK_MONOTONIC,&ts);
   return ts.tv_sec + ts.tv_nsec * 1e-9;
}

static inline int random_int(int min, int max)
{
   return min + (int)(random() % (unsigned)(max - min + 1));
}

static inline void put_pixel(framebuffer_t *fb, int x, int y, uint32_t argb)
{
   uint8_t  *base = (uint8_t *)fb->pixels;
   uint32_t *row  = (uint32_t *)(base + (uint64_t)y * fb->pitch);
   row[x] = argb;
}

static inline void draw_line(framebuffer_t *fb, int x0, int y0, int x1, int y1, uint32_t argb)
{
   int dx = (x1 > x0) ? (x1 - x0) : (x0 - x1);
   int sx = (x0 < x1) ? 1 : -1;
   int dy = (y1 > y0) ? (y0 - y1) : (y1 - y0);
   int sy = (y0 < y1) ? 1 : -1;
   int err = dx + dy;

   for(;;) {
      put_pixel(fb, x0, y0, argb);
      if (x0 == x1 && y0 == y1) break;
      int e2 = 2 * err;
      if (e2 >= dy) { err += dy; x0 += sx; }
      if (e2 <= dx) { err += dx; y0 += sy; }
   }
}

framebuffer_t init_framebuffer()
{
   int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
   drmModeRes *res = drmModeGetResources(fd);
   drmModeConnector *conn = drmModeGetConnector(fd, res->connectors[0]);
   drmModeEncoder *enc = drmModeGetEncoder(fd, conn->encoder_id);
   drmModeModeInfo mode = conn->modes[0];

   struct drm_mode_create_dumb creq = {0};
   creq.width = mode.hdisplay;
   creq.height = mode.vdisplay;
   creq.bpp = 32;
   ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &creq);

   uint32_t fb;
   drmModeAddFB(fd,creq.width, creq.height, 24, 32, creq.pitch, creq.handle, &fb);
   drmModeSetCrtc(fd, enc->crtc_id, fb, 0, 0, &conn->connector_id, 1, &mode);

   struct drm_mode_map_dumb mreq = {0};
   mreq.handle = creq.handle;
   ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq);

   uint32_t *p = mmap(0, creq.size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, mreq.offset);

   framebuffer_t fb_t = {
       .pixels = p,
       .width = creq.width,
       .height = creq.height,
       .pitch  = creq.pitch,
       .size   = creq.size,
   };
   return fb_t;
}

void *worker(void *arg)
{
   thread_arg_t *a = (thread_arg_t*)arg;

   for (int i = a->start; i < a->end; i++) {
      a->line_list[i].x0 = random_int(0, a->fb->width-1);
      a->line_list[i].y0 = random_int(0, a->fb->height-1);
      a->line_list[i].x1 = random_int(0, a->fb->width-1);
      a->line_list[i].y1 = random_int(0, a->fb->height-1);
      a->line_list[i].c  = 0xFF000000u | (random() & 0x00FFFFFFu);
   }

   for (int i = a->start; i < a->end; i++) {
      draw_line(a->fb,
                a->line_list[i].x0,
                a->line_list[i].y0,
                a->line_list[i].x1,
                a->line_list[i].y1,
                a->line_list[i].c);
   }

   return NULL;
}

int main()
{
   int line_count = 100000;
   line_t line_list[line_count];

   framebuffer_t fb_t = init_framebuffer();
   srandom(time(NULL));

   const int THREADS = 4;
   pthread_t threads[THREADS];
   thread_arg_t args[THREADS];

   while(1)
   {
      double t0 = get_seconds();

      int chunk = line_count / THREADS;

      for (int t = 0; t < THREADS; t++) {
         args[t].fb = &fb_t;
         args[t].line_list = line_list;
         args[t].start = t * chunk;
         args[t].end = (t == THREADS-1) ? line_count : (t+1)*chunk;
         pthread_create(&threads[t], NULL, worker, &args[t]);
      }

      for (int t = 0; t < THREADS; t++)
         pthread_join(threads[t], NULL);

      double t1 = get_seconds();

      printf("Total Time : %.6f sec\n\n", (t1 - t0));

      sleep(1);
   }
}

Here are the results (with plot_counter disabled):

  • THREADS = 1 β†’ Total Time: 2.81 s
  • THREADS = 2 β†’ Total Time: 1.55 s β†’ speedup β‰ˆ 1.81Γ—
  • THREADS = 3 β†’ Total Time: 1.18 s β†’ speedup β‰ˆ 2.38Γ—
  • THREADS = 4 β†’ Total Time: 1.12 s β†’ speedup β‰ˆ 2.51Γ—
  • THREADS = 5 β†’ Total Time: 1.12 s β†’ speedup β‰ˆ 2.51Γ—

As the QRB2210 is a quad-core MPU, there is no further improvement beyond 4 threads (and in practice the workload becomes bandwidth-/contention-limited even before that). This is not surprising: even though the computation is embarrassingly parallel at the line level, all threads are still writing into the same shared framebuffer with essentially random access patterns.

Still, the speedup is not too bad. But just wait until we enable the GPU. Stay tuned…

In the last post, we saw that the CPU has limited performance for rendering graphics. A GPU, in contrast, is built as a massively parallel rasterization machine. When the code calls glDrawArrays(GL_LINES, ...), the CPU is not looping over pixels. It submits a high-level command. The GPU then processes vertices in parallel, rasterizes line primitives in fixed-function hardware, and writes the results efficiently using a memory architecture optimized for this workload.

For programming, a standardized API called OpenGL ES is used. β€œES” stands for Embedded Systems. It defines how applications express rendering commands but does not define how those commands reach the hardware. Mesa is an open-source implementation of that specification. Over time, Mesa has evolved into a complete userspace graphics stack, including hardware drivers such as Freedreno for Adreno GPUs.

OpenGL ES alone does not know how to present images to a display. That role is fulfilled by EGL, which acts as the bridge between the rendering API and the native display system. In the code, eglGetDisplay, eglCreateWindowSurface, and eglMakeCurrent connect the OpenGL ES context to a native surface. In this case, KMS/DRM is used.

The OpenGL ES / GPU performance benefit comes with some cost. Setting everything up adds quite a few lines to our line benchmark program. However, all setup-related code is encapsulated in the functions graphics_init() and graphics_present(). For those interested in the details, here are some explanatory notes - see also the following illustration:

To make the connection efficient, the stack introduces GBM, the Generic Buffer Manager, which lives in userspace as part of Mesa. GBM allocates buffer objects that are suitable both for GPU rendering and for display scanout. When the code creates the GBM surface with GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING, it ensures that the same buffer can be rendered by the GPU and then handed directly to the display controller without copying.

Under the hood, these GBM buffer objects correspond to GEM objects inside the kernel’s DRM subsystem. GEM (Graphics Execution Manager) manages GPU-visible memory in the kernel. When gbm_surface_lock_front_buffer returns a gbm_bo, it effectively references a GEM-backed buffer. The code then wraps that buffer into a DRM framebuffer using drmModeAddFB and assigns it to a CRTC with drmModeSetCrtc. At that point, the display controller scans out the rendered image directly to the monitor.

The rendering path shown in the illustrationβ€”Application β†’ OpenGL ES (Mesa/Freedreno) β†’ EGL β†’ GBM β†’ GEM β†’ DRM/KMS β†’ CRTC/DPU β†’ Display β€” is therefore not merely conceptual but directly reflected in the code.

One important detail in the code is that OpenGL ES works with floating-point values for vertex attributes such as positions and colors. Furthermore the coordinate system, ranges from –1.0 to +1.0 in both the x and y directions. RGB colors range also from 0.0 to 1.0. A final mapping to screen coordinates is done with glViewport().

// Build:
// gcc ogl-min-line-perf.c -o ogl-min-line-perf 
//         $(pkg-config --cflags --libs egl glesv2 gbm libdrm)

#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <stdint.h>

#include <xf86drm.h>
#include <xf86drmMode.h>
#include <gbm.h>
#include <EGL/egl.h>
#include <GLES3/gl3.h>

typedef struct {
    int drm_fd;
    int screen_width;
    int screen_height;

    drmModeModeInfo mode;
    drmModeConnector *connector;
    drmModeEncoder   *encoder;

    struct gbm_device  *gbm_device;
    struct gbm_surface *gbm_surface;
    struct gbm_bo *previous_bo;
    uint32_t previous_framebuffer;

    EGLDisplay egl_display;
    EGLConfig  egl_config;
    EGLContext egl_context;
    EGLSurface egl_surface;

    GLuint shader_program;
    GLuint vertex_array_object;
    GLuint vertex_buffer_object;

} GraphicsContext;

static inline double get_seconds()
{
   struct timespec ts;
   clock_gettime(CLOCK_MONOTONIC,&ts);
   return ts.tv_sec + ts.tv_nsec * 1e-9;
}

static GLuint create_shader(GLenum type, const char *source)
{
    GLuint shader = glCreateShader(type);
    glShaderSource(shader, 1, &source, 0);
    glCompileShader(shader);
    return shader;
}

static GLuint create_program(const char *vs, const char *fs)
{
    GLuint program = glCreateProgram();
    glAttachShader(program, create_shader(GL_VERTEX_SHADER, vs));
    glAttachShader(program, create_shader(GL_FRAGMENT_SHADER, fs));
    glLinkProgram(program);
    return program;
}

static GraphicsContext graphics_init(void)
{
    GraphicsContext gfx = {0};

    gfx.drm_fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);

    drmModeRes *resources = drmModeGetResources(gfx.drm_fd);
    gfx.connector = drmModeGetConnector(gfx.drm_fd, resources->connectors[0]);
    gfx.mode = gfx.connector->modes[0];
    gfx.encoder = drmModeGetEncoder(gfx.drm_fd, gfx.connector->encoder_id);

    gfx.screen_width  = gfx.mode.hdisplay;
    gfx.screen_height = gfx.mode.vdisplay;

    gfx.gbm_device = gbm_create_device(gfx.drm_fd);

    gfx.egl_display = eglGetDisplay((EGLNativeDisplayType)gfx.gbm_device);
    eglInitialize(gfx.egl_display, 0, 0);
    eglBindAPI(EGL_OPENGL_ES_API);

    EGLint config_attributes[] = {
        EGL_SURFACE_TYPE,    EGL_WINDOW_BIT,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
        EGL_RED_SIZE,   8,
        EGL_GREEN_SIZE, 8,
        EGL_BLUE_SIZE,  8,
        EGL_ALPHA_SIZE, 8,
        EGL_NONE
    };

    EGLint num_configs;
    eglChooseConfig(gfx.egl_display, config_attributes, &gfx.egl_config, 1, &num_configs);

    EGLint format;
    eglGetConfigAttrib(gfx.egl_display, gfx.egl_config, EGL_NATIVE_VISUAL_ID, &format);

    gfx.gbm_surface = gbm_surface_create(
        gfx.gbm_device,
        gfx.screen_width,
        gfx.screen_height,
        format,
        GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING
    );

    gfx.egl_context = eglCreateContext(
        gfx.egl_display,
        gfx.egl_config,
        EGL_NO_CONTEXT,
        (EGLint[]){ EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }
    );

    gfx.egl_surface = eglCreateWindowSurface(
        gfx.egl_display,
        gfx.egl_config,
        (EGLNativeWindowType)gfx.gbm_surface,
        0
    );

    eglMakeCurrent(gfx.egl_display,
                   gfx.egl_surface,
                   gfx.egl_surface,
                   gfx.egl_context);

    const char *vertex_shader_source =
        "#version 300 es\n"
        "layout(location=0) in vec2 position;"
        "layout(location=1) in vec4 color;"
        "out vec4 vColor;"
        "void main(){"
        "vColor = color;"
        "gl_Position = vec4(position,0.0,1.0);"
        "}";

    const char *fragment_shader_source =
        "#version 300 es\n"
        "precision mediump float;"
        "in vec4 vColor;"
        "out vec4 fragColor;"
        "void main(){"
        "fragColor = vColor;"
        "}";

    gfx.shader_program = create_program(vertex_shader_source,
                                        fragment_shader_source);

    glUseProgram(gfx.shader_program);

    glGenVertexArrays(1, &gfx.vertex_array_object);
    glBindVertexArray(gfx.vertex_array_object);

    glGenBuffers(1, &gfx.vertex_buffer_object);
    glBindBuffer(GL_ARRAY_BUFFER, gfx.vertex_buffer_object);

    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(float), 0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(2 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glViewport(0, 0, gfx.screen_width, gfx.screen_height);

    eglSwapInterval(gfx.egl_display, 0);

    return gfx;
}

static void graphics_present(GraphicsContext *gfx)
{
    struct gbm_bo *new_bo = gbm_surface_lock_front_buffer(gfx->gbm_surface);

    uint32_t new_fb;
    drmModeAddFB(gfx->drm_fd, gfx->screen_width, gfx->screen_height,
                 24, 32, gbm_bo_get_stride(new_bo),gbm_bo_get_handle(new_bo).u32,
                 &new_fb);

    drmModeSetCrtc(gfx->drm_fd, gfx->encoder->crtc_id, new_fb, 0, 0,
                   &gfx->connector->connector_id, 1, &gfx->mode);

    if (gfx->previous_framebuffer) drmModeRmFB(gfx->drm_fd, gfx->previous_framebuffer);
    if (gfx->previous_bo) gbm_surface_release_buffer(gfx->gbm_surface, gfx->previous_bo);

    gfx->previous_bo = new_bo;
    gfx->previous_framebuffer = new_fb;
}

int main(void)
{
    GraphicsContext gfx = graphics_init();

    int line_count = 100000;
    int vertices_per_line = 2;
    int total_vertices = line_count * vertices_per_line;
    int floats_per_vertex = 6;

    size_t vertex_buffer_size = (size_t)total_vertices * (size_t)floats_per_vertex * sizeof(float);

    float *vertex_data = malloc(vertex_buffer_size);

    glBufferData(GL_ARRAY_BUFFER, vertex_buffer_size, 0, GL_STREAM_DRAW);

    srandom(time(0));

    glClear(GL_COLOR_BUFFER_BIT);
    eglSwapBuffers(gfx.egl_display, gfx.egl_surface);
    graphics_present(&gfx);

    for (;;)
    {
        double t0 = get_seconds();
        for (int i = 0; i < line_count; i++)
        {
            int x0 = random() % gfx.screen_width;
            int y0 = random() % gfx.screen_height;
            int x1 = random() % gfx.screen_width;
            int y1 = random() % gfx.screen_height;

            float fx0 = 2.0f * x0 / (gfx.screen_width - 1) - 1.0f;
            float fy0 = 1.0f - 2.0f * y0 / (gfx.screen_height - 1);

            float fx1 = 2.0f * x1 / (gfx.screen_width - 1) - 1.0f;
            float fy1 = 1.0f - 2.0f * y1 / (gfx.screen_height - 1);

            float r = (random() % 256) / 255.0f;
            float g = (random() % 256) / 255.0f;
            float b = (random() % 256) / 255.0f;

            int base = i * vertices_per_line * floats_per_vertex;

            vertex_data[base + 0]  = fx0;
            vertex_data[base + 1]  = fy0;
            vertex_data[base + 2]  = r;
            vertex_data[base + 3]  = g;
            vertex_data[base + 4]  = b;
            vertex_data[base + 5]  = 1.0f;

            vertex_data[base + 6]  = fx1;
            vertex_data[base + 7]  = fy1;
            vertex_data[base + 8]  = r;
            vertex_data[base + 9]  = g;
            vertex_data[base + 10] = b;
            vertex_data[base + 11] = 1.0f;
        }
        double t1 = get_seconds();
        glClear(GL_COLOR_BUFFER_BIT);
        glBufferData(GL_ARRAY_BUFFER, vertex_buffer_size, NULL, GL_STREAM_DRAW); // orphan
        glBufferSubData(GL_ARRAY_BUFFER, 0, vertex_buffer_size, vertex_data);
        glDrawArrays(GL_LINES, 0, total_vertices);

        eglSwapBuffers(gfx.egl_display, gfx.egl_surface);
        graphics_present(&gfx);

        double t2 = get_seconds();
        printf("Create Vert: %.6f sec \n", (t1 - t0));
        printf("Draw Lines : %.6f sec \n", (t2 - t1));
        printf("Total Time : %.6f sec \n \n", (t2 - t0));
        sleep(1);
    }

    return 0;
}

These are the GPU accelerated results for plotting 100.000 lines.

Create Vert : 0.085 sec
Draw Lines : 0.149 sec
Total Time : 0.234 sec

Compared with the single-threaded CPU reference time of 2.81 s, and the GPU-accelerated total time of 0.234 s, the speed-up is:

Speed-up= 2.81/ 0.234 β‰ˆ 12.0

So we obtain a speed-up of approximately 12Γ—

This benchmark is only meant as a demonstration. GPUs are primarily optimized for 3D workloads, especially triangle rasterization and shading, not for classic 2D line drawing. However, the example still illustrates the architectural advantage: once expressed as a draw call, the GPU performs rasterization and pixel processing massively in parallel, whereas a CPU-based renderer typically executes per-pixel work sequentially and is limited by memory bandwidth.

Switch from direct DRM/KMS rendering to Wayland/Weston rendering

So far we have rendered graphics directly through the Linux graphics stack using DRM/KMS, with framebuffers or GPU buffers created via GBM and rendered through EGL. In this setup our application talks directly to the graphics subsystem and is responsible for producing the final image that is scanned out to the display.

However, as soon as we want to run multiple graphical programs at the same time, things become more complicated. We need a way to coordinate which application draws where on the screen and which program should receive user input such as mouse or keyboard events.

This is where Wayland comes in. Wayland defines a protocol that standardizes the communication between graphical applications and a display server. Instead of rendering directly to the screen, applications render their graphics into their own buffers (for example GBM/EGL GPU buffers or shared-memory buffers) and submit these buffers to the display server.

In the Wayland architecture this display server is called the compositor. The compositor collects the buffers from all running applications, composes them into the final image that is displayed on the screen, and routes input events to the appropriate client.

Weston is the reference implementation of such a Wayland compositor and is often used as a lightweight compositor on embedded systems.

In other words, when moving from a pure DRM/KMS setup to a Wayland system, we introduce an additional layer: applications become Wayland clients, and the compositor (for example Weston) takes over the responsibility of composing the final display image and managing input.

Installing Wayland and Weston on Debian Trixie

On Debian Trixie the necessary components can be installed directly from the package repository.

sudo apt install weston wayland-utils wayland-protocols

The packages provide:

Package Purpose
weston reference Wayland compositor
wayland-utils diagnostic tools (wayland-info)
wayland-protocols XML protocol definitions such as xdg-shell

Now Weston can be started directly on the DRM/KMS backend:

weston --backend=drm --renderer=gl

often even the following is sufficient

weston

If everything works, Weston will open a simple desktop environment.

From a DRM/KMS Application to a Wayland Client

Our previous example rendered directly to the screen using:

  • DRM/KMS
  • GBM
  • EGL
  • OpenGL ES

The application created scanout buffers and called drmModeSetCrtc() to present them.

In a Wayland system this is no longer possible. Only the compositor (Weston) talks to DRM/KMS. Applications become Wayland clients that render into buffers which are then submitted to the compositor. Conceptually the rendering pipeline now looks like this:

Application
β”‚ OpenGL ES / EGL
β–Ό
Wayland buffer
β–Ό
Weston compositor
β–Ό
DRM / KMS
β–Ό
Display

The OpenGL rendering itself stays almost identical. What changes is the window creation and buffer presentation. Instead of:

GBM surface β†’ DRM framebuffer β†’ drmModeSetCrtc

we now use:

wl_surface β†’ wl_egl_window β†’ eglSwapBuffers β†’ compositor

The following example is the Wayland version of the previous DRM/KMS line-rendering benchmark. The rendering code is mostly unchanged. The additional code is mainly needed for:

  • connecting to the Wayland server
  • discovering global interfaces
  • creating a surface
  • attaching the xdg-shell window role
  • handling configure events from the compositor

The core differences compared to the DRM/KMS version are:

1. Connect to the Wayland server
wl_display_connect(NULL);

2. Discover compositor interfaces
wl_registry_add_listener(...)

3. Create a surface and window role

wl_surface
xdg_surface
xdg_toplevel

4. Create an EGL window

wl_egl_window
eglCreateWindowSurface

5. Render and swap buffers
eglSwapBuffers()

After eglSwapBuffers() the compositor takes care of presenting the frame. Unlike the DRM version, the client does not control scanout anymore.

// Build preparation:
//
// xdg-shell
// wayland-scanner client-header \
//   /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml \
//   xdg-shell-client-protocol.h
//
// wayland-scanner private-code \
//   /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml \
//   xdg-shell-protocol.c
//
// presentation-time
// On many systems:
// wayland-scanner client-header \
//   /usr/share/wayland-protocols/staging/presentation-time/presentation-time.xml \
//   presentation-time-client-protocol.h
//
// wayland-scanner private-code \
//   /usr/share/wayland-protocols/staging/presentation-time/presentation-time.xml \
//   presentation-time-protocol.c
//
// On some older systems the XML may instead be here:
//   /usr/share/wayland-protocols/unstable/presentation-time/presentation-time.xml
//
// Build:
// gcc ogl-min-line-perf-wayland-present.c \
//     xdg-shell-protocol.c presentation-time-protocol.c \
//     -o ogl-min-line-perf-wayland-present \
//     $(pkg-config --cflags --libs wayland-client wayland-egl egl glesv2)

#include <wayland-client.h>
#include <wayland-egl.h>
#include "xdg-shell-client-protocol.h"
#include "presentation-time-client-protocol.h"

#include <EGL/egl.h>
#include <GLES3/gl3.h>

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <inttypes.h>

typedef struct {
    struct wl_display    *display;
    struct wl_registry   *registry;
    struct wl_compositor *compositor;
    struct wl_surface    *surface;

    struct xdg_wm_base   *wm_base;
    struct xdg_surface   *xdg_surface;
    struct xdg_toplevel  *xdg_toplevel;

    struct wp_presentation *presentation;

    struct wl_egl_window *egl_window;

    EGLDisplay egl_display;
    EGLConfig  egl_config;
    EGLContext egl_context;
    EGLSurface egl_surface;

    GLuint shader_program;
    GLuint vertex_array_object;
    GLuint vertex_buffer_object;

    int width;
    int height;
    int configured;
    int running;

    int presentation_clock_id;
    int have_presentation_clock;

    struct wl_callback *frame_callback;
    struct wp_presentation_feedback *presentation_feedback;

    uint64_t t_frame_begin_ns;
    uint64_t t_vertices_done_ns;
    uint64_t t_draw_begin_ns;
    uint64_t t_gpu_done_ns;
    uint64_t t_swap_return_ns;
    uint64_t t_frame_done_ns;
    uint64_t t_present_ns;

    int frame_done_received;
    int presentation_received;
    int presentation_discarded;
} GraphicsContext;

static void fatal(const char *msg)
{
    fprintf(stderr, "%s\n", msg);
    exit(1);
}

static void check_egl_bool(const char *what, EGLBoolean ok)
{
    if (!ok) {
        EGLint err = eglGetError();
        fprintf(stderr, "%s failed, EGL error = 0x%04x\n", what, err);
        exit(1);
    }
}

static uint64_t get_time_ns_from_clock(clockid_t clk)
{
    struct timespec ts;
    if (clock_gettime(clk, &ts) != 0) {
        perror("clock_gettime");
        exit(1);
    }
    return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
}

static uint64_t get_now_ns(const GraphicsContext *gfx)
{
    clockid_t clk = CLOCK_MONOTONIC;
    if (gfx->have_presentation_clock) {
        clk = (clockid_t)gfx->presentation_clock_id;
    }
    return get_time_ns_from_clock(clk);
}

static double ns_to_ms(uint64_t ns)
{
    return (double)ns / 1e6;
}

static GLuint create_shader(GLenum type, const char *source)
{
    GLuint shader = glCreateShader(type);
    if (!shader) {
        fatal("glCreateShader failed");
    }

    glShaderSource(shader, 1, &source, NULL);
    glCompileShader(shader);

    GLint ok = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
    if (!ok) {
        char log[2048];
        glGetShaderInfoLog(shader, sizeof(log), NULL, log);
        fprintf(stderr, "Shader compile error: %s\n", log);
        exit(1);
    }
    return shader;
}

static GLuint create_program(const char *vs, const char *fs)
{
    GLuint v = create_shader(GL_VERTEX_SHADER, vs);
    GLuint f = create_shader(GL_FRAGMENT_SHADER, fs);

    GLuint program = glCreateProgram();
    if (!program) {
        fatal("glCreateProgram failed");
    }

    glAttachShader(program, v);
    glAttachShader(program, f);
    glLinkProgram(program);

    GLint ok = 0;
    glGetProgramiv(program, GL_LINK_STATUS, &ok);
    if (!ok) {
        char log[2048];
        glGetProgramInfoLog(program, sizeof(log), NULL, log);
        fprintf(stderr, "Program link error: %s\n", log);
        exit(1);
    }

    glDeleteShader(v);
    glDeleteShader(f);

    return program;
}

/* ---------------- xdg_wm_base ---------------- */

static void handle_wm_base_ping(void *data, struct xdg_wm_base *wm_base, uint32_t serial)
{
    (void)data;
    xdg_wm_base_pong(wm_base, serial);
}

static const struct xdg_wm_base_listener wm_base_listener = {
    .ping = handle_wm_base_ping
};

/* ---------------- xdg_surface ---------------- */

static void handle_xdg_surface_configure(void *data, struct xdg_surface *surface, uint32_t serial)
{
    GraphicsContext *gfx = (GraphicsContext *)data;
    xdg_surface_ack_configure(surface, serial);
    gfx->configured = 1;
}

static const struct xdg_surface_listener xdg_surface_listener = {
    .configure = handle_xdg_surface_configure
};

/* ---------------- xdg_toplevel ---------------- */

static void handle_toplevel_configure(void *data,
                                      struct xdg_toplevel *toplevel,
                                      int32_t width,
                                      int32_t height,
                                      struct wl_array *states)
{
    (void)toplevel;
    (void)states;

    GraphicsContext *gfx = (GraphicsContext *)data;

    if (width > 0) {
        gfx->width = width;
    }
    if (height > 0) {
        gfx->height = height;
    }

    if (gfx->egl_window) {
        wl_egl_window_resize(gfx->egl_window, gfx->width, gfx->height, 0, 0);
        glViewport(0, 0, gfx->width, gfx->height);
    }
}

static void handle_toplevel_close(void *data, struct xdg_toplevel *toplevel)
{
    (void)toplevel;
    GraphicsContext *gfx = (GraphicsContext *)data;
    gfx->running = 0;
}

static const struct xdg_toplevel_listener toplevel_listener = {
    .configure = handle_toplevel_configure,
    .close = handle_toplevel_close
};

/* ---------------- wl_surface.frame ---------------- */

static void handle_frame_done(void *data, struct wl_callback *cb, uint32_t callback_data)
{
    (void)callback_data;

    GraphicsContext *gfx = (GraphicsContext *)data;
    gfx->t_frame_done_ns = get_now_ns(gfx);
    gfx->frame_done_received = 1;

    if (cb) {
        wl_callback_destroy(cb);
    }
    if (gfx->frame_callback == cb) {
        gfx->frame_callback = NULL;
    }
}

static const struct wl_callback_listener frame_listener = {
    .done = handle_frame_done
};

/* ---------------- wp_presentation ---------------- */

static void handle_presentation_clock_id(void *data,
                                         struct wp_presentation *presentation,
                                         uint32_t clk_id)
{
    (void)presentation;
    GraphicsContext *gfx = (GraphicsContext *)data;
    gfx->presentation_clock_id = (int)clk_id;
    gfx->have_presentation_clock = 1;
    fprintf(stderr, "presentation clock_id = %u\n", clk_id);
}

static const struct wp_presentation_listener presentation_listener = {
    .clock_id = handle_presentation_clock_id
};

static void handle_feedback_sync_output(void *data,
                                        struct wp_presentation_feedback *feedback,
                                        struct wl_output *output)
{
    (void)data;
    (void)feedback;
    (void)output;
}

static void handle_feedback_presented(void *data,
                                      struct wp_presentation_feedback *feedback,
                                      uint32_t tv_sec_hi,
                                      uint32_t tv_sec_lo,
                                      uint32_t tv_nsec,
                                      uint32_t refresh,
                                      uint32_t seq_hi,
                                      uint32_t seq_lo,
                                      uint32_t flags)
{
    (void)refresh;
    (void)seq_hi;
    (void)seq_lo;
    (void)flags;

    GraphicsContext *gfx = (GraphicsContext *)data;
    uint64_t sec = ((uint64_t)tv_sec_hi << 32) | (uint64_t)tv_sec_lo;
    gfx->t_present_ns = sec * 1000000000ull + (uint64_t)tv_nsec;
    gfx->presentation_received = 1;

    if (feedback) {
        wp_presentation_feedback_destroy(feedback);
    }
    if (gfx->presentation_feedback == feedback) {
        gfx->presentation_feedback = NULL;
    }
}

static void handle_feedback_discarded(void *data,
                                      struct wp_presentation_feedback *feedback)
{
    GraphicsContext *gfx = (GraphicsContext *)data;
    gfx->presentation_discarded = 1;

    if (feedback) {
        wp_presentation_feedback_destroy(feedback);
    }
    if (gfx->presentation_feedback == feedback) {
        gfx->presentation_feedback = NULL;
    }
}

static const struct wp_presentation_feedback_listener presentation_feedback_listener = {
    .sync_output = handle_feedback_sync_output,
    .presented   = handle_feedback_presented,
    .discarded   = handle_feedback_discarded
};

/* ---------------- registry ---------------- */

static void registry_global(void *data,
                            struct wl_registry *registry,
                            uint32_t name,
                            const char *interface,
                            uint32_t version)
{
    GraphicsContext *gfx = (GraphicsContext *)data;

    if (strcmp(interface, wl_compositor_interface.name) == 0) {
        uint32_t bind_version = version < 4 ? version : 4;
        gfx->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, bind_version);
    } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
        gfx->wm_base = wl_registry_bind(registry, name, &xdg_wm_base_interface, 1);
    } else if (strcmp(interface, wp_presentation_interface.name) == 0) {
        uint32_t bind_version = version < 1 ? version : 1;
        gfx->presentation = wl_registry_bind(registry, name, &wp_presentation_interface, bind_version);
    }
}

static void registry_global_remove(void *data, struct wl_registry *registry, uint32_t name)
{
    (void)data;
    (void)registry;
    (void)name;
}

static const struct wl_registry_listener registry_listener = {
    .global = registry_global,
    .global_remove = registry_global_remove
};

static void process_wayland_once(GraphicsContext *gfx)
{
    wl_display_dispatch_pending(gfx->display);
    wl_display_flush(gfx->display);
}

static void reset_frame_measurements(GraphicsContext *gfx)
{
    gfx->frame_done_received = 0;
    gfx->presentation_received = 0;
    gfx->presentation_discarded = 0;
    gfx->t_frame_done_ns = 0;
    gfx->t_present_ns = 0;
}

static void destroy_pending_callbacks(GraphicsContext *gfx)
{
    if (gfx->frame_callback) {
        wl_callback_destroy(gfx->frame_callback);
        gfx->frame_callback = NULL;
    }

    if (gfx->presentation_feedback) {
        wp_presentation_feedback_destroy(gfx->presentation_feedback);
        gfx->presentation_feedback = NULL;
    }
}

static void wait_for_presentation_feedback(GraphicsContext *gfx)
{
    while (gfx->running &&
           !gfx->presentation_received &&
           !gfx->presentation_discarded) {
        if (wl_display_dispatch(gfx->display) < 0) {
            fprintf(stderr, "wl_display_dispatch failed while waiting for presentation feedback\n");
            gfx->running = 0;
            break;
        }
    }
}

static void graphics_init(GraphicsContext *gfx)
{
    memset(gfx, 0, sizeof(*gfx));

    gfx->width = 1920;
    gfx->height = 1080;
    gfx->running = 1;
    gfx->egl_display = EGL_NO_DISPLAY;
    gfx->egl_context = EGL_NO_CONTEXT;
    gfx->egl_surface = EGL_NO_SURFACE;

    gfx->display = wl_display_connect(NULL);
    if (!gfx->display) {
        fatal("Cannot connect to Wayland display");
    }

    gfx->registry = wl_display_get_registry(gfx->display);
    if (!gfx->registry) {
        fatal("wl_display_get_registry failed");
    }

    wl_registry_add_listener(gfx->registry, &registry_listener, gfx);
    wl_display_roundtrip(gfx->display);
    wl_display_roundtrip(gfx->display);

    if (!gfx->compositor || !gfx->wm_base) {
        fatal("Missing required Wayland globals");
    }

    if (gfx->presentation) {
        wp_presentation_add_listener(gfx->presentation, &presentation_listener, gfx);
        wl_display_roundtrip(gfx->display);
    } else {
        fprintf(stderr, "Warning: compositor does not advertise wp_presentation\n");
        fprintf(stderr, "Actual display presentation timestamps will not be available\n");
    }

    xdg_wm_base_add_listener(gfx->wm_base, &wm_base_listener, gfx);

    gfx->surface = wl_compositor_create_surface(gfx->compositor);
    if (!gfx->surface) {
        fatal("wl_compositor_create_surface failed");
    }

    gfx->xdg_surface = xdg_wm_base_get_xdg_surface(gfx->wm_base, gfx->surface);
    if (!gfx->xdg_surface) {
        fatal("xdg_wm_base_get_xdg_surface failed");
    }
    xdg_surface_add_listener(gfx->xdg_surface, &xdg_surface_listener, gfx);

    gfx->xdg_toplevel = xdg_surface_get_toplevel(gfx->xdg_surface);
    if (!gfx->xdg_toplevel) {
        fatal("xdg_surface_get_toplevel failed");
    }

    xdg_toplevel_set_title(gfx->xdg_toplevel, "ogl-min-line-perf-wayland-present");
    xdg_toplevel_set_app_id(gfx->xdg_toplevel, "ogl-min-line-perf-wayland-present");
    xdg_toplevel_add_listener(gfx->xdg_toplevel, &toplevel_listener, gfx);

    wl_surface_commit(gfx->surface);

    while (!gfx->configured) {
        if (wl_display_dispatch(gfx->display) < 0) {
            fatal("wl_display_dispatch failed during initial configure");
        }
    }

    gfx->egl_display = eglGetDisplay((EGLNativeDisplayType)gfx->display);
    if (gfx->egl_display == EGL_NO_DISPLAY) {
        fatal("eglGetDisplay failed");
    }

    check_egl_bool("eglInitialize", eglInitialize(gfx->egl_display, NULL, NULL));
    check_egl_bool("eglBindAPI", eglBindAPI(EGL_OPENGL_ES_API));

    EGLint config_attributes[] = {
        EGL_SURFACE_TYPE,    EGL_WINDOW_BIT,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
        EGL_RED_SIZE,        8,
        EGL_GREEN_SIZE,      8,
        EGL_BLUE_SIZE,       8,
        EGL_ALPHA_SIZE,      8,
        EGL_NONE
    };

    EGLint num_configs = 0;
    check_egl_bool("eglChooseConfig",
                   eglChooseConfig(gfx->egl_display,
                                   config_attributes,
                                   &gfx->egl_config,
                                   1,
                                   &num_configs));

    if (num_configs < 1) {
        fatal("No suitable EGL config found");
    }

    gfx->egl_context = eglCreateContext(
        gfx->egl_display,
        gfx->egl_config,
        EGL_NO_CONTEXT,
        (EGLint[]){ EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }
    );
    if (gfx->egl_context == EGL_NO_CONTEXT) {
        fprintf(stderr, "eglCreateContext failed, EGL error = 0x%04x\n", eglGetError());
        exit(1);
    }

    gfx->egl_window = wl_egl_window_create(gfx->surface, gfx->width, gfx->height);
    if (!gfx->egl_window) {
        fatal("wl_egl_window_create failed");
    }

    gfx->egl_surface = eglCreateWindowSurface(
        gfx->egl_display,
        gfx->egl_config,
        (EGLNativeWindowType)gfx->egl_window,
        NULL
    );
    if (gfx->egl_surface == EGL_NO_SURFACE) {
        fprintf(stderr, "eglCreateWindowSurface failed, EGL error = 0x%04x\n", eglGetError());
        exit(1);
    }

    check_egl_bool("eglMakeCurrent",
                   eglMakeCurrent(gfx->egl_display,
                                  gfx->egl_surface,
                                  gfx->egl_surface,
                                  gfx->egl_context));

    {
        const char *vertex_shader_source =
            "#version 300 es\n"
            "layout(location=0) in vec2 position;\n"
            "layout(location=1) in vec4 color;\n"
            "out vec4 vColor;\n"
            "void main(){\n"
            "    vColor = color;\n"
            "    gl_Position = vec4(position, 0.0, 1.0);\n"
            "}\n";

        const char *fragment_shader_source =
            "#version 300 es\n"
            "precision mediump float;\n"
            "in vec4 vColor;\n"
            "out vec4 fragColor;\n"
            "void main(){\n"
            "    fragColor = vColor;\n"
            "}\n";

        gfx->shader_program = create_program(vertex_shader_source, fragment_shader_source);
    }

    glUseProgram(gfx->shader_program);

    glGenVertexArrays(1, &gfx->vertex_array_object);
    glBindVertexArray(gfx->vertex_array_object);

    glGenBuffers(1, &gfx->vertex_buffer_object);
    glBindBuffer(GL_ARRAY_BUFFER, gfx->vertex_buffer_object);

    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 6 * (GLsizei)sizeof(float), (const void *)0);
    glEnableVertexAttribArray(0);

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 6 * (GLsizei)sizeof(float),
                          (const void *)(2 * sizeof(float)));
    glEnableVertexAttribArray(1);

    glViewport(0, 0, gfx->width, gfx->height);

    if (!eglSwapInterval(gfx->egl_display, 1)) {
        fprintf(stderr, "Warning: eglSwapInterval(1) failed, EGL error = 0x%04x\n", eglGetError());
    }
}

static void cleanup(GraphicsContext *gfx)
{
    destroy_pending_callbacks(gfx);

    if (gfx->shader_program) {
        glDeleteProgram(gfx->shader_program);
        gfx->shader_program = 0;
    }

    if (gfx->vertex_buffer_object) {
        glDeleteBuffers(1, &gfx->vertex_buffer_object);
        gfx->vertex_buffer_object = 0;
    }

    if (gfx->vertex_array_object) {
        glDeleteVertexArrays(1, &gfx->vertex_array_object);
        gfx->vertex_array_object = 0;
    }

    if (gfx->egl_display != EGL_NO_DISPLAY) {
        eglMakeCurrent(gfx->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);

        if (gfx->egl_surface != EGL_NO_SURFACE) {
            eglDestroySurface(gfx->egl_display, gfx->egl_surface);
            gfx->egl_surface = EGL_NO_SURFACE;
        }

        if (gfx->egl_context != EGL_NO_CONTEXT) {
            eglDestroyContext(gfx->egl_display, gfx->egl_context);
            gfx->egl_context = EGL_NO_CONTEXT;
        }

        eglTerminate(gfx->egl_display);
        gfx->egl_display = EGL_NO_DISPLAY;
    }

    if (gfx->egl_window) {
        wl_egl_window_destroy(gfx->egl_window);
        gfx->egl_window = NULL;
    }

    if (gfx->xdg_toplevel) {
        xdg_toplevel_destroy(gfx->xdg_toplevel);
        gfx->xdg_toplevel = NULL;
    }

    if (gfx->xdg_surface) {
        xdg_surface_destroy(gfx->xdg_surface);
        gfx->xdg_surface = NULL;
    }

    if (gfx->surface) {
        wl_surface_destroy(gfx->surface);
        gfx->surface = NULL;
    }

    if (gfx->presentation) {
        wp_presentation_destroy(gfx->presentation);
        gfx->presentation = NULL;
    }

    if (gfx->wm_base) {
        xdg_wm_base_destroy(gfx->wm_base);
        gfx->wm_base = NULL;
    }

    if (gfx->compositor) {
        wl_compositor_destroy(gfx->compositor);
        gfx->compositor = NULL;
    }

    if (gfx->registry) {
        wl_registry_destroy(gfx->registry);
        gfx->registry = NULL;
    }

    if (gfx->display) {
        wl_display_disconnect(gfx->display);
        gfx->display = NULL;
    }
}

int main(void)
{
    GraphicsContext gfx;
    graphics_init(&gfx);

    const int line_count = 100000;
    const int vertices_per_line = 2;
    const int total_vertices = line_count * vertices_per_line;
    const int floats_per_vertex = 6;

    size_t vertex_buffer_size =
        (size_t)total_vertices * (size_t)floats_per_vertex * sizeof(float);

    float *vertex_data = (float *)malloc(vertex_buffer_size);
    if (!vertex_data) {
        fprintf(stderr, "malloc failed\n");
        cleanup(&gfx);
        return 1;
    }

    glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)vertex_buffer_size, NULL, GL_STREAM_DRAW);

    srandom((unsigned int)time(NULL));

    while (gfx.running) {
        process_wayland_once(&gfx);
        reset_frame_measurements(&gfx);

        gfx.t_frame_begin_ns = get_now_ns(&gfx);

        for (int i = 0; i < line_count; i++) {
            int x0 = (int)(random() % gfx.width);
            int y0 = (int)(random() % gfx.height);
            int x1 = (int)(random() % gfx.width);
            int y1 = (int)(random() % gfx.height);

            float fx0 = 2.0f * (float)x0 / (float)(gfx.width - 1) - 1.0f;
            float fy0 = 1.0f - 2.0f * (float)y0 / (float)(gfx.height - 1);

            float fx1 = 2.0f * (float)x1 / (float)(gfx.width - 1) - 1.0f;
            float fy1 = 1.0f - 2.0f * (float)y1 / (float)(gfx.height - 1);

            float r = (float)(random() % 256) / 255.0f;
            float g = (float)(random() % 256) / 255.0f;
            float b = (float)(random() % 256) / 255.0f;

            int base = i * vertices_per_line * floats_per_vertex;

            vertex_data[base + 0]  = fx0;
            vertex_data[base + 1]  = fy0;
            vertex_data[base + 2]  = r;
            vertex_data[base + 3]  = g;
            vertex_data[base + 4]  = b;
            vertex_data[base + 5]  = 1.0f;

            vertex_data[base + 6]  = fx1;
            vertex_data[base + 7]  = fy1;
            vertex_data[base + 8]  = r;
            vertex_data[base + 9]  = g;
            vertex_data[base + 10] = b;
            vertex_data[base + 11] = 1.0f;
        }

        gfx.t_vertices_done_ns = get_now_ns(&gfx);
        gfx.t_draw_begin_ns = get_now_ns(&gfx);

        glClear(GL_COLOR_BUFFER_BIT);
        glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)vertex_buffer_size, NULL, GL_STREAM_DRAW);
        glBufferSubData(GL_ARRAY_BUFFER, 0, (GLsizeiptr)vertex_buffer_size, vertex_data);
        glDrawArrays(GL_LINES, 0, total_vertices);

        {
            GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
            if (!fence) {
                fprintf(stderr, "glFenceSync failed\n");
                break;
            }

            glFlush();

            for (;;) {
                GLenum wait_result = glClientWaitSync(
                    fence,
                    GL_SYNC_FLUSH_COMMANDS_BIT,
                    1000000000ull
                );

                if (wait_result == GL_ALREADY_SIGNALED ||
                    wait_result == GL_CONDITION_SATISFIED) {
                    break;
                }

                if (wait_result == GL_WAIT_FAILED) {
                    fprintf(stderr, "glClientWaitSync failed\n");
                    glDeleteSync(fence);
                    free(vertex_data);
                    cleanup(&gfx);
                    return 1;
                }
            }

            gfx.t_gpu_done_ns = get_now_ns(&gfx);
            glDeleteSync(fence);
        }

        destroy_pending_callbacks(&gfx);

        gfx.frame_callback = wl_surface_frame(gfx.surface);
        if (!gfx.frame_callback) {
            fprintf(stderr, "wl_surface_frame failed\n");
            break;
        }
        wl_callback_add_listener(gfx.frame_callback, &frame_listener, &gfx);

        if (gfx.presentation) {
            gfx.presentation_feedback = wp_presentation_feedback(gfx.presentation, gfx.surface);
            if (!gfx.presentation_feedback) {
                fprintf(stderr, "wp_presentation_feedback failed\n");
                break;
            }
            wp_presentation_feedback_add_listener(
                gfx.presentation_feedback,
                &presentation_feedback_listener,
                &gfx
            );
        }

        if (!eglSwapBuffers(gfx.egl_display, gfx.egl_surface)) {
            fprintf(stderr, "eglSwapBuffers failed, EGL error = 0x%04x\n", eglGetError());
            break;
        }

        gfx.t_swap_return_ns = get_now_ns(&gfx);

        if (gfx.presentation) {
            wait_for_presentation_feedback(&gfx);
        } else {
            for (int i = 0; i < 10 && gfx.running; i++) {
                process_wayland_once(&gfx);
                usleep(1000);
            }
        }

        printf("Create Vert CPU   : %8.3f ms\n",
               ns_to_ms(gfx.t_vertices_done_ns - gfx.t_frame_begin_ns));

        printf("Draw+Upload submit: %8.3f ms\n",
               ns_to_ms(gfx.t_swap_return_ns - gfx.t_draw_begin_ns));

        printf("GPU done          : %8.3f ms\n",
               ns_to_ms(gfx.t_gpu_done_ns - gfx.t_frame_begin_ns));

        printf("Swap return       : %8.3f ms\n",
               ns_to_ms(gfx.t_swap_return_ns - gfx.t_frame_begin_ns));

        if (gfx.frame_done_received) {
            printf("frame callback    : %8.3f ms\n",
                   ns_to_ms(gfx.t_frame_done_ns - gfx.t_frame_begin_ns));
        } else {
            printf("frame callback    : (not received yet)\n");
        }

        if (gfx.presentation_received) {
            printf("PRESENTED         : %8.3f ms\n",
                   ns_to_ms(gfx.t_present_ns - gfx.t_frame_begin_ns));

        } else if (gfx.presentation_discarded) {
            printf("PRESENTED         : discarded by compositor\n");
        } else {
            printf("PRESENTED         : unavailable\n");
        }

        printf("\n");

        sleep(1);
    }

    free(vertex_data);
    cleanup(&gfx);
    return 0;
}

The Wayland program is much larger than the DRM/KMS version.

Most of these additional lines are not rendering code but protocol handling:

  • Wayland registry discovery
  • xdg-shell window setup
  • configure event handling
  • Wayland event dispatching
  • Wayland presentation-time protocol to measure rendering time

In the DRM/KMS version our application owned the display completely. In the Wayland version it becomes one client among potentially many and must cooperate with the compositor.

The picture shows the line performance test running in fullscreen mode. The terminal window displaying the measurement results is overlaid by the compositor - a really nice feature of the compositor.

Despite the additional composition step in the rendering pipeline, the total rendering time increases only slightly. This is because the compositor does not need to re-render the entire scene. Instead, each application renders into its own buffer, and the compositor combines these buffers during the final display composition.

DRM/KMS rendering : 0.234 s

Weston Wayland rendering : 0.269 s

Ressources:

Repository of Wayland / Weston:

This will probably be the last post in this thread with new content. My experiments with pure Weston programming were more driven by curiosity than by practical reasons.

In this step, I will use the Firefox browser within Weston and use WebGPU for my line performance test. The browser adds another abstraction layer to the workflow, since it takes care of the direct interaction with Weston and the other low-level layers.

WebGPU is a modern web API that provides direct access to the GPU from within the browser. It is designed as the successor of WebGL and allows more efficient and flexible use of graphics hardware. Compared to WebGL, it gives lower-level control over GPU resources and better performance, especially for compute-heavy tasks. WebGPU is based on modern graphics concepts similar to Vulkan, Metal, or Direct3D 12, and can be used not only for rendering but also for general-purpose GPU computations.

To make this work, it is important to use a recent Firefox version (>=147), because WebGPU support is only properly available there.

On Debian-based systems, you can install the latest Firefox version using the Mozilla repository. I prefer doing this manually with nano instead of scripts:

First, create the keyring directory:

sudo install -d -m 0755 /etc/apt/keyrings

Download the Mozilla signing key:

wget https://packages.mozilla.org/apt/repo-signing-key.gpg
sudo mv repo-signing-key.gpg /etc/apt/keyrings/packages.mozilla.org.asc

Then add the repository:

sudo nano /etc/apt/sources.list.d/mozilla.list

Insert:

deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main

Save with CTRL+O, ENTER and exit with CTRL+X.

Next, set the package priority:

sudo nano /etc/apt/preferences.d/mozilla

Insert:

Package: *
Pin: origin packages.mozilla.org
Pin-Priority: 1000

After this:

sudo apt update
sudo apt install firefox

We can start Firefox by just typing firefox into the Weston CLI. To check if everything is working properly we type the following in the URL bar:

about:config

Make sure:

dom.webgpu.enabled = true
gfx.webrender.all = true

Also check in:

about:support

That:

* Compositing = WebRender
* WebGPU = available / enabled

If everything is correct, WebGPU should work directly when starting Firefox inside Weston. Instead of the freedreno OpenGL ES driver webGPU uses the tulip Vulkan driver of the Adreno 702 GPU. And instead of C, we use HTML / JavaScript in the browser.

<!doctype html>
<html lang="de">
<head>
  <meta charset="utf-8">
  <title>WebGPU Line Performance Test</title>
  <style>
    html, body {
      margin: 0;
      width: 100%;
      height: 100%;
      overflow: hidden;
      background: black;
      color: white;
      font-family: monospace;
    }

    #gpuCanvas {
      display: block;
      width: 100vw;
      height: 100vh;
    }

    #overlay {
      position: fixed;
      left: 12px;
      top: 12px;
      padding: 8px 10px;
      background: rgba(0,0,0,0.55);
      white-space: pre;
      pointer-events: none;
    }
  </style>
</head>
<body>
  <canvas id="gpuCanvas"></canvas>
  <div id="overlay">Init...</div>

  <script type="module">
    const canvas = document.getElementById("gpuCanvas");
    const overlay = document.getElementById("overlay");

    const lineCount = 100000;
    const verticesPerLine = 2;
    const floatsPerVertex = 6; // x,y,r,g,b,a
    const totalVertices = lineCount * verticesPerLine;
    const totalFloats = totalVertices * floatsPerVertex;
    const vertexData = new Float32Array(totalFloats);

    function nowSeconds() {
      return performance.now() * 1e-3;
    }
    
    function nextRAF() {
      return new Promise(resolve => requestAnimationFrame(resolve));
    } 

    function resizeCanvasToDisplaySize(device, context, format) {
      const dpr = window.devicePixelRatio || 1;
      const width = Math.max(1, Math.floor(canvas.clientWidth * dpr));
      const height = Math.max(1, Math.floor(canvas.clientHeight * dpr));

      if (canvas.width !== width || canvas.height !== height) {
        canvas.width = width;
        canvas.height = height;

        context.configure({
          device,
          format,
          alphaMode: "opaque",
        });
      }
    }

    function fillRandomLines(width, height) {
      const w1 = Math.max(1, width - 1);
      const h1 = Math.max(1, height - 1);

      for (let i = 0; i < lineCount; i++) {
        const x0 = (Math.random() * width) | 0;
        const y0 = (Math.random() * height) | 0;
        const x1 = (Math.random() * width) | 0;
        const y1 = (Math.random() * height) | 0;

        const fx0 =  2.0 * x0 / w1 - 1.0;
        const fy0 =  1.0 - 2.0 * y0 / h1;

        const fx1 =  2.0 * x1 / w1 - 1.0;
        const fy1 =  1.0 - 2.0 * y1 / h1;

        const r = Math.random();
        const g = Math.random();
        const b = Math.random();

        const base = i * verticesPerLine * floatsPerVertex;

        vertexData[base + 0]  = fx0;
        vertexData[base + 1]  = fy0;
        vertexData[base + 2]  = r;
        vertexData[base + 3]  = g;
        vertexData[base + 4]  = b;
        vertexData[base + 5]  = 1.0;

        vertexData[base + 6]  = fx1;
        vertexData[base + 7]  = fy1;
        vertexData[base + 8]  = r;
        vertexData[base + 9]  = g;
        vertexData[base + 10] = b;
        vertexData[base + 11] = 1.0;
      }
    }

    async function main() {
      if (!("gpu" in navigator)) {
        overlay.textContent = "WebGPU nicht verfΓΌgbar.";
        throw new Error("navigator.gpu not available");
      }

      const adapter = await navigator.gpu.requestAdapter({
        powerPreference: "high-performance",
      });

      if (!adapter) {
        overlay.textContent = "Kein WebGPU-Adapter gefunden.";
        throw new Error("No GPU adapter");
      }

      const device = await adapter.requestDevice();
      const context = canvas.getContext("webgpu");
      const format = navigator.gpu.getPreferredCanvasFormat();

      resizeCanvasToDisplaySize(device, context, format);
      window.addEventListener("resize", () => {
        resizeCanvasToDisplaySize(device, context, format);
      });

      const shaderModule = device.createShaderModule({
        code: `
struct VertexIn {
  @location(0) position : vec2<f32>,
  @location(1) color    : vec4<f32>,
};

struct VertexOut {
  @builtin(position) position : vec4<f32>,
  @location(0) color          : vec4<f32>,
};

@vertex
fn vs_main(v: VertexIn) -> VertexOut {
  var out: VertexOut;
  out.position = vec4<f32>(v.position, 0.0, 1.0);
  out.color = v.color;
  return out;
}

@fragment
fn fs_main(inFrag: VertexOut) -> @location(0) vec4<f32> {
  return inFrag.color;
}
        `
      });

      const pipeline = await device.createRenderPipelineAsync({
        layout: "auto",
        vertex: {
          module: shaderModule,
          entryPoint: "vs_main",
          buffers: [
            {
              arrayStride: 6 * 4,
              attributes: [
                {
                  shaderLocation: 0,
                  offset: 0,
                  format: "float32x2",
                },
                {
                  shaderLocation: 1,
                  offset: 2 * 4,
                  format: "float32x4",
                },
              ],
            },
          ],
        },
        fragment: {
          module: shaderModule,
          entryPoint: "fs_main",
          targets: [{ format }],
        },
        primitive: {
          topology: "line-list",
        },
      });

      const vertexBufferSize = vertexData.byteLength;

      const vertexBuffer = device.createBuffer({
        size: vertexBufferSize,
        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
      });

      let lastPrint = 0.0;

      async function frame() {
        resizeCanvasToDisplaySize(device, context, format);

        const t0 = nowSeconds();

        fillRandomLines(canvas.width, canvas.height);

        const t1 = nowSeconds();

        device.queue.writeBuffer(
          vertexBuffer,
          0,
          vertexData.buffer,
          vertexData.byteOffset,
          vertexData.byteLength
        );

        const encoder = device.createCommandEncoder();

        const pass = encoder.beginRenderPass({
          colorAttachments: [
            {
              view: context.getCurrentTexture().createView(),
              clearValue: { r: 0, g: 0, b: 0, a: 1 },
              loadOp: "clear",
              storeOp: "store",
            }
          ]
        });

        pass.setPipeline(pipeline);
        pass.setVertexBuffer(0, vertexBuffer);
        pass.draw(totalVertices, 1, 0, 0);
        pass.end();

        device.queue.submit([encoder.finish()]);
        const t2 = nowSeconds();

        await device.queue.onSubmittedWorkDone();
        const t3 = nowSeconds();

        await nextRAF();
        const t4 = nowSeconds();

        if (t4 - lastPrint > 0.25) {
          const createMs     = (t1 - t0) * 1000.0;
          const submitMs     = (t2 - t1) * 1000.0;
          const gpuDoneMs    = (t3 - t0) * 1000.0;
          const repaintMs    = (t4 - t0) * 1000.0;
          const estFpsGpu    = gpuDoneMs > 0 ? 1000.0 / gpuDoneMs : 0.0;
          const estFpsPaint  = repaintMs > 0 ? 1000.0 / repaintMs : 0.0;

          overlay.textContent =
            `Lines         : ${lineCount}\n` +
            `Vertices      : ${totalVertices}\n` +
            `Canvas        : ${canvas.width} x ${canvas.height}\n` +
            `Create Vert   : ${createMs.toFixed(3)} ms\n` +
            `Submit        : ${submitMs.toFixed(3)} ms\n` +
            `GPU done      : ${gpuDoneMs.toFixed(3)} ms\n` +
            `Next rAF      : ${repaintMs.toFixed(3)} ms\n` +
            `Est. FPS GPU  : ${estFpsGpu.toFixed(1)}\n` +
            `Est. FPS Paint: ${estFpsPaint.toFixed(1)}`;

          console.log(`Create Vert : ${createMs.toFixed(3)} ms`);
          console.log(`Submit      : ${submitMs.toFixed(3)} ms`);
          console.log(`GPU done    : ${gpuDoneMs.toFixed(3)} ms`);
          console.log(`Next rAF    : ${repaintMs.toFixed(3)} ms\n`);

          lastPrint = t4;
        }

        setTimeout(() => requestAnimationFrame(frame), 1000);
      }

      requestAnimationFrame(frame);
    }

    main().catch(err => {
      console.error(err);
      overlay.textContent = "Fehler:\n" + err.message;
    });
  </script>
</body>
</html>

The script realizes the 100,000 lines performance test. It initializes a GPU adapter and device via navigator.gpu , creates a WebGPU canvas context, and configures it with the preferred swapchain format. All low-level parts are handled by WebGPU. This significantly reduces the size of the script.

A shader module (WGSL) defines a simple vertex and fragment shader. The render pipeline is set up with a vertex buffer layout containing position and color attributes, and uses a line-list primitive topology.

For each frame, new vertex data is generated on the CPU and uploaded to a GPU buffer using device.queue.writeBuffer. A command encoder and render pass are created, the pipeline and vertex buffer are bound, and a draw call is issued. The commands are submitted to the GPU queue, and synchronization is done via onSubmittedWorkDone().

This setup shows the typical WebGPU workflow: device creation, pipeline definition, buffer management, command encoding, and submission to the GPU.

To run the script, a local web server is required, since WebGPU is only available in a secure context and does not work when opening the file directly. A simple way is to use a Python-based server:

python3 -m http.server 8000

Then open the browser and navigate to http://localhost:8000.

Pressing F11 switches Firefox to fullscreen mode:

The measurement results are rendered by the browser as an additional CSS layer.

0.306 s is a slight increase compared to plain Weston/Wayland (0.269 s). But keep in mind that the CPU code is running as JavaScript in the browser engine instead of compiled C code.

So here are the final results of the rudimentary test of drawing 100,000 random lines on a 1920 x 1080 display:

Single-thread CPU rendering on DRM/KMS      : 2.81 s
Multi-threaded CPU rendering on DRM/KMS     : 1.12 s
OpenGL ES GPU rendering on DRM/KMS          : 0.23 s
OpenGL ES GPU rendering on Wayland/Weston   : 0.27 s
Firefox/WebGPU rendering on Wayland/Weston  : 0.31 s

So summing it all up, I will proceed with Firefox/WebGPU. The high level of abstraction and ease of use is worth the slight performance impact compared to plain C code. In my applications, data is mainly transmitted via web protocols, which is a big advantage for a browser-based solution.

To structure this forum thread a little better, I have created a table of contents at the following link:

https://diy-ecg.org/posts/arduino-forum-graphics-stack/

Furthermore, you can find the WebGPU line performance test at the link below. You can run it in a browser of your choice on any platform and compare the results (if WebGPU is supported):

https://diy-ecg.org/webGPU-lineperf.html

For example the Microsoft Edge browser (Win 11) on a PC with a Intel Core i5-11400 and a NVIDIA GeForce RTX3050 produces the following result:

These are the results for the Safari browser on a Silicon M1 MacBook Pro: