DRM(Direct Rendering Manager)学习简介

时间:2024-05-22 15:13:25

 

DRM

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

DRM从模块上划分,可以简单分为3部分:libdrm、KMS、GEM

DRM_architecture.svg-44.9kB
(图片来自Wiki)
libdrm

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

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

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

DRM框架涉及到的元素很多,大致如下:
KMS:CRTC,ENCODER,CONNECTOR,PLANE,FB,VBLANK,property
GEM:DUMB、PRIME、fence

Screenshot from 2018-11-04 17:02:56.png-34.9kB
(图片来源: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学习教程的目录汇总,后续我会以示例代码的形式和大家分享上述知识点的学习过程,并不断更新目录链接,敬请期待!

    最简单的DRM应用程序 (single-buffer)
    最简单的DRM应用程序 (double-buffer)
    最简单的DRM应用程序 (page-flip)
    最简单的DRM应用程序 (plane-test)
    DRM应用程序进阶 (Property)
    DRM应用程序进阶 (atomic-crtc)
    DRM应用程序进阶 (atomic-plane)
    DRM (Direct Rendering Manager) 的发展历史
    DRM 驱动程序开发(开篇)

参考资料

    Wiki: Direct Rendering Manager
    wowotech: Linux graphic subsystem(2)_DRI介绍
    Boris Brezillon: The DRM/KMS subsystem from a newbie’s point of view
    线·飘零 博客园:Linux环境下的图形系统和AMD R600显卡编程(1)
    Younix脏羊 ****博客:Linux DRM(二)基本概念和特性
---------------------
作者:何小龙
来源:****
原文:https://blog.****.net/hexiaolong2009/article/details/83720940

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

 

在上一篇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_id、connector_id、fb_id、drm_mode 这几个参数。可以看到,几乎所有的代码都是为了该函数能够顺利传参而编写的:

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

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

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

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

运行结果:(模拟效果)
modeset-single-buffer.gif-46.9kB
描述:程序运行后,显示全屏白色,等待用户输入按键;当用户按下任意按键后,程序退出,显示黑屏。

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

 

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

 

在上一篇 最简单的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);
        ...
    }
    ...
}

 

详细参考代码如下:

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 链路连接关系没有发生变化,就不会重新初始化硬件。

运行结果:(模拟效果)
modeset-double-buffer.gif-58.4kB
描述:程序运行后,屏幕显示红色;输入回车后,屏幕显示蓝色;再次输入回车后,程序退出。

    注意:程序运行之前,请确保没有其它应用或服务占用/dev/dri/card0节点,否则将出现 Permission Denied 错误。
---------------------
最简单的DRM应用程序 (page-flip)

在上一篇 最简单的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);
    }
}

 

详细参考代码如下:

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

 

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

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

运行结果:(模拟效果)
page-flip.gif-87.2kB
描述:程序运行后,屏幕在红色和蓝色之间来回切换;当输入CTRL+C后,程序退出。

    注意:程序运行之前,请确保没有其它应用或服务占用/dev/dri/card0节点,否则将出现 Permission Denied 错误。
---------------------
最简单的DRM应用程序 (plane-test)

在上一篇 最简单的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, src_y, src_w << 16, src_h << 16);
    ...
}

   

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

plane-draft.png-42.4kB
(上图实现了裁剪、平移和放大的效果)

    当 SRC_X/Y 和 CRTC_X/Y 不相等时,就实现了平移的效果;
    当 SRC_W/H 和 CRTC_W/H 不相等时,就实现了缩放的效果;
    当 SRC_W/H 和 FB_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 Plane和Primary 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, 150, 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设置将无效。

运行结果:(模拟效果)

DRM(Direct Rendering Manager)学习简介

 

DRM应用程序进阶 (Property)

前言

通过前面几篇《最简单的DRM应用程序》系列文章,我们学习了如何编写一个最基本的DRM应用程序。但是,这些程序所使用的接口,在如今的DRM架构中其实早已经被标记为 Legacy(过时的) 接口了,而目前DRM主要推荐使用的是 Atomic(原子的) 接口。Atomic接口我会在下篇文章中重点介绍,本篇主要介绍Atomic操作必须依赖的基本元素,Property(属性)。
Property

