记录开发中和平时学习总结的关于GPU方面的知识。
VAAPI
Video Acceleration Api(VA API)是一个源码(libva)实现完全开源的遵循MIT许可证的API接口协议。VAAPI主要目的是通过使用GPU等硬件资源去加速视频的编解码和后端处理,以减轻CPU的计算压力。
VAAPI本身是平台无关的,但是主要还是针对于类UNIX系统下的X window图像系统中的DRI层(Direct Rendering Infrastructure)。
vaapi中的基本对象
Surface
描述一种图像格式,包含了宽高、像素格式、内存布局(tiler/non-tiler)等信息。surfaces中的数据client不可以直接访问,并且surfaces中的数据格式 是具体实现决定的。
/**
* \brief Creates an array of surfaces
*
* Creates an array of surfaces. The optional list of attributes shall
* be constructed based on what the underlying hardware could expose
* through vaQuerySurfaceAttributes().
*
* @param[in] dpy the VA display
* @param[in] format the desired surface format. See \c VA_RT_FORMAT_*
* @param[in] width the surface width
* @param[in] height the surface height
* @param[out] surfaces the array of newly created surfaces
* @param[in] num_surfaces the number of surfaces to create
* @param[in] attrib_list the list of (optional) attributes, or \c NULL
* @param[in] num_attribs the number of attributes supplied in
* \c attrib_list, or zero
*/
VAStatus
vaCreateSurfaces(
VADisplay dpy,
unsigned int format,
unsigned int width,
unsigned int height,
VASurfaceID *surfaces,
unsigned int num_surfaces,
VASurfaceAttrib *attrib_list,
unsigned int num_attribs
);
因为VAAPI的接口要兼容不同的硬件,所有接口设计一定要最大限度的减小耦合,所以vaCreateSurfaces()接口返回一个整形的VASurfaceID来索引创建出的 surface对象。
Image
Image用于表示图像,可以通过GPU的sample采样器进行快速读取。OpenCL中的Image默认是tiled模式。
/**
* Create a VAImage structure
* The width and height fields returned in the VAImage structure may get
* enlarged for some YUV formats. Upon return from this function,
* image->buf has been created and proper storage allocated by the library.
* The client can access the image through the Map/Unmap calls.
*/
VAStatus vaCreateImage (
VADisplay dpy,
VAImageFormat *format,
int width,
int height,
VAImage *image /* out */
);
typedef struct _VAImage
{
VAImageID image_id; /* uniquely identify this image */
VAImageFormat format;
VABufferID buf; /* image data buffer */
/*
* Image data will be stored in a buffer of type VAImageBufferType to facilitate
* data store on the server side for optimal performance. The buffer will be
* created by the CreateImage function, and proper storage allocated based on the image
* size and format. This buffer is managed by the library implementation, and
* accessed by the client through the buffer Map/Unmap functions.
*/
uint16_t width;
uint16_t height;
uint32_t data_size;
uint32_t num_planes; /* can not be greater than 3 */
/*
* An array indicating the scanline pitch in bytes for each plane.
* Each plane may have a different pitch. Maximum 3 planes for planar formats
*/
uint32_t pitches[3];
/*
* An array indicating the byte offset from the beginning of the image data
* to the start of each plane.
*/
uint32_t offsets[3];
/* The following fields are only needed for paletted formats */
int32_t num_palette_entries; /* set to zero for non-palette images */
/*
* Each component is one byte and entry_bytes indicates the number of components in
* each entry (eg. 3 for YUV palette entries). set to zero for non-palette images
*/
int32_t entry_bytes;
/*
* An array of ascii characters describing the order of the components within the bytes.
* Only entry_bytes characters of the string are used.
*/
int8_t component_order[4];
/** \brief Reserved bytes for future use, must be zero */
uint32_t va_reserved[VA_PADDING_LOW];
} VAImage;
通过VAImage结构体可知,Image对象使用的内存是由Buffer对象表示。Image对象是通过vaDeriveImage()接口或者vaGetImage()接口从Surface对象中 获取图像数据的。
Buffers
Buffers属于线性模式、通用的内存。被用来从client传递各种类型的数据到server端。
/** Buffer functions */
/**
* Creates a buffer for "num_elements" elements of "size" bytes and
* initalize with "data".
* if "data" is null, then the contents of the buffer data store
* are undefined.
* Basically there are two ways to get buffer data to the server side. One is
* to call vaCreateBuffer() with a non-null "data", which results the data being
* copied to the data store on the server side. A different method that
* eliminates this copy is to pass null as "data" when calling vaCreateBuffer(),
* and then use vaMapBuffer() to map the data store from the server side to the
* client address space for access.
* The user must call vaDestroyBuffer() to destroy a buffer.
* Note: image buffers are created by the library, not the client. Please see
* vaCreateImage on how image buffers are managed.
*/
VAStatus vaCreateBuffer (
VADisplay dpy,
VAContextID context,
VABufferType type, /* in */
unsigned int size, /* in */
unsigned int num_elements, /* in */
void *data, /* in */
VABufferID *buf_id /* out */
);
和Surface对象一样,为了接口的低耦合,vaCreateBuffer()返回的也是整形的VABufferID来索引创建出来的buffer内存。由于vaBuffer()的低耦合,vaapi中的buffers对象被重载成很多其他数据参数对象。这些参数对象通过VABufferType描述。
typedef enum
{
VAPictureParameterBufferType = 0,
VAIQMatrixBufferType = 1,
VABitPlaneBufferType = 2,
VASliceGroupMapBufferType = 3,
VASliceParameterBufferType = 4,
VASliceDataBufferType = 5,
VAMacroblockParameterBufferType = 6,
VAResidualDataBufferType = 7,
VADeblockingParameterBufferType = 8,
VAImageBufferType = 9,
VAProtectedSliceDataBufferType = 10,
VAQMatrixBufferType = 11,
VAHuffmanTableBufferType = 12,
VAProbabilityBufferType = 13,
/* Following are encode buffer types */
VAEncCodedBufferType = 21,
VAEncSequenceParameterBufferType = 22,
VAEncPictureParameterBufferType = 23,
VAEncSliceParameterBufferType = 24,
VAEncPackedHeaderParameterBufferType = 25,
VAEncPackedHeaderDataBufferType = 26,
VAEncMiscParameterBufferType = 27,
VAEncMacroblockParameterBufferType = 28,
VAEncMacroblockMapBufferType = 29,
/**
* \brief Encoding QP buffer
*
* This buffer contains QP per MB for encoding. Currently
* VAEncQPBufferH264 is defined for H.264 encoding, see
* #VAEncQPBufferH264 for details
*/
VAEncQPBufferType = 30,
/* Following are video processing buffer types */
/**
* \brief Video processing pipeline parameter buffer.
*
* This buffer describes the video processing pipeline. See
* #VAProcPipelineParameterBuffer for details.
*/
VAProcPipelineParameterBufferType = 41,
/**
* \brief Video filter parameter buffer.
*
* This buffer describes the video filter parameters. All buffers
* inherit from #VAProcFilterParameterBufferBase, thus including
* a unique filter buffer type.
*
* The default buffer used by most filters is #VAProcFilterParameterBuffer.
* Filters requiring advanced parameters include, but are not limited to,
* deinterlacing (#VAProcFilterParameterBufferDeinterlacing),
* color balance (#VAProcFilterParameterBufferColorBalance), etc.
*/
VAProcFilterParameterBufferType = 42,
VABufferTypeMax
} VABufferType;
Config
Config对象包含profile和entrypoint信息,用来描述当前的硬件加速上下文环境配置参数信息。
/**
* Create a configuration for the video decode/encode/processing pipeline
* it passes in the attribute list that specifies the attributes it cares
* about, with the rest taking default values.
*/
VAStatus vaCreateConfig (
VADisplay dpy,
VAProfile profile,
VAEntrypoint entrypoint,
VAConfigAttrib *attrib_list,
int num_attribs,
VAConfigID *config_id /* out */
);
目前,1.8.0版本的libva支持的profile和entrypoint如下:
/**
* Currently defined entrypoints
*/
typedef enum
{
VAEntrypointVLD = 1,
VAEntrypointIZZ = 2,
VAEntrypointIDCT = 3,
VAEntrypointMoComp = 4,
VAEntrypointDeblocking = 5,
VAEntrypointEncSlice = 6, /* slice level encode */
VAEntrypointEncPicture = 7, /* pictuer encode, JPEG, etc */
/*
* For an implementation that supports a low power/high performance variant
* for slice level encode, it can choose to expose the
* VAEntrypointEncSliceLP entrypoint. Certain encoding tools may not be
* available with this entrypoint (e.g. interlace, MBAFF) and the
* application can query the encoding configuration attributes to find
* out more details if this entrypoint is supported.
*/
VAEntrypointEncSliceLP = 8,
VAEntrypointVideoProc = 10, /**< Video pre/post-processing. */
} VAEntrypoint;
/** Currently defined profiles */
typedef enum
{
/** \brief Profile ID used for video processing. */
VAProfileNone = -1,
VAProfileMPEG2Simple = 0,
VAProfileMPEG2Main = 1,
VAProfileMPEG4Simple = 2,
VAProfileMPEG4AdvancedSimple = 3,
VAProfileMPEG4Main = 4,
VAProfileH264Baseline va_deprecated_enum = 5,
VAProfileH264Main = 6,
VAProfileH264High = 7,
VAProfileVC1Simple = 8,
VAProfileVC1Main = 9,
VAProfileVC1Advanced = 10,
VAProfileH263Baseline = 11,
VAProfileJPEGBaseline = 12,
VAProfileH264ConstrainedBaseline = 13,
VAProfileVP8Version0_3 = 14,
VAProfileH264MultiviewHigh = 15,
VAProfileH264StereoHigh = 16,
VAProfileHEVCMain = 17,
VAProfileHEVCMain10 = 18,
VAProfileVP9Profile0 = 19,
VAProfileVP9Profile1 = 20,
VAProfileVP9Profile2 = 21,
VAProfileVP9Profile3 = 22
} VAProfile;
Context
context代表的是一个虚拟的video decode pipeline。上面的surfaces对象是context中的render targets。当context被创建时,surfaces对象 需要绑定到context中,并且不能同时绑定到其他context中。
/**
* vaCreateContext - Create a context
* dpy: display
* config_id: configuration for the context
* picture_width: coded picture width
* picture_height: coded picture height
* flag: any combination of the following:
* VA_PROGRESSIVE (only progressive frame pictures in the sequence when set)
* render_targets: a hint for render targets (surfaces) tied to the context
* num_render_targets: number of render targets in the above array
* context: created context id upon return
*/
VAStatus vaCreateContext (
VADisplay dpy,
VAConfigID config_id,
int picture_width,
int picture_height,
int flag,
VASurfaceID *render_targets,
int num_render_targets,
VAContextID *context /* out */
);
和surface对象一样,context对象也是由整形ID索引的,所以对于clients来说,surfaces、contexts的内部实现都是不可见的。
Both contexts and surfaces are identified by unique IDs and its implementation specific internals are kept opaque to the clients.
vaapi调用流程
基本流程
- 同后端server协商一个可以工作的configuration,确认好profile、entrypoints以及其他一些属性。
- 创建一个decode context来模拟硬件解码环境。
- 获取decode buffers,并且根据entrypoints的不同,使用picture level、slice level以及macroblock level数据填充进这些buffers。
- 将上面的buffers传输到后端server,使用后端server完成最终的解码。
初始化流程细节
- 确认支持的profiles。
- 确认profile对应的entrypoints。
- 确认对应profile\entrypints的配置属性。
- 通过上面的信息创建一个config对象实例供decoder使用。
更具体的函数调用关系图如下:
linux下GPU架构的演变
理解一项技术、一个软件框架,从了解其发展历程着手可以对理解其技术细节有非常大的帮助。了解技术背后的文化往往比单纯已经技术本身重要很多。
X11的内部架构
DIX - Device-Independennt X DDX - Device-Dependent X
DRI/DRM 内部架构
一开始,linux下只有XFree86 server会去直接访问显卡,所有当时的设计很简单,XFree86以特权模式运行,可以直接在用户层访问显卡硬件,而不需要linux内核的支持进行2D加速。这样的设计的好处就是架构非常的简单,从而使XFree86 server进行不同操作系统的移植非常的方便。
后来,第一个对立与硬件的3D加速器Utah-GLX合入了linux中。和之前的2D驱动一样,这个3D软件也是在用户层直接访问显卡硬件。
与此同时,framebuffer驱动架构模型渐渐得到广泛应用,它代表的是另一种模拟直接访问显卡硬件的组件。由于多了framebuffer这样的和XFree86 Server可能同时竞争访问显卡硬件资源的组件,内核中引入了VT switchs。它会通过内核向X server发送信号告诉x derver去保存一下当前的显卡状态数据。因为多了这样的几张上,导致显驱动的开发不再那么简单,而是变得bug百出。
上图就是早期linux下的graphics stack。显然,这种模型是有缺陷的。首先,它要求授予用户层的应用程序特权去直接访问硬件的设备。其次,所有的GL加速必须通过X 协议间接的实现,这使得加速效果大打折扣。这对于那些数据密集的功能如3D纹理加载影响很大。由于大家更加关注linux下的安全性和性能,必须得设计新的模型替代上面的架构。
于是引出了DRI模块,这个模块依赖于在内核中的一个负责检查3D命令流的正确性和安全性的组件。加入这个组件后,原来直接访问底层硬件的程序现在改成向该组件发送command buffers来实现间接访问。这个架构还只是针对与3D引擎的,2D引擎和之前一样仍然通过拥有超级用户权限的应用程序来访问底层硬件。
目前实际的架构是为了满足一些新的需求。首先,要解决X server使用超级用户权限带来的安全性问题。其次,解决之前设计中不同驱动竞争同一硬件带来的问题。要解决这些问题,需要完成两个部分的.1)将内核中framebuffer的功能合入到DRM模块中;2)让X.Org通过非特权模式访问DRM模块来间接访问显卡硬件。这就是所谓的KMS(Kernel Modesetting)架构。在这个架构中,DRM模块负责为framebuffer和X.org通过modesetting 服务。
The Linux graphics stack
GTT
工作中刚接触到GPU相关内容时,经常看到听到关于GTT(Graphic Translation Table)的概念。单纯研究理解GTT的概念可能不是很好懂,有时候我们需要跳出问题本身而站的高一点
来看问题就容易理解很多。
所以我们先研究显卡显存的管理机制。
显卡使用的内存分为两部分,一部分是显卡自身集成的内存叫做VRAM
,另一部分是使用系统的内存。所谓的集成显卡就是没有自身内存而完全使用系统内存的显卡。因为访问显存的速度显然要比访问系统内存快很多,所以高端游戏笔记本独显是基本配置。对于即要使用自身显存,又要使用系统内存的显卡,如何管理这两种内存,就引出了GTT的概念。因为显卡如果要想使用系统内存,肯定得申请,而且一般需要的内存基本上是大于512M的大快内存,如果让内核一次性分配连续的这么大的内存给显卡使用肯定是不可能的,因为者会造成很大的浪费。所以显存必须按需向系统申请内存。这样显卡得到的系统内存肯定是不连续的。而显卡自身的内存一般是连续的。对于管理这两种内存,最简单的方法就是统一编址(这类似与RISC机器上IO和内存统一遍址)。统一编址管理不连续的内存,显然就得建立内存页表进行映射。所有GTT全称正是:graphics address remapping table.说白了,GTT就是显卡中用于管理系统内存的一种机制,和系统自身所使用的页表映射非常类似。
除了GTT,一般GPU中还包含PPGTT概念,其实就是多级页表映射,具体见下面2图:
对于集成的显卡,一般都是通过PCIE总线挂接到系统总线上的,如下图所示:
DRM
从前面章节可见DRM(Direct Rendering Manager)子系统在GPU驱动架构下发挥着非常重要的作用,下面详细剖析DRM内部架构。
从用户层角度概况来说,DRM主要提供两个功能,向GPU发生命令和数据、配置显示模块的显示模式。从内核层角度来说,为了实现将用户层的命令和数据送进 GPU,它得实现CPU和GPU内存之间互相访问的机制以及所有涉及到的内存对象的管理工作,这部分工作由DR内部的GEM(Graphics Execution Manager)、DMA_Buffer等组件或者接口完成;为了可以让用户灵活地设置显示模式,DRM内部实现了KMS(kernel Mode Setting)组件。所以DRM内部架构大致如下图:
从上图可见,DRM驱动遵循了UNIX的哲学-everything is a file
,没有自定义一套系统调用,而是将每个检测到的GPU设备输出为根文件系统下/dev/dri/cardX
的一个DRM设备文件,如果硬件上只有一个GPU,那么显然前面的这个X就为0,即/dev/dri/card0
。
DRM驱动主要由DRM Core
和DRM Driver
两部分组成,其中DRM Core提供基本的软件框架,可以让不同的DRM Driver注册进来,并且它只提供少量和硬件无关通用的IOCTL接口;而DRM Driver实现了和硬件特性相关的IOCTL接口。与之对应到用户层,libdrm库封装实现了DRM Core提供的接口;libdrm-driver库封装实现了DRM Driver提供的接口。
GEM
GEM(Graphic Execution Manager)主要提供了管理内存的API原语。通过GEM,一个用户层程序可以创建、处理以及销毁存在于GPU中的内存对象,即GEM object
。内核中关于这块的对象表达和层次关系见下图:
上图是基于intel的i915系列GPU的内核驱动中的内存管理对象,struct drm_device显然是DRM Core中的核心对象,其中dev_private字段就是我们上面提到的用来注册DRM Driver对象的,也就是描述特定GPU硬件特性的对象。gtt->base->mm,即struct drm_mm对象,描述的是GTT的映射关系。从命名方式上就可以看出,drm_mm对象也是DRM Core中的对象。drm_mm包含了已分配的所有内存节点(head_node)和可用的hole(hole_stack)资源。
上面提到的GEM object对应的就是struct drm_i915_gem_object结构体,DRM中所有不同的GEM object都会挂载到struct i915_gem_mm中的bound_list
中。值得注意的是,GEM object并没有直接管理实际内存,管理GTT线性地址分配以及实际物理内存的是struct i915_vma对象,然后这个对象创建后都会通过vma_list绑定到GEM object中。所有描述物理内存的对象实例i915_vma最终都会挂载到drm_mm对象中的head_node字段中去,这样gtt->base就管理了DRM中所有的内存。
flink
GEM内存管理的很重要的一部分内容是如何让多个用户进程访问同一个GEM object,flink是其中的一种方式。
GEM handles是一个32位的整形数,显然这个handle只能是在一个进程(线程)内对立的,在其他进程中肯定存在相同的handle值但是指向不同的GEM object。所以需要一个全局的命名空间(global namespace)。其实实现这个功能很简单,GEM就是简单的使用了另外一个整形数,叫GEM names,来索引这个GEM object。和GEM handle不同的是,这个整形数是整个DRM驱动中从开始运行后一直唯一的一个数。
所谓的flink,就是从一个GEM handle获得相应的GEM name的方法。有了这个全局的GEM name作为中转,不同的进程就可以互相访问对方的GEM object了。
应用层中对应于内核中的drm_i915_gem_object对象的是libdrm中的Bo对象,libdrm中通过flink机制创建bo对象以及相应内核内部发生的动作流程如下:
联系前面VAAPI章节的内容,其实flink最大用途就是将不同线程下创建的gem object对象绑定到同一一个surface对象中去。
prime
上面的flink方式虽然简单可行,但是存在明显的安全问题,因为一个整形的全局GEM name很容易被猜测出来,这样相关的内存很容易就被篡改,所有DRM中还有另外一个
基于DMA_BUF的更安全的共享GEM object的方式 - prime
。
简单来说,prime就是使用了文件描述符替换了flink中的全局整形数来作为中转,因为文件描述符is not a global namespace,而是通过Unix domain socket方式实现的,所以它很难被猜测到。
prime是基于DMA Buffer Sharing API的,它是Linux内核中一个通用的组件,用来让不同类型的设备驱动可以共享DMA buffers。让不同驱动设备可以直接共享相同的内存的最大意义在于可以实现0拷贝(zero-copy)。
关于DMA_BUF的详细说明和应用实例见这篇文章。
KMS(Kernel Mode Setting)
为了能够让显示器正常显示输出图像,显卡必须正确配置分辨率、色深、刷新率等,这个操作过程就是mode-setting。因为这个配置过程必须要访问显卡的寄存器,
所以在早期,负责进行mode-setting的应用程序必须进入特权模式才可以设置。比如早期x server中的DDX driver就存在适配不同显卡的mode-seting模块。
这种在应用层进行mode-setting的方式被称为User space Mode-Setting,即UMS
。
UMS存在如下几个问题:
-
它违背了操作系统应该隔离应用程序和硬件的基本原则,这样会引发安全和稳定性问题。
-
如果有多个应用程序需要进行mode-setting,必然导致并发竞争问题。
-
当显卡出现问题时,内核无法打印相关的信息,内核只知道VESA BIOS standard text modes。
为了解决上面这些问题,mode-setting的功能只能被放到内核中去实现,但是为什么不是实现成单独的内核模块而是放进DRM驱动中呢?因为mode-setting的
过程和DRM内存管理有很大的关系。这种在内核中进行mode-setting的方式很自然的更名为KMS
- kernel Mode-Setting。
KMS有如下优点:
-
首先最直接的好处就是,从应用层到内核层(linux console,fbdev)避免了大量的重复的modt-setting相关的代码。
-
上面存在的问题也都没有了。
-
解决了长期存在的显示器热插拔问题。
KMS设备模型
linux内核代码都是面向对象的设计,所以实现驱动的第一步就应该是合理的设备建模,KMS的设备模式如下:
-
CRTC: CRTC是显示控制器的scanout引擎,它的作用是读取scanout buffer(framebuffer)中的piexl data,然后在PLL电路的帮助下生成视频时序 信号。
-
Encoders: 显示控制器通过Enocder将来自CRTC的视频时序信号编码成所接connector可以识别的信号格式。比如所接的是数字输出的话,这个编码器 得将信号编码成TMDS或者LVDS格式;如果接的是模拟输出(VGA、TV),那么这个编码器内部还得集成特殊的DAC模块。
-
Connectors: 相对于前面两个都是纯粹的软件抽象,connector描述的更接近于实际的硬件连接接口,比如VGA、DVI、HDMI、DP等等。另外和这些接口连接 的显示设备的连接状态信息、EDID信息、DPMS状态等等信息也得存储在connector对象中。
-
Planes: plane是一个比前两个更要完全的软件抽象,它就是一个内存对象,是CRTC处理的数据源。每个CRTC都至少得有一个primary plane这样的数据源。 其他还有cursor plane等等。