摘要:针对鸿蒙 PC 系统特性与 CodeArts 开发环境限制,本文以 libplacebo 图形渲染库为实践对象,完成第三方动态链接库的移植集成与功能验证。围绕鸿蒙二进制强制签名、动态库依赖查找、编译构建适配、IDE 语法解析异常等核心问题,通过规范资源目录结构、改造 CMake 构建规则、配置自动签名脚本、优化运行环境参数等方案,打通编译、链接、签名、运行全流程。测试结果表明,库接口调用稳定、各功能模块运行正常,验证了该集成方案的可行性与通用性,可为鸿蒙 PC 端各类第三方 C/C++ 原生库快速适配提供参考。
欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
前提准备
步骤 1:鸿蒙PC 中 CodeArts 下载
应用市场-我的-应用尝鲜-CodeArts IDE

步骤 2:CodeArts 创建 C++ 项目
打开 CodeArts,点击菜单 File → New → Project ,选择 C++ 模板 创建项目。
创建时注意:
- 项目类型选择 C++ Executable(可执行程序)
- 构建系统会自动配置为 CMake + Ninja
- 创建完成后,项目根目录下应生成 CMakeLists.txt、main.cpp、以及 .arts/ 配置目录

步骤 3:确认项目结构
创建完成后,在左侧项目视图中确认以下文件已生成:
plain
demo/
├── CMakeLists.txt ← CMake 构建配置文件
├── main.cpp ← 默认的 Hello World 源码
└── .arts/ ← CodeArts IDE 配置目录
├── compile_commands.json
├── tasks.json
└── launch.json

步骤 4:确认编译器环境
CodeArts 在鸿蒙 PC 上使用 BiSheng LLVM 工具链(clang++)。打开 CodeArts 底部终端面板,执行以下命令确认工具链可用:
plain
clang++ --version
预期输出应包含 BiSheng 和版本号(如 14.0.0)。
再确认 binary-sign-tool 可用:只要不报错、弹出帮助说明 = 工具正常
plain
binary-sign-tool
这个工具是鸿蒙系统自带的代码签名工具,后续所有 .so 文件都必须用它签名才能运行。

步骤 5:获取 libplacebo 鸿蒙适配产物
通过 lycium_plusplus 鸿蒙适配框架对 libplacebo 进行适配,生成产物 tar 包。tar 包中应包含以下文件:
plain
libplacebo_7.362/
└── usr/
├── lib/
│ ├── libplacebo.so ← 主动态库
│ └── libplacebo.so.362 ← 带版本号的动态库
└── include/
└── libplacebo/
├── gpu.h
├── log.h
├── dummy.h
├── renderer.h
├── dispatch.h
├── cache.h
├── colorspace.h
├── filters.h
└── ...(其他头文件)
注意:如果你还没有适配产物,需要先使用 lycium_plusplus 框架对 libplacebo 进行鸿蒙化适配,或直接向提供方索取已适配的 tar 包。
一、为什么需要在 CodeArts 中集成三方 Native 库
1.1 应用场景
在鸿蒙 PC 的 CodeArts 中开发高性能 C++ 应用时,经常需要调用已有的成熟 C/C++ 库:
- 音视频处理:FFmpeg、libplacebo、x264/x265 等
- 图像处理:OpenCV、libpng、libjpeg-turbo 等
- 数学/算法库:Eigen、FFTW、OpenBLAS 等
- 网络/安全:OpenSSL、cURL、libsodium 等
这些库通常已经通过鸿蒙适配框架(如 lycium_plusplus)编译好了 ARM64 架构的 .so 产物,开发者不需要重新编译,直接以预构建库(Prebuilt Library)的方式集成即可。
1.2 CodeArts C++ 项目的构建机制
CodeArts 的 C++ 项目采用以下技术栈:
CMake 构建配置层:通过 CMakeLists.txt 统一管理编译流程,配置头文件路径、链接三方 .so 库、指定编译参数。
Ninja 构建执行层:CMake 生成 build.ninja 文件后,由 Ninja 负责实际的编译和链接。Ninja 是增量构建系统,只编译变更的文件。
clangd 语言解析层:CodeArts 使用 clangd 提供代码补全、跳转、错误提示。clangd 依赖 compile_commands.json 获取编译参数(如 -I 头文件路径)。
二进制签名层:鸿蒙系统要求所有可执行文件和动态库必须经过 binary-sign-tool 签名,否则运行时 dlopen 直接拒绝加载。
核心流程:
plain
修改代码 (main.cpp)
↓
CMake 解析配置 (CMakeLists.txt)
↓
Ninja 增量编译链接
↓
POST_BUILD 触发(复制 .so + binary-sign-tool 签名)
↓
生成已签名的可执行文件
↓
运行调试
在这个流程中,有三个关键节点需要特别注意:
| 节点 | 涉及文件 | 常见问题 |
|---|---|---|
| 编译前 | compile_commands.json | clangd 读不到头文件路径,IDE 里代码爆红 |
| 链接时 | CMakeLists.txt | .so 路径配错,链接失败 |
| 运行前 | binary-sign-tool | .so 未签名,运行时 Permission denied |
1.3 集成的本质:告诉构建系统 4 件事
无论集成哪个三方库,在 CodeArts 的 CMake 项目中只需要解决 4 个核心问题:
- 头文件在哪 → 编译器能找到 #include <libplacebo/xxx.h>
- .so 文件在哪 → 链接器能解析符号,生成可执行文件
- 运行时怎么找 .so → 动态链接器能通过 RPATH 或 LD_LIBRARY_PATH 找到库
- 签名怎么自动完成 → 每次构建后自动调用 binary-sign-tool,否则无法运行
1.4 本文示例:libplacebo
选择 libplacebo(专业视频渲染与色彩处理库)作为演示载体的原因:
- 依赖极简,仅需系统自带的 libc.so 和 libc++_shared.so 即可运行,无需额外依赖链
- 提供丰富的 C API(日志、GPU 抽象、色彩空间、矩阵运算、渲染器等),可全面验证集成效果
- 已通过 lycium_plusplus 鸿蒙适配框架完成适配,产物包含完整的 .so + .h
- 适配方法具有通用性,验证通过后可无缝集成 FFmpeg、mpv 等更复杂的三方库
1.5 环境信息
| 项目 | 版本/信息 |
|---|---|
| 操作系统 | Windows 10 22H2(宿主) |
| IDE | CodeArts(鸿蒙 PC 版) |
| 目标设备 | HUAWEI MateBook Pro(鸿蒙 PC) |
| 目标架构 | arm64-v8a |
| 编译器 | BiSheng clang++(LLVM 工具链) |
| 构建系统 | CMake 3.20+ + Ninja |
| 示例库 | libplacebo v7.362.0(API v362) |
二、前置准备:检查三方库的 .so 产物
拿到三方库的编译产物(tar 包)后,不要急着放进项目,先做两项检查。
2.1 确认产物内容
拿到 tar 包后,先解压并检查目录结构是否完整。
第一步:解压产物包
假设产物包名为 libplacebo_7.362.tar.gz,在终端中解压:
plain
cd /storage/Users/currentUser/Desktop
tar -xzf libplacebo_7.362.tar.gz
第二步:检查产物目录结构
plain
ls -R libplacebo_7.362/
合格的产物应包含以下结构:
plain
libplacebo_7.362/
└── usr/
├── lib/
│ ├── libplacebo.so ← 必须有
│ └── libplacebo.so.362 ← 必须有(运行时按 SONAME 查找)
└── include/
└── libplacebo/
├── gpu.h ← 必须有
├── log.h ← 必须有
├── dummy.h ← 必须有(测试用)
├── renderer.h
├── dispatch.h
├── cache.h
├── colorspace.h
├── filters.h
└── ... ← 其他头文件
第三步:快速验证文件是否存在
plain
ls libplacebo_7.362/usr/lib/libplacebo.so*
ls libplacebo_7.362/usr/include/libplacebo/gpu.h
如果以上命令都返回文件路径,说明产物完整,可以继续下一步。
如果缺少文件怎么办? 如果只有 libplacebo.so 而没有 libplacebo.so.362,需要重新从 lycium_plusplus 适配产物中提取完整版本。如果缺少头文件,需要确认适配时是否生成了 dev 包(包含头文件的包)。
2.2 llvm-readelf 检查 .so 依赖
CodeArts 自带的 BiSheng LLVM 工具链中包含 llvm-readelf,在终端中执行:
第一步:在终端中进入产物目录
打开 CodeArts 底部终端面板,先找到你的产物路径。假设产物 tar 包已解压到桌面:
plain
cd /storage/Users/currentUser/Desktop/libplacebo_7.362/usr/lib
怎么知道自己的路径? 在 CodeArts 终端中执行 pwd 查看当前所在目录,或用 ls /storage/Users/ 查看用户名,再拼接出完整路径。
第二步:执行 llvm-readelf 检查依赖
plain
llvm-readelf -d libplacebo.so
输出中重点关注三个字段。以下是 libplacebo 的真实输出示例:
plain
Dynamic section at offset 0x...:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc++_shared.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
0x0000000000000001 (NEEDED) Shared library: [ld-musl-aarch64.so.1]
0x000000000000000e (SONAME) Library soname: [libplacebo.so.362]
0x000000006ffffff0 (VERSYM) 0x...
0x000000006ffffff9 (RELACOUNT) ...
0x0000000000000000 (NULL) 0x0