所谓Property,其实就是把前几篇的legacy接口传入的参数单独抽出来,做成一个个独立的全局属性。通过设置这些属性参数,即可完成对显示参数的设置。

Property的结构简单概括主要由3部分组成:name、id 和 value。其中id为该property在DRM框架中全局唯一的标识符。

采用property机制的好处是:

        减少上层应用接口的维护工作量。当开发者有新的功能需要添加时,无需增加新的函数名和IOCTL,只需在底层驱动中新增一个property,然后在自己的应用程序中获取/操作该property的值即可。
        增强了参数设置的灵活性。一次IOCTL可以同时设置多个property,减少了user space与kernel space切换的次数,同时最大限度的满足了不同硬件对于参数设置的要求,提高了软件效率。

DRM中的property大多以功能进行划分,并且还定义了一组 Standard Properties,这些标准properties在任何平台上都会被创建。

下表列出了应用程序开发中,常用的property:
CRTC
name     desctription
ACTIVE     CRTC当前的使能状态,一般用于控制CRTC上下电
MODE_ID     CRTC当前所使用的display mode ID,通过该ID可以找到具体的display mode配置参数
OUT_FENCE_PTR     输出fence指针,指向当前正在显示的buffer所对应的fence fd,该fence由DRM驱动创建,供上层应用程序使用,用来表示当前buffer CRTC是否还在占用

(optional)
name     desctription
DEGAMMA_LUT     de-gamma查找表参数
DEGAMMA_LUT_SIZE     de-gamma查找表参数长度
CTM     Color Transformation Matrix,颜色矩阵转换参数,3x3的矩阵
GAMMA_LUT     gamma查找表参数
GAMMA_LUT_SIZE     gamma查找表参数长度
PLANE
name     desctription
type     plane的类型,CURSOR、PRIMARY或者OVERLAY
FB_ID     与当前plane绑定的framebuffer object ID
IN_FENCE_FD     与当前plane相关联的input fence fd,由buffer的生产者创建,供DRM底层驱动使用,用来标识当前传下来的buffer是否可以开始访问
CRTC_ID     当前plane所关联的CRTC object ID,与CONNECTOR中的CRTC_ID属性是同一个property
SRC_X     当前framebuffer crop区域的起始偏移x坐标
SRC_Y     当前framebuffer crop区域的起始偏移y坐标
SRC_W     当前framebuffer crop区域的宽度
SRC_H     当前framebuffer crop区域的高度
CRTC_X     屏幕显示区域的起始偏移x坐标
CRTC_Y     屏幕显示区域的起始偏移y坐标
CRTC_W     屏幕显示区域的宽度
CRTC_H     屏幕显示区域的高度

(optional)
name     desctription
IN_FORMATS     用于标识特殊的颜色存储格式,如AFBC、IFBC存储格式,该属性为只读
rotation     当前图层的旋转角度
zposition     当前图层在所有图层中的Z轴顺序
alpha     当前图层的global alpha(非pixel alpha),用于多层合成
pixel blend mode     当前图层的合成方式,如Pre-multiplied/Coverage等
CONNECTOR
name     desctription
EDID     Extended Display Identification Data,标识显示器的参数信息,是一种VESA标准数据格式
DPMS     Display Power Management Signaling,用于控制显示器的电源状态,如休眠唤醒。也是一种VESA标准
link-status     用于标识当前connector的连接状态,如Good/Bad
CRTC_ID     当前connector所连接的CRTC object ID,与PLANE中CRTC_ID属性是同一个property

(optional)
name     desctription
PATH     DisplayPort专用的属性,主要用于Multi-Stream Transport (MST) 功能,即多路显示应用场景
TILE     用于标识当前connector是否应用于多屏拼接场景,如平时随处可见的多屏拼接显示的广告大屏幕
Property Type

Property的类型分为如下几种:

        enum
        bitmask
        range
        signed range
        object
        blob

