一、写在前面
欢迎加入鸿蒙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 程序都不同,这次适配真正难的地方在于:
- 怎样让一个 Qt5 桌面合成器 进入鸿蒙 Stage 模型,并由
libentry.so启动(和 Minitube / TupiTube 一脉相承的 QPA 思路)。 - 怎样把这套 超大工程 + 一堆重型 C++ 依赖 (OIIO/OCIO/OpenEXR/boost/cairo/ceres)全部交叉编译、再编进一个
libentry.so。 - 查看器用的是桌面 OpenGL 2.0 的老式固定管线 (
glBegin、矩阵堆栈、PBO、*ARB系列入口)------而鸿蒙PC上是 GLES ,这些函数要么不存在、要么是 NULL 指针,一调就崩。怎样在不动 Natron 渲染逻辑的前提下,做一层 GLES 固定管线仿真把查看器点亮。 - Natron 的特效是 OFX 插件,桌面版从插件目录扫
.ofx.bundle动态加载;鸿蒙沙箱不便这么做,怎样改成 OFX 静态插件 (OFX_USE_STATIC_PLUGINS)把生成器和 12 个特效直接编进来。 - 怎样让它真正"能干活":用 openfx-io(WriteOIIO/ReadOIIO) 把合成结果导出成 EXR/JPEG/TIFF,再读回查看器。
- 怎样接上鸿蒙系统的文件管理器,让"打开/保存"能跳出应用沙箱去选真实文件。
- 切到后台再切回来就 SIGABRT 闪退 ,以及启动头几秒的白屏 ------怎样从
hilog崩溃栈定位 QPA 的 RGB565 快照问题并彻底修掉。
本次适配采用逐步验证 的路线:保留 Natron 原有 C++ 主体,新建 harmony_pc/ 作为鸿蒙工程壳;ArkTS 侧只负责 Ability、窗口和 XComponent;真正的 UI 和逻辑仍由 Qt 运行时承载;鸿蒙特有改动全部用 __NATRON_OHOS__ 宏收敛,桌面构建完全不受影响。

二、项目背景:Natron 是"宿主 + 一堆重型依赖"的大型 Qt 工程
确认它是 Qt 项目很简单:根目录是 CMake 工程,App/NatronApp_main.cpp 走的是 QApplication,Gui/ 整个就是 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.so → qtmain / 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.h:QtOh::runOnJsUIThreadNoWait/AndWait、uiEnv()、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.a里operations/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.txt 把 Engine/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/glRotated、glVertexPointer 客户端数组)、PBO 异步上传纹理、以及一堆 *ARB 后缀的扩展入口(glMapBufferARB、glBindBufferARB...)。
而鸿蒙PC上是 GLES。问题分三层:
- 很多桌面 GL 函数在 GLES 上根本不存在------固定管线、矩阵堆栈 GLES2 全砍了。
- 更阴险的是
*ARB入口在 OHOS 上是 NULL 指针 :eglGetProcAddress("glMapBufferARB")返回 NULL,而 Natron 代码直接调用,于是跳到地址 0 崩溃 ------崩得还没有报错信息。这个坑专门记了一条备忘:OHOS GLES 上不要碰*ARB。 - 字节纹理的格式
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.cpp的transferBufferFromRAMtoGPU):OHOS 分支绕过 PBO 映射 (因为glMapBufferARB/glBindBufferARB是 NULL),直接把内存里的像素用fillOrAllocateTexture上传。 - 字节纹理格式 (
Engine/Texture.cpp):OHOS 上改用GL_RGBA/GL_RGBA/GL_UNSIGNED_BYTE,规避GL_BGRA(红蓝互换在灰阶棋盘上看不出来)。 - 绘制路径 (
Gui/ViewerGLPrivate.cpp的drawRenderingVAO):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.cpp、ofxsTransformInteract.cpp、ofxsShutter.cpp等;Transform/Crop 的叠加层正是促使我们给 GL 仿真补glRotated的原因。
这些特效都能注册、能实例化、能真的改变图像。(把 Invert 作用在已经 premult 的棋盘格上会得到全黑,这其实是正确的:alpha 1→0。CImg 系(Blur 等)还需要补 CImg 头依赖,留待后续。)
十、图像导入导出:让它真正"能干活"
光能看还不够,得能导出成片、再读回来 。这一步靠 openfx-io 的 WriteOIIO / 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/Save(harmony_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 的 qFatal:mapNativeBufferFormatToQImageFormatOrFail: 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::hideSplashScreen → NatronOHOS::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/TIFF → Read 读回查看器;
- 打开/保存接上鸿蒙系统文件管理器,能选沙箱外的真实文件;
- 深色加载页取代白屏,启动体验干净。
目前的边界(后续可继续):
- 没有 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上把核心流程跑起来了。