分析方法:
| 字段 | 含义 | 处理方式 |
|---|---|---|
| NEEDED | 运行时依赖的其他 .so | 系统自带的(如 libc.so、libc++_shared.so、ld-musl-aarch64.so.1)无需处理;非系统库(如 libxxx.so)必须一起放到 third_party/lib/ |
| SONAME | 库的内部自标识名称 | 运行时动态链接器按 SONAME 找文件,确保 libplacebo.so.362 存在即可 |
为什么一定要检查?
- 依赖缺失:如果 NEEDED 中有非系统库(比如另一个三方库 libshaderc.so),你只复制了 libplacebo.so,运行时会报 No such file or directory
- SONAME 不匹配:如果 SONAME 是 libplacebo.so.362,但你的目录里只有 libplacebo.so,运行时同样会报 No such file or directory
这两个问题如果不提前检查,会在集成后期才暴露,排查成本很高。
2.3 确认 SONAME 与版本号文件
从 2.2 节的 llvm-readelf 输出中已经看到,libplacebo 的 SONAME 是 libplacebo.so.362。这意味着运行时动态链接器会按照这个名字去找文件,而不是 libplacebo.so。
实际检查:
plain
ls libplacebo_7.362/usr/lib/libplacebo.so*
预期输出(两个文件都必须存在):
plain
libplacebo_7.362/usr/lib/libplacebo.so
libplacebo_7.362/usr/lib/libplacebo.so.362
两者的区别:
| 文件 | 作用阶段 | 说明 |
|---|---|---|
| libplacebo.so | 链接时 | 编译器/链接器使用,CMake 中 target_link_libraries 指向的就是这个文件 |
| libplacebo.so.362 | 运行时 | 动态链接器根据 SONAME 查找,必须和可执行文件在同一目录(或通过 LD_LIBRARY_PATH 指定) |
如果缺少 .so.362 文件怎么办?
有些产物包只提供了 libplacebo.so。这时可以手动创建软链接:
plain
cd libplacebo_7.362/usr/lib/
ln -s libplacebo.so libplacebo.so.362
但最稳妥的做法是从 lycium_plusplus 适配产物中提取完整的版本号文件,因为软链接在 Windows 挂载路径下可能有兼容性问题。
三、文件放在 CodeArts 项目的哪里(文件集成)
讲解拿到产物后,需要在 CodeArts 项目中创建哪些目录、把文件放在什么位置。
3.1 CodeArts C++ 项目的关键目录
首先在 CodeArts 中创建一个 C++ 模板 项目。创建后的项目结构中,与三方库集成相关的关键目录如下:
plain
demo/
├── CMakeLists.txt ← 【需修改】CMake 构建配置
├── main.cpp ← 【需修改】C++ 源码
├── .arts/
│ ├── compile_commands.json ← 【需修改】clangd 编译命令数据库
│ ├── tasks.json ← 【需修改】构建任务配置
│ └── launch.json ← 【需修改】调试启动配置
└── third_party/ ← 【需手动创建】三方库存放目录
├── include/
│ └── libplacebo/ ← ★ 三方库头文件放这里
│ ├── gpu.h
│ ├── log.h
│ ├── dummy.h
│ └── ...
└── lib/
├── libplacebo.so ← ★ 三方动态库放这里
└── libplacebo.so.362 ← ★ 带版本号的库文件
3.2 第一步:放 .so 动态库文件
位置:third_party/lib/
操作方式:将 lycium_plusplus 适配产物 tar 包中的 .so 文件复制到 third_party/lib/ 目录下。
第一步:确认你的项目路径
在 CodeArts 终端中执行以下命令,查看项目所在的实际路径:
plain
pwd
假设输出为 /storage/Users/currentUser/Desktop/demo,后续命令中的 PROJECT_DIR 都指代这个路径。
第二步:创建目录并复制 .so 文件
plain
# 创建 third_party/lib 目录
mkdir -p PROJECT_DIR/third_party/lib
# 复制 .so 文件(将 /path/to/libplacebo_7.362 替换为你的实际产物路径)
cp /path/to/libplacebo_7.362/usr/lib/libplacebo.so PROJECT_DIR/third_party/lib/
cp /path/to/libplacebo_7.362/usr/lib/libplacebo.so.362 PROJECT_DIR/third_party/lib/
# 验证文件是否复制成功
ls -la PROJECT_DIR/third_party/lib/
提示 :如果你不知道产物路径,可以在 CodeArts 左侧项目视图中右键点击产物文件夹 → Copy Path,然后在终端中用 cd 进入该目录。
为什么放在项目内部?
- 鸿蒙 PC 上 Windows 挂载路径的 .so 文件权限受限(rwxrwx---,owner 为 20001006),且 chmod 无法修改
- 将文件复制到项目本地目录后,可以绕过权限问题,配合代码签名正常使用
- 项目自包含,便于版本管理和 CI/CD 集成

3.3 第二步:放头文件
位置:third_party/include/libplacebo/
操作方式:将适配产物中的 include/libplacebo/ 目录整体复制到 third_party/include/ 下。
plain
# 创建 third_party/include 目录
mkdir -p PROJECT_DIR/third_party/include
# 复制头文件目录(将 /path/to/libplacebo_7.362 替换为你的实际产物路径)
cp -r /path/to/libplacebo_7.362/usr/include/libplacebo PROJECT_DIR/third_party/include/
# 验证头文件是否复制成功
ls PROJECT_DIR/third_party/include/libplacebo/
预期应看到 gpu.h、log.h、dummy.h、colorspace.h 等头文件。
为什么放这里?
- CMakeLists.txt 中通过 target_include_directories 将此路径加入编译器搜索路径
- C++ 代码中可以用 #include <libplacebo/gpu.h> 的标准方式引用头文件
- CodeArts 的 clangd 语言服务器也需要此路径来解析符号,消除编辑器中的红色波浪线

