README_zh.md
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```