VirtIO-GPU —— 2D加速原理分析

文章目录

  • 前言
  • 数据结构
    • 命令传递
      • virtio_gpu_ctrl_command
      • virtio_gpu_ctrl_hdr
    • 图像处理
      • virtio_gpu_simple_resource
      • virtio_gpu_scanout
    • GPU设备
  • 流程分析
    • 后端通用流程
    • 资源初始化
    • 资源绑定
    • Host转换图像
    • 输出图像

前言

  • 为什么要引入virtio-gpu?
  • TODO
  • 本文基于qemu-2.4.1分析,因为2.4.1版本刚引入virtio-gpu功能,实现比较简单。

数据结构

命令传递

virtio_gpu_ctrl_command

struct virtio_gpu_ctrl_command {
    VirtQueueElement elem;						/* 1 */
    VirtQueue *vq;								/* 2 */
    struct virtio_gpu_ctrl_hdr cmd_hdr;			/* 3 */
	......
    QTAILQ_ENTRY(virtio_gpu_ctrl_command) next;	/* 4 */
};
1. 从环上取下来的元素。如果guest向host发送数据,qemu在处理的时候会将其映射到elem.out_sg中,out_num指明有多少个descriptor。
2. 指向共享的virtio队列,virtio-gpu有两个队列用作数据传输,一个是命令字队列ctrl_vq,一个是光标信息队列cursor_vq。
3. 从ctrl_vq队列映射出来的信息。qemu在处理队列上的数据时,首先取出队列中存放的数据地址,将其映射到elem。然后根据地址转换成具体的结构
体,就是这里的cmd_hdr。virtio_gpu_ctrl_hdr可以认为是基类,每个virtio-gpu的命令字都是这个类的派生,因此对于不同命令字,还可以转换为不同的
派生类,后面会详细解释。
4. 前端在发送图像处理的命令时,一次性可以发送多个命令字,每个命令字对应一条descriptor chain,包含多个descriptor。后端在解析的时候,将每个
descriptor chain解析出的命令填入virtio_gpu_ctrl_command。多个command通过next字段统一链入到VirtIOGPU->cmdq上。在处理图像命令时依次取出cmdq上的每个元素command,调用virtio_gpu_process_cmdq挨个处理command。

virtio_gpu_ctrl_hdr

  • virtio_gpu_ctrl_hdr是virtio队列ctrl_vq上的信息,前端将图像处理命令放到ctr_vq队列上,作为数据传递给后端。
struct virtio_gpu_ctrl_hdr {
	uint32_t type;				/* 1 */
	uint32_t flags;
	uint64_t fence_id;
	uint32_t ctx_id;
	uint32_t padding;
};
1. 图像处理命令,这是virtio-gpu定义的一组图像处理命令集,virtio_gpu_ctrl_type包含了前后端可以处理的所有图像处理命令,virtio-gpu的前端将DRM
框架涉及到的图像处理命令截获,分解成virtio-gpu自己定义的一组命令,通过virtio队列传递给后端处理,后端或者利用GPU硬件加速图像处理,或者利
用软件模拟最终交给CPU计算处理,从而在host上实现处理图像命令。virtio-gpu的本质,就是让原本应该在Guest进行的图像计算,交给Host来做,
Host既可以使用GPU加速,也可以使用CPU计算。
  • 对于前端下发的每一个命令字,都需要包含这个结构体作为公共的头部,然后附加上每个命令字的数据结构。如下图所示:
    VirtIO-GPU —— 2D加速原理分析_第1张图片

图像处理

virtio_gpu_simple_resource

  • virtio-gpu最开始加入的时候,后端只处理2d的图片,对前端创建的每一个2d图片,qemu都把它抽象成一个图片资源的结构,如下:
struct virtio_gpu_simple_resource {
    uint32_t resource_id;								/* 1 */
    uint32_t width;										/* 2 */
    uint32_t height;								
    uint32_t format;
    struct iovec *iov;									/* 3 */
    unsigned int iov_cnt;
    pixman_image_t *image;								/*  4 */
    QTAILQ_ENTRY(virtio_gpu_simple_resource) next;		/*  5 */
};
1. 图片资源的ID,通过resource_id可以在VirtIOGPU->reslist链表中查找到GPU设备维护的图片资源
2. 图片资源的长,宽和格式,前端在创建图片的命令字RESOURCE_CREATE_2D中会附加上图片的长宽格式等基本信息,后端根据这个信息创建
图片资源
3. 图片资源的像素,我们知道图片由像素组成,每个像素就是一组rgb的数据。对于图片的处理,前端可以直接将图片像素放到队列上,然后通知后端,后端接到通知后,将队列上的像素信息转换成主机上pixman库可以显示的信息,由此完成前端的像素信息转换成后端的像素信息。iov就是
前端的像素数据,image就是从iov中拷贝的像素数据。当前端发送TRANSFER_TO_HOST_2D命令的时候,除了命令字,队列上还附加了前端的
像素数据。后端处理该命令字的动作就是将队列上指明的像素数据地址拷贝到image指定的内存地址空间。
4. pixman库函数操作的图片资源类型,qemu想要pixman处理2D图片,需要将像素数据转换成pixman定义的像素资源类型,遵循pixman的编程接口。
5. 每个图片资源通过next结构体被组织起来,放到VirtIOGPU->reslist上。

