博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
AndroidN 中vulkan 的支持
阅读量:3621 次
发布时间:2019-05-21

本文共 10690 字,大约阅读时间需要 35 分钟。

原文地址:

背景

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。在中,这个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.<ro.product.platform>.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基本结构其实都是指针。
[cpp]
  1. VK_DEFINE_HANDLE(VkInstance)   
  2. VK_DEFINE_HANDLE(VkPhysicalDevice)  
  3. VK_DEFINE_HANDLE(VkDevice)  
  4. ....  

而这些指针指向相应的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中:

[cpp]
  1. void* libvulkan = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);  
  2. if (!libvulkan)  
  3.      return 0;  
  4.   
  5. // Vulkan supported, set function addresses  
  6. vkCreateInstance = reinterpret_cast<PFN_vkCreateInstance>(dlsym(libvulkan, "vkCreateInstance"));  
  7. vkDestroyInstance = reinterpret_cast<PFN_vkDestroyInstance>(dlsym(libvulkan, "vkDestroyInstance"));  
  8. ...  
这些vkXXX函数的定义在api_gen.cpp中(注意这些_gen.*形式的文件都是根据模板用apic工具生成的)。这里看下大体流程,首先是vkCreateInstance()函数:
[cpp]
  1. VKAPI_ATTR VkResult vkCreateInstance(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* pInstance) {  
  2.     return vulkan::api::CreateInstance(pCreateInfo, pAllocator, pInstance);  
  3. }  
它的真正实现在api.cpp中。为了简单,这里假设用的是null_driver,且没有validation layer。
[cpp]
  1. CreateInstance()  
  2.      EnsureInitialized()  
  3.           // 初始化真正的driver,std::call_once()保证只调用一次。  
  4.           driver::OpenHAL()  
  5.                Hal::Open()  
  6.                     hw_get_module("vulkan", ...) // 初始化hwvulkan_module_t  
  7.                     module->common.methods->open(&module->common, HWVULKAN_DEVICE_0, ...) // 初始化hwvulkan_device_t  
  8.           DiscoverLayers() // 查找layer lib,会在搜索路径下寻找validation layer的实现库。  
  9.      LayerChain::CreateInstance()  
  10.           LayerChain chain(...)  
  11.           chain.ActivateLayers() // 从create info中取出layer信息并通过LoadLayer()加载,并将它们在SetupLayerLinks()里链接起来。  
  12.           chain.Create(create_info, ...)  
  13.                vulkan::driver::GetInstanceProcAddr(VK_NULL_HANDLE, "vkCreateinstance")  
  14.                     GetProcHook() // 先找是否有对应hook函数。这个hook列表在全局变量g_proc_hooks里。  
  15.                                   // 对于vkCreateInstance,它在hook列表中,因此会返回该hook函数地址。  
  16.                CreateInstance() // 位于driver.cpp中  
  17.                     AllocateInstanceData() // 分配每个instance所关联的InstanceData。  
  18.                     Hal::Device().CreateInstance() // ICD中的CreateInstance(),这里假设是null_driver.cpp里的CreateIntance()。  
  19.                     SetData() // 将InstanceData和VkInstance绑定。  
  20.                     InitDriverTable() // 实现在driver_gen.cpp中,将InstanceDriverTable结构InstanceData.driver中的API函数指针初始化好。  
  21.                                       // 使用的是Hal::Device().GetInstanceProcAddr(),这里假设是null_driver.cpp中的GetInstanceProcAddr(),  
  22.                                       // 它的实现在null_driver_gen.cpp中。  
  23.                InitDispatchTable() // 实现在api_gen.cpp中。和上面类似,初始化InstanceDispatchTable结构InstanceData.dispatch。  
  24.                                    // 这里用的是driver.cpp中的driver::GetInstanceProcAddr()。  
  25.                                    // 像CreateAndroidSurfaceKHR这些平台相关的接口都是作为其中的扩展。  
这里看下上面用到的driver::GetInstanceProcAddr()的实现,先是看该函数是否在hook表中,否则调用ICD的GetInstanceProcAddr()来搜索函数。这时返回的就是ICD中的相应实现了。
[cpp]
  1. PFN_vkVoidFunction GetInstanceProcAddr(VkInstance instance, const char* pName) {  
  2.      const ProcHook* hook = GetProcHook(pName);  
  3.      if (!hook)  
  4.           return Hal::Device().GetInstanceProcAddr(instance, pName);  
按照vulkan spec,创建了instance,接下来需要创建device。为了简单,先忽略查找设备的过程,从vkCreateDevice()看起。其流程比vkCreateInstance()简单些,但也涉及了很多重要的初始化工作。
[cpp]
  1. vkCreateDevice()  
  2.      CreateDevice() // 位于api.cpp  
  3.           LayerChain::CreateDevice()  
  4.                LayerChain chain()  
  5.                chain.ActivateLayers() // Layer处理  
  6.                chain.Create(physical_dev, ...)  
  7.                     driver::CreateDevice()  
  8.                          // 分配DeviceData  
  9.                          null_driver::CreateDevice()  
  10.                          SetData() // 将DeviceData绑定VkDevice.  
  11.                          InitDriverTable() // 这里使用的是null_driver::GetDeviceProcAddr()。  
  12.                     InitDispatchTable(dev, ) // 这里使用的是driver::GetDeviceProcAddr()。  
可以看到只有当创建了instance,真正的driver库才会被打开,相应的函数指针才会被初始化。App中的vulkan资源有两大类:一类是per-instance的,一类是per-device的。前者主要结构为InstanceData,后者为DeviceData。

初始化完后,app就可以调用vulkan的接口了。举例来说,当app调用了vkAllocateMemory(),该函数首先会调用到api_gen.cpp中的wrapper:

[cpp]
  1. VKAPI_ATTR VkResult vkAllocateMemory(VkDevice device, const VkMemoryAllocateInfo* pAllocateInfo, const VkAllocationCallbacks* pAllocator, VkDeviceMemory* pMemory) {  
  2.      return vulkan::api::AllocateMemory(device, pAllocateInfo, pAllocator, pMemory);  
  3. }  
这里会从DeviceData中的跳板函数表dispatch中找AllocateMemory相应的地址并调用:
[cpp]
  1. VKAPI_ATTR VkResult AllocateMemory(VkDevice device, const VkMemoryAllocateInfo* pAllocateInfo, const VkAllocationCallbacks* pAllocator, VkDeviceMemory* pMemory) {  
  2.      return GetData(device).dispatch.AllocateMemory(device, pAllocateInfo, pAllocator, pMemory);  
  3. }  
那这个函数地址指向哪里呢?从前面InitDispatchTable()可以看到,该地址是通过driver::GetInstanceProcAddr()取得的。而该函数中首先从g_proc_hooks全局表中找是否有相应的函数,失败的话则调用ICD中的GetDeviceProcAddr()来查找。
[cpp]
  1. PFN_vkVoidFunction GetDeviceProcAddr(VkDevice device, const char* pName) {  
  2.      const ProcHook* hook = GetProcHook(pName);  
  3.      if (!hook)  
  4.           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的实现。
你可能感兴趣的文章
finally_Keyword
查看>>
final_Keyword
查看>>
enum_Keyword
查看>>
extends_Keyword
查看>>
if_Keyword
查看>>
implements_Keyword
查看>>
import_Keyword
查看>>
int_Keyword
查看>>
专访小邪:从十年技术之路看阿里技术体系的变革
查看>>
超详细解读:神经语义解析的结构化表示学习 | 附代码分析
查看>>
在 CentOS 6 里安装 OpenJDK 1.8 和 Tomcat 8.5
查看>>
在 Debian 9 里安装 php5.3 fpm
查看>>
【干货合集】从菜鸟到老司机,20篇文章带你了解DevOps!
查看>>
【干货合集】12篇文章带你读懂敏捷架构!
查看>>
手把手教你从零搭建深度学习项目(附链接)
查看>>
主题演讲:漏洞扫描在Web安全的应用
查看>>
量子计算会成为下一次革命的新机会吗?
查看>>
getValueFromEvent和shouldUpdate
查看>>
2019/07/01 tomcat replication cluster(01)
查看>>
2019/07/18 kvm虚拟化(04)
查看>>