顯卡使用的內存分為兩部分,一部分是顯卡自帶的顯存稱為VRAM內存,另外一部分是系統主存稱為GTT內存(graphics translation table和后面的GART含義相同,都是指顯卡的頁表,GTT 內存可以就理解為需要建立GPU頁表的顯存)。在嵌入式系統或者集成顯卡上,顯卡通常是不自帶顯存的,而是完全使用系統內存。通常顯卡上的顯存訪存速度數倍於系統內存,因而許多數據如果是放在顯卡自帶顯存上,其速度將明顯高於使用系統內存的情況(比如紋理,OpenGL中分普通紋理和常駐紋理)。
某些內容是必須放在vram中的,比如最終用於顯示的“幀緩存”,以及后面說的頁表GART (graphics addres remapping table),另外有一些比如后面將介紹的命令環緩沖區(ring buffer)是要放在GTT 內存中的。另一方面,VRAM內存是有限的,如果VRAM內存使用完了,則必須將一些數據放入GTT內存中。
通常GTT內存是按需分配的,而且是給設備使用的,比如radeon r600顯卡最多可以使用512M系統內存(Linux內核中是這樣設置的),一次性分配512M連續的給設備用的內存在linux系統中是不可能成功的,而且即使可以成功,有相當多的內存是會被浪費掉的。按照按需分配的原則,使用多少就從系統內存中分配多少,這樣得到的GTT內存在內存中肯定是不連續的。GPU同時需要使用VRAM內存和GTT內存,最簡單的方法就是將這兩片內存統一編址(這類似RISC機器上IO和MEM統一編址),VRAM是顯卡自帶的內存,其地址一定是連續的,但是不連續的GTT內存如果要統一編址,就必須通過頁表建立映射關系了,這個頁表被稱為GTT或者GART,這也是這些內存被稱為GTT內存的原因。
和CPU端地址類似,我們將GPU使用的地址稱為“GPU虛擬地址”,經過查頁表之后的地址稱為“GPU物理地址”,這些地址是GPU最終用於訪存的地址,由於GPU掛接在設備總線上,因此這里的“GPU物理地址”就是“總線地址”,當然落在vram 區域的內存是不用建頁表的,這一片內存區域的地址我們只關心其“GPU 虛擬地址”。
R600顯卡核心存管理有關的寄存器如表1示,目前並沒有找到完整的描述這些寄存器的手冊,表中的數據根據閱讀代碼獲取到。
| 寄存器名稱 | 偏移地址 | 功能 |
| R600_CONFIG_MEMSIZE | 0x5428 | VRAM大小 |
| MC_VM_FB_LOCATION | 0x2180 | VRAM區域在GPU虛擬地址空間的起始地址和長度 |
| MC_VM_SYSTEM_APERTURE_LOW_ADDR | 0x2190 | VRAM區域在GPU虛擬地址空間的起始地址 |
| MC_VM_SYSTEM_APERTURE_HIGH_ADDR | 0x2194 | VRAM區域在GPU虛擬地址空間的結束地址 |
| VM_L2_CNTL | GPU L2 Cache控制寄存器 | |
| MC_VM_L1_TLB_MCB | GPU TLB控制寄存器 | |
| VM_CONTEXT0_PAGE_TABLE_START_ADDR |
0x1594 | GTT內存的起始地址 |
| VM_CONTEXT0_PAGE_TABLE_END_ADDR | 0x15B4 | GTT內存的結束地址 |
| VM_CONTEXT0_PAGE_TABLE_BASE_ADDR | 0x1547 | GPU頁表基地址 |
| VM_CONTEXT0_CNTL | 0x1440 | GPU虛擬地址空間使能寄存器 |
| VM_CONTEXT0_PROTECTION_FAULT_DEFAULT_ADDR | 0x1554 | 頁故障處理程序地址 |
| RADEON_PCIE_TX_DISCARD_RD_ADDR_LO/HI | ||
| RADEON_PCIE_TX_GART_ERROR |
表1
在Radeon顯卡中,VRAM內存涉及到“visiable vram”和“real vram”兩個說法,visiable vram是可以使用pci設備內存映射方式映射出來的內存,這部分內存可供軟件訪問,而顯卡的vram還有一部分是不可見的,不能被軟件直接訪問(是GPU自身使用的?),這部分內存加上visiable ram共同構成顯卡的real vram。
通過讀取pci配置空間可以獲取到visiable vram,比如在一個機器上讀出visiable ram大小為256M,讀取RADEON_CONFIG_MEMSIZE獲取real vram大小為512M,於是vram長度為512M,將vram起始地址設置為0x0,那么結束地址為0x1fffffff,然后將起始地址和結束地址寫入R_000004_MC_FB_LOCATION寄存器:
rv515_mc_wreg(R_000004_MC_FB_LOCATION, S_000004_MC_FB_START(rdev->vram_start >> 16) |S_000004_MC_FB_TOP(rdev->vram_end >> 16));
然后是設置GTT內存和GART。GTT的大小是由驅動自己確定的,GTT大小確定后,GART占用的內存也就確定了。參考內核源碼和上面表給給出的說明應該很容易明白這個過程。
相比於CPU使用的3級頁表,radeon GPU使用的頁表比較簡單,radeon GPU使用的是1級頁表(是否可配置),頁表大小為4K,那么頁表項的后面12位(212=4k)為標志位。在早期的radeon GPU中,GPU使用的頁表頁表項是32位的,到r600 之后GPU 頁表項為64位,頁表項的12位標志位中只有后6位有用,定義如圖1。

圖1
GPU頁表在GPU VRAM內存中,VM_CONTEXT0_PAGE_TABLE_BASE_ADDR和VM_CONTEXT0_PAGE_TABLE_END_ADDR兩個寄存器表明了頁表在vram中的位置。
xxxx linux內核中的代碼【待修改】
上述函數有兩個參數,dma\_addr是分配的系統內存經過映射后的總線地址,這個地址用於設備訪問主存,也是我們上文說的“GPU物理地址”,后面一個參數index是頁表項索引。
代碼中ptr是頁表所在的內存在CPU虛擬地址空間中的地址,r600的頁表項為64位,r500及以下的頁表為32位。
下面來看一片內存的分配和映射情況。在下一篇博文中將使用一個稱為ring buffer分配一片內存,這片內存用於放置命令,cpu將命令放置到這一片內存中,gpu從這一片內存中拿命令對GPU進行配置。
xxx ring_init過程描述【待修改】
在GPU初始化完成后,R600顯卡GPU按照圖2(代碼中看到的是這個樣子,是否有錯誤?)顯示的過程進行內存訪問。

圖2
如果是GTT內存,則需要查GPU頁表,根據64位地址(在當前的驅動中實際上只用了32位)的前面50 位定位GPU 頁表項,根據頁表項內容的后12位與上0即是內存在PCI設備空間中的“頁基址”,“頁基址”加上原來64 位地址的后12位(頁內偏移)就得到對應的總線地址。
注意到由於vram和GTT統一編址,而vram並不參與這里的頁表地址轉換過程,因而需要有減去GTT內存基址的過程。
在linux內核中是有一套完善的內存管理機制的,這套機制是TTM和GEM(相關參考資料)。和操作系統里面的系統內存管理一樣,這套機制比較復雜,我們這里不詳細描述這套機制的具體實現,而是簡單描述如何在核內核外獲取和使用顯存。
內核使用顯存
在radeon內核驅動代碼radeon_device_init(drivers/gpu/drm/radeon/radeon_device.c)函數中有如下代碼:
810 if (radeon_testing) {
811 radeon_test_moves(rdev);
812 }
810行是一個全局變量開關,當這個開關開啟的時候,驅動會做一個拷屏操作,這段代碼在drivers/gpu/drm/radeon/radeon_test.c文件中,radeon_test_moves做些數據拷貝操作,包括從vram到系統主存和系統主存到vram之間的數據拷貝,在系統啟動的時候就能在屏幕上看到效果(這個是能夠直接在radeon內核驅動代碼中運行並且能夠看到效果的命令處理過程)。在這個地方,內核已經完成了初始化工作,后續對顯卡的部分編程可以放在這個地方,重新系統后就能看到效果。下面是一段使用內核API進行顯存分配和操作的示例代碼:
1 struct radeon_bo *vram_obj = NULL;
2 struct radeon_bo *gtt_obj = NULL;
3 uint64_t vram_addr, gtt_addr;
4 unsigned size;
5 void *vram_map, *gtt_map;
6
7 size = 1024 * 768 * 4;
8 r = radeon_bo_create(rdev, size, PAGE_SIZE, true,
9 RADEON_GEM_DOMAIN_VRAM, &vram_obj);
10 if (r) {
11 DRM_ERROR("Failed to create VRAM object\n");
12 goto out_cleanup;
13 }
14 r = radeon_bo_reserve(vram_obj, false);
15 if (unlikely(r != 0))
16 goto out_cleanup;
17 r = radeon_bo_pin(vram_obj, RADEON_GEM_DOMAIN_VRAM, &vram_addr);
18 if (r) {
19
20 DRM_ERROR("Failed to pin VRAM object\n");
21 goto out_cleanup;
22 }
23 r = radeon_bo_kmap(vram_obj, &vram_map);
24 if (r) {
25 DRM_ERROR("Failed to map VRAM object\n");
26 goto out_cleanup;
27 }
28
29 r = radeon_bo_create(rdev, size, PAGE_SIZE, true,
30 RADEON_GEM_DOMAIN_GTT, >t_obj);
31 if (r) {
32 DRM_ERROR("Failed to create GTT object\n");
33 goto out_cleanup;
34 }
35 r = radeon_bo_reserve(gtt_obj, false);
36 if (unlikely(r != 0))
37 goto out_cleanup;
38 r = radeon_bo_pin(gtt_obj, RADEON_GEM_DOMAIN_GTT, >t_addr);
39 if (r) {
40 DRM_ERROR("Failed to pin GTT object\n");
41 goto out_cleanup;
42 }
43 r = radeon_bo_kmap(gtt_obj, >t_map);
44 if (r) {
45 DRM_ERROR("Failed to map GTT object\n");
46 goto out_cleanup;
47 }
48
49 out_cleanup:
50 if (vram_obj) {
51 if (radeon_bo_is_reserved(vram_obj)) {
52 radeon_bo_unpin(vram_obj);
53 radeon_bo_unreserve(vram_obj);
54 }
55 radeon_bo_unref(&vram_obj);
56 }
57 if(gtt_obj){
58 if(radeon_bo_is_reserved(gtt_obj)){
59 radeon_bo_unpin(gtt_obj);
60 radeon_bo_unreserve(gtt_obj);
61 }
62 radeon_bo_unref(>t_obj);
63 }
以上代碼顯示了創建兩個buffer object(bo)、分別從vram和gtt內存中分配內存空間並最終釋放內存空間和bo的過程。Buffer object是顯卡對顯存管理的基本結構,是對一片內存的抽象,radeon顯卡驅動中使用的是radeon_bo結構來管理和描述一片顯存。
1-2行,這里我們有兩個bo對象(分配兩片顯存),一片內存來自vram,另外一片來自gtt內存。
8行,創建並初始化一個bo,分配顯存。參數如下:
- rdev,radeon_device結構體指針;
- size,該bo的大小;
- True,來自內核還是用戶空間的請求,如果是內核,則分配bo結構過程是不可中斷的,並且從用戶空間和內核空間訪問這篇顯存的時候虛擬地址和物理地址間的映射關系是不同的;
- RADEON_GEM_DOMAIN_VRAM,顯存位於vram還是gtt內存,radeon驅動中定義了3中類型的顯存RADEON_GEM_DOMAIN_CPU(0x1)、RADEON_GEM_DOMAIN_GTT(0x2)、define RADEON_GEM_DOMAIN_VRAM(0x4),RADEON_GEM_DOMAIN_CPU暫不清楚是何用途,后面兩個表示內存分別來自gtt 內存和vram;
- vram_obj,bo指針,返回的bo結構體。
14行,reserve(保留)bo,(表明當前bo已經被使用,不允許其他代碼使用??)。如果bo已經被reserve,那么這里的要等到bo被unreserve之后才能使用。
17行,獲取bo代表的顯存的GPU虛擬地址,GPU將使用這個地址訪問內存,后面我們讓GPU訪存的時候用的都是這類型的地址。
23行,映射bo代表的顯存空間,該函數的第二個參數返回映射后的CPU虛擬地址,驅動將使用這個訪問這片內存。
29-47行代碼和上面說的原理相同,不同的是這片內存來自GTT內存,在API函數內部處理的時候區別會比較大,但是使用API時只有只有顯存類型這個參數不同。
50-56行釋放內存和bo結構。
核外使用顯存
用戶空間通過libdrm獲取顯存。下面這段代碼顯示了核外如何獲取和使用顯存:
1 int ret;
2 struct kms_bo *bo;
3 unsigned bo_attribs[] = {
4 KMS_WIDTH, 0,
5 KMS_HEIGHT, 0,
6 KMS_BO_TYPE, KMS_BO_TYPE_SCANOUT_X8R8G8B8,
7 KMS_TERMINATE_PROP_LIST
8 };
9 bo_attribs[1] = width;
10 bo_attribs[3] = height;
11 ret = kms_bo_create(kms, bo_attribs, &bo);
12 if (ret) {
13 fprintf(stderr, "failed to alloc buffer: %s\n", strerror(-ret));
14 return NULL;
15 }
16 ret = kms_bo_get_prop(bo, KMS_PITCH, stride);
17 if (ret) {
18 fprintf(stderr, "failed to retreive buffer stride: %s\n", strerror(-ret));
19 kms_bo_destroy(&bo);
20 return NULL;
21 }
22 ret = kms_bo_map(bo, &virtual);
23 if (ret) {
24 fprintf(stderr, "failed to map buffer: %s\n", strerror(-ret));
25 kms_bo_destroy(&bo);
26 return NULL;
27 }
28 return bo;
這段代碼和內核中的代碼很相似,讀者根據調用的函數的函數名就應該能夠理解其含義了。要編寫完整的程序,可以參考libdrm源碼附帶的示例或者這里的代碼。
