鸿蒙PC迁移:Natron 节点式影视合成器鸿蒙PC适配全记录

一、写在前面

欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区:https://harmonypc.csdn.net/

项目开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_Natron

欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743

这篇文章记录的是 Natron 在 HarmonyOS PC / OpenHarmony PC 环境中的一次完整适配过程。

Natron 不是 Electron 项目,也不是一个普通 ArkUI 应用。它是一款 C++ / Qt5 的节点式(node-based)影视后期合成软件 ,对标 Nuke / After Effects------左边是查看器(Viewer),中间是节点图(Node Graph),右边是参数面板,靠把一个个节点连成一张图来做调色、抠像、变换、合成、读写序列帧。更关键的是,它本身是一个 OpenFX(OFX)插件宿主 :调色、变换、合成这些"特效"都是标准 OFX 插件,运行时被宿主加载、实例化、渲染。它的代码体量很大------Engine/Gui/Global/App 四个目录就有约 290 个 .cpp,再加上在树内的一堆第三方库(OpenImageIO、OpenColorIO、OpenEXR、boost、cairo、ceres、libmv 跟踪器......),是这一系列适配里依赖最重、最"桌面"的一个。

和 Web 套壳、甚至和一般的单库 Qt 程序都不同,这次适配真正难的地方在于:

  1. 怎样让一个 Qt5 桌面合成器 进入鸿蒙 Stage 模型,并由 libentry.so 启动(和 Minitube / TupiTube 一脉相承的 QPA 思路)。
  2. 怎样把这套 超大工程 + 一堆重型 C++ 依赖 (OIIO/OCIO/OpenEXR/boost/cairo/ceres)全部交叉编译、再编进一个 libentry.so
  3. 查看器用的是桌面 OpenGL 2.0 的老式固定管线glBegin、矩阵堆栈、PBO、*ARB 系列入口)------而鸿蒙PC上是 GLES ,这些函数要么不存在、要么是 NULL 指针,一调就崩。怎样在不动 Natron 渲染逻辑的前提下,做一层 GLES 固定管线仿真把查看器点亮。
  4. Natron 的特效是 OFX 插件,桌面版从插件目录扫 .ofx.bundle 动态加载;鸿蒙沙箱不便这么做,怎样改成 OFX 静态插件OFX_USE_STATIC_PLUGINS)把生成器和 12 个特效直接编进来。
  5. 怎样让它真正"能干活":用 openfx-io(WriteOIIO/ReadOIIO) 把合成结果导出成 EXR/JPEG/TIFF,再读回查看器。
  6. 怎样接上鸿蒙系统的文件管理器,让"打开/保存"能跳出应用沙箱去选真实文件。
  7. 切到后台再切回来就 SIGABRT 闪退 ,以及启动头几秒的白屏 ------怎样从 hilog 崩溃栈定位 QPA 的 RGB565 快照问题并彻底修掉。

本次适配采用逐步验证 的路线:保留 Natron 原有 C++ 主体,新建 harmony_pc/ 作为鸿蒙工程壳;ArkTS 侧只负责 Ability、窗口和 XComponent;真正的 UI 和逻辑仍由 Qt 运行时承载;鸿蒙特有改动全部用 __NATRON_OHOS__ 宏收敛,桌面构建完全不受影响

二、项目背景:Natron 是"宿主 + 一堆重型依赖"的大型 Qt 工程

确认它是 Qt 项目很简单:根目录是 CMake 工程,App/NatronApp_main.cpp 走的是 QApplicationGui/ 整个就是 Qt Widgets 的界面层;它要求 Qt5。

和前面那些 Qt 小工具不同,Natron 的难点在"":

  • 它是 OFX 宿主Engine/OfxHost.cpp 负责扫描、加载、管理 OFX 插件;特效本身(调色、变换、合成)是独立的 OFX 插件工程(openfx-misc),读写器(EXR/JPEG/TIFF)是另一个 OFX 插件工程(openfx-io)。
  • 查看器是 OpenGL 的Gui/ViewerGL.cpp 用桌面 OpenGL 2.x 画图像、画叠加层(overlay),大量用到固定管线和 PBO。
  • 依赖树非常深。在树内/需要交叉编译的就有:OpenImageIO(图像 I/O 总管)、OpenColorIO(色彩管理)、OpenEXR/Imath、boost(filesystem/system/serialization...)、cairo、libtess、ceres + glog + libmv(跟踪器)、Python(脚本,本次先关掉)......

