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

前言

  • 爲什麼要引入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_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上維護的所有資源組織如下:
    在這裏插入圖片描述

流程分析

後端通用流程

  • 當前端要處理圖片時,首先創建一張空白的圖片,指定這個圖片的大小和格式。然後是填充像素數據,填充像素有兩種方式,一是創建默認的像素數據,二是拷貝已有的像素數據。像素數據填充後,就是像素的處理,對於簡單的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(&region, scanout->x, scanout->y,
                                scanout->width, scanout->height);

        pixman_region_intersect(&finalregion, &flush_region, &region);
        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層根據具體驅動調用對應的實現。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章