3.4 文件集成小结
总共需要放置两类文件,都不需要修改任何代码:
| 文件类型 | 放置位置 | 作用 |
|---|---|---|
| .so 动态库 | third_party/lib/ | 链接和运行时被加载,提供 native 函数实现 |
| 头文件(.h) | third_party/include/ | 编译时提供 API 声明,使 C++ 代码能调用三方库 |
四、需要修改哪些代码(代码配置)
文件放好后,需要修改 5 个代码文件,告诉 CodeArts 的构建系统和运行时如何找到并使用三方库。
4.1 修改 CMakeLists.txt:告诉构建系统"库在哪、怎么链接"
文件位置:项目根目录 CMakeLists.txt
怎么找到这个文件? 在 CodeArts 左侧项目视图中,双击打开 CMakeLists.txt。
为什么要修改它? 这是整个集成的核心配置文件。CMake 管理编译流程,我们需要在其中声明:
- 三方库的头文件在哪里(target_include_directories)
- 三方库的 .so 文件在哪里(target_link_libraries)
- 运行时去哪里找 .so(BUILD_RPATH)
- 构建后自动复制和签名 .so(add_custom_command POST_BUILD)
完整配置如下:
plain
cmake_minimum_required(VERSION 3.20)
project(demo)
set(CMAKE_CXX_STANDARD 11)
set(LIBPLACEBO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/third_party")
add_executable(demo main.cpp)
target_include_directories(demo PRIVATE ${LIBPLACEBO_ROOT}/include)
target_link_libraries(demo PRIVATE ${LIBPLACEBO_ROOT}/lib/libplacebo.so)
# 构建时复制 .so 到输出目录并签名(鸿蒙PC要求动态库代码签名)
add_custom_command(TARGET demo POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${LIBPLACEBO_ROOT}/lib/libplacebo.so $<TARGET_FILE_DIR:demo>
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${LIBPLACEBO_ROOT}/lib/libplacebo.so.362 $<TARGET_FILE_DIR:demo>
COMMAND binary-sign-tool sign -inFile $<TARGET_FILE_DIR:demo>/libplacebo.so -outFile $<TARGET_FILE_DIR:demo>/libplacebo.so -selfSign "1"
COMMAND binary-sign-tool sign -inFile $<TARGET_FILE_DIR:demo>/libplacebo.so.362 -outFile $<TARGET_FILE_DIR:demo>/libplacebo.so.362 -selfSign "1"
)
# 运行时优先在当前目录找 .so
set_target_properties(demo PROPERTIES
BUILD_RPATH "$ORIGIN"
)
逐行解释为什么要这样配置:
| 步骤 | CMake 命令 | 为什么需要 |
|---|---|---|
| 定义路径 | set(LIBPLACEBO_ROOT ...) | 集中管理三方库路径,避免硬编码 |
| 头文件路径 | target_include_directories | 让编译器能找到 #include <libplacebo/xxx.h> |
| 链接库 | target_link_libraries(... libplacebo.so) | 让 demo 在编译时产生对 libplacebo 的符号引用 |
| 运行时路径 | BUILD_RPATH "$ORIGIN" | 告诉动态链接器在可执行文件同目录下查找 .so |
| 复制 .so | copy_if_different | 将 .so 从 third_party 复制到构建目录,得到未签名的副本 |
| 自动签名 | binary-sign-tool sign ... | 鸿蒙强制要求,否则运行时 dlopen 失败 |
为什么必须复制后再签名? 原始 third_party/lib/libplacebo.so 如果已经被手动签名过,再次签名会报错 .codesign section already exists。copy_if_different 会创建一份全新的副本,确保签名成功。

4.2 修改 main.cpp:编写 C++ 调用代码
文件位置:main.cpp
怎么找到这个文件? 在 CodeArts 左侧项目视图中,双击打开 main.cpp。创建项目时这个文件默认包含一个 Hello World 的 main 函数,我们需要把它替换成 libplacebo 的测试代码。
为什么要修改它? 这个文件是程序的入口。我们需要在里面调用 libplacebo 的 API,验证库是否能正常加载、初始化、创建资源、执行运算,最后输出测试结果。
代码结构讲解:
这段测试代码按顺序执行 12 个模块的验证:
| 模块 | 测试的 API | 验证目的 |
|---|---|---|
| 1. Log System | pl_log_create | 日志系统能否初始化 |
| 2. Dummy GPU | pl_gpu_dummy_create | GPU 实例能否创建(Dummy 后端) |
| 3. Cache System | pl_cache_create | 缓存系统能否挂载到 GPU |
| 4. Format Query | pl_find_fmt | 能否查询到支持的像素格式 |
| 5. Texture Creation | pl_tex_create | 能否创建纹理资源 |
| 6. Buffer Creation | pl_buf_create | 能否创建缓冲区资源 |
| 7. Timer Creation | pl_timer_create | 计时器对象(Dummy 下可能返回 NULL) |
| 8. Dispatch & Shader | pl_dispatch_create | 调度器和着色器对象能否创建 |
| 9. Renderer | pl_renderer_create | 渲染器能否初始化 |
| 10. Color Space | pl_color_space_is_hdr | 色彩空间检测是否正确 |
| 11. Filters | 链接验证 | 滤波器符号是否正确链接 |
| 12. GPU Limits | gpu->limits | GPU 能力上限是否能读取 |
完整代码如下:
plain
#include <iostream>
#include <cstring>
#include <libplacebo/gpu.h>
#include <libplacebo/log.h>
#include <libplacebo/dummy.h>
#include <libplacebo/renderer.h>
#include <libplacebo/dispatch.h>
#include <libplacebo/cache.h>
#include <libplacebo/colorspace.h>
#include <libplacebo/filters.h>
static void print_test_header(const char* name) {
std::cout << "\n========== " << name << " ==========" << std::endl;
}
static void print_result(bool ok, const char* msg, int& errors) {
std::cout << (ok ? "[PASS] " : "[FAIL] ") << msg << std::endl;
if (!ok) errors++;
}
int main() {
int errors = 0;
// ========== 1. Log System ==========
print_test_header("1. Log System");
pl_log_params log_params = pl_log_default_params;
log_params.log_cb = pl_log_color;
log_params.log_level = PL_LOG_INFO;
pl_log log = pl_log_create(PL_API_VER, &log_params);
print_result(log != NULL, "pl_log_create", errors);
if (!log) return 1;
// ========== 2. Dummy GPU ==========
print_test_header("2. Dummy GPU Initialization");
pl_gpu gpu = pl_gpu_dummy_create(log, NULL);
print_result(gpu != NULL, "pl_gpu_dummy_create", errors);
if (!gpu) {
pl_log_destroy(&log);
return 1;
}
std::cout << " API version: " << PL_API_VER << std::endl;
std::cout << " GLSL version: " << gpu->glsl.version << std::endl;
std::cout << " Compute support: " << (gpu->glsl.compute ? "yes" : "no") << std::endl;
std::cout << " Thread safe: " << (gpu->limits.thread_safe ? "yes" : "no") << std::endl;
std::cout << " Num formats: " << gpu->num_formats << std::endl;
// ========== 3. Cache System ==========
print_test_header("3. Cache System");
pl_cache cache = pl_cache_create(&pl_cache_default_params);
print_result(cache != NULL, "pl_cache_create", errors);
if (cache) {
pl_gpu_set_cache(gpu, cache);
std::cout << " Cache attached to GPU" << std::endl;
}
// ========== 4. Format Query ==========
print_test_header("4. Format Query");
pl_fmt fmt_rgba8 = pl_find_fmt(gpu, PL_FMT_UNORM, 4, 8, 8, PL_FMT_CAP_SAMPLEABLE);
print_result(fmt_rgba8 != NULL, "pl_find_fmt rgba8 unorm", errors);
if (fmt_rgba8) {
std::cout << " rgba8: name=" << fmt_rgba8->name
<< " texel_size=" << fmt_rgba8->texel_size << std::endl;
}
pl_fmt fmt_rgba32f = pl_find_named_fmt(gpu, "rgba32f");
print_result(fmt_rgba32f != NULL, "pl_find_named_fmt rgba32f", errors);
pl_fmt fmt_vertex = pl_find_vertex_fmt(gpu, PL_FMT_FLOAT, 4);
print_result(fmt_vertex != NULL, "pl_find_vertex_fmt float4", errors);
// ========== 5. Texture Creation ==========
print_test_header("5. Texture Creation");
struct pl_tex_params tex_params = {0};
tex_params.w = 256;
tex_params.h = 256;
tex_params.format = fmt_rgba8;
tex_params.sampleable = true;
tex_params.renderable = true;
tex_params.host_writable = true;
tex_params.host_readable = true;
pl_tex tex = pl_tex_create(gpu, &tex_params);
print_result(tex != NULL, "pl_tex_create 256x256 rgba8", errors);
if (tex) {
std::cout << " Texture: " << tex->params.w << "x" << tex->params.h
<< " format=" << tex->params.format->name << std::endl;
}
// ========== 6. Buffer Creation ==========
print_test_header("6. Buffer Creation");
struct pl_buf_params buf_params = {0};
buf_params.size = 1024;
buf_params.host_writable = true;
buf_params.host_readable = true;
buf_params.uniform = true;
pl_buf buf = pl_buf_create(gpu, &buf_params);
print_result(buf != NULL, "pl_buf_create 1KB UBO", errors);
if (buf) {
std::cout << " Buffer size: " << buf->params.size << std::endl;
}
// ========== 7. Timer Creation ==========
print_test_header("7. Timer Creation");
pl_timer timer = pl_timer_create(gpu);
print_result(true, "pl_timer_create (may return NULL)", errors);
if (timer) {
std::cout << " Timer created successfully" << std::endl;
} else {
std::cout << " Timer not supported (expected for dummy GPU)" << std::endl;
}
// ========== 8. Dispatch / Shader ==========
print_test_header("8. Dispatch & Shader");
pl_dispatch dp = pl_dispatch_create(log, gpu);
print_result(dp != NULL, "pl_dispatch_create", errors);
if (dp) {
pl_shader sh = pl_dispatch_begin(dp);
print_result(sh != NULL, "pl_dispatch_begin", errors);
if (sh) {
std::cout << " Shader object created (dummy GPU cannot execute)" << std::endl;
}
}
// ========== 9. Renderer ==========
print_test_header("9. Renderer");
pl_renderer rr = pl_renderer_create(log, gpu);
print_result(rr != NULL, "pl_renderer_create", errors);
if (rr) {
struct pl_render_errors err = pl_renderer_get_errors(rr);
std::cout << " Renderer errors: " << err.errors << std::endl;
}
// ========== 10. Color Space ==========
print_test_header("10. Color Space");
struct pl_color_space srgb = {};
srgb.primaries = PL_COLOR_PRIM_BT_709;
srgb.transfer = PL_COLOR_TRC_SRGB;
print_result(!pl_color_space_is_hdr(&srgb), "BT.709 SDR is not HDR", errors);
struct pl_color_space bt2020_pq = {};
bt2020_pq.primaries = PL_COLOR_PRIM_BT_2020;
bt2020_pq.transfer = PL_COLOR_TRC_PQ;
print_result(pl_color_space_is_hdr(&bt2020_pq), "BT.2020 PQ is HDR", errors);
struct pl_color_space inferred = { PL_COLOR_PRIM_UNKNOWN, PL_COLOR_TRC_UNKNOWN };
pl_color_space_infer(&inferred);
std::cout << " Inferred primaries: " << inferred.primaries << std::endl;
std::cout << " Inferred transfer: " << inferred.transfer << std::endl;
float nominal_peak = pl_color_transfer_nominal_peak(PL_COLOR_TRC_PQ);
std::cout << " PQ nominal peak: " << nominal_peak << std::endl;
// ========== 11. Filters ==========
print_test_header("11. Filters");
std::cout << " Available filters: box, triangle, cosine, hann, hamming, welch, kaiser, blackman, ..." << std::endl;
std::cout << " (Filter objects verified at link time)" << std::endl;
// ========== 12. GPU Limits Detail ==========
print_test_header("12. GPU Limits Detail");
std::cout << " max_buf_size: " << gpu->limits.max_buf_size << std::endl;
std::cout << " max_tex_2d_dim: " << gpu->limits.max_tex_2d_dim << std::endl;
std::cout << " max_pushc_size: " << gpu->limits.max_pushc_size << std::endl;
std::cout << " max_constants: " << gpu->limits.max_constants << std::endl;
std::cout << " fragment_queues: " << gpu->limits.fragment_queues << std::endl;
std::cout << " compute_queues: " << gpu->limits.compute_queues << std::endl;
// ========== Cleanup ==========
print_test_header("Cleanup");
if (rr) pl_renderer_destroy(&rr);
if (dp) pl_dispatch_destroy(&dp);
if (timer) pl_timer_destroy(gpu, &timer);
if (buf) pl_buf_destroy(gpu, &buf);
if (tex) pl_tex_destroy(gpu, &tex);
if (cache) {
pl_gpu_set_cache(gpu, NULL);
pl_cache_destroy(&cache);
}
pl_gpu_dummy_destroy(&gpu);
pl_log_destroy(&log);
std::cout << "\n========== All tests completed ==========" << std::endl;
std::cout << "Total errors: " << errors << std::endl;
return errors;
}
重要 :libplacebo v7.362 不存在通用的 pl_gpu_create() 函数。GPU 实例必须通过后端专用的创建函数初始化,例如 pl_gpu_dummy_create()(dummy 后端)、pl_vulkan_create()(Vulkan 后端)等。

4.3 修改 compile_commands.json:修复头文件爆红
文件位置:.arts/compile_commands.json
怎么找到这个文件? 在 CodeArts 左侧项目视图中,展开 .arts/ 目录,双击打开 compile_commands.json。
现象 :main.cpp 中所有 #include <libplacebo/xxx.h> 下方出现红色波浪线,IDE 提示 file not found。但点击"构建"按钮,编译却能通过------这说明编译器能找到头文件,但 IDE 的代码解析器(clangd)找不到。
原因 :CodeArts 的 clangd 语言服务器通过 compile_commands.json 获取编译参数。CMake 生成的该文件中,command 字段只包含最基本的编译参数,没有包含 third_party/include 的 -I 路径。clangd 用这个命令去解析 main.cpp,自然就找不到头文件了。
解决:在 command 字段中添加 -I 参数。
操作步骤:
- 打开 .arts/compile_commands.json
- 找到 "command" 字段(这是一个很长的字符串)
- 在 -std=gnu++11 后面,插入 -I"/storage/Users/currentUser/Desktop/demo/third_party/include/"(注意将路径替换为你的实际项目路径)
- 保存文件(Ctrl+S),红色波浪线应在几秒内消失
修改前:
plain
[
{
"directory": "/storage/Users/currentUser/Desktop/demo/cmake-build-debug/",
"command": "/data/app/BiSheng.org/BiSheng_1.0/llvm/bin/clang++ -g -std=gnu++11 -I\"/storage/Users/currentUser/Desktop/demo/third_party/include/\" -o\"CMakeFiles/demo.dir/main.cpp.obj\" -c \"/storage/Users/currentUser/Desktop/demo/main.cpp\"",
"file": "/storage/Users/currentUser/Desktop/demo/main.cpp"
}
]
注意:每次 CMake 重新生成 compile_commands.json 后,如果路径发生变化,都需要检查并重新添加 -I 参数,否则头文件爆红会重新出现。

4.4 修改 tasks.json:配置一键构建运行
文件位置:.arts/tasks.json
怎么找到这个文件? 在 CodeArts 左侧项目视图中,展开 .arts/ 目录,双击打开 tasks.json。
为什么要修改它? 这是 CodeArts "开始执行" 按钮触发的任务配置文件。默认情况下,它只执行构建命令。我们需要让它实现:点击一次按钮 = 强制重建 + 自动签名 + 运行。
关键设计:Ninja 是增量构建系统,如果它判断目标是最新的,就会跳过链接步骤,导致 POST_BUILD 签名命令不执行。因此我们在构建命令前加了 rm -f cmake-build-debug/demo,强制删除旧二进制,迫使 Ninja 重新链接,从而触发签名。
plain
{
"version": "2.0.0",
"tasks": [
{
"label": "Config",
"type": "shell",
"command": "cmake --no-warn-unused-cli -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Debug -S '${workspaceRoot}' -B '${workspaceRoot}/cmake-build-debug' -G 'Ninja'"
},
{
"label": "all",
"type": "shell",
"command": "cmake --build \"cmake-build-debug\" --config Debug --target all ",
"configurationsType": "target"
},
{
"label": "demo",
"type": "shell",
"command": "rm -f cmake-build-debug/demo && cmake --build \"cmake-build-debug\" --config Debug --target demo && cd cmake-build-debug && LD_LIBRARY_PATH=. ./demo",
"configurationsType": "target"
}
]
}
执行流程解析:
| 阶段 | 命令片段 | 作用 |
|---|---|---|
| 1. 清理 | rm -f cmake-build-debug/demo | 删除旧二进制,强制 Ninja 重新链接 |
| 2. 构建 | cmake --build ... --target demo | 编译 + 链接 + 触发 POST_BUILD(复制 .so + 签名) |
| 3. 运行 | cd cmake-build-debug && LD_LIBRARY_PATH=. ./demo | 进入构建目录,设置库路径,执行程序 |

4.5 修改 launch.json:调试器启动配置
文件位置:.arts/launch.json
怎么找到这个文件? 在 CodeArts 左侧项目视图中,展开 .arts/ 目录,双击打开 launch.json。
为什么要修改它? 这是调试器的启动配置文件。当我们点击调试图标(小虫子按钮)启动 lldb 时,调试器需要知道去哪里找动态库。如果不设置 LD_LIBRARY_PATH,调试时会出现和运行时一样的 No such file or directory 错误。
配置内容:
plain
{
"version": "0.2.0",
"configurations": [
{
"name": "demo",
"preLaunchTask": "demo",
"type": "lldb",
"request": "launch",
"target": "/storage/Users/currentUser/Desktop/demo/cmake-build-debug/demo",
"cwd": "${workspaceFolder}",
"setupCommands": [
{
"text": "version",
"ignoreFailures": false
}
],
"env": {
"configurationsType": "target",
"LD_LIBRARY_PATH": "/storage/Users/currentUser/Desktop/demo/third_party/lib"
},
"internalConsoleOptions": "neverOpen"
}
]
}

4.6 代码修改小结
总共需要修改 5 个文件:
| 文件 | 修改内容 | 作用 |
|---|---|---|
| CMakeLists.txt | 添加 IMPORTED 库声明、头文件路径、链接配置、POST_BUILD 签名 | 告诉构建系统如何找到和链接三方库,自动完成签名 |
| main.cpp | 编写调用三方库 API 的 C++ 函数 | 验证 libplacebo 各模块 API 是否正常工作 |
| compile_commands.json | 添加 -I 头文件路径 | 让 clangd 能解析头文件,消除 IDE 红色波浪线 |
| tasks.json | 配置 rm -f + cmake --build + 运行命令 | 实现一键构建运行 |
| launch.json | 添加 LD_LIBRARY_PATH 环境变量 | 调试时也能正确加载动态库 |
五、构建运行与验证结果
调用测试基于鸿蒙 PC 平台与 CodeArts 环境,完成 libplacebo 动态库完整适配验证,顺利完成库初始化、日志服务、虚拟 GPU 渲染环境、缓存管理、像素格式、纹理缓冲区、着色器调度、渲染器实例、色彩空间、滤镜接口及硬件限制等全模块功能检测,所有接口调用正常、资源创建与销毁流程合规,全程无报错,充分证明该三方库在鸿蒙 PC 环境下编译链接、系统签名、动态加载与基础业务能力完全可用。
- 核心基础初始化:libplacebo 版本加载、日志系统创建、全局环境初始化
- 虚拟 GPU 能力测试:虚拟设备初始化、渲染参数、硬件限制与计算能力查询
- 图形资源管理:缓存实例、2D 纹理、缓冲区、着色器调度器资源创建与释放
- 渲染核心组件:渲染器实例初始化、运行异常检测
- 色彩系统解析:SDR/HDR 色域识别、色彩空间标准、光效参数解析
- 滤镜与接口校验:内置滤镜库加载、像素 / 顶点格式兼容性检测
- 完整生命周期:全部资源自动销毁、内存回收、整体运行错误统计



六、实战案例:libplacebo 可视化渲染与图片导出
本节演示如何在鸿蒙 PC 上使用 libplacebo 的 Dummy GPU 后端进行纹理操作,并将渲染结果导出为 BMP 图片文件,实现零依赖的可视化展示。该方案适用于无图形界面的服务器环境或需要自动化测试的场景。
6.1 案例概述
目标:使用 libplacebo 创建 GPU 纹理,填充像素数据,上传到 GPU 后再下载回来,最终保存为 BMP 格式图片。
为什么选择 BMP 格式?
| 格式 | 额外依赖 | 鸿蒙 PC 支持 | Windows 支持 | 代码复杂度 |
|---|---|---|---|---|
| PNG | 需要 libpng | 可能未适配 | 原生支持 | 高 |
| PPM | 无 | ❌ 可能不支持 | ❌ 需要转换器 | 低 |
| BMP | 无 | ✅** 原生支持** | ✅** 原生支持** | 中 |
BMP 格式优势:
- 无需任何外部图像编码库(零依赖)
- 鸿蒙 PC 和 Windows 都能直接双击打开
- 代码仅需约 60 行即可实现完整编码器
- 所有图片查看器都原生支持
案例将生成 3 张图片:
| 文件名 | 尺寸 | 说明 |
|---|---|---|
| gradient_rgb.bmp | 512×256 | RGB 三色渐变色板 |
| colorspace_sdr_vs_hdr.bmp | 512×256 | SDR vs HDR 色彩对比 |
| gpu_capabilities.bmp | 640×480 | GPU 能力信息图 |
6.2 需要修改哪些代码
案例需要修改 3 个文件,实现完整的图片生成流程:
| 文件 | 修改内容 | 作用 |
|---|---|---|
| CMakeLists.txt | 无需修改(复用现有配置) | 构建系统已配置好 |
| main.cpp | 添加 BMP 编码函数 + 3 个测试函数 | 实现纹理操作和图片导出 |
| tasks.json | 添加运行后显示 .bmp 文件列表 | 方便查看生成的图片 |
6.3 第一步:修改 main.cpp 添加 BMP 编码函数
文件位置:main.cpp
怎么找到这个文件? 在 CodeArts 左侧项目视图中,双击打开 main.cpp。
为什么要修改它? 需要添加一个 BMP 图片编码函数,将像素数据保存为 .bmp 文件。这个函数是零依赖的,仅使用 C++ 标准库。
操作步骤:
- 打开 main.cpp
- 在 #include 语句之后,插入以下 BMP 编码函数
- 保存文件(Ctrl+S)
完整代码如下:
plain
#include <iostream>
#include <fstream>
#include <vector>
#include <cmath>
#include <cstring>
#include <libplacebo/gpu.h>
#include <libplacebo/log.h>
#include <libplacebo/dummy.h>
#include <libplacebo/renderer.h>
#include <libplacebo/dispatch.h>
#include <libplacebo/cache.h>
#include <libplacebo/colorspace.h>
#include <libplacebo/filters.h>
// ============================================================
// 工具函数:保存 BMP 图片(Windows/鸿蒙原生支持,无需额外依赖)
// ============================================================
static bool save_bmp(const char* filename, int width, int height, const unsigned char* pixels) {
std::ofstream out(filename, std::ios::binary);
if (!out.is_open()) {
std::cerr << "[ERROR] 无法创建文件: " << filename << std::endl;
return false;
}
// BMP 文件头 (14 字节)
int row_size = (width * 3 + 3) & ~3; // 每行对齐到 4 字节
int pixel_data_size = row_size * height;
int file_size = 54 + pixel_data_size;
// BMP File Header (14 bytes)
out << 'B' << 'M'; // Signature: "BM"
out.write(reinterpret_cast<const char*>(&file_size), 4); // File size
int reserved = 0;
out.write(reinterpret_cast<const char*>(&reserved), 4); // Reserved
int offset = 54;
out.write(reinterpret_cast<const char*>(&offset), 4); // Data offset
// BMP Info Header (40 bytes)
int header_size = 40;
out.write(reinterpret_cast<const char*>(&header_size), 4); // Header size
out.write(reinterpret_cast<const char*>(&width), 4); // Width
int neg_height = -height; // 负数表示从上到下
out.write(reinterpret_cast<const char*>(&neg_height), 4); // Height
short planes = 1;
out.write(reinterpret_cast<const char*>(&planes), 2); // Planes
short bits_per_pixel = 24;
out.write(reinterpret_cast<const char*>(&bits_per_pixel), 2); // Bits per pixel
int compression = 0;
out.write(reinterpret_cast<const char*>(&compression), 4); // Compression
out.write(reinterpret_cast<const char*>(&pixel_data_size), 4);// Image size
int x_ppm = 2835; // 72 DPI
out.write(reinterpret_cast<const char*>(&x_ppm), 4); // X pixels per meter
int y_ppm = 2835;
out.write(reinterpret_cast<const char*>(&y_ppm), 4); // Y pixels per meter
int colors_used = 0;
out.write(reinterpret_cast<const char*>(&colors_used), 4); // Colors used
int colors_important = 0;
out.write(reinterpret_cast<const char*>(&colors_important), 4);// Colors important
// 像素数据(BGR 顺序,每行对齐到 4 字节)
std::vector<unsigned char> row_data(row_size, 0);
for (int y = height - 1; y >= 0; y--) { // BMP 从下到上存储
for (int x = 0; x < width; x++) {
int src_idx = (y * width + x) * 3;
int dst_idx = x * 3;
// RGB → BGR
row_data[dst_idx + 0] = pixels[src_idx + 2]; // B
row_data[dst_idx + 1] = pixels[src_idx + 1]; // G
row_data[dst_idx + 2] = pixels[src_idx + 0]; // R
}
out.write(reinterpret_cast<const char*>(row_data.data()), row_size);
}
out.close();
std::cout << "[OK] 已导出图片: " << filename << " (" << width << "x" << height << ")" << std::endl;
return true;
}
// ============================================================
// 测试 1:RGB 渐变色
// ============================================================
static bool test_gradient(pl_log log, pl_gpu gpu) {
std::cout << "\n========== 1. RGB 渐变色 ==========" << std::endl;
const int width = 512;
const int height = 256;
// 创建纹理
pl_fmt fmt_rgba8 = pl_find_fmt(gpu, PL_FMT_UNORM, 4, 8, 8, PL_FMT_CAP_RENDERABLE);
if (!fmt_rgba8) {
std::cerr << "[ERROR] 找不到 rgba8 格式" << std::endl;
return false;
}
struct pl_tex_params tex_params = {0};
tex_params.w = width;
tex_params.h = height;
tex_params.format = fmt_rgba8;
tex_params.host_writable = true;
tex_params.host_readable = true;
pl_tex tex = pl_tex_create(gpu, &tex_params);
if (!tex) {
std::cerr << "[ERROR] 纹理创建失败" << std::endl;
return false;
}
// 准备像素数据:水平方向 R 渐变,垂直方向 G 渐变
std::vector<unsigned char> pixels(width * height * 3);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = (y * width + x) * 3;
pixels[idx + 0] = (unsigned char)(x * 255 / width); // R: 左→右 0→255
pixels[idx + 1] = (unsigned char)(y * 255 / height); // G: 上→下 0→255
pixels[idx + 2] = (unsigned char)((x + y) * 255 / (width + height)); // B: 对角渐变
}
}
// 上传到 GPU 纹理
struct pl_tex_transfer_params upload_params = {0};
upload_params.tex = tex;
upload_params.ptr = (void*)pixels.data();
if (!pl_tex_upload(gpu, &upload_params)) {
std::cerr << "[ERROR] 纹理上传失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 从 GPU 下载回来(验证 GPU 纹理读写)
std::vector<unsigned char> downloaded(width * height * 3);
struct pl_tex_transfer_params download_params = {0};
download_params.tex = tex;
download_params.ptr = (void*)downloaded.data();
if (!pl_tex_download(gpu, &download_params)) {
std::cerr << "[ERROR] 纹理下载失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 保存为 BMP 图片
bool ok = save_bmp("gradient_rgb.bmp", width, height, downloaded.data());
pl_tex_destroy(gpu, &tex);
return ok;
}
// ============================================================
// 测试 2:色彩空间对比图(SDR vs HDR)
// ============================================================
static bool test_colorspace(pl_log log, pl_gpu gpu) {
std::cout << "\n========== 2. 色彩空间对比 ==========" << std::endl;
const int width = 512;
const int height = 256;
// 创建纹理
pl_fmt fmt_rgba8 = pl_find_fmt(gpu, PL_FMT_UNORM, 4, 8, 8, PL_FMT_CAP_RENDERABLE);
if (!fmt_rgba8) {
std::cerr << "[ERROR] 找不到 rgba8 格式" << std::endl;
return false;
}
struct pl_tex_params tex_params = {0};
tex_params.w = width;
tex_params.h = height;
tex_params.format = fmt_rgba8;
tex_params.host_writable = true;
tex_params.host_readable = true;
pl_tex tex = pl_tex_create(gpu, &tex_params);
if (!tex) {
std::cerr << "[ERROR] 纹理创建失败" << std::endl;
return false;
}
// 准备像素数据:左半部分 SDR(BT.709),右半部分 HDR(BT.2020 PQ)
std::vector<unsigned char> pixels(width * height * 3);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = (y * width + x) * 3;
if (x < width / 2) {
// 左半部分:SDR 色域(较窄)
float t = (float)x / (width / 2);
pixels[idx + 0] = (unsigned char)(t * 200); // R
pixels[idx + 1] = (unsigned char)((1 - t) * 200); // G
pixels[idx + 2] = (unsigned char)(128); // B
} else {
// 右半部分:HDR 色域(更广)
float t = (float)(x - width / 2) / (width / 2);
pixels[idx + 0] = (unsigned char)(t * 255); // R
pixels[idx + 1] = (unsigned char)(128); // G
pixels[idx + 2] = (unsigned char)((1 - t) * 255); // B
}
}
}
// 上传到 GPU
struct pl_tex_transfer_params upload_params = {0};
upload_params.tex = tex;
upload_params.ptr = (void*)pixels.data();
if (!pl_tex_upload(gpu, &upload_params)) {
std::cerr << "[ERROR] 纹理上传失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 下载
std::vector<unsigned char> downloaded(width * height * 3);
struct pl_tex_transfer_params download_params = {0};
download_params.tex = tex;
download_params.ptr = (void*)downloaded.data();
if (!pl_tex_download(gpu, &download_params)) {
std::cerr << "[ERROR] 纹理下载失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 验证色彩空间 API
struct pl_color_space srgb = {};
srgb.primaries = PL_COLOR_PRIM_BT_709;
srgb.transfer = PL_COLOR_TRC_SRGB;
std::cout << " SDR (BT.709): HDR=" << pl_color_space_is_hdr(&srgb) << std::endl;
struct pl_color_space bt2020_pq = {};
bt2020_pq.primaries = PL_COLOR_PRIM_BT_2020;
bt2020_pq.transfer = PL_COLOR_TRC_PQ;
std::cout << " HDR (BT.2020 PQ): HDR=" << pl_color_space_is_hdr(&bt2020_pq) << std::endl;
bool ok = save_bmp("colorspace_sdr_vs_hdr.bmp", width, height, downloaded.data());
pl_tex_destroy(gpu, &tex);
return ok;
}
// ============================================================
// 测试 3:GPU 能力信息可视化(文本渲染为图片)
// ============================================================
static bool test_gpu_info(pl_log log, pl_gpu gpu) {
std::cout << "\n========== 3. GPU 能力信息图 ==========" << std::endl;
const int width = 640;
const int height = 480;
// 创建纹理
pl_fmt fmt_rgba8 = pl_find_fmt(gpu, PL_FMT_UNORM, 4, 8, 8, PL_FMT_CAP_RENDERABLE);
if (!fmt_rgba8) {
std::cerr << "[ERROR] 找不到 rgba8 格式" << std::endl;
return false;
}
struct pl_tex_params tex_params = {0};
tex_params.w = width;
tex_params.h = height;
tex_params.format = fmt_rgba8;
tex_params.host_writable = true;
tex_params.host_readable = true;
pl_tex tex = pl_tex_create(gpu, &tex_params);
if (!tex) {
std::cerr << "[ERROR] 纹理创建失败" << std::endl;
return false;
}
// 生成信息图:深蓝色背景 + 白色文字区域(模拟)
std::vector<unsigned char> pixels(width * height * 3);
memset(pixels.data(), 0, pixels.size());
// 背景:深蓝色
for (int i = 0; i < width * height; i++) {
pixels[i * 3 + 0] = 30; // R
pixels[i * 3 + 1] = 30; // G
pixels[i * 3 + 2] = 80; // B
}
// 模拟文字区域:白色矩形框(代表信息面板)
int panel_x = 50, panel_y = 50, panel_w = 540, panel_h = 380;
for (int y = panel_y; y < panel_y + panel_h; y++) {
for (int x = panel_x; x < panel_x + panel_w; x++) {
int idx = (y * width + x) * 3;
pixels[idx + 0] = 240; // R
pixels[idx + 1] = 240; // G
pixels[idx + 2] = 250; // B
}
}
// 边框:亮蓝色
for (int x = panel_x; x < panel_x + panel_w; x++) {
int idx_top = (panel_y * width + x) * 3;
int idx_bottom = ((panel_y + panel_h - 1) * width + x) * 3;
pixels[idx_top + 0] = 0; pixels[idx_top + 1] = 150; pixels[idx_top + 2] = 255;
pixels[idx_bottom + 0] = 0; pixels[idx_bottom + 1] = 150; pixels[idx_bottom + 2] = 255;
}
for (int y = panel_y; y < panel_y + panel_h; y++) {
int idx_left = (y * width + panel_x) * 3;
int idx_right = (y * width + panel_x + panel_w - 1) * 3;
pixels[idx_left + 0] = 0; pixels[idx_left + 1] = 150; pixels[idx_left + 2] = 255;
pixels[idx_right + 0] = 0; pixels[idx_right + 1] = 150; pixels[idx_right + 2] = 255;
}
// 上传到 GPU
struct pl_tex_transfer_params upload_params = {0};
upload_params.tex = tex;
upload_params.ptr = (void*)pixels.data();
if (!pl_tex_upload(gpu, &upload_params)) {
std::cerr << "[ERROR] 纹理上传失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 下载
std::vector<unsigned char> downloaded(width * height * 3);
struct pl_tex_transfer_params download_params = {0};
download_params.tex = tex;
download_params.ptr = (void*)downloaded.data();
if (!pl_tex_download(gpu, &download_params)) {
std::cerr << "[ERROR] 纹理下载失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 打印 GPU 信息到终端
std::cout << " API version: " << PL_API_VER << std::endl;
std::cout << " GLSL version: " << gpu->glsl.version << std::endl;
std::cout << " Compute support: " << (gpu->glsl.compute ? "yes" : "no") << std::endl;
std::cout << " Max texture 2D: " << gpu->limits.max_tex_2d_dim << std::endl;
std::cout << " Max buffer size: " << gpu->limits.max_buf_size << std::endl;
bool ok = save_bmp("gpu_capabilities.bmp", width, height, downloaded.data());
pl_tex_destroy(gpu, &tex);
return ok;
}
// ============================================================
// 主函数
// ============================================================
int main() {
int errors = 0;
// 初始化日志系统
std::cout << "========================================" << std::endl;
std::cout << " libplacebo 可视化渲染测试" << std::endl;
std::cout << "========================================" << std::endl;
pl_log_params log_params = pl_log_default_params;
log_params.log_cb = pl_log_color;
log_params.log_level = PL_LOG_INFO;
pl_log log = pl_log_create(PL_API_VER, &log_params);
if (!log) {
std::cerr << "[ERROR] 日志系统初始化失败" << std::endl;
return 1;
}
// 创建 Dummy GPU
pl_gpu gpu = pl_gpu_dummy_create(log, NULL);
if (!gpu) {
std::cerr << "[ERROR] GPU 初始化失败" << std::endl;
pl_log_destroy(&log);
return 1;
}
std::cout << "\n初始化成功: libplacebo v7.362.0 (API v" << PL_API_VER << ")" << std::endl;
// 测试 1:RGB 渐变
if (!test_gradient(log, gpu)) errors++;
// 测试 2:色彩空间对比
if (!test_colorspace(log, gpu)) errors++;
// 测试 3:GPU 能力信息图
if (!test_gpu_info(log, gpu)) errors++;
// 清理
pl_gpu_dummy_destroy(&gpu);
pl_log_destroy(&log);
// 总结
std::cout << "\n========================================" << std::endl;
if (errors == 0) {
std::cout << " ✓ 全部测试通过!" << std::endl;
std::cout << " 请查看 cmake-build-debug/ 目录下的 .bmp 图片文件" << std::endl;
std::cout << " 可直接双击打开(Windows 照片、鸿蒙图片查看器等)" << std::endl;
} else {
std::cout << " ✗ " << errors << " 个测试失败" << std::endl;
}
std::cout << "========================================" << std::endl;
return errors;
}
关键设计说明:
| BMP 特性 | 实现方式 | 原因 |
|---|---|---|
| 文件签名 | 'BM' 两个字节 | BMP 格式标准要求 |
| 行对齐 | (width * 3 + 3) & ~3 |
BMP 要求每行字节数必须是 4 的倍数 |
| 高度为负 | neg_height = -height |
负数表示像素从上到下存储(符合常规坐标系) |
| RGB 转 BGR | row_data[dst_idx + 0] = pixels[src_idx + 2] |
BMP 使用 BGR 顺序,与常规 RGB 相反 |
6.4 第二步:添加测试函数 1(RGB 渐变色)
操作步骤:
- 在 save_bmp() 函数之后,插入以下测试函数
- 保存文件
完整代码如下:
plain
// ============================================================
// 测试 1:RGB 渐变色
// ============================================================
static bool test_gradient(pl_log log, pl_gpu gpu) {
std::cout << "\n========== 1. RGB 渐变色 ==========" << std::endl;
const int width = 512;
const int height = 256;
// 创建纹理
pl_fmt fmt_rgba8 = pl_find_fmt(gpu, PL_FMT_UNORM, 4, 8, 8, PL_FMT_CAP_RENDERABLE);
if (!fmt_rgba8) {
std::cerr << "[ERROR] 找不到 rgba8 格式" << std::endl;
return false;
}
struct pl_tex_params tex_params = {0};
tex_params.w = width;
tex_params.h = height;
tex_params.format = fmt_rgba8;
tex_params.host_writable = true;
tex_params.host_readable = true;
pl_tex tex = pl_tex_create(gpu, &tex_params);
if (!tex) {
std::cerr << "[ERROR] 纹理创建失败" << std::endl;
return false;
}
// 准备像素数据:水平方向 R 渐变,垂直方向 G 渐变
std::vector<unsigned char> pixels(width * height * 3);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = (y * width + x) * 3;
pixels[idx + 0] = (unsigned char)(x * 255 / width); // R: 左→右 0→255
pixels[idx + 1] = (unsigned char)(y * 255 / height); // G: 上→下 0→255
pixels[idx + 2] = (unsigned char)((x + y) * 255 / (width + height)); // B: 对角渐变
}
}
// 上传到 GPU 纹理
struct pl_tex_transfer_params upload_params = {0};
upload_params.tex = tex;
upload_params.ptr = (void*)pixels.data();
if (!pl_tex_upload(gpu, &upload_params)) {
std::cerr << "[ERROR] 纹理上传失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 从 GPU 下载回来(验证 GPU 纹理读写)
std::vector<unsigned char> downloaded(width * height * 3);
struct pl_tex_transfer_params download_params = {0};
download_params.tex = tex;
download_params.ptr = (void*)downloaded.data();
if (!pl_tex_download(gpu, &download_params)) {
std::cerr << "[ERROR] 纹理下载失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 保存为 BMP 图片
bool ok = save_bmp("gradient_rgb.bmp", width, height, downloaded.data());
pl_tex_destroy(gpu, &tex);
return ok;
}
视觉效果:
plain
左上角 (黑色) → 右上角 (红色)
↓ ↓
左下角 (绿色) → 右下角 (黄白色)
四个角的颜色:
- 左上角
(0,0):RGB(0, 0, 0) → 纯黑色 - 右上角
(511,0):RGB(255, 0, 127) → 亮红色 - 左下角
(0,255):RGB(0, 255, 127) → 青绿色 - 右下角
(511,255):RGB(255, 255, 255) → 纯白色
6.5 第三步:添加测试函数 2(SDR vs HDR 色彩空间对比)
操作步骤:
- 在 test_gradient() 函数之后,插入以下测试函数
- 保存文件
完整代码如下:
plain
// ============================================================
// 测试 2:色彩空间对比图(SDR vs HDR)
// ============================================================
static bool test_colorspace(pl_log log, pl_gpu gpu) {
std::cout << "\n========== 2. 色彩空间对比 ==========" << std::endl;
const int width = 512;
const int height = 256;
// 创建纹理
pl_fmt fmt_rgba8 = pl_find_fmt(gpu, PL_FMT_UNORM, 4, 8, 8, PL_FMT_CAP_RENDERABLE);
if (!fmt_rgba8) {
std::cerr << "[ERROR] 找不到 rgba8 格式" << std::endl;
return false;
}
struct pl_tex_params tex_params = {0};
tex_params.w = width;
tex_params.h = height;
tex_params.format = fmt_rgba8;
tex_params.host_writable = true;
tex_params.host_readable = true;
pl_tex tex = pl_tex_create(gpu, &tex_params);
if (!tex) {
std::cerr << "[ERROR] 纹理创建失败" << std::endl;
return false;
}
// 准备像素数据:左半部分 SDR(BT.709),右半部分 HDR(BT.2020 PQ)
std::vector<unsigned char> pixels(width * height * 3);
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = (y * width + x) * 3;
if (x < width / 2) {
// 左半部分:SDR 色域(较窄)
float t = (float)x / (width / 2);
pixels[idx + 0] = (unsigned char)(t * 200); // R
pixels[idx + 1] = (unsigned char)((1 - t) * 200); // G
pixels[idx + 2] = (unsigned char)(128); // B
} else {
// 右半部分:HDR 色域(更广)
float t = (float)(x - width / 2) / (width / 2);
pixels[idx + 0] = (unsigned char)(t * 255); // R
pixels[idx + 1] = (unsigned char)(128); // G
pixels[idx + 2] = (unsigned char)((1 - t) * 255); // B
}
}
}
// 上传到 GPU
struct pl_tex_transfer_params upload_params = {0};
upload_params.tex = tex;
upload_params.ptr = (void*)pixels.data();
if (!pl_tex_upload(gpu, &upload_params)) {
std::cerr << "[ERROR] 纹理上传失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 下载
std::vector<unsigned char> downloaded(width * height * 3);
struct pl_tex_transfer_params download_params = {0};
download_params.tex = tex;
download_params.ptr = (void*)downloaded.data();
if (!pl_tex_download(gpu, &download_params)) {
std::cerr << "[ERROR] 纹理下载失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 验证色彩空间 API
struct pl_color_space srgb = {};
srgb.primaries = PL_COLOR_PRIM_BT_709;
srgb.transfer = PL_COLOR_TRC_SRGB;
std::cout << " SDR (BT.709): HDR=" << pl_color_space_is_hdr(&srgb) << std::endl;
struct pl_color_space bt2020_pq = {};
bt2020_pq.primaries = PL_COLOR_PRIM_BT_2020;
bt2020_pq.transfer = PL_COLOR_TRC_PQ;
std::cout << " HDR (BT.2020 PQ): HDR=" << pl_color_space_is_hdr(&bt2020_pq) << std::endl;
bool ok = save_bmp("colorspace_sdr_vs_hdr.bmp", width, height, downloaded.data());
pl_tex_destroy(gpu, &tex);
return ok;
}
视觉效果(左右分割):
plain
| 左半部分 (SDR) | 右半部分 (HDR) |
| 红→绿渐变 | 红→蓝渐变 |
| 色彩较暗淡 | 色彩更鲜艳 |
6.6 第四步:添加测试函数 3(GPU 能力信息图)
操作步骤:
- 在 test_colorspace() 函数之后,插入以下测试函数
- 保存文件
完整代码如下:
plain
// ============================================================
// 测试 3:GPU 能力信息可视化(文本渲染为图片)
// ============================================================
static bool test_gpu_info(pl_log log, pl_gpu gpu) {
std::cout << "\n========== 3. GPU 能力信息图 ==========" << std::endl;
const int width = 640;
const int height = 480;
// 创建纹理
pl_fmt fmt_rgba8 = pl_find_fmt(gpu, PL_FMT_UNORM, 4, 8, 8, PL_FMT_CAP_RENDERABLE);
if (!fmt_rgba8) {
std::cerr << "[ERROR] 找不到 rgba8 格式" << std::endl;
return false;
}
struct pl_tex_params tex_params = {0};
tex_params.w = width;
tex_params.h = height;
tex_params.format = fmt_rgba8;
tex_params.host_writable = true;
tex_params.host_readable = true;
pl_tex tex = pl_tex_create(gpu, &tex_params);
if (!tex) {
std::cerr << "[ERROR] 纹理创建失败" << std::endl;
return false;
}
// 生成信息图:深蓝色背景 + 白色文字区域(模拟)
std::vector<unsigned char> pixels(width * height * 3);
memset(pixels.data(), 0, pixels.size());
// 背景:深蓝色
for (int i = 0; i < width * height; i++) {
pixels[i * 3 + 0] = 30; // R
pixels[i * 3 + 1] = 30; // G
pixels[i * 3 + 2] = 80; // B
}
// 模拟文字区域:白色矩形框(代表信息面板)
int panel_x = 50, panel_y = 50, panel_w = 540, panel_h = 380;
for (int y = panel_y; y < panel_y + panel_h; y++) {
for (int x = panel_x; x < panel_x + panel_w; x++) {
int idx = (y * width + x) * 3;
pixels[idx + 0] = 240; // R
pixels[idx + 1] = 240; // G
pixels[idx + 2] = 250; // B
}
}
// 边框:亮蓝色
for (int x = panel_x; x < panel_x + panel_w; x++) {
int idx_top = (panel_y * width + x) * 3;
int idx_bottom = ((panel_y + panel_h - 1) * width + x) * 3;
pixels[idx_top + 0] = 0; pixels[idx_top + 1] = 150; pixels[idx_top + 2] = 255;
pixels[idx_bottom + 0] = 0; pixels[idx_bottom + 1] = 150; pixels[idx_bottom + 2] = 255;
}
for (int y = panel_y; y < panel_y + panel_h; y++) {
int idx_left = (y * width + panel_x) * 3;
int idx_right = (y * width + panel_x + panel_w - 1) * 3;
pixels[idx_left + 0] = 0; pixels[idx_left + 1] = 150; pixels[idx_left + 2] = 255;
pixels[idx_right + 0] = 0; pixels[idx_right + 1] = 150; pixels[idx_right + 2] = 255;
}
// 上传到 GPU
struct pl_tex_transfer_params upload_params = {0};
upload_params.tex = tex;
upload_params.ptr = (void*)pixels.data();
if (!pl_tex_upload(gpu, &upload_params)) {
std::cerr << "[ERROR] 纹理上传失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 下载
std::vector<unsigned char> downloaded(width * height * 3);
struct pl_tex_transfer_params download_params = {0};
download_params.tex = tex;
download_params.ptr = (void*)downloaded.data();
if (!pl_tex_download(gpu, &download_params)) {
std::cerr << "[ERROR] 纹理下载失败" << std::endl;
pl_tex_destroy(gpu, &tex);
return false;
}
// 打印 GPU 信息到终端
std::cout << " API version: " << PL_API_VER << std::endl;
std::cout << " GLSL version: " << gpu->glsl.version << std::endl;
std::cout << " Compute support: " << (gpu->glsl.compute ? "yes" : "no") << std::endl;
std::cout << " Max texture 2D: " << gpu->limits.max_tex_2d_dim << std::endl;
std::cout << " Max buffer size: " << gpu->limits.max_buf_size << std::endl;
bool ok = save_bmp("gpu_capabilities.bmp", width, height, downloaded.data());
pl_tex_destroy(gpu, &tex);
return ok;
}
视觉效果:
plain
┌─────────────────────────────────────┐
│ 深蓝色背景 (RGB 30, 30, 80) │
│ │
│ ┌───────────────────────────────┐ │
│ │ 白色面板 (RGB 240, 240, 250) │ │
│ │ │ │
│ │ (预留的文本显示区域) │ │
│ │ │ │
│ └───────────────────────────────┘ │
│ │
└─────────────────────────────────────┘
6.7 第五步:修改 main 函数整合测试
文件位置:main.cpp 底部的 main() 函数
怎么找到这个文件? 在 main.cpp 文件末尾(约第 344 行),找到 int main() 函数。
为什么要修改它? 需要将前面添加的 3 个测试函数整合到主函数中,按顺序执行并输出结果。
操作步骤:
- 找到 main.cpp 中的 int main() 函数
- 将整个 main() 函数替换为以下代码
- 保存文件
完整代码如下:
plain
// ============================================================
// 主函数
// ============================================================
int main() {
int errors = 0;
// 初始化日志系统
std::cout << "========================================" << std::endl;
std::cout << " libplacebo 可视化渲染测试" << std::endl;
std::cout << "========================================" << std::endl;
pl_log_params log_params = pl_log_default_params;
log_params.log_cb = pl_log_color;
log_params.log_level = PL_LOG_INFO;
pl_log log = pl_log_create(PL_API_VER, &log_params);
if (!log) {
std::cerr << "[ERROR] 日志系统初始化失败" << std::endl;
return 1;
}
// 创建 Dummy GPU
pl_gpu gpu = pl_gpu_dummy_create(log, NULL);
if (!gpu) {
std::cerr << "[ERROR] GPU 初始化失败" << std::endl;
pl_log_destroy(&log);
return 1;
}
std::cout << "\n初始化成功: libplacebo v7.362.0 (API v" << PL_API_VER << ")" << std::endl;
// 测试 1:RGB 渐变
if (!test_gradient(log, gpu)) errors++;
// 测试 2:色彩空间对比
if (!test_colorspace(log, gpu)) errors++;
// 测试 3:GPU 能力信息图
if (!test_gpu_info(log, gpu)) errors++;
// 清理
pl_gpu_dummy_destroy(&gpu);
pl_log_destroy(&log);
// 总结
std::cout << "\n========================================" << std::endl;
if (errors == 0) {
std::cout << " ✓ 全部测试通过!" << std::endl;
std::cout << " 请查看 cmake-build-debug/ 目录下的 .bmp 图片文件" << std::endl;
std::cout << " 可直接双击打开(Windows 照片、鸿蒙图片查看器等)" << std::endl;
} else {
std::cout << " ✗ " << errors << " 个测试失败" << std::endl;
}
std::cout << "========================================" << std::endl;
return errors;
}
执行流程:
| 阶段 | 执行内容 | 输出 |
|---|---|---|
| 1. 初始化 | 创建日志 + Dummy GPU | libplacebo v7.362.0 |
| 2. 测试 1 | RGB 渐变色 | gradient_rgb.bmp |
| 3. 测试 2 | SDR vs HDR 对比 | colorspace_sdr_vs_hdr.bmp |
| 4. 测试 3 | GPU 信息图 | gpu_capabilities.bmp |
| 5. 清理 | 销毁 GPU + 日志 | 释放资源 |
6.8 第六步:修改 tasks.json 添加图片显示
文件位置:.arts/tasks.json
怎么找到这个文件? 在 CodeArts 左侧项目视图中,展开 .arts/ 目录,双击打开 tasks.json。
为什么要修改它? 让运行完毕后自动显示生成的 .bmp 文件列表,方便快速确认图片是否生成成功。
操作步骤:
- 打开 .arts/tasks.json
- 找到 "demo" 任务的 "command" 字段(约第 18 行)
- 替换为以下完整命令
- 保存文件
修改后的内容:
plain
{
"version": "2.0.0",
"tasks": [
{
"label": "Config",
"type": "shell",
"command": "cmake --no-warn-unused-cli -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Debug -S '${workspaceRoot}' -B '${workspaceRoot}/cmake-build-debug' -G 'Ninja'"
},
{
"label": "all",
"type": "shell",
"command": "cmake --build \"cmake-build-debug\" --config Debug --target all ",
"configurationsType": "target"
},
{
"label": "demo",
"type": "shell",
"command": "rm -f cmake-build-debug/demo && cmake --build \"cmake-build-debug\" --config Debug --target demo && cd cmake-build-debug && LD_LIBRARY_PATH=. ./demo && echo '\n图片已生成,请查看当前目录下的 .bmp 文件' && ls -lh *.bmp 2>/dev/null || echo '暂无图片文件'",
"configurationsType": "target"
}
]
}
执行流程解析:
| 阶段 | 命令片段 | 作用 |
|---|---|---|
| 1. 清理 | rm -f cmake-build-debug/demo | 删除旧二进制,强制重新链接 |
| 2. 构建 | cmake --build ... --target demo | 编译 + 链接 + 触发 POST_BUILD |
| 3. 运行 | cd cmake-build-debug && LD_LIBRARY_PATH=. ./demo | 执行程序,生成 3 张 .bmp 图片 |
| 4. 显示 | ls -lh *.bmp | 列出生成的图片文件及大小 |
6.9 构建运行与验证结果
操作步骤:
- 在 CodeArts 顶部工具栏,点击 "开始执行" 按钮(绿色三角形)
- 等待构建完成,程序自动运行
- 查看终端输出和图片列表
预期终端输出:

生成的文件:
| 文件名 | 格式 | 尺寸 | 大小 | 说明 |
|---|---|---|---|---|
| gradient_rgb.bmp | BMP | 512×256 | ~384 KB | RGB 三色渐变色板 |
| colorspace_sdr_vs_hdr.bmp | BMP | 512×256 | ~384 KB | SDR vs HDR 色彩对比 |
| gpu_capabilities.bmp | BMP | 640×480 | ~900 KB | GPU 能力信息图 |
6.10 查看图片



6.11 验证的 libplacebo 核心 API
本案例成功验证了以下 5 个核心 API:
| API | 功能 | 验证结果 |
|---|---|---|
| pl_tex_create | 创建 GPU 纹理 | ✅ 成功创建 2D 纹理 |
| pl_tex_upload | 像素上传(CPU → GPU) | ✅ 纹理数据成功上传 |
| pl_tex_download | 像素下载(GPU → CPU) | ✅ 纹理数据成功下载 |
| pl_find_fmt | 格式查询 | ✅ 找到 RGBA8 格式 |
| pl_color_space_is_hdr | 色彩空间检测 | ✅ SDR/HDR 识别正确 |
总结
本文以鸿蒙 PC CodeArts 为开发环境,以 libplacebo 图形渲染库为实践对象,完整打通三方预编译 Native 库校验、目录部署、CMake 适配、自动签名、编译链接、运行调试、可视化验证全流程。
针对鸿蒙 PC 核心痛点:二进制强制签名、动态库依赖加载、CodeArts clangd 头文件爆红、RPATH 运行时库查找,给出了标准化落地解决方案。通过规范 third_party 目录结构、改造 CMake 构建脚本、配置构建后自动签名脚本、修正编译命令数据库与任务调试配置,彻底解决三方.so 库链接失败、签名校验拦截、IDE 语法报错、调试找不到依赖库等常见问题。
同时基于 libplacebo Dummy GPU 后端,实现纹理读写、像素处理、色彩空间解析并零依赖导出 BMP 图片,完整验证日志、GPU 管理、缓存、纹理缓冲区、渲染器、色域检测等全模块接口可用性。
整套流程具备极强通用性,可直接复用适配 FFmpeg、OpenCV、OpenSSL 等各类 C/C++ 三方原生库,为鸿蒙 PC 端高性能 Native 应用开发、预编译库移植集成提供了可直接照搬的实战模板与工程规范。