鸿蒙 PC CodeArts 实战:libplacebo 三方 Native 库编译、签名与调用全流程

摘要:针对鸿蒙 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 个核心问题:

  1. 头文件在哪 → 编译器能找到 #include <libplacebo/xxx.h>
  2. .so 文件在哪 → 链接器能解析符号,生成可执行文件
  3. 运行时怎么找 .so → 动态链接器能通过 RPATH 或 LD_LIBRARY_PATH 找到库
  4. 签名怎么自动完成 → 每次构建后自动调用 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 存在即可

为什么一定要检查?

  1. 依赖缺失:如果 NEEDED 中有非系统库(比如另一个三方库 libshaderc.so),你只复制了 libplacebo.so,运行时会报 No such file or directory
  2. 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 管理编译流程,我们需要在其中声明:

  1. 三方库的头文件在哪里(target_include_directories)
  2. 三方库的 .so 文件在哪里(target_link_libraries)
  3. 运行时去哪里找 .so(BUILD_RPATH)
  4. 构建后自动复制和签名 .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 参数。

操作步骤

  1. 打开 .arts/compile_commands.json
  2. 找到 "command" 字段(这是一个很长的字符串)
  3. 在 -std=gnu++11 后面,插入 -I"/storage/Users/currentUser/Desktop/demo/third_party/include/"(注意将路径替换为你的实际项目路径)
  4. 保存文件(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++ 标准库。

操作步骤

  1. 打开 main.cpp
  2. 在 #include 语句之后,插入以下 BMP 编码函数
  3. 保存文件(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 渐变色)

操作步骤

  1. 在 save_bmp() 函数之后,插入以下测试函数
  2. 保存文件

完整代码如下

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 色彩空间对比)

操作步骤

  1. 在 test_gradient() 函数之后,插入以下测试函数
  2. 保存文件

完整代码如下

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 能力信息图)

操作步骤

  1. 在 test_colorspace() 函数之后,插入以下测试函数
  2. 保存文件

完整代码如下

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 个测试函数整合到主函数中,按顺序执行并输出结果。

操作步骤

  1. 找到 main.cpp 中的 int main() 函数
  2. 将整个 main() 函数替换为以下代码
  3. 保存文件

完整代码如下

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 文件列表,方便快速确认图片是否生成成功。

操作步骤

  1. 打开 .arts/tasks.json
  2. 找到 "demo" 任务的 "command" 字段(约第 18 行)
  3. 替换为以下完整命令
  4. 保存文件

修改后的内容

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 构建运行与验证结果

操作步骤

  1. 在 CodeArts 顶部工具栏,点击 "开始执行" 按钮(绿色三角形)
  2. 等待构建完成,程序自动运行
  3. 查看终端输出和图片列表

预期终端输出

生成的文件

文件名 格式 尺寸 大小 说明
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 应用开发、预编译库移植集成提供了可直接照搬的实战模板与工程规范。

相关推荐
特立独行的猫a3 小时前
鸿蒙PC的包管理工具 Homebrew 正式上线,Harmonybrew介绍及使用指南
华为·harmonyos·homebrew·鸿蒙pc·harmonybrew
wei_shuo3 小时前
从零开始写 HPKBUILD:以 MediaInfo 为例的三方库鸿蒙PC适配实战
鸿蒙pc·三方库适配
wei_shuo3 小时前
[鸿蒙三方库适配实战] 高性能视频渲染库 libplacebo 的 鸿蒙PC 平台迁移实践
鸿蒙pc·三方库适配
wei_shuo3 天前
Windows 鸿蒙 PC 应用开发:DevEco Studio 集成与调用三方 Native 库实战指南
鸿蒙·鸿蒙pc·三方库适配
特立独行的猫a6 天前
鸿蒙 PC 命令行工具迁移实战直播课 · pngquant命令行移植实战
华为·ai·harmonyos·vcpkg·鸿蒙pc·lycim
特立独行的猫a7 天前
鸿蒙 PC 命令行工具迁移实战 · 直播PPT
android·华为·harmonyos·vcpkg·三方库移植·鸿蒙pc
特立独行的猫a7 天前
鸿蒙 PC 三方库移植实战 · 直播课件(详细教案)
华为·harmonyos·移植·鸿蒙pc·opendesk
特立独行的猫a9 天前
鸿蒙 PC 命令行工具迁移实战 · 内部课件(详细配套版)
华为·harmonyos·移植·鸿蒙pc
特立独行的猫a12 天前
HarmonyOS / OpenHarmony 鸿蒙PC平台三方库移植:AI自动化编译框架build_in_harmonyos介绍及使用
人工智能·自动化·harmonyos·三方库移植·鸿蒙pc·opendesk