Skip to content

zuoyi001/zuoyi001.github.io

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 

Repository files navigation

DRM(Direct Rendering Manager)学习简介

分类专栏: [DRM (Direct Rendering Manager)] 文章标签: [DRM][Direct Rendering Manager] [KMS]

版权

学习DRM一年多了,由于该架构较为复杂,代码量较多,且国内参考文献较少,初学者学习起来较为困难。因此决定将自己学习的经验总结分享给大家,希望对正在学习DRM的同学有所帮助,同时交流经验。

由于本人工作中只负责Display驱动,因此分享的DRM学习经验都只局限于Display这一块,对于GPU这一块本人无能为力,如果大家有相关经验分享,还请在留言中通知一声,我会常去浏览你的博客,大家相互学习。

DRM

DRM是Linux目前主流的图形显示框架,相比FB架构,DRM更能适应当前日益更新的显示硬件。比如FB原生不支持多层合成,不支持VSYNC,不支持DMA-BUF,不支持异步更新,不支持fence机制等等,而这些功能DRM原生都支持。同时DRM可以统一管理GPU和Display驱动,使得软件架构更为统一,方便管理和维护。

DRM从模块上划分,可以简单分为3部分:libdrmKMSGEM

在这里插入图片描述 (图片来自Wiki)

libdrm

对底层接口进行封装,向上层提供通用的API接口,主要是对各种IOCTL接口进行封装。

KMS

Kernel Mode Setting,所谓Mode setting,其实说白了就两件事:更新画面设置显示参数更新画面:显示buffer的切换,多图层的合成方式,以及每个图层的显示位置。 设置显示参数:包括分辨率、刷新率、电源状态(休眠唤醒)等。

GEM

Graphic Execution Manager,主要负责显示buffer的分配和释放,也是GPU唯一用到DRM的地方。

基本元素

DRM框架涉及到的元素很多,大致如下: KMS:CRTCENCODERCONNECTORPLANEFBVBLANKproperty GEM:DUMBPRIMEfence

