【问题标题】:Writing into framebuffer with a C program is very slow (Raspberry Pi)使用 C 程序写入帧缓冲区非常慢(Raspberry Pi)
【发布时间】:2021-03-03 23:25:51
【问题描述】:

我想做一个非常密集的模拟。我需要 Raspberry Pi 提供尽可能多的电力。为此,我将带有 OS Lite(不带桌面)的卡刷到 Micro SD 卡上,并使用 example 使用 C 程序写入帧缓冲区。

结果很慢。我可以看到图像正在更新并从上到下扫描。它很长:0.2s左右。这意味着我永远不会达到 30 或 60 fps。

有没有更好(更快)的方法来做到这一点? Raspberry Pi OS 的 X Window Manager 也必须以某种方式写入帧缓冲区才能工作,因此必须有更快的方法...

【问题讨论】:

  • 我不认为这是特定于 pi 的。这是一个经典的图形编程新手问题,仅在 rpi 和 rpi 的错误示例代码的上下文中。
  • 如果你确定你需要什么功能,你可能会得到更好的帮助。例如,您是否需要下拉菜单和复选框?您需要能够绘制多边形、实心圆吗?还是您只需要绘制位图图像?这里有一些想法...stackoverflow.com/a/58816979/2836621 和这里...stackoverflow.com/a/60787990/2836621
  • 我不需要任何 UI 元素,只需要在屏幕上绘制单个像素的能力。
  • 所以你可以mmap() C 中的帧缓冲区,就像我在 Python 中所做的那样...stackoverflow.com/a/58816979/2836621 并使用屏幕外的memcpy() 用纯色填充屏幕通过将像素地址计算为(row * screen_width) + column 来缓冲或绘制像素,并写入您的mmap() 指针偏移该地址。

标签: c linux raspberry-pi framebuffer


【解决方案1】:

图形服务使用内存缓冲区来制作屏幕图像并将整个缓冲区或仅将显示的修改部分写入屏幕设备(例如clipping)。

所以,这里有两个稍微增强的程序版本:

  1. 第一个先写入内存缓冲区,然后将整个缓冲区写入帧缓冲区
  2. 第二个先写入内存中映射的文件 (memfd_create()),然后使用 sendfile() 系统调用将文件复制到帧缓冲区中

在两者中,还添加了对 gettimeofday() 的调用以测量显示期间经过的时间。

#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;
  
}

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;
  
}

如果程序在没有参数的情况下启动,它会暂停直到用户键入 CTRL-C 否则,它会在显示后立即返回。在运行 Linux 32 位的 Raspberry Pi 3 B+ 上:

$ 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

您分享的page中的原始程序在相同条件下速度较慢(我修改了循环使其像前两个示例一样写入整个屏幕,我添加了内联和静态关键字并编译它 - 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

【讨论】:

  • 也许可以考虑对这种东西使用优化,即gcc -O3 ...
  • 感谢@Rachid K.,但达到 30 fps 仍然太慢,每帧花费的时间不得超过 0.0334 秒。
  • 小心,这是打开fb设备、执行ioctls、映射、写入、打印和退出所需的时间。如果你想确定帧率,你应该只测量填充屏幕的时间。
  • @MarkSetchell:我通过调用gettimeofday() 更新了帖子,以测量更新帧缓冲区期间经过的时间。原程序显示 0.2 秒,两个增强版显示 0.002 秒。
  • 您可以考虑将 putpixel() 函数内联以减少调用开销。
【解决方案2】:

该示例程序使用了put_pixel 函数,这是一个经典的图形反模式,让新手感到他们无法编写任何不慢的东西。在足够高的优化级别(如果函数被设为静态,则更有可能),编译器可能能够内联它并排除为您编写的每个像素在帧缓冲区中计算偏移量的所有低效率,但这只是要工作的抽象层错误。不应该有put_pixel

帧缓冲区只是一个数组,您想直接将其作为数组写入,而不是使用免费的辅助函数。此外,您不需要一遍又一遍地做y*stride+x 之类的事情。如果您有一个位置作为数组中的索引(或指针),您可以通过加或减 1(水平)或加或减步幅(垂直)来寻址相邻像素。这里的“stride”是一个常用的术语,表示行间的偏移;它可能是线宽,也可能是由于对齐或其他考虑而产生的较大值。

【讨论】:

  • 谢谢!你有任何关于我如何写入帧缓冲区数组的例子吗?我没有在网上找到任何东西。
猜你喜欢
  • 2013-01-30
  • 2015-10-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多