Android N(7.0)中的Vulkan支持

版权声明:本文为博主原创文章,只要标明出处即可转载。 https://blog.csdn.net/ariesjzj/article/details/52430543

原文地址:http://blog.csdn.net/jinzhuojun/article/details/52430543


背景

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.<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基本结构其实都是指针。
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<PFN_vkCreateInstance>(dlsym(libvulkan, "vkCreateInstance"));
vkDestroyInstance = reinterpret_cast<PFN_vkDestroyInstance>(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的实现。

猜你喜欢

转载自blog.csdn.net/ariesjzj/article/details/52430543