在这里插入图片描述 (图片来源:The DRM/KMS subsystem from a newbie’s point of view

元素 说明
CRTC 对显示buffer进行扫描,并产生时序信号的硬件模块,通常指Display Controller
ENCODER 负责将CRTC输出的timing时序转换成外部设备所需要的信号的模块,如HDMI转换器或DSI Controller
CONNECTOR 连接物理显示设备的连接器,如HDMI、DisplayPort、DSI总线,通常和Encoder驱动绑定在一起
PLANE 硬件图层,有的Display硬件支持多层合成显示,但所有的Display Controller至少要有1个plane
FB Framebuffer,单个图层的显示内容,唯一一个和硬件无关的基本元素
VBLANK 软件和硬件的同步机制,RGB时序中的垂直消影区,软件通常使用硬件VSYNC来实现
property 任何你想设置的参数,都可以做成property,是DRM驱动中最灵活、最方便的Mode setting机制
DUMB 只支持连续物理内存,基于kernel中通用CMA API实现,多用于小分辨率简单场景
PRIME 连续、非连续物理内存都支持,基于DMA-BUF机制,可以实现buffer共享,多用于大内存复杂场景
fence buffer同步机制,基于内核dma_fence机制实现,用于防止显示内容出现异步问题

学习DRM驱动其实就是学习上面各个元素的实现及用法,如果你能掌握这些知识点,那么在编写DRM驱动的时候就能游刃有余。

目录(持续更新中)

本篇博客将作为本人DRM学习教程的目录汇总,后续我会以示例代码的形式和大家分享上述知识点的学习过程,并不断更新目录链接,敬请期待!

  1. [最简单的DRM应用程序 (single-buffer)]
  2. [最简单的DRM应用程序 (double-buffer)]
  3. [最简单的DRM应用程序 (page-flip)]
  4. [最简单的DRM应用程序 (plane-test)]
  5. [DRM应用程序进阶 (Property)]
  6. [DRM应用程序进阶 (atomic-crtc)]
  7. [DRM应用程序进阶 (atomic-plane)]
  8. [DRM (Direct Rendering Manager) 的发展历史]
  9. [DRM 驱动程序开发(开篇)]
  10. [DRM 驱动程序开发(VKMS)]
  11. [关于 DRM 中 DUMB 和 PRIME 名字的由来]
  12. [DRM GEM 驱动程序开发(dumb)]
  13. [DRM 驱动 mmap 详解:(一)预备知识]
  14. [DRM 驱动 mmap 详解:(二)CMA Helper]
  15. [LWN 翻译:Atomic Mode Setting 设计简介(上)]
  16. [LWN 翻译:Atomic Mode Setting 设计简介(下)]
  17. 翻译:Mainline Explicit Fencing

dma-buf 系列: [dma-buf 由浅入深(一) —— 最简单的 dma-buf 驱动程序] [dma-buf 由浅入深(二) —— kmap / vmap] [dma-buf 由浅入深(三) —— map attachment] [dma-buf 由浅入深(四) —— mmap] [dma-buf 由浅入深(五) —— File] [dma-buf 由浅入深(六) —— begin / end cpu_access] [dma-buf 由浅入深(七) —— alloc page 版本] [dma-buf 由浅入深(八) —— ION 简化版] [LWN 翻译:DMA-BUF cache handling: Off the DMA API map (part 1)] [LWN 翻译:DMA-BUF cache handling: Off the DMA API map (part 2)]


参考资料

  1. Wiki: Direct Rendering Manager
  2. wowotech: Linux graphic subsystem(2)_DRI介绍
  3. Boris Brezillon: The DRM/KMS subsystem from a newbie’s point of view
  4. 线·飘零 博客园:Linux环境下的图形系统和AMD R600显卡编程(1)
  5. Younix脏羊 CSDN博客:[Linux DRM(二)基本概念和特性]

最简单的DRM应用程序 (single-buffer)

分类专栏: [DRM (Direct Rendering Manager)]

版权

在上一篇[DRM (Direct Rendering Manager) 学习简介] 中,我们学习了DRM的基本概念以及基本组成元素。从本篇开始,我将以示例代码的形式,给大家分享学习DRM驱动开发的整个学习过程。

在学习DRM驱动之前,应该首先了解如何使用DRM驱动。以下使用伪代码的方式,简单介绍如何编写一个最简单的DRM应用程序。

伪代码:

int main(int argc, char **argv)
{
	/* open the drm device */
	open("/dev/dri/card0");

	/* get crtc/encoder/connector id */
	drmModeGetResources(...);

	/* get connector for display mode */
	drmModeGetConnector(...);

	/* create a dumb-buffer */
	drmIoctl(DRM_IOCTL_MODE_CREATE_DUMB);

	/* bind the dumb-buffer to an FB object */
	drmModeAddFB(...);

	/* map the dumb buffer for userspace drawing */
	drmIoctl(DRM_IOCTL_MODE_MAP_DUMB);
	mmap(...);

	/* start display */
	drmModeSetCrtc(crtc_id, fb_id, connector_id, mode);
}

当执行完mmap之后,我们就可以直接在应用层对framebuffer进行绘图操作了。


详细参考代码如下:

modeset-single-buffer.c

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

struct buffer_object {
	uint32_t width;
	uint32_t height;
	uint32_t pitch;
	uint32_t handle;
	uint32_t size;
	uint8_t *vaddr;
	uint32_t fb_id;
};

struct buffer_object buf;

static int modeset_create_fb(int fd, struct buffer_object *bo)
{
	struct drm_mode_create_dumb create = {};
 	struct drm_mode_map_dumb map = {};

	/* create a dumb-buffer, the pixel format is XRGB888 */
	create.width = bo->width;
	create.height = bo->height;
	create.bpp = 32;
	drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);

	/* bind the dumb-buffer to an FB object */
	bo->pitch = create.pitch;
	bo->size = create.size;
	bo->handle = create.handle;
	drmModeAddFB(fd, bo->width, bo->height, 24, 32, bo->pitch,
			   bo->handle, &bo->fb_id);

	/* map the dumb-buffer to userspace */
	map.handle = create.handle;
	drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map);

	bo->vaddr = mmap(0, create.size, PROT_READ | PROT_WRITE,
			MAP_SHARED, fd, map.offset);

	/* initialize the dumb-buffer with white-color */
	memset(bo->vaddr, 0xff, bo->size);

	return 0;
}

