Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android N(7.0)中的Vulkan支持

Android N(7.0)中的Vulkan支持

編輯:關於Android編程

背景

Vulkan為Khronos Group推出的下一代跨平台圖形開發接口,用於替代歷史悠久的OpenGL。Android從7.0(Nougat)開始加入了對其的支持。Vulkan與OpenGL相比,接口更底層,從而使開發者能更直接地控制GPU。由於更好的並行支持,及更小的開銷,性能上也有一定的提升。另外層式架構可以幫助減少調試和測試的時間。但是,代價是實現相同的功能更復雜了。原本用OpenGL寫個最簡單的demo百來行,用vulkan祼寫的話沒千把行下不來。因此實際使用中需要有utility層來簡化接口調用。Android對vulkan的支持主要是提供接口讓開發者可以用vulkan開發圖形相關應用,尤其是像游戲這樣的3D渲染場景。如果有支持vulkan的Android移動設備,開發者就可以利用NDK進行基於vulkan的開發了。

我們知道vulkan的架構是loader+layers+ICD的層式結構(圖請參見https://vulkan.lunarg.com/doc/view/1.0.13.0/windows/LoaderAndLayerInterface.html)。Layer一般用於提供loader和ICD沒有提供的附加功能,比如驗證,調試等。因為vulkan的API設計上為了避免性能損失基本不做錯誤檢查。這樣就需要我們在開發過程中enable validation layer,發布時再去除,那麼就既可免除過多錯誤檢測帶來的開銷,又可為開發帶來方便。每個layer可以定義若干個entry point。流程上來說,vulkan接口的調用會采用chain of responsibility模式。即依次查找各個layer中是否有該接口的entry point,如果都沒有最後到達ICD。之前基於OpenGL要做類似的事情需要做wrapper layer,現在vulkan把它融入了架構設計。

從前面的架構可以看到,從app到GPU IHV提供的ICD之間,需要有vulkan runtime。在Android中,這個runtime主要位於/frameworks/native/vulkan目錄,它會編譯成libvulkan.so。主要作用是對driver的封裝,及提供API hook能力,還有與本地native window的整合,同時它提供了一個ICD的參考實現null_driver。其中主要的幾個目錄包括api(用於生成api的模板),libvulkan(loader),nulldrv(默認ICD實現)。include下為vulkan暴露的頭文件,其中最主要的兩個頭文件為vulkan.h(通用內容),vk_platform.h(平台相關內容)。

 

Vulkan runtime (libvulkan.so)

根據vulkan的架構,一個app要真正用到vulkan driver的函數,需要先通過loader,這個loader在Android上的實現在/frameworks/native/vulkan/libvulksn下。這個loader的作用,顧名思義主要是加載和調用driver。因為vulkan中可以有0個到多個layer,因此當app調用這些入口後,loader會負責將它們dispatch給相應的layer。我們知道Android中大多數driver是通過HAL機制(/hardware/libhardware/hardware.c)來尋找和加載的。因此,和gralloc, hwcomposer,一樣,廠商需要提供vulkan..so。我們知道對於HAL機制下的每個模塊,需要提供hw_module_t和hw_device_t等通用接口。對於vulkan,它相應的定義是hwvulkan_module_t和hwvulkan_device_t,位於 /frameworks/native/vulkan/include/hardware/hwvulkan.h。前者是通用模塊定義,可以支持多個driver;後者對應單個driver,目前只支持HWVULKAN_DEVICE_0。廠商提供的driver需要實現並暴露這兩個結構。hwvulkan_dispatch_t是vulkan特有的,表示dispatchable object handle。從定義來看像VkInstance, VkPhysicalDevice, VkDevice這些vulkan基本結構其實都是指針。
VK_DEFINE_HANDLE(VkInstance) 
VK_DEFINE_HANDLE(VkPhysicalDevice)
VK_DEFINE_HANDLE(VkDevice)
....

而這些指針指向相應的driver中的結構VkXXX_T。這些結構首個成員都是hwvulkan_dispatch_t,其中的vtbl經初始化由loader設置指向相應的結構。

\

在/frameworks/native/vulkan目錄下,有兩個driver的實現:null_driver和stubhal。它們有些類似,都是真正driver不存在情況下的fallback。兩者的區別在於,前者是硬件driver不存在下的fallback,類似於gralloc.defaut.so和hwcomposer.default.so,而後者的目的是loader在沒有HAL實現的情況下避免每次檢查HAL為null。

在接下去之前,先看一下vulkan中的一些基本相關背景。Vulkan中不再有全局狀態,所有app相關的狀態存在VkInstance中,所以app會先通過vkCreateInstance()創建instance,然後通過vkEnumeratePhysicalDevices()查詢系統中的物理設備,接著用vkCreateDevice()根據指定物理設備創建邏輯設備。另外,根據vulkan spec,vulkan不必要靜態暴露接口,接口函數的指針可以通過vkGetInstanceProcAddr()來獲得,類似於OpenGL中的GetProcAddress系函數。而vkGetInstanceProcAddr函數本身是通過平台相關的loader來提供的。vkGetDeviceProcAddr()和其它接受VkInstance或VkPhysicalDevice為第一參數的函數地址可通過vkGetInstanceProcAddr()獲得,它們是per-instance的;以VkDevice, VkQueue或VkCommandBuffer為第一參數的函數地址可通過vkGetDeviceProcAddr()獲得,它們是per-device的。如果通過直接調用這些查詢到的API地址就可以避免dispatch所帶來的開銷。

\

以https://github.com/googlesamples/android-vulkan-tutorials中的sample為例,一個app要使用vulkan,需要打開libvulkan.so,然後通過dlsym取其中的vulkan接口函數地址。這些common的code在vulkan_wrapper.cpp中:

void* libvulkan = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
if (!libvulkan)
     return 0;

// Vulkan supported, set function addresses
vkCreateInstance = reinterpret_cast(dlsym(libvulkan, "vkCreateInstance"));
vkDestroyInstance = reinterpret_cast(dlsym(libvulkan, "vkDestroyInstance"));
...
這些vkXXX函數的定義在api_gen.cpp中(注意這些_gen.*形式的文件都是根據模板用apic工具生成的)。這裡看下大體流程,首先是vkCreateInstance()函數:
VKAPI_ATTR VkResult vkCreateInstance(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance) {
    return vulkan::api::CreateInstance(pCreateInfo, pAllocator, pInstance);
}
它的真正實現在api.cpp中。為了簡單,這裡假設用的是null_driver,且沒有validation layer。
CreateInstance()
     EnsureInitialized()
          // 初始化真正的driver,std::call_once()保證只調用一次。
          driver::OpenHAL()
               Hal::Open()
                    hw_get_module("vulkan", ...) // 初始化hwvulkan_module_t
                    module->common.methods->open(&module->common, HWVULKAN_DEVICE_0, ...) // 初始化hwvulkan_device_t
          DiscoverLayers() // 查找layer lib,會在搜索路徑下尋找validation layer的實現庫。
     LayerChain::CreateInstance()
          LayerChain chain(...)
          chain.ActivateLayers() // 從create info中取出layer信息並通過LoadLayer()加載,並將它們在SetupLayerLinks()裡鏈接起來。
          chain.Create(create_info, ...)
               vulkan::driver::GetInstanceProcAddr(VK_NULL_HANDLE, "vkCreateinstance")
                    GetProcHook() // 先找是否有對應hook函數。這個hook列表在全局變量g_proc_hooks裡。
                                  // 對於vkCreateInstance,它在hook列表中,因此會返回該hook函數地址。
               CreateInstance() // 位於driver.cpp中
                    AllocateInstanceData() // 分配每個instance所關聯的InstanceData。
                    Hal::Device().CreateInstance() // ICD中的CreateInstance(),這裡假設是null_driver.cpp裡的CreateIntance()。
                    SetData() // 將InstanceData和VkInstance綁定。
                    InitDriverTable() // 實現在driver_gen.cpp中,將InstanceDriverTable結構InstanceData.driver中的API函數指針初始化好。
                                      // 使用的是Hal::Device().GetInstanceProcAddr(),這裡假設是null_driver.cpp中的GetInstanceProcAddr(),
                                      // 它的實現在null_driver_gen.cpp中。
               InitDispatchTable() // 實現在api_gen.cpp中。和上面類似,初始化InstanceDispatchTable結構InstanceData.dispatch。
                                   // 這裡用的是driver.cpp中的driver::GetInstanceProcAddr()。
                                   // 像CreateAndroidSurfaceKHR這些平台相關的接口都是作為其中的擴展。
這裡看下上面用到的driver::GetInstanceProcAddr()的實現,先是看該函數是否在hook表中,否則調用ICD的GetInstanceProcAddr()來搜索函數。這時返回的就是ICD中的相應實現了。
PFN_vkVoidFunction GetInstanceProcAddr(VkInstance instance, const char* pName) {
     const ProcHook* hook = GetProcHook(pName);
     if (!hook)
          return Hal::Device().GetInstanceProcAddr(instance, pName);

按照vulkan spec,創建了instance,接下來需要創建device。為了簡單,先忽略查找設備的過程,從vkCreateDevice()看起。其流程比vkCreateInstance()簡單些,但也涉及了很多重要的初始化工作。
vkCreateDevice()
     CreateDevice() // 位於api.cpp
          LayerChain::CreateDevice()
               LayerChain chain()
               chain.ActivateLayers() // Layer處理
               chain.Create(physical_dev, ...)
                    driver::CreateDevice()
                         // 分配DeviceData
                         null_driver::CreateDevice()
                         SetData() // 將DeviceData綁定VkDevice.
                         InitDriverTable() // 這裡使用的是null_driver::GetDeviceProcAddr()。
                    InitDispatchTable(dev, ) // 這裡使用的是driver::GetDeviceProcAddr()。

可以看到只有當創建了instance,真正的driver庫才會被打開,相應的函數指針才會被初始化。App中的vulkan資源有兩大類:一類是per-instance的,一類是per-device的。前者主要結構為InstanceData,後者為DeviceData。

\

初始化完後,app就可以調用vulkan的接口了。舉例來說,當app調用了vkAllocateMemory(),該函數首先會調用到api_gen.cpp中的wrapper:

VKAPI_ATTR VkResult vkAllocateMemory(VkDevice device, const VkMemoryAllocateInfo* pAllocateInfo, const VkAllocationCallbacks* pAllocator, VkDeviceMemory* pMemory) {
     return vulkan::api::AllocateMemory(device, pAllocateInfo, pAllocator, pMemory);
}
這裡會從DeviceData中的跳板函數表dispatch中找AllocateMemory相應的地址並調用:
VKAPI_ATTR VkResult AllocateMemory(VkDevice device, const VkMemoryAllocateInfo* pAllocateInfo, const VkAllocationCallbacks* pAllocator, VkDeviceMemory* pMemory) {
     return GetData(device).dispatch.AllocateMemory(device, pAllocateInfo, pAllocator, pMemory);
}
那這個函數地址指向哪裡呢?從前面InitDispatchTable()可以看到,該地址是通過driver::GetInstanceProcAddr()取得的。而該函數中首先從g_proc_hooks全局表中找是否有相應的函數,失敗的話則調用ICD中的GetDeviceProcAddr()來查找。
PFN_vkVoidFunction GetDeviceProcAddr(VkDevice device, const char* pName) {
     const ProcHook* hook = GetProcHook(pName);
     if (!hook)
          return GetData(device).driver.GetDeviceProcAddr(device, pName);

這裡是通過null_driver::GetDeviceProcAddr()查找就會返回null_driver::AllocateMemory。如果在平台上廠商有提供vulkan實現的話此時就會調用到廠商的相應實現中去。

 

WSI(Window System Integration)

和OpenGL一樣,當實際使用時還需要和平台的native window系統對接。對vulkan而言,也是類似的。這層對接層稱為WSI。對OpenGL而言,Android平台的WSI是通過EGL實現的。而在vulkan中,Android在libvulkan.so中提供了VK_KHR_surface , VK_KHR_swapchain , and VK_KHR_android_surface,VK_ANDROID_native_buffer這些extension來做WSI。說白了它們就是把Android中ANativeWindow,ANativeWindowBuffer那坨平台相關的東西與vulkan driver中的接口和數據結構做橋接。具體地,主要是將vulkan接口與Android中的gralloc, BufferQueue結合。Vulkan spec中定義了swapchain(VkSwapchainKHR),用於建模surface更新渲染結果的過程,它抽象了一組與surface綁定的可提交image(VkImage)。在每個vulkan支持的平台上,我們都可以找到類似的presentation模型。舉例來說,Android中的buffer交換模型和vulkan中的通用模型本質上是類似的:

 

\

Android中libvulkan做的事之一就是將這兩者結合起來。首先ICD中需要實現幾個Android相關的extension:vkGetSwapchainGrallocUsageANDROID,vkAcquireImageANDROID,vkQueueSignalReleaseImageANDROID。他們的用途一會兒會提到。對於app而言,典型的和WSI相關流程如下:


1. 先通過vkCreateAndroidSurfaceKHR() 創建VkSurfaceKHR。其實現位於swapchain.cpp中的CreateAndroidSurfaceKHR()。可以看到其中創建了driver::Surface對象並初始化。參數中傳入的ANativeWindow會賦到創建的Surface對象window成員中。同時還可以看到,在Android平台上VkSurfaceKHR其實就是這個driver::Surface類的指針。這個Surface的作用是維護了ANativeWindow和VkSwapchain的對應關系。它與後面要創建的Swapchain相關的數據結構關系如下:

\

2. 通過vkCreateSwapchainKHR()創建VkSwapchainKHR。它的實現在swapchain.cpp中的CreateSwapchainKHR()。可以看到,其中對於driver::Surface中指向的libgui::Surface初始化了一坨屬性(通過ANativeWindow接口)。其中會通過擴展接口vkGetSwapchainGrallocUsageANDROID()將vulkan中的屬性轉成gralloc能認的屬性。另外會通過libgui::Surface從BufferQueue中取出buffer(ANativeWindowBuffer)並轉為VkImage。這個過程依賴於對vkCreateImage()的擴展。轉化過程中首先根據ANativeWindowBuffer中的值構造VkNativeBufferAndroid,然後VkNativeBufferAndroid作為vkCreateImage()的參數創建相應的VkImage。ANativeWindowBuffer和VkImage的對應關系由driver::Swapchain::Image數組來維護(如上圖)。數組中元素的最大個數與libgui中BufferQueue中的定義一樣。另外VkSwapchainKHR其實就是Swapchain的指針。

前兩步相當於EGL中的eglCreateWindowSurface(),完成surface的初始化,大體流程圖如下:

\

3. 每當需要繪制新的一幀時,先調用vkAcquireNextImageKHR()獲得一個app可用的buffer。該buffer由上面提到的Swapchain中的images數組的index表示。但此時可能GPU還是在操作該buffer,因此拿到後還需等返回的semaphore signal後才能確認該buffer真正可用。接下來就可以真正渲染了。

4. 渲染完一幀後,調用vkQueuePresentKHR()提交前面獲取的buffer。同樣的,buffer用index表示。

後兩步大體流程圖如下:

\

vkAcquireNextImageKHR()和vkQueuePresentKHR()的實現分別位於swapchain.cpp中的AcquireNextImageKHR()和QueuePresentKHR()。前者本質是調用libgui::Surface的dequeueBuffer()拿到一個buffer,然後找到該buffer在driver::Swapchain::Image數組中的index並返回。後者本質上是調用queueBuffer()將該buffer放回到BufferQueue中去。可以看到,本質上這些WSI相關接口就是把vulkan中的接口轉為Android中的相應接口。相應的數據結構也需做類似的轉化。

還有個問題就是Android中的buffer同步使用的是fence fd(通過ANDROID_native_fence_sync擴展),vulkan中使用的是VkSemaphore和VkFence,因此這之間也需要轉換。這時上面提到的vkAcquireImageANDROID()和vkQueueSignalReleaseImageANDROID()就起到作用了。前者將native fence fd轉為VkSemaphore和VkFence;後者創建一個native fence fd。

 

驗證和調試

和通用做法不一樣,由於Android中的安全策略限制,loader只會從指定路徑下搜索libVkLayer_*.so作為validation layer的庫。NDK中提供了一些validation layer,比如libVkLayer_core_validation.so,libVkLayer_image.so,libVkLayer_object_tracker.so,libVkLayer_parameter_validation.so,libVkLayer_threading.so等。當ro.debuggable為true時,即可調試設備上,還會從/data/local/debug/vulkan目錄查找。

另外VK_EXT_debug_report擴展可以允許app在指定的一些點調用自定義的回調函數。詳見https://developer.android.com/ndk/guides/graphics/validation-layer.html#debug及https://github.com/googlesamples/android-vulkan-tutorials中的例子。

 

其它

Android N中在surfaceflinger進程增加了GpuService,讓其它進程可以通過surfaceflinger進程查詢vulkan的能力,以json格式輸出。它利用了Android N中binder新增加的SHELL_COMMAND_TRANSACTION。它本質上是通過IPC實現了進程A在進程B中執行shell命令並返回結果。一個典型例子見/frameworks/native/cmds/cmd/cmd.cpp(該工具通過將本進程的stdin, stdout, stderr傳給目標進程,達到以指定service進程身份執行命令的目的)。

其它一些相關的project包括:
/external/vulkan-validation-layers/loader/:改編自Khronos官方的loader和validation layer實現(https://github.com/KhronosGroup/Vulkan-LoaderAndValidationLayers)。
/external/deqp/external/vulkancts/:Vulkan CTS,它屬於dEQP(drawElements Quality Program)。OEM可以通過它來測試vulkan的實現。

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved