Merlin0216
Merlin0216

Reputation: 39

Writing into framebuffer with a C program is very slow (Raspberry Pi)

I want to make a very intensive simulation. I need as much power as possible from a Raspberry Pi. To make it, I flashed the card with OS Lite (without Desktop) onto a Micro SD Card and wrote to the frame buffer with a C program using this example.

The result is VERY slow. I can see the image being updated and scanning from top to bottom. It is very long: 0.2s or so. This means that I will never get 30 or 60 fps.

Is there a better (and faster) way to do this? The X Window Manager of Raspberry Pi OS must also somehow write to the frame buffer to be able to work, so there must be a faster way...

Upvotes: 3

Views: 3772

Answers (2)

Rachid K.
Rachid K.

Reputation: 5211

Graphic services use an in-memory buffer to make the screen image and write the whole buffer or only the modified parts of the display into the screen device (e.g. clipping).

So, here are two slightly enhanced versions of the program:

  1. The first one writes first in a in-memory buffer and then write the whole buffer into the frame buffer
  2. The second one writes first in a mmapped in-memory file (memfd_create()) and use the sendfile() system call to copy the file into the frame buffer

In both, a call to gettimeofday() has also been added to measure the elapsed time during the display.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <signal.h>
#include <sys/time.h>

// 'global' variables to store screen info
char *fbp = 0;
struct fb_var_screeninfo vinfo;
struct fb_fix_screeninfo finfo;
size_t screensize = 0;

// helper function for drawing - no more need to go mess with
// the main function when just want to change what to draw...
void draw() {

  int x, y, line;
  char *buffer;
  struct timeval before, after, delta;

  buffer = (char *)malloc(screensize);

  for (y = 0; y < vinfo.yres; y++) {
    line = y * finfo.line_length;
    for (x = 0; x < vinfo.xres; x++) {

      // color based on the 16th of the screen width
      int c = 16 * x / vinfo.xres;
    
      // call the helper function
      buffer[x + line] = c;
    }
  }

  gettimeofday(&before, NULL);  
  memcpy(fbp, buffer, screensize);
  gettimeofday(&after, NULL);
  timersub(&after, &before, &delta);

  printf("Display duration: %lu s, %lu us\n", delta.tv_sec, delta.tv_usec);

  free(buffer);
}

void sighdl(int sig)
{
  printf("SIGINT\n");
}

// application entry point
int main(int ac, char* av[])
{
  int fbfd = 0;
  struct fb_var_screeninfo orig_vinfo;

  signal(SIGINT, sighdl);

  // Open the file for reading and writing
  fbfd = open("/dev/fb0", O_RDWR);
  if (!fbfd) {
    printf("Error: cannot open framebuffer device.\n");
    return(1);
  }
  printf("The framebuffer device was opened successfully.\n");

  // Get variable screen information
  if (ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo)) {
    printf("Error reading variable information.\n");
  }
  printf("Original %dx%d, %dbpp\n", vinfo.xres, vinfo.yres, 
         vinfo.bits_per_pixel );

  // Store for reset (copy vinfo to vinfo_orig)
  memcpy(&orig_vinfo, &vinfo, sizeof(struct fb_var_screeninfo));

  // Change variable info
  vinfo.bits_per_pixel = 8;
  if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &vinfo)) {
    printf("Error setting variable information.\n");
  }

  // Get fixed screen information
  if (ioctl(fbfd, FBIOGET_FSCREENINFO, &finfo)) {
    printf("Error reading fixed information.\n");
  }

  // map fb to user mem 
  screensize = vinfo.xres * vinfo.yres;
  fbp = (char*)mmap(0, 
                    screensize, 
                    PROT_READ | PROT_WRITE, 
                    MAP_SHARED, 
                    fbfd, 
                    0);

  if ((int)fbp == -1) {
    printf("Failed to mmap.\n");
  }
  else {
    // draw...
    draw();

    // If no parameter, pause until a CTRL-C...
    if (ac == 1)
      pause();
  }

  // cleanup
  munmap(fbp, screensize);
  if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &orig_vinfo)) {
    printf("Error re-setting variable information.\n");
  }
  close(fbfd);

  return 0;
  
}

Second program with sendfile():

