Linux Graphics Stack on the Arduino UNO Q

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: