OpenGL ES 2.0 知識串講(2)――EGL詳解

出處:電子設備中的畫家|王爍 於 2017 年 7 月 9 日發表,原文鏈接(http://geekfaner.com/shineengine/blog3_OpenGLESv2_2.html)

 

上節回顧

上一節我們初步學習了 OpenGL ES、EGL、GLSL 的相關概念,瞭解了它們的功能,以及它們之間的關聯。我們知道了 EGL 是繪製 API(比如 OpenGL ES)與 底層平臺窗口系統之間的接口,用於與手機設備打交道,比如獲取繪製 buffer。 而 OpenGL ES 與 GLSL 的主要功能,就是往這塊 buffer 上繪製圖片。由於繪製的第一步就是獲取繪製 buffer,而這完全通過 EGL 來實現的,那麼這一節,我們來仔細研究一下,EGL 是如何跟手機產生關聯,並如何從手機那裏獲取一塊 buffer 用於繪製。


EGL API 總覽

上一節,我們提到 OpenGL ES 其實是一個圖形學庫,由 109 個 API 組成,只要明白了這 109 個 API 的意義和用途,就掌握了 OpenGL ES。

這個道理在 EGL 上也同樣適用。EGL 包含了 34 個 API。

首先,有 7 個 API,用於與手機關聯並獲取手機支持的配置信息。我們知道現在手機種類是各種各樣,從操作系統來說,有 iOS、Android 等。同是 Android 手機,手機品牌和型號也是各種各樣。當然我們不排除有兩款手機,只是外形不同,而內部卻完全一樣,但是總的來說,大部分手機與手機的內部,還是存在着一定的區別的。所以,當我們使用 EGL 與某款手機硬件進行關聯的時候,首先要做的,就是查看一下這款手機支持什麼樣的配置信息。而所謂的配置信息,就是手機支持多少種格式的繪製 buffer,每種格式對應着的 RGBA 是如何劃分的,以及是否支持 depth、stencil 等。關於 RGBA 的格式,等我們在講紋理圖片的時候 再詳細進行說明。而關於 depth、stencil,我們也將在講 OpenGL ES 相應 API 的時 候進行解釋說明。

然後,有 16 個 API,用於根據需要生成手機支持的 surface 和 context,對 surface 和 context 進行關聯,並將 surface 對應的繪製 buffer 顯示到手機屏幕上。 當我們知道了手機支持什麼樣格式的繪製 buffer 之後,就要根據我們所寫的圖形程序的需要,去對這些格式進行篩選,找到一個能滿足我們需求,且手機支持的格式,然後通過 EGL 生成一塊該格式的繪製 buffer。生成繪製 buffer 的過程,其實是我們通過 API 生成一塊 surface,surface 是一個抽象概念,但是這個 surface 包含了繪製 buffer,假如我們所選擇的格式是支持 RGBA、depth、stencil 的。那 麼 surface 對應的繪製 buffer 有一塊 Color buffer,到時候會用於保存圖片的顏色信息,保存的方式在上一節我們做過介紹,就是 buffer 會對應上百萬個像素點, 每個像素點有自己的顏色值,將這些顏色值按照像素點的順序保存在 color buffer 中,就形成了一張完整的 color buffer。繪製 buffer 還有一塊 depth buffer,depth buffer 也按照同樣的方法,按照順序保存了所有像素點的 depth 值;以及一塊 stencil buffer,同理可知,stencil buffer 也是按照順序保存了所有像素點的 stencil 值。

EGL 還會根據格式生成一塊 context,context 也是一塊 buffer。我們知道 OpenGL ES 是狀態集,那麼在繪製中會牽扯到各種各樣的狀態,這些狀態全部都有默認值,可以通過 OpenGL ES 對這些狀態進行改變。這些狀態值就會保存在 context 中。比如 OpenGL ES 所用到的混合模式、紋理圖片、program 還有各種 BO 等信息。

當然 EGL 可以創建多個 surface 和 context,每個 surface 在創建的時候就是 包含了對應的繪製 buffer,每個 context 創建的時候內部都是默認值。然後我們可以根據自己的需要選擇啓動任意一套 surface 和 context,然後對選中的 surface 和 context 進行操作。一個進程同一時間只能啓動有相同格式的一塊 surface 和一塊對應於 OpenGL ES 的 context,一塊 context 同時也只能被一個進程啓動。

之後,有 3 個 API,用於指定使用哪個版本的 OpenGL ES,並與 OpenGL ES 建立關聯。由於 EGL 生成的繪製 buffer 終歸還是要提供給 OpenGL ES 使用,所以, 需要通過 EGL 來指定使用哪個版本的 OpenGL ES。

OpenGL ES 發展到現在已經有了好幾個版本,從最早的 1.1 版本(在 1.1 中 使用的還是固定管線,還不存在 shader 的概念),到現在最普遍的 2.0 版本(2.0 版本中將 OpenGL ES 發展爲上一節我們詳細介紹的管線,加入了可編程模塊 shader),以及目前市面上很多手機已經支持的 3.0 和 3.1 版本(shader 依然存在, 只是變的更加複雜)。所以在 EGL 中需要指定使用哪個版本的 OpenGL ES。

再之後,有 6 個 API,用於操作 EGL 上紋理,以及與多線程相關的高級功能。 紋理圖片,又稱紋理貼圖,一般都是在 OpenGL ES 中,當頂點已經固定,具體形狀已經成型的時候,將其貼上去,把虛擬的形狀變成一個可以看見的物體。比如我們用 OpenGL ES 繪製,用頂點座標勾勒出一個球形,然後把世界地圖作爲紋理貼上去,那麼這個球看上去就變成了地球。所以按照理解紋理應該就是在繪製的時候進行使用,但是在 EGL 中偶爾也會使用到。

EGL 是用於在手機中生成繪製 buffer 提供給 OpenGL ES 進行繪製的,那麼有時候也會設計到多線程操作,每個 thread 可以擁有自己的 surface 和 context,但是也要滿足剛纔我們所說的限制。一個 thread 同一時間只能啓動有相同格式的 一塊 surface 和一塊對應於 OpenGL ES 的 context,一塊 context 同時也只能被一 個 thread 啓動。

最後還有 2 個 API,分別是用於初始化某個版本的 EGL,以及檢測在執行上述 EGL API 的時候是否產生錯誤和產生了什麼錯誤。

這些 API 有一些屬於基本 API,就是在任何手機圖形程序中都會使用到的 API, 這一屆會把這一部分的 API 做詳細介紹。剩下一些屬於進階版的 API,由於我們 這幾節課主要還是爲了講解 OpenGL ES,那些進階版的 API 會放到後面的課程進行補充學習。


EGL API 詳解

EGLint eglGetError(void);

當我們調用 EGL 的 API 的時候,大部分 API 可以通過返回值判斷這個 API 執 行的成功還是失敗。比如當返回值的類型是 EGLBoolean 的時候,返回 EGL_TRUE 代表着成功,而 EGL_FALSE 代表着失敗。而比如當創建 context 的時候,返回一個正常的 context 代表着成功,返回 EGL_NO_CONTEXT 則代表着失敗。但是,當失敗的時候,可能我們還需要更詳細的信息,來判斷爲什麼失敗,是傳入參數有誤,還是發生了別的衝突之類的情況。所以我們需要 eglGetError 這個函數,它的功能是用於返回當前 thread 如果 EGL 的 API 出錯的話,最近一個錯誤所對應的錯誤代碼。

這個函數的輸入爲空。因爲這個 API 是針對目前結果進行判斷的,所以不需要任何輸入。其實在 GPU driver 中,當執行 EGL API 的時候,如果出錯了,會將錯誤代碼寫在寄存器中,然後通過這個函數直接去到寄存器去取即可,所以不需要任何輸入。

這個函數的輸出是可以用來判斷詳細錯誤信息的錯誤代碼。比如當返回 EGL_SUCCESS 的時候,說明截至到目前爲止,所有的 EGL API 都運行正常,沒有出錯。除此之外還有 15 個錯誤代碼,標誌着 15 種錯誤情況,這 15 種情況等我們說到對應 API 的時候再進行具體的解釋說明。

有一種特殊情況,假如在調用這個 API 之前,出現過不止一次錯誤,那麼調用這個 API 獲取的將是最近一次錯誤的錯誤代碼,並將該錯誤代碼的標記重置。然後再調用一次,獲取的將是倒數第二次錯誤的錯誤代碼,以此類推,直至所有被標記的錯誤代碼全部被重置後,再調用這個 API,則返回EGL_SUCCESS。

EGLDisplay eglGetDisplay(EGLNativeDisplayType display_id);

這個函數的功能是用於從 EGL 運行的操作系統中獲取一個 Display 的 handle。

這個函數的輸入是 display_id,這個 display_id 我們可以看作是從操作系統中得知的 Display 的 ID。假如一個手機是由多個顯示屏,那麼不同的 ID 可能就會對應於不同的屏幕,而究竟哪個屏幕對應哪個 ID,我們需要從操作系統中得知, 然後將它傳給這個函數。並且由於 EGL 可以運行在多種操作系統上,所以針對不同的操作系統,這裏的輸入值 display_id 的格式也不同。

但無論是任何操作系統,如果 display_id 爲 EGL_DEFAULT_DISPLAY,會得到一個默認的 Display。

這個函數的輸出是用於顯示圖片繪製的 Display 的 handle。絕大多數 EGL 的 API 都會與這個 Display 的 handle 有關,所有 EGL 相關的對象,比如 surface、context 等,都與這個值有關,且存在於這個 display 的命名空間中。大多數情況下,這 個 display 對應着一塊物理的屏幕。

另外,無論調用這個函數多少次,只要 display_id 不變,那麼返回值就不變, 如果沒有一個 Display 是對應這個 display_id 的,那麼就會返回 EGL_NO_DISPLAY,不報任何錯誤。

EGLBoolean eglInitialize(EGLDisplay dpy, EGLint *major, EGLint *minor);

這個函數的功能是用於針對某 display 初始化某版本的 EGL。

這個函數輸入的第一個參數是 EGLDisplay,我們剛纔已經說了,EGL 的絕大 多數 API都會與這個 Display 的 handle 相關,EGL 所有的對象,都存在於這個 display 的命名空間中。所以我們需要針對這個 display,對 EGL 進行初始化,初始化的時候還需要指定 EGL 的版本。因爲發展到現在 EGL 也是存在很多版本,目前使用的比較多的是 EGL1.4 版本。所以這個函數的第二個和第三個參數,也就是用於指定 EGL 的版本號,其中 major 對應着大號碼,minor 對應着小號碼,所以 1.4 在 這裏,major 是 1,minor 是 4。而當 major 和 minor 爲 NULL 的時候,則不對 EGL 進行初始化。

這個函數的輸出是對 EGL 初始化的結果,當 EGL 初始化成功的時候,返回 EGL_TRUE。失敗的話返回 EGL_FALSE。我們剛纔在說 API eglGetError 的時候,就說了,成功的情況只有一個,但是錯誤的情況卻千千萬,僅靠一個返回值 EGL_FALSE 是無法判斷失敗在哪裏了。所以我們還需要借用 eglGetError 這個 API 來抓取錯誤代碼,進行判斷。假如這個函數返回 EGL_FALSE 的時候,我們調用 eglGetError 來看下錯誤代碼,假如獲取的錯誤代碼是 EGL_BAD_DISPLAY,則說明第一個輸入參數 dpy 並非一個合法的 EGLDisplay;假如獲取的錯誤代碼是 EGL_NOT_INITIALIZED,則說明雖然 dpy 是合法的 EGLDisplay,但是依然無法針對 其初始化 EGL。

另外,可以針對一個已經初始化 EGL 過的 display 重新進行初始化,唯一的 結果就是返回 EGL_TRUE,並且更新 EGL 的版本號。一個在某個 thread 已經初始 化 EGL 過的 display,可以直接使用在另外一個 thread 中,無需再進行初始化。

EGLBoolean eglGetConfigs(EGLDisplay dpy, EGLConfig *configs, EGLint config_size, EGLint *num_config);

這個函數的功能是用於獲取某 display 支持的配置信息。剛纔已經介紹過, 不同的手機支持的配置信息可能是不同的。假如一個手機具備兩個屏幕,那麼這兩個屏幕分別對應於兩個 display,這兩個 display 支持的配置信息可能也是不同的。那麼在這裏,我們就以 display 爲單位,查詢這個 display 支持的配置信息。

這個函數輸入的第一個參數是 EGLDisplay。用於查看特定 Display 的配置信息。第二個參數是一個指針,一般會被預留一定的空間,內部爲空,在調用這個 API 的時候,將 display 支持的配置信息存儲在這個指針中,這個參數與第三個參數相關。第三個參數是我們想要獲取的手機配置信息的最大個數,假如 display 支持 50 種配置信息,而我們只想要 20 個,那麼第三個參數傳入 20,則第二個參數中的指針只會保存 20 個配置信息的內容,如果 display 支持 50 個配置信息, 但是我們想要 100 個,那麼第三個參數傳入 100,第二個參數中的指針還是隻會保存 50 個配置信息的內容。第四個參數是 display 支持配置信息的個數,一般會傳入一個變量的地址,然後將配置信息的個數寫在變量中。所以雖然看上去是 4 個輸入參數,其實應該是兩個輸入參數,兩個輸出參數,第一和第三位輸入參數, 第二和第四個參數經過這個 API,把 display 對應的配置信息,get 出來了。

這個函數的輸出是獲取配置信息的結果,當成功的時候,返回 EGL_TRUE。 失敗的話返回 EGL_FALSE。比如,當輸入參數 dpy 是一個合法的 EGLDisplay,但是這個 display 並沒有通過 eglInitialize 被初始化的時候,返回 EGL_FALSE,通過 eglGetError 獲取的錯誤代碼爲 EGL_NOT_INITIALIZED。如果第四個參數傳入的不是一個變量的地址,而是 NULL,那麼,返回 EGL_FALSE,通過 eglGetError 獲取的錯誤代碼爲 EGL_BAD_PARAMETER。

另外,如果第二個參數傳入的不是一個指針,而是 NULL,那麼就不會有配置信息返回,不過第四個參數依然會返回 display 支持的配置信息的數量。

EGLBoolean eglChooseConfig(EGLDisplay dpy, const EGLint *attrib_list, EGLConfig *configs, EGLint config_size, EGLint *num_config);

這個函數的功能是用於獲取與需求匹配,且某 display 支持的配置信息。剛纔已經介紹過如何獲取 display 支持的配置信息,那麼 display 可能支持多種配置信息,但我們在寫程序的時候,其實只需要選擇一種與我們所寫的圖形程序匹配的即可。那麼我們會制定一個需求,這個需求寫在一個指針中,以 key-value 對的形式存在,key 是 EGLConfig 的屬性,比如 EGL_RED_SIZE,代表所需要配置信息中紅色分量的尺寸,EGL_STENCIL_SIZE,代表所需要配置信息中 stencil 分量 的尺寸, EGL_RENDERABLE_TYPE,代表所需要配置信息中的繪製 API 類型。value 就是這些 key 所對應的我們所需要的值。這裏的需求信息並非需要把 EGLConfig的屬性完全定義一遍,只需要定義一些我們需要的信息,然後通過 eglChooseConfig 這個 API,會遍歷該 display 所支持的所有配置信息,然後獲取到所有與需求信息匹配,且 display 支持的配置信息。

這個函數輸入的第一個參數是 EGLDisplay。用於查看特定 Display 的配置信息。第二個參數是一個保存了需求信息的指針,指針中已經定義好了需求。第三個參數是一個指針,一般會被預留一定的空間,內部爲空,然後用於在這個 API 中,將與需求信息符合,且 display 支持的配置信息存儲在這個指針中,這個參數與第四個參數相關。第四個參數是我們想要獲取的匹配配置信息的最大個數, 假如 display 中匹配配置信息有 50 個,而我們只想要 20 個,那麼第四個參數傳入 20,第三個參數中的指針只會保存 20 個配置信息的內容,如果 display 中匹配配置信息有 50 個,但是我們想要 100 個,那麼第四個參數傳入 100,第三個參數中的指針還是隻會保存 50 個配置信息的內容。第五個參數是 display 匹配配置信息的個數,一般會傳入一個變量的地址,然後將匹配配置信息的個數寫在變量中。所以雖然看上去是五個輸入參數,其實應該是三個輸入參數,兩個輸出參數, 第一和第二和第四位輸入參數,第三和第五個參數經過這個 API,把 display 匹配的配置信息,get 出來了。

這個函數的輸出是獲取配置信息的結果,當成功的時候,返回 EGL_TRUE。 失敗的話返回 EGL_FALSE。比如,當第二個輸入參數,在制定需求的時候,使用到了一個非法的 EGLConfig 的屬性,或者某個 EGLConfig 屬性對應的 value 不識別或者超出了範圍,那麼在執行了這個 API 之後,返回 EGL_FALSE,通過 eglGetError 獲取的錯誤代碼爲 EGL_BAD_ATTRIBUTE 。

另外,在定義第二個參數,書寫需求的時候,EGLConfig 的屬性與 value 應該 一一對應,然後在結尾的地方寫上 EGL_NONE。如果在需求中,沒有定義到某個 EGLConfig 屬性,那麼就按照默認值處理。如果在需求中,針對某個 EGLConfig 屬 性,對應的 value 爲 EGL_DONT_CARE,則在匹配 display 配置信息的時候忽略這個屬性,EGL_DONT_CARE 這個值可以制定給任何屬性,除了 EGL_LEVEL。

如果第二個參數傳入的不是一個指針,而是 NULL,或者第二個參數的第一個值就是 EGL_NONE,那麼就按照 EGLConfig 默認的標準對配置信息進行選擇和排序。

我們來簡單的介紹一下這個默認的標準,剛纔我們介紹了,如果在需求中, 沒有定義到某個 EGLConfi 屬性,那麼就按照默認值處理。比如假如我們沒有在需求中定義 EGL_RED_SIZE,那麼其實相當於我們需求的 EGL_RED_SIZE 爲 0,然後在手機的配置信息中,我們會過濾出所有 EGL_RED_SIZE 大於等於 0 的配置信息,依此類推,當所有的屬性都過濾完畢之後,如果沒有配置信息滿足要求,那麼依然返回 EGL_TRUE,但是最後一個參數將返回 0。如果超過一條配置信息滿足要求,那麼我們需要對這些配置信息進行排序, 由於配置信息有很多屬性,我們會按照屬性來對配置信息進行排序,且屬性與屬性之間也是有優先級的,優先級最高的是屬性 EGL_CONFIG_CAVEAT,但是由於這個屬性默認值爲 EGL_DONT_CARE,那麼我們除非特別需求,否則就會跳過這個屬性。假如我們對這個屬性進行特殊需求,比如在需求中定義這個屬性對應的 value 爲 NULL,那麼就會對手機配置信息中的這個屬性進行排序,排序的順序 是 EGL_NONE、EGL_SLOW_CONFIG, EGL_NON_CONFORMANT_CONFIG。優先級第二的屬性是 EGL_COLOR_BUFFER_TYPE,在這裏我們就不對這些屬性的默認值、 優先級、排序方式進行一一說明了。

EGLBoolean eglBindAPI(EGLenum api);

這個函數的功能是設置當前 thread 的繪製 API,後面創建的 surface 和 context 要與這個 API 相匹配。

這個函數輸入的參數是繪製 API。比如 EGL_OPENGL_API、EGL_OPENGL_ES_API, 或者 EGL_OPENVG_API。需要注意的是,從這裏開始 OpenGL 的 API 與 OpenGL ES 的 api 將開始分離,OpenGL 的 API 主要用於 PC 端的繪製,OpenGL ES 的 API 主 要用於移動端的繪製,所以在這裏,我們一般是傳入 EGL_OPENGL_ES_API 這個參數。OpenVG 是另外一種繪製 API,在這裏我們用不到,也就不進行詳細的描述。

這個函數如果成功,則返回 EGL_TRUE。如果失敗,則返回 EGL_FALSE 。 我們可以通過錯誤代碼判斷失敗的原因。假如傳入參數不是剛纔我們所介紹的三種 API,或者設備不支持我們傳入的參數 API,那麼錯誤代碼爲 EGL_BAD_PARAMETER。

EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config, EGLNativeWindowType win, const EGLint *attrib_list);

這個函數的功能是用於根據需求,創建一個 on-Screen 的 rendering surface, 可以提供給繪製 API,比如 OpenGL ES 進行繪製。surface 一共有三種,這個只是其中的一種,另外還有兩種分別是通過 eglCreatePbufferSurface 以 及 eglCreatePixmapSurface 來創建。我們對這三種 surface 進行一下對比。EGL 和 OpenGL ES 支持兩種繪製模式,back buffer 和 single buffer,windows surface 和 pbuffersurface 都是使用的 back buffer,顧名思義,也就是一塊顯存(GPU)中的 buffer,當繪製完畢的時候,由於 windows surface 於 window 有關聯,那麼可以使用 eglswapbuffer 將其轉移到 window 上進行顯示。而 pbuffer 於 window 沒有關聯,也就無法顯示。而 pixmapsurface 是使用的 single buffer,single buffer 可以看作是保存在系統內存中的位圖,OpenGL ES 不支持將其轉移到 windows 進行顯示,所以 pixmapsurface 也是不可顯示的。

由於另外兩種 surface 都是不可顯示的 surface,且使用的比較少,這裏也就不具體介紹它們的創建函數。

這個函數輸入的第一個參數是 EGLDisplay。用於指定一個特定的 Display 進行 surface 創建。第二個參數是用於創建 surface 的配置信息,一般我們會把 eglChooseConfig 得到的已經匹配好的配置信息傳入,剛纔我們也已經知道了,匹配好的配置信息可能有很多個,不過它們已經排好序,那麼我們就可以直接取第一個作爲這裏的輸入參數。第三個參數是一個平臺相關的參數,是 native window 的 handle。第四個參數,類似於 eglChooseConfig 的第二個參數,是需求信息, 格式也類似,都是 key-value 對,剛纔的 key 是 EGLConfig 的屬性,這裏的 key 是 EGL_RENDER_BUFFER 等屬性,它也是提供了一個接口,可以給一些特殊的平臺創建 surface 的時候規定一些特殊的屬性,類似 EGL extension。這裏我們拿一個屬性進行解釋,比如 EGL_RENDER_BUFFER,它就定義了繪製 API 繪製的時候應該會繪製到哪個 buffer 中,可以繪製到 single buffer,也可以繪製到 back buffer, 我們已經知道了 windowsurface 創建的是 backbuffer,那麼如果繪製到 single buffer,則相當於直接繪製到屏幕上,如果繪製到 back buffer,那麼就會先繪製到 back buffer,再通過 eglswapbuffer 轉移到屏幕上。

這個參數也可以直接寫成 NULL,或者第一個值就是 EGL_NONE,那麼所有的屬性對應的 value 就按照默認值處理,比如 EGL_RENDER_BUFFER 的默認值爲 back buffer。

這個函數如果成功,則輸出是創建的 rendering surface 的 handle。如果失敗, 則返回 EGL_NO_SURFACE。同樣的,我們也可以通過錯誤代碼判斷失敗的原因。 假如是第三個參數 native window 的 handle 與第二個參數 display 的配置信息 EGLConfig 不匹配,那麼錯誤代碼爲 EGL_BAD_MATCH。 如果第二個參數,在配置信息中顯示不支持繪製到 window,也就是在配置信息的屬性 EGL_SURFACE_TYPE 中不包含 EGL_WINDOW_BIT 這一位的時候,當然默認這個屬性是包含這一位的。錯誤代碼也是 EGL_BAD_MATCH。

如果第二個參數,我們需求中的 color 和 alpha 信息與第四個參數我們額外的需求不匹配,錯誤代碼也是 EGL_BAD_MATCH。這種情況確實很有可能發生, 比如我們在 eglChooseConfig 的時候沒有強調使用什麼樣子的 color 或 alpha 信息, 然後根據手機自動匹配,可能匹配一個普通的 color 和 alpha 信息,但是我們在 這個 API 中又通過第四個參數增加了對 color 和 alpha 的需求,那麼很有可能就不匹配,也就出現了這種錯誤。

其他的,如果第二個參數 EGLConfig 不是一個合法的 config,那麼錯誤代碼 EGL_BAD_CONFIG。

如果第三個參數 win 不是一個合法的 native window 的 handle,那麼錯誤代 碼 EGL_BAD_NATIVE_WINDOW。

如果已經使用第三個參數的 win 創建過一個 windowsurface,或者是其他的任何情況導致在創建 windowsurface 的時候分配資源失敗,那麼錯誤代碼 EGL_BAD_ALLOC。

EGLContext eglCreateContext(EGLDisplay dpy, EGLConfig config, EGLContext share_context, const EGLint *attrib_list);

這個函數的功能是用於根據需求,針對當前的繪製 API 創建一個 rendering context。

rendering surface 需要與 rendering context 進行搭配使用的,我們知道 context 中是可以保存 OpenGL ES 狀態集信息的,所以如果一對 surface 和 context 兼容, 那麼 context 就可以使用自己內部保存的信息往 rendering surface 上進行繪製。

通過這個 API 可以在 context 中針對繪製 API 初始化一套狀態集。

這個函數輸入的第一個參數是 EGLDisplay。用於針對哪個 Display 進行 context 的創建。第二個參數是用於創建 context 的配置信息,一般我們會把 eglChooseConfig 得到的已經匹配好的配置信息傳入,和創建 surface 的時候類似, 匹配好的配置信息可能有很多個,不過它們已經排好序,那麼我們一般會直接取第一個作爲這裏的輸入參數。第三個參數可以是另外一個 context 的 handle,那麼新創建的 context 就可以與該 context 共享所有可以共享的數據,如果該 context 之前已經與其他 context 進行了共享,那麼它們三個或者多個之間,都可以進行共享,在 OpenGL ES 對應的 Context 之間,共享的東西可以有紋理、program 和 BO 等信息。如果第三個參數爲 NULL,那麼該 context 暫時不與其他 context 共享。 第四個參數,類似於 eglCreateWindowSurface 的第四個參數,是需求信息,格式也類似,都是 key-value 對,這裏的 key 只有一個,就是 EGL_CONTEXT_CLIENT_VERSION 屬性,該屬性只針對 OpenGL ES API 對應的 context 有效,也就是用於指定該 context 是針對 OpenGL ES 的哪個版本。如果 value 爲 1, 則針對 OpenGL ES 1.x 版本,如果 value 爲 2,則針對 OpenGL ES 2.x 版本,如果 value 爲 3,則針對 OpenGL ES 3.x 版本。這個參數也可以直接寫成 NULL,或者第一個值就是 EGL_NONE,那麼屬性對應的 value 就按照默認值處理,而 EGL_CONTEXT_CLIENT_VERSION 的默認值爲 1。

這個函數如果成功,則輸出是創建的 rendering context 的 handle。如果失敗, 則返回 EGL_NO_CONTEXT 。同樣的,我們也可以通過錯誤代碼判斷失敗的原因。 假如當前繪製 API 爲 EGL_NONE 的時候,也就是當前設備不支持 OpenGL ES,且沒有設置當前的繪製 API 的時候,錯誤代碼爲 EGL_BAD_MATCH。 如果第二個參數,不是一個合法的 EGLConfig,或者不支持參數四指定的具體版本的繪製 API,則錯誤代碼爲 EGL_BAD_CONFIG。這種情況確實很有可能發生,比如我們在 eglChooseConfig 的時候沒有強調使用什麼樣子的 EGL_RENDERABLE_TYPE,那麼默認爲 EGL_OPENGL_ES_BIT,但是參數四指定了具體的版本,也就是如果參數四指定的是 1,那麼在 config 中 EGL_RENDERABLE_TYPE 需要是 EGL_OPENGL_ES_BIT;如果參數四指定的是 2,那麼在 config 中 EGL_RENDERABLE_TYPE 需要是 EGL_OPENGL_ES2_BIT;如果參數四指定的是 3,那麼在 config 中 EGL_RENDERABLE_TYPE 需要是 EGL_OPENGL_ES3_BIT。 假如第三個參數 share context,不是 NULL ,而是一個合法的但與當前繪製 API不匹配的 context,錯誤代碼爲 EGL_BAD_CONTEXT。

如果第三個參數制定的 share_context 是針對另外一個 display 的話,那麼錯誤代碼爲 EGL_BAD_MATCH。

如果沒有足夠的資源用於生成這個 context,那麼錯誤代碼爲EGL_BAD_ALLOC。

EGLBoolean eglMakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);

這個函數的功能是用於 enable surface 和 context,也就是將一個指定的 context綁定到當前的繪製thread上,與讀、寫的surface關聯上。make current 之後,就可以調用 OpenGL ES 的 API 對 context 中的狀態集進行設定,然後進而往 surface 中繪製內容,再從 surface 中把內容讀取出來。

總結一下,一個 native window handle 只能創建一個 rendering surface,而一個 display 可以創建多個 rendering surface。context 與 native window 無關,也就是 display 可以創建多個 context,每個 context 對應一種繪製 API,只要 surface 和 context 的格式匹配,兩者就可以進行關聯,但是同一時間,一個 surface 只能 和一個 context 進行關聯,一個 thread 中,一種繪製 API 也只能有一個 context。

這個函數輸入的第一個參數是 EGLDisplay。用於指定操作特定的 Display。第二個參數是一個 surface,該 surface 用於寫入以及其他除了讀取和複製之外的所有操作。第三個參數也是一個 surface,該 surface 用於讀取和複製。需要注意的是第二個參數和第三個參數可以是同一個參數,一般我們也會將它們設置爲同一個參數。第四個參數就是傳入我們將會 enable 的 context,如果當前 thread 已經有了一個相同繪製 API 的 context,那麼之前的這個 context 就會先進行 flush 操 作,把未執行的命令全部執行完畢,然後將其設置爲 disable 狀態,再把新傳入的 context 設置爲 enable 狀態。