virtio_gpu_scanout

struct virtio_gpu_scanout {
    QemuConsole *con;
    DisplaySurface *ds;
    uint32_t width, height;
    int x, y;
    int invalidate;
    uint32_t resource_id;
    QEMUCursor *current_cursor;
};
  • TODO

GPU设备

typedef struct VirtIOGPU {
    VirtIODevice parent_obj;							/* 1 */

    QEMUBH *ctrl_bh;									/* 2 */
    QEMUBH *cursor_bh;									/* 3 */
    VirtQueue *ctrl_vq;									/* 4 */
    VirtQueue *cursor_vq;								/* 5 */
	......
    QTAILQ_HEAD(, virtio_gpu_simple_resource) reslist;	/* 6 */
	......
} VirtIOGPU;
1. virtio-gpu基于virtio设备
2. virtio-gpu处理图像命令的下半部。当控制队列上由数据到达通知到后端时,对应的回调函数只是触发提前注册的下半部处理例程,然后当前线程返回。真正的处理会在主线程中执行。
3. 同ctrl_bh类似,只不过处理的时光标相关的命令
4. virtio-gpu命令队列,用于存放前端的图像处理命令。在virtio-gpu中,图像处理的命令被封装成数据,放到了virtio的环上发送给后端
5. 同ctrl_vq类似,只不过存放的是光标相关信息
6. 后端要处理的所有图像的链表,前端发送RESOURCE_CREATE_2D命令后,后端调用主机上的pixman 2d图像处理库pixman创建一张图片资
源,然后添加到这个链表中管理起来。同时将创建的图片资源发送给前端。当前端发送其它图像处理命令的时候,将图片资源的resource_id也附
加到命令字中,指定要处理的是哪个图片,后端就可以根据命令字从reslist找到对应资源,然后调用主机上的pixman处理对应的图片就可以了。
  • VirtIOGPU上维护的所有资源组织如下:
    VirtIO-GPU —— 2D加速原理分析_第2张图片

流程分析

后端通用流程

  • 当前端要处理图片时,首先创建一张空白的图片,指定这个图片的大小和格式。然后是填充像素数据,填充像素有两种方式,一是创建默认的像素数据,二是拷贝已有的像素数据。像素数据填充后,就是像素的处理,对于简单的2d图像,qemu调用linux上的pixman库接口实现。像素处理实际上就是对像素数据进行加工,比如明暗处理,光栅化处理等。这个过程可以认为是针对每个像素数据做了一次函数变换,最终得到新的像素,重新填充到图片中。像素处理完成后,就是最后的相片显示。
  • virtio-gpu的后端和核心实现,就是接受前端下发的一系列2d图像处理操作,或是qemu自己处理,或是调用主机上的图形库接口处理,完成前端期望的动作,最后达到主机侧处理虚机图像目的。
  • 前端下发的图像处理命令由枚举类型virtio_gpu_ctrl_type表示,如下:
enum virtio_gpu_ctrl_type {
	VIRTIO_GPU_UNDEFINED = 0,

	/* 2d commands */
	VIRTIO_GPU_CMD_GET_DISPLAY_INFO = 0x0100,				/* 1 */
	VIRTIO_GPU_CMD_RESOURCE_CREATE_2D,						/* 2 */
	VIRTIO_GPU_CMD_RESOURCE_UNREF,							/* 3 */
	VIRTIO_GPU_CMD_SET_SCANOUT,								/* 4 */	
	VIRTIO_GPU_CMD_RESOURCE_FLUSH,							/* 5 */
	VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D,						/* 6 */
	VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING,					/* 7 */
	VIRTIO_GPU_CMD_RESOURCE_DETACH_BACKING,

	/* cursor commands */
	VIRTIO_GPU_CMD_UPDATE_CURSOR = 0x0300,					/* 8 */
	VIRTIO_GPU_CMD_MOVE_CURSOR,
	......
}
1. 2d图像操作的命令集,获取中断的显示信息,该信息由qemu的VirtIOGPU.req_state字段保存,qemu收到命令后直接从内存中取出信息返回给前端
2. 创建一个图片资源,不填充像素
3. 删除一个图片资源
4. TODO
5. 向终端传输图片像素数据,显示图像
6. 将后端qemu维护的像素数据传递给成pixman库,从进行像素处理
7. 将前端传递的像素数据从virtio队列中取出,转换成后端qemu维护的像素数据
8. 光标显示相关的命令
  • virtio-gpu定义了两个队列,分别用来传递图像处操作和光标显示操作,他们是VirtIOGPU.ctrl_vq和VirtIOGPU.cursor_vq。当ctrl_vq队列被前端填充数据后,会通知到后端qemu。后端qemu的响应从virtio_gpu_handle_ctrl_cb开始,它是队列处理的回调函数,注册如下:
virtio_gpu_device_realize
	g->ctrl_vq   = virtio_add_queue(vdev, 64, virtio_gpu_handle_ctrl_cb);		/* 9 */
	g->cursor_vq = virtio_add_queue(vdev, 16, virtio_gpu_handle_cursor_cb);

static void virtio_gpu_handle_ctrl_cb(VirtIODevice *vdev, VirtQueue *vq)
{
    VirtIOGPU *g = VIRTIO_GPU(vdev);
    qemu_bh_schedule(g->ctrl_bh);												/* 10 */
}
9. 注册virtio队列的对调函数
10. 当virtio队列由数据到达时,出发回调,执行下半部的操作
  • 可以看到回调函数实际上只是出发了一个下半部操作,其注册如下:
virtio_gpu_device_realize
    g->ctrl_bh = qemu_bh_new(virtio_gpu_ctrl_bh, g);
    g->cursor_bh = qemu_bh_new(virtio_gpu_cursor_bh, g);
  • 从这里可以看到,处理virtio-gpu队列是在主线程的下半部进行,继续分析:
virtio_gpu_ctrl_bh
	virtio_gpu_handle_ctrl(&g->parent_obj, g->ctrl_vq);

static void virtio_gpu_handle_ctrl(VirtIODevice *vdev, VirtQueue *vq)
{
    VirtIOGPU *g = VIRTIO_GPU(vdev);
    struct virtio_gpu_ctrl_command *cmd;
	......
    cmd = g_new(struct virtio_gpu_ctrl_command, 1);				/* 11 */
    while (virtqueue_pop(vq, &cmd->elem)) {						/* 12 */
        cmd->vq = vq;											/* 13 */
        cmd->finished = false;
		......
        virtio_gpu_simple_process_cmd(g, cmd);					/* 14 */
		......
	}
}
11. 为cmd结构体分配内存,用于存放队列上的数据
12. 从队列上取数据,存放到cmd->elem钟
13. 初始化cmd的其它字段
14. 每个elem就是一个virtio-gpu的命令字,依次处理
  • virtio_gpu_simple_process_cmd根据队列中的命令,执行具体图像处理操作,如下:
static void virtio_gpu_simple_process_cmd(VirtIOGPU *g,
                                          struct virtio_gpu_ctrl_command *cmd)
{
    VIRTIO_GPU_FILL_CMD(cmd->cmd_hdr);				/* 15 */

    switch (cmd->cmd_hdr.type) {					/* 16 */
    case VIRTIO_GPU_CMD_GET_DISPLAY_INFO:
        virtio_gpu_get_display_info(g, cmd);
        break;
    case VIRTIO_GPU_CMD_RESOURCE_CREATE_2D:
        virtio_gpu_resource_create_2d(g, cmd);
        break;
	......
    }
  	if (!cmd->finished) {							/* 17 */
        virtio_gpu_ctrl_response_nodata(g, cmd, cmd->error ? cmd->error :
                                        VIRTIO_GPU_RESP_OK_NODATA);
    }
}
15. 从virtio队列中取出具体的命令字,填充到cmd->cmd_hdr中
16. 根据cmd_hdr命令字,做对应的图像处理
17. 对于有些命令,后端在处理之后还需要返回信息给前端,比如VIRTIO_GPU_CMD_RESOURCE_CREATE_2D命令,他需要将资源的ID传回给前
端,这里就是往前端回送信息

资源初始化

  • 图片资源包含3个基本的特性:
  1. 属性,长,宽,格式。
  2. 像素数据,图片信息由像素存储,对于黑白图片,一个像素由灰度值表示,范围是0-255,0为黑色,255为白色,对于彩色图片,一个像素由三个原色按比例组成(RGB——红绿蓝)组成。
  3. 像素操作相关上下文,在虚拟化场景下,虚机对于图片的像素操作有两种选择,一是使用虚机内部的像素操作库函数(比如pixman),二是利用主机上的pixman库函数进行像素操作。前者实际上是CPU模拟完成的图片处理,后者利用主机CPU完成像素操作,显然后者效率更高,这就是2D加速。要使用pixman进行图片处理,需要遵循pixman的编程API,创建一个pixman可以识别的图片资源存放像素数据,然后针对这个资源进行像素处理
  • 基于上面的分析,前端如果想利用后端的pixman加速处理2D图片,一种常规的实现方案是,告诉后端自己想处理的2D图片的属性,比如长,宽,图片格式,让后端准备好相应的资源,这里后端准备的东西包括上面介绍的1和3,对于2像素数据,不能让后端知晓,否则就会有安全隐患。后端准备好资源后,前端再分配一块内存用于存放像素数据,然后通知后端把这块内存和之前创建的图片资源绑定,当前端往这块内存中写入像素数据时,后端会检查到,然后或调用pixman接口操作像素数据,或直接输出到显示buffer。virtio-gpu的工作方式就是这样,资源创建与像素填充分开,主机侧负责资源创建,虚机负责像素填充,最终主机侧负责图片处理,virtio-gpu把这两个步骤抽象成两个命令,分别是资源创建VIRTIO_GPU_CMD_RESOURCE_CREATE_2D和资源绑定VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING,资源创建由以下函数:
static void virtio_gpu_resource_create_2d(VirtIOGPU *g,
                                          struct virtio_gpu_ctrl_command *cmd)
{
    pixman_format_code_t pformat;
    struct virtio_gpu_simple_resource *res;
    struct virtio_gpu_resource_create_2d c2d;

    VIRTIO_GPU_FILL_CMD(c2d);									/* 1 */
	......
    res = virtio_gpu_find_resource(g, c2d.resource_id);			/* 2 */
    if (res) {
        qemu_log_mask(LOG_GUEST_ERROR, "%s: resource already exists %d\n",
                      __func__, c2d.resource_id);
        cmd->error = VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID;
        return;
    }

    res = g_new0(struct virtio_gpu_simple_resource, 1);			/* 3 */
    res->width = c2d.width;
    res->height = c2d.height;
    res->format = c2d.format;
    res->resource_id = c2d.resource_id;							/* 4 */

    pformat = get_pixman_format(c2d.format);					/* 5 */
	......
    res->image = pixman_image_create_bits(pformat,
                                          c2d.width,
                                          c2d.height,
                                          NULL, 0);
	......
    QTAILQ_INSERT_HEAD(&g->reslist, res, next);					/* 6 */
}
4. 从virtio队列中取出数据,放到c2d结构体中,从这里看到它的设计很巧妙。因为在公共处理流程中,也是从virtio队列的iov中取数据,这里也是从同样的iov中取数据,但长度不同,取出的信息不同。virtio-gpu的ctrl_vq队列的头部是各个命令字公用的——virtio_gpu_ctrl_hdr,相当于是所有命令字的基类,或者说他们由相同格式的头部,之后的数据,各个结构体有差异。这里virtio_gpu_ctrl_hdr结构体就是virtio_gpu_resource_create_2d的基类。
5. 从后端的VirtIOGPU中查找是否已经存在该资源,如果存在,不允许重复创建,资源ID是索引资源的唯一标识,不允许重复
6. 为资源对象分配内存并根据前端传入的属性初始化
7. 将前端下发的资源ID保存,之后前端操作资源的时候,通过给出resource_id就可以让后端qemu明白操作的是那个图片资源
8. 调用linux上低级的图像处理库pixman,根据前端传输的图片基本属性,创建图片资源并报错到res->image中。之后对图像的处理操作,最终会调用pixman接口,而pixman接口只接受自己创建的图片资源,就是这里的res->image
9. 将创建好的图片资源放到VirtIOGPU设备中维护起来

资源绑定

  • 图片资源在后端被创建之后还没有像素数据,这个需要前端填充,前端填充后把像素数据所在内存的地址传递给后端,使用绑定命令VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING让qemu把图片的资源和图片的内容关联起来,这里的绑定,实际上就是映射,告诉qemu,在像素操作时去前端知会的那个地方去操作,函数如下:
static void
virtio_gpu_resource_attach_backing(VirtIOGPU *g,
                                   struct virtio_gpu_ctrl_command *cmd)
{
    struct virtio_gpu_simple_resource *res;
    struct virtio_gpu_resource_attach_backing ab;
    VIRTIO_GPU_FILL_CMD(ab);														/* 1 */	
    res = virtio_gpu_find_resource(g, ab.resource_id);								/* 2 */
   	virtio_gpu_create_mapping_iov(&ab, cmd, &res->iov);								/* 3 */
        	(*iov)[i].iov_base = dma_memory_map(VIRTIO_DEVICE(g)->dma_as,			/* 4 */
                                            a, &len, DMA_DIRECTION_TO_DEVICE);
}
1. 从队列上取出信息,主要是获取要attach的图片资源ID。
2. 根据资源ID从VirtIOGPU设备上找到图片资源。
3. 图片内容绑定到资源,将像素数据所在内存保存,通过dma映射的方式使用这段内存,当后端处理2D图片时就去这个地方取像素数据。
4. 具体的DMA映射操作,最终前端告知的内存地址会保存到virtio_gpu_simple_resource.iov中。

Host转换图像

  • 图片像素被前端传输后,存放到qemu维护的virtio_gpu_simple_resource的iov字段中,如果要真正的显示出来,还需要调用主机上的图形库接口,因此需要将像素数据转换成图形库接口可以识别的结构体,并拷贝到对应的内存,总结以下,就是将res->iov中的数据转换成res->image数据。这个过程由VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D命令实现,如下:
static void virtio_gpu_transfer_to_host_2d(VirtIOGPU *g,
                                           struct virtio_gpu_ctrl_command *cmd)
{
    struct virtio_gpu_simple_resource *res;
    struct virtio_gpu_transfer_to_host_2d t2d;

    VIRTIO_GPU_FILL_CMD(t2d);									/* 1 */
    res = virtio_gpu_find_resource(g, t2d.resource_id);			/* 2 */
	......
    if (t2d.offset || t2d.r.x || t2d.r.y ||
        t2d.r.width != pixman_image_get_width(res->image)) {
        void *img_data = pixman_image_get_data(res->image);		/* 3 */
        for (h = 0; h < t2d.r.height; h++) {
            src_offset = t2d.offset + stride * h;
            dst_offset = (t2d.r.y + h) * stride + (t2d.r.x * bpp);

            iov_to_buf(res->iov, res->iov_cnt, src_offset,		/* 4 */
                       (uint8_t *)img_data
                       + dst_offset, t2d.r.width * bpp);
        }
    } 
    ......
}
1. 从队列中取出命令字附加的信息,主要是找到资源ID
2. 从VirtIOGPU维护的资源链表中找到资源ID对应的图片资源
3. 获取主机侧图形库中图片资源的像素地址
4. 将res->iov中的数据拷贝到res->image中

输出图像

  • 输出图像就对应的是VIRTIO_GPU_CMD_RESOURCE_FLUSH动作,他将pixman处理维护的像素数据组织好,传输给终端设备,如下:
static void virtio_gpu_resource_flush(VirtIOGPU *g,
                                      struct virtio_gpu_ctrl_command *cmd)
{
    struct virtio_gpu_simple_resource *res;
    struct virtio_gpu_resource_flush rf;

    VIRTIO_GPU_FILL_CMD(rf);												/* 1 */
    res = virtio_gpu_find_resource(g, rf.resource_id);						/* 2 */
	......
    for (i = 0; i < VIRTIO_GPU_MAX_SCANOUT; i++) {
        struct virtio_gpu_scanout *scanout;
        pixman_region16_t region, finalregion;
        pixman_box16_t *extents;											/* 3 */
        scanout = &g->scanout[i];

        pixman_region_init(&finalregion);
        pixman_region_init_rect(®ion, scanout->x, scanout->y,
                                scanout->width, scanout->height);

        pixman_region_intersect(&finalregion, &flush_region, ®ion);
        pixman_region_translate(&finalregion, -scanout->x, -scanout->y);
        extents = pixman_region_extents(&finalregion);
        /* work out the area we need to update for each console */
        dpy_gfx_update(g->scanout[i].con,									/* 4 */
                       extents->x1, extents->y1,
                       extents->x2 - extents->x1,
                       extents->y2 - extents->y1);
		......
    }
}
1. 取命令字附加信息
2. 查找图片资源
3. 将图片资源中关于像素的信息搜集起来,放到extents中
4. 将extents中的信息输出到显示器,这里的显示器驱动可以有不同实现,比如vnc,spice,sdl,gtk。virtio-gpu层根据具体驱动调用对应的实现。

你可能感兴趣的:(GPU,虚拟化,VirtIO)