前言
數據結構
命令傳遞
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;
};
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傳回給前
端,這裏就是往前端回送信息
資源初始化
- 屬性,長,寬,格式。
- 像素數據,圖片信息由像素存儲,對於黑白圖片,一個像素由灰度值表示,範圍是0-255,0爲黑色,255爲白色,對於彩色圖片,一個像素由三個原色按比例組成(RGB——紅綠藍)組成。
- 像素操作相關上下文,在虛擬化場景下,虛機對於圖片的像素操作有兩種選擇,一是使用虛機內部的像素操作庫函數(比如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層根據具體驅動調用對應的實現。