static void modeset_destroy_fb(int fd, struct buffer_object *bo)
{
	struct drm_mode_destroy_dumb destroy = {};

	drmModeRmFB(fd, bo->fb_id);

	munmap(bo->vaddr, bo->size);

	destroy.handle = bo->handle;
	drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy);
}

int main(int argc, char **argv)
{
	int fd;
	drmModeConnector *conn;
	drmModeRes *res;
	uint32_t conn_id;
	uint32_t crtc_id;

	fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);

	res = drmModeGetResources(fd);
	crtc_id = res->crtcs[0];
	conn_id = res->connectors[0];

	conn = drmModeGetConnector(fd, conn_id);
	buf.width = conn->modes[0].hdisplay;
	buf.height = conn->modes[0].vdisplay;

	modeset_create_fb(fd, &buf);

	drmModeSetCrtc(fd, crtc_id, buf.fb_id,
			0, 0, &conn_id, 1, &conn->modes[0]);

	getchar();

	modeset_destroy_fb(fd, &buf);

	drmModeFreeConnector(conn);
	drmModeFreeResources(res);

	close(fd);

	return 0;
}

上面代码有一个关键的函数,它就是drmModeSetCrtc(),该函数需要crtc_idconnector_idfb_iddrm_mode 这几个参数。可以看到,几乎所有的代码都是为了该函数能够顺利传参而编写的:

为了获取 crtc_idconnector_id,需要调用 drmModeGetResources() 为了获取 fb_id,需要调用 drmModeAddFB() 为了获取 drm_mode,需要调用 drmModeGetConnector

通过调用drmModeSetCrtc(),整个底层显示pipeline硬件就都初始化好了,并且还在屏幕上显示出了FB的内容,非常简单。

以上代码其实是基于kernel DRM maintainer David Herrmann 所写的drm-howto/modeset.c 文件修改的,需要注意的是,以上参考代码删除了许多异常错误处理,且只有在以下条件都满足时,才能正常运行:

  1. DRM驱动支持MODESET;
  2. DRM驱动支持dumb-buffer(即连续物理内存);
  3. DRM驱动至少支持1个CRTC,1个Encoder,1个Connector;
  4. DRM驱动的Connector至少包含1个有效的drm_display_mode。

运行结果:(模拟效果)

在这里插入图片描述

描述:程序运行后,显示全屏白色,等待用户输入按键;当用户按下任意按键后,程序退出,显示黑屏。

注意:程序运行之前,请确保没有其它应用或服务占用/dev/dri/card0节点,否则将出现 Permission Denied 错误。

最简单的DRM应用程序 (double-buffer)

分类专栏: DRM (Direct Rendering Manager) 文章标签: DRM [Direct Rendering Manager](https://so.csdn.net/so/search/s.do?q=Direct Rendering Manager&t=blog&o=vip&s=&l=&f=&viparticle=) libdrm drmModeSetCrtc

版权

在上一篇 最简单的DRM应用程序 (single-buffer)中,我们学习了如何去编写一个最基本的DRM应用程序。而本篇文章,将在 modeset-single-buffer 的基础上,对其进行扩展,使用双buffer机制的案例,来加深大家对drmModeSetCrtc()函数的印象。

使用上一节中的modeset-single-buffer程序,如果用户想要修改画面内容,就只能对mmap()后的buffer进行修改,这就会导致用户能很明显的在屏幕上看到软件修改buffer的过程,用户体验大大降低。而双buffer机制则能很好的避免这种问题,双buffer的概念无需过多赘述,大家听名字就知道什么意思了,即前后台buffer切换机制。

伪代码:

int main(void)
{
    ...
    while(1) {
        drmModeSetCrtc(fb0);
        ...
        drmModeSetCrtc(fb1);
        ...
    }
    ...
}
1234567891011

详细参考代码如下:

modeset-double-buffer.c

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

struct buffer_object {
	uint32_t width;
	uint32_t height;
	uint32_t pitch;
	uint32_t handle;
	uint32_t size;
	uint32_t *vaddr;
	uint32_t fb_id;
};

struct buffer_object buf[2];

static int modeset_create_fb(int fd, struct buffer_object *bo, uint32_t color)
{
	struct drm_mode_create_dumb create = {};
 	struct drm_mode_map_dumb map = {};
	uint32_t i;

	create.width = bo->width;
	create.height = bo->height;
	create.bpp = 32;
	drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);

	bo->pitch = create.pitch;
	bo->size = create.size;
	bo->handle = create.handle;
	drmModeAddFB(fd, bo->width, bo->height, 24, 32, bo->pitch,
			   bo->handle, &bo->fb_id);

	map.handle = create.handle;
	drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map);

	bo->vaddr = mmap(0, create.size, PROT_READ | PROT_WRITE,
			MAP_SHARED, fd, map.offset);

	for (i = 0; i < (bo->size / 4); i++)
		bo->vaddr[i] = color;

	return 0;
}

static void modeset_destroy_fb(int fd, struct buffer_object *bo)
{
	struct drm_mode_destroy_dumb destroy = {};

	drmModeRmFB(fd, bo->fb_id);

	munmap(bo->vaddr, bo->size);

	destroy.handle = bo->handle;
	drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy);
}

int main(int argc, char **argv)
{
	int fd;
	drmModeConnector *conn;
	drmModeRes *res;
	uint32_t conn_id;
	uint32_t crtc_id;

	fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);

	res = drmModeGetResources(fd);
	crtc_id = res->crtcs[0];
	conn_id = res->connectors[0];

	conn = drmModeGetConnector(fd, conn_id);
	buf[0].width = conn->modes[0].hdisplay;
	buf[0].height = conn->modes[0].vdisplay;
	buf[1].width = conn->modes[0].hdisplay;
	buf[1].height = conn->modes[0].vdisplay;

	modeset_create_fb(fd, &buf[0], 0xff0000);
	modeset_create_fb(fd, &buf[1], 0x0000ff);

	drmModeSetCrtc(fd, crtc_id, buf[0].fb_id,
			0, 0, &conn_id, 1, &conn->modes[0]);

	getchar();

	drmModeSetCrtc(fd, crtc_id, buf[1].fb_id,
			0, 0, &conn_id, 1, &conn->modes[0]);

	getchar();

	modeset_destroy_fb(fd, &buf[1]);
	modeset_destroy_fb(fd, &buf[0]);

	drmModeFreeConnector(conn);
	drmModeFreeResources(res);

	close(fd);

	return 0;
}

以上代码是基于David Herrmann 所写的 drm-howto/modeset-double-buffered.c 文件修改的。和modeset-single-buffer案例一样,为了方便大家关注重点,以上代码删除了许多异常错误处理,并对程序功能做了简化。

从上面的代码我们可以看出,drmModeSetCrtc() 的功能除了可以初始化整条显示pipeline,建立crtc到connector之间的连接关系外,它还可以更新屏幕显示内容,即通过修改fb_id,来完成显示buffer的切换。

有的同学可能会担心,重复调用drmModeSetCrtc()会导致硬件链路被重复初始化。其实不必担心,因为DRM驱动框架会对传入的参数进行检查,只要display mode 和 pipeline 链路连接关系没有发生变化,就不会重新初始化硬件。

运行结果:(模拟效果)

在这里插入图片描述

描述:程序运行后,屏幕显示红色;输入回车后,屏幕显示蓝色;再次输入回车后,程序退出。

注意:程序运行之前,请确保没有其它应用或服务占用/dev/dri/card0节点,否则将出现 Permission Denied 错误。

源码下载:modeset-double-buffer

参考资料: David Herrmann’s Blog: Advanced DRM Mode-Setting API David Herrmann’s Github: drm-howto/modeset-double-buffered.c

文章汇总:DRM (Direct Rendering Manager) 学习简介

最简单的DRM应用程序 (page-flip)

分类专栏: DRM (Direct Rendering Manager) 文章标签: DRM [Direct Rendering Manager](https://so.csdn.net/so/search/s.do?q=Direct Rendering Manager&t=blog&o=vip&s=&l=&f=&viparticle=) libdrm drmModePageFlip

版权

在上一篇 最简单的DRM应用程序 (double-buffer)中,我们了解了DRM更新图像的一个重要接口drmModeSetCrtc()。在本篇文章中,我们将一起来学习DRM另一个重要的刷图接口:drmModePageFlip()

drmModePageFlip()的功能也是用于更新显示内容的,但是它和drmModeSetCrtc()最大的区别在于,drmModePageFlip()只会等到VSYNC到来后才会真正执行framebuffer切换动作,而drmModeSetCrtc()则会立即执行framebuffer切换动作。很明显,drmModeSetCrtc()对于某些硬件来说,很容易造成 撕裂(tear effect)问题,而drmModePageFlip()则不会造成这种问题。

由于drmModePageFlip()本身是基于VSYNC事件机制的,因此底层DRM驱动必须支持VBLANK事件

伪代码:

void my_page_flip_handler(...)
{
	drmModePageFlip(DRM_MODE_PAGE_FLIP_EVENT);
	...
}

int main(void)
{
	drmEventContext ev = {};

	ev.version = DRM_EVENT_CONTEXT_VERSION;
	ev.page_flip_handler = my_page_flip_handler;
	...

	drmModePageFlip(DRM_MODE_PAGE_FLIP_EVENT);
	
	while (1) {
		drmHandleEvent(&ev);
	}
}
1234567891011121314151617181920

详细参考代码如下:

modeset-page-flip.c

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <signal.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

struct buffer_object {
	uint32_t width;
	uint32_t height;
	uint32_t pitch;
	uint32_t handle;
	uint32_t size;
	uint32_t *vaddr;
	uint32_t fb_id;
};

struct buffer_object buf[2];
static int terminate;

static int modeset_create_fb(int fd, struct buffer_object *bo, uint32_t color)
{
	struct drm_mode_create_dumb create = {};
 	struct drm_mode_map_dumb map = {};
	uint32_t i;

	create.width = bo->width;
	create.height = bo->height;
	create.bpp = 32;
	drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);

	bo->pitch = create.pitch;
	bo->size = create.size;
	bo->handle = create.handle;
	drmModeAddFB(fd, bo->width, bo->height, 24, 32, bo->pitch,
			   bo->handle, &bo->fb_id);

	map.handle = create.handle;
	drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map);

	bo->vaddr = mmap(0, create.size, PROT_READ | PROT_WRITE,
			MAP_SHARED, fd, map.offset);

	for (i = 0; i < (bo->size / 4); i++)
		bo->vaddr[i] = color;

	return 0;
}

static void modeset_destroy_fb(int fd, struct buffer_object *bo)
{
	struct drm_mode_destroy_dumb destroy = {};

	drmModeRmFB(fd, bo->fb_id);

	munmap(bo->vaddr, bo->size);

	destroy.handle = bo->handle;
	drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy);
}

static void modeset_page_flip_handler(int fd, uint32_t frame,
				    uint32_t sec, uint32_t usec,
				    void *data)
{
	static int i = 0;
	uint32_t crtc_id = *(uint32_t *)data;

	i ^= 1;

	drmModePageFlip(fd, crtc_id, buf[i].fb_id,
			DRM_MODE_PAGE_FLIP_EVENT, data);

	usleep(500000);
}

static void sigint_handler(int arg)
{
	terminate = 1;
}

int main(int argc, char **argv)
{
	int fd;
	drmEventContext ev = {};
	drmModeConnector *conn;
	drmModeRes *res;
	uint32_t conn_id;
	uint32_t crtc_id;

	/* register CTRL+C terminate interrupt */
	signal(SIGINT, sigint_handler);

	ev.version = DRM_EVENT_CONTEXT_VERSION;
	ev.page_flip_handler = modeset_page_flip_handler;

	fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);

	res = drmModeGetResources(fd);
	crtc_id = res->crtcs[0];
	conn_id = res->connectors[0];

	conn = drmModeGetConnector(fd, conn_id);
	buf[0].width = conn->modes[0].hdisplay;
	buf[0].height = conn->modes[0].vdisplay;
	buf[1].width = conn->modes[0].hdisplay;
	buf[1].height = conn->modes[0].vdisplay;

	modeset_create_fb(fd, &buf[0], 0xff0000);
	modeset_create_fb(fd, &buf[1], 0x0000ff);

	drmModeSetCrtc(fd, crtc_id, buf[0].fb_id,
			0, 0, &conn_id, 1, &conn->modes[0]);

	drmModePageFlip(fd, crtc_id, buf[0].fb_id,
			DRM_MODE_PAGE_FLIP_EVENT, &crtc_id);

	while (!terminate) {
		drmHandleEvent(fd, &ev);
	}

	modeset_destroy_fb(fd, &buf[1]);
	modeset_destroy_fb(fd, &buf[0]);

	drmModeFreeConnector(conn);
	drmModeFreeResources(res);

	close(fd);

	return 0;
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139

以上代码是基于David Herrmann 所写的 drm-howto/modeset-vsync.c 文件修改的。和前两篇的案例一样,为了方便大家关注重点,以上代码删除了许多异常错误处理,并对程序功能做了简化。

从上面的代码可以看出,要使用drmModePageFlip(),就必须依赖drmHandleEvent()函数,该函数内部以阻塞的形式等待底层驱动返回相应的vblank事件,以确保和VSYNC同步。需要注意的是,drmModePageFlip()不允许在1个VSYNC周期内被调用多次,否则只有第一次调用有效,后面几次调用都会返回-EBUSY错误(-16)

运行结果:(模拟效果)

在这里插入图片描述

描述:程序运行后,屏幕在红色和蓝色之间来回切换;当输入CTRL+C后,程序退出。

注意:程序运行之前,请确保没有其它应用或服务占用/dev/dri/card0节点,否则将出现 Permission Denied 错误。

源码下载:modeset-page-flip

参考资料: David Herrmann’s Blog: Advanced DRM Mode-Setting API David Herrmann’s Github: drm-howto/modeset-vsync.c

文章汇总: DRM (Direct Rendering Manager) 学习简介

最简单的DRM应用程序 (plane-test)

分类专栏: DRM (Direct Rendering Manager) 文章标签: DRM [Direct Rendering Manager](https://so.csdn.net/so/search/s.do?q=Direct Rendering Manager&t=blog&o=vip&s=&l=&f=&viparticle=) libdrm drmModeSetPlane

版权

在上一篇 最简单的DRM应用程序 (page-flip)中,我们学习了drmModePageFlip()的用法。而在更早的两篇文章中,我们还学习了drmModeSetCrtc()的使用方法。但是这两个接口都只能全屏显示framebuffer的内容,如何才能在屏幕上只显示framebuffer的一部分内容呢?本篇我们将一起来学习DRM另一个重要的刷图接口:drmModeSetPlane()

在学习该函数之前,我们首先来了解一下,什么是Plane?在开篇 DRM (Direct Rendering Manager) 学习简介 文章中,曾简单描述过Plane的概念,即硬件图层。今天,我们将详细了解下Plane的概念。

DRM中的Plane和我们常说的YUV/YCbCr图形格式中的plane完全是两个不同的概念。YUV图形格式中的plane指的是图像数据在内存中的排列形式,一般Y通道占一段连续的内存块,UV通道占另一段连续的内存块,我们称之为YUV-2plane (也叫YUV 2平面),属于软件层面。而DRM中的Plane指的是Display Controller中用于多层合成的单个硬件图层模块,属于硬件层面。二者概念上不要混淆。

Plane的历史

随着软件技术的不断更新,对硬件的性能要求越来越高,在满足功能正常使用的前提下,对功耗的要求也越来越苛刻。本来GPU可以处理所有图形任务,但是由于它运行时的功耗实在太高,设计者们决定将一部分简单的任务交给Display Controller去处理(比如合成),而让GPU专注于绘图(即渲染)这一主要任务,减轻GPU的负担,从而达到降低功耗提升性能的目的。于是,Plane(硬件图层单元)就诞生了。

Plane是连接FB与CRTC的纽带,是内存的搬运工。

伪代码:

int main(void)
{
	...
	drmSetClientCap(DRM_CLIENT_CAP_UNIVERSAL_PLANES);
	drmModeGetPlaneResources();

	drmModeSetPlane(plane_id, crtc_id, fb_id, 0,
			crtc_x, crtc_y, crtc_w, crtc_h,
			src_x << 16, src_y << 16, src_w << 16, src_h << 16);
	...
}
1234567891011

先来了解一下drmModeSetPlane()参数含义:

在这里插入图片描述

(上图实现了裁剪、平移和放大的效果)

当 SRC 与 CRTC 的 X/Y 不相等时,则实现了平移的效果; 当 SRC 与 CRTC 的 W/H 不相等时,则实现了缩放的效果; 当 SRC 与 FrameBuffer 的 W/H 不相等时,则实现了裁剪的效果;

一个高级的Plane,通常具有如下功能:

功能 说明
Crop 裁剪,如上图
Scaling 缩放,放大或缩小
Rotation 旋转,90° 180° 270° X/Y镜像
Z-Order Z-顺序,调整当前层在总图层中的Z轴顺序
Blending 合成,pixel alpha / global alpha
Format 颜色格式,ARGB888 XRGB888 YUV420 等

再次强调,以上这些功能都是由硬件直接完成的,而非软件实现。

在DRM框架中,Plane又分为如下3种类型:

类型 说明
Cursor 光标图层,一般用于PC系统,用于显示鼠标
Overlay 叠加图层,通常用于YUV格式的视频图层
Primary 主要图层,通常用于仅支持RGB格式的简单图层

其实随着现代半导体技术的飞速发展,Overlay PlanePrimary Plane之间已经没有明显的界限了,许多芯片的图层处理能力已经非常强大,不仅仅可以处理简单的RGB格式,也可以处理YUV视频格式,甚至FBC压缩格式。针对这类硬件图层,它既可以是Overlay Plane,也可以是Primary Plane,至于驱动如何定义,就要看工程师的喜好了。 而对于一些早期处理能力比较弱的硬件,为了节约成本,每个图层支持的格式并不一样,比如将平常使用格式最多的RGB图层作为Primary Plane,而将平时用不多的YUV视频图层作为Overlay Plane,那么这个时候上层应用程序在使用这两种plane的时候就需要区别对待了。

需要注意的是,并不是所有的Display Controller都支持Plane,从前面single-buffer 案例中的drmModeSetCrtc()函数也能看出,即使没有plane_id,屏幕也能正常显示。比如s3c2440这种骨灰级ARM9 SoC,它的LCDC就没有Plane的概念。但是DRM框架规定,任何一个CRTC,必须要有1个Primary Plane。 即使像S3C2440这种不带真实Plane硬件的Display Controller,我们也认为它的Primary Plane就是LCDC本身,因为它实现了从Framebuffer到CRTC的数据搬运工作,而这正是一个Plane最基本的功能。

为什么要设置DRM_CLIENT_CAP_UNIVERSAL_PLANES

因为如果不设置,drmModeGetPlaneResources()就只会返回Overlay Plane,其他Plane都不会返回。而如果设置了,DRM驱动则会返回所有支持的Plane资源,包括cursor、overlay和primary。


详细参考代码如下:

modeset-plane-test.c

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>

struct buffer_object {
	uint32_t width;
	uint32_t height;
	uint32_t pitch;
	uint32_t handle;
	uint32_t size;
	uint8_t *vaddr;
	uint32_t fb_id;
};

struct buffer_object buf;

static int modeset_create_fb(int fd, struct buffer_object *bo)
{
	struct drm_mode_create_dumb create = {};
 	struct drm_mode_map_dumb map = {};

	create.width = bo->width;
	create.height = bo->height;
	create.bpp = 32;
	drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create);

	bo->pitch = create.pitch;
	bo->size = create.size;
	bo->handle = create.handle;
	drmModeAddFB(fd, bo->width, bo->height, 24, 32, bo->pitch,
			   bo->handle, &bo->fb_id);

	map.handle = create.handle;
	drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map);

	bo->vaddr = mmap(0, create.size, PROT_READ | PROT_WRITE,
			MAP_SHARED, fd, map.offset);

	memset(bo->vaddr, 0xff, bo->size);

	return 0;
}