原始项目结构(节选)大致如下:

text 复制代码
Natron-ohos/
├── CMakeLists.txt
├── App/                       # 主程序入口 NatronApp_main.cpp
├── Engine/                    # 渲染引擎 + OFX 宿主(OfxHost.cpp ...)
├── Gui/                       # Qt Widgets 界面:ViewerGL / NodeGraph / 各种 Dialog
├── Global/                    # 跨平台胶水:GLIncludes.h / Macros.h ...
├── libs/                      # 在树第三方库
│   ├── OpenFX/                # OFX 标准头 + 宿主支持
│   ├── openfx-misc/           # 生成器 + 调色/变换/合成等特效插件
│   ├── openfx-io/             # 读写器插件(WriteOIIO / ReadOIIO)
│   ├── SequenceParsing/       # 序列帧名解析
│   └── ...                    # ceres / libmv / gflags / hoedown ...
└── harmony_pc/                # 本次新增的鸿蒙工程壳

鸿蒙适配工程集中放在:

text 复制代码
Natron-ohos/harmony_pc/
├── AppScope/app.json5                 # 包名 fr.natron.Natron
├── build-profile.json5                # 签名配置(用户自己签)
├── entry/
│   └── src/main/
│       ├── cpp/CMakeLists.txt         # 把整个 Natron + OFX 编成 libentry.so
│       ├── ets/                       # AbilityStage / EntryAbility / Index
│       ├── module.json5
│       └── ...
├── deps/                              # 重型依赖的交叉编译脚本 + 产物 prefix
│   ├── ohos-env.sh                    # NDK / CMake / 工具链环境
│   ├── build_deps.sh                  # OIIO/OCIO/OpenEXR/boost/cairo ... 一键编
│   └── prefix/                        # 编好的 .a + 头文件(27 个静态库)
├── ohos-shims/                        # 鸿蒙专用补丁层(GL 仿真 / 文件选择器 ...)
└── qtforharmony_sdk/                  # 项目自带的 Qt for Harmony SDK(234MB)

把鸿蒙改动全部塞进 harmony_pc/ 和少量 #if defined(__NATRON_OHOS__) 分支,是这次适配能保持"桌面不受影响"的前提。

三、鸿蒙工程壳:ArkTS 只管壳,Qt 仍是主角

启动链路和这一系列的 Qt 适配完全一致:

ArkTS Stage UIAbility → XComponent(NODE 类型)→ Qt 的 OpenHarmony QPA 插件 → dlopen libentry.soqtmain / main

  • entry/src/main/ets/entryability/EntryAbility.ets:标准 Stage UIAbility,负责窗口生命周期;并在这里注册了"系统文件选择器"的桥接(后面第十一节讲)。
  • entry/src/main/ets/pages/Index.ets:页面里只放一个 XComponent
typescript 复制代码
XComponent({
  id: this.windowId,
  type: XComponentType.NODE,
  libraryname: 'plugins_platforms_qopenharmony'
})

libraryname 指向 Qt 的 OpenHarmony 平台插件,它会去 dlopen 我们编出来的 libentry.so 并调起 Natron 的 main()。从此 Qt 接管一切:真正的菜单、查看器、节点图都是 Qt 画的,ArkTS 这边只剩一个"画布"。

这样做的好处是:Natron 的 C++ 主体一行不用为鸿蒙重写,所有界面/逻辑照旧,鸿蒙只负责把它"托"起来。

四、项目自带 Qt for Harmony SDK

和 TupiTube/Minitube 一样,这次没有去依赖系统里某个版本的 Qt,而是把一整套 Qt for Harmony SDK 直接放进工程harmony_pc/qtforharmony_sdk/,约 234MB)。好处是:

  • 版本可控,CI/换机都不用重装;
  • 这套 SDK 额外提供了几个鸿蒙专属能力的 C++ 头,后面接系统文件管理器、跨线程回 UI 线程全靠它们:
    • QtCore/qopenharmony.hQtOh::runOnJsUIThreadNoWait/AndWaituiEnv()pathFromUri()/uriFromPath()
    • QtNapi/napi.h:node-addon-api 风格的 C++ 绑定,用来在 C++ 里调 ArkTS 的全局函数、处理 Promise。

这里有一个一开始没注意、后面踩了坑的事实:Qt 跑在和 ArkTS JS 线程不同的线程上。这既是麻烦(不能直接碰 UI),也是救命稻草------后面 C++ 阻塞等待文件选择器结果时,正因为两者不同线程才不会死锁。

五、先验证链路:先让"空壳"跑起来

经验是:上来就全量编译这么大的工程,编译错误会糊一脸,根本分不清是"链路问题"还是"代码问题"。所以先编一个最小壳 ,确认 ArkTS → XComponent → QPA → libentry.so → main 这条路能走通、能在设备上出一个窗口,再往里填肉。

链路验证用的命令(贯穿全程):

bash 复制代码
# 设备:HarmonyOS PC,hdc target 3QC0124C20001268
HDC=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc
T="-t 3QC0124C20001268"

$HDC $T install entry/build/default/outputs/default/entry-default-signed.hap
$HDC $T shell aa start -a EntryAbility -b fr.natron.Natron
# Natron 自己的 qDebug 会进 hilog,tag 是 A04E54:
$HDC $T shell hilog -x | grep A04E54
# 崩溃栈在 DfxFaultLogger 里。

链路一通,接下来就是把整个 Natron 真正编进去。

六、FULL 全量编译:把整个 Natron + 依赖塞进一个 libentry.so

这一步是体力活,分两半:

(1) 先把重型依赖交叉编译出来。 harmony_pc/deps/build_deps.sh 用 OHOS NDK(aarch64-linux-ohos,musl libc,NDK 自带 CMake 3.28)把 OpenEXR/Imath、boost、OpenImageIO、OpenColorIO、cairo、libtess... 一个个编成静态库,装进 deps/prefix/(最后是 27 个 .a)。这里有几个非踩不可的坑,都已经固化进脚本:

  • boost.filesystem 静默残废 :boost 1.82 编出来的 libboost_filesystem.aoperations/path/directory 这几个目标文件被悄悄丢掉了,因为 OHOS 的 libc++ 没有 std::atomic_ref。现象是链接时一堆 boost::filesystem::* undefined。修法:编 boost 时加 -DBOOST_FILESYSTEM_NO_CXX20_ATOMIC_REF
  • boost 非 PIC :默认编出来的 boost 组件库不是 -fPIC,进不了 .so。全部加 -fPIC 重编。
  • OCIO 的 minizip-ng :OpenColorIO 2.3 自己 vendor 了 libminizip-ng.a(藏在 build/ocio/ext/dist/lib),OCIO 引用了一堆 mz_* 符号却不把它装进 prefix。修法:把这个 .a 拷进 prefix 并显式链 minizip-ng

(2) 再把整个 Natron 编进 libentry.so harmony_pc/entry/src/main/cpp/CMakeLists.txtEngine/Gui/Global/App 全部源码 + 在树库 + OFX 支持代码 + 上面那堆 prefix 静态库,统一编/链成一个 libentry.so。最终产物约 100MB(未裁剪前更大)。

成功的标志就是 hvigorw assembleHap 打出 BUILD SUCCESSFUL,并产出 entry-default-signed.hap。完整构建命令:

bash 复制代码
cd harmony_pc
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk
export PATH="/Applications/DevEco-Studio.app/Contents/tools/node/bin:$PATH"
sh /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw \
   assembleHap --mode module -p product=default -p debuggable=true --no-daemon

七、第一次运行与启动期崩溃

