diff --git a/docs/zh/plans/README.md b/docs/zh/plans/README.md index 2eba46592..065fa2f25 100644 --- a/docs/zh/plans/README.md +++ b/docs/zh/plans/README.md @@ -8,6 +8,7 @@ | 文档 | 内容 | 启动前提 | |---|---|---| | [`riir.md`](riir.md) | **RIIR 绞杀者模式执行计划**:C ABI 迁移完成后,把 liboakengine 安全拆成若干小模块,再逐个用 Rust 重写;含 API 冻结保证、模块图、六步流程与验证门禁 | C ABI 迁移战役验收完成 | +| [`riir/`](riir/) | **模块拆分执行手册**(riir.md 第一阶段落地):00 总览 → 01 双层适配器规范 → 02 依赖矩阵与拆分顺序 → 03 测试规范 → M1-M9 逐模块手册(C API 冻结);只拆分不重写,模块间 C ABI | R7 完成后启动(可与 R7 并行准备) | | [`ai-agent-design.md`](ai-agent-design.md) | **AI Agent 设计**:多模态 LLM 经 MCP 调用策展工具面自动剪辑,渲染帧回喂形成"编辑→看图→再编辑"视觉闭环;含工具面、回放回路、安全与测试 | RIIR 拆分完成(面对一堆小库) | | [`gtest-migration-guide.md`](gtest-migration-guide.md) | **测试统一到 Google Test**:把 OAK_ADD_TEST 宏框架、纯 C assert、已有 gtest 三套收敛为单一 Google Test,ctest 仅作运行器 | R5 验收完成后启动(可与 UI 改版并行) | | [`ui-redesign-plan.md`](ui-redesign-plan.md) | **主界面 UI 改版**:依据 `design/` 三张设计图落地 10 个工作包(工具条、双监看、效果栈检查器、节点编辑器移位、电平条、状态栏等),全部文字精确定义 | R5 验收完成后启动(可与 GTest 迁移并行) | diff --git a/docs/zh/plans/riir/00-overview.md b/docs/zh/plans/riir/00-overview.md new file mode 100644 index 000000000..077ca31c2 --- /dev/null +++ b/docs/zh/plans/riir/00-overview.md @@ -0,0 +1,59 @@ +# RIIR 模块拆分计划 · 00 总览 + +> 本目录是 liboakengine 的**模块拆分**执行计划:把单个 +> liboakengine.so 拆成一组小库,模块之间只经 C ABI 调用。 +> **只拆分,不重写**——这是 riir.md 绞杀者路线的第一阶段,拆分完成 +> 并稳定后,才逐模块用 Rust 重写(届时模块的 C ABI 原样保留,Rust +> 实现替换 C++ 实现对调用方透明)。 +> +> 阅读顺序:`00`(本文)→ `01-adapter-pattern.md`(适配器规范, +> 所有模块共用)→ `02-modules-and-order.md`(模块清单、依赖矩阵、 +> 拆分顺序)→ `03-testing.md`(测试规范)→ `M1`…`M9`(逐模块执行 +> 手册,**C API 已在各手册中冻结**)。 + +## 目标与判据 + +终态: + +``` +oakcore(已有,不动) +oakcommon ─ oakundo ─ oaknode ─ oaktimeline ─ oakcodec ─ oakrender ─ oaktask ─ oakplugin + │ + oakaudio ───────────────────┤ + ▼ + liboakengine(= facade + coreengine,纯装配层) +``` + +完成判据(每条都可命令验证): + +1. 每个模块是独立 CMake 目标(`add_library(oak STATIC|SHARED ...)`), + 有自己的 `include/` 公共头目录;模块 A 链接模块 B 时**只包含** B 的 + `include/oak/` 下的头,不包含 B 的私有头。 +2. 模块间调用 100% 经 C ABI(`extern "C"`,见 01)。验证: + `nm -D --defined-only liboak.so | grep -c " T _Z"` = 0(不导出 + C++ 符号),且消费方 `nm -D | grep " U _ZN5olive"` = 0。 +3. 全量构建 0 error;全量 ctest 绿(45+,随模块测试增加只增不减)。 +4. 每个模块的每个 C API 有 Google Test 覆盖(规范见 03)。 + +## 铁律 + +1. **只拆不写**:除边界适配器(01)和必要的反向依赖切割外,不改任何 + 函数实现、不改行为。每个模块拆完,ctest 必须保持全绿。 +2. **C API 先冻结后实现**:每个模块手册(M1-M9)里的 C API 表就是 + 契约,实现不得偏离;执行中确需调整的,先改手册再改代码,并在手册 + 里标注修订记录。 +3. **qmake 式渐进**:一次只拆一个模块,闭环(构建+ctest+nm)后提交, + 再开下一个。顺序见 02,按依赖叶先根后。 +4. 三条红线沿用(禁 inline 化、禁 stub、禁 dlsym)。 +5. Qt 依赖:模块可继续用 Qt(拆分不是去 Qt),但 C ABI 边界上不许 + 出现 Qt 类型(QString/QVariant/QList/信号槽),全部 POD 化 + 回调 + (见 01 §4)。 + +## 术语 + +- **提供侧(callee)**:被调用的模块,按 01 §1 实现 C API。 +- **消费侧(caller)**:调用方模块,按 01 §2 用同名 C++ 适配类包住 + C API,使本模块内原有调用点代码**零改动**。 +- **跨界类**:被其他模块消费的类(进入该模块的 C API 表面)。 +- **切割点**:阻碍模块独立的反向/环依赖 include,逐条在模块手册里 + 列出并给出处理方式。 diff --git a/docs/zh/plans/riir/01-adapter-pattern.md b/docs/zh/plans/riir/01-adapter-pattern.md new file mode 100644 index 000000000..f6e8eed69 --- /dev/null +++ b/docs/zh/plans/riir/01-adapter-pattern.md @@ -0,0 +1,178 @@ +# 01 · 双层适配器模式(所有模块共用规范) + +> 本规范是拆分能在"只拆不写"约束下成立的核心机制。任何模块的 +> C API 设计与适配类实现都必须照此执行。命名、内存所有权、错误码、 +> 线程与信号的处理在此**冻结**。 + +## 1. 提供侧:C API 层 + +对被消费的每个 C++ 类 `Clazz`,模块在 `include/oak/clazz.h` +里暴露一组 `extern "C"` 函数: + +```c +/* 构造/析构 */ +OAKMOD_API OakModClazz *oakmod_clazz_init(/* 与某个构造函数对应的参数 */); +OAKMOD_API void oakmod_clazz_free(OakModClazz *self); + +/* 普通成员函数:self 为第一参数,其余参数按 §3 POD 化 */ +OAKMOD_API oakmod_clazz_(OakModClazz *self, ...); + +/* 静态成员函数:无 self */ +OAKMOD_API oakmod_clazz__s(/* 参数 */); +``` + +规则: + +1. `OakModClazz` 是**不透明类型**(`typedef struct OakModClazz OakModClazz;`), + 提供侧实现里它就是 `olive::Clazz*`(`reinterpret_cast`),消费侧 + 永远无法解引用。 +2. `init` 每个对应一个实际在用的构造函数重载;返回 NULL 表示失败。 + `free` 对 NULL 是 no-op。**所有权:init 创建的对象归调用方,必须 + 配对 free**;借用指针(不转移所有权)在文档注释里写 `/* borrowed */`。 +3. 多个构造重载用后缀区分:`oakmod_clazz_init`(默认)、 + `oakmod_clazz_init_from_file`、`oakmod_clazz_init_copy` 等。 +4. 命名全小写,模块前缀 `oak_`(oakundo/oaknode/oaktimeline/ + oakcodec/oakrender/oaktask/oakaudio/oakplugin/oakcommon)。 +5. 导出宏 `OAKMOD_API` 照 `oakengine/export.h` 样式 + (`__attribute__((visibility("default")))`),模块编译加 + `-fvisibility=hidden`——每个模块**出生即 visibility 干净**, + R7-B 的全局收口变成顺水推舟。 + +## 2. 消费侧:同名 C++ 适配类 + +消费模块里放一个与原始类**同名**的适配类(放在消费侧私有头 +`/adapter/clazz.h`,namespace 保持 `olive`): + +```cpp +namespace olive { + +class Clazz { +public: + Clazz(/* 原构造签名 */) + : h_(oakmod_clazz_init(/* 相同参数 */)) {} + ~Clazz() { oakmod_clazz_free(h_); } + + // 原成员函数签名原样保留,体内转发一行 + Ret func(Arg a) { return oakmod_clazz_func(h_, a); } + + // 句柄逃生口(确实需要混用时的过渡手段,注释标注) + OakModClazz *handle() const { return h_; } + +private: + OakModClazz *h_; + // 拷贝语义与原类一致:原类不可拷贝就 = delete; + // 原类可拷贝则 init_copy。 +}; + +} +``` + +效果:消费模块里所有 `clazz.func(a)`、`new Clazz(...)`、 +`clazz->func(a)` 调用点**零改动**——原来 include 提供侧 C++ 头的 +地方,改 include 本模块的适配头即可。这就是"双层适配器": +**C++ 调用点 → 同名适配类 → C ABI → 提供侧 C++ 实现**。 + +注意: + +- 适配类是**逐模块私有的**:oakrender 消费的 Node 适配类和 + oaktimeline 消费的 Node 适配类是两份独立的小头文件,各自只有 + 自己用到的方法子集。允许重复,禁止共享(共享就重新耦合了)。 +- 适配类只转发**本模块实际用到**的方法(按 02 的依赖矩阵逐个 + grep 确认清单,写在模块手册里)。 +- 原类有继承体系的(如 UndoCommand 子类族),见 §5。 + +## 3. 参数与返回值的 POD 化 + +C ABI 上只允许:整数、`double`、`int64_t`、指针、`const char *`、 +以及 `include/oak/types.h` 里定义的纯 C POD(无构造/析构/ +方法)。Qt/C++ 类型按下表映射: + +| C++ 类型 | C ABI 形态 | +|---|---| +| `QString` | `const char *`(入,UTF-8);出:buf/size 两段式(先 NULL 查长度) | +| `olive::core::Rational` | 两个 `int64_t`(num, den);时间戳用 `int64_t` 帧戳 | +| `TimeRange` | 两个 `int64_t`(in_ts, out_ts) | +| `QVariant`/`NodeValue` | `oak_node_value` POD(oakengine/node.h 已有,各模块复用该定义或复制同构 POD) | +| `VideoParams`/`AudioParams` | `oak_video_params`/POD 字段拍平 | +| `QList`/`QVector` | 指针 + count;返回集合用 `count` + `at(i)` 访问器对 | +| `Color` | 4 × double | +| `Qt::enum`/内部枚举 | `int`(取值表写进手册,两侧枚举**序数一致性**用 static_assert 或测试钉死) | +| `std::shared_ptr` | 不透明句柄 + retain/free(协议见 display.h R7-A 的先例) | + +## 4. 信号、回调与线程 + +Qt 信号不许跨模块。处理优先级: + +1. **回调注册**:`oakmod_clazz_set__cb(self, fn, userdata)`, + 提供侧在发信号处调 `fn(event_pod, userdata)`。userdata 所有权归 + 注册方,适配类析构时先 `set_*_cb(self, NULL, NULL)` 反注册。 +2. **事件总线**:模块级通知(非单对象)用 + `oakmod_subscribe(event_id, fn, userdata)` → 返回订阅 id, + `oakmod_unsubscribe(id)`——照 `oakengine/events.h` 的现成模式。 +3. 线程语义照现状:提供侧在发射线程同步调回调(DirectConnection + 等价),需要跨线程排队是消费侧适配类自己的事(它可以用 + `QMetaObject::invokeMethod(..., Qt::QueuedConnection)`)。 +4. **适配类可以把 C 回调再转回 Qt 信号**:适配类继承 QObject、 + 静态 trampoline 里 `emit` 同名信号——消费侧原有 `connect()` 全部 + 零改动。这是大多数 widget 侧适配的默认做法。 + +## 5. 继承与虚函数 + +原类的继承体系**不出模块**。跨界时: + +- 基类引用(如 `UndoCommand *`)→ 基类句柄(`OakUndoCommand *`)。 +- 消费侧不构造具体子类,只经提供侧的工厂函数: + `oakmod_clazz_create_(...)`(返回基类句柄)。 +- 消费侧确实需要子类行为的(极少数,需逐个人工论证),在提供侧 + 加专用函数,不放虚函数跨界。 + +## 6. 错误码 + +沿用 facade 约定:`OAKMOD_OK=0`,负值错误 +(`OAKMOD_E_INVALID/E_STATE/E_NOT_FOUND/E_FAILED`),在各模块 +`types.h` 里统一定义(值与 oakengine 的现有值对齐)。会失败的 +`init` 返回 NULL,错误细节经 `oakmod_last_error(buf, size)` 取 +(线程局部,照 facade 的 `set_error` 模式)。 + +## 7. 一个完整例子(UndoStack,M2 照此落地) + +```c +/* oakundo/include/oakundo/undostack.h */ +typedef struct OakUndoStack OakUndoStack; +OAKUNDO_API OakUndoStack *oakundo_undostack_init(void *parent_qobject); +OAKUNDO_API void oakundo_undostack_free(OakUndoStack *self); +OAKUNDO_API void oakundo_undostack_push(OakUndoStack *self, + OakUndoCommand *cmd, const char *name); +OAKUNDO_API int oakundo_undostack_can_undo(const OakUndoStack *self); +/* ... 完整表见 M2 手册 ... */ +OAKUNDO_API int64_t oakundo_undostack_subscribe(OakUndoStack *self, + int event_id, oakundo_event_fn fn, void *userdata); +OAKUNDO_API void oakundo_unsubscribe(int64_t id); +``` + +```cpp +// 消费侧 adapter/undostack.h —— app/other 模块里 connect() 零改动 +class UndoStack : public QObject { + Q_OBJECT +public: + explicit UndoStack(QObject *p = nullptr) + : QObject(p), h_(oakundo_undostack_init(p)) { + sub_ = oakundo_undostack_subscribe(h_, OAKUNDO_EVENT_INDEX_CHANGED, + &UndoStack::tramp, this); + } + ~UndoStack() override { oakundo_unsubscribe(sub_); oakundo_undostack_free(h_); } + void push(UndoCommand *c, const QString &n) { + oakundo_undostack_push(h_, c->handle(), n.toUtf8().constData()); + } + bool canUndo() const { return oakundo_undostack_can_undo(h_) != 0; } +signals: + void index_changed(int); +private: + static void tramp(int event_id, int64_t a, int64_t b, void *ud) { + if (event_id == OAKUNDO_EVENT_INDEX_CHANGED) + emit static_cast(ud)->index_changed(int(a)); + } + OakUndoStack *h_; + int64_t sub_; +}; +``` diff --git a/docs/zh/plans/riir/02-modules-and-order.md b/docs/zh/plans/riir/02-modules-and-order.md new file mode 100644 index 000000000..e7e632531 --- /dev/null +++ b/docs/zh/plans/riir/02-modules-and-order.md @@ -0,0 +1,88 @@ +# 02 · 模块清单、依赖矩阵与拆分顺序 + +> 本文基于 2026-07-26 对 engine/ 全量 490 个源文件的 include 扫描 +> (方法:按 include 目标首段归类计数)。每个模块手册(M1-M9)里的 +> 切割点清单都出自这张表。 + +## 1. 依赖矩阵(include 次数,空格=0) + +| from\to | audio | codec | common | config | node | plugin | render | task | timeline | undo | core | +|---|---|---|---|---|---|---|---|---|---|---|---| +| audio | | 1 | 5 | 2 | | | 2 | | | | 5 | +| codec | | | 12 | 1 | 3 | | 11 | 5 | | | 4 | +| common | | 1 | | 1 | 3 | 1 | 7 | | | 1 | 2 | +| config | | 1 | 4 | | 1 | | | | 1 | | | +| node | 4 | 8 | 31 | 10 | | 3 | 47 | | 5 | 4 | 4 | +| pluginSupport | | | 6 | | 6 | | 6 | | | 2 | 2 | +| render | 3 | 9 | 24 | 6 | 38 | 5 | | 2 | | 1 | 8 | +| task | | 4 | 4 | 2 | 38 | | 4 | | 1 | | | +| timeline | | | 4 | 2 | 32 | | | | | 1 | 3 | +| undo | | | 2 | | 1 | | | | | | | +| src(capi) | 9 | 13 | 1 | 3 | 110 | 3 | 48 | 12 | 16 | 14 | 3 | + +已知分层违规 1 处:`render/` 引用了 `src/capi/displayinternal.h` +(R7-A 重做 display.h 时一并消除)。 + +## 2. 模块定义与拆分顺序 + +顺序原则:叶子先、根后;每步只引入"已拆模块的 C ABI",不引入 +"未拆模块的 C++ 头"。 + +| 序 | 模块 | 内容(engine/ 下目录) | 主要切割点 | +|---|---|---|---| +| M0 | oakcore | `core/`(已完成,不动) | — | +| M1 | oakcommon | `common/`(41 文件工具集)+ `config/` | common→render/node/undo/codec/plugin 的 12 次反向 include(清单见 M1 §3) | +| M2 | oakundo | `undo/`(undocommand/undostack) | undo→node/project.h 1 处(M2 §3) | +| M3 | oaknode | `node/`(图、工厂、keyframe、nodeundo、traverser) | node→render 47、node→codec 8、node→timeline 5、node→audio 4、node→undo 4(M3 §3,最大的活) | +| M4 | oaktimeline | `timeline/`(marker/workarea/timeline undo 命令族/timelinecommon) | timeline→node 32(经 oaknode C ABI + 适配类) | +| M5 | oakcodec | `codec/`(decoder/encoder/frame/proxy/conform) | codec→render 11(videoparams 等随 M3.5 下沉)、codec→task 5、codec→node 3 | +| M6 | oakaudio | `audio/`(AudioManager/AudioProcessor/输出) | audio→render 2、audio→codec 1 | +| M7 | oakrender | `render/`(Renderer/PlaybackCache/ColorManager/帧缓存/job) | render→node 38、render→codec 9、render→task 2、render→undo 1、render→src 1(违规) | +| M8 | oaktask | `task/`(Task/TaskManager/项目 load/save/import/OTIO) | task→node 38、task→codec 4、task→render 4 | +| M9 | oakplugin | `pluginSupport/`(OpenFX host) | plugin→node 6、plugin→render 6、plugin→undo 2、plugin→coreengine 2 | +| — | liboakengine | `src/capi` + `coreengine` + `tool/` + `ui/` 残余 | 纯装配层:facade 内部调用改经各模块 C ABI(或保持现状直接链,见 M9 §4 裁决) | + +**M3.5(伴随 M3 的类型下沉)**:`render/videoparams.h`、 +`render/subtitleparams.h`、`render/colortransform.h` 是纯数据类型, +codec/node 都重度引用——下沉到 **oakcommon**(或独立 oakmedia 目录, +执行时二选一,默认并 oakcommon),切断 codec→render 的大头。 + +## 3. 依赖环处理总表 + +| 环 | 数据 | 处理 | +|---|---|---| +| node ↔ render | 47/38 | M3 时 node 侧 47 次引用经 oakrender **尚未存在**——因此 M3 拆分时 node→render 的引用先经"前向 C ABI"处理:把 node 用到的 render 类(ColorProcessor/RenderManager/footagejob/pluginjob/videoparams)的 C API 定义在 **oaknode 手册里但由 M7 实现**?**否**——正确顺序见 §4 说明 | +| node ↔ timeline | 5/32 | node→timeline 5 次(timelinecommon×2、marker/workarea/timelineundogeneral 各1):枚举/常量头下沉 oakcommon,其余经 M4 反向 C ABI | +| node ↔ codec | 8/3 | node→codec 8(decoder/frame/encoder/proxymanager):M5 反向 C ABI | +| node ↔ audio | 4/4 | M6 反向 C ABI | +| render ↔ codec | 9/11 | M3.5 类型下沉后剩 ~3(renderer.h/framemanager.h),M5 时处理 | +| codec ↔ task | 5/4 | codec→task 5(taskmanager/conform/proxy 的编排引用):重排归属(proxy/conform 的 task 依赖上移 oaktask),M5/M8 处理 | +| common ↔ 各 | 12 | M1 §3 逐条 | + +## 4. 关键顺序裁决:node ↔ render 怎么破 + +node→render 的 47 次引用(ColorProcessor 8、videoparams 5、 +footagejob 4、rendermanager 3、pluginjob 3 等)在 M3 时 oakrender +还不存在。两条路: + +- **A(选定)**:M3 阶段不追求 oaknode 立即独立链接,先把 oaknode + 的**公共 C ABI 头**(node/project/viewer/track/block/footage 等 + 跨界类的 init/free/func)定义并实现出来;node→render 的引用在 + **M7(oakrender 拆分)时**统一改成经 oakrender C ABI。 + 即:M3 只要求"oaknode 有自己的 include/ + C API + 测试",链接 + 验证推迟到 M7 闭环。 +- B(否决):先把 render 里被 node 引用的类全搬到下层——伤筋动骨, + 违反"只拆不写"。 + +M3 的完成判据因此放宽为:oaknode C API 实现 + 测试绿 + oaknode 内 +不再新增对 render 的引用;**链接级独立**在 M7 复核。 + +## 5. 每模块通用落地步骤(M 手册都按此节奏) + +1. 建目录:`oak/include/oak/`、`oak/src/`(先软链接/ + 移动源文件,CMake 独立目标)。 +2. 按手册 C API 表写 `include/oak/*.h` + `src/capi_*.cpp` 实现 + (01 §1)。 +3. 消费侧逐个换:include 换适配头(01 §2),反向切割点逐条处理。 +4. 按 03 写测试(该模块每个 C API 至少 1 个 TEST)。 +5. 全量构建 + 全量 ctest + nm 审计(00 §判据 2)+ 提交。 diff --git a/docs/zh/plans/riir/03-testing.md b/docs/zh/plans/riir/03-testing.md new file mode 100644 index 000000000..339b3732e --- /dev/null +++ b/docs/zh/plans/riir/03-testing.md @@ -0,0 +1,75 @@ +# 03 · 测试规范(每模块每 C API 必有 Google Test) + +> 适用于 M1-M9 全部模块。拆分阶段的测试回答一个问题:**经过双层 +> 适配器之后,行为和直连 C++ 时一致**。本文冻结测试结构、覆盖要求 +> 与 fixture 模式。 + +## 1. 结构 + +- 每个模块一个 gtest 二进制:`oak/tests/`,目标名 + `oak_gtest`,链接 `oak` + `GTest::gtest` + + `GTest::gtest_main` + Qt(Core/Gui 按需),经 + `gtest_discover_tests()` 注册进 ctest(项目统一约定:Google Test + 编写、ctest 运行)。 +- 共享 `main.cpp`:Qt 初始化照 `tests/gtest/main.cpp` 现有模式 + (需要 QApplication 的用 offscreen QPA;纯逻辑的不用建)。 +- 需要 engine 全局状态的(EngineCore 单例、ColorManager 配置), + fixture 的 `SetUpTestSuite` 里 `oakengine_init(OAKENGINE_INIT_HEADLESS)`, + `TearDownTestSuite` 里 shutdown——照 `engine/tests/` 现有惯例。 + +## 2. 覆盖要求(硬指标) + +1. **每个 C API 函数至少 1 个 TEST**。模块手册的 C API 表逐行对应 + 测试用例;模块完成判据包含一张核对表(M 手册附录,打勾)。 +2. 每个函数至少覆盖:**正常路径 1 个** + **错误路径 1 个** + (NULL self / 越界索引 / E_INVALID 参数)。 +3. **init/free 配对**:每个 init 测试都用泄漏断言收尾(模块级 + `oak_debug_alive_count()` 调试计数器——各模块在 capi 实现里 + 顺手暴露,测试用它断言"测试前后存活对象数相等")。 +4. **枚举序数一致性**:C 侧 POD/枚举与 C++ 侧枚举的映射(01 §3 表) + 每个映射 1 个 TEST(如 `oakundo` 的 movement mode 0-3 ⇄ + `Timeline::MovementMode`)。 +5. **事件/回调**:每个 `set_*_cb`/subscribe 至少 1 个 TEST:触发后 + 断言回调被调、payload 正确;反注册后断言不再被调。 +6. **所有权**:borrowed 句柄(文档注释标了 `/* borrowed */` 的) + free 后原对象仍存活,1 个 TEST。 + +## 3. 双层往返测试(每个模块至少一套) + +证明"消费侧适配类 ⇄ C ABI ⇄ 提供侧实现"全链路与原 C++ 行为一致: + +```cpp +class UndoStackRoundtripTest : public ::testing::Test { +protected: + void SetUp() override { + // 提供侧直接 C++ 操作 + 经 C API 操作,对比可观察状态 + } +}; + +TEST_F(UndoStackRoundtripTest, PushPopSymmetry) { + OakUndoStack *h = oakundo_undostack_init(nullptr); + // 经 C API push 两条命令 + // 断言 can_undo==1、count==2、jump(0) 后 can_undo==0 + // 再经消费侧适配类 olive::UndoStack 包一层做同样操作,断言一致 + oakundo_undostack_free(h); +} +``` + +规则:往返测试操作的是**真实实现**(非 mock);对比点选 +"可观察状态"(计数、标志位、文本、事件序列),不比较内部指针。 + +## 4. 禁止 + +- 禁止 mock 提供侧实现来"证明"适配层正确(那是自欺欺人)。 +- 禁止用 `assert()` 裸断言(项目规则:一律 `EXPECT_*/ASSERT_*`)。 +- 禁止测试间共享可变全局状态(每个 TEST 自建对象;单例类资源在 + fixture 里清理)。 +- GPU/GL 相关用例:沿用 `GTEST_SKIP()` 判定模式(offscreen 不可绘 + 就跳过,不许靠超时区分)。 + +## 5. ctest 基线纪律 + +- 每模块拆完:`ctest -N` 用例数只增不减;新增模块测试全部绿; + 既有 45 个不许回归。 +- flaky 规则沿用(`oak_cli_transcode`/`oakengine_export_test`/ + `olive-gtest` 单独重跑一次,连续两次失败才算回归)。 diff --git a/docs/zh/plans/riir/M1-oakcommon.md b/docs/zh/plans/riir/M1-oakcommon.md new file mode 100644 index 000000000..13041a741 --- /dev/null +++ b/docs/zh/plans/riir/M1-oakcommon.md @@ -0,0 +1,103 @@ +# M1 · oakcommon 拆分手册 + +> 内容:`engine/common/`(41 文件通用工具集)+ `engine/config/`。 +> 依赖:oakcore(2)。被依赖:几乎全员(node 31、render 24、 +> codec 12、plugin 6、audio 5、task 4、timeline 4、undo 2)。 +> 拆分顺序第 1 位(叶子)。 + +## 1. 目标形态 + +``` +oakcommon/ + include/oakcommon/ # 公共头(C ABI + 允许直引的纯头工具) + src/ # 现有 .cpp 原样迁入 + tests/ # oakcommon_gtest +``` + +- **纯头工具**(lerp.h、define.h、decibel.h、tohex.h、digit.h、 + memorypool.h、threadsafemap.h、range.h 等无 .cpp 的):作为 + oakcommon 公共头直接提供给其他模块 include——拆分阶段允许 + (不产生链接依赖)。RIIR 阶段这些会改写成各语言自有实现。 +- **config/config.h**:单头配置存取,被全模块引用(10+)。 + 它是 Qt 依赖(QSettings 包装),按 §2 冻结 C ABI。 + +## 2. 冻结 C API(有 .cpp 实现的函数族) + +命名前缀 `oakcommon_`。以下按头分组(签名机械规则见 01 §3, +此处冻结函数清单与特殊约定): + +### 2.1 `oakcommon/config.h`(对应 config/config.h) + +```c +void oakcommon_config_set(const char *group, const char *key, + const char *value_utf8); +int oakcommon_config_get(const char *group, const char *key, + char *buf, int buf_size); /* 两段式 */ +int oakcommon_config_get_int(const char *group, const char *key, + int fallback); +double oakcommon_config_get_double(const char *group, const char *key, + double fallback); +void oakcommon_config_set_int(const char *group, const char *key, int v); +``` + +### 2.2 `oakcommon/xml.h`(对应 common/xmlutils.h,8 次被引) + +```c +/* XMLAttributeLoop/xml_read_next_start_element 的 C 化: + * 以迭代器句柄包装 QXmlStreamReader */ +typedef struct OakCommonXmlReader OakCommonXmlReader; +OakCommonXmlReader *oakcommon_xml_reader_init(const char *utf8, int len); +void oakcommon_xml_reader_free(OakCommonXmlReader *r); +int oakcommon_xml_read_next_start_element(OakCommonXmlReader *r); +int oakcommon_xml_reader_name(OakCommonXmlReader *r, char *buf, int n); +int oakcommon_xml_reader_attr(OakCommonXmlReader *r, const char *attr, + char *buf, int n); +int oakcommon_xml_reader_read_element_text(OakCommonXmlReader *r, + char *buf, int n); +void oakcommon_xml_reader_skip_current(OakCommonXmlReader *r); +``` + +写出侧 `oakcommon_xml_writer_*`(init_to_string/write_attribute/ +write_text_element/free 得字符串,两段式)。 + +### 2.3 `oakcommon/files.h`(common/filefunctions.h,render 7 次) + +```c +int oakcommon_file_exists(const char *path); /* 1/0 */ +int oakcommon_file_size(const char *path); /* -1 失败 */ +int oakcommon_file_read_all(const char *path, char *buf, int n); /* 两段式 */ +int oakcommon_file_write_all(const char *path, const char *data, int n); +int oakcommon_dir_mkpath(const char *path); +int oakcommon_get_config_path(char *buf, int n); +int oakcommon_get_temp_path(char *buf, int n); +``` + +### 2.4 `oakcommon/ocio.h`、`oakcommon/oii.h`、`oakcommon/ffmpeg.h` + +(ocioutils/oiioutils/ffmpegutils,按 01 §3 机械 POD 化; +OAK/OIIO/FFmpeg 类型全部句柄化或拍平字段。) + +### 2.5 `oakcommon/misc.h` + +`jobtime`(`double oakcommon_jobtime_now(void)`)、`current` +(`oakcommon_current_get/set` 线程局部当前对象句柄)。 + +## 3. 切割点(common 的 12 次反向 include,逐条) + +| 现状 | 处理 | +|---|---| +| common → render/ 7 次(播放钟/自动滚动等引 render 类型) | 涉及文件(playbackaudioclock/autoscroll 等)**上移出 oakcommon**:它们不是底层工具,归 oakrender(M7) | +| common → node/ 3 次 | 同上,归 oaknode(M3) | +| common → undo/ 1 次 | 同上,归 oakundo(M2) | +| common → codec/ 1 次 | 同上,归 oakcodec(M5) | +| common → pluginSupport/ 1 次 | 同上,归 oakplugin(M9) | + +判据:切完后 `grep -rn '#include "' oakcommon/src | grep -vE +'"(oakcommon|olive/core)'` 为空(只剩 oakcore 与 Qt/系统头)。 + +## 4. 测试(映射 03 §2) + +- config:set/get 往返、int/double fallback、两段式 buf。 +- xml:reader 解析样例串、attr/text 读取、skip、writer 产出解析回读。 +- files:临时目录建/写/读/尺寸/删除。 +- 每函数 1 正常 + 1 错误路径;`oakcommon_debug_alive_count()` 泄漏断言。 diff --git a/docs/zh/plans/riir/M2-oakundo.md b/docs/zh/plans/riir/M2-oakundo.md new file mode 100644 index 000000000..cfc3f9e05 --- /dev/null +++ b/docs/zh/plans/riir/M2-oakundo.md @@ -0,0 +1,102 @@ +# M2 · oakundo 拆分手册 + +> 内容:`engine/undo/`(UndoCommand、MultiUndoCommand、UndoStack)。 +> 依赖:common 2、node/project.h **1 处**(undocommand.cpp)。 +> 被依赖:node 4、timeline 1、render 1、plugin 2、capi 14。 +> 拆分顺序第 2 位(近叶子)。 + +## 1. 目标形态 + +``` +oakundo/ + include/oakundo/{undocommand.h, undostack.h, types.h, export.h} + src/ + tests/ # oakundo_gtest +``` + +## 2. 冻结 C API + +### 2.1 `oakundo/undocommand.h` + +```c +typedef struct OakUndoCommand OakUndoCommand; + +/* 回调式命令(app 与跨模块命令的统一载体) */ +typedef void (*oakundo_command_fn)(void *userdata); +OAKUNDO_API OakUndoCommand *oakundo_command_create( + const char *name, oakundo_command_fn redo, oakundo_command_fn undo, + oakundo_command_fn free_fn, void *userdata); +OAKUNDO_API OakUndoCommand *oakundo_command_create_multi(void); +OAKUNDO_API int oakundo_command_multi_add_child(OakUndoCommand *multi, + OakUndoCommand *child); +OAKUNDO_API int oakundo_command_multi_child_count( + const OakUndoCommand *multi); +OAKUNDO_API void oakundo_command_redo_now(OakUndoCommand *cmd); +OAKUNDO_API void oakundo_command_undo_now(OakUndoCommand *cmd); +OAKUNDO_API void oakundo_command_free(OakUndoCommand *cmd); /* NULL no-op */ +``` + +### 2.2 `oakundo/undostack.h` + +```c +typedef struct OakUndoStack OakUndoStack; +OAKUNDO_API OakUndoStack *oakundo_undostack_init(void *parent_qobject); +OAKUNDO_API void oakundo_undostack_free(OakUndoStack *self); + +OAKUNDO_API void oakundo_undostack_push(OakUndoStack *self, + OakUndoCommand *cmd, const char *name); +/* push_pre_executed:子命令已 redo 过,入栈不重复 redo(undo 分组用) */ +OAKUNDO_API void oakundo_undostack_push_pre_executed(OakUndoStack *self, + OakUndoCommand *cmd, const char *name); + +OAKUNDO_API int oakundo_undostack_can_undo(const OakUndoStack *self); +OAKUNDO_API int oakundo_undostack_can_redo(const OakUndoStack *self); +OAKUNDO_API void oakundo_undostack_undo(OakUndoStack *self); +OAKUNDO_API void oakundo_undostack_redo(OakUndoStack *self); +OAKUNDO_API void oakundo_undostack_jump(OakUndoStack *self, int64_t index); +OAKUNDO_API void oakundo_undostack_clear(OakUndoStack *self); +OAKUNDO_API int64_t oakundo_undostack_count(const OakUndoStack *self); +OAKUNDO_API int64_t oakundo_undostack_index(const OakUndoStack *self); +OAKUNDO_API int oakundo_undostack_command_text(OakUndoStack *self, + int64_t row, char *buf, int buf_size); +OAKUNDO_API int oakundo_undostack_command_is_done(OakUndoStack *self, + int64_t row); +OAKUNDO_API void oakundo_undostack_update_actions(OakUndoStack *self); +/* QAction* 句柄(GUI 菜单绑定用,borrowed) */ +OAKUNDO_API void *oakundo_undostack_undo_action(OakUndoStack *self); +OAKUNDO_API void *oakundo_undostack_redo_action(OakUndoStack *self); + +/* 事件(index_changed) */ +#define OAKUNDO_EVENT_INDEX_CHANGED 1 +typedef void (*oakundo_event_fn)(int event_id, int64_t a, int64_t b, + void *userdata); +OAKUNDO_API int64_t oakundo_undostack_subscribe(OakUndoStack *self, + int event_id, oakundo_event_fn fn, void *userdata); +OAKUNDO_API void oakundo_unsubscribe(int64_t subscription_id); +``` + +## 3. 切割点(1 处) + +`undocommand.cpp` include `node/project.h`(`get_relevant_project()` +虚函数的 Project 类型)。 +处理:`UndoCommand::get_relevant_project()` 的返回类型在 oakundo +内部改为不透明 `void *`(oakundo 不解释它);node 侧(M3)在自己的 +适配层把 `void *` 与 `olive::Project *` 互转。**不改语义**—— +Project* 本来就只是作为不透明身份被使用(修改标记归属)。 + +## 4. 消费侧适配(按 01 §2) + +- `node/`(4 处)、`timeline/`(1)、`render/`(1)、`plugin/`(2): + 各自放 `adapter/undocommand.h`,转发用到的方法子集。 +- `src/capi/undo.cpp`(14 处):facade 的 undo 族实现改为转发 + oakundo C API(或保持 facade 直链 oakundo——facade 是装配层, + 裁决见 M9 §4,默认直链不绕圈)。 + +## 5. 测试(映射 03 §2/§3) + +- 每条 API 正常+错误路径(NULL self、空 push 不入栈—— + 空 MultiUndoCommand 被删除的既有行为必须有 TEST 钉死)。 +- push/undo/redo/jump/clear 全序列;command_text/is_done 边界行。 +- 事件:push 后 index_changed 触发且 a=新 index;unsubscribe 后不再触发。 +- 往返测试:C API 与适配类各做一遍 push-undo-redo,状态一致。 +- `oakundo_debug_alive_count()`:init/free 配对无泄漏。 diff --git a/docs/zh/plans/riir/M3-oaknode.md b/docs/zh/plans/riir/M3-oaknode.md new file mode 100644 index 000000000..13bd5a3bf --- /dev/null +++ b/docs/zh/plans/riir/M3-oaknode.md @@ -0,0 +1,80 @@ +# M3 · oaknode 拆分手册 + +> 内容:`engine/node/`(Node/NodeGroup/NodeKeyframe/NodeInput/ +> NodeValue/NodeFactory/NodeTraverser/NodeGroup/Project/Folder/ +> Sequence/Track/TrackList/Block/ClipBlock/Footage/Serializer/ +> ColorManager/各类节点实现)。 +> 被依赖:timeline 32、task 38、render 38、plugin 6、codec 3、capi 110。 +> 拆分顺序第 3 位(最大的模块;**链接级独立推迟到 M7 复核**,裁决 +> 见 02 §4)。 + +## 1. 目标形态 + +``` +oaknode/ + include/oaknode/ # node.h, project.h, track.h, block.h, footage.h, + # keyframe.h, factory.h, traverser.h, serializer.h, + # colormanager.h, types.h, export.h + src/ # 现有 node/ 全量迁入(含全部具体节点实现子目录) + tests/ # oaknode_gtest +``` + +## 2. 冻结 C API(跨界类清单) + +跨界类 = 被其他模块引用的类(来源:02 依赖矩阵的热门头统计)。 +每类按 01 §1 生成 `init/free/func`,此处冻结**函数族与特殊约定** +(逐函数签名照 oakengine 现有 facade 同族对齐——oakengine/node.h、 +project.h、timeline.h 的对应函数就是模板,参数命名前缀换 +`oaknode_`): + +| 类 | 主要消费方 | 函数族(每族含 init/free) | +|---|---|---| +| Node | 全部 | label/color/enabled 存取、input 列举与存取(`oak_node_value`)、connect/disconnect、output_connections 枚举、links、context positions、id() 字符串 | +| NodeGroup | nodeparamview 链 | passthrough add/remove/列举、resolve_input | +| NodeKeyframe | curve/keyframe | time/value/type/bezier 存取(live + undoable 变体) | +| NodeFactory | factorymenu、构造点 | id_count/id_at/name_from_id/create_from_id/node_at | +| NodeTraverser | nodevaluetree 等 | traverse db 创建/行枚举/释放 | +| Project | task/render | root、name/filename/cache_path、modified、is_modified、add/remove_node | +| Folder | projectexplorer | child 增删/列举/move_children | +| Sequence | timeline/render | track_list、workarea、markers、video/audio params、playhead | +| Track / TrackList | timeline | height/mute/lock/index/type、block 增删、split/ripple 原语 | +| Block / ClipBlock / GapBlock / TransitionBlock | timeline | in/out/length/media_in、speed/reverse/loop、links | +| Footage | task | filename、streams、proxy、duration | +| ProjectSerializer | task | save/load/copy/paste(clipboard 族,照 oakengine/serializer.h 模板) | +| ColorManager | render | config、default config、display transform | + +**特殊约定**: +1. undoable 变体与 live 变体成对(`_live` 后缀或 `, void *command` + 尾参),与 oakengine 现状一致。 +2. 事件:Node 族事件(label/input/keyframe/context 等)经 + `oaknode_subscribe(handle, event_id, fn, userdata)`——事件 ID 表 + 直接沿用 `oakengine/events.h` 的 70-95 段(值不变,便于 + EngineEventBridge 逐步换绑)。 +3. `Node *`、`Project *` 等句柄即 `OakNodeNode *`/`OakNodeProject *`, + 不透明。 +4. 虚函数不出模块(01 §5);具体节点类型经 + `oaknode_factory_create_from_id` 构造,消费侧不碰子类。 + +## 3. 切割点(node 的对下引用) + +| 现状(次数) | 处理 | +|---|---| +| node → render/ 47(colorprocessor 8、videoparams 5、footagejob 4、rendermanager 3、pluginjob 3 等) | videoparams/colortransform 随 M3.5 下沉 oakcommon;其余 **M7 时**改经 oakrender C ABI(02 §4 裁决 A:M3 暂不断链,禁止新增) | +| node → codec/ 8(decoder 4、frame 2、encoder 1、proxymanager 1) | M5 时改经 oakcodec C ABI(M5 手册已含 frame/decoder 家族) | +| node → timeline/ 5(timelinecommon 2、marker 1、workarea 1、timelineundogeneral 1) | timelinecommon 的枚举/常量下沉 oakcommon/types.h;marker/workarea 引用(均在 node/project/ 序列化路径)M4 时改经 oaktimeline C ABI | +| node → audio/ 4 | M6 时改经 oakaudio C ABI | +| node → undo/ 4(undocommand.h) | M2 后改 include oakundo 公共头 + 适配类 | + +M3 阶段判据(放宽版):oaknode 目录就位、C API 实现、oaknode_gtest +全绿、对上述各向**无新增引用**(grep 快照对比)。M7 后复核 +"oaknode 只经 C ABI 调下"。 + +## 4. 测试(映射 03 §2/§3) + +- 重点:Node 增删连边、Project/Folder 层级、Track 属性、 + keyframe live/undoable 对称、serializer 剪贴板往返、 + factory 枚举与创建。 +- 事件:每族至少 1 个 subscribe/trigger/unsubscribe 用例。 +- 枚举序数:NodeValue::Type ⇄ oak_node_value_type 映射表(已在 + nodevaluehandle.h 钉过一次,oaknode 测试再钉一次,防两侧漂移)。 +- `oaknode_debug_alive_count()` 泄漏断言。 diff --git a/docs/zh/plans/riir/M4-oaktimeline.md b/docs/zh/plans/riir/M4-oaktimeline.md new file mode 100644 index 000000000..0105e6685 --- /dev/null +++ b/docs/zh/plans/riir/M4-oaktimeline.md @@ -0,0 +1,114 @@ +# M4 · oaktimeline 拆分手册 + +> 内容:`engine/timeline/`(TimelineMarker/TimelineMarkerList/ +> TimelineWorkArea/timelinecommon/timeline undo 命令族: +> timelineundogeneral/timelineundopointer/timelineundoripple/ +> timelineundosplit/timelineundotrack/timelineundoworkarea)。 +> 依赖:node 32(重)、common 4、config 2、undo 1、core 3。 +> 拆分顺序第 4 位。 + +## 1. 目标形态 + +``` +oaktimeline/ + include/oaktimeline/{marker.h, workarea.h, edit.h, types.h, export.h} + src/ + tests/ # oaktimeline_gtest +``` + +timeline 的 undo 命令类**不出模块**(01 §5):消费侧经 +`edit.h` 的语义函数或 `_command` 工厂拿基类 `OakUndoCommand *` +(oakundo 句柄)组进自己的 MultiUndoCommand。 + +## 2. 冻结 C API + +### 2.1 `oaktimeline/marker.h` + +```c +typedef struct OakTimelineMarkerList OakTimelineMarkerList; +/* list 句柄为 borrowed(属 Sequence/Clip 所有),不需要 free */ +OAKTL_API OakTimelineMarkerList *oaktimeline_marker_list_of( + OakNodeNode *owner /* sequence 或 clip 句柄 */); + +OAKTL_API int oaktimeline_marker_count(const OakTimelineMarkerList *l); +OAKTL_API int oaktimeline_marker_at(const OakTimelineMarkerList *l, int i, + int64_t *in_ts, int64_t *out_ts, int *color, char *name_buf, int n); +OAKTL_API int oaktimeline_marker_add(OakTimelineMarkerList *l, + int64_t in_ts, int64_t out_ts, const char *name, int color, + void *command); /* command=NULL 时自行入栈 */ +OAKTL_API int oaktimeline_marker_remove_at(OakTimelineMarkerList *l, + int i, void *command); +OAKTL_API int oaktimeline_marker_set_time(OakTimelineMarkerList *l, + int i, int64_t in_ts, int64_t out_ts, void *command); +OAKTL_API int oaktimeline_marker_set_props(OakTimelineMarkerList *l, + int i, int color, const char *name, void *command); +/* 事件:MARKER_ADDED/REMOVED/MODIFIED(id 沿用 oakengine events 段) */ +OAKTL_API int64_t oaktimeline_subscribe(void *handle, int32_t event_id, + oaktl_event_fn fn, void *userdata); +OAKTL_API void oaktimeline_unsubscribe(int64_t id); +``` + +### 2.2 `oaktimeline/workarea.h` + +```c +typedef struct OakTimelineWorkarea OakTimelineWorkarea; +OAKTL_API int oaktimeline_workarea_get(const OakTimelineWorkarea *w, + int64_t *in_ts, int64_t *out_ts, int *enabled); +OAKTL_API int oaktimeline_workarea_set_range(OakTimelineWorkarea *w, + int64_t in_ts, int64_t out_ts); /* live */ +OAKTL_API int oaktimeline_workarea_set_range_undoable( + OakTimelineWorkarea *w, int64_t in_ts, int64_t out_ts, + int64_t old_in_ts, int64_t old_out_ts, void *command); +OAKTL_API int oaktimeline_workarea_set_enabled_undoable( + OakTimelineWorkarea *w, int enabled, void *command); +OAKTL_API void oaktimeline_workarea_reset(int64_t *in_ts, int64_t *out_ts); +/* load/save 经 oakcommon_xml 句柄在 oaknode 序列化路径调用 */ +OAKTL_API int oaktimeline_workarea_load(OakTimelineWorkarea *w, + OakCommonXmlReader *r); +OAKTL_API int oaktimeline_workarea_save(const OakTimelineWorkarea *w, + OakCommonXmlWriter *x); +``` + +### 2.3 `oaktimeline/edit.h`(timeline undo 命令族的语义入口) + +照 `oakengine/timeline.h` 的 timeline 编辑原语逐一对齐(那本来就是 +这一族的 facade 版,签名模板直接搬): + +```c +OAKTL_API void *oaktimeline_add_track_command(OakNodeTrackList *list); +OAKTL_API void *oaktimeline_remove_track_command(OakNodeTrackList *list, + int index); +OAKTL_API void *oaktimeline_place_block_command(OakNodeTrackList *list, + int track_index, OakNodeBlock *block, int64_t in_ts); +OAKTL_API void *oaktimeline_replace_block_with_gap_command( + OakNodeTrack *track, OakNodeBlock *block, int64_t in_ts); +OAKTL_API void *oaktimeline_trim_command(OakNodeTrack *track, + OakNodeBlock *block, int64_t point_ts, int trim_in); +OAKTL_API void *oaktimeline_split_command(OakNodeBlock *const *blocks, + int count, int64_t point_ts); /* preserving links 变体加 _links */ +OAKTL_API void *oaktimeline_ripple_delete_gaps_command( + OakNodeSequence *seq, const int64_t *in_ts, const int64_t *out_ts, + const int *track_types, const int *track_indexes, int range_count); +OAKTL_API void *oaktimeline_slide_command(OakNodeTrack *track, + OakNodeBlock *block, int track_delta, int64_t time_delta_ts); +OAKTL_API int64_t oaktimeline_nearest_block_ts(OakNodeTrack *track, + int64_t ts, int direction /* -1/0/1 */); +``` + +## 3. 切割点(timeline → node 32 次) + +全部经 oaknode C ABI + 适配类(01 §2): + +- `node/output/track/track.h`(5)、`tracklist.h`(3):timeline 内部 + 对 Track 的操作换 `OakNodeTrack*` 适配。 +- `node/block/gap/gap.h`(4)、`transition.h`(4):Gap/Transition 的 + 构造换 `oaknode_factory_create_from_id`。 +- `node/project.h`(3):Project 适配类。 + +## 4. 测试(映射 03 §2/§3) + +- marker:增删改查、undo 往返(push 后 undo 恢复)、事件三件套。 +- workarea:set/get、undoable 旧值恢复、reset 哨兵、xml 往返。 +- edit:每个 `_command` 工厂 1 个"构造→入栈→undo 还原"用例 + (track 增删、place/replace/trim/split/ripple/slide)。 +- `oaktimeline_debug_alive_count()` 泄漏断言。 diff --git a/docs/zh/plans/riir/M5-oakcodec.md b/docs/zh/plans/riir/M5-oakcodec.md new file mode 100644 index 000000000..4b0271371 --- /dev/null +++ b/docs/zh/plans/riir/M5-oakcodec.md @@ -0,0 +1,100 @@ +# M5 · oakcodec 拆分手册 + +> 内容:`engine/codec/`(decoder/encoder/frame/waveform/proxymanager/ +> conformmanager)。 +> 依赖:common 12、render 11(M3.5 下沉后剩 ~3)、task 5、node 3、 +> core 4。 +> 拆分顺序第 5 位。 + +## 1. 目标形态 + +``` +oakcodec/ + include/oakcodec/{frame.h, decoder.h, encoder.h, waveform.h, proxy.h, + types.h, export.h} + src/ + tests/ # oakcodec_gtest +``` + +## 2. 冻结 C API + +### 2.1 `oakcodec/frame.h`(render 7 次引 codec/frame.h) + +```c +typedef struct OakCodecFrame OakCodecFrame; +OAKCD_API OakCodecFrame *oakcodec_frame_init(void); +OAKCD_API OakCodecFrame *oakcodec_frame_init_copy(const OakCodecFrame *o); +OAKCD_API void oakcodec_frame_free(OakCodecFrame *f); +OAKCD_API OakCodecFrame *oakcodec_frame_retain(OakCodecFrame *f); /* +1 */ +OAKCD_API int oakcodec_frame_set_params(OakCodecFrame *f, + const oak_video_params *p); +OAKCD_API int oakcodec_frame_get_params(const OakCodecFrame *f, + oak_video_params *out); +OAKCD_API int oakcodec_frame_allocate(OakCodecFrame *f); +OAKCD_API void *oakcodec_frame_data(OakCodecFrame *f); +OAKCD_API int oakcodec_frame_linesize(const OakCodecFrame *f); +OAKCD_API int64_t oakcodec_frame_timestamp(const OakCodecFrame *f); +OAKCD_API void oakcodec_frame_set_timestamp(OakCodecFrame *f, int64_t ts); +``` + +### 2.2 `oakcodec/decoder.h` + +```c +typedef struct OakCodecDecoder OakCodecDecoder; +OAKCD_API OakCodecDecoder *oakcodec_decoder_init(const char *filename); +OAKCD_API void oakcodec_decoder_free(OakCodecDecoder *d); +OAKCD_API int oakcodec_decoder_stream_count(const OakCodecDecoder *d, + int media_type /* 0=video 1=audio */); +OAKCD_API int oakcodec_decoder_get_video_stream(const OakCodecDecoder *d, + int index, oak_video_params *out, int64_t *duration_ts); +OAKCD_API int oakcodec_decoder_get_audio_stream(const OakCodecDecoder *d, + int index, int *sample_rate, uint64_t *layout, int *format); +OAKCD_API OakCodecFrame *oakcodec_decoder_decode_video( + OakCodecDecoder *d, int stream, int64_t ts); /* NULL=EOF/错误 */ +OAKCD_API int oakcodec_decoder_decode_audio(OakCodecDecoder *d, + int stream, int64_t ts, float *buf, int frame_count); +OAKCD_API int oakcodec_decoder_last_error(OakCodecDecoder *d, + char *buf, int n); +``` + +### 2.3 `oakcodec/encoder.h` + +```c +typedef struct OakCodecEncoder OakCodecEncoder; +OAKCD_API OakCodecEncoder *oakcodec_encoder_init( + const oak_encoding_params *params); /* POD 照 oakengine/encoding.h */ +OAKCD_API void oakcodec_encoder_free(OakCodecEncoder *e); +OAKCD_API int oakcodec_encoder_write_video(OakCodecEncoder *e, + const OakCodecFrame *f); +OAKCD_API int oakcodec_encoder_write_audio(OakCodecEncoder *e, + const float *samples, int frame_count); +OAKCD_API int oakcodec_encoder_flush(OakCodecEncoder *e); +OAKCD_API int oakcodec_encoder_last_error(OakCodecEncoder *e, + char *buf, int n); +``` + +### 2.4 `oakcodec/waveform.h`、`oakcodec/proxy.h` + +waveform:`oakcodec_waveform_extract(filename, stream, out_min_max_pairs, +progress_cb)`(AudioWaveformCache 的磁盘格式不变)。 +proxy:照 `oakengine/proxy.h` 模板(get_or_start/state/cancel)。 + +## 3. 切割点 + +| 现状 | 处理 | +|---|---| +| codec → render/ 11 | videoparams/subtitleparams/colortransform 已随 M3.5 下沉 oakcommon;剩 renderer.h(2)、framemanager.h(1) → framemanager 是 codec 内部缓存编排,**随 codec 一起走**(从 render/ 移入 oakcodec/src,纯文件移动,它本来就主要服务 codec) | +| codec → task/ 5(taskmanager/conform/proxy 编排) | proxy/conform 对 TaskManager 的引用改为 01 §4 回调注册(`oakcodec_set_task_submit_cb`),Task 对象创建上移 oaktask(M8),oakcodec 只调回调 | +| codec → node/ 3 | 经 oaknode C ABI(M3 已就位) | + +## 4. 测试(映射 03 §2/§3) + +- frame:params 往返、allocate/data/linesize、retain/free 引用计数。 +- decoder:`tests/demo.mp4` 开流、stream 枚举、decode 首帧非空、 + 错误路径(不存在文件 → E_NOT_FOUND + last_error 非空)。 +- encoder:写小 mp4(照 oak_cli_transcode 的参数),flush 后文件 + 可再被 decoder 打开(往返)。 +- waveform:demo.mp4 提取返回非空且长度与时长一致(容差断言)。 +- proxy:mock 免(03 §4)——用 demo.mp4 低分辨率参数真跑一次或 + 按环境跳过。 +- `oakcodec_debug_alive_count()` 泄漏断言。 diff --git a/docs/zh/plans/riir/M6-oakaudio.md b/docs/zh/plans/riir/M6-oakaudio.md new file mode 100644 index 000000000..cd2437627 --- /dev/null +++ b/docs/zh/plans/riir/M6-oakaudio.md @@ -0,0 +1,67 @@ +# M6 · oakaudio 拆分手册 + +> 内容:`engine/audio/`(AudioManager、AudioProcessor、输出设备 +> 抽象、audiovisualwaveform)。 +> 依赖:common 5、core 5、config 2、render 2、codec 1。拆分顺序第 6 位 +> (小模块,穿插在 render 前做掉)。 + +## 1. 目标形态 + +``` +oakaudio/ + include/oakaudio/{manager.h, processor.h, types.h, export.h} + src/ + tests/ # oakaudio_gtest +``` + +## 2. 冻结 C API + +### 2.1 `oakaudio/processor.h`(实时重采样/格式转换,R6 已建过 +`oakengine_audio_processor_*`,本表即其 oakaudio 版,语义不变) + +```c +typedef struct OakAudioProcessor OakAudioProcessor; +OAKAU_API OakAudioProcessor *oakaudio_processor_init(void); +OAKAU_API void oakaudio_processor_free(OakAudioProcessor *p); +OAKAU_API int oakaudio_processor_open(OakAudioProcessor *p, + int in_rate, uint64_t in_layout, int in_format, + int out_rate, uint64_t out_layout, int out_format, double speed); +OAKAU_API void oakaudio_processor_close(OakAudioProcessor *p); +OAKAU_API int oakaudio_processor_convert(OakAudioProcessor *p, + float **data, int frame_count); +``` + +### 2.2 `oakaudio/manager.h` + +```c +OAKAU_API int oakaudio_manager_init(int prefer_backend /* -1=auto */); +OAKAU_API void oakaudio_manager_shutdown(void); +OAKAU_API int oakaudio_manager_set_output_params(int rate, + uint64_t layout, int format); +OAKAU_API int oakaudio_manager_get_output_params(int *rate, + uint64_t *layout, int *format); +/* 推流:播放路径逐块喂 samples(float 交错) */ +OAKAU_API int oakaudio_manager_push(const float *samples, + int frame_count, double speed); +OAKAU_API int oakaudio_manager_is_playing(void); +OAKAU_API void oakaudio_manager_stop(void); +/* 输出参数变化事件 */ +OAKAU_API int64_t oakaudio_manager_subscribe_params_changed( + oakaudio_event_fn fn, void *userdata); +OAKAU_API void oakaudio_unsubscribe(int64_t id); +``` + +## 3. 切割点 + +| 现状 | 处理 | +|---|---| +| audio → render/ 2 | audiovisualwaveform 对 render 类型的引用:POD 化(波形 min/max 对数组),残留类型随 M3.5 下沉 | +| audio → codec/ 1 | 经 oakcodec C ABI(M5 已就位) | + +## 4. 测试(映射 03 §2/§3) + +- processor:open/convert/close 全链(44.1k stereo → 48k stereo, + 帧数换算正确、无爆音断言用能量差阈值)、速度 1.5x。 +- manager:无音频设备环境用 null backend 初始化(现有后端探测 + 模式),params set/get 往返、事件触发。 +- `oakaudio_debug_alive_count()` 泄漏断言。 diff --git a/docs/zh/plans/riir/M7-oakrender.md b/docs/zh/plans/riir/M7-oakrender.md new file mode 100644 index 000000000..1dd94e473 --- /dev/null +++ b/docs/zh/plans/riir/M7-oakrender.md @@ -0,0 +1,126 @@ +# M7 · oakrender 拆分手册 + +> 内容:`engine/render/`(Renderer/OpenGLRenderer/DynamicRenderer/ +> PlaybackCache/FrameHashCache/AudioWaveformCache/ColorProcessor/ +> ColorManager/RenderManager/PreviewAutoCacher/DiskManager/job 族/ +> shaders 加载)。 +> 依赖:node 38、common 24、codec 9、core 8、config 6、plugin 5、 +> audio 3、task 2、undo 1、**src 1(分层违规)**。 +> 拆分顺序第 7 位。M7 完成后 oaknode 的链接级独立复核(02 §4)。 + +## 1. 目标形态 + +``` +oakrender/ + include/oakrender/{renderer.h, cache.h, color.h, manager.h, display.h, + types.h, export.h} + src/ + tests/ # oakrender_gtest +``` + +## 2. 冻结 C API + +### 2.1 `oakrender/renderer.h`(渲染器/纹理/blit) + +签名照 R7-A(`docs/zh/r7-pure-abi-plan.md` §A.2)的 display.h 重写版 +**原样采用**——R7-A 先做的话,M7 直接把它从 facade 层搬进 +oakrender 并改前缀 `oakrender_display_*`;本表不重复,以 R7-A 为准。 +补充后端管理: + +```c +OAKRD_API int oakrender_backend_count(void); +OAKRD_API int oakrender_backend_id_at(int i, char *buf, int n); +OAKRD_API int oakrender_set_backend(const char *backend_id); +OAKRD_API int oakrender_current_backend(char *buf, int n); +``` + +### 2.2 `oakrender/cache.h`(PlaybackCache/FrameHashCache/waveform 缓存) + +```c +typedef struct OakRenderCache OakRenderCache; +OAKRD_API void oakrender_cache_invalidate(OakRenderCache *c, + int64_t in_ts, int64_t out_ts); +OAKRD_API void oakrender_cache_validate(OakRenderCache *c, + int64_t in_ts, int64_t out_ts); +OAKRD_API int oakrender_cache_has_validated_ranges( + const OakRenderCache *c); +OAKRD_API int oakrender_cache_indicator_height(void); /* 常量查询 */ +/* 帧哈希缓存 */ +OAKRD_API int oakrender_frame_cache_load(OakRenderCache *c, + const char *path, const char *uuid, int64_t ts, + OakCodecFrame **out_frame); +OAKRD_API void oakrender_frame_cache_save(OakRenderCache *c, + const char *path, const char *uuid, const OakCodecFrame *f); +/* 缓存事件(playback invalidated/validated、frame invalidated) */ +OAKRD_API int64_t oakrender_cache_subscribe(OakRenderCache *c, + int32_t event_id, oakrender_event_fn fn, void *userdata); +``` + +### 2.3 `oakrender/color.h` + +```c +typedef struct OakColorProcessor OakColorProcessor; +OAKRD_API OakColorProcessor *oakrender_color_processor_create( + const char *src_space, const char *dst_transform, int direction); +OAKRD_API void oakrender_color_processor_free(OakColorProcessor *p); +OAKRD_API int oakrender_color_processor_convert(OakColorProcessor *p, + double ir, double ig, double ib, double ia, + double *or_, double *og, double *ob, double *oa); +/* ColorManager */ +OAKRD_API int oakrender_color_manager_set_up_default_config(void); +OAKRD_API int oakrender_color_manager_get_config(char *buf, int n); +OAKRD_API int oakrender_color_manager_display_transform( + const char *display, const char *view, char *buf, int n); +``` + +### 2.4 `oakrender/manager.h`(RenderManager/PreviewAutoCacher/ +DiskManager/job 提交) + +```c +OAKRD_API int oakrender_manager_init(void); +OAKRD_API void oakrender_manager_shutdown(void); +/* 异步帧请求:完成经回调送 OakCodecFrame(跨线程,retain 规则同 A.3) */ +typedef void (*oakrender_frame_ready_fn)(OakCodecFrame *frame, + int64_t ts, void *userdata); +OAKRD_API int64_t oakrender_request_frame(OakNodeNode *viewer, + int64_t ts, oakrender_frame_ready_fn cb, void *userdata); +OAKRD_API void oakrender_cancel_request(int64_t request_id); +OAKRD_API int oakrender_set_cacher_multicam(OakNodeNode *multicam_or_NULL); +OAKRD_API int oakrender_set_display_color_processor( + OakColorProcessor *p_or_NULL); +OAKRD_API int oakrender_disk_cache_path(char *buf, int n); +OAKRD_API int64_t oakrender_disk_cache_size(void); +OAKRD_API int oakrender_disk_cache_clear(void); +``` + +## 3. 切割点 + +| 现状 | 处理 | +|---|---| +| render → node/ 38(project 7、viewer 5、value 4、footage 4、traverser 3 等) | 全部经 oaknode C ABI + 适配类(M3 已就位)——**本手册的工作量主体** | +| render → codec/ 9(frame 7、decoder 1、conformmanager 1) | 经 oakcodec C ABI(M5) | +| render → task/ 2 | 回调注册(01 §4),task 句柄不传 | +| render → undo/ 1(undostack.h) | oakundo 适配类(M2) | +| render → audio/ 3 | 经 oakaudio C ABI(M6) | +| render → src/capi/displayinternal.h 1 次(违规) | R7-A 时随 display.h 重写消除;若 R7-A 未做,M7 先把该内部头的内容并入 oakrender | +| render → plugin/ 5 | pluginjob 等:M9 时经 oakplugin C ABI(M7 暂不断链,禁止新增) | + +## 4. 测试(映射 03 §2/§3) + +- cache:invalidate/validate 状态机、帧缓存存取往返、事件触发。 +- color:默认 config 建置、processor convert 已知值(sRGB→Linear + 抽样点数值断言,容差 1e-3)。 +- manager:request_frame 对 demo.mp4 + 最小 sequence 出帧非空 + (offscreen 可跑的部分);cancel 路径。 +- GL 相关:沿用 GTEST_SKIP 模式,不强求离屏可绘。 +- `oakrender_debug_alive_count()` 泄漏断言。 + +## 5. 收尾复核(02 §4 裁决 A 的兑现) + +M7 闭环时执行: +``` +grep -rn '#include "render/' oaknode/src | wc -l # 必须 0 +nm -D oaknode 构建产物 | grep -c " U _ZN5olive.*render" # 必须 0 +``` +node→render 的 47 次引用此时应全部经 oakrender C ABI(M3 遗留的 +"暂不断链"在此结清)。 diff --git a/docs/zh/plans/riir/M8-oaktask.md b/docs/zh/plans/riir/M8-oaktask.md new file mode 100644 index 000000000..bc1e04d4c --- /dev/null +++ b/docs/zh/plans/riir/M8-oaktask.md @@ -0,0 +1,94 @@ +# M8 · oaktask 拆分手册 + +> 内容:`engine/task/`(Task 基类、TaskManager、project/ +> load/save/import/loadotio/saveotio、cache 任务)。 +> 依赖:node 38、codec 4、render 4、common 4、config 2、timeline 1、 +> coreengine 2。 +> 拆分顺序第 8 位。 + +## 1. 目标形态 + +``` +oaktask/ + include/oaktask/{task.h, manager.h, project.h, types.h, export.h} + src/ + tests/ # oaktask_gtest +``` + +## 2. 冻结 C API + +### 2.1 `oaktask/task.h`(单任务句柄,照 oakengine/task.h 模板对齐) + +```c +typedef struct OakTaskTask OakTaskTask; +OAKTK_API void oaktask_task_free(OakTaskTask *t); +OAKTK_API int oaktask_task_start(OakTaskTask *t); /* 异步 */ +OAKTK_API int oaktask_task_start_sync(OakTaskTask *t); +OAKTK_API int oaktask_task_cancel(OakTaskTask *t); +OAKTK_API int oaktask_task_is_finished(const OakTaskTask *t); +OAKTK_API int oaktask_task_succeeded(const OakTaskTask *t); +OAKTK_API int oaktask_task_progress(const OakTaskTask *t, double *out); +OAKTK_API int oaktask_task_title(OakTaskTask *t, char *buf, int n); +OAKTK_API int oaktask_task_error(OakTaskTask *t, char *buf, int n); +/* 事件:STARTED/PROGRESS/FINISHED(id 沿用 oakengine events 段) */ +OAKTK_API int64_t oaktask_task_subscribe(OakTaskTask *t, int32_t event_id, + oaktask_event_fn fn, void *userdata); +``` + +### 2.2 `oaktask/project.h`(任务工厂 + 结果访问器) + +```c +OAKTK_API OakTaskTask *oaktask_create_project_load(const char *filename); +OAKTK_API OakTaskTask *oaktask_create_project_save(OakNodeProject *p, + const char *filename_or_NULL, int use_compression, + const void *layout_or_NULL); +OAKTK_API OakTaskTask *oaktask_create_project_import(OakNodeNode *folder, + const char *const *urls, int url_count); +OAKTK_API OakTaskTask *oaktask_create_project_load_otio( + const char *filename, const int *sequence_indexes, int count); +OAKTK_API OakTaskTask *oaktask_create_project_save_otio( + OakNodeProject *p, const char *filename, const int *sequence_indexes, + int count); +/* import 结果(task 成功后读,borrowed) */ +OAKTK_API void *oaktask_import_take_command(OakTaskTask *t); /* 所有权转移 */ +OAKTK_API int oaktask_import_footage_count(OakTaskTask *t); +OAKTK_API OakNodeNode *oaktask_import_footage_at(OakTaskTask *t, int i); +OAKTK_API int oaktask_import_invalid_count(OakTaskTask *t); +OAKTK_API int oaktask_import_invalid_at(OakTaskTask *t, int i, + char *buf, int n); +/* load 结果 */ +OAKTK_API OakNodeProject *oaktask_load_take_project(OakTaskTask *t); +``` + +### 2.3 `oaktask/manager.h` + +```c +OAKTK_API int oaktask_manager_count(void); +OAKTK_API OakTaskTask *oaktask_manager_at(int i); /* borrowed */ +OAKTK_API void oaktask_manager_delete_finished(void); +/* 事件:TASK_ADDED/REMOVED/FAILED/LIST_CHANGED */ +OAKTK_API int64_t oaktask_manager_subscribe(int32_t event_id, + oaktask_event_fn fn, void *userdata); +OAKTK_API void oaktask_unsubscribe(int64_t id); +``` + +## 3. 切割点 + +| 现状 | 处理 | +|---|---| +| task → node/ 38(footage 6、project 5、sequence 3、serializer/layout 3、colormanager 2 等) | 全部经 oaknode C ABI + 适配类(M3 已就位)——本手册工作量主体 | +| task → codec/ 4 | 经 oakcodec C ABI(M5) | +| task → render/ 4 | 经 oakrender C ABI(M7) | +| task → timeline/ 1 | 经 oaktimeline C ABI(M4) | +| task → coreengine.h 2 | coreengine 的 Task 注册点改为 oaktask_manager 自管(manager 原本就是单例,注册调用内聚进 oaktask) | + +## 4. 测试(映射 03 §2/§3) + +- 每个工厂 1 个用例:demo.mp4 import(footage_count>0、command 可 + 入栈)、save/load 往返(临时目录 .ove,load 后 project 非空、 + root 非空)。 +- start_sync 成功/失败路径(不存在文件 → failed + error 非空)。 +- 事件:STARTED→PROGRESS→FINISHED 序列(导入任务断言至少一次 + PROGRESS 且 FINISHED.succeeded==1)。 +- manager:添加/删除/list_changed 事件。 +- `oaktask_debug_alive_count()` 泄漏断言。 diff --git a/docs/zh/plans/riir/M9-oakplugin.md b/docs/zh/plans/riir/M9-oakplugin.md new file mode 100644 index 000000000..66c193067 --- /dev/null +++ b/docs/zh/plans/riir/M9-oakplugin.md @@ -0,0 +1,98 @@ +# M9 · oakplugin 拆分手册 + facade 装配层裁决 + +> 内容:`engine/pluginSupport/`(OpenFX host:OliveHost、 +> OlivePluginInstance、PluginNode、PluginProgressReporter)。 +> 依赖:node 6、render 6、common 6、core 2、undo 2、coreengine 2。 +> 拆分顺序第 9 位(最后拆的实体模块)。文末 §4 是 liboakengine +> 装配层的最终裁决。 + +## 1. 目标形态 + +``` +oakplugin/ + include/oakplugin/{host.h, instance.h, progress.h, types.h, export.h} + src/ + tests/ # oakplugin_gtest +``` + +## 2. 冻结 C API + +### 2.1 `oakplugin/host.h` + +```c +OAKPL_API int oakplugin_host_init(void); +OAKPL_API void oakplugin_host_shutdown(void); +OAKPL_API int oakplugin_host_scan(const char *const *bundle_dirs, + int dir_count); +OAKPL_API int oakplugin_host_plugin_count(void); +OAKPL_API int oakplugin_host_plugin_id_at(int i, char *buf, int n); +OAKPL_API int oakplugin_host_plugin_label(const char *plugin_id, + char *buf, int n); +``` + +### 2.2 `oakplugin/instance.h` + +```c +typedef struct OakPluginInstance OakPluginInstance; +OAKPL_API OakPluginInstance *oakplugin_instance_create( + const char *plugin_id); +OAKPL_API void oakplugin_instance_free(OakPluginInstance *i); +OAKPL_API int oakplugin_instance_set_param(OakPluginInstance *i, + const char *param_id, const oak_node_value *v); +OAKPL_API int oakplugin_instance_get_param(OakPluginInstance *i, + const char *param_id, oak_node_value *out); +OAKPL_API int oakplugin_instance_render(OakPluginInstance *i, + OakCodecFrame *dst, const OakCodecFrame *src, int64_t ts); +/* 进度事件(R6 已把 cancelled 信号改成 C 回调,沿用该机制) */ +OAKPL_API void oakplugin_instance_set_progress_cb(OakPluginInstance *i, + oakplugin_progress_fn fn, void *userdata); +OAKPL_API void oakplugin_instance_cancel(OakPluginInstance *i); +``` + +### 2.3 `oakplugin/progress.h` + +```c +typedef struct OakPluginProgress OakPluginProgress; /* 报告器句柄 */ +OAKPL_API OakPluginProgress *oakplugin_progress_create_dialog( + const char *message, const char *title); /* UI 侧实现 */ +OAKPL_API void oakplugin_progress_free(OakPluginProgress *p); +``` + +## 3. 切割点 + +| 现状 | 处理 | +|---|---| +| plugin → node/ 6(plugins/plugin.h 3 等) | 经 oaknode C ABI(PluginNode 是节点,留在 oaknode;oakplugin 只经 factory/host 接口交互)——**PluginNode 归属裁决:PluginNode 类随 oaknode 走**(它是节点体系成员),oakplugin 提供 host/instance | +| plugin → render/ 6(videoparams 3 等) | videoparams 已下沉;其余经 oakrender C ABI | +| plugin → undo/ 2 | oakundo 适配类 | +| plugin → coreengine.h 2 | 插件注册点内聚进 oakplugin_host_init | + +## 4. facade 装配层最终裁决(liboakengine 的终态) + +M1-M9 全部完成后,`engine/src/capi` + `coreengine` + `tool/` + +`ui/` 残余构成 liboakengine。裁决(选定): + +**facade 直链各模块,不绕 C ABI 自调**。即:facade 的 `oakengine_*` +实现可以继续直接调用各模块的 C++ 内部(链接 oaknode/oakrender 等 +的静态或共享库),不要求 facade 经各模块的公共 C ABI 兜圈。 +理由:facade 与 app 的边界(oakengine_*)已经纯 C 且 nm=0,模块间 +边界是给"模块互相调用"用的;facade 是装配层,自家人不绕远路。 +**但**:`coreengine.h` 被 node/render/task/plugin 引用的 5+2+2+2 +处必须内聚(各模块的初始化改由各自的 `oak_init` 完成, +coreengine 只做编排调用)。 + +终态验证: + +``` +nm -D --defined-only liboakengine.so | grep -c " T _Z" # 0(R7-B) +nm -D --defined-only liboaknode.so | grep -c " T _Z" # 0(每模块同查) +ldd liboakengine.so | grep oak # 只见 oak* 模块库 +全量构建 0 error;全量 ctest 绿 +``` + +## 5. 测试(映射 03 §2/§3) + +- host:init/scan/枚举(照现有 OliveHost 测试的 bundle fixture)。 +- instance:Shadertoy 类插件(CI 里可用的)创建/参数/渲染一帧非空。 +- progress:回调触发与 cancel。 +- `oakplugin_debug_alive_count()` 泄漏断言。