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
This commit is contained in:
2026-08-05 14:48:08 +08:00
parent b55d626c0c
commit 774eda75fe
14 changed files with 563 additions and 97 deletions
+102 -7
View File
@@ -147,11 +147,11 @@
liboakengine-facade(壳:capi + 事件 + init liboakengine-facade(壳:capi + 事件 + init
┌────────┬────────┼─────────┬──────────┐ ┌────────┬────────┼─────────┬──────────┬──────────────
oaktask oakrender oakplugin oakaudio oakserialize oaktask oakrender oakplugin oakaudio oakserialize oakstorage
│ │ │ │ │ │ │ │ │ │ (工程文件读写,
└────────┴────┬───┴──────────┴──────────┘ └────────┴────┬───┴──────────┴──────────┘ 独立模块,未来
替换为数据库)
oakmodel(节点图 + 项目模型 + 时间线模型) oakmodel(节点图 + 项目模型 + 时间线模型)
┌────────┼─────────┐ ┌────────┼─────────┐
@@ -160,6 +160,99 @@
ffmpeg_bridge(已是 C ABI .so 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<mod>_*`,调用方知道影响);虚线 = 仅两种允许的反向通知:
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<br/>壳:oakengine_* 公共 C ABI(冻结)<br/>+ 事件注册表 + init/shutdown"]
subgraph 引擎模块["引擎模块(facade 之下,只经内部 C ABI 互调)"]
TASK["oaktask<br/>任务编排(Task/TaskManager"]
RENDER["oakrender<br/>渲染管线/缓存/色彩"]
PLUGIN["oakplugin<br/>OpenFX 宿主"]
AUDIO["oakaudio<br/>音频 DSP/输出/电平"]
SERIAL["oakserialize<br/>节点图 XML 序列化<br/>(剪贴板 copy/paste,不落盘)"]
STORAGE["oakstorage<br/>工程持久化(URI 打开/保存/探测)<br/>后端可插拔:ove-xml 文件|未来 oakdb 数据库"]
UNDO["oakundo<br/>UndoStack/UndoCommand"]
MODEL["oakmodel<br/>Node 类型簇 + Project/Sequence/Track/Block<br/>(最大不可拆分类型簇)"]
CODEC["oakcodec<br/>decoder/encoder/conform/proxy"]
CORE["liboakcore<br/>rational/timecode/bezier/samplebuffer"]
BACKEND["oakbackend<br/>GPU 插件:oakgl / oakvulkan / 未来 Rust(wgpu)"]
FFMPEG["ffmpeg_bridge<br/>FFmpeg 的 C ABI 桥(现成 .so"]
end
APP -->|"oakengine_* 调用"| FACADE
CLI --> FACADE
WRK --> FACADE
FACADE -.->|"oakengine_event 变更通知<br/>(发射线程同步回调)"| APP
FACADE -->|"任务工厂/进度"| TASK
FACADE -->|"渲染请求/帧句柄"| RENDER
FACADE --> AUDIO
FACADE --> PLUGIN
FACADE -->|"工程打开/保存(URI"| STORAGE
FACADE --> UNDO
TASK -->|"load/save 任务委托<br/>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/ - `Node` 及其子类簇(Project/Folder/Footage/Sequence/Block/Track/Clip/Gap/
Transition/Subtitle/各效果节点)是 C++ 继承绑死的**不可拆分类型簇**—— Transition/Subtitle/各效果节点)是 C++ 继承绑死的**不可拆分类型簇**——
@@ -177,7 +270,8 @@
| M0 | **oakcore** | liboakcore 整体(rational/timecode/bezier/samplebuffer/audioparamsQt-free | 无 | 极低;工具链试金石 | | M0 | **oakcore** | liboakcore 整体(rational/timecode/bezier/samplebuffer/audioparamsQt-free | 无 | 极低;工具链试金石 |
| M1 | **oakaudio** | AudioProcessor、AudioSynchronizer、AudioLevelMeter、波形计算 | oakcore | 低;顺带消掉 AudioProcessor 豁免项 | | M1 | **oakaudio** | AudioProcessor、AudioSynchronizer、AudioLevelMeter、波形计算 | oakcore | 低;顺带消掉 AudioProcessor 豁免项 |
| M2 | **oakcodec** | decoder/encoder/conform/proxy | ffmpeg_bridge | 中;FFmpeg 行为复刻 | | 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 | 中;全局调用点多 | | M4 | **oakundo** | UndoCommand/UndoStack/MultiUndoCommand | oakmodel(经 facade | 中;全局调用点多 |
| M5 | **oakrender** | RenderManager/ticket/watcher/cache/PreviewAutoCacher/ColorProcessor | oakmodel、oakcodec | 高;线程与 OCIO | | 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 | 最高;最大类型簇 | | M6 | **oakmodel** | Node/NodeInput/keyframe/traverser/factory/Project/Folder/Footage/Sequence/Block/Track/效果节点 | oakcore、oakcodec | 最高;最大类型簇 |
@@ -312,7 +406,8 @@ timeline.h,本就是为外部消费设计的)充当模块间缝,缝的质
1. **S1 完成**:Rust 工具链 + 门禁脚本进 CIM0liboakcoreG6 退役。 1. **S1 完成**:Rust 工具链 + 门禁脚本进 CIM0liboakcoreG6 退役。
2. **M1M2 完成**:音频 DSP 与编解码 Rust 化;AudioProcessor 豁免项消除。 2. **M1M2 完成**:音频 DSP 与编解码 Rust 化;AudioProcessor 豁免项消除。
3. **M3M4 完成**序列化与 undo Rust 化;项目文件 round-trip 金标准常青。 3. **M3aM4 完成**工程存储(oakstorage,含后端可插拔接口冻结)、剪贴板
序列化与 undo Rust 化;项目文件 round-trip 金标准常青。
4. **M5 完成**:渲染管线 Rust 化(OCIO 孤岛与否已裁决并记录)。 4. **M5 完成**:渲染管线 Rust 化(OCIO 孤岛与否已裁决并记录)。
5. **M6 完成**oakmodel Rust 化——**最大里程碑**,此后 liboakengine 主体为 Rust。 5. **M6 完成**oakmodel Rust 化——**最大里程碑**,此后 liboakengine 主体为 Rust。
6. **M7M8 完成**:任务系统与 facade 壳 Rust 化;liboakengine.soC++ 版)正式退役。 6. **M7M8 完成**:任务系统与 facade 壳 Rust 化;liboakengine.soC++ 版)正式退役。
+9 -6
View File
@@ -6,9 +6,10 @@
> 并稳定后,才逐模块用 Rust 重写(届时模块的 C ABI 原样保留,Rust > 并稳定后,才逐模块用 Rust 重写(届时模块的 C ABI 原样保留,Rust
> 实现替换 C++ 实现对调用方透明)。 > 实现替换 C++ 实现对调用方透明)。
> >
> 阅读顺序:`00`(本文)→ `01-adapter-pattern.md`(适配器规范 > 阅读顺序:`00`(本文)→ `01-adapter-pattern.md`(适配器规范 + §0
> 所有模块共用)→ `02-modules-and-order.md`(模块清单、依赖矩阵、 > 接口铁律,所有模块共用)→ `02-modules-and-order.md`(模块清单、依赖矩阵、
> 拆分顺序)→ `03-testing.md`(测试规范)→ `M1`…`M9`模块执行 > 拆分顺序)→ `03-testing.md`(测试规范)→ `04-interfaces.md`(模块
> 接口 provides/consumes 全表)→ `M1`…`M10`(逐模块执行
> 手册,**C API 已在各手册中冻结**)。 > 手册,**C API 已在各手册中冻结**)。
## 目标与判据 ## 目标与判据
@@ -18,9 +19,11 @@
``` ```
oakcore(已有,不动) oakcore(已有,不动)
oakcommon ─ oakundo ─ oaknode ─ oaktimeline ─ oakcodec ─ oakrender ─ oaktask ─ oakplugin oakcommon ─ oakundo ─ oaknode ─ oaktimeline ─ oakcodec ─ oakrender ─ oaktask ─ oakplugin
oakaudio ───────────────────┤ └────────────── oakstorage(工程持久化, ┘
后端可插拔:文件→数据库)
oakaudio ─────────────┐
liboakengine= facade + coreengine,纯装配层) liboakengine= facade + coreengine,纯装配层)
``` ```
+54 -29
View File
@@ -4,6 +4,32 @@
> C API 设计与适配类实现都必须照此执行。命名、内存所有权、错误码、 > 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_<func>(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 层 ## 1. 提供侧:C API 层
对被消费的每个 C++ 类 `Clazz`,模块在 `include/oak<mod>/clazz.h` 对被消费的每个 C++ 类 `Clazz`,模块在 `include/oak<mod>/clazz.h`
@@ -32,7 +58,7 @@ OAKMOD_API <ret> oakmod_clazz_<func>_s(/* 参数 */);
3. 多个构造重载用后缀区分:`oakmod_clazz_init`(默认)、 3. 多个构造重载用后缀区分:`oakmod_clazz_init`(默认)、
`oakmod_clazz_init_from_file``oakmod_clazz_init_copy` 等。 `oakmod_clazz_init_from_file``oakmod_clazz_init_copy` 等。
4. 命名全小写,模块前缀 `oak<mod>_`oakundo/oaknode/oaktimeline/ 4. 命名全小写,模块前缀 `oak<mod>_`oakundo/oaknode/oaktimeline/
oakcodec/oakrender/oaktask/oakaudio/oakplugin/oakcommon)。 oakcodec/oakrender/oaktask/oakaudio/oakplugin/oakcommon/oakstorage)。
5. 导出宏 `OAKMOD_API``oakengine/export.h` 样式 5. 导出宏 `OAKMOD_API``oakengine/export.h` 样式
`__attribute__((visibility("default")))`),模块编译加 `__attribute__((visibility("default")))`),模块编译加
`-fvisibility=hidden`——每个模块**出生即 visibility 干净** `-fvisibility=hidden`——每个模块**出生即 visibility 干净**
@@ -99,22 +125,26 @@ C ABI 上只允许:整数、`double`、`int64_t`、指针、`const char *`、
| `Qt::enum`/内部枚举 | `int`(取值表写进手册,两侧枚举**序数一致性**用 static_assert 或测试钉死) | | `Qt::enum`/内部枚举 | `int`(取值表写进手册,两侧枚举**序数一致性**用 static_assert 或测试钉死) |
| `std::shared_ptr<T>` | 不透明句柄 + retain/free(协议见 display.h R7-A 的先例) | | `std::shared_ptr<T>` | 不透明句柄 + retain/free(协议见 display.h R7-A 的先例) |
## 4. 信号、回调与线程 ## 4. 信号、回调与线程(2026-08 修订:上层对下层只有命令)
Qt 信号不许跨模块。处理优先级: Qt 信号不许跨模块;**下层对上层也不许持有回调**——上层调用下层时
必须知道其影响(改了什么全在返回值/出参里),变更通知由**调用方所在
层**发出,不经下层反向通知。各模块 C ABI 因此一律不含
subscribe/unsubscribe 类函数。处理规则:
1. **回调注册**`oakmod_clazz_set_<event>_cb(self, fn, userdata)` 1. **同步命令**提供侧把结果放在返回值/出参;消费侧适配类在调用后
提供侧在发信号处调 `fn(event_pod, userdata)`。userdata 所有权归 自行发 Qt 信号(适配类知道刚执行了什么命令,见 §7 例)。原
注册方,适配类析构时先 `set_*_cb(self, NULL, NULL)` 反注册 `connect()` 到适配类信号的 widget 代码零改动
2. **事件总线**:模块级通知(非单对象)用 2. **唯一例外——异步任务**:后台执行单元(oaktask 任务、oakrender
`oakmod_subscribe(event_id, fn, userdata)` → 返回订阅 id 渲染 ticket)提交时拿不到结果,允许进度/完成回调作为该命令的
`oakmod_unsubscribe(id)`——照 `oakengine/events.h` 的现成模式。 返回通道:`oak<mod>_<async>_start(handle, done_fn, userdata)` 形式,
3. 线程语义照现状:提供侧在发射线程同步调回调(DirectConnection 一次性语义,完成后自动失效。
3. 线程语义照现状:异步回调在发射线程同步调用(DirectConnection
等价),需要跨线程排队是消费侧适配类自己的事(它可以用 等价),需要跨线程排队是消费侧适配类自己的事(它可以用
`QMetaObject::invokeMethod(..., Qt::QueuedConnection)`)。 `QMetaObject::invokeMethod(..., Qt::QueuedConnection)`)。
4. **适配类可以把 C 回调再转回 Qt 信号**:适配类继承 QObject、 4. **facade→app 的 `oakengine_event` 通道不在此列**:那是引擎对最外层
静态 trampoline 里 `emit` 同名信号——消费侧原有 `connect()` 全部 的唯一通知机制(riir.md §6.1),事件由 facade 在命令完成后发射,
零改动。这是大多数 widget 侧适配的默认做法 不由下层模块直接发射
## 5. 继承与虚函数 ## 5. 继承与虚函数
@@ -139,40 +169,35 @@ Qt 信号不许跨模块。处理优先级:
```c ```c
/* oakundo/include/oakundo/undostack.h */ /* oakundo/include/oakundo/undostack.h */
typedef struct OakUndoStack OakUndoStack; 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_free(OakUndoStack *self);
OAKUNDO_API void oakundo_undostack_push(OakUndoStack *self, OAKUNDO_API void oakundo_undostack_push(OakUndoStack *self,
OakUndoCommand *cmd, const char *name); OakUndoCommand *cmd, const char *name);
OAKUNDO_API int oakundo_undostack_can_undo(const OakUndoStack *self); OAKUNDO_API int oakundo_undostack_can_undo(const OakUndoStack *self);
/* ... 完整表见 M2 手册 ... */ OAKUNDO_API int64_t oakundo_undostack_index(const OakUndoStack *self);
OAKUNDO_API int64_t oakundo_undostack_subscribe(OakUndoStack *self, /* ... 完整表见 M2 手册(纯命令接口,无任何 subscribe ... */
int event_id, oakundo_event_fn fn, void *userdata);
OAKUNDO_API void oakundo_unsubscribe(int64_t id);
``` ```
```cpp ```cpp
// 消费侧 adapter/undostack.h —— app/other 模块里 connect() 零改动 // 消费侧 adapter/undostack.h —— app/other 模块里 connect() 零改动
// 通知规则(§4):适配类发了变更命令,它知道影响,信号由适配类自己 emit。
class UndoStack : public QObject { class UndoStack : public QObject {
Q_OBJECT Q_OBJECT
public: public:
explicit UndoStack(QObject *p = nullptr) explicit UndoStack(QObject *p = nullptr)
: QObject(p), h_(oakundo_undostack_init(p)) { : QObject(p), h_(oakundo_undostack_init(
sub_ = oakundo_undostack_subscribe(h_, OAKUNDO_EVENT_INDEX_CHANGED, reinterpret_cast<const OakUndoObjectParent *>(p))) {}
&UndoStack::tramp, this); ~UndoStack() override { oakundo_undostack_free(h_); }
}
~UndoStack() override { oakundo_unsubscribe(sub_); oakundo_undostack_free(h_); }
void push(UndoCommand *c, const QString &n) { void push(UndoCommand *c, const QString &n) {
oakundo_undostack_push(h_, c->handle(), n.toUtf8().constData()); 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; } bool canUndo() const { return oakundo_undostack_can_undo(h_) != 0; }
signals: signals:
void index_changed(int); void index_changed(int);
private: 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<UndoStack *>(ud)->index_changed(int(a));
}
OakUndoStack *h_; OakUndoStack *h_;
int64_t sub_;
}; };
``` ```
+7 -1
View File
@@ -23,6 +23,11 @@
已知分层违规 1 处:`render/` 引用了 `src/capi/displayinternal.h` 已知分层违规 1 处:`render/` 引用了 `src/capi/displayinternal.h`
R7-A 重做 display.h 时一并消除)。 R7-A 重做 display.h 时一并消除)。
> **2026-08 增补(oakstorage 拆出)**:矩阵扫描早于 oakstorage 单列。
> 其依赖关系为:oakstorage → nodeserializer 落盘路径,含版本化
> serializerXXXXXX 族)+ commonoaktask → oakstorageload/save/otio
> 委托);facade → oakstorage。详细契约见 04 §2.10 与 M10。
## 2. 模块定义与拆分顺序 ## 2. 模块定义与拆分顺序
顺序原则:叶子先、根后;每步只引入"已拆模块的 C ABI",不引入 顺序原则:叶子先、根后;每步只引入"已拆模块的 C ABI",不引入
@@ -34,11 +39,12 @@
| M1 | oakcommon | `common/`41 文件工具集)+ `config/` | common→render/node/undo/codec/plugin 的 12 次反向 include(清单见 M1 §3 | | 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 | | 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 4M3 §3,最大的活) | | M3 | oaknode | `node/`(图、工厂、keyframe、nodeundo、traverser | node→render 47、node→codec 8、node→timeline 5、node→audio 4、node→undo 4M3 §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 + 适配类) | | M4 | oaktimeline | `timeline/`marker/workarea/timeline undo 命令族/timelinecommon | timeline→node 32(经 oaknode C ABI + 适配类) |
| M5 | oakcodec | `codec/`decoder/encoder/frame/proxy/conform | codec→render 11videoparams 等随 M3.5 下沉)、codec→task 5、codec→node 3 | | M5 | oakcodec | `codec/`decoder/encoder/frame/proxy/conform | codec→render 11videoparams 等随 M3.5 下沉)、codec→task 5、codec→node 3 |
| M6 | oakaudio | `audio/`AudioManager/AudioProcessor/输出) | audio→render 2、audio→codec 1 | | 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(违规) | | 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 已划给 oakstorageM3a** | task→node 38、task→codec 4、task→render 4、task→storageload/save 委托) |
| M9 | oakplugin | `pluginSupport/`OpenFX host | plugin→node 6、plugin→render 6、plugin→undo 2、plugin→coreengine 2 | | 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 裁决) | | — | liboakengine | `src/capi` + `coreengine` + `tool/` + `ui/` 残余 | 纯装配层:facade 内部调用改经各模块 C ABI(或保持现状直接链,见 M9 §4 裁决) |
+4 -2
View File
@@ -29,8 +29,10 @@
4. **枚举序数一致性**:C 侧 POD/枚举与 C++ 侧枚举的映射(01 §3 表) 4. **枚举序数一致性**:C 侧 POD/枚举与 C++ 侧枚举的映射(01 §3 表)
每个映射 1 个 TEST(如 `oakundo` 的 movement mode 0-3 ⇄ 每个映射 1 个 TEST(如 `oakundo` 的 movement mode 0-3 ⇄
`Timeline::MovementMode`)。 `Timeline::MovementMode`)。
5. **事件/回调**:每个 `set_*_cb`/subscribe 至少 1 个 TEST:触发后 5. **回调(仅异步任务)**:模块间 C ABI 无 subscribe 类接口(04 §3);
断言回调被调、payload 正确;反注册后断言不再被调。 仅异步命令(任务/渲染 ticket)的进度/完成回调需要测试:触发后
断言回调被调、payload 正确、FINISHED 后自动失效。同步命令的测试
改为"调用后直接读状态断言生效"。
6. **所有权**:borrowed 句柄(文档注释标了 `/* borrowed */` 的) 6. **所有权**:borrowed 句柄(文档注释标了 `/* borrowed */` 的)
free 后原对象仍存活,1 个 TEST。 free 后原对象仍存活,1 个 TEST。
+147
View File
@@ -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/colortransformM3.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.h4 处) | — | marker/workarea2 处,M4 反向) | decoder/frame/proxy8 处,M5 反向) | audio 参数(4 处,M6 反向) | colorprocessor/rendermanager/jobM7 反向,02 §4 裁决 A | — | — | — | rational/bezier |
| **oakundo** | 工具 | — | — | — | — | — | — | — | — | — | — |
| **oakstorage** | 工具 | — | **project/root/序列化建图取图** | — | — | — | — | — | — | — | — |
| **oakcommon** | — | — | — | — | — | — | — | — | — | — | — |
(空格 = 无依赖。"N 处"数据来自 02 的 include 扫描。oakcore 与
ffmpeg_bridge 为现成独立库,不参与拆分顺序。)
## 2. 逐模块接口契约
### 2.1 oakcommonM1)— 纯下沉,无业务对象
- **提供**`include/oakcommon/types.h` 的全模块共用 POD(时间戳/区间/
枚举常量,含 M3.5 下沉的 `OakVideoParams`/`OakSubtitleParams`/
`OakColorTransform`);工具函数(全 `_s` 静态式,无句柄)。
- **消费**:无(叶子)。
- **边界数据**:纯 POD 值,无所有权问题。
### 2.2 oakundoM2)— 手工虚表的第一个用户
- **提供**`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 oaknodeM3)— 最大提供方
- **提供**`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 oaktimelineM4
- **提供**marker/workarea/timeline 编辑原语句柄族(`OakTimelineMarker`
等),timeline 专用 undo 命令**经 oakundo 的回调式命令**注册,不自带
命令子类。
- **消费**oaknode32 处,全部经句柄族)、oakundo、oakcommon。
### 2.5 oakcodecM5
- **提供**`OakCodecDecoder/Encoder/Frame/ProxyManager` 句柄族;
帧以 `OakCodecFrame *` 不透明句柄跨边界(owned,配对 free),
像素数据经 `oakcodec_frame_data(frame, plane, &linesize)` 取出指针
borrowed,生命周期随 frame)。
- **消费**oakcommon、oaknodefootage 流信息)、oakcore、ffmpeg_bridge。
### 2.6 oakaudioM6
- **提供**`OakAudioManager/Processor/Synchronizer` 句柄族;波形/电平
数据以 POD 数组 + count 出参。
- **消费**oakcoresamplebuffer)、oakcodec1 处)。
### 2.7 oakrenderM7
- **提供**`OakRenderRenderer/Ticket/Cache/ColorProcessor` 句柄族;
渲染结果帧为 owned 句柄;渲染 ticket 是**异步命令**(后台线程),
进度/完成回调是它的返回通道——这是 §3 允许回调的唯一情形
(线程语义按 riir.md §6.2 钉死)。
- **消费**oaknode、oakcodec、oakcommon、oakundo1 处)、oakbackend
GPU 插件,经 `renderbackend_c.h` 手工虚表——现有先例)。
### 2.8 oaktaskM8)— 编排者
- **提供**`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 oakpluginM9
- **提供**:OFX 插件加载/实例句柄族(`OakPluginHost/Instance`)。
- **消费**oaknode、oakrender、oakundo。
### 2.10 oakstorageM10,新拆)— 工程持久化
- **提供**`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<mod>_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<MOD>_E_*`(值与 oakengine 现有对齐);
细节经 `oak<mod>_last_error(buf, size)`(线程局部)。
- 所有权注释三档:`/* owned */`init/take 返回,必须配对 free)、
`/* borrowed */`(访问器返回,禁 free)、`/* retained */`register
类,配对 unregister)。句柄默认 owned。
- 每个模块提供 `oak<mod>_debug_alive_count()`(测试专用,钉死泄漏)。
+176
View File
@@ -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)。
> 依赖:oaknodeproject/root/序列化建图取图)、oakcommon。
> 被依赖:oaktaskload/save 任务委托)、facade。
> 拆分顺序:M3aoaknode 之后、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 保存到 URIsave / 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);
/* 会话来源 URIbuf/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 → URIoptions 透传 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_*` 族)取图/建图。剪贴板分支留 oaknodeM3/M3b |
| `task/project/load|save` 含文件 IO | IO 部分下沉 oakstorage;task 保留任务编排(进度、取消、事件) |
| `task/project/loadotio|saveotio` | 同上,注册为 "otio" 后端(scheme=filecan_handle 认 .otio |
| serializer 对 Qt 文件对话框/布局的引用 | 布局信息(SerializedLayoutInfo)随保存走 options 的不透明 blobbuf/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 配对无泄漏。
+14 -11
View File
@@ -40,7 +40,12 @@ OAKUNDO_API void oakundo_command_free(OakUndoCommand *cmd); /* NULL no-op */
```c ```c
typedef struct OakUndoStack OakUndoStack; 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);
/* OakUndoObjectParentQObject 父子树挂载点的 borrowed 不透明句柄
* 2026-08 修订:按 01 §0.1 由 void* 改为有类型句柄;
* 提供侧内部即 QObject*,消费侧不可解引用;可传 NULL 表无父) */
OAKUNDO_API void oakundo_undostack_free(OakUndoStack *self); OAKUNDO_API void oakundo_undostack_free(OakUndoStack *self);
OAKUNDO_API void oakundo_undostack_push(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 */ /* QAction* 句柄(GUI 菜单绑定用,borrowed */
OAKUNDO_API void *oakundo_undostack_undo_action(OakUndoStack *self); OAKUNDO_API void *oakundo_undostack_undo_action(OakUndoStack *self);
OAKUNDO_API void *oakundo_undostack_redo_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 处) ## 3. 切割点(1 处)
`undocommand.cpp` include `node/project.h``get_relevant_project()` `undocommand.cpp` include `node/project.h``get_relevant_project()`
@@ -96,7 +98,8 @@ Project* 本来就只是作为不透明身份被使用(修改标记归属)
- 每条 API 正常+错误路径(NULL self、空 push 不入栈—— - 每条 API 正常+错误路径(NULL self、空 push 不入栈——
空 MultiUndoCommand 被删除的既有行为必须有 TEST 钉死)。 空 MultiUndoCommand 被删除的既有行为必须有 TEST 钉死)。
- push/undo/redo/jump/clear 全序列;command_text/is_done 边界行 - push/undo/redo/jump/clear 全序列;command_text/is_done 边界行
- 事件:push 后 index_changed 触发且 a=新 indexunsubscribe 后不再触发。 每步后 `oakundo_undostack_index` 读数与预期一致(替代原事件断言——
调用方知道影响,直接读状态)。
- 往返测试:C API 与适配类各做一遍 push-undo-redo,状态一致。 - 往返测试:C API 与适配类各做一遍 push-undo-redo,状态一致。
- `oakundo_debug_alive_count()`init/free 配对无泄漏。 - `oakundo_debug_alive_count()`init/free 配对无泄漏。
+11 -9
View File
@@ -40,16 +40,18 @@ project.h、timeline.h 的对应函数就是模板,参数命名前缀换
| Track / TrackList | timeline | height/mute/lock/index/type、block 增删、split/ripple 原语 | | 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 | | Block / ClipBlock / GapBlock / TransitionBlock | timeline | in/out/length/media_in、speed/reverse/loop、links |
| Footage | task | filename、streams、proxy、duration | | Footage | task | filename、streams、proxy、duration |
| ProjectSerializer | task | save/load/copy/pasteclipboard 族,照 oakengine/serializer.h 模板) | | ProjectSerializer | task / oakstorage | **剪贴板** copy/paste + 节点图 XML 的内存形态(SaveData/LoadData,照 oakengine/serializer.h 模板);**落盘 save/load 迁 oakstorageM10),不在本模块** |
| ColorManager | render | config、default config、display transform | | ColorManager | render | config、default config、display transform |
**特殊约定** **特殊约定**
1. undoable 变体与 live 变体成对(`_live` 后缀或 `, void *command` 1. undoable 变体与 live 变体成对(`_live` 后缀或
尾参),与 oakengine 现状一致。 `, OakUndoCommand *command` 尾参——有类型句柄,禁 void*),
2. 事件:Node 族事件(label/input/keyframe/context 等)经 与 oakengine 现状一致。
`oaknode_subscribe(handle, event_id, fn, userdata)`——事件 ID 表 2. 无事件订阅接口(2026-08 修订,04 §3):oaknode 的所有修改都经
直接沿用 `oakengine/events.h` 的 70-95 段(值不变,便于 命令函数完成,调用方知道影响;Node 族的变更通知(label/input/
EngineEventBridge 逐步换绑)。 keyframe/context 等)由 facade 在执行命令后经既有 `oakengine_event`
通道发出(事件 id 沿用 `oakengine/events.h` 70-95 段,值不变),
oaknode 自身不持有任何上层回调。
3. `Node *``Project *` 等句柄即 `OakNodeNode *`/`OakNodeProject *` 3. `Node *``Project *` 等句柄即 `OakNodeNode *`/`OakNodeProject *`
不透明。 不透明。
4. 虚函数不出模块(01 §5);具体节点类型经 4. 虚函数不出模块(01 §5);具体节点类型经
@@ -73,8 +75,8 @@ M3 阶段判据(放宽版):oaknode 目录就位、C API 实现、oaknode_g
- 重点:Node 增删连边、Project/Folder 层级、Track 属性、 - 重点:Node 增删连边、Project/Folder 层级、Track 属性、
keyframe live/undoable 对称、serializer 剪贴板往返、 keyframe live/undoable 对称、serializer 剪贴板往返、
factory 枚举与创建 factory 枚举与创建;每个变更命令执行后直接读状态断言生效
- 事件:每族至少 1 个 subscribe/trigger/unsubscribe 用例 (无事件可断言——通知在 facade 层测)
- 枚举序数:NodeValue::Type ⇄ oak_node_value_type 映射表(已在 - 枚举序数:NodeValue::Type ⇄ oak_node_value_type 映射表(已在
nodevaluehandle.h 钉过一次,oaknode 测试再钉一次,防两侧漂移)。 nodevaluehandle.h 钉过一次,oaknode 测试再钉一次,防两侧漂移)。
- `oaknode_debug_alive_count()` 泄漏断言。 - `oaknode_debug_alive_count()` 泄漏断言。
+11 -11
View File
@@ -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); int64_t *in_ts, int64_t *out_ts, int *color, char *name_buf, int n);
OAKTL_API int oaktimeline_marker_add(OakTimelineMarkerList *l, OAKTL_API int oaktimeline_marker_add(OakTimelineMarkerList *l,
int64_t in_ts, int64_t out_ts, const char *name, int color, int64_t in_ts, int64_t out_ts, const char *name, int color,
void *command); /* command=NULL 时自行入栈 */ OakUndoCommand *command); /* command=NULL 时自行入栈2026-08void* → 有类型句柄) */
OAKTL_API int oaktimeline_marker_remove_at(OakTimelineMarkerList *l, 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, 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, OAKTL_API int oaktimeline_marker_set_props(OakTimelineMarkerList *l,
int i, int color, const char *name, void *command); int i, int color, const char *name, OakUndoCommand *command);
/* 事件MARKER_ADDED/REMOVED/MODIFIEDid 沿用 oakengine events 段) */ /* 事件接口(2026-08 修订,04 §3):marker 的增删改都是调用方发的
OAKTL_API int64_t oaktimeline_subscribe(void *handle, int32_t event_id, * 命令,MARKER_ADDED/REMOVED/MODIFIED 通知由调用方所在层(facade,
oaktl_event_fn fn, void *userdata); * id 沿用 oakengine events 段)在命令后发出。 */
OAKTL_API void oaktimeline_unsubscribe(int64_t id);
``` ```
### 2.2 `oaktimeline/workarea.h` ### 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 */ int64_t in_ts, int64_t out_ts); /* live */
OAKTL_API int oaktimeline_workarea_set_range_undoable( OAKTL_API int oaktimeline_workarea_set_range_undoable(
OakTimelineWorkarea *w, int64_t in_ts, int64_t out_ts, 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( 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); OAKTL_API void oaktimeline_workarea_reset(int64_t *in_ts, int64_t *out_ts);
/* load/save 经 oakcommon_xml 句柄在 oaknode 序列化路径调用 */ /* load/save 经 oakcommon_xml 句柄在 oaknode 序列化路径调用 */
OAKTL_API int oaktimeline_workarea_load(OakTimelineWorkarea *w, 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 ## 4. 测试(映射 03 §2/§3
- marker:增删改查、undo 往返(push 后 undo 恢复)、事件三件套。 - marker:增删改查、undo 往返(push 后 undo 恢复);每命令后读
count/at 断言生效(无事件——通知在 facade 层测)。
- workareaset/get、undoable 旧值恢复、reset 哨兵、xml 往返。 - workareaset/get、undoable 旧值恢复、reset 哨兵、xml 往返。
- edit:每个 `_command` 工厂 1 个"构造→入栈→undo 还原"用例 - edit:每个 `_command` 工厂 1 个"构造→入栈→undo 还原"用例
track 增删、place/replace/trim/split/ripple/slide)。 track 增删、place/replace/trim/split/ripple/slide)。
+3 -5
View File
@@ -45,10 +45,8 @@ OAKAU_API int oakaudio_manager_push(const float *samples,
int frame_count, double speed); int frame_count, double speed);
OAKAU_API int oakaudio_manager_is_playing(void); OAKAU_API int oakaudio_manager_is_playing(void);
OAKAU_API void oakaudio_manager_stop(void); OAKAU_API void oakaudio_manager_stop(void);
/* 输出参数变化事件 */ /* 无事件接口(2026-08 修订,04 §3):输出参数的修改是调用方发的
OAKAU_API int64_t oakaudio_manager_subscribe_params_changed( * 命令(set_params),params_changed 通知由调用方所在层发出。 */
oakaudio_event_fn fn, void *userdata);
OAKAU_API void oakaudio_unsubscribe(int64_t id);
``` ```
## 3. 切割点 ## 3. 切割点
@@ -63,5 +61,5 @@ OAKAU_API void oakaudio_unsubscribe(int64_t id);
- processoropen/convert/close 全链(44.1k stereo → 48k stereo - processoropen/convert/close 全链(44.1k stereo → 48k stereo
帧数换算正确、无爆音断言用能量差阈值)、速度 1.5x。 帧数换算正确、无爆音断言用能量差阈值)、速度 1.5x。
- manager:无音频设备环境用 null backend 初始化(现有后端探测 - manager:无音频设备环境用 null backend 初始化(现有后端探测
模式),params set/get 往返、事件触发 模式),params set/get 往返(set 后 get 读数即生效——无事件)
- `oakaudio_debug_alive_count()` 泄漏断言。 - `oakaudio_debug_alive_count()` 泄漏断言。
+6 -4
View File
@@ -51,9 +51,10 @@ OAKRD_API int oakrender_frame_cache_load(OakRenderCache *c,
OakCodecFrame **out_frame); OakCodecFrame **out_frame);
OAKRD_API void oakrender_frame_cache_save(OakRenderCache *c, OAKRD_API void oakrender_frame_cache_save(OakRenderCache *c,
const char *path, const char *uuid, const OakCodecFrame *f); const char *path, const char *uuid, const OakCodecFrame *f);
/* 缓存事件(playback invalidated/validated、frame invalidated */ /* 缓存事件(2026-08 修订,04 §3):缓存的 invalidate/validate
OAKRD_API int64_t oakrender_cache_subscribe(OakRenderCache *c, * 编辑命令的调用方触发并知情,通知由 facade 在命令后发出;
int32_t event_id, oakrender_event_fn fn, void *userdata); * oakrender 不持有上层回调。渲染 ticket 的完成回调属异步命令
* 返回通道,不在此限(见 renderer.h 族)。 */
``` ```
### 2.3 `oakrender/color.h` ### 2.3 `oakrender/color.h`
@@ -107,7 +108,8 @@ OAKRD_API int oakrender_disk_cache_clear(void);
## 4. 测试(映射 03 §2/§3 ## 4. 测试(映射 03 §2/§3
- cacheinvalidate/validate 状态机、帧缓存存取往返、事件触发 - cacheinvalidate/validate 状态机、帧缓存存取往返(调用方触发
读状态断言——无事件)。
- color:默认 config 建置、processor convert 已知值(sRGB→Linear - color:默认 config 建置、processor convert 已知值(sRGB→Linear
抽样点数值断言,容差 1e-3)。 抽样点数值断言,容差 1e-3)。
- managerrequest_frame 对 demo.mp4 + 最小 sequence 出帧非空 - managerrequest_frame 对 demo.mp4 + 最小 sequence 出帧非空
+16 -11
View File
@@ -1,7 +1,8 @@
# M8 · oaktask 拆分手册 # M8 · oaktask 拆分手册
> 内容:`engine/task/`Task 基类、TaskManager、project/ > 内容:`engine/task/`Task 基类、TaskManager、任务编排、cache 任务;
> load/save/import/loadotio/saveotio、cache 任务)。 > **工程文件 IO——project/load/save/loadotio/saveotio 的落盘部分——
> 已划给 oakstorage(M10),本模块只做任务壳**)。
> 依赖:node 38、codec 4、render 4、common 4、config 2、timeline 1、 > 依赖:node 38、codec 4、render 4、common 4、config 2、timeline 1、
> coreengine 2。 > coreengine 2。
> 拆分顺序第 8 位。 > 拆分顺序第 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_progress(const OakTaskTask *t, double *out);
OAKTK_API int oaktask_task_title(OakTaskTask *t, char *buf, int n); OAKTK_API int oaktask_task_title(OakTaskTask *t, char *buf, int n);
OAKTK_API int oaktask_task_error(OakTaskTask *t, char *buf, int n); OAKTK_API int oaktask_task_error(OakTaskTask *t, char *buf, int n);
/* 事件:STARTED/PROGRESS/FINISHEDid 沿用 oakengine events 段) */ /* 进度/完成回调:任务是异步命令,回调即其返回通道——04 §3 唯一
* 例外情形(一次性语义,FINISHED 后自动失效) */
OAKTK_API int64_t oaktask_task_subscribe(OakTaskTask *t, int32_t event_id, OAKTK_API int64_t oaktask_task_subscribe(OakTaskTask *t, int32_t event_id,
oaktask_event_fn fn, void *userdata); 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, OAKTK_API OakTaskTask *oaktask_create_project_save(OakNodeProject *p,
const char *filename_or_NULL, int use_compression, const char *filename_or_NULL, int use_compression,
const void *layout_or_NULL); const void *layout_or_NULL);
/* 注:以上两个工厂是薄壳——文件 IO 全部委托 oakstorageM10 §2.4),
* 任务只保留进度/取消/事件编排;`use_compression` 等打包进
* oakstorage_save 的 options 位掩码。 */
OAKTK_API OakTaskTask *oaktask_create_project_import(OakNodeNode *folder, OAKTK_API OakTaskTask *oaktask_create_project_import(OakNodeNode *folder,
const char *const *urls, int url_count); const char *const *urls, int url_count);
OAKTK_API OakTaskTask *oaktask_create_project_load_otio( 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, OakNodeProject *p, const char *filename, const int *sequence_indexes,
int count); int count);
/* import 结果(task 成功后读,borrowed */ /* 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 int oaktask_import_footage_count(OakTaskTask *t);
OAKTK_API OakNodeNode *oaktask_import_footage_at(OakTaskTask *t, int i); 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_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 int oaktask_manager_count(void);
OAKTK_API OakTaskTask *oaktask_manager_at(int i); /* borrowed */ OAKTK_API OakTaskTask *oaktask_manager_at(int i); /* borrowed */
OAKTK_API void oaktask_manager_delete_finished(void); OAKTK_API void oaktask_manager_delete_finished(void);
/* 事件:TASK_ADDED/REMOVED/FAILED/LIST_CHANGED */ /* 无 manager 事件(2026-08 修订,04 §3):任务的创建/删除都由调用方
OAKTK_API int64_t oaktask_manager_subscribe(int32_t event_id, * 发起(facade 建任务即知 ADDEDdelete_finished 的调用方知 REMOVED),
oaktask_event_fn fn, void *userdata); * LIST_CHANGED 通知由调用方所在层发出。 */
OAKTK_API void oaktask_unsubscribe(int64_t id);
``` ```
## 3. 切割点 ## 3. 切割点
@@ -77,6 +81,7 @@ OAKTK_API void oaktask_unsubscribe(int64_t id);
| 现状 | 处理 | | 现状 | 处理 |
|---|---| |---|---|
| task → node/ 38footage 6、project 5、sequence 3、serializer/layout 3、colormanager 2 等) | 全部经 oaknode C ABI + 适配类(M3 已就位)——本手册工作量主体 | | task → node/ 38footage 6、project 5、sequence 3、serializer/layout 3、colormanager 2 等) | 全部经 oaknode C ABI + 适配类(M3 已就位)——本手册工作量主体 |
| task → 工程文件 IOload/save/loadotio/saveotio 落盘) | 划给 **oakstorage**(M3a/M10):任务工厂保留签名,实现改为委托 `oakstorage_open/save` |
| task → codec/ 4 | 经 oakcodec C ABIM5 | | task → codec/ 4 | 经 oakcodec C ABIM5 |
| task → render/ 4 | 经 oakrender C ABIM7 | | task → render/ 4 | 经 oakrender C ABIM7 |
| task → timeline/ 1 | 经 oaktimeline C ABIM4 | | task → timeline/ 1 | 经 oaktimeline C ABIM4 |
@@ -88,7 +93,7 @@ OAKTK_API void oaktask_unsubscribe(int64_t id);
入栈)、save/load 往返(临时目录 .oveload 后 project 非空、 入栈)、save/load 往返(临时目录 .oveload 后 project 非空、
root 非空)。 root 非空)。
- start_sync 成功/失败路径(不存在文件 → failed + error 非空)。 - start_sync 成功/失败路径(不存在文件 → failed + error 非空)。
- 事件:STARTED→PROGRESS→FINISHED 序列(导入任务断言至少一次 - 事件(异步任务例外)STARTED→PROGRESS→FINISHED 序列(导入任务
PROGRESS 且 FINISHED.succeeded==1)。 断言至少一次 PROGRESS 且 FINISHED.succeeded==1)。
- manager添加/删除/list_changed 事件 - managercount/at/delete_finished 行为(无事件,直接读状态断言)
- `oaktask_debug_alive_count()` 泄漏断言。 - `oaktask_debug_alive_count()` 泄漏断言。
+3 -1
View File
@@ -43,7 +43,9 @@ OAKPL_API int oakplugin_instance_get_param(OakPluginInstance *i,
const char *param_id, oak_node_value *out); const char *param_id, oak_node_value *out);
OAKPL_API int oakplugin_instance_render(OakPluginInstance *i, OAKPL_API int oakplugin_instance_render(OakPluginInstance *i,
OakCodecFrame *dst, const OakCodecFrame *src, int64_t ts); 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, OAKPL_API void oakplugin_instance_set_progress_cb(OakPluginInstance *i,
oakplugin_progress_fn fn, void *userdata); oakplugin_progress_fn fn, void *userdata);
OAKPL_API void oakplugin_instance_cancel(OakPluginInstance *i); OAKPL_API void oakplugin_instance_cancel(OakPluginInstance *i);