這個函數如果成功,則返回 EGL_TRUE。如果失敗,則返回 EGL_FALSE。同樣的,我們也可以通過錯誤代碼判斷失敗的原因。

假如第一個參數 dpy 不是一個合法的 EGLDisplay handle,那麼錯誤代碼EGL_BAD_DISPLAY。

假如第二個或者第三個參數的 surface 與 context 不匹配的話,錯誤代碼爲EGL_BAD_MATCH。

假如第二個、第三個、第四個參數的 surface 和 context 中的其中一個目前被別的 thread 使用,那麼錯誤代碼爲 EGL_BAD_ACCESS。

假如第二個或者第三個參數的 surface 不是合法的 surface,那麼錯誤代碼爲EGL_BAD_SURFACE。

如果第四個參數 ctx 不是一個合法的 context,那麼錯誤代碼爲EGL_BAD_CONTEXT。

假如第二個或者第三個參數的 surface 對應的 windows 不再合法,那麼錯誤代碼爲 EGL_BAD_NATIVE_WINDOW。

假如第二個或者第三個參數的 surface 不匹配,則錯誤代碼 EGL_BAD_MATCH。

假如當前 thread 目前針對該繪製 API 已經有 context,且該 context 存在未 flush的命令,而且舊的 surface 又突然變成不合法的了,那麼錯誤代碼 EGL_BAD_CURRENT_SURFACE。

第二個或者第三個參數的 surface 在 context 需要的時候,會分配 buffer,但是假如無法針對這個 API 再分配 buffer 了,則錯誤代碼 EGL_BAD_ALLOC。一旦分配成功,這個 buffer 會一直伴隨着 surface,直到 surface 被刪除。

假如這個 API 調用成功,但是之後,draw surface 被破壞掉了,那麼剩下的繪製命令還是會執行,context 的內容依然會被更新,但是寫入 surface 的內容會變成未定義。

假如這個 API 調用成功,但是之後,read surface 被破壞掉了,那麼讀取的數據(比如使用 glReadPixel 讀取)爲未定義。

如果想要釋放當前的 context,也就是將當前的 context disable,那麼將第二個和第三個參數設置爲 EGL_NO_SURFACE,第四個參數設置爲 EGL_NO_CONTEXT 即可。設置完之後原本 context 中未運行的繪製 API 會被 flush,然後將 context 設置爲 disable。在運行這種情況的時候,第一個參數 dpy 可以被傳入一個未初 始化的 display,而除了這種情況,假如 dpy 傳入一個合法的,但是未初始化的 display,錯誤代碼爲 EGL_NOT_INITIALIZED。

如果 context 爲 EGL_NO_CONTEXT,但是 surface 不是 EGL_NO_SURFACE。或者 surface 是 EGL_NO_SURFACE,而 context 不是 EGL_NO_CONTEXT,那麼錯誤代 碼 EGL_BAD_MATCH。

OpenGL ES 中有一個概念叫做視口,視口會有一個視口大小的,這個視口大小是用於表示在一定尺寸的繪製 buffer 中,有多大一塊空間會被顯示出來。當這個 API 運行成功,且 current context 是針對 OpenGL ES 的 API 的時候,這個視口大小和裁剪大小都會被設置爲 surface 的尺寸。關於視口和裁剪,我們會在下面講 OpenGL ES API 的時候進行詳細說明,這裏只要知道 makecurrent 之後,由於 surface 對應 windows,那麼 surface 從 window 獲取到尺寸,然後再被 context 得到,作爲 OpenGL ES 繪製的某個值的初始狀態。

EGLBoolean eglSwapBuffers(EGLDisplay dpy, EGLSurface surface);

當 OpenGL ES 把內容繪製到 surface 上之後,可以通過這個函數,把 surface 中 color buffer 的內容顯示出來。我們還記得 surface 中可能有 color buffer、depth buffer、stencil buffer,而被展示的只是 color buffer。也就是通過這個函數,讓我們看到了手機上不停變換顯示的圖片。

這個函數輸入的第一個參數是 EGLDisplay。用於指定特定的 display 進行顯示。第二個參數是一個 surface,就是指定顯示特定的 surface 中的內容。

這個函數如果成功,則返回 EGL_TRUE。如果失敗,則返回 EGL_FALSE。

