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