static void modeset_destroy_fb(int fd, struct buffer_object *bo)
{
	struct drm_mode_destroy_dumb destroy = {};

	drmModeRmFB(fd, bo->fb_id);

	munmap(bo->vaddr, bo->size);

	destroy.handle = bo->handle;
	drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy);
}

int main(int argc, char **argv)
{
	int fd;
	drmModeConnector *conn;
	drmModeRes *res;
	drmModePlaneRes *plane_res;
	uint32_t conn_id;
	uint32_t crtc_id;
	uint32_t plane_id;

	fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);

	res = drmModeGetResources(fd);
	crtc_id = res->crtcs[0];
	conn_id = res->connectors[0];

	drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
	plane_res = drmModeGetPlaneResources(fd);
	plane_id = plane_res->planes[0];

	conn = drmModeGetConnector(fd, conn_id);
	buf.width = conn->modes[0].hdisplay;
	buf.height = conn->modes[0].vdisplay;

	modeset_create_fb(fd, &buf);

	drmModeSetCrtc(fd, crtc_id, buf.fb_id,
			0, 0, &conn_id, 1, &conn->modes[0]);

	getchar();

	/* crop the rect from framebuffer(100, 150) to crtc(50, 50) */
	drmModeSetPlane(fd, plane_id, crtc_id, buf.fb_id, 0,
			50, 50, 320, 320,
			100 << 16, 150 << 16, 320 << 16, 320 << 16);

	getchar();

	modeset_destroy_fb(fd, &buf);

	drmModeFreeConnector(conn);
	drmModeFreePlaneResources(plane_res);
	drmModeFreeResources(res);

	close(fd);

	return 0;
}

以上代码参考Google Android工程中external/libdrm/tests/planetest/planetest.c文件,为了演示方便,仅仅实现了一个最简单的drmModeSetPlane()调用。需要注意的是,该函数调用之前,必须先通过drmModeSetCrtc()初始化整个显示链路,否则Plane设置将无效。

运行结果:(模拟效果)

![在这里插入图片描述](data:image/gif;base64,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

Releases

No releases published

Packages

No packages published