第一次跑起来,没那么顺------Natron 启动期做了一堆"桌面假设",在鸿蒙沙箱里会直接崩或卡:

  • Python 脚本子系统:Natron 内置 Python 解释器跑启动脚本,本次先用"无 Python"模式跑(核心合成流程不依赖它),避免一大类初始化崩溃。
  • 启动期的几个模态弹窗 :检查更新、外观设置、OCIO 配置检查、自动保存恢复------这些在桌面是弹窗等用户点,在鸿蒙这种弹窗(尤其会临时建顶层窗口的)很危险。本次在 Gui/GuiAppInstance.cpp 里把这几个提示按 __NATRON_OHOS__ 收敛掉:更新检查直接置否不弹、外观/OCIO 检查跳过、自动保存改成静默恢复不弹框。

这一节修完,Natron 能稳定进主界面了。但真正的"硬骨头"是查看器。

八、查看器:桌面 OpenGL → GLES 固定管线仿真

这是整个适配最难、也最有意思的一节。

Natron 的查看器(Gui/ViewerGL.cpp)和很多老牌图形软件一样,用的是桌面 OpenGL 2.x 的老式风格 :固定管线(glBegin/glEnd、矩阵堆栈 glMatrixMode/glTranslated/glRotatedglVertexPointer 客户端数组)、PBO 异步上传纹理、以及一堆 *ARB 后缀的扩展入口(glMapBufferARBglBindBufferARB...)。

而鸿蒙PC上是 GLES。问题分三层:

  1. 很多桌面 GL 函数在 GLES 上根本不存在------固定管线、矩阵堆栈 GLES2 全砍了。
  2. 更阴险的是 *ARB 入口在 OHOS 上是 NULL 指针eglGetProcAddress("glMapBufferARB") 返回 NULL,而 Natron 代码直接调用,于是跳到地址 0 崩溃 ------崩得还没有报错信息。这个坑专门记了一条备忘:OHOS GLES 上不要碰 *ARB
  3. 字节纹理的格式 GL_BGRA / GL_UNSIGNED_INT_8_8_8_8_REV 不是 GLES 核心格式。

解决思路是:不改 Natron 的渲染逻辑,在底下垫一层 GLES 仿真 。具体落在 harmony_pc/ohos-shims/ 和几个 __NATRON_OHOS__ 分支里:

  • ohos_gl_compat.cpp:一个 GLES 固定管线仿真 ------把 Natron 调的 glBegin/glColor/glMatrixMode/glTranslated/glRotated/glMultMatrixd... 接住,内部翻译成 VBO + GLES 着色器来画。glRotated/glRotatef 是为 Transform/Crop 这类特效的叠加层补的旋转矩阵。
  • ohos_ofx_gl_stubs.cpp:给 OFX 插件叠加层用的老式 GL 符号(glBegin/glColor3f/3d/glMatrixMode/glTranslated/glMultMatrixd/glRotated...)提供实体,转发到上面的仿真指针;gladLoadEGL 直接返回 0。
  • 查看器纹理上传Gui/ViewerGL.cpptransferBufferFromRAMtoGPU):OHOS 分支绕过 PBO 映射 (因为 glMapBufferARB/glBindBufferARB 是 NULL),直接把内存里的像素用 fillOrAllocateTexture 上传。
  • 字节纹理格式Engine/Texture.cpp):OHOS 上改用 GL_RGBA/GL_RGBA/GL_UNSIGNED_BYTE,规避 GL_BGRA(红蓝互换在灰阶棋盘上看不出来)。
  • 绘制路径Gui/ViewerGLPrivate.cppdrawRenderingVAO):OHOS 分支把固定管线的客户端数组换成走仿真的立即模式(glBegin(GL_TRIANGLE_STRIP))。

这一层垫好之后,查看器终于点亮了------能把生成器(比如 CheckerBoard 棋盘格)正确显示出来。

九、OFX 插件静态化:生成器 + 12 个特效

Natron 的特效是 OFX 插件,桌面版从插件目录扫 .ofx.bundle 动态加载。鸿蒙沙箱不便这么搞,于是改成 OFX 静态插件OFX_USE_STATIC_PLUGINS):把插件源码直接编进 libentry.so,宿主端用 _staticBinary 的方式找到它们。这里有个关键细节------宿主要拿到"自己这个二进制"的路径,本次在 Engine/OfxHost.cpp 里用 dladdr 反查 libentry.so 的路径(setHostApplicationBinPath)来对接静态插件表。

最终静态编入并在设备上注册成功的有:

  • 生成器 :CheckerBoard、ColorBars、Constant、Solid(来自 openfx-misc);
  • 12 个特效 :Grade、ColorCorrect、Saturation、Invert、Gamma、Clamp、Premult、Transform、Crop、Merge、Shuffle、Switch。其中 Merge 自己就有约 30 种混合模式变体,所以设备上一共注册了 44 个 OFX 插件
  • 新增的 OFX 支持代码:ofxsTransform3x3.cppofxsTransformInteract.cppofxsShutter.cpp 等;Transform/Crop 的叠加层正是促使我们给 GL 仿真补 glRotated 的原因。

这些特效都能注册、能实例化、能真的改变图像。(把 Invert 作用在已经 premult 的棋盘格上会得到全黑,这其实是正确的:alpha 1→0。CImg 系(Blur 等)还需要补 CImg 头依赖,留待后续。)

十、图像导入导出:让它真正"能干活"

光能看还不够,得能导出成片、再读回来 。这一步靠 openfx-ioWriteOIIO / ReadOIIO 两个 OFX 插件,底层走我们自己编的 OpenImageIO

  • Write 节点 :把合成结果导出。本次构建出的 OIIO 格式有 JPEG / TIFF / OpenEXR / BMP / DPX / TGA / HDR ------没有 PNG(没编 libpng)。
  • Read 节点:把刚导出的文件读回查看器。

端到端验证过:CheckerBoard → Write → export.exr(合法的 1920×1080 OpenEXR,在主机上用 file / sips 检查颜色正确)→ Read 节点再把它读回查看器显示。导出目录是应用沙箱 /data/storage/el2/base/files

openfx-io 静态链接踩的坑(都已固化进构建脚本):

  • openfx-io #include "SequenceParsing/SequenceParsing.h"(带子目录前缀),而我们在树是扁平的 libs/SequenceParsing/*.h------把头文件拷进 libs/openfx-io/IOSupport/SequenceParsing/ 即可。
  • 本次不开 OFX_IO_USING_OCIO:导出时像素原样写出,不在运行时强依赖 OCIO 配置文件,省掉一类"找不到 config"的麻烦。

十一、接上鸿蒙系统文件管理器

到这里"打开/保存"还只能在应用沙箱里转。要让用户去 Documents、Downloads、U 盘里选真实文件,鸿蒙得用系统的 DocumentViewPicker,而它是 ArkTS API------C++ 这边调不到。于是搭了一座"跨线程桥":

把 Natron 所有文件对话框收敛到一个入口 ------Gui/SequenceFileDialog.cpp 在 OHOS 上重写 exec(),转去调 NatronOHOS::pickFileForOpen/Saveharmony_pc/ohos-shims/ohos_file_picker.cpp)。桥的走法是:

复制代码
Qt 线程 (SequenceFileDialog)            ArkTS UI 线程 (globalThis.natronPick)
------------------------                --------------------------------------
pickFileForOpen()
  runOnJsUIThreadNoWait(λ) ───────────▶ λ 在这里跑:调 natronPick(),
  fut.get()  (阻塞)                       它返回一个 Promise<uri>;
                                          挂 .then(onResolve/onReject)
                                    ◀──── onResolve 把 uri 写进 std::promise
  pathFromUri(uri) ─▶ 本地路径
  • C++ 侧用 QtOh::runOnJsUIThreadNoWait 把"调起选择器"这件事抛回 ArkTS UI 线程;
  • ArkTS 侧(EntryAbility.ets 里注册的 globalThis.natronPick)真正 new picker.DocumentViewPicker(context).select()/.save()
  • 结果 URI 通过 std::promise/std::future 回传,C++ 这边阻塞等结果------因为 Qt 和 JS 不是同一个线程,阻塞 Qt 线程不会死锁
  • 最后用 QtOh::pathFromUri(底层 OH_FileUri_GetPathFromUri)把 URI 转成本地路径喂给 Natron。

