GPUImage移植總結

天天不在發表於2021-05-20

專案github地址: aoce

我是去年年底才知道有GPUImage這個專案,以前也一直沒有在移動平臺開發過,但是我在win平臺有編寫一個類似的專案oeip(不要關注了,所有功能都移植或快移植到aoce裡了),移動平臺是大勢所趨,開始是想著把oeip移植到android平臺上,後面發現不現實,就直接重開專案,從頭開始,從Vulkan到CMake,再到GPUImage,開發主力平臺也從Visual Studio 2017換到VSCode了,這也算是前半年的總結了.

Vulkan移植GPUImage(一)高斯模糊與自適應閾值

Vulkan移植GPUImage(二)Harris角點檢測與導向濾波

Vulkan移植GPUImage(三)從A到C的濾鏡

Vulkan移植GPUImage(四)從D到O的濾鏡

Vulkan移植GPUImage(五)從P到Z的濾鏡

CMake 常用命令

在Android用vulkan完成藍綠幕扣像

android下vulkan與opengles紋理互通

Vulkan與DX11互動

PC的Vulkan運算層時間計算記錄

Vulkan移植GPUImage的Compute Shader總目錄

選擇Vulkan的Compute Shader處理管線

當初選擇Vulkan,一是越來越多裝置與平臺支援,且有獨立的計算管線.

獨立的計算管線在移植GPUImage裡時好處如下.

1 避免很多UV生成類,如GPUImage裡的GPUImageTwoInputFilter / GPUImageTwoInputCrossTextureSamplingFilter等等這種要麼多個輸入,要麼需要查詢周圍點來生成不同UV,特別還有多個輸入與需要周邊UV結合,導致其中GPUImage中有很多類就是用來給FS提供UV.

2 不需要一個對應Vulkan渲染輸出視窗,簡單來說,你可以無視窗執行計算流程,並把結果直接對接win平臺GUI32/DX11的CPU輸出/GPU紋理,也可以在android中對接opengl es紋理,也可以方便對接引擎UE4/Unity3D.

3 計算管線可以利用區域性共享視訊記憶體,區域性共享視訊記憶體在那種需要查詢周邊多個點的情況能大幅提高效能,原則上來說,CS比渲染管線少PS之前的那一系列階段,最新的硬體應該會比用VS+PS高吧?我用vulkan/cuda/dx11(原oeip實現)比較了下執行復雜計算管線的情況,cuda的GPU佔比最低,vulkan其次,dx11會在cuda/vulkan的二倍以上.

不過缺點也有,其中有三個沒移植GPUImage的功能,其中二個就是畫多條線的,主要就是利用VS/PS渲染管線完成,暫時還沒想出好的方法移植,還有一個影像2D-3D多角度轉換利用VS/PS渲染管線也很方便,不過這個在獨立的計算管線應該也好做.

Vulkan資料處理流程

我定義主要實現要滿足二點.

  1. 計算流程可以多個輸入/輸出,每個節點可以多個輸入輸出,每個節點可以關閉開啟,也可關閉開啟此節點分支.

  2. 別的使用者能非常容易擴充套件自己的功能,就是自定義影像處理層的功能.

第一點,我受FrameGraph|設計&基於DX12實現啟發,想到利用有向無環圖來實現.在開始構建時,節點互相連線好.然後利用深度優先搜尋自動排除到關閉自己/分支的節點,拿到最終的有效連線線,有向無環圖可以根據有效連線線生成正確的執行順序,然後檢查每層節點與連線的節點的影像型別是否符合,檢查成功後就初始化每層節點的資源,如果是Vulkan模組,所有層資源生成後,就會把所有執行命令填充到當前圖層的VkCommandBuffer物件中,執行時執行VkCommandBuffer記錄的指令.

在執行時,設定節點/分支是否可用,以及有些層引數改變會影響輸出大小都會導致圖層重啟標記開啟,用標記是考慮到更新引數層與執行GPU運算不在同一執行緒的情況,圖層下次執行前,檢測到重啟標記開啟,就會重新開始構建.

相關原始碼在PipeGraph

而第二點,為了方便使用者擴充套件自己的層,我需要儘可能的自動完善各種資訊來讓使用者只專注需求實現.

對於運算層基類(BaseLayer)注意如下幾個時序.

  1. onInit/onInitNode 當層被加入PipeGraph前/後分別呼叫,在之後,會有個弱引用關聯PipeGraph的PipeNode,同樣,檢查這個引用是否有效可以知道是否已經附加到PipeGraph上.

  2. onInitLayer 當PipeGraph生成正確的有效連線線後,根據有效連線線重新連線上下層並生成正確執行順序後,對各運算層呼叫.

  3. onInitBuffer 每層輸入檢查對應連線層的輸出的影像型別是否符合.

  4. onFrame 每楨執行時呼叫.

  5. onUpdateParamet 層的引數更新,時序獨立於上面的4個點,設定要求隨時可以呼叫.

相關原始碼在BaseLayer

