一、為何需要 double buffer?
single buffer 會導致:
屏幕撕裂(tearing),即在屏幕上同時看到多幀數據拼接在一起。
點擊查看大圖
single buffer 為何會造成撕裂:
refresh rate 和 frame rate 不一致。
refresh rate 表示的是 屏幕每秒能更新多少次顯示,例如 30hz / 60hz。
點擊查看大圖
frame rate 表示的是 lcd controller / gpu 每秒能繪制多少幀數據,例如 30fps / 60fps。
點擊查看大圖
LCD controller / gpu 和 屏幕協作完成一幀圖像的顯示:
點擊查看大圖
在 single buffer 的場景下,LCD user 和 LCD controller / gpu 總是在共用同一個 framebuffer,且沒有同步機制。
LCD user 是寫者,LCD controller / gpu 是讀者。
由于存在競爭關系且讀寫沒有同步機制,framebuffer 里必須會發生同時存在frame N 和 frame N-1 的數據,此時 LCD 將 framebuffer 的數據顯示出來時,就會看到撕裂的效果:
點擊查看大圖
可以通過 double buffer+vsync 解決撕裂的問題。
double buffer,顧名思義,就是有 2 個 framebuffer,其工作邏輯如下:
LCD controller : draw fb0 to screen
LCD user : write data to fb1
LCD controller : draw fb1 to screen
LCD user : write data to fb0
循環。..
vsync 機制則用于確保一幀圖像能不被打斷地顯示在屏幕。
如何支持 double buffer?
需要驅動和應用互相配合:
二、編寫支持 double buffer 的fbdev 驅動
fbdev 框圖:
先梳理一下思路:
讓驅動支持 double buffer 需要做 3 件事。
1. 申請2 x buffer:
size = (2 * width * height);
fbi-》screen_base = dma_alloc_wc(sfb-》dev, size, &map_dma, GFP_KERNEL);
2. 將 buffer 相關的信息保存 struct fb_info-》 struct fb_var_screeninfo。
struct fb_var_screeninfo {
__u32 xres; /* visible resolution */
__u32 yres;
__u32 xres_virtual; /* virtual resolution */
__u32 yres_virtual;
__u32 xoffset; /* offset from virtual to visible */
__u32 yoffset; /* resolution */
。..
}
點擊查看大圖
xres 和 yres 是真實的 LCD 分辨率的寬和長;
xres_virtual 和 yres_virtual 是顯存區域的寬和長;
xoffset 和 yoffset 用于指定當前使用哪一個 Buffer 進行繪制。使用 Buffer0 時 ,xoffset = 0,yoffset=0; 使用 Buffer1 時,xoffset = 0, yoffset = yres * 1;
3. 支持切換 buffer,具體的就是實現 ioctl:FBIOPAN_DISPLAY。
pan 的本意是平移,可以想象成顯存上方有一個取景框,平移取景框可以看到不同的顯示內容。
實例分析:goldfishfb.c
goldfishfb.c 是虛擬硬件 goldfish 的 fbdev 驅動,我們可以參考這個文件,學習如何實現 double buffer。
1. 分配 2 x buffer:
int goldfish_fb_probe()
{
。..
framesize = width * height * 2 * 2;
fb-》fb.screen_base = (char __force __iomem *)dma_alloc_coherent(&pdev-》dev, framesize, &fbpaddr, GFP_KERNEL);
}
2. 設置 fb_var_screeninfo:
int goldfish_fb_probe()
{
。..
fb-》fb.var.xres = width;
fb-》fb.var.yres = height;
fb-》fb.var.xres_virtual = width;
fb-》fb.var.yres_virtual = height * 2;
}
3. 實現 ioctl / FBIOPAN_DISPLAY:
static struct fb_ops goldfish_fb_ops = {
。..
.fb_pan_display = goldfish_fb_pan_display,
};
int goldfish_fb_pan_display()
{
。..
// 將新的顯存地址告知 lcd controller
writel(fb-》fb.fix.smem_start + fb-》fb.var.xres * 2 * var-》yoffset,
fb-》reg_base + FB_SET_BASE);
// 等待 LCD controller 的 vsync 信號
wait_event_timeout(fb-》wait,fb-》base_update_count != base_update_count, HZ / 15);
}
當LCD controller 將一幀圖像完整地顯示在 LCD 上后,就會產生一個中斷,在中斷里就會執行喚醒睡眠在 fb_pan_display 里的進程。
如果你想多了解一些,可以閱讀 DRM 框架里的 fbdev 兼容代碼,此代碼也是支持 double buffer的:
linux/drivers/gpu/drm/*/*_drm_fbdev.c
linux/drivers/gpu/drm/drm_fb_helper.c
三、編寫支持 double buffer 的 fbdev 應用
驅動支持 double buffer 后,還得在應用程序里將其使用起來。
先梳理一下思路:
檢查是否支持 double buffer;
使能 double buffer:FBIOPUT_VSCREENINFO;
更新 buffer 里數據;
通知驅動切換 buffer:FBIOPAN_DISPLAY;
等待切換完成:FBIO_WAITFORVSYNC;
實例分析:show_color.c
static int fd_fb;
static struct fb_fix_screeninfo fix; /* Current fix */
static struct fb_var_screeninfo var; /* Current var */
static int screen_size;
static unsigned char *fb_base;
static unsigned int line_width;
static unsigned int pixel_width;
int main(int argc, char **argv)
{
int i;
int ret;
int buffer_num;
int buf_idx = 1;
char *buf_next;
unsigned int colors[] = {0x00FF0000, 0x0000FF00, 0x000000FF, 0, 0x00FFFFFF}; /* 0x00RRGGBB */
struct timespec time;
。..
fd_fb = open(“/dev/fb0”, O_RDWR);
ioctl(fd_fb, FBIOGET_FSCREENINFO, &fix);
ioctl(fd_fb, FBIOGET_VSCREENINFO, &var);
line_width = var.xres * var.bits_per_pixel / 8;
pixel_width = var.bits_per_pixel / 8;
screen_size = var.xres * var.yres * var.bits_per_pixel / 8;
// 1. 獲得 buffer 個數
buffer_num = fix.smem_len / screen_size;
printf(“buffer_num = %d
”, buffer_num);
fb_base = (unsigned char *)mmap(NULL , fix.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0);
if (fb_base == (unsigned char *)-1) {
printf(“can‘t mmap
”);
return -1;
}
if ((argv[1][0] == ’s‘) || (buffer_num == 1)) {
printf(“single buffer:
”);
while (1) {
for (i = 0; i 《 sizeof(colors)/sizeof(colors[0]); i++) {
lcd_draw_screen(fb_base, colors[i]);
nanosleep(&time, NULL);
}
}
} else {
printf(“double buffer:
”);
// 2. 使能多 buffer
var.yres_virtual = buffer_num * var.yres;
ioctl(fd_fb, FBIOPUT_VSCREENINFO, &var);
while (1) {
for (i = 0; i 《 sizeof(colors)/sizeof(colors[0]); i++) {
// 3. 更新 buffer 里的數據
buf_next = fb_base + buf_idx * screen_size;
lcd_draw_screen(buf_next, colors[i]);
// 4. 通知驅動切換 buffer
var.yoffset = buf_idx * var.yres;
ret = ioctl(fd_fb, FBIOPAN_DISPLAY, &var);
if (ret 《 0) {
perror(“ioctl() / FBIOPAN_DISPLAY”);
}
// 5. 等待幀同步完成
ret = 0;
ioctl(fd_fb, FBIO_WAITFORVSYNC, &ret);
if (ret 《 0) {
perror(“ioctl() / FBIO_WAITFORVSYNC”);
}
buf_idx = !buf_idx;
nanosleep(&time, NULL);
}
}
}
munmap(fb_base , screen_size);
close(fd_fb);
return 0;
}
運行:
$ 。/show_color single
buffer_num = 1
single buffer:
$ 。/show_color double
buffer_num = 2
double buffer:
該程序會在屏幕上循環的顯示不同的顏色。
當傳入 “single” 參數時,使用單 buffer,可見撕裂。
當傳入 “double” 參數時,使用雙 buffer,不再撕裂。
代碼不是很復雜,我就不再詳細分析了。
如果你想多了解一些,可以閱讀開源軟件 SDL-1.2 里的 sdl_fbvideo.c,此代碼也支持了 double buffer。
編輯:lyn
-
驅動
+關注
關注
12文章
1841瀏覽量
85326 -
編輯
+關注
關注
0文章
28瀏覽量
11558
原文標題:Linux 驅動開發 / fbdev 雙緩存 / 快速入門
文章出處:【微信號:zhuyandz,微信公眾號:FPGA之家】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論