#define _GNU_SOURCE  // for memfd_create()
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <signal.h>
#include <sys/sendfile.h>
#include <sys/time.h>

// 'global' variables to store screen info
struct fb_var_screeninfo vinfo;
struct fb_fix_screeninfo finfo;
int fbfd = 0;
size_t screensize = 0;

// helper function for drawing - no more need to go mess with
// the main function when just want to change what to draw...
void draw() {

  int x, y, line;
  int memfd;
  off_t offset;
  char *mem;
  struct timeval before, after, delta;

  memfd = memfd_create("framebuf", 0);
  if (memfd < 0) {
    fprintf(stderr, "memfd_create(): %m");
    return;
  }

  ftruncate(memfd, screensize);

  mem = (char*)mmap(0, 
                    screensize, 
                    PROT_READ | PROT_WRITE, 
                    MAP_SHARED, 
                    memfd,
                    0);
  if (mem == MAP_FAILED) {
    fprintf(stderr, "mmap(): %m");
    return;
  }

  // Fill the memory buffer
  for (y = 0; y < vinfo.yres; y++) {
    line = y * finfo.line_length;
    for (x = 0; x < vinfo.xres; x++) {

      // color based on the 16th of the screen width
      int c = 16 * x / vinfo.xres;

      mem[x + line] = c;
    }
  }

  // Copy the buffer into the framebuffer
  offset = 0;
  gettimeofday(&before, NULL);
  sendfile(fbfd, memfd, &offset, screensize);
  gettimeofday(&after, NULL);
  timersub(&after, &before, &delta);

  printf("Display duration: %lu s, %lu us\n", delta.tv_sec, delta.tv_usec);

  munmap(mem, screensize);
  close(memfd);

}


void sighdl(int sig)
{
  printf("SIGINT\n");
}


// application entry point
int main(int ac, char* av[])
{
  struct fb_var_screeninfo orig_vinfo;

  signal(SIGINT, sighdl);

  // Open the file for reading and writing
  fbfd = open("/dev/fb0", O_RDWR);
  if (!fbfd) {
    printf("Error: cannot open framebuffer device.\n");
    return(1);
  }
  printf("The framebuffer device was opened successfully.\n");

  // Get variable screen information
  if (ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo)) {
    printf("Error reading variable information.\n");
  }
  printf("Original %dx%d, %dbpp\n", vinfo.xres, vinfo.yres, 
         vinfo.bits_per_pixel );

  // Store for reset (copy vinfo to vinfo_orig)
  memcpy(&orig_vinfo, &vinfo, sizeof(struct fb_var_screeninfo));

  // Change variable info
  vinfo.bits_per_pixel = 8;
  if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &vinfo)) {
    printf("Error setting variable information.\n");
  }

  // Get fixed screen information
  if (ioctl(fbfd, FBIOGET_FSCREENINFO, &finfo)) {
    printf("Error reading fixed information.\n");
  }

  screensize = vinfo.xres * vinfo.yres;

  // draw...
  draw();

  // If no parameter, pause until a CTRL-C...
  if (ac == 1)
    pause();

  // cleanup
  if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &orig_vinfo)) {
    printf("Error re-setting variable information.\n");
  }
  close(fbfd);

  return 0;
  
}

If the program is launched without parameter, it pauses until the user type CTRL-C otherwise, it returns immediately after the display. On a Raspberry Pi 3 B+ running Linux 32-bits:

$ gcc fb1.c -o fb1
$ gcc fb2.c -o fb2
$ ./fb1 arg  # argument to make it return immediately
The framebuffer device was opened successfully.
Original 1920x1080, 32bpp
Display duration: 0 s, 2311 us
$ ./fb2 arg   # argument to make it return immediately
The framebuffer device was opened successfully.
Original 1920x1080, 32bpp
Display duration: 0 s, 2963 us

The original program in the page that you shared is slower in the same conditions (I modified the loop to make it write the whole screen as in the previous two examples, I added inline and static keywords and compiled it with -O3):

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <signal.h>
#include <sys/time.h>

// 'global' variables to store screen info
char *fbp = 0;
struct fb_var_screeninfo vinfo;
struct fb_fix_screeninfo finfo;

// helper function to 'plot' a pixel in given color
static inline void put_pixel(int x, int y, int c)
{
  // calculate the pixel's byte offset inside the buffer
  unsigned int pix_offset = x + y * finfo.line_length;

  // now this is about the same as 'fbp[pix_offset] = value'
  *((char*)(fbp + pix_offset)) = c;

}

// helper function for drawing - no more need to go mess with
// the main function when just want to change what to draw...
static void draw() {

  int x, y;
  struct timeval before, after, delta;

  gettimeofday(&before, NULL);
  for (y = 0; y < vinfo.yres; y++) {
    for (x = 0; x < vinfo.xres; x++) {

      // color based on the 16th of the screen width
      int c = 16 * x / vinfo.xres;
    
      // call the helper function
      put_pixel(x, y, c);

    }
  }
  gettimeofday(&after, NULL);
  timersub(&after, &before, &delta);

  printf("Display duration: %lu s, %lu us\n", delta.tv_sec, delta.tv_usec);
}

static void sighdl(int sig)
{
  printf("SIGINT\n");
}

// application entry point
int main(int ac, char* av[])
{

  int fbfd = 0;
  struct fb_var_screeninfo orig_vinfo;
  long int screensize = 0;

  signal(SIGINT, sighdl);

  // Open the file for reading and writing
  fbfd = open("/dev/fb0", O_RDWR);
  if (!fbfd) {
    printf("Error: cannot open framebuffer device.\n");
    return(1);
  }
  printf("The framebuffer device was opened successfully.\n");

  // Get variable screen information
  if (ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo)) {
    printf("Error reading variable information.\n");
  }
  printf("Original %dx%d, %dbpp\n", vinfo.xres, vinfo.yres, 
         vinfo.bits_per_pixel );

  // Store for reset (copy vinfo to vinfo_orig)
  memcpy(&orig_vinfo, &vinfo, sizeof(struct fb_var_screeninfo));

  // Change variable info
  vinfo.bits_per_pixel = 8;
  if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &vinfo)) {
    printf("Error setting variable information.\n");
  }

  // Get fixed screen information
  if (ioctl(fbfd, FBIOGET_FSCREENINFO, &finfo)) {
    printf("Error reading fixed information.\n");
  }

  // map fb to user mem 
  screensize = vinfo.xres * vinfo.yres;
  fbp = (char*)mmap(0, 
                    screensize, 
                    PROT_READ | PROT_WRITE, 
                    MAP_SHARED, 
                    fbfd, 
                    0);

  if ((int)fbp == -1) {
    printf("Failed to mmap.\n");
  }
  else {
    // draw...
    draw();

    // If no parameter, pause until a CTRL-C...
    if (ac == 1)
      pause();
  }

  // cleanup
  munmap(fbp, screensize);
  if (ioctl(fbfd, FBIOPUT_VSCREENINFO, &orig_vinfo)) {
    printf("Error re-setting variable information.\n");
  }
  close(fbfd);

  return 0;
  
}
$ gcc -O3 fb0.c -o fb0
$ ./fb0 arg      # argument to make it return immediately
The framebuffer device was opened successfully.
Original 1920x1080, 32bpp
Display duration: 0 s, 88081 us

Upvotes: 3

R.. GitHub STOP HELPING ICE
R.. GitHub STOP HELPING ICE

Reputation: 215221

That sample program is using a put_pixel function, which is a classic graphics anti-pattern that confuses newcomers into feeling like they can't write anything that's not slow. At sufficiently high optimization level (and more likely if the function were made static), the compiler might be able to inline it and factor out all the inefficiency of computing an offset in the framebuffer for every single pixel you write, but this is just the wrong abstraction layer to be working at. There should not be a put_pixel.

A framebuffer is just an array, and you want to write to it as an array directly rather than with gratuitous helper functions. In addition, you don't need to do things like y*stride+x over and over. If you have a position you're working at as an index in the array (or a pointer), you can address neighboring pixels just by adding or subtracting 1 (horizontal) or adding or subtracting stride (vertical). Here "stride" is a term commonly used for the offset between lines; it may be the line width, or some larger value due to alignment or other considerations.

Upvotes: 2

Related Questions