以上类型中需要着重介绍的是object和blob类型,其它类型看名字就知道什么意思,所以就不做介绍了。
Object Property

Object类型的property,它的值用drm object ID来表示。目前的DRM架构中仅用到2个Object Property,它们分别是 "FB_ID" 和 "CRTC_ID" ,它们的property值分别表示framebuffer object ID和crtc object ID。
Blob Property

Blob类型的property,它的值用blob object ID来表示。所谓blob,说白了就是一个自定义长度的内存块,用来存放自定义的结构体数据。典型的Blob Property,如 "MODE_ID" ,它的值为blob object ID,drm驱动可以根据该ID找到对应的drm_property_blob结构体,该结构体中存放着modeinfo的相关信息。

    在DRM的property type中,还有2种特殊的type,它们分别是 IMMUTABLE TYPE 和 ATOMIC TYPE。这两种type的特殊性在于,它们可以和上面任意一种property进行组合使用,用来修饰上面的property。

        IMMUTABLE TYPE:表示该property为只读,应用程序无法修改它的值,如"IN_FORMATS"。
        ATOMIC TYPE:表示该property只有在drm应用程序(drm client)支持ATOMIC操作时才可见。

概念图

还是习惯性的上一张图吧:

DRM(Direct Rendering Manager)学习简介

property.png
总结

DRM的Property,其实有点类似于kernel中的sysfs属性节点。DRM将kernel空间中的重要参数通过property机制导出给上层应用,使得上层应用可以通过修改property的值,来轻松实现参数的传递,而无需额外的IOCTL操作。

通过本篇的学习,我们了解了DRM Property的基本概念,这有助于我们学习后面的Atomic编程。在下一篇文章中,我们将一起来学习如何使用libdrm的Atomic接口进行编程,敬请期待!
---------------------
DRM应用程序进阶 (atomic-crtc)

前言

在上一篇《DRM应用程序进阶(Property)》中,我们学习了Property的基本概念及作用。在本篇中,我们将一起来学习如何操作这些Property,即libdrm Atomic接口的用法。
Atomic

为什么叫“Atomic Commit”?
初学者第一次接触到DRM时,总会好奇当初开发者为什么要起 Atomic 这个名字。Wiki上关于该名词有较详细的解释,如果大家感兴趣可以通过本篇结尾的参考资料获取链接查看。我这里用白话简单概括就是:本次commit操作,要么成功,要么保持原来的状态不变。 即如果中途操作失败了,那些已经生效的配置需要恢复成之前的状态,就像没发生过commit操作似的,这就是Atomic的含义。

而用Commit 一词,是因为本次操作可能会修改到多个参数,等修改好这些参数后,再一次性发起操作请求,有点类似与填表后“提交”材料的意思。

如何操作property?
在上一篇我们了解了Property的基本组成结构,即name、id 和 value。因此操作property就变得非常简单,通过name 来获取property,通过id 来操作property,通过value 来修改property的值。而完成这些操作的应用接口,就是libdrm提供的Atomic接口。

    需要记住一点,在libdrm中,所有的操作都是以Object ID来进行访问的,因此要操作property,首先需要获取该property的Object ID。

伪代码:

int main(void)
{
    ...
    drmSetClientCap(DRM_CLIENT_CAP_ATOMIC);

    drmModeObjectGetProperties(...);
    drmModeGetProperty(property_id)
    ...
    drmModeAtomicAlloc();
    drmModeAtomicAddProperty(..., property_id, property_value);
    drmModeAtomicCommit(...);
    drmModeAtomicFree();
    ...
}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

首先通过drmModeGetProperty()来获取property的相关信息,然后通过drmModeAtomicAddProperty()来修改property的值,最后通过drmModeAtomicCommit()来发起真正的修改请求。

为什么要设置 DRM_CLIENT_CAP_ATOMIC ?
在上一篇《DRM应用程序进阶(Property)》中有介绍过,凡是被DRM_MODE_PROP_ATOMIC修饰过的property,只有在drm应用程序支持Atomic操作时才可见,否则该property对应用程序不可见。因此通过设置DRM_CLIENT_CAP_ATOMIC这个flag,来告知DRM驱动该应用程序支持Atomic操作。
参考代码

