1# XComponent组件对接Vulkan 2### 介绍 3XComponent组件作为绘制组件, 可用于满足开发者较为复杂的自定义绘制需求, 如相机预览流显示和游戏画面绘制。 4该组件分为`surface`类型和`component`类型, 可通过指定`type`字段来确定。 其中`surface`类型可支持开发者将相关数据传入XComponent单独拥有的surface来渲染画面。 5本篇示例基于"Native C++"模板, 演示了XComponent调用Vulkan API完成三角形绘制, 并将渲染结果显示在屏幕上的流程。 6 7示例主要使用[@ohos.app.ability.UIAbility](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/ui/Readme-CN.md), 8[@ohos.hilog](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/dfx), 9[@ohos.window](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/windowmanager/window-overview.md), 10[NativeWindow](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/graphics/native-window-guidelines.md), 11[Vulkan](https://gitee.com/openharmony/docs/tree/master/zh-cn/application-dev/reference/native-lib)接口。 12 13### 效果预览 14如下图所示, 打开应用, 屏幕中心会绘制一个旋转中的三角形, 可以点击'stop/start'按钮控制三角形的旋转状态。 15 16### 工程目录 17 18``` 19entry/src/main/ 20|---cpp 21| |---common 22| | |---logger_common.h // Hilog日志宏定义 23| |---render 24| | |---vulkan 25| | | |---shader 26| | | | | |---triangle.frag // fragment shader 27| | | | | |---triangle.vert // vertex shader 28| | | |---vulkan_example.h // 本示例中用于实现三角形绘制的VulkanExample类 29| | | |---vulkan_example.cpp 30| | | |---vulkan_utils.h // 本示例中用于加载Vulkan动态库以及Vulkan函数 31| | | |---vulkan_utils.cpp 32| | |---plugin_manager.h // 对接XComponent 33| | |---plugin_manager.cpp 34| | |---plugin_render.h // 对接后端VulkanExample, 使能Vulkan能力 35| | |---plugin_render.cpp 36| |---CMakeLists.txt 37| |---plugin.cpp // 注册NAPI 38|---ets 39| |---entryability 40| | |---EntryAbility.ts // 定义组件的入口点以及日志类封装 41| |---pages 42| | |---Index.ets // 应用界面布局描述以及shader二进制加载 43``` 44 45### 具体实现 46#### XComponent 47XComponent组件可通过NDK接口为开发者在C++层提供NativeWindow用于创建Vulkan环境. 48##### NAPI注册 49首先填充`napi_module`结构体, 然后调用`napi_module_register`函数注册. 50``` 51// entry/src/main/cpp/plugin.cpp 52static napi_module SampleModule = { 53 .nm_version = 1, 54 .nm_flags = 0, 55 .nm_filename = nullptr, 56 .nm_register_func = Init, // 指定加载C++层编译的动态so的NAPI注册函数名 57 .nm_modname = "nativerender", // 指定被NAPI加载的so的名称, 应与cpp目录下CMakeLists.txt指定的编译so名称一致 58 .nm_priv = ((void *)0), 59 .reserved = {0}, 60}; 61 62extern "C" __attribute__((constructor)) void RegisterEntryModule(void) { 63 napi_module_register(&SampleModule); 64} 65``` 66注意`napi_module`中的成员变量`.nm_register_func`指定了加载so时的注册函数名为`Init`, 我们通过修改该函数来控制注册NAPI时要执行的操作. 67在Init函数中我们封装了一个单例类PluginManager, 以应对多个NativeXComponent实例的情况. 68``` 69// entry/src/main/cpp/plugin.cpp 70static napi_value Init(napi_env env, napi_value exports) 71{ 72 if (!PluginManager::GetInstance()->Init(env, exports)) { 73 LOGE("Failed to init NAPI!"); 74 } 75 return exports; 76} 77``` 78##### PluginManager 79PluginManager::Init函数中主要做了两件事 80###### 获取NativeXComponent指针 81`NativeXComponent`为`XComponent`提供了在`native`层的实例, 可作为ts/js层与`native`层`XComponent`绑定的桥梁. 82``` 83// entry/src/main/cpp/render/plugin_manager.cpp 84bool PluginManager::Init(napi_env env, napi_value exports) 85{ 86 napi_value exportInstance = nullptr; 87 OH_NativeXComponent *nativeXComponent = nullptr; // NativeXComponent指针 88 // 首先调用napi_get_name_property, 传入OH_NATIVE_XCOMPONENT_OBJ, 解析得到exportInstance. 89 napi_status status = napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &exportInstance); 90 // 然后调用napi_unwrap, 从exportInstance中解析得到nativeXComponent实例指针 91 status = napi_unwrap(env, exportInstance, reinterpret_cast<void **>(&nativeXComponent)); 92 ... 93} 94``` 95 96###### 创建和初始化PluginRender 97`PluginManager`可以管理多个`PluginRender`实例(示例中只使用一个), 在`PluginManager::Init`中进行`PluginRender`的创建和初始化. 98实际对接`Vulkan`渲染后端是通过`PluginRender`类完成的, 每个`XComponent`实例对应一个`PluginRender`实例, 通过`XComponentId`进行区分. 99``` 100// entry/src/main/cpp/render/plugin_manager.cpp 101bool PluginManager::Init(napi_env env, napi_value exports) 102{ 103 // 省略获取nativeXComponent的部分 104 105 // 获取XComponentId 106 char idStr[OH_XCOMPONENT_ID_LEN_MAX + 1] = {}; 107 uint64_t idSize = OH_XCOMPONENT_ID_LEN_MAX + 1; 108 // get nativeXComponent Id 109 int32_t ret = OH_NativeXComponent_GetXComponentId(nativeXComponent, idStr, &idSize); 110 if (ret != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { 111 LOGE("PluginManager::Export OH_NativeXComponent_GetXComponentId failed, ret:%{public}d", ret); 112 return false; 113 } 114 115 std::string id(idStr); 116 // 获取PluginManger单例指针 117 auto context = PluginManager::GetInstance(); 118 if (context != nullptr) { 119 context->SetNativeXComponent(id, nativeXComponent); 120 // 传入XComponentId初始化PluginRender 121 auto render = context->GetRender(id); 122 if (render == nullptr) { 123 LOGE("Failed to get render context!"); 124 return false; 125 } 126 render->Export(env, exports); // 注册开放给js/ts层调用的native函数 127 render->SetNativeXComponent(nativeXComponent); // 设置navetiveXComponent指针和回调函数 128 return true; 129 } 130 LOGE("Failed to get PluginManager instance! XComponentId:%{public}s", idStr); 131 return false; 132``` 133 134##### PluginRender 135###### 注册XComponent事件回调 136在`PluginManager::Init`中解析得到`NativeXComponent`指针后, 通过`PluginRender::SetNativeXComponent`将指针传递给`PluginRender`并调用`OH_NativeXComponent_RegisterCallback`注册事件回调. 137``` 138// entry/src/main/cpp/render/plugin_render.cpp 139OH_NativeXComponent_Callback PluginRender::callback_; 140 141OH_NativeXComponent_Callback *PluginRender::GetNXComponentCallback() { 142 return &PluginRender::callback_; 143} 144 145PluginRender::PluginRender(std::string &id) : id_(id), component_(nullptr) 146{ 147 auto renderCallback = PluginRender::GetNXComponentCallback(); 148 renderCallback->OnSurfaceCreated = OnSurfaceCreatedCB; // surface创建成功后触发,开发者可以从中获取native window的句柄 149 renderCallback->OnSurfaceChanged = OnSurfaceChangedCB; // surface发生变化后触发,开发者可以从中获取native window的句柄以及XComponent的变更信息 150 renderCallback->OnSurfaceDestroyed = OnSurfaceDestroyedCB; // surface销毁时触发,开发者可以在此释放资源 151 renderCallback->DispatchTouchEvent = DispatchTouchEventCB; // XComponent的touch事件回调接口,开发者可以从中获得此次touch事件的信息 152} 153 154void PluginRender::SetNativeXComponent(OH_NativeXComponent *component) 155{ 156 component_ = component; 157 OH_NativeXComponent_RegisterCallback(component_, &PluginRender::callback_); // 注册事件回调 158} 159``` 160###### 定义暴露给前端的方法 161`XComponent`支持将`native`方法暴露给前端调用, 一般将该函数命名为`Export`, 它会通过js引擎绑定到js层的一个js对象 162开发者首先需要填充结构体`napi_property_descriptor`, 再调用`napi_define_properties`完成注册. 163下面的示例代码中将native方法`stopOrStart`暴露给前端, 后续会关键到触控事件, 使得用户能通过按钮控制本示例中三角形的旋转. 164``` 165// entry/src/main/cpp/render/plugin_render.cpp 166napi_value PluginRender::Export(napi_env env, napi_value exports) 167{ 168 napi_property_descriptor desc[] = { 169 { "stopOrStart", nullptr, PluginRender::NapiStopMovingOrRestart, nullptr, nullptr, nullptr, 170 napi_default, nullptr} 171 }; 172 napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); 173 return exports; 174} 175``` 176###### 调用Vulkan后端 177在`Surface`创建时触发的回调函数里创建`Vulkan`环境和渲染管线, 并将绘制函数绑定到主线程. 178``` 179// entry/src/main/cpp/render/plugin_render.cpp 180void PluginRender::OnSurfaceCreated(OH_NativeXComponent *component, void *window) 181{ 182 int32_t ret = OH_NativeXComponent_GetXComponentSize(component, window, &width_, &height_); 183 if (vulkanExample_ == nullptr) { 184 vulkanExample_ = std::make_unique<vkExample::VulkanExample>(); 185 vulkanExample_->SetupWindow(static_cast<OHNativeWindow *>(window)); 186 if (!vulkanExample_->InitVulkan()) { 187 LOGE("PluginRender::OnSurfaceCreated vulkanExample initVulkan failed!"); 188 return; 189 } 190 vulkanExample_->SetUp(); 191 renderThread_ = std::thread(std::bind(&PluginRender::RenderThread, this)); 192 } 193} 194 195void PluginRender::RenderThread() 196{ 197 while (vulkanExample_ != nullptr && vulkanExample_->IsInited()) { 198 std::unique_lock<std::mutex> locker(mutex_); 199 if (isTriangleRotational_) { 200 vulkanExample_->RenderLoop(); 201 } else { 202 con_.wait(locker); 203 } 204 } 205} 206``` 207 208#### Vulkan后端 209本示例中用于绘制三角形的`Vulkan`后端被封装成了类`VulkanExample`, 它对外部暴露6个接口: 210``` 211bool InitVulkan(); // 用于初始化Vulkan环境, 包括加载vulkan动态库, 创建Instance, 选择PhysicalDevice以及创建LogicalDevice 212void SetupWindow(NativeWindow* nativeWindow); // 将NativeXComponent的NativeWindow指针传入, 用于surface创建 213void SetUp(); // 创建Swapchain, ImageView及渲染相关组件 214void RenderLoop(); // 在渲染线程循环调用, 用于绘制三角形 215bool IsInited() const; // 判断Vulkan环境是否初始化成功 216void SetRecreateSwapChain(); // 设置下次渲染前重建swapchain 217``` 218因为本示例主要用于展示`XComponent`组件调用`Vulkan`API的流程, 因此对Vulkan绘制三角形的一般流程不做讲解(相关知识可参考[Vulkan官方指导](https://vulkan-tutorial.com/)). 219仅讲解与`XComponent`以及`OpenHarmony`相关的部分, 更多`OpenHarmony VulkanAPI`使用指导可参考[鸿蒙Vulkan](https://gitee.com/openharmony/docs/tree/master/zh-cn/application-dev/reference/native-lib). 220##### libvulkan.so动态库加载 221OpenHarmony操作系统中Vulkan动态库的名称是`libvulkan.so`, 可通过`dlopen`函数加载. 222 223示例代码: 224``` 225#include <dlfcn.h> 226 227const char* path_ = "libvulkan.so"; 228void libVulkan = dlopen(path_, RTLD_NOW | RTLD_LOCAL); 229``` 230 231##### Vulkan函数加载 232Vulkan函数分为`Instance`域函数, `PhysicalDevice`域函数, `Device`域函数. 233 234`Instance`域函数中的`全局函数`可通过`dlsym`函数获取其函数指针. 235 236示例代码: 237``` 238// 全局函数加载 239#include <dlfcn.h> 240// 省略libvulkan.so的加载 241PFN_vkEnumerateInstanceExtensionProperties vkEnumerateInstanceExtensionProperties = 242 reinterpret_cast<PFN_vkEnumerateInstanceExtensionProperties>(dlsym(libVulkan, "vkEnumerateInstanceExtensionProperties")); 243PFN_vkEnumerateInstanceLayerProperties vkEnumerateInstanceLayerProperties = 244 reinterpret_cast<PFN_vkEnumerateInstanceLayerProperties>(dlsym(libVulkan, "vkEnumerateInstanceLayerProperties")); 245PFN_vkCreateInstance vkCreateInstance = 246 reinterpret_cast<PFN_vkCreateInstance>(dlsym(libVulkan, "vkCreateInstance")); 247PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = 248 reinterpret_cast<PFN_vkGetInstanceProcAddr>(dlsym(libVulkan, "vkGetInstanceProcAddr")); 249PFN_vkGetDeviceProcAddr vkGetDeviceProcAddr = 250 reinterpret_cast<PFN_vkGetDeviceProcAddr>(dlsym(libVulkan, "vkGetDeviceProcAddr")); 251``` 252 253在获取`vkGetInstanceProcAddr`函数后, 可通过它加载`Instance`域函数和`PhysicalDevice`域函数; 在获取`vkGetDeviceProcAddr`函数后, 可通过它加载`Device`域函数. 254 255示例代码: 256``` 257// Instance域函数加载 258PFN_vkCreateDevice vkCreateDevice = 259 reinterpret_cast<PFN_vkCreateDevice>(vkGetInstanceProcAddr(instance, "vkCreateDevice")); 260 261// Device域函数加载 262PFN_vkCreateSwapchainKHR vkCreateSwapchainKHR = 263 reinterpret_cast<PFN_vkCreateSwapchainKHR>(vkGetDeviceProcAddr(device, "vkCreateSwapchainKHR")); 264``` 265##### Instance创建 266创建`Instance`时, 为保证后续能成功创建`OpenHarmony`平台下的`surface`, 需要开启extension `VK_OHOS_SURFACE_EXTENSION_NAME`和`VK_KHR_SURFACE_EXTENSION_NAME`. 267 268示例代码: 269``` 270bool VulkanExample::CreateInstance() { 271 VkInstanceCreateInfo createInfo{}; 272 createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; 273 ... 274 // 省略其他成员变量定义过程 275 ... 276 std::vector<const char *> extensions = { VK_KHR_SURFACE_EXTENSION_NAME, VK_OHOS_SURFACE_EXTENSION_NAME }; 277 createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size()); 278 createInfo.ppEnabledExtensionNames = extensions.data(); 279 if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { 280 LOGE("Failed to create instance!"); 281 return false; 282 } 283 return true; 284} 285``` 286##### Surface创建 287OHOS上的`surface`创建需要填写结构体`VkSurfaceCreateInfoOHOS`, XCompoenent通过`SetupWindow`函数传入`window`指针用于`surface`的创建. 288 289示例代码: 290``` 291// window为VulkanExample的成员变量, 其类型为NativeWindow* 292void VulkanExample::SetupWindow(NativeWindow* nativeWindow) 293{ 294 window = nativeWindow; 295} 296 297bool VulkanExample::CreateSurface() { 298 VkSurfaceCreateInfoOHOS surfaceCreateInfo{}; 299 surfaceCreateInfo.sType = VK_STRUCTURE_TYPE_SURFACE_CREATE_INFO_OHOS; 300 if (window == nullptr) { 301 LOGE("Nativewindow is nullptr. Failed to create surface!"); 302 return false; 303 } 304 surfaceCreateInfo.window = window; 305 if (vkCreateSurfaceOHOS(instance, &surfaceCreateInfo, nullptr, &surface) != VK_SUCCESS) { 306 LOGE("Failed to create OHOS surface!"); 307 return false; 308 } 309 return true; 310} 311 312``` 313### 相关权限 314本示例不涉及特殊系统权限。 315### 依赖 316本示例不依赖其他sample。 317### 约束与限制 3181. 本示例要求设备底层驱动已实现[Vulkan API接口](https://gitee.com/openharmony/docs/tree/master/zh-cn/application-dev/reference/native-lib), rk开发板目前不支持(需厂商驱动实现)。 3192. 本示例基于OpenHarmony API 11 SDK(4.1.7.5)。 320 如果想要在HarmonyOS工程上运行, 需保证HarmonyOS SDK版本为API10及以上, 运行方法如下: 321 322 1. 新建一个HarmonyOS Native C++工程; 323 2. 将本工程AppScope/resources/rawfile目录拷贝至新工程AppScope/resources目录下; 324 3. 删除新工程entry/src目录, 将本工程entry目录下所有文件拷贝至新工程的entry目录下。 3253. 本示例需要使用DevEco Studio版本号(4.0 Release)及以上版本才可编译运行。 326### 下载 327如需单独下载本工程,执行如下命令: 328``` 329git init 330git config core.sparsecheckout true 331echo code/BasicFeature/Native/NdkVulkan/ > .git/info/sparse-checkout 332git remote add origin https://gitee.com/openharmony/applications_app_samples.git 333git pull origin master 334```