假如這個 surface 的屬性 EGL_SWAP_BEHAVIOR 不是 EGL_BUFFER_PRESERVED, 那麼 surface 中 color buffer 的內容爲未定義。

EGLBoolean eglTerminate(EGLDisplay dpy);

這個函數的功能是用於將特定 display 對應的 EGL 相關的資源釋放,比如與 這個 display 關聯的 surface、context 等。如果某個 surface 和 context 在被釋放的時候,依然被使用着,那麼它們並沒有被真正的釋放掉。如果繼續使用它們用於 OpenGL ES 繪製的話,並不會導致程序癱瘓,而只會使得繪製結果不確定,並且繪製命令出錯。只有當使用 API eglReleaseThread,把整個 thread 釋放掉,或者使用 eglMakeCurrent 把該 surface 和 context 設置爲非當前使用的資源,它們纔會被真正的釋放掉。

當調用了 API eglTerminate 之後,surface 和 context 等會變成 invalid,如果在eglTerminate 之後,通過其他的 EGL API 再繼續使用這些資源,會得到錯誤代碼 EGL_BAD_SURFACE 或者 EGL_BAD_CONTEXT。

這個 API 與 eglInitialize 相對應。

這個函數輸入的第一個參數是 EGLDisplay。用於特定釋放哪個 Display 對應的資源。

這個函數的輸出是資源釋放的結果,當成功的時候,返回 EGL_TRUE。失敗的話返回 EGL_FALSE。比如,當輸入參數 dpy 不是一個合法的 EGLDisplay 的時候, 返回 EGL_FALSE,通過 eglGetError 獲取的錯誤代碼爲 EGL_BAD_DISPLAY 。

如果一個 display 已經被 terminate 了,或者尚未 init。那麼它本身其實並沒有相對應的 EGL 的相關資源,這個時候對這個 display 進行 terminate 其實並沒有意義,雖然是被允許的,但是唯一的結果就是會返回 EGL_TRUE。

如果一個 display 已經被 terminate 了,這個時候可以對它進行 re-init,只是 re-init 之後,那些已經被標記爲刪除的資源不變,依然保持標記爲刪除,使用 它們仍然是不合法的。

總結一下,一個 display 隨時隨地可以被初始化或者終止。而所有的 display 都是起始於終止狀態。當調用了 eglInitialize,且成功之後,display 會變成初始成功;調用了 eglTerminate,且成功之後,display 就變成終止狀態。

在 display 處 於 終 止 狀 態 , 只 有 eglMakeCurrent 和 eglReleaseThread , eglInitialize 和 eglTerminate 這 4 個 EGL 的 API 還可以被正常工作,而前兩個 API 是用於進一步的清除這些資源。而除了這 4 個 api,如果再調用任何其他 api,雖然 display 本身還是 valid,但是這個時候它已經被終止了,那麼會產生錯誤代碼 EGL_NOT_INITIALIZED。


EGL API 總結

EGL 的 API 還有很多,這一節只是把其中最重要也是最常用的 11 個 API 拿出來進行了講解,最後總結一下 EGL 使用的大概流程如下:

先獲取 display 的 handle,對 display 進行 EGL 初始化。從設備上獲取匹配的配置信息,再綁定一個繪製 API 用於之後的繪製。根據獲取 display 的 handle、 配置信息以及當前繪製 API 生成 surface 和 context,再把它們綁定在一起,綁定在當前 thread 上,下面就可以使用繪製 API 進行繪製。繪製完成之後,可以把繪製的 surface 中的 color buffer 拿出來顯示。最後,記得把 display 上 EGL 相關資源進行釋放。

除此之外還有很多重要的 API,比如和配置信息相關的 eglGetConfigAttrib, 用於獲取配置信息中具體屬性對應的值。比如對 surface 和 context 用完之後的刪除 API eglDestroySurface 和 eglDestroyContext 等。

這些內容等我們以後講 EGL 補充學習的時候再進行詳細說明。

本節教程就到此結束,希望大家繼續閱讀我之後的教程。

謝謝大家,再見!


寫在末尾的話

Git項目ShineEngine中,使用的是Android Studio進行開發,採取了sdk+ndk的方式,java層使用了控件GLSurfaceView,所以沒有使用這完整的11個EGL API。

其中,創建了GLSurfaceView,然後通過GLSurfaceView.EGLConfigChooser接口獲取一個合適的EGLConfig(其中使用到了eglChooseConfig),然後通過GLSurfaceView.EGLContextFactory接口創建一個合適的Context(其中使用到了eglCreateContext)。由於將這個GLSurfaceView直接進行顯示了,所以在項目中看不到eglGetDisplay、eglInitialize、eglBindAPI、eglCreateWindowSurface、eglMakeCurrent這些API。這樣EGL的環境,依然已經配置完畢。

由於採用了ndk的方式,所以GL API寫在了c文件中,所以在Cmake中需要在target_link_libraries中加入GLESv2庫。目前程序按計劃運行成功。

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