From 774eda75fea300a19ee0712f0168539ce391f858 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Wed, 5 Aug 2026 14:48:08 +0800 Subject: [PATCH] docs(riir): design inter-module C ABI interfaces and split out oakstorage - riir.md: split project file read/write into standalone oakstorage module (pluggable backend, DB-replaceable); add Mermaid data flow diagram of all modules; adjust batch table and milestones - riir/04-interfaces.md: new provides/consumes contract matrix for all modules - riir/M10-oakstorage.md: new manual with frozen C API (typed opaque handles, manual vtable for storage backends, URI addressing) - riir/01: codify pure-C OO interface rules (typed handles instead of void*, init/free pairs, manual vtables, no C++ objects or member calls across shared library boundaries); upper layers issue commands only, no module-to-module event subscriptions (async task callbacks excepted) - sync 00/02/03 and M2-M9 manuals: remove subscribe APIs, replace void* with typed handles, update tests accordingly --- docs/zh/plans/riir.md | 109 ++++++++++++- docs/zh/plans/riir/00-overview.md | 15 +- docs/zh/plans/riir/01-adapter-pattern.md | 83 ++++++---- docs/zh/plans/riir/02-modules-and-order.md | 8 +- docs/zh/plans/riir/03-testing.md | 6 +- docs/zh/plans/riir/04-interfaces.md | 147 +++++++++++++++++ docs/zh/plans/riir/M10-oakstorage.md | 176 +++++++++++++++++++++ docs/zh/plans/riir/M2-oakundo.md | 25 +-- docs/zh/plans/riir/M3-oaknode.md | 20 +-- docs/zh/plans/riir/M4-oaktimeline.md | 22 +-- docs/zh/plans/riir/M6-oakaudio.md | 8 +- docs/zh/plans/riir/M7-oakrender.md | 10 +- docs/zh/plans/riir/M8-oaktask.md | 27 ++-- docs/zh/plans/riir/M9-oakplugin.md | 4 +- 14 files changed, 563 insertions(+), 97 deletions(-) create mode 100644 docs/zh/plans/riir/04-interfaces.md create mode 100644 docs/zh/plans/riir/M10-oakstorage.md diff --git a/docs/zh/plans/riir.md b/docs/zh/plans/riir.md index 54f1d752b..2a7764d31 100644 --- a/docs/zh/plans/riir.md +++ b/docs/zh/plans/riir.md @@ -147,11 +147,11 @@ │ liboakengine-facade(壳:capi + 事件 + init) │ - ┌────────┬────────┼─────────┬──────────┐ - oaktask oakrender oakplugin oakaudio oakserialize - │ │ │ │ │ - └────────┴────┬───┴──────────┴──────────┘ - │ + ┌────────┬────────┼─────────┬──────────┬──────────────┐ + oaktask oakrender oakplugin oakaudio oakserialize oakstorage + │ │ │ │ │ (工程文件读写, + └────────┴────┬───┴──────────┴──────────┘ 独立模块,未来 + │ 替换为数据库) oakmodel(节点图 + 项目模型 + 时间线模型) │ ┌────────┼─────────┐ @@ -160,6 +160,99 @@ ffmpeg_bridge(已是 C ABI .so) ``` +**oakstorage 单列说明(本计划对原模块图的唯一结构性修改)**: +工程文件的读写(`node/project/serializer` 的落盘路径 + `task/project/` +load/save/loadotio/saveotio 的文件 IO)从 oakserialize / oaktask 中**单独拆出**为 +oakstorage 模块。它对上只暴露**存储后端无关**的 C ABI(打开/保存/探测工程, +URI 寻址),当前唯一后端是 XML .ove 文件;**未来替换为数据库时只新增一个 +后端实现,上层(oaktask/facade)零改动**。剪贴板序列化(copy/paste)不属于 +存储,仍留在 oakserialize。详细接口设计见 `riir/04-interfaces.md` 与 +`riir/M10-oakstorage.md`。 + +### 3.1.1 模块数据流图(Mermaid) + +下图描述终态各模块之间的调用与数据流向。**实线 = 命令调用(上层→下层, +内部 C ABI `oak_*`,调用方知道影响);虚线 = 仅两种允许的反向通知: +facade→app 的 `oakengine_event` 通道,以及异步任务(oaktask 任务 / +渲染 ticket / GPU 帧完成)的完成回调。下层对上层没有事件订阅。** + +```mermaid +flowchart TB + subgraph 消费侧["消费侧(不感知实现语言)"] + APP["oak-editor (app)"] + CLI["oak-cli"] + WRK["oak-render-worker"] + end + + FACADE["liboakengine-facade
壳:oakengine_* 公共 C ABI(冻结)
+ 事件注册表 + init/shutdown"] + + subgraph 引擎模块["引擎模块(facade 之下,只经内部 C ABI 互调)"] + TASK["oaktask
任务编排(Task/TaskManager)"] + RENDER["oakrender
渲染管线/缓存/色彩"] + PLUGIN["oakplugin
OpenFX 宿主"] + AUDIO["oakaudio
音频 DSP/输出/电平"] + SERIAL["oakserialize
节点图 XML 序列化
(剪贴板 copy/paste,不落盘)"] + STORAGE["oakstorage
工程持久化(URI 打开/保存/探测)
后端可插拔:ove-xml 文件|未来 oakdb 数据库"] + UNDO["oakundo
UndoStack/UndoCommand"] + MODEL["oakmodel
Node 类型簇 + Project/Sequence/Track/Block
(最大不可拆分类型簇)"] + CODEC["oakcodec
decoder/encoder/conform/proxy"] + CORE["liboakcore
rational/timecode/bezier/samplebuffer"] + BACKEND["oakbackend
GPU 插件:oakgl / oakvulkan / 未来 Rust(wgpu)"] + FFMPEG["ffmpeg_bridge
FFmpeg 的 C ABI 桥(现成 .so)"] + end + + APP -->|"oakengine_* 调用"| FACADE + CLI --> FACADE + WRK --> FACADE + FACADE -.->|"oakengine_event 变更通知
(发射线程同步回调)"| APP + + FACADE -->|"任务工厂/进度"| TASK + FACADE -->|"渲染请求/帧句柄"| RENDER + FACADE --> AUDIO + FACADE --> PLUGIN + FACADE -->|"工程打开/保存(URI)"| STORAGE + FACADE --> UNDO + + TASK -->|"load/save 任务委托
(Project 句柄 + XML 字节流)"| STORAGE + TASK -->|"import/conform/proxy 任务"| CODEC + TASK -->|"导出任务"| RENDER + + RENDER -->|"遍历节点图求值"| MODEL + RENDER -->|"解码帧请求"| CODEC + RENDER -->|"上传纹理/绘制"| BACKEND + RENDER -->|"OCIO 色彩变换"| CORE + + STORAGE -->|"反序列化建图 / 序列化取图(同步命令)"| MODEL + SERIAL --> MODEL + + MODEL -->|"读取媒体参数"| CODEC + MODEL --> CORE + UNDO -->|"命令持有 Node/Project 句柄"| MODEL + AUDIO --> CORE + CODEC --> FFMPEG + CODEC --> CORE + BACKEND -.->|"帧完成(异步任务回调,ticket 返回通道)"| RENDER +``` + +**数据流要点**: + +1. **命令流(自上而下)**:app 的每个动作 → facade `oakengine_*` → 对应 + 模块内部 C ABI。facade 是唯一入口,模块不允许被 app 直接链接。 +2. **工程 IO 流**:`oaktask` 创建 load/save 任务 → 委托 `oakstorage`; + oakstorage 按 URI scheme 选后端(`file://*.ove` → ove-xml 后端, + 未来 `oakdb://` → 数据库后端),序列化/反序列化时对 `oakmodel` 建图 + 取图。**替换为数据库只发生在 oakstorage 内部。** +3. **帧数据流**:`oakcodec` 经 `ffmpeg_bridge` 解码 → `oakrender` 遍历 + `oakmodel` 节点图求值 → `oakbackend`(GPU 插件)上屏/导出;帧以不透明 + 句柄 + buf/size 约定跨边界,不传递 C++ 对象。 +4. **通知流(仅两种反向通道,虚线)**:模块间没有事件订阅——上层调 + 下层只有命令,调用方知道影响;变更通知由命令发起层(通常是 facade) + 经 `oakengine_event` 同步回调给 app,Rust 化后只是发射端换语言, + 通道不变(§6.1)。另一例外是异步任务的完成回调(oaktask 任务、 + 渲染 ticket、GPU 帧完成)——回调即该异步命令的返回通道。 +5. **undo 流**:所有可撤销编辑(无论来自 facade 还是模块内部)都包装成 + `OakUndoCommand` 进入 `oakundo` 栈,命令体内只持 oakmodel 句柄。 + **关键架构事实(拆分顺序的依据)**: - `Node` 及其子类簇(Project/Folder/Footage/Sequence/Block/Track/Clip/Gap/ Transition/Subtitle/各效果节点)是 C++ 继承绑死的**不可拆分类型簇**—— @@ -177,7 +270,8 @@ | M0 | **oakcore** | liboakcore 整体(rational/timecode/bezier/samplebuffer/audioparams,Qt-free) | 无 | 极低;工具链试金石 | | M1 | **oakaudio** | AudioProcessor、AudioSynchronizer、AudioLevelMeter、波形计算 | oakcore | 低;顺带消掉 AudioProcessor 豁免项 | | M2 | **oakcodec** | decoder/encoder/conform/proxy | ffmpeg_bridge | 中;FFmpeg 行为复刻 | -| M3 | **oakserialize** | node/project/serializer/*(XML 项目文件) | oakmodel(经 facade node/project 族) | 中;round-trip 必须字节一致 | +| M3a | **oakstorage** | node/project/serializer 落盘路径 + task/project/{load,save,loadotio,saveotio} 文件 IO(工程持久化,后端可插拔:当前 XML 文件,未来数据库) | oakmodel(经 facade node/project 族) | 中;round-trip 必须字节一致;后端接口一次冻结 | +| M3b | **oakserialize** | node/project/serializer 的剪贴板/节点图 XML 序列化(copy/paste,不落盘) | oakmodel(经 facade node/project 族) | 中;round-trip 必须字节一致 | | M4 | **oakundo** | UndoCommand/UndoStack/MultiUndoCommand | oakmodel(经 facade) | 中;全局调用点多 | | M5 | **oakrender** | RenderManager/ticket/watcher/cache/PreviewAutoCacher/ColorProcessor | oakmodel、oakcodec | 高;线程与 OCIO | | M6 | **oakmodel** | Node/NodeInput/keyframe/traverser/factory/Project/Folder/Footage/Sequence/Block/Track/效果节点 | oakcore、oakcodec | 最高;最大类型簇 | @@ -312,7 +406,8 @@ timeline.h,本就是为外部消费设计的)充当模块间缝,缝的质 1. **S1 完成**:Rust 工具链 + 门禁脚本进 CI;M0(liboakcore)G6 退役。 2. **M1–M2 完成**:音频 DSP 与编解码 Rust 化;AudioProcessor 豁免项消除。 -3. **M3–M4 完成**:序列化与 undo Rust 化;项目文件 round-trip 金标准常青。 +3. **M3a–M4 完成**:工程存储(oakstorage,含后端可插拔接口冻结)、剪贴板 + 序列化与 undo Rust 化;项目文件 round-trip 金标准常青。 4. **M5 完成**:渲染管线 Rust 化(OCIO 孤岛与否已裁决并记录)。 5. **M6 完成**:oakmodel Rust 化——**最大里程碑**,此后 liboakengine 主体为 Rust。 6. **M7–M8 完成**:任务系统与 facade 壳 Rust 化;liboakengine.so(C++ 版)正式退役。 diff --git a/docs/zh/plans/riir/00-overview.md b/docs/zh/plans/riir/00-overview.md index 077ca31c2..d9599daf5 100644 --- a/docs/zh/plans/riir/00-overview.md +++ b/docs/zh/plans/riir/00-overview.md @@ -6,9 +6,10 @@ > 并稳定后,才逐模块用 Rust 重写(届时模块的 C ABI 原样保留,Rust > 实现替换 C++ 实现对调用方透明)。 > -> 阅读顺序:`00`(本文)→ `01-adapter-pattern.md`(适配器规范, -> 所有模块共用)→ `02-modules-and-order.md`(模块清单、依赖矩阵、 -> 拆分顺序)→ `03-testing.md`(测试规范)→ `M1`…`M9`(逐模块执行 +> 阅读顺序:`00`(本文)→ `01-adapter-pattern.md`(适配器规范 + §0 +> 接口铁律,所有模块共用)→ `02-modules-and-order.md`(模块清单、依赖矩阵、 +> 拆分顺序)→ `03-testing.md`(测试规范)→ `04-interfaces.md`(模块间 +> 接口 provides/consumes 全表)→ `M1`…`M10`(逐模块执行 > 手册,**C API 已在各手册中冻结**)。 ## 目标与判据 @@ -18,9 +19,11 @@ ``` oakcore(已有,不动) oakcommon ─ oakundo ─ oaknode ─ oaktimeline ─ oakcodec ─ oakrender ─ oaktask ─ oakplugin - │ - oakaudio ───────────────────┤ - ▼ + │ │ + └────────────── oakstorage(工程持久化, ┘ + 后端可插拔:文件→数据库) + oakaudio ─────────────┐ + ▼ liboakengine(= facade + coreengine,纯装配层) ``` diff --git a/docs/zh/plans/riir/01-adapter-pattern.md b/docs/zh/plans/riir/01-adapter-pattern.md index f6e8eed69..ade6e467a 100644 --- a/docs/zh/plans/riir/01-adapter-pattern.md +++ b/docs/zh/plans/riir/01-adapter-pattern.md @@ -4,6 +4,32 @@ > C API 设计与适配类实现都必须照此执行。命名、内存所有权、错误码、 > 线程与信号的处理在此**冻结**。 +## 0. 接口铁律(2026-08 修订,优先级高于本文件其余各节) + +模块边界上的接口必须是**纯 C 的、面向对象的**: + +1. **对象 = 不透明句柄**。每个跨界类型是一个不透明结构体指针: + `typedef struct OakModClazz OakModClazz;`(**手写 struct 标签的不透明 + 指针**,禁止用 `void *` 充当对象——现有代码里 `void *parent_qobject`、 + `void *oaktask_import_take_command()` 这类用法是反面教材,新接口一律 + 禁止,旧接口在所属模块拆分时顺手改为有类型句柄)。 +2. **构造/析构 = init/free 函数对**:`oakmod_clazz_init*()` / + `oakmod_clazz_free()`。free 对 NULL 是 no-op。 +3. **成员函数 = 首参为 self 句柄的普通函数**: + `oakmod_clazz_(OakModClazz *self, ...)`;静态成员函数无 self + (`_s` 后缀)。 +4. **多态 = 手工虚函数表**。确需"基类句柄 + 多种实现"(如存储后端、 + undo 命令、渲染后端插件)时,在公共头里定义纯 C 函数指针表 + (`typedef struct { ...; int (*save)(...); ... } OakModClazzVTable;`), + 提供侧填充、消费侧经表调用,**不得让 C++ vtable 跨界**。模板见 + `M10-oakstorage.md` §2.3 的 `OakStorageBackend`。 +5. **禁止 C++ 对象跨越动态库边界**:边界上只出现 C 类型(整数、double、 + 指针、`const char *`、纯 C POD、不透明句柄)。C++ 类实例、引用、 + `std::` 类型、Qt 类型一律不得出现在任何模块的 `include/` 公共头里。 +6. **禁止跨界调用 C++ 成员函数**:消费侧对提供侧对象的一切操作必须经 + 该对象的 C ABI 函数;拿到句柄后 `reinterpret_cast` 回 C++ 指针再调 + 成员函数视为违规(nm 审计 + 代码评审双保险)。 + ## 1. 提供侧:C API 层 对被消费的每个 C++ 类 `Clazz`,模块在 `include/oak/clazz.h` @@ -32,7 +58,7 @@ OAKMOD_API oakmod_clazz__s(/* 参数 */); 3. 多个构造重载用后缀区分:`oakmod_clazz_init`(默认)、 `oakmod_clazz_init_from_file`、`oakmod_clazz_init_copy` 等。 4. 命名全小写,模块前缀 `oak_`(oakundo/oaknode/oaktimeline/ - oakcodec/oakrender/oaktask/oakaudio/oakplugin/oakcommon)。 + oakcodec/oakrender/oaktask/oakaudio/oakplugin/oakcommon/oakstorage)。 5. 导出宏 `OAKMOD_API` 照 `oakengine/export.h` 样式 (`__attribute__((visibility("default")))`),模块编译加 `-fvisibility=hidden`——每个模块**出生即 visibility 干净**, @@ -99,22 +125,26 @@ C ABI 上只允许:整数、`double`、`int64_t`、指针、`const char *`、 | `Qt::enum`/内部枚举 | `int`(取值表写进手册,两侧枚举**序数一致性**用 static_assert 或测试钉死) | | `std::shared_ptr` | 不透明句柄 + retain/free(协议见 display.h R7-A 的先例) | -## 4. 信号、回调与线程 +## 4. 信号、回调与线程(2026-08 修订:上层对下层只有命令) -Qt 信号不许跨模块。处理优先级: +Qt 信号不许跨模块;**下层对上层也不许持有回调**——上层调用下层时 +必须知道其影响(改了什么全在返回值/出参里),变更通知由**调用方所在 +层**发出,不经下层反向通知。各模块 C ABI 因此一律不含 +subscribe/unsubscribe 类函数。处理规则: -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 +1. **同步命令**:提供侧把结果放在返回值/出参;消费侧适配类在调用后 + 自行发 Qt 信号(适配类知道刚执行了什么命令,见 §7 例)。原 + `connect()` 到适配类信号的 widget 代码零改动。 +2. **唯一例外——异步任务**:后台执行单元(oaktask 任务、oakrender + 渲染 ticket)提交时拿不到结果,允许进度/完成回调作为该命令的 + 返回通道:`oak__start(handle, done_fn, userdata)` 形式, + 一次性语义,完成后自动失效。 +3. 线程语义照现状:异步回调在发射线程同步调用(DirectConnection 等价),需要跨线程排队是消费侧适配类自己的事(它可以用 `QMetaObject::invokeMethod(..., Qt::QueuedConnection)`)。 -4. **适配类可以把 C 回调再转回 Qt 信号**:适配类继承 QObject、 - 静态 trampoline 里 `emit` 同名信号——消费侧原有 `connect()` 全部 - 零改动。这是大多数 widget 侧适配的默认做法。 +4. **facade→app 的 `oakengine_event` 通道不在此列**:那是引擎对最外层 + 的唯一通知机制(riir.md §6.1),事件由 facade 在命令完成后发射, + 不由下层模块直接发射。 ## 5. 继承与虚函数 @@ -139,40 +169,35 @@ Qt 信号不许跨模块。处理优先级: ```c /* oakundo/include/oakundo/undostack.h */ typedef struct OakUndoStack OakUndoStack; -OAKUNDO_API OakUndoStack *oakundo_undostack_init(void *parent_qobject); +typedef struct OakUndoObjectParent OakUndoObjectParent; /* borrowed QObject 挂载点 */ +OAKUNDO_API OakUndoStack *oakundo_undostack_init( + const OakUndoObjectParent *parent); 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); +OAKUNDO_API int64_t oakundo_undostack_index(const OakUndoStack *self); +/* ... 完整表见 M2 手册(纯命令接口,无任何 subscribe) ... */ ``` ```cpp -// 消费侧 adapter/undostack.h —— app/other 模块里 connect() 零改动 +// 消费侧 adapter/undostack.h —— app/other 模块里 connect() 零改动。 +// 通知规则(§4):适配类发了变更命令,它知道影响,信号由适配类自己 emit。 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_); } + : QObject(p), h_(oakundo_undostack_init( + reinterpret_cast(p))) {} + ~UndoStack() override { oakundo_undostack_free(h_); } void push(UndoCommand *c, const QString &n) { oakundo_undostack_push(h_, c->handle(), n.toUtf8().constData()); + emit index_changed(int(oakundo_undostack_index(h_))); // 命令后自发通知 } 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 index e7e632531..5bf58d9e2 100644 --- a/docs/zh/plans/riir/02-modules-and-order.md +++ b/docs/zh/plans/riir/02-modules-and-order.md @@ -23,6 +23,11 @@ 已知分层违规 1 处:`render/` 引用了 `src/capi/displayinternal.h` (R7-A 重做 display.h 时一并消除)。 +> **2026-08 增补(oakstorage 拆出)**:矩阵扫描早于 oakstorage 单列。 +> 其依赖关系为:oakstorage → node(serializer 落盘路径,含版本化 +> serializerXXXXXX 族)+ common;oaktask → oakstorage(load/save/otio +> 委托);facade → oakstorage。详细契约见 04 §2.10 与 M10。 + ## 2. 模块定义与拆分顺序 顺序原则:叶子先、根后;每步只引入"已拆模块的 C ABI",不引入 @@ -34,11 +39,12 @@ | 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,最大的活) | +| M3a | **oakstorage** | `node/project/serializer` 落盘路径 + `task/project/{load,save,loadotio,saveotio}` 文件 IO(**工程持久化单列**,后端可插拔:当前 XML 文件,未来数据库) | storage→node(序列化建图取图,经 oaknode C ABI);手册 M10 | | 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 | +| M8 | oaktask | `task/`(Task/TaskManager/项目任务编排/cache 任务;**工程文件 IO 已划给 oakstorage,M3a**) | task→node 38、task→codec 4、task→render 4、task→storage(load/save 委托) | | 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 裁决) | diff --git a/docs/zh/plans/riir/03-testing.md b/docs/zh/plans/riir/03-testing.md index 339b3732e..1f820efc5 100644 --- a/docs/zh/plans/riir/03-testing.md +++ b/docs/zh/plans/riir/03-testing.md @@ -29,8 +29,10 @@ 4. **枚举序数一致性**:C 侧 POD/枚举与 C++ 侧枚举的映射(01 §3 表) 每个映射 1 个 TEST(如 `oakundo` 的 movement mode 0-3 ⇄ `Timeline::MovementMode`)。 -5. **事件/回调**:每个 `set_*_cb`/subscribe 至少 1 个 TEST:触发后 - 断言回调被调、payload 正确;反注册后断言不再被调。 +5. **回调(仅异步任务)**:模块间 C ABI 无 subscribe 类接口(04 §3); + 仅异步命令(任务/渲染 ticket)的进度/完成回调需要测试:触发后 + 断言回调被调、payload 正确、FINISHED 后自动失效。同步命令的测试 + 改为"调用后直接读状态断言生效"。 6. **所有权**:borrowed 句柄(文档注释标了 `/* borrowed */` 的) free 后原对象仍存活,1 个 TEST。 diff --git a/docs/zh/plans/riir/04-interfaces.md b/docs/zh/plans/riir/04-interfaces.md new file mode 100644 index 000000000..c90e0f2da --- /dev/null +++ b/docs/zh/plans/riir/04-interfaces.md @@ -0,0 +1,147 @@ +# 04 · 模块间接口设计(provides / consumes 全表) + +> 本文是各模块手册(M1–M10)C API 的**汇总视图**:每个模块对外提供哪些 +> 接口族、消费哪些模块的哪些接口族、边界上流什么数据。逐函数签名以各 +> M 手册的"冻结 C API"节为准;本文管"模块与模块之间的契约关系",管不到 +> 逐函数细节。 +> +> 全部接口遵守 `01-adapter-pattern.md` §0 的铁律:**纯 C、面向对象**—— +> 有类型的不透明句柄(禁止 `void *` 当对象)、`init_*`/`free_*` 构造析构、 +> 首参 self 的普通函数当成员函数、多态用手工函数指针表、C++ 对象与成员 +> 函数一律不跨动态库边界。 + +## 1. 接口关系总表 + +行 = 消费方,列 = 提供方;单元格 = 消费的接口族(详见各提供方手册)。 + +| 消费 ↓ \ 提供 → | oakcommon | oakundo | oaknode | oaktimeline | oakcodec | oakaudio | oakrender | oakstorage | oaktask | oakplugin | oakcore | +|---|---|---|---|---|---|---|---|---|---|---|---| +| **facade**(src/capi) | 工具/类型 | undo 全族 | node/project/footage/serializer(clipboard) 族 | timeline 全族 | decoder/frame/proxy 族 | audio 全族 | renderer/playback/preview 族 | open/save/probe 族 | task/manager 全族 | plugin 全族 | rational/timecode | +| **oaktask** | 工具 | command 句柄 | footage/project/sequence/folder 族 | — | conform/proxy 族 | — | 导出用 render 族 | **load/save/otio 委托** | — | — | — | +| **oakrender** | videoparams/colortransform(M3.5 下沉) | command 句柄 | node 遍历/取值、project、colormanager | — | decoder/frame 族 | 音频参数 | — | — | — | plugin 实例句柄 | — | +| **oakplugin** | 工具 | command 句柄 | node 参数读写 | — | — | — | 帧缓冲/纹理句柄 | — | — | — | — | +| **oakaudio** | 工具 | — | — | — | frame/解码(1 处) | — | videoparams(下沉后消失) | — | — | — | samplebuffer | +| **oakcodec** | videoparams/subtitleparams(下沉) | — | footage 流信息(3 处) | — | — | — | — | — | — | — | rational | +| **oaktimeline** | timelinecommon 枚举(下沉) | command 句柄 | track/block/sequence 族(32 处) | — | — | — | — | — | — | — | — | +| **oaknode** | 枚举/常量/工具 | undocommand.h(4 处) | — | marker/workarea(2 处,M4 反向) | decoder/frame/proxy(8 处,M5 反向) | audio 参数(4 处,M6 反向) | colorprocessor/rendermanager/job(M7 反向,02 §4 裁决 A) | — | — | — | rational/bezier | +| **oakundo** | 工具 | — | — | — | — | — | — | — | — | — | — | +| **oakstorage** | 工具 | — | **project/root/序列化建图取图** | — | — | — | — | — | — | — | — | +| **oakcommon** | — | — | — | — | — | — | — | — | — | — | — | + +(空格 = 无依赖。"N 处"数据来自 02 的 include 扫描。oakcore 与 +ffmpeg_bridge 为现成独立库,不参与拆分顺序。) + +## 2. 逐模块接口契约 + +### 2.1 oakcommon(M1)— 纯下沉,无业务对象 + +- **提供**:`include/oakcommon/types.h` 的全模块共用 POD(时间戳/区间/ + 枚举常量,含 M3.5 下沉的 `OakVideoParams`/`OakSubtitleParams`/ + `OakColorTransform`);工具函数(全 `_s` 静态式,无句柄)。 +- **消费**:无(叶子)。 +- **边界数据**:纯 POD 值,无所有权问题。 + +### 2.2 oakundo(M2)— 手工虚表的第一个用户 + +- **提供**:`OakUndoCommand` / `OakUndoStack` 两类句柄。命令的多态 + (redo/undo 行为)经 **回调函数指针** 实现 + (`oakundo_command_create(name, redo, undo, free_fn, userdata)`), + 即铁律 §0.4 的手工虚表;消费侧**不构造 C++ 子类**。 +- **消费**:oakcommon。 +- **边界数据**:命令句柄(owned)。无事件——push/undo/redo 的调用方 + 知道栈索引变化,通知由调用方(facade 适配层)发出(见 §3)。 + +### 2.3 oaknode(M3)— 最大提供方 + +- **提供**:`OakNodeNode/NodeGroup/NodeKeyframe/NodeFactory/NodeTraverser/ + Project/Folder/Sequence/Track/TrackList/Block/Footage/ColorManager` + 句柄族。逐族清单见 M3 §2。无订阅接口——所有修改经命令函数完成, + 调用方知道影响(§3)。 +- **消费**:oakcommon、oakundo;对 render/codec/audio/timeline 的引用按 + 02 §3/§4 的反向切割表在各模块就位后改经其 C ABI。 +- **边界数据**:节点句柄(borrowed 为主,工程拥有节点)、 + `oak_node_value` POD、id 字符串(buf/size)。 + +### 2.4 oaktimeline(M4) + +- **提供**:marker/workarea/timeline 编辑原语句柄族(`OakTimelineMarker` + 等),timeline 专用 undo 命令**经 oakundo 的回调式命令**注册,不自带 + 命令子类。 +- **消费**:oaknode(32 处,全部经句柄族)、oakundo、oakcommon。 + +### 2.5 oakcodec(M5) + +- **提供**:`OakCodecDecoder/Encoder/Frame/ProxyManager` 句柄族; + 帧以 `OakCodecFrame *` 不透明句柄跨边界(owned,配对 free), + 像素数据经 `oakcodec_frame_data(frame, plane, &linesize)` 取出指针 + (borrowed,生命周期随 frame)。 +- **消费**:oakcommon、oaknode(footage 流信息)、oakcore、ffmpeg_bridge。 + +### 2.6 oakaudio(M6) + +- **提供**:`OakAudioManager/Processor/Synchronizer` 句柄族;波形/电平 + 数据以 POD 数组 + count 出参。 +- **消费**:oakcore(samplebuffer)、oakcodec(1 处)。 + +### 2.7 oakrender(M7) + +- **提供**:`OakRenderRenderer/Ticket/Cache/ColorProcessor` 句柄族; + 渲染结果帧为 owned 句柄;渲染 ticket 是**异步命令**(后台线程), + 进度/完成回调是它的返回通道——这是 §3 允许回调的唯一情形 + (线程语义按 riir.md §6.2 钉死)。 +- **消费**:oaknode、oakcodec、oakcommon、oakundo(1 处)、oakbackend + (GPU 插件,经 `renderbackend_c.h` 手工虚表——现有先例)。 + +### 2.8 oaktask(M8)— 编排者 + +- **提供**:`OakTaskTask` 句柄 + 任务工厂族 + TaskManager 查询函数。 + 任务是**异步命令**:进度/完成回调即其返回通道(§3 唯一例外), + 无其他事件。load/save/import/otio 任务工厂保留,但**实现改为委托 + oakstorage**(见 M10 §4);任务结果(Project、Footage 列表)以 + **有类型句柄**返回(`oaktask_import_take_command` 的 `void *` 返回 + 按铁律 §0.1 改为 `OakUndoCommand *`)。 +- **消费**:oakstorage、oaknode、oakcodec、oakrender、oaktimeline。 + +### 2.9 oakplugin(M9) + +- **提供**:OFX 插件加载/实例句柄族(`OakPluginHost/Instance`)。 +- **消费**:oaknode、oakrender、oakundo。 + +### 2.10 oakstorage(M10,新拆)— 工程持久化 + +- **提供**:`OakStorageProject`(打开的工程会话)、`OakStorageBackend` + (手工虚函数表,后端注册用)两类句柄 + `oakstorage_open/save/probe` + 静态函数。**URI 寻址**:`file://…/*.ove` 走内建 ove-xml 后端;未来 + `oakdb://` 走数据库后端——替换数据库 = 新增一个后端实现并注册, + 消费侧零改动。 +- **消费**:oaknode(反序列化建图 / 序列化取图)、oakcommon。 +- **边界数据**:工程句柄(owned)、XML 字节流(buf/size)、后端表。 + 无事件——open/save 是同步命令,成败与结果全在返回值里,调用方 + (oaktask/facade)知道影响,由它发通知(§3)。 +- 详见 `M10-oakstorage.md`。 + +## 3. 通知的统一约定(2026-08 修订:上层对下层只有命令) + +**铁律:上层调下层只发命令,不调订阅。** 上层调用下层时必须知道该调用的 +影响——改了什么、结果是什么,全部由返回值/出参告知,调用方自己决定后续 +动作。因此: + +- **模块间 C ABI 一律不提供 subscribe/unsubscribe 类接口。** 下层不持有 + 上层的函数指针,不反向通知。各模块手册里原有的 `oak_subscribe` + 设计全部作废(M2/M3/M10 已改)。 +- **变更通知由命令发起方负责。** 例:app 经 facade 调 `undo` 命令后, + facade 知道栈索引变了,由 facade 经既有 `oakengine_event` 通道通知 + app 侧——通知的起点永远是最靠近调用者的那一层。 +- **唯一例外:异步任务。** 后台执行的单元(oaktask 的任务、oakrender 的 + 渲染 ticket)本质上是"提交时拿不到结果"的命令,允许进度/完成回调—— + 回调即该命令的返回通道,线程语义按 01 §4 / riir.md §6.2 钉死。 + 同步接口不得配回调。 + +## 4. 错误与所有权(横向统一) + +- 返回码:`0 = OK`,负值 `OAK_E_*`(值与 oakengine 现有对齐); + 细节经 `oak_last_error(buf, size)`(线程局部)。 +- 所有权注释三档:`/* owned */`(init/take 返回,必须配对 free)、 + `/* borrowed */`(访问器返回,禁 free)、`/* retained */`(register + 类,配对 unregister)。句柄默认 owned。 +- 每个模块提供 `oak_debug_alive_count()`(测试专用,钉死泄漏)。 diff --git a/docs/zh/plans/riir/M10-oakstorage.md b/docs/zh/plans/riir/M10-oakstorage.md new file mode 100644 index 000000000..e38d6f4b3 --- /dev/null +++ b/docs/zh/plans/riir/M10-oakstorage.md @@ -0,0 +1,176 @@ +# M10 · oakstorage 拆分手册(工程持久化,数据库替换预备) + +> 内容:工程文件的读写——`engine/node/project/serializer/` 的**落盘路径** +> (Load/Save/SaveData/LoadData 的文件分支 + 版本化 serializerXXXXXX 族) +> 与 `engine/task/project/{load,save,loadotio,saveotio}` 的**文件 IO 部分**。 +> **不含**剪贴板序列化(copy/paste 的节点图 XML 留在 oaknode 的 +> serializer 族,见 M3)。 +> 依赖:oaknode(project/root/序列化建图取图)、oakcommon。 +> 被依赖:oaktask(load/save 任务委托)、facade。 +> 拆分顺序:M3a(oaknode 之后、oakserialize 同批)。 +> +> **本模块存在的理由**:把"工程从哪来、存到哪去"收敛成唯一模块、唯一 +> 接口,当前后端是 XML .ove 文件,**未来整体替换为数据库**。替换时新增 +> 一个后端实现并注册即可,oaktask / facade / app 零改动。因此本模块的 +> 接口按"存储后端无关"设计:URI 寻址、手工虚函数表、字节流 + 句柄, +> 接口里**不出现任何文件路径特有的概念以外的存储语义**(事务、连接串 +> 等都藏在后端内部)。 + +## 1. 目标形态 + +``` +oakstorage/ + include/oakstorage/{storage.h, backend.h, types.h, export.h} + src/ + capi_storage.cpp # storage.h 实现:URI 分发 + 会话 + backends/ove_xml/ # 内建后端:XML .ove(现有 serializer 落盘路径迁入) + backends/... # 未来:oakdb(数据库后端) + tests/ # oakstorage_gtest +``` + +## 2. 冻结 C API + +遵守 01 §0 铁律:纯 C、有类型不透明句柄、init/free、self 首参、 +多态用手工虚函数表。 + +### 2.1 `oakstorage/types.h` + +```c +/* 错误码(0=OK,负值错误;版本/格式探测用正值信息码) */ +#define OAKSTORAGE_OK 0 +#define OAKSTORAGE_TOO_OLD 1 /* 工程版本过旧(信息码) */ +#define OAKSTORAGE_TOO_NEW 2 /* 工程版本过新 */ +#define OAKSTORAGE_UNKNOWN_VERSION 3 +#define OAKSTORAGE_E_INVALID -1 +#define OAKSTORAGE_E_STATE -2 +#define OAKSTORAGE_E_NOT_FOUND -3 +#define OAKSTORAGE_E_FAILED -4 +#define OAKSTORAGE_E_NO_BACKEND -5 /* 无后端认领该 URI */ +#define OAKSTORAGE_E_FORMAT -6 /* 解析失败(XML/DB 约束) */ +#define OAKSTORAGE_E_IO -7 /* 读写失败 */ +``` + +### 2.2 `oakstorage/storage.h`(消费侧主接口) + +```c +/* 打开的工程会话:包裹一个已加载(或待保存)的 oakmodel Project。 + * owned 句柄,配对 oakstorage_project_free。 */ +typedef struct OakStorageProject OakStorageProject; + +/* --- 静态函数(无 self,对应"类方法") --- + * 全部是同步命令:成败与结果全在返回值/出参里,调用方知道影响, + * 由调用方(oaktask/facade)负责对外通知;本模块无任何回调/事件。 */ + +/* 探测 URI:返回认领该 URI 的后端名(buf/size,先 NULL 查长度), + * 或负值(OAKSTORAGE_E_NO_BACKEND)。不写盘、不建会话。 */ +OAKSTORAGE_API int oakstorage_probe(const char *uri, char *buf, int buf_size); + +/* 打开工程(load)。URI scheme 选后端: + * file:///path/to/proj.ove → ove-xml 后端 + * file:///…/proj.otio → otio 后端(import 语义) + * oakdb://… → 未来数据库后端 + * 失败返回 NULL,细节经 oakstorage_last_error。 */ +OAKSTORAGE_API OakStorageProject *oakstorage_open( + const char *uri, int *result_code); + +/* 把 project 保存到 URI(save / save-as)。 + * `options`:位掩码,OAKSTORAGE_SAVE_COMPRESS 等;后端忽略不识别的位。 */ +#define OAKSTORAGE_SAVE_COMPRESS 0x1 +OAKSTORAGE_API int oakstorage_save(OakNodeProject *project, + const char *uri, unsigned options); + +/* --- 成员函数(self 首参) --- */ + +OAKSTORAGE_API void oakstorage_project_free(OakStorageProject *self); +/* 取出工程句柄:所有权转移给调用方(此后 self 为空壳,仍须 free)。 + * 对应 oakengine/task.h 的 take_project 语义。 */ +OAKSTORAGE_API OakNodeProject *oakstorage_project_take_project( + OakStorageProject *self); +/* borrowed:不转移所有权 */ +OAKSTORAGE_API OakNodeProject *oakstorage_project_project( + const OakStorageProject *self); +/* 会话来源 URI(buf/size) */ +OAKSTORAGE_API int oakstorage_project_uri(const OakStorageProject *self, + char *buf, int buf_size); + +OAKSTORAGE_API int oakstorage_last_error(char *buf, int buf_size); +OAKSTORAGE_API int oakstorage_debug_alive_count(void); /* 测试专用 */ +``` + +### 2.3 `oakstorage/backend.h`(手工虚函数表——后端注册接口) + +多态按 01 §0.4:纯 C 函数指针表,提供侧(后端)填充,oakstorage 核心 +经表调用。**后端实现不进公共头**;数据库后端未来只是多注册一行。 + +```c +/* 存储后端虚表。所有函数必需;返回码用 OAKSTORAGE_*。 + * 句柄协议:load 成功时 *out_project 收到 owned OakNodeProject*; + * 后端可在 vtable 之外持有任意私有状态(连接池、事务句柄等)。 */ +typedef struct OakStorageBackend { + const char *name; /* "ove-xml" / "otio" / "oakdb"(静态字符串) */ + const char *uri_scheme; /* "file" / "oakdb";同一 scheme 可注册多个 + * 后端,按 can_handle 顺序裁决 */ + + /* 是否认领该 URI(后缀、magic bytes、连接可达性等,后端自决) */ + int (*can_handle)(const char *uri); + + /* 加载:URI → owned Project 句柄;*result_code 收 OAKSTORAGE_* */ + OakNodeProject *(*load)(const char *uri, int *result_code, + char *err_buf, int err_buf_size); + + /* 保存:Project → URI;options 透传 storage.h 的位掩码 */ + int (*save)(OakNodeProject *project, const char *uri, unsigned options, + char *err_buf, int err_buf_size); +} OakStorageBackend; + +/* 注册/注销。oakstorage 核心不拷贝表体——后端必须保证表与 name 字符串 + * 在 unregister 前存活(内建后端为静态存储期,天然满足)。 */ +OAKSTORAGE_API int oakstorage_backend_register(const OakStorageBackend *backend); +OAKSTORAGE_API int oakstorage_backend_unregister(const char *name); +``` + +**数据库替换路径(未来的活,接口已预留)**: +1. 写 `backends/oakdb/`:`can_handle` 认 `oakdb://`,`load/save` 走 SQL, + 建表 schema 是后端私事; +2. `oakstorage_backend_register(&oakdb_backend);` 一行接入; +3. oaktask/facade/app 不动;`file://` 的 .ove 后端继续共存(迁移期 + 双后端并存,经 URI 显式选择)。 +**反向约束**:任何"必须改本手册 §2.2 才能接数据库"的需求,说明接口 +冻结有洞——先改本手册再动手。 + +### 2.4 与 oaktask 的边界(任务只是壳) + +M8 的任务工厂保留原签名,实现改为薄委托: + +```c +/* oaktask/project.cpp(概念) */ +OakTaskTask *oaktask_create_project_load(const char *filename) { + /* 任务体内:oakstorage_open(uri) → 完成回调里 + * oakstorage_project_take_project() */ +} +``` + +import/conform/precache 等非工程 IO 任务不经 oakstorage。 + +## 3. 切割点 + +| 现状 | 处理 | +|---|---| +| `node/project/serializer/*` 落盘路径在 oaknode 内 | 文件分支 + serializerXXXXXX 版本族迁入 `oakstorage/backends/ove_xml/`;**节点图 XML 生成/解析(SaveData/LoadData 的内存形态)仍属 oaknode**,ove-xml 后端经 oaknode C ABI(`oaknode_serializer_*` 族)取图/建图。剪贴板分支留 oaknode(M3/M3b) | +| `task/project/load|save` 含文件 IO | IO 部分下沉 oakstorage;task 保留任务编排(进度、取消、事件) | +| `task/project/loadotio|saveotio` | 同上,注册为 "otio" 后端(scheme=file,can_handle 认 .otio) | +| serializer 对 Qt 文件对话框/布局的引用 | 布局信息(SerializedLayoutInfo)随保存走 options 的不透明 blob(buf/size),不进入本手册冻结面 | + +## 4. 测试(映射 03 §2/§3) + +- **round-trip 字节一致**(金标准):project_with_footage.ove → + `oakstorage_open` → `oakstorage_save` 到临时 URI → 两文件字节一致; + 再 load 后 `oakstorage_project_project()` 非空、root 非空。 +- probe:.ove(压缩/未压缩各一)、.otio、未知 scheme → + E_NO_BACKEND。 +- 错误路径:不存在文件 open → NULL + last_error 非空;TOO_NEW 版本 + 头 → result_code = OAKSTORAGE_TOO_NEW。 +- 后端虚表:注册一个内存 mock 后端(`mem://`,load/save 记日志), + 断言 open/save 全走虚表、unregister 后 probe 报 E_NO_BACKEND—— + **这条用例就是"数据库可插拔"的接口验证**。 +- `oakstorage_debug_alive_count()`:open/take/free 配对无泄漏。 diff --git a/docs/zh/plans/riir/M2-oakundo.md b/docs/zh/plans/riir/M2-oakundo.md index cfc3f9e05..8dc01a939 100644 --- a/docs/zh/plans/riir/M2-oakundo.md +++ b/docs/zh/plans/riir/M2-oakundo.md @@ -40,7 +40,12 @@ OAKUNDO_API void oakundo_command_free(OakUndoCommand *cmd); /* NULL no-op */ ```c typedef struct OakUndoStack OakUndoStack; -OAKUNDO_API OakUndoStack *oakundo_undostack_init(void *parent_qobject); +typedef struct OakUndoObjectParent OakUndoObjectParent; /* borrowed QObject 挂载点 */ +OAKUNDO_API OakUndoStack *oakundo_undostack_init( + const OakUndoObjectParent *parent); +/* OakUndoObjectParent:QObject 父子树挂载点的 borrowed 不透明句柄 + * (2026-08 修订:按 01 §0.1 由 void* 改为有类型句柄; + * 提供侧内部即 QObject*,消费侧不可解引用;可传 NULL 表无父) */ OAKUNDO_API void oakundo_undostack_free(OakUndoStack *self); OAKUNDO_API void oakundo_undostack_push(OakUndoStack *self, @@ -65,16 +70,13 @@ 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); ``` +**无事件接口(2026-08 修订)**:上层对下层只有命令。push/undo/redo/jump +的调用方知道栈索引的变化(`oakundo_undostack_index` 调用后即可读), +`index_changed` 通知改由**调用方所在层**(facade 的 undo 适配层)在 +每次变更命令后自行发出——oakundo 不持有任何上层回调。 + ## 3. 切割点(1 处) `undocommand.cpp` include `node/project.h`(`get_relevant_project()` @@ -96,7 +98,8 @@ Project* 本来就只是作为不透明身份被使用(修改标记归属) - 每条 API 正常+错误路径(NULL self、空 push 不入栈—— 空 MultiUndoCommand 被删除的既有行为必须有 TEST 钉死)。 -- push/undo/redo/jump/clear 全序列;command_text/is_done 边界行。 -- 事件:push 后 index_changed 触发且 a=新 index;unsubscribe 后不再触发。 +- push/undo/redo/jump/clear 全序列;command_text/is_done 边界行; + 每步后 `oakundo_undostack_index` 读数与预期一致(替代原事件断言—— + 调用方知道影响,直接读状态)。 - 往返测试: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 index 13bd5a3bf..7eb6c550d 100644 --- a/docs/zh/plans/riir/M3-oaknode.md +++ b/docs/zh/plans/riir/M3-oaknode.md @@ -40,16 +40,18 @@ project.h、timeline.h 的对应函数就是模板,参数命名前缀换 | 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 模板) | +| ProjectSerializer | task / oakstorage | **剪贴板** copy/paste + 节点图 XML 的内存形态(SaveData/LoadData,照 oakengine/serializer.h 模板);**落盘 save/load 迁 oakstorage(M10),不在本模块** | | 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 逐步换绑)。 +1. undoable 变体与 live 变体成对(`_live` 后缀或 + `, OakUndoCommand *command` 尾参——有类型句柄,禁 void*), + 与 oakengine 现状一致。 +2. 无事件订阅接口(2026-08 修订,04 §3):oaknode 的所有修改都经 + 命令函数完成,调用方知道影响;Node 族的变更通知(label/input/ + keyframe/context 等)由 facade 在执行命令后经既有 `oakengine_event` + 通道发出(事件 id 沿用 `oakengine/events.h` 70-95 段,值不变), + oaknode 自身不持有任何上层回调。 3. `Node *`、`Project *` 等句柄即 `OakNodeNode *`/`OakNodeProject *`, 不透明。 4. 虚函数不出模块(01 §5);具体节点类型经 @@ -73,8 +75,8 @@ M3 阶段判据(放宽版):oaknode 目录就位、C API 实现、oaknode_g - 重点:Node 增删连边、Project/Folder 层级、Track 属性、 keyframe live/undoable 对称、serializer 剪贴板往返、 - factory 枚举与创建。 -- 事件:每族至少 1 个 subscribe/trigger/unsubscribe 用例。 + factory 枚举与创建;每个变更命令执行后直接读状态断言生效 + (无事件可断言——通知在 facade 层测)。 - 枚举序数: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 index 0105e6685..51e505c59 100644 --- a/docs/zh/plans/riir/M4-oaktimeline.md +++ b/docs/zh/plans/riir/M4-oaktimeline.md @@ -35,17 +35,16 @@ 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 时自行入栈 */ + OakUndoCommand *command); /* command=NULL 时自行入栈(2026-08:void* → 有类型句柄) */ OAKTL_API int oaktimeline_marker_remove_at(OakTimelineMarkerList *l, - int i, void *command); + int i, OakUndoCommand *command); OAKTL_API int oaktimeline_marker_set_time(OakTimelineMarkerList *l, - int i, int64_t in_ts, int64_t out_ts, void *command); + int i, int64_t in_ts, int64_t out_ts, OakUndoCommand *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); + int i, int color, const char *name, OakUndoCommand *command); +/* 无事件接口(2026-08 修订,04 §3):marker 的增删改都是调用方发的 + * 命令,MARKER_ADDED/REMOVED/MODIFIED 通知由调用方所在层(facade, + * id 沿用 oakengine events 段)在命令后发出。 */ ``` ### 2.2 `oaktimeline/workarea.h` @@ -58,9 +57,9 @@ 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); + int64_t old_in_ts, int64_t old_out_ts, OakUndoCommand *command); OAKTL_API int oaktimeline_workarea_set_enabled_undoable( - OakTimelineWorkarea *w, int enabled, void *command); + OakTimelineWorkarea *w, int enabled, OakUndoCommand *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, @@ -107,7 +106,8 @@ OAKTL_API int64_t oaktimeline_nearest_block_ts(OakNodeTrack *track, ## 4. 测试(映射 03 §2/§3) -- marker:增删改查、undo 往返(push 后 undo 恢复)、事件三件套。 +- marker:增删改查、undo 往返(push 后 undo 恢复);每命令后读 + count/at 断言生效(无事件——通知在 facade 层测)。 - workarea:set/get、undoable 旧值恢复、reset 哨兵、xml 往返。 - edit:每个 `_command` 工厂 1 个"构造→入栈→undo 还原"用例 (track 增删、place/replace/trim/split/ripple/slide)。 diff --git a/docs/zh/plans/riir/M6-oakaudio.md b/docs/zh/plans/riir/M6-oakaudio.md index cd2437627..5aa254f82 100644 --- a/docs/zh/plans/riir/M6-oakaudio.md +++ b/docs/zh/plans/riir/M6-oakaudio.md @@ -45,10 +45,8 @@ 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); +/* 无事件接口(2026-08 修订,04 §3):输出参数的修改是调用方发的 + * 命令(set_params),params_changed 通知由调用方所在层发出。 */ ``` ## 3. 切割点 @@ -63,5 +61,5 @@ OAKAU_API void oakaudio_unsubscribe(int64_t id); - processor:open/convert/close 全链(44.1k stereo → 48k stereo, 帧数换算正确、无爆音断言用能量差阈值)、速度 1.5x。 - manager:无音频设备环境用 null backend 初始化(现有后端探测 - 模式),params set/get 往返、事件触发。 + 模式),params set/get 往返(set 后 get 读数即生效——无事件)。 - `oakaudio_debug_alive_count()` 泄漏断言。 diff --git a/docs/zh/plans/riir/M7-oakrender.md b/docs/zh/plans/riir/M7-oakrender.md index 8d9a4a129..b42998aea 100644 --- a/docs/zh/plans/riir/M7-oakrender.md +++ b/docs/zh/plans/riir/M7-oakrender.md @@ -51,9 +51,10 @@ OAKRD_API int oakrender_frame_cache_load(OakRenderCache *c, 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); +/* 无缓存事件(2026-08 修订,04 §3):缓存的 invalidate/validate 由 + * 编辑命令的调用方触发并知情,通知由 facade 在命令后发出; + * oakrender 不持有上层回调。渲染 ticket 的完成回调属异步命令 + * 返回通道,不在此限(见 renderer.h 族)。 */ ``` ### 2.3 `oakrender/color.h` @@ -107,7 +108,8 @@ OAKRD_API int oakrender_disk_cache_clear(void); ## 4. 测试(映射 03 §2/§3) -- cache:invalidate/validate 状态机、帧缓存存取往返、事件触发。 +- cache:invalidate/validate 状态机、帧缓存存取往返(调用方触发后 + 读状态断言——无事件)。 - color:默认 config 建置、processor convert 已知值(sRGB→Linear 抽样点数值断言,容差 1e-3)。 - manager:request_frame 对 demo.mp4 + 最小 sequence 出帧非空 diff --git a/docs/zh/plans/riir/M8-oaktask.md b/docs/zh/plans/riir/M8-oaktask.md index bc1e04d4c..60a14e95b 100644 --- a/docs/zh/plans/riir/M8-oaktask.md +++ b/docs/zh/plans/riir/M8-oaktask.md @@ -1,7 +1,8 @@ # M8 · oaktask 拆分手册 -> 内容:`engine/task/`(Task 基类、TaskManager、project/ -> load/save/import/loadotio/saveotio、cache 任务)。 +> 内容:`engine/task/`(Task 基类、TaskManager、任务编排、cache 任务; +> **工程文件 IO——project/load/save/loadotio/saveotio 的落盘部分—— +> 已划给 oakstorage(M10),本模块只做任务壳**)。 > 依赖:node 38、codec 4、render 4、common 4、config 2、timeline 1、 > coreengine 2。 > 拆分顺序第 8 位。 @@ -30,7 +31,8 @@ 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 段) */ +/* 进度/完成回调:任务是异步命令,回调即其返回通道——04 §3 唯一 + * 例外情形(一次性语义,FINISHED 后自动失效) */ OAKTK_API int64_t oaktask_task_subscribe(OakTaskTask *t, int32_t event_id, oaktask_event_fn fn, void *userdata); ``` @@ -42,6 +44,9 @@ 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); +/* 注:以上两个工厂是薄壳——文件 IO 全部委托 oakstorage(M10 §2.4), + * 任务只保留进度/取消/事件编排;`use_compression` 等打包进 + * oakstorage_save 的 options 位掩码。 */ OAKTK_API OakTaskTask *oaktask_create_project_import(OakNodeNode *folder, const char *const *urls, int url_count); OAKTK_API OakTaskTask *oaktask_create_project_load_otio( @@ -50,7 +55,7 @@ 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 OakUndoCommand *oaktask_import_take_command(OakTaskTask *t); /* 所有权转移(2026-08 修订:按 01 §0.1 由 void* 改为有类型句柄) */ 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); @@ -66,10 +71,9 @@ OAKTK_API OakNodeProject *oaktask_load_take_project(OakTaskTask *t); 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); +/* 无 manager 事件(2026-08 修订,04 §3):任务的创建/删除都由调用方 + * 发起(facade 建任务即知 ADDED;delete_finished 的调用方知 REMOVED), + * LIST_CHANGED 通知由调用方所在层发出。 */ ``` ## 3. 切割点 @@ -77,6 +81,7 @@ OAKTK_API void oaktask_unsubscribe(int64_t id); | 现状 | 处理 | |---|---| | task → node/ 38(footage 6、project 5、sequence 3、serializer/layout 3、colormanager 2 等) | 全部经 oaknode C ABI + 适配类(M3 已就位)——本手册工作量主体 | +| task → 工程文件 IO(load/save/loadotio/saveotio 落盘) | 划给 **oakstorage**(M3a/M10):任务工厂保留签名,实现改为委托 `oakstorage_open/save` | | task → codec/ 4 | 经 oakcodec C ABI(M5) | | task → render/ 4 | 经 oakrender C ABI(M7) | | task → timeline/ 1 | 经 oaktimeline C ABI(M4) | @@ -88,7 +93,7 @@ OAKTK_API void oaktask_unsubscribe(int64_t id); 入栈)、save/load 往返(临时目录 .ove,load 后 project 非空、 root 非空)。 - start_sync 成功/失败路径(不存在文件 → failed + error 非空)。 -- 事件:STARTED→PROGRESS→FINISHED 序列(导入任务断言至少一次 - PROGRESS 且 FINISHED.succeeded==1)。 -- manager:添加/删除/list_changed 事件。 +- 事件(异步任务例外):STARTED→PROGRESS→FINISHED 序列(导入任务 + 断言至少一次 PROGRESS 且 FINISHED.succeeded==1)。 +- manager:count/at/delete_finished 行为(无事件,直接读状态断言)。 - `oaktask_debug_alive_count()` 泄漏断言。 diff --git a/docs/zh/plans/riir/M9-oakplugin.md b/docs/zh/plans/riir/M9-oakplugin.md index 66c193067..74fe7bba0 100644 --- a/docs/zh/plans/riir/M9-oakplugin.md +++ b/docs/zh/plans/riir/M9-oakplugin.md @@ -43,7 +43,9 @@ 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 回调,沿用该机制) */ +/* 进度回调:instance_render 是异步命令(渲染中进行),进度/取消回调 + * 即其返回通道——04 §3 唯一例外情形(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);