效果:新建 Read/Write 节点、或工程的打开/保存,都会弹出鸿蒙系统的文件管理器来选文件;取消也能干净地解除阻塞。

十二、闪退修复与加载界面

最后两个体验问题:

(1) 切后台再切回来 SIGABRT 闪退。 崩溃栈是 QPA 的 qFatalmapNativeBufferFormatToQImageFormatOrFail: unsupported format 3------format 3 是 OHOS 的 NATIVEBUFFER_PIXEL_FMT_RGB_565。根因:当别的应用抢到前台时,QPA 想给我们这个16 位(RGB565)的顶层窗口 做快照,但它不支持 16 位栅格窗口,于是直接 qFatal 把进程拍死。三管齐下修掉:

  • App/NatronApp_main.cpp强制默认 QSurfaceFormat 为 32 位 RGBA,从根上不让窗口落到 RGB565;
  • 禁用启动闪屏GuiApplicationManager.cpp,闪屏本身就是个临时的 RGB565 顶层窗口,是另一个崩点);
  • 配合第七节把启动期的模态弹窗都收敛掉。

一句话教训:OHOS 的 QPA 在窗口快照/生命周期切换上比较脆,尽量别在桌面进程里多开顶层/模态窗口。

(2) 启动头几秒白屏。 闪屏一禁用,启动那约 10 秒里 XComponent 是一块白色 surface,很难看。解决:在 ArkTS 侧(Index.ets)盖一层深色加载浮层 ------#2B2B2B 背景 + 居中的 LoadingProgress 转圈 + "正在加载..."文字;等 Natron 主窗口就绪,native 侧通过 globalThis.natronHideLoading(由 GuiApplicationManager::hideSplashScreenNatronOHOS::hideLoadingOverlay 调用)把浮层撤掉。白屏从此变成和 Natron 主题一致的深色加载页。

typescript 复制代码
// Index.ets:深色加载浮层,主窗口就绪前盖住白屏
if (this.showLoading) {
  Column() {
    LoadingProgress().width(72).height(72).color('#D0D0D0');
    Text('正在加载...').fontSize(18).fontColor('#D0D0D0').margin({ top: 16 });
  }
  .justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)
  .width('100%').height('100%').backgroundColor('#2B2B2B');
}

十三、阶段性结果与边界

到这里,Natron 在鸿蒙PC上已经跑通了一条完整的合成-导出-再读取链路:

已经能用的:

  • 完整 Natron 界面(菜单 / 查看器 / 节点图 / 参数面板)在鸿蒙PC上稳定运行,能稳定进主界面、切后台再回来不崩;
  • 查看器靠 GLES 固定管线仿真点亮,能正确显示图像和特效叠加层;
  • 44 个 OFX 插件静态注册:CheckerBoard/ColorBars/Constant/Solid 生成器 + Grade/ColorCorrect/Saturation/Invert/Gamma/Clamp/Premult/Transform/Crop/Merge/Shuffle/Switch 等 12 类特效;
  • 端到端验证:生成器 → 特效 → 查看器 → Write 导出 EXR/JPEG/TIFFRead 读回查看器;
  • 打开/保存接上鸿蒙系统文件管理器,能选沙箱外的真实文件;
  • 深色加载页取代白屏,启动体验干净。

目前的边界(后续可继续):

  • 没有 PNG(没编 libpng);导出格式限于 JPEG/TIFF/EXR/BMP/DPX/TGA/HDR。
  • Python 脚本子系统先关着(核心合成不依赖它),脚本化/Python 节点暂不可用。
  • CImg 系特效(Blur 等)还需补 CImg 头依赖。
  • 查看器走的是 GLES 仿真而非原生桌面 GL,复杂叠加层/GPU 加速路径还有打磨空间。

整体路线值得复用:Qt 主体一行不改、鸿蒙改动全部收敛进 harmony_pc/__NATRON_OHOS__ 分支 ------重型依赖交叉编译进一个 .so、用 GLES 仿真接住老式 OpenGL、把动态加载的 OFX 插件改成静态编入、再用 Napi 桥接系统能力。一个对标 Nuke 的桌面级合成器,就这样在鸿蒙PC上把核心流程跑起来了。