Search code examples
clinuxraspberry-piframebuffer

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...


Solution

  • 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