準確到Vulkan模組,Vulkan下的運算層基類(VkLayer)會針對BaseLayer提供更精確的Vulkan資源時序.

  1. 初始化,一般指定使用的shader路徑,UBO大小,更新UBO內資料.預設認為一個輸入,一個輸出,如果是多輸入與多輸出,可以在這指定.注意輸入/輸出個數一定要在附加在PipeGraph之前確定,相應陣列會根據這二個值生成空間.

  2. onInitGraph,當vklayer被新增到VkPipeGraph時上被呼叫.一般用來載入shader,根據輸入與輸出個數生成pipelineLayout,如果有自己邏輯,請override.預設指定輸入輸出的的影像格式為rgba8,如果不是,請在這指定對應影像格式.如果層內包含別的處理層邏輯,請在這添上別的處理層.

  3. onInitNode,當onInitGraph後被新增到PipeGraph後呼叫.本身layer在onInitGraph後,onInitNode前新增到PipeGraph了,當層內包含別的層時,用來指定層內之間的資料如何連結.

  4. onInitLayer,當PipeGraph根據連線線重新構建正確的執行順序後.根據各層是否啟用等,PipeGraph構建正確的各層執行順序,在這裡,每層都知道對應層資料的輸入輸出層,也知道輸入輸出層的大小.當前層的輸入大小預設等於第0個輸入層的輸出大小,並指定執行緒組的分配大小,如果邏輯需要變化,請在這裡修改.

  5. onInitVkBuffer,當所有有效層執行完後onInitLayer後,各層開始呼叫onInitBuffer,其在自動查詢到輸入層的輸出Texture,並生成本層的輸出Texture給當前層的輸出使用後呼叫.如果自己有Vulkan Buffer需要處理,請在onInitVkBuffer裡處理.

  6. onInitPipe,當本層執行完onInitVkBuffer後呼叫,在這裡,根據輸入與輸出的Texture自動更新VkWriteDescriptorSet,並且生成ComputePipeline.如果有自己的邏輯,請override實現.

  7. onCommand 當所有層執行完onInitBuffer後,填充vkCommandBuffer,vkCmdBindPipeline/vkCmdBindDescriptorSets/vkCmdDispatch 三件套.

  8. onFrame 每楨處理時呼叫,一般來說,只有輸入層或輸出層override處理,用於把vulkan texture交給CPU/opengl es/dx11等等.

相關原始碼在VkLayer

雖然列出有這麼多,但是從我移植GPUImage裡來看,很多層特別是混合模式那些處理,完全一個都不用過載,就只在初始化指定下glslPath就行了,還有許多層按上面設定只需要過載一到二個方法就不用管了.

其中Vulkan圖層中,每個圖層中包含一個VulkanContext物件,其有獨立的VkCommandBuffer物件,這樣可以保證每個圖層在多個執行緒互不干擾,各個執行緒可以獨立執行一個或是多個圖層,對於cuda圖層來說,每個圖層也有個cudaStream_t物件,做到各個執行緒獨立執行.

其中aoce_vulkan我定義了VkPipeGraph/VkLayer的實現,以及各個Vulkan物件的封裝,還有輸入/輸出,包含RGBA<->YUV的轉化這些基本的計算層,餘下的GPUImage的所有層全在aoce_vulkan_extra完成,也算是對方便使用者擴充套件自己的層的一個測試,說實話,在移植GPUImage到aoce_vulkan_extra模組過程中,我感覺以前儲存的一些Vulkan知識已經快被我忘光了.

最後到這,使用者實現自己的vulkan處理層,就不需要懂太多vulkan知識就能完成,只需要寫好glsl原始碼,繼承VkLayer,然後根據需求過載上面的一二個函式就行了,歡迎大家在這基礎之上實現自己的運算層.

框架資料流程

資料提供現主要包含如下三種.

  1. 攝像頭,在win端,有aoce_win_mf模組提供,在android端,有aoce_android提供.

  2. 對於多媒體檔案(本地多媒體,RTMP等),由aoce_ffmpeg(win/android都支援)提供解碼.

  3. 直接非壓縮的影像二進位制資料.

資料處理模組現有aoce_cuda/aoce_vulkan模組處理,win端現支援這二個模組,而android端只支援aoce_vulkan模組.

如果資料提供的是楨資料,對應攝像頭/多媒體模組都會解析到VideoFrame並給出回撥,而在資料處理模組會有InputLayer層,專門用來接收上面三種資料.

而處理後資料會根據對應OutputLayer需要,匯出CPU資料以及GPU資料對接對應系統常用渲染引擎對應紋理上,如在win端,aoce_cuda/aoce_vulkan模組的OutputLayer都支援直接導致到對應DX11紋理,而在android上,aoce_vulkan能直接導致到對應opengl es紋理上,這樣就能直接與對應引擎(UE4/Unity3D)底層進行對接.

匯出給使用者呼叫

在重新整理了框架與結構,完善了一些內容,API應該不會有大的變動了,現開始考慮外部使用者使用.

在框架各模組內部,引用匯出的類不要求什麼不能用STL,畢竟肯定你編譯這些模組肯定是相同編譯環境,但是如果匯出給別的使用者使用,需要限制匯出的資料與格式,以保證別的使用者與你不同的編譯環境也不會有問題.

配合CMake,使用install只匯出特殊編寫的.h標頭檔案給外部程式使用,這些標頭檔案主要包含如下三種型別.

  1. C風格的結構,C風格匯出幫助函式,與C風格匯出用來建立對應工廠/管理物件.

  2. 純淨的抽像類,不包含任何STL物件結構,主要用來呼叫API,使用者不要繼承這些類.

  3. 字尾為Observer的抽像類,使用者繼承針對介面處理回撥.