基于之前的《最简单的DRM应用程序(plane-test)》的参考代码,我们使用Atomic接口来替代原来的drmModeSetCrtc()接口,从而通过差异对比来学些Atomic接口的操作。

modeset-atomic-crtc.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);
}

static uint32_t get_property_id(int fd, drmModeObjectProperties *props,
                const char *name)
{
    drmModePropertyPtr property;
    uint32_t i, id = 0;

    /* find property according to the name */
    for (i = 0; i < props->count_props; i++) {
        property = drmModeGetProperty(fd, props->props[i]);
        if (!strcmp(property->name, name))
            id = property->prop_id;
        drmModeFreeProperty(property);

        if (id)
            break;
    }

    return id;
}

int main(int argc, char **argv)
{
    int fd;
    drmModeConnector *conn;
    drmModeRes *res;
    drmModePlaneRes *plane_res;
    drmModeObjectProperties *props;
    drmModeAtomicReq *req;
    uint32_t conn_id;
    uint32_t crtc_id;
    uint32_t plane_id;
    uint32_t blob_id;
    uint32_t property_crtc_id;
    uint32_t property_mode_id;
    uint32_t property_active;

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

    drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1);

    /* get connector properties */
    props = drmModeObjectGetProperties(fd, conn_id,    DRM_MODE_OBJECT_CONNECTOR);
    property_crtc_id = get_property_id(fd, props, "CRTC_ID");
    drmModeFreeObjectProperties(props);

    /* get crtc properties */
    props = drmModeObjectGetProperties(fd, crtc_id, DRM_MODE_OBJECT_CRTC);
    property_active = get_property_id(fd, props, "ACTIVE");
    property_mode_id = get_property_id(fd, props, "MODE_ID");
    drmModeFreeObjectProperties(props);

    /* create blob to store current mode, and retun the blob id */
    drmModeCreatePropertyBlob(fd, &conn->modes[0],
                sizeof(conn->modes[0]), &blob_id);

    /* start modeseting */
    req = drmModeAtomicAlloc();
    drmModeAtomicAddProperty(req, crtc_id, property_active, 1);
    drmModeAtomicAddProperty(req, crtc_id, property_mode_id, blob_id);
    drmModeAtomicAddProperty(req, conn_id, property_crtc_id, crtc_id);
    drmModeAtomicCommit(fd, req, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
    drmModeAtomicFree(req);

    printf("drmModeAtomicCommit SetCrtc\n");
    getchar();

    drmModeSetPlane(fd, plane_id, crtc_id, buf.fb_id, 0,
            50, 50, 320, 320,
            0, 0, 320 << 16, 320 << 16);

    printf("drmModeSetPlane\n");
    getchar();

    modeset_destroy_fb(fd, &buf);

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

    close(fd);

    return 0;
}

 
 

通过上面的代码我们可以看出,原来的 drmModeSetCrtc(crtc_id, fb_id, conn_id, &mode) 被下面这部分代码取代了:

    req = drmModeAtomicAlloc();
    drmModeAtomicAddProperty(req, crtc_id, property_active, 1);
    drmModeAtomicAddProperty(req, crtc_id, property_mode_id, blob_id);
    drmModeAtomicAddProperty(req, conn_id, property_crtc_id, crtc_id);
    drmModeAtomicCommit(fd, req, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
    drmModeAtomicFree(req);

 

虽然代码量增加了,但是应用程序的灵活性和可扩展性也增强了。

由于以上代码没有添加对 fb_id 的操作,因此它的作用只是初始化CRTC/ENCODER/CONNECTOR硬件,以及建立硬件链路的连接关系,并不会显示framebuffer的内容,即保持黑屏状态。framebuffer的显示将由后面的 drmModeSetPlane() 操作来完成。
运行结果

以上代码运行的效果如下(模拟效果):

DRM(Direct Rendering Manager)学习简介
---------------------