通常通過讀寫設備寄存器對設備進行編程,在X86系統上,有專門的IO指令進行編程,在其他諸如MIPS、SPARC這類系統上,通過將設備的寄存器映射到內存地址空間直接使用讀寫內存的方式對設備進行編程。
Radeon顯卡提供兩種方式對硬件進行編程,一種稱為“推模式”(push mode)即直接寫寄存器的方式,另一種稱為拉模式,這篇blog討論拉模式,這也是驅動中使用的模式。
在拉模式下,驅動使用命令流(Command Stream)的形式進行對顯卡編程:驅動程序將需要對顯卡進行配置的一連串命令寫入命令緩沖區,寫完之后進入讓出處理器,顯卡按照命令寫入的順序執行這些命令,執行完成后觸發中斷通知驅動。CPU將這些命令放入一個稱為命令環的環形緩沖區中,命令環是GTT內存中分出來的一片內存,驅動程序往命令環中填充命令,填充完后通知GPU命令已經寫入命令,GPU的命令處理器CP(Command Processor)。上一篇博客即是通過ring環內存的使用來說明如何在系統中分配內存以及建立映射關系的。
驅動寫入的命令流由命令處理器CP進行解析,具體來說,CP完成以下工作:
- 接收驅動程序的命令流。驅動程序將命令流先寫入系統內存,然后由CP通過總線主設備訪問方式進行獲取,當前支持三種命令流,除了前面說的環形緩沖命令流,還有間接緩沖1命令流和間接緩沖2命令流;
- 解析命令流,將解析后的數據傳輸給圖形控制器的其他模塊,包括3D圖形處理器、2D圖形處理器、視頻處理器。
命令環緩沖區
在拉模式下,驅動程序在系統內存中為命令流申請一塊緩沖區。GPU會根據這些命令流去執行屏幕繪圖等操作。這種命令緩沖區按照環形方式進行管理,是CPU和GPU 共享的一片系統主存,CPU負責寫入命令包,GPU負責讀取和解析命令包。因為CPU和GPU 看到的環形緩沖區狀態必須是一致性,所以CPU和GPU都要共同維護和管理環形緩沖區的狀態:基地址、長度、寫指針和讀指針。為了使Ring Buffer能夠正常工作,CPU和GPU 必須維護這種狀態的一致性。Ring Buffer基地址和大小是在系統第一次啟動時已經初始化好的,之后一般也不會改變。當操作Ring Buffer時, 讀指針和寫指針的修改非常頻繁。為了維護環形緩沖區的狀態一致性,當寫操作者(CPU)更新寫指針時,它必須將寫指針告訴GPU。同樣的,當讀操作者(GPU)更新讀指針時,它必須將讀指針告知CPU。無論是CPU還是GPU都是從低地址開始進行填寫或抽取操作的,一旦到了環形緩沖區的結束處,又從環形緩沖區起始處繼續。
圖1
整個過程如圖1示,左邊的Host(CPU)和右邊的GPU各自記錄了命令環的起始地址,並各自保存了一份讀寫指針,CPU寫之前首先查詢讀指針,確認有空閑空間之后寫入內容並更新寫指針,GPU讀取了命令之后更新讀指針。
間接緩沖
在系統主存中,除了環形緩沖區之外,CP還可以從間接緩沖1和間接緩沖2中獲取命令包。這個過程是這樣完成的:在主命令流中(ring buffer)有一個設置CP的間接緩沖1地址和大小的寄存器。寫間接緩沖1的寄存器觸發CP從提供的地址處取間接緩沖區1的命令流。主命令的最后一個命令包設置間接緩沖1地址和大小;然后CP開始從間接緩沖1中取數據。間接緩沖1的數命令流可能使用間接緩沖區2。和之前的過程一樣,寫間接緩沖1的寄存器觸發CP從間接緩沖區2中獲取新的命令流。間接緩沖1流中的最后一個包設置間接緩沖2的地址和大小。CP從間接緩沖2取命令直到全部去完;執行完間接緩沖區2的命令后返回到間接緩沖1的命令流。CP從間接緩沖1中取剩余的命令一直到間接緩沖1的末尾,返回到主命令流中。
這個過程有點類似函數調用。程序在運行過程中遇到函數調用,則會使用跳轉指令跳到被調用函數入口,執行完函數后跳回到原來的程序位置繼續執行。這的最大調用“深度”為2。
在Linux內核radeon驅動中有一個ring test過程用於驗證ring buffer是否工作正常,如果ring test通過,那么GPU和CPU交互的部分已經配置正確,可以正常工作了。
Ring buffer機制幾乎在所有類型的芯片上都是一樣的,區別只是r600以后的芯片ring buffer GPU端讀寫指針的寄存器地址發生了變化。Linux內核驅動針對不同GPU核實現ring buffer機制以及ring test過程的代碼幾乎是完全相同的。
從內核中拿出ring test過程的代碼:
2287 int r600_ring_test(struct radeon_device *rdev)
2288 {
2289 uint32_t scratch;
2290 uint32_t tmp = 0;
2291 unsigned i;
2292 int r;
2293
2294 r = radeon_scratch_get(rdev, &scratch);
2295 if (r) {
2296 DRM_ERROR("radeon: cp failed to get scratch reg (%d).\n", r);
2297 return r;
2298 }
2299 WREG32(scratch, 0xCAFEDEAD);
2300 r = radeon_ring_lock(rdev, 3);
2301 if (r) {
2302 DRM_ERROR("radeon: cp failed to lock ring (%d).\n", r);
2303 radeon_scratch_free(rdev, scratch);
2304 return r;
2305 }
2306 radeon_ring_write(rdev, PACKET3(PACKET3_SET_CONFIG_REG, 1));
2307 radeon_ring_write(rdev, ((scratch - PACKET3_SET_CONFIG_REG_OFFSET) >> 2));
2308 radeon_ring_write(rdev, 0xDEADBEEF);
2309 radeon_ring_unlock_commit(rdev);
2310 for (i = 0; i < rdev->usec_timeout; i++) {
2311 tmp = RREG32(scratch);
2312 if (tmp == 0xDEADBEEF)
2313 break;
2314 DRM_UDELAY(1);
2315 }
2316 if (i < rdev->usec_timeout) {
2317 DRM_INFO("ring test succeeded in %d usecs\n", i);
2318 } else {
2319 DRM_ERROR("radeon: ring test failed (scratch(0x%04X)=0x%08X)\n",
2320 scratch, tmp);
2321 r = -EINVAL;
2322 }
2323 radeon_scratch_free(rdev, scratch);
2324 return r;
2325 }
2294行獲取一個可用的scratch寄存器,scratch寄存器是功能未定義的寄存器,由(驅動)軟件定義其功能。
2299行使用mmio的方式直接向寄存器中寫入值“0xCAFEDEAD”,此時該scratch寄存器的內容為0xCAFEDEAD。
2300行向內核驅動中的ring buffer機制申請3個dword(gpu命令都是以4字節為單位計的),同時由於會有多個程序並發訪問ring buffer,這里還會對ring buffer加鎖。
2306-2308行代碼向剛才申請到的ring buffer內存中寫入3個dword的命令,關於GPU命令在下一章會詳細介紹,這里的命令的意思是向剛才的scratch寄存器中寫入值“0xDEADBEEF”。
2309行提交命令,上面三行代碼寫的命令寫入ring buffer后並不會被執行,直到調用radeon_ring_unlock_commit之后命令才會被執行。
2310-2314行是一個通過輪詢的方式檢查scratch寄存器的過程,如果上面的命令正常運行,那么scratch寄存器的值將會是“0xDEADBEEF”,否則命令沒有正常運行,ring test 失敗。
從上面的示例代碼中可以看到,在radeon內核驅動使用了下面三個函數就可以操作ring buffer了:
API接口函數 | 功能 | 參數 |
radeon_ring_lock | 申請ring buffer內存並鎖住ring buffer,如果ring buffer被用完,則更新CPU端的讀指針 | N為申請的dwords數目 |
radeon_ring_write | 向ring buffer寫入命令和命令參數,這里只更新CPU端的寫指針 | |
radeon_ring_commit | 更新GPU端的寫指針,釋放ring buffer鎖 |
需要提及的是scratch寄存器,scratch寄存器是GPU預留給軟件使用的寄存器,r300以前的顯卡只5個scratch寄存器,以后的顯卡有7個寄存器,GPU本身並不依賴這些寄存器對其進行配置,軟件可以自定義其功能。上面這段代碼僅僅用於驗證命令是否正確執行,然而后面的輪詢過程卻對我們有所啟發:軟件發送了命令之后什么時候直到命令被執行完成了?可以按照這里面的做法,在命令尾部再添加一條寫scratch寄存器的命令(當然必須保證往scratch寄存器寫入的值和scratch寄存器原來的值不一樣),而后輪詢該scratch寄存器,如果這個寄存器被寫入了我們要求其寫入的值,那么就可以確定命令已經執行完了。這里實際上定義了一個軟硬件同步的機制,后面中斷機制的章節會討論驅動中fence機制的實現,fence機制是使用中斷實現的,但是那里面使用了我們上面提到的思想。
經過上面描述之后,閱讀ring buffer的實現代碼應該不難讀懂了。
Linux內核中完成ring test后,會有一個indirect buffer test過程。這個過程和ring test過程完成的操作一樣,寫scratch寄存器。
2660 int r600_ib_test(struct radeon_device *rdev)
2661 {
2662 struct radeon_ib *ib;
2663 uint32_t scratch;
2664 uint32_t tmp = 0;
2665 unsigned i;
2666 int r;
2667
2668 r = radeon_scratch_get(rdev, &scratch);
......
2673 WREG32(scratch, 0xCAFEDEAD);
2674 r = radeon_ib_get(rdev, &ib);
......
2679 ib->ptr[0] = PACKET3(PACKET3_SET_CONFIG_REG, 1);
2680 ib->ptr[1] = ((scratch - PACKET3_SET_CONFIG_REG_OFFSET) >> 2);
2681 ib->ptr[2] = 0xDEADBEEF;
2682 ib->ptr[3] = PACKET2(0);
2683 ib->ptr[4] = PACKET2(0);
2684 ib->ptr[5] = PACKET2(0);
2685 ib->ptr[6] = PACKET2(0);
2686 ib->ptr[7] = PACKET2(0);
2687 ib->ptr[8] = PACKET2(0);
2688 ib->ptr[9] = PACKET2(0);
2689 ib->ptr[10] = PACKET2(0);
2690 ib->ptr[11] = PACKET2(0);
2691 ib->ptr[12] = PACKET2(0);
2692 ib->ptr[13] = PACKET2(0);
2693 ib->ptr[14] = PACKET2(0);
2694 ib->ptr[15] = PACKET2(0);
2695 ib->length_dw = 16;
2696 r = radeon_ib_schedule(rdev, ib);
......
2703 r = radeon_fence_wait(ib->fence, false);
......
2708 for (i = 0; i < rdev->usec_timeout; i++) {
2709 tmp = RREG32(scratch);
2710 if (tmp == 0xDEADBEEF)
2711 break;
2712 DRM_UDELAY(1);
2713 }
.....
2721 radeon_scratch_free(rdev, scratch);
2722 radeon_ib_free(rdev, &ib);
2723 return r;
2724 }
2668-2673行的內容和ring test的過程一樣。
2674行從系統中獲取一個indirect buffer,ib->ptr中記錄了indirect buffer在內存中的位置。
2679-2694向indirect buffer中填充命令和參數,這里填寫的命令和參數與ring test 中填寫的命令和參數是相同的,當然這里也有對齊要求。
2696 行將填寫好的indirect buffer添加到調度隊列中。
2703行涉及fence機制,在中斷機制一節中我們將詳細介紹。
同樣讀懂indirect buffer機制的代碼也不會有太大困難。
Indirect buffer要能夠正常運行,必須將其插入到ring buffer的代碼中去,這就類似在匯編代碼中插入"call xx"指令進行函數調用一樣。radeon_ring_ib_execute函數添加的命令就相當於函數調用時使用的call指令。
下一篇將描述這些命令的格式,並給出一些例子。
參考資料:
這部分的描述的內容基本上來自“Radeon R5xx Acceleration”文檔。
“Graphic Engine Resource Management”對命令的調度有一些改進,可以作為進一步學習的參考。