Graphics Stack總結(二) Mesa漫遊

回顧

前一篇文章中我們對Linux graphics stack有了一個快速介紹,接下來我將解釋爲什麼我們稱之爲graphics driver in Linux實際上是三個不同drivers的組合:

  • the user space X server DDX driver, which handles 2D graphics.
  • the user space 3D OpenGL driver, that can be provided by Mesa.
  • the kernel space DRM driver.

現在我們知道Mesa在哪個位置了,而我們接下來對它會進行詳細瞭解。

DRI drivers and non-DRI drivers

正如解釋的那樣,Mesa 通過提供OpenGL API的實現來handles(處理) 3D graphics。Mesa OpenGL drivers實際上也通常被叫做DRI drievrs。請記住這一點,畢竟DRI架構生來就是被精確的enable OpenGL drivers在Linux中的高效實現的,正如前文介紹的那樣,DRI/DRM在Mesa中是OpenGL drivers的building blocks。

這裏也會有OpenGL API的其他實現可以獲得。Hardware供應商會提供Linux drivers,這些Linux drivers是他們自己對於OpenGL API的實現,通常是以一些binary的形式。舉個例子,如果你有NVDIA的GPU並且安裝了NVDIA的專有driver,那麼在其中也會包含NVDIA自己的libGL.so

注意,在Linux中創建不遵循DRI架構的graphics drivers也是可以的。舉個例子,NVDIA的專有driver安裝了一個kernel module,其實現了DRM的相似功能,但是它的API是與DRM不同的,是由NVDIA自己設計的。所以顯然,相對應的user space的drivers(DDX 和 OpenGL)也會用NVDIA自行設計的API與NVDIA kernel space driver通信,而不是DRM的API。

Mesa, the framework

你可能已經注意到了,當我談論到Mesa的時候我通常說"drivers",複數。這是因爲Mesa本身並不是一個單個driver,而是一個project,這個Project包含了多個drivers(這是因爲Mesa project有好幾種OpenGL API的實現)。

事實上,Mesa作爲framework來說是OpenGL的最佳的實現,它提供了很好的抽象概念/抽象層,使得可以被多種driver共用。顯然,有關OpenGL的多個層面的實現是獨立的,與底層硬件無關的,因此這些部分可以被抽象和複用。

舉個例子,如果你對OpenGL比較熟悉的話,你會知道OpenGL是基於API提供state的(狀態機是OpenGL中的重要概念),這意思着很多API調用不是立即生效的,他們只會在driver中修改某些變量的值,而不會立即把這些值push到硬件中。事實上,通常當我們通過調用glDrawArrays() or a similar API來真正渲染(render)一些東西的時候纔會生效:在那個時刻driver將會根據之前API調用設置的state(狀態)來配置3D pipeline,然後來執行渲染。由於這些APIs不會與硬件交互,所以這些APIs的實現可以被多種drivers來共用,接着,每個driver中glDrawArrays()的implementation可以獲得狀態機中存儲的值,然後立即將這些值轉換爲硬件中的執行代碼。

這樣來說,Mesa爲很多東西提供了抽象,甚至完成了很多個不需要與硬件交互的OpenGL APIs的實現,至少是不必立即產生交互的。

Mesa也定義了多個hooks,這些hooks是drivers中可能需要做硬件相關的工作,比如the implementation of glDrawArrays().

 

Looking into glDrawArrays()

讓我們舉一個關於這些hooks的例子,通過檢測Mesa中glDrawArrays() 產生的stacktrace來進入硬件。在這個例子中,我會在程序中用Mesa Intel DRI driver並且會在function render()調用glDrawArrays() ,以下是stacktrace中相關的部分:

brw_upload_state () at brw_state_upload.c:651
brw_try_draw_prims () at brw_draw.c:483
brw_draw_prims () at brw_draw.c:578
vbo_draw_arrays () at vbo/vbo_exec_array.c:667
vbo_exec_DrawArrays () at vbo/vbo_exec_array.c:819
render () at main.cpp:363

注意我們前面提到的glDrawArrays() 在代碼中實際上是vbo_exec_DrawArrays().。關於這個stack中有意思的部分是vbo_exec_DrawArrays()vbo_draw_arrays()是不依賴硬件(hardware independent)的,從而被mesa中的很多drivers複用。如果你不像我一樣用的是Intel的GPU,但同樣用的是Mesa,你的backtrace也應該是相似的。這些通用的functions通常會做檢查API use error,reformatting inputs等事情,從而使後續的處理或者爲當前狀態機(current state)獲取額外的信息更方便,這些都是在hardware中實現真正的操作所需要的。

Notice that glDrawArrays() is actually vbo_exec_DrawArrays(). What is interesting about this stack is that vbo_exec_DrawArrays() and vbo_draw_arrays() are hardware independent and reused by many drivers inside Mesa. If you don’t have an Intel GPU like me, but also use a Mesa, your backtrace should be similar. These generic functions would usually do things like checks for API use errors, reformatting inputs in a way that is more appropriate for later processing or fetching additional information from the current state that will be needed to implement the actual operation in the hardware.

在某些時刻,然而我們需要去做實際的渲染,這涉及到根據我們發出的命令和在先前 API 調用中設置的相關狀態,來配置hardware pipeline。在上面的stracktrace中,這從函數 brw_draw_prims()開始。這個函數調用是Intel DRI driver的一部分,是一個Hook,在這個Hook中Intel driver會做一些準備工作,用來執行配置Intel GPU進行繪製。正如你所見,後面會調用類似 brw_upload_state()的函數,其會upload一堆的狀態給hardware去做完成此任務,比如根據當前program的需求配置各種shader的stage。

Registering driver hooks

在後續文章中我們將會用更多細節討論driver是如何配置pipeline的,但此刻我們僅會探討Intel driver是如何爲glDrawArrays() 的調用註冊它的hook的。如果我們觀察stackstrace,知道brw_draw_prims() 是Intel driver中的hook,我們可以看到它是如何在vbo_draw_arrays()中被調用的:

static void
vbo_draw_arrays(struct gl_context *ctx, GLenum mode, GLint start,
                GLsizei count, GLuint numInstances, GLuint baseInstance)
{
   struct vbo_context *vbo = vbo_context(ctx);
   (...)
   vbo->draw_prims(ctx, prim, 1, NULL, GL_TRUE, start, start + count - 1,
                   NULL, NULL);
   (...)
}

所以hook是vbo_context中的draw_prims() 。在源代碼中做一些深入搜索,我們可以看到hook是在brw_draw_init()中被setup的,如下所示:

void brw_draw_init( struct brw_context *brw )
{
   struct vbo_context *vbo = vbo_context(ctx);
   (...)
   /* Register our drawing function:
    */
   vbo->draw_prims = brw_draw_prims;
   (...)
}

讓我們設置一個斷點,看Mesa是什麼時候調用到那裏的:

brw_draw_init () at brw_draw.c:583
brwCreateContext () at brw_context.c:767
driCreateContextAttribs () at dri_util.c:435
dri2_create_context_attribs () at dri2_glx.c:318
glXCreateContextAttribsARB () at create_context.c:78
setupOpenGLContext () at main.cpp:411
init () at main.cpp:419
main () at main.cpp:477

所以當我們setup OpenGL context時,Mesa毫無意外的調用到Intel DRI driver中,此時driver會註冊各種各樣的hooks,包含繪製圖形的hook.

我們可以做相似的事情,來觀察driver時如何註冊context creation的hook的。我們可以看到Intel driver(Mesa中其他vendor的driver也一樣)給需要的hook assign了全局變量,如下所示

static const struct __DriverAPIRec brw_driver_api = {
   .InitScreen           = intelInitScreen2,
   .DestroyScreen        = intelDestroyScreen,
   .CreateContext        = brwCreateContext,
   .DestroyContext       = intelDestroyContext,
   .CreateBuffer         = intelCreateBuffer,
   .DestroyBuffer        = intelDestroyBuffer,
   .MakeCurrent          = intelMakeCurrent,
   .UnbindContext        = intelUnbindContext,
   .AllocateBuffer       = intelAllocateBuffer,
   .ReleaseBuffer        = intelReleaseBuffer
};
 
PUBLIC const __DRIextension **__driDriverGetExtensions_i965(void)
{
   globalDriverAPI = &brw_driver_api;
 
   return brw_driver_extensions;
}

在Mesa裏的DRI的實現中,這些全局變量始終被使用,根據需求來調用到不同的hardware driver。

我們可以看到有兩種不同類型的hook,一種是把driver給link到DRI的實現(implementation)中所需的Hook(這是Mesa中driver的主入口),另一種是爲OpenGL的硬件實現的相關任務添加的Hook,通常由driver在上下文創建(context creation)時註冊。

要寫一個新的DRI driver, 開發者只寫這些Hook的實現就可以了,其他剩餘的部分已經被Mesa實現完成,並且可以在不同的driver中複用。

Gallium3D, a framework inside a framework

如今,我們可以把Mesa DRI 驅動分成兩類:傳統(classic)drivers(非基於Gallium3D framework)和Gallium drivers。

Gallium3D 是Mesa的一部分,試圖讓3D driver的開發比之前更加簡單和實用。舉個例子,classic Mesa drivers是與OpenGL緊密耦合的,這意味着要實現對其他APIs(比如Direct3D)的支持,工作量跟寫一個新的完整driver/implementation差不多。Gallium 3D framework通過提供一個API解決了這個問題,這個API暴露了現代GPUs的hardware function,而不是專注於OpenGL這樣的某個特定API。

Gallium的其他好處包括,通過分離driver中依賴底層操作系統中的某些特定方面的部分,來實現對不同操作系統的支持。

在過去的這些年裏,我們已經看到很多drivers轉到了Gallium基礎設施架構中,包括 (the open source driver for NVIDIA GPUs),和不同種類的radeon drivers,以及一些software drivers (swrast, llvmpipe) .

Gallium3D driver model (image via wikipedia)

儘管在過去有人試圖把Intel driver port到Gallium中並且付出一些努力,但是據我所知,Intel Gallium drivers(i915g和i965g)的開發已經停滯不前。Intel正專注於driver的傳統版本。這可能是因爲在保證同樣的feature和穩定性下,把現在的classic diver port到Gallium需要大量的時間和effort,這是由於當前的classic driver包含了太多代的Intel GPU。而且現在爲了增加對新的OpenGL feature的支持,同樣需要大量的工作要做,這有着更高的優先級。

Gallium and LLVM

正如我們在未來的文章中將會看到的更多細節,寫一個現代GPU driver會包含許多native code generation和優化。並且,OpenGL包含OpenGL Shading Language (GLSL),其直接要求在driver中提供GLSL編譯器。

毫無疑問,對Mesa開發者而言,明智的想法是重用現有的編譯器基礎設施,而不是構建和用他們自己的: enter LLVM.

It is no wonder then that Mesa developers thought that it would make sense to reuse existing compiler infrastructure rather than building and using their own: enter LLVM.

通過將LLVM引入mix,Mesa開發者希望爲着色器帶來新的更好的優化,並生成更好的本地代碼(native code),這對性能至關重要。

這也可以消除Mesa和/或驅動程序中的大量代碼。事實上,Mesa有自己完整的GLSL編譯器實現,包括GLSL解析器、編譯器和鏈接器,以及許多優化,包括Mesa中代碼的抽象表示,以及實際硬件驅動程序中特定(specific)GPU的實際本機代碼(actual native code)。

Gallium插入LLVM的方式很簡單:Mesa解析GLSL並生成着色器代碼的LLVM中間表示,然後將其傳遞給LLVM,LLVM將負責優化。在此場景中,硬件驅動程序的作用僅限於提供描述各自GPU(指令集、寄存器、約束等)的LLVM後端,以便LLVM知道如何爲目標GPU工作。

Hardware and Software drivers

即使在今天,我也看到一些人相信Mesa只是OpenGL的一個軟件實現(software implementation)。如果你已經閱讀了這篇文章,應該很清楚這不是真的:Mesa提供了OpenGL的多個實現(驅動程序),其中大多數都是硬件加速驅動程序,但Mesa也提供了軟件驅動程序。

Software drivers 有時非常有用:

  • 出於開發和測試的目的,特別是當你想排除掉硬件因素時。從這個角度來看,software representation可以爲不受任何特定硬件約束或約束的預期行爲提供參考。例如,如果您有一個OpenGL程序不能正常運行,我們可以使用軟件驅動程序運行它:如果它運行良好,那麼我們就知道問題在硬件驅動程序中,否則我們可以懷疑問題在應用程序本身。
  • 允許在缺少3D硬件驅動程序的系統中執行OpenGL。這顯然會很慢,但在某些情況下,這可能就足夠了,而且肯定比沒有任何3D支持要好。

我最初打算在這篇文章中涵蓋更多內容,但它已經足夠長了,所以我們現在就到此爲止。在下一篇文章中,我們將討論如何check and change Mesa使用的driver,例如在software和hardware driver之間切換,然後我們將開始研究Mesa的源代碼